Java并发锁:volatile、synchronized和lock API对比

本文详细探讨了Java并发编程中的volatile关键字、synchronized关键字以及Lock接口的机制和区别。volatile确保可见性和禁止指令重排,但在某些情况下无法保证原子性。synchronized提供了一种内置的锁机制,作用于方法或代码块。Lock接口提供了更灵活的加锁方式,如非阻塞加锁、超时和中断。文章还讨论了锁的升级过程,从无锁到偏向锁、轻量级锁再到重量级锁,并介绍了ReentrantLock等具体实现。

整理了一些自己之前学习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、上述几种状态的图

锁状态25bit4bit1bit2bit
23bit2bit是否是偏向锁锁标志位
轻量级锁指向栈中锁记录的指针(当前线程会直接把这里的内容拷贝到自己的栈里)0
重量级锁指向互斥量(重量级锁)的指针10
GC标记11
偏向锁线程IDEpoch对象分代年龄11
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值