导图社区 java多线程
java多线程,详细概述了使用线程,基础线程机制,中断,互斥同步,线程之间的协作,线程状态,线程安全,锁优化的内容。
编辑于2022-04-24 16:37:06java多线程
一、使用线程
实现 Runnable 接口
实现 Callable 接口
与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装
继承 Thread 类
实现接口 VS 继承 Thread
Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口; 类可能只要求可执行就行,继承整个 Thread 类开销过大
二、基础线程机制
Executor
JDK给我们提供了Excutor框架来使用线程池,它是线程池的基础
Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期
Executor框架成员
ThreadPoolExecutor实现类
ThreadPoolExecutor是Executor接口的一个重要的实现类,是线程池的具体实现,用来执行被提交的任务
子类继承ThreadPoolExecutor,并且其中的一些参数已经被配置好
FixedThreadPool
FixedThreadPool是线程数量固定的线程池,适用于为了满足资源管理的需求,而需要适当限制当前线程数量的情景,适用于负载比较重的服务器
SingleThreadExecutor
SingleThreadExecutor是只有一个线程的线程池,常用于需要让线程顺序执行,并且在任意时间,只能有一个任务被执行,而不能有多个线程同时执行的场景
CachedThreadPool
CachedThreadPool适用于执行很多短期异步任务的小程序,或者是负载较轻的服务器
ScheduledThreadPoolExecutor实现类
继承了ThreadPoolExecutor并实现了ScheduledExecutorService接口。主要用于在给定的延迟后执行任务或者定期执行任务
ScheduledThreadPoolExecutor
适用于若干个(固定)线程延时或者定期执行任务,同时为了满足资源管理的需求而需要限制后台线程数量的场景
SingleThreadScheduledExecutor
适用于需要单个线程延时或者定期的执行任务,同时需要保证各个任务顺序执行的应用场景
ForkJoinPool线程池
ForkJoinPool 主要用于实现“分而治之”的算法,最适合的是计算密集型的任务
commonPool是ForkJoinPool内置的一个线程池对象
创建了ForkJoinPool实例之后,就可以调用ForkJoinPool的submit(ForkJoinTask task) 或invoke(ForkJoinTask task)方法来执行指定任务了
ForkJoinTask是一个抽象类,它还有两个抽象子类:RecusiveAction和RecusiveTask
RecusiveTask代表有返回值的任务
RecusiveAction代表没有返回值的任务
Future接口
Future接口和其唯一的实现类FutureTask类一般用于表示异步计算的结果
用FutureTask包装Runnable或者Callable对象 交给线程池的Execute或submit方法执行
Runnable和Callable接口
用于实现线程要执行的工作单元
Executors工厂类
提供了常见配置线程池的方法,因为ThreadPoolExecutor的参数众多且意义重大,为了避免配置出错,才有了Executors工厂类
Daemon
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。 当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程
在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程
sleep()
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒
yield()
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行
三、中断
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束
InterruptedException
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞
interrupted()
如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。 但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程
Executor 的中断操作
调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法
如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程
四、互斥同步
synchronized
1. 同步一个代码块
它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步
2. 同步一个方法
它和同步代码块一样,作用于同一个对象
3. 同步一个类
作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步
4. 同步一个静态方法
作用于整个类
ReentrantLock
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁
比较
1. 锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的
2. 性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同
3. 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。 ReentrantLock 可中断,而 synchronized 不行
4. 公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。 synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的
5. 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象
使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放
五、线程之间的协作
当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调
join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束
wait() notify() notifyAll()
调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。 它们都属于 Object 的一部分,而不属于 Thread。 只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException。 使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁
wait() 和 sleep() 的区别
wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法; wait() 会释放锁,sleep() 不会。
await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。 相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。 使用 Lock 来获取一个 Condition 对象
六、线程状态
一个线程只能处于一种状态,并且这里的线程状态特指 Java 虚拟机的线程状态,不能反映线程在特定操作系统下的状态
新建(NEW)
创建后尚未启动
可运行(RUNABLE)
正在 Java 虚拟机中运行。但是在操作系统层面,它可能处于运行状态,也可能等待资源调度(例如处理器资源),资源调度完成就进入运行状态。所以该状态的可运行是指可以被运行,具体有没有运行要看底层操作系统的资源调度
阻塞(BLOCKED)
请求获取 monitor lock 从而进入 synchronized 函数或者代码块,但是其它线程已经占用了该 monitor lock,所以出于阻塞状态。要结束该状态进入从而 RUNABLE 需要其他线程释放 monitor lock
无限期等待(WAITING)
等待其它线程显式地唤醒
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取 monitor lock。而等待是主动的,通过调用 Object.wait() 等方法进入
限期等待(TIMED_WAITING)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒
死亡(TERMINATED)
可以是线程结束任务之后自己结束,或者产生了异常而结束
七、J.U.C - AQS
java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心
CountDownLatch
CountDownLatch是一个同步的辅助类,允许一个或多个线程一直等待,直到其它线程完成它们的操作
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒
CyclicBarrier
CyclicBarrier注重的是:当线程到达某个状态后,暂停下来等待其他线程,所有线程均到达以后,继续执行
Semaphore
Semaphore(信号量)实际上就是可以控制同时访问的线程个数,它维护了一组"许可证"
当调用acquire()方法时,会消费一个许可证。如果没有许可证了,会阻塞起来 当调用release()方法时,会添加一个许可证。 这些"许可证"的个数其实就是一个count变量罢了
八、J.U.C - 其它组件
FutureTask
FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果
BlockingQueue
提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置
ForkJoin
主要用于并行计算中,和 MapReduce 原理类似,都是把大的计算任务拆分成多个小任务并行计算
九、线程不安全示例
十、Java 内存模型
主内存与工作内存
内存间交互操作
内存模型三大特性
先行发生原则
十一、线程安全
不可变
互斥同步
非阻塞同步
无同步方案
十二、锁优化
自旋锁
锁消除
锁粗化
轻量级锁
偏向锁