整理了一些自己之前学习Java并发锁的笔记,包括volatile、synchronized和lock API的机制和它们之间的对比。
一、volatile和synchronized
1、volatile关键字
1.1 要点
-
保证可见性;
-
禁止指令重排;
-
特殊情况下可以保证原子性(i++这种情况,要分条才能保证一致性);
1.2 如何保证可见性
-
可见性,指让其他线程可见:多个线程修改同一个变量,一个线程修改完的结果要让其他线程知道;
-
线程提交修改,推到主存到总线的过程中,其他cpu嗅探总线的数据流通:通过嗅探,在缓存一致性协议的保证下,能嗅探到修改,如果自己的缓存中有这条数据,则置为不可用,有线程需要读则从主存中拉取最新值,更新到缓存中
-
cpu如何知道总线上这个变量是volatile?汇编指令码中会加入lock指令,lock两层含义:
-
1、将这条指令推到主存;
-
2、lock指令过总线时其他cpu会嗅探带lock的汇编指令、置其他缓存行为不可用;
-
-
嗅探,协议,lock指令;
1.3 如何禁止指令重排
-
在编译阶段,即在编译指令;
-
volatile写前,加入store-store屏障,写写不能重排;volatile写后,加入store-load屏障,写读不能重排;volatile读后,加入load-load屏障,读读不能重排;volatile读后,还要加入load-store屏障,读写不能重排(开销大,目前都不会使用单独的load-store)
1.4 i++为什么不能保证原子性
-
i = i + 1,是三条jvm指令码:i load 读取,i add 加,i store 写入,单独一条才可以保证原子性,
2、synchronized关键字
-
作用到代码块、普通方法、静态方法
-
作用到静态方法时,锁的是当前的class:不创建新对象
-
普通方法,锁在当前对象
-
同步代码块:可以指定锁,
3、volatile与synchronized的区别
场景:student类,一个变量int age,对age写了get set方法,现在要同步加锁,以下两种方法都能保证数据安全性:
-
方法一:将age用volatile修饰,get set不处理
-
方法二:age不用修饰,对get set方法加synchronized
问题:
-
对于读多写少的情况,选哪个比较快?volatile,修改少的话,线程可以直接从自己的缓存中拿数返回,非常快。
-
读少写多。volatile慢了,cpu需要嗅探、置当前缓存中不可用、频繁去主存读数,损耗总线资源。
在多线程环境下,重量级锁可以提高程序的吞吐量。
二、lock
1、synchronized与lock的区别
|
层面 |
synchronized |
lock |
|---|---|---|
|
基本使用层面 |
是关键字 |
接口,要new出子类 |
|
隐式加锁 |
显式加锁 | |
|
作用到方法和代码块上 |
只能作用到代码块上:try加锁,finally释放 | |
|
加锁方式 |
非阻塞加锁,可超时加锁,可中断式的加锁 | |
|
底层 |
对象监视器 |
aqs |
|
一个同步队列,一个等待队列 |
一个同步队列,多个等待队列 | |
|
锁竞争 |
非公平锁 |
公平锁,非公平锁 |
|
等待唤醒机制 |
与object配合 |
与condition接口配合 |
|
个性化定制 |
aqs是自己封装的,使用模版方法模式,可以重写很多方法readwritelock:支持并发读 |
2、lock接口的方法与实现类
2.1 方法
-
lock方法,普通加锁方法;
-
unlock,释放;
-
try lock,非阻塞加锁;
-
try lock加时间,超时非阻塞加锁;
-
lock interrupt,可中断加锁;
-
配合lock使用的condition,完成通知机制
2.2 实现类
reentrantlock,reentrantreadwritelock;
3、作用域
-
synchronized作用在静态方法和普通方法的区别:静态锁当前class,普通方法是当前类的对象,作用在代码块上可以用this或new object
-
synchronized加在代码块上和加在方法上,对于反编译后有什么区别:对于代码块加锁,反编译后需要代码块进入点monitor entry,代码块出点monitor exit、正常出口和异常的出口;对于加到方法上,在方法的flag(访问标志)里加入acc_synchronized
-
lock.lock时,是自己写的方法,和monitor不一样
4、加锁方式
4.1 非阻塞加锁
-
try lock,try lock with time
-
好处:没有获得锁就不需要线程上下文切换,
4.2 超时加锁
-
try lock with time,避免死锁发生
4.3 可中断
-
lock.interrupt,synchronized做不到
5、底层原理
5.1 syn
-
monitor:线程获取锁失败时进入同步队列entrylist,等待当前线程释放唤醒它;执行过程中调用wait释放锁后,进入waitset等待队列
-
lock:aqs,通过int state判断锁是否被持有,被持有则加入双端队列尾部;可能有多条线程竞争失败加入队尾,此时aqs使用cas加锁,
6、同步队列与等待队列
lock中有多个等待队列
7、锁的竞争
-
如果abc竞争锁,a成功,bc失败加入队列,a释放锁后:b开始与新来的线程竞争锁、此时为非公平锁,新线程直接加入队尾、此时为公平锁
-
线程饥饿问题:非公平锁下,b可能一直等待
8、等待唤醒机制
-
lock condition,与syn相似,名字不同,condition中叫await和signal
-
condition中线程调用wait后去哪了?被加入condition中的等待队列,不用使用cas,因为线程调用wait之前先会加入队列、此时仍然持有当前锁、没有其他线程竞争尾节点,加完调用wait
-
wait后,加入等待队列,到头部时被唤醒,进行竞争,竞争失败后加入同步队列,cas加,排队到头部被唤醒,判断是否公平,获取锁,到wait方法再执行
9、个性化定制
-
模版方法:acquire获取锁,acquire shared,acquire interrupt,acquire shared interrupt,try acquire nanonse?,try acquire shared nanose?,
-
可以复写的方法:try acquire,try acquire shared,try release,try release share
10、reentrantlock
-
只能读读并发,读写、写读、写写都不行
-
通过一个int控制,高16位读、低16位写:读的时候检查低16位有无,无则读;写的时候都检查,全无才写
三、锁升级(32位jvm虚拟机为例)
1、对象头部
-
25位hashcode,4位分代年龄,1位偏向锁标记位,2位锁标记位
-
当没有调用Object.hashcode()时,25位hashcode不存在,以0占位
-
调用复写的hashcode不会产生25位hashcode,只有obejct
2、无锁升级为偏向锁
-
无锁状态且25位hashcode为空,也就是说如果创建对象时调用hashcode()、对象头中25位hashcode有值,则不能使用偏向锁;
-
偏向锁要把线程ID 23位 + Epoch 2位放到25位的hashcode上,他的逻辑里不允许覆盖hashcode
-
是否为偏向锁,从0变成1,
-
锁标记位从00变成11
-
好处:再次加锁时,检查线程Id是不是自己线程的Id即可,不用检查锁是否释放、是否竞争、挂起之类的
3、偏向锁升级为轻量级锁
3.1 升级过程(竞争环境下偏向锁不一定升级为轻量级锁)
-
线程A将ID放入,线程B进入,检查锁标记位是否为偏向锁、当前是,B检查A的存活状态,A如果没有执行同步代码、B则直接置对象为无锁状态然后争抢;
-
B抢到则放入自己的ID、即现在偏向锁偏向B,B没抢到C抢到了将对象置为偏向C的状态同时开始执行,即出现B在争抢而C在执行,B发起偏向锁撤销操作,但此时需要等待C到达安全点;
-
C到达安全点后,C的栈会被遍历记录锁,有三种情况:1 升级为轻量级锁、将锁标记位置为00,2 变成偏向锁偏向B,3 将对象置为不可使用偏向锁;
-
升级为轻量级锁理解为无锁状态?因为只改了标记位,没有添加指向栈中锁记录的指针
-
重新偏向B?批量重偏向和批量撤销,jvm有配置这里的批量操作阈值是20,如果B操作了很多对象进行上述操作2,就会触发批量重偏向;
-
批量撤销,直接置为00,即上述操作3,阈值是40
-
3.2 轻量级锁
-
A抢到后,将除锁标志位以外的30位直接复制到自己栈的lock record,并将对象的30位改成指向自己lock record的指针
-
B争抢发现对象头的30位存的是上述指针后,循环10次cas,仍然失败,升级
4、轻量级锁升级为重量级锁
-
cas失败后,会将轻量级锁的指针直接替换为重量级锁,即指向Object monitor
-
但此时A还在执行,当释放时会将lock record通过cas替换回去,此时发现替换失败,则走重量级锁退出逻辑:将lock record放入monitor,并将monitor里的own变量设置为自己,保证对象头不丢失
5、上述几种状态的图
| 锁状态 | 25bit | 4bit | 1bit | 2bit | |
| 23bit | 2bit | 是否是偏向锁 | 锁标志位 | ||
| 轻量级锁 | 指向栈中锁记录的指针(当前线程会直接把这里的内容拷贝到自己的栈里) | 0 | |||
| 重量级锁 | 指向互斥量(重量级锁)的指针 | 10 | |||
| GC标记 | 空 | 11 | |||
| 偏向锁 | 线程ID | Epoch | 对象分代年龄 | 1 | 1 |
本文详细探讨了Java并发编程中的volatile关键字、synchronized关键字以及Lock接口的机制和区别。volatile确保可见性和禁止指令重排,但在某些情况下无法保证原子性。synchronized提供了一种内置的锁机制,作用于方法或代码块。Lock接口提供了更灵活的加锁方式,如非阻塞加锁、超时和中断。文章还讨论了锁的升级过程,从无锁到偏向锁、轻量级锁再到重量级锁,并介绍了ReentrantLock等具体实现。
479

被折叠的 条评论
为什么被折叠?



