一文看懂Java锁机制
点击上方“码农突围”,马上关注 这里是码农充电第一站,回复“666”,获取一份专属大礼包 真爱,请设置“星标”或点个“在看”
背景知识
指令流水线
cpu多级缓存
问题引入
原子性
读取变量i的值
进行加一操作
将新的值赋值给变量i
read 从主存读取到工作内存 (非必须)
load 赋值给工作内存的变量副本(非必须)
use 工作内存变量的值传给执行引擎
执行引擎执行加一操作
assign 把从执行引擎接收到的值赋给工作内存的变量
store 把工作内存中的一个变量的值传递给主内存(非必须)
write 把工作内存中变量的值写到主内存中的变量(非必须)
可见性
while (flag) {//语句1
doSomething();//语句2
}
flag = false;//语句3
顺序性
if (inited == false) {
context = loadContext(); //语句1
inited = true; //语句2
}
doSomethingwithconfig(context); //语句3
JMM内存模型
Java虚拟机规范定义了Java内存模型(Java Memory Model,JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果(C/C++等则直接使用物理机和OS的内存模型,使得程序须针对特定平台编写),它在多线程的情况下尤其重要。
内存划分
主内存(Main Memory)存储所有共享变量的值。
工作内存(Working Memory)存储该线程使用到的共享变量在主内存的的值的副本拷贝。
这种划分与Java内存区域中堆、栈、方法区等的划分是不同层次的划分,两者基本没有关系。硬要联系的话,大致上主内存对应Java堆中对象的实例数据部分、工作内存对应栈的部分区域;从更低层次上说,主内存对应物理硬件内存、工作内存对应寄存器和高速缓存。
内存间交互规则
lock: 将一个变量标识为被一个线程独占状态
unclock: 将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定
read: 将一个变量的值从主内存传输到工作内存中,以便随后的load操作
load: 把read操作从主内存中得到的变量值放入工作内存的变量的副本中
use: 把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令
assign: 把一个从执行引擎接收到的值赋给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时,都要使用该操作
store: 把工作内存中的一个变量的值传递给主内存,以便随后的write操作
write: 把store操作从工作内存中得到的变量的值写到主内存中的变量
不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步会主内存中
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和load操作。
一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。
如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。
如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)
read、load、use必须成对顺序出现,但不要求连续出现。assign、store、write同之;
变量诞生和初始化:变量只能从主内存“诞生”,且须先初始化后才能使用,即在use/store前须先load/assign;
lock一个变量后会清空工作内存中该变量的值,使用前须先初始化;unlock前须将变量同步回主内存;
一个变量同一时刻只能被一线程lock,lock几次就须unlock几次;未被lock的变量不允许被执行unlock,一个线程不能去unlock其他线程lock的变量。
先行发生原则
Java内存模型具备一些先天的“有序性”,即不需要通过任何同步手段(volatile、synchronized等)就能够得到保证的有序性,这个通常也称为happens-before原则。
程序次序规则(Program Order Rule):一个线程内,逻辑上书写在前面的操作先行发生于书写在后面的操作。
锁定规则(Monitor Lock Rule):一个unLock操作先行发生于后面对同一个锁的lock操作。“后面”指时间上的先后顺序。
volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作。“后面”指时间上的先后顺序。
传递规则(Transitivity):如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每个一个动作。
线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生(通过Thread.interrupted()检测)。
线程终止规则(Thread Termination Rule):线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
对象终结规则(Finaizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize()方法的开始。
问题解决
原子性
由JMM直接保证的原子性变量操作包括read、load、use、assign、store、write;
基本数据类型的读写(工作内存)是原子性的
可见性
“对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)”。
"如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值"
顺序性
开发篇
volatile
对一个volatile变量的写操作先行发生于后面对这个变量的读操作。“后面”指时间上的先后顺序
当写一个 volatile 变量时,JMM 会把该线程对应的工作内存中的共享变量刷新到主内存。
当读一个 volatile 变量时,JMM 会把该线程对应的工作内存置为无效,线程接下来将从主内存中读取共享变量。
对变量的写入操作不依赖于其当前值,即仅仅是读取和单纯的写入,比如操作完成、中断或者状态之类的标志
禁止对volatile变量操作指令的重排序
volatile底层是通过cpu提供的内存屏障指令来实现的。硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
阻止屏障两侧的指令重排序
强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效
final
写final域的重排序规则:在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
读final域的重排序规则:初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
public class FinalExample {
int i;//普通域
final int j;//final域
static FinalExample obj;
public FinalExample () {
i = 1;//写普通域。对普通域的写操作【可能会】被重排序到构造函数之外
j = 2;//写final域。对final域的写操作【不会】被重排序到构造函数之外
}
// 写线程A执行
public static void writer () { 
obj = new FinalExample ();
}
// 读线程B执行
public static void reader () { 
FinalExample object = obj;//读对象引用
int a = object.i;//读普通域。可能会看到结果为0(由于i=1可能被重排序到构造函数外,此时y还没有被初始化)
int b = object.j;//读final域。保证能够看到结果为2
}
}
在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
synchronized
确保代码的同步执行(即不同线程间的互斥)(原子性)
确保对共享变量的修改能够及时可见(可见性)
有效解决指令重排问题(顺序性)
进入/加锁时执行字节码指令MonitorEnter
退出/解锁时执行字节码指令MonitorExit
当执行代码有异常退出方法/代码段时,会自动解锁
修饰对象方法时,使用当前对象的监视器
修饰静态方法时,使用类类型(Class 的对象)监视器
修饰代码块时,使用括号中的对象的监视器
必须为 Object 类或其子类的对象
每个对象都有一个关联的监视器。
监视器被锁住,当且仅当它有属主(Owner)时。
线程执行MonitorEnter就是为了成为Monitor的属主。
如果 Monitor 对象的记录数(Entry Count,拥有它的线程的重入次数)为 0, 将其置为 1,线程将自己置为 Monitor 对象的属主。
如果Monitor的属主为当前线程,就会重入监视器,将其记录数增一。
如果Monitor的属主为其它线程,当前线程会阻塞,直到记录数为0,才会 去竞争属主权。
执行MonitorExit的线程一定是这个对象所关联的监视器的属主。
线程将Monitor对象的记录数减一。
如果Monitor对象的记录数为0,线程就会执行退出动作,不再是属主。
此时其它阻塞的线程就被允许竞争属主。
线程
关联监视器的对象
存放类的属性数据信息,包括父类的属性信息
如果是数组的实例变量,还包括数组的长度
这部分内存按4字节对齐
由于虚拟机要求对象起始地址必须是8字节的整数倍
填充数据仅仅是为了字节对齐
保障下一个对象的起始地址为 8 的整数倍
长度可能为0
对象头由 Mark Word 、Class Metadata Address(类元数据地址) 和 数组长度(对象为数组时)组成
在 32 位和 64 位的虚拟机中,Mark Word 分别占用 32 字节和 64 字节,因此称其为 word
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
LIFO,单向链表
很多线程都可以把请求锁的线程放入队列中
但只有一个线程能将线程出队
双向链表
只有拥有锁的线程才可以访问或变更 EntryLis
只有拥有锁的线程在释放锁时,并且在 EntryList 为空、ContentionList 不为 空的情况下,才能将ContentionList 中的线程全部出队,放入到EntryList 中
将进行 wait() 调用的线程放入WaitSet
当进行 notify()、notifyAll()调用时,会将线程放入到ContentionList或EntryList 队列中
对一个线程而言,在任何时候最多只处于三个集合中的一个
处于这三个集合中的线程,均为 BLOCKED 状态,底层使用互斥量来进行阻塞
在获取锁时,如果发生竞争,则使用自旋锁来争用,如果自旋后仍得不 到,再放入上述队列中。
自旋可以减少ContentionList和EntryList上出队入队的操作,也就是减少了内部 维护的这些锁的争用。
如果由1变为0,表示无竞争,继续执行
如果小于 0,表示有竞争,调用 futex(..., FUTEX_WAIT, ...) 使当前线程休眠
如果futex变量由0变为1,表示无竞争,继续执行
如果 futex 变量变化前为负值,表示有竞争,调用 futex(..., FUTEX_WAKE, ...) 唤醒一个或多个等待线程
Lock
- END - 最近热文
• 女友回老家了!没吊事,手把手带你搭建一台服务器! • 尼玛,Github上最邪恶的开源项目了!未满18或者女孩子勿进哦~ • 永别了,91网站!宣布永久关闭 • 腾讯员工晒出薪资:真实 985 毕业薪资,大家看我还有救吗?网友:日薪?