Java锁:volatile、synchronized和lock API

144 阅读8分钟

整理一些学习Java锁的笔记。

一、volatile和synchronized

1、volatile关键字

1.1 要点

  • 保证可见性
  • 禁止指令重排
  • 特殊情况下可以保证原子性(i++这种情况,要分条才能保证一致性),理解不深入不要答

1.2 如何保证可见性

  • 可见性,指让其他线程可见:多个线程修改同一个变量,一个线程修改完的结果要让其他线程知道;
  • 线程提交修改,推到主存到总线的过程中,其他cpu嗅探总线的数据流通:通过嗅探,在缓存一致性协议的保证下,能嗅探到修改,如果自己的缓存中有这条数据,则置为不可用,有线程需要读则从主存中拉取最新值,更新到缓存中
  • cpu如何知道总线上这个变量是volatile?汇编指令码中会加入lock指令,lock两层含义:1将这条指令推到主存,2lock指令过总线时其他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的区别

层面synchronizedlock
基本使用层面是关键字接口,要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