Java并发机制的底层实现原理
零、JMM
1. JMM是什么
java内存模型(Java Memory Model,JMM)是java虚拟机规范定义的,用来屏蔽掉java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在各种不同的平台上都能达到内存访问的一致性。为了实现了JVM的跨平台性,在向上提供了一系列的指令的同时,也提供了一些编程规则需要理解和遵守,比如Happens-Before原则,as-if-serial原则,主内存工作内存的概念等等。
Happens-Before
1. 程序次序规则:
在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
2. 管程锁定规则:
就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
3. volatile变量规则:
就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
4. 线程启动规则:
在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
5. 线程终止规则:
在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
6. 线程中断规则:
对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
7. 传递规则:
这个简单的,就是happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。
8. 对象终结规则:
这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
as-if-serial
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。但依然会对毫无关联的两个语句进行指令重排,比如 int a=1; int b = 2; 它们的先后顺序可能会不一样.
主内存和工作内存
JMM定义的内存变量的访问规则(这里的变量是指线程共享的变量),有了主内存和工作内存的概念。 Java虚拟机规定所有变量(非线程私有的变量)都存在主内存中,而线程私有的局部变量存在线程独有的工作内存中,这两个概念比较类似Java内存规范中的堆(主内存)和虚拟机栈(工作内存),只是比较类似!
而解决主内存的中的变量和工作内存中的变量的同步的方式就是用volatile关键字。
一、Volatile关键字
-
作用:
- 保证了“共享变量”在多线程环境下的“可见性”.
-
底层实现原则:
- 对被volatile修饰的变量进行写操作的时候,JVM会向处理器发送一条#Lock前缀指令,这个指令的作用就是将对应缓存行的数据写回到被缓存的内存。
- 一个处理器将缓存回写到内存
<addr>
中,那么其他处理器中对<addr>
内存地址的缓存都会被标记成’失效’。(缓存一致性协议MESI:由嗅探技术实现,每个缓存行会有一个标示位,分别代表 :M(被修改),E(独占的), S(共享的), I(无效的), 若读取的缓存行是无效,那么会重新从内存读取)
二、Synchronized关键字
1. 作用:
对于 Synchronized 关键字而言,每一个Java对象都可以作为锁,具体表现为:
- 普通
Synchronized
方法,锁是当前对象 - 静态
Synchronized
方法,锁是当前类的Class对象 - 对于
Synchronized
方法块,锁是括号里配置的对象
2. 对象头:
- 普通对象的对象头占2个字,分别为:
- Mark Word:存储了对象的HashCode和锁信息
- Class Metadata Address:存储对象类型的数据指针
- 数组类对象的对象头占3个字,除了上面两个还有一个:
- Array Length: 数组的长度
Mark Word:
3. 锁的升级和对比:
https://www.cnblogs.com/pomer-huang/p/10965228.html
在讲重量级锁的调用的时候,可以说一下Java对管程的实现,即每一个对象都可以被视作一个MonitorObject,且维护着一个WaitSet,EntrySet,具体可以看 这个 。
4. 原子操作的实现:
-
处理器实现院子操作:
-
1.总线锁:
- 处理器提供一个LOCK #信号,当一个处理器在总线上输出此信号时,其他处理器的请求将会被阻塞,此时处理器可以独享内存。缺点是,内存的不同地址之间其实不存在同步关系,这样会使得效率很低.
-
2. 缓存锁
- 利用缓存一致性协议(如MESI协议),和处理器提供的LOCK指令对指定内存上锁,完成了对共享资源操作的互斥。
- 需要注意的是以下两个情况不能使用缓存锁:
- 数据无法写入到缓存中,或操作数据跨多个缓存行。
- 处理器不支持缓存锁,此时会使用总线锁.
-
Java实现原子操作:
CMPXCHG
指令信实现,CAS的作用是:相等则交换。 -
三、ReentrantLock与AQS
Java内存模型
1. Happens-Before原则:
编译器,处理器进行不同层次上的指令重排会对多线程编程造成一定的影响,对于一些不应该进行指令重排的场景下,Java编译器通过在适当的位置插入内存屏障指令来禁止特定类型的处理器重排序,JMM把内存屏障指令分为如下四类:
从JDK5开始,Java使用新的JSR-133内存模型,其主要作用就是提供了happens-before原则,屏蔽掉了底层解决内存可见行问题的实现,编程者只要记住happens-before原则,并在理解这一原则下进行编程,happens-before原则为:
- 程序顺序原则:一个线程中的每个操作,都happens-before于该线程中的任意后续操作;
- 监视器锁规则:对一个锁的解锁,一定happens-before于随后对这个锁的读
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读.
- 传递性:A happens-before B, B happens-before C, 那么 A happens-before C.
2. as-if-serial语义
对于单线程而言,如果操作之间不存在数据依赖,且改变顺序后不对最终结果产生影响,那么会对其进行指令重排.
- 例如:
int a = 1;
bool k = true;
这一重排尽管不会改变其对单线程的执行,但可能会导致多线程的执行结果, 因此一定要记住这个!
- 存疑的ps: 在32位系统的JVM中,写一个64位的long long会被分成两个32位的原子操作! Java语言规范鼓励但不强求JVM对64位的long long 型变量和double变量的写操作具有原子性.
琐碎知识点:
1. 信号量和条件变量的区别是什么?
- 条件变量可以通过
signal()
唤醒队首阻塞线程,使用signalAll()
来唤醒所有阻塞线程;而信号量只能通过release()
唤醒队首阻塞线程. - 信号量可以初始化初始的值,但条件变量不可以,但条件变量+共享变量可以实现初始值大于0的信号量的功能。(个人理解成,条件变量的功能类似一个初始值为0的信号量 )。