文章目录
一、相关概念
1、同步和异步
同步:线程 A 调用一个方法,必须等到方法调用返回后,线程A才能继续运行
异步:线程 A 调用一个方法,然后继续执行;而这个方法一般会在另一个线程 B 中执行。整个过程中,这个方法不会阻碍线程 A 的工作
2、并发和并行
并行:多个任务同时
执行
并发:侧重于多个任务交替
执行,还有可能串行
对于系统而言,有单核和多核的区别,这两种情况下的多线程情况是不一样的
- 单核多线程:只能并发,不能并行
- 多喝多线程:可以并发,可以并行
3、临界区
临界区:表示公共资源或者共享数据,可被多个线程使用
它有如下特点:
- 每次只能有一个线程使用临界区
- 一旦临界区被占用,其它想使用这个临界区的线程就必须等待
在并行程序中,临界区资源是重点保护的对象
4、阻塞和非阻塞
阻塞和非阻塞用来形容多线程间的相互影响
阻塞:若一个线程占用了临界区资源,那么其它想使用这个临界区资源的所有线程必须在这个临界区中等待,而等待会导致线程挂起,这就是阻塞
非阻塞:强调没有线程可以妨碍其它线程执行,所有线程都会向前执行
5、死锁、饥饿和活锁
死锁: 不同线程彼此相互占用了其它线程所需的资源,但都不愿意释放自己持有的资源。
饥饿: 指一个或多个线程因为某些原因无法获得所需要的资源,导致一直无法执行。
饥饿发生的原因:
- 自身线程优先级太低,高优先级线程不断抢占它所需要的资源
- 某一个线程一直占着关键资源不放,导致需要此资源的其它所有线程无法正常执行
饥饿和死锁的区别:
- 死锁无法解决
- 而饥饿在未来一段时间内有可能解决。例如:高优先级线程执行完成,不会在抢占低优先级线程所需的资源
**活锁:**不同的线程总是主动将资源释放给其它线程使用,导致资源虽在不同线程之间跳动,但是没有线程同时拿到所有资源并执行
二、并发级别
1、阻塞
一个线程A是阻塞的,那么在其它线程释放A所需的资源前,线程A无法继续执行
2、无饥饿
如果线程有优先级,那么线程调度的时候总是倾向于先满足高优先级的线程
非公平锁:允许高优先级线程插队,但有可能导致低优先级线程产生饥饿
公平锁:按照先来先服务的原则,不会产生饥饿现象
3、无障碍
无障碍: 无障碍是一种最弱的非阻塞调度。两个线程无障碍地执行,不会因为临界区的问题导致某一方被挂起(都可以大摇大摆的进去临界区,可以一起修改数据)
如果把数据改坏了怎么办: 对于无障碍地线程来说,一旦检测到这种情况,就会对自己所做地修改进行回滚,确保数据安全
阻塞和非阻塞的比较
- 阻塞:是一种悲观策略。系统认为多个线程之间很大概率会发生冲突,因此以保护数据为第一优先级
- 非阻塞:是种乐观策略。系统认为多个线程之间很小概率会发生冲突,因此线程先无障碍地执行,若检测到发生了冲突,立即回滚
4、无锁
- 无锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区进行访问,但无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区
- 典型特点:包含一个无穷的循环。在循环中,线程会不断尝试修改共享变量,如果没有冲突,那么修改成功,线程退出,否则继续尝试。无论如何,无锁的并行总能保证有一个线程是可以执行完的
请看如下代码,如果修改不成功,那么循环就不会停止
while ( ! atomicVar.compareAndSet ( localVar , localVar + 1) ) {
localVar = atomicVar.get( ) ;
}
5、无等待
和无锁相比,无等待要求所有的线程必须在有限步内完成,这样就不会引起饥饿问题了。
一种典型的无等待循环结构就是
RCU
(Read Copy Update)。它的基本思想:
对数据的读不加控制,读线程都是无等待的;但在写数据时,先取得原始数据的副本,接着只修改副本数据(这就是读不加控制的原因),修改完之后,在合适的时机回写数据
三、JMM
JMM的关键技术点都是围绕者多线程的原子性、可见性、有序性来建立的
1、原子性
原子性: 指一个操作是不可中断的。即使是多个线程同时执行,一个操作一旦开始,就不会被其它线程干扰
long
的问题: 在32位系统上, long
型数据的读写不是原子性的。因为 long
型数据有64位,如果同时有两个线程同时对 long
型数据进行写入,对线程之间的结果是有干扰的
2、可见性
可见性: 指一个线程修改了共享变量的值时,其它线程能否立即知道这个修改
串 / 并行程序中的可见性问题:
- 在串行程序中,不存在可见性问题,因为在任何一个操作步骤中修改了某个变量,在后续的步骤中读取这个变量时,读取的一定是修改后的值
- 但在并行程序中未必。例如两个线程A和B分别运行在CPU1和CPU2上,因为编译器或者硬件优化的原因,线程B将全局变量的值缓存在了
cache
中或寄存器中,当CPU1中的线程A修改了全局变量的值,CPU2中线程依然会读取cache
或寄存器中的旧值。于是,就产生了可见性问题。
3、有序性
有序性: 编译器或硬件会对代码进行优化,代码执行的顺序可能会发生改变。在串行程序中,这不会改变执行结果,但在多线程并发时,程序的执行可能回发生乱序
有序性问题产生的原因: 程序在执行过程中,发生了指令重排序,重排序后的指令与原指令的顺序相比发生了改变
指令重排注意点:
- 指令重排不会使串行的语义逻辑发生问题,这是指令重排的基本前提
- 指令重排保证串行语义的一致性,但不保证并行语义的一致性
4、Happen-Before 原则
虽然
Java
虚拟机和执行系统会对指令进行一定的重排序,但不是所有的指令都可以随意改变位置,而是符合相关的原则
- 程序顺序原则:一个线程内保证语义的串行性
volatile
规则:volatile
变量的写先于读发生,这保证了volatile
变量的可见性- 锁规则:解锁(unlock)必然先于加锁(lock)发生
- 传递性:A 先于 B,B 先于 C,A 必然先于 C
- 线程的
start()
方法先于它的每一个动作 - 线程的所有操作先于线程的终结(
Thread.join()
) - 线程的中断(
interrupt()
)先于被中断程序的代码 - 对象的构造函数的执行、结束先于
finalize()
方法