Java学习(十)
多线程
-
线程的创建和启动
三种方式:
-
继承
Thread
类创建线程类(自定义线程类)重写
run()
方法,调用线程类对象的start()
方法来启动Thread.currentThread()
返回当前正在执行的线程对象getName()
实例方法返回调用该方法的线程名,默认是Thread-n,可以用setName(String name)
为线程设置名字但是如果不是这种方式实现的线程,就必须用
Thread.currentThread().getName()
来得到名字了 -
实现Runnable接口创建线程类
Runnable对象仅仅作为Thread对象的target,其内部的
run()
方法仅作为线程执行体,实际的线程对象仍然是Thread
实例,但是这个target对象可以被多个线程共享 -
Callable和Future接口
Callable
接口的call()
方法可以作为线程的执行体,具有返回值用于让任意方法作为线程的执行体,但由于该接口不是Runnable
的子接口,故不能直接作为targetFuture
接口提供的FutureTask
实现类同时实现了Future
接口和Runnable
接口提供了取消Callable任务、获取call()的返回值、暂停任务等方法
创建并启动有返回值的线程:
- 创建
Callable
接口的实现类(函数式接口),实现call()
方法(有返回值) - 使用
FutureTask
类包装Callable对象 - 将
FutureTask
对象作为Thread
对象的target - 通过
FutureTask
对象的get()
取得返回值
- 创建
-
-
线程生命周期
-
新建和就绪态
调用
start()
方法后,线程处于就绪态,此时JVM为其创建方法调用栈和程序计数器,如果想要让其立即运行,可以让当前运行的线程使用Thread.sleep(1)
,进入阻塞态,则JVM会让新线程进入运行态 -
运行和阻塞态
yield()
方法可以让一个线程从运行态进入就绪态 -
线程死亡
- 正常结束:
run()
或call()
- 抛出异常或错误
- 调用该线程的
stop()
方法,易死锁
死亡状态的线程无法再start()
- 正常结束:
-
-
控制线程
Thread类提供的
join()
实例方法,使得调用该实例方法的主线程程序体必须等待调用该方法的线程执行完毕后才能继续运行。 -
后台线程
特性:所有前台线程死亡,后台线程会自动死亡;前台线程创建的线程默认是前台线程,同理,后台默认是后台,主线程是前台线程。
可以在线程
start()
之前,使用相应对象的setDaemon(true)
来设置其为后台线程。 -
线程睡眠
即
sleep()
方法,进入阻塞态 -
线程让步
即
yield()
方法,线程进入就绪态 -
改变线程优先级
Thread类提供了实例方法
setPriority(int newPriority)
来设置优先级,范围是1~10,并且有如下三个静态常量MAX_PRIORITY
:10MIN_PRIORITY
:1NORM_PRIORITY
:5但不同OS对优先级的支持不同,所以一般使用这三个。
还提供了
getPriority()
方法来获取优先级 -
线程同步
-
同步监视器
Java引入同步监视器来解决线程同步的问题,通用方法是使用同步代码块,格式如下:
synchronized(obj)/*obj就是同步监视器,一般使用要共享的资源作为同步监视器*/ { ...... }
任何时候都只能有一个线程获得同步监视器的锁定
-
同步方法
用
synchronized
修饰实例方法即可(不能是static
),监视器是this
,即整个对象 -
释放同步监视器的锁定
释放:
- 代码执行结束
- 代码遇到错误或异常
- 调用了同步监视器对象的
wait()
方法
不释放:
- 调用了
sleep()
或yield()
方法 - 调用了
suspend()
挂起线程
-
同步锁
每次只能有一个线程对Lock对象加锁,需要在代码的相应位置手动加锁/释放锁;
常用的是可重入锁
ReentrantLock
Extra:
synchronized
只能修饰实例方法和代码块,不能修饰构造器和成员变量。- 如果运行环境可变,有单/多线程两种环境,那么应该提供两种方案——线程安全和线程不安全,保证性能和可靠性
-
-
线程通信
传统通信,Object类的三个方法,必须由同步监视器对象调用
-
wait()
:当前线程等待,直到其他线程调用notify()
或notifyAll()
来唤醒;或者用带参数的wait自己醒来notify()
:唤醒在该监视器上等待的单个线程,多个等待则选择任意的一个,只有当前线程放弃锁定时才可执行被唤醒的线程。notifyAll()
:唤醒所有线程,只有当前线程放弃锁定时才可执行被唤醒的线程。 -
如果不存在隐式的同步监视器对象,即用了
Lock
对象,就使用Condition
类来协调通信Condition
对象被绑定在Lock
对象上,使用newCondition()
可以得到特定的Lock
对象对应的Condition
实例await()
:类似wait()
使当前线程等待signal()
:唤醒单个线程,类似notify()
signalAll()
:唤醒在此Lock
对象上等待的所有线程,类似notifyAll()
-
阻塞队列控制线程通信
BlockingQueue
接口为Queue
接口的子接口:生产者线程想往队列放入元素时,如果队列已满则该线程被阻塞。提供了两个支持阻塞的方法:
put(E e)
:放入该队列,若已满则阻塞该线程take()
:从队列头部取出元素,若为空则阻塞该线程
此外还有
ArrayBlockQueue
等支持线程阻塞的队列
-
-
线程组
Java采用
ThreadGroup
表示线程组。特点:
- 如果没有显式指定线程组,那么则属于默认线程组;子线程默认属于父线程所在的线程组。
- 线程加入指定线程组后,直到线程死亡都不能改变其所属线程组
Thread
类提供了带有ThreadGroup
参数的构造器可以通过调用一个
Thread
对象的getThreadGroup()
方法来返回其所属的线程组。线程组有两个构造器:
ThreadGroup(String name)
ThreadGroup(ThreadGroup parent, String name)
:指定名字的同时,指定父线程组 -
线程池
线程池适合程序需要创建大量生存期很短的线程。
特点:
当系统启动时,线程池会创建大量空闲的线程,把一个Runnable对象或Callable对象传给线程池时,线程池就会启动一个线程来执行它们的
run()
或者call()
方法,执行完毕后线程不死亡,而是返回线程池成为空闲状态,等待执行下一个Runnable
对象的run()
或者call()
方法创建线程池:
-
Executors
工厂类(提供静态方法)ExecutorService newCachedThreadPool()
:具有缓存功能的线程池,根据需要创建并缓存ExecutorService newFixedThreadPool(int nThreads)
:创建固定数量线程的线程池ExecutorService newSingleThreadExecutor()
:创建单线程的线程池ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
:指定线程数,并在指定延迟后执行线程任务ScheduledExecutorService newSingleThreadScheduledExecutor()
:只有一个线程的线程池,在指定延迟后执行线程任务ExecutorService newWorkStealingPool(int parallelism)
:创建持有足够的线程的线程池来支持给定的并行级别,还会使用多队列来减少竞争。ExecutorService newWorkStealingPool()
:简化版本,其值和CPU数量相等。其中的
ExecutorService
对象代表线程池,可以执行Runnable
或Callable
对象代表的线程上述中间两个方法的
ScheduledExecutorService
是前者子类,在指定延迟后执行任务。最后两个方法生成的是后台线程池
ExecutorService
对象提供了如下三个方法:Future<?> submit(Runnable task)
:有空闲线程时执行任务,Future
对象会接受返回值,如果是run()
方法则返回null
<T> Future<T> submit(Runnable task, T result)
:有空闲线程时执行任务,result
显式指定返回值,该值是由Future
对象返回的<T> Future<T> submit(Callable<T> task)
:有空闲线程时执行任务,Future
接受call()
的返回值shutdown()
:关闭线程池
-
ForkJoinPool
是
ExecutorService
的实现类,他利用CPU的多核优势,将一个任务拆分成多个小任务进行并行计算,再把多个小任务的结果合并起来。两种构造器:
-
ForkJoinPool(int parallelism)
:指定并行程度 -
ForkJoinPool()
:以Runtime.getRuntime().availableProcessors()
的返回值作为parallelism
执行任务
submit(ForkJoinTask task)
或invoke(ForkJoinTask task)
ForkJoinTask
代表可并行任务,为抽象类,有两个抽象子类RecursiveAction
(无返回值)和RecursiveTask
(有返回值,使用时继承的是RecursiveTask<T>
,T即为返回值类型)分解:需要手动定义分解的任务,且需要调用
fork()
函数。合并:返回值需要调用子任务的
join()
函数,其结果需要自定义 -
-
-
线程相关类
-
ThreadLocal类
为每个使用
ThreadLocal<T>
对象的线程提供一个该变量值的副本,每个线程可以独立改变自己的副本。可以理解为:该变量对于不同的线程来讲是独立的,即每个线程都有自己的一个
ThreadLocal
。示例:
class Account { private ThreadLocal<String> name = new ThreadLocal<>(); public Account(String str) { this.name.set(str); System.out.println("===" + this.name.get()); } public String getName() { return name.get(); } public void setName(String str) { this.name.set(str); } } class Mytest extends Thread { private Account account; public Mytest(Account account, String name) { super(name); this.account = account; } public void run() { for (int i = 0;i < 10;i++) { if (i == 6) { account.setName(getName()); } System.out.println(account.getName() + ' ' + i); } } } public class test { public static void main(String[] args) throws InterruptedException { Account at = new Account("init"); new Mytest(at, "t1").start();/*对于t1和t2来说,at中,name的值是各自私有的*/ new Mytest(at, "t2").start(); } }
提供三个
public
实例方法T get()
:返回副本值void remove()
:删除值void set(T value)
:设置值通过上述方式,可以保证共享对象的安全,如果其他线程只是写不读取(不通信)共享对象,那么完全可以用
ThreadLocal
替代锁或者同步,来提高性能。 -
包装线程不安全的集合为线程安全的集合
使用
Collections
提供的静态方法即可 -
线程安全的集合类
两种:
-
Concurrent开头的集合类
支持多线程并发访问,使用迭代器遍历时,可能无法反映出创建迭代器之后元素的变化
-
CopyOnWrite开头的集合类
底层封装了
CopyOnWriteArrayList
,会完全复制原始数组,只是读取的话性能很赚,如果频繁写入就很浪费
-
-