0 java中有哪些锁?
一直以来只知道java中有Synchronized和ReentrantLock这两种常用的锁实现,但是对于java中锁的设计理念,各种互斥、同步接口的实现,以及他们和java对象管程的关系云里雾里的,借此机会梳理汇总一下java中与锁相关的概念及其底层实现。
那么java中有哪些锁?
锁的分类方式有很多种,公平锁/非公平锁、可重入锁、互斥锁/共享锁、乐观锁/悲观锁、分段锁、偏向锁/轻量级锁/重量级锁、自旋锁等,这些锁的分类概念往往还存在一些交叉,为了更清晰了解java中锁的设计理念,这里可以将java中的锁划分为两个层面的实现:
-
基于JVM实现的 内置锁, 内置锁是基于Java语法层面实现的锁
-
基于JDK实现的 显示锁, 显式锁是基于java API层面实现的锁
事实上,在并发编程领域,主要有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作等。而锁的设计往往和这两个问题相关联。
1 JVM内置锁(Synchronized)解析
内置锁是如何解决两个核心问题的?
Synchronized是通过锁住Monitor管程对象来实现互斥的;
通过对象的Object方法,比如wait()、notify()、notifyAll来实现线程通信协作的;
Synchronized锁的到底是什么?
锁的是对象(在Java中有两种对象:Class对象和实例对象);正式因为锁的是对象,因此Synchronized的作用粒度最小只能到达对象,而且可重入;这也是为什么说 ReentrantLock(锁的粒度可以自由控制) 比 Synchronized 更加灵活的原因;
Synchronized在使用上有三种方式:
-
静态方法加上关键字
锁的是Class对象;
-
实例方法(也就是普通方法)加上关键字
1 2 3 4
public synchronized void method() { // 同步方法todo }
锁的是实例对象;
-
方法中使用同步代码块,性能要比前两种方法要好;
根据传入的参数是object还是object.class来定;
如果传入的是this,则锁的是实例对象; 实例对象锁和类对象锁不冲突!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
private Object lock1 = new Object(); public void demo1() { synchronized (lock1) { //锁的是实例对象 //同步代码块todo } } public void demo2() { synchronized (tihs) { //锁的是这个方法所在实例对象 不是lock1 //todo } } public void demo3() { synchronized (lock1.class) { //锁的是lock1的class对象,与demo1()不冲突 //todo } }
既然锁的是对象,那么对象为什么能被锁?或者说对象作为堆中一种数据结构存在,怎么实现锁动作的? 先看对象在内存中的结构组成,对象在内存中存储的结构由三部分组成:对象头、实例数据、对齐填充。对象头又由MarkWord(64bit)+KlassPoint(32bit or 64bit)+ArrayLength(0 or 64bit)组成,如下图:
在不同的锁状态下,MarkWord会存储不同的信息,这也是为了节约内存常用的设计。如上图所示,当锁状态为重量级锁(锁标识位=10)时,Mark word中会记录指向Monitor对象的指针(ptr_to_heavyweight_monitor),这个Monitor对象也称为管程或监视器锁。每个对象都存在着一个 Monitor对象与之关联。其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法。(关于Monitor对象中具体包含哪些内容可以参考我的另一篇博文:Monitor对象全解析-Peakiz Blog)
synchronized获取锁执行的是 monitorenter 指令,目的是取获取与之关联的Monitor 对象的所有权(这个所有权管控着当前线程是否可以触达当前普通对象的内部代码同步区,即被synchronized锁定的临界区),抢到了所有权就算成功获取了锁;执行 monitorexit 指令则是释放了Monitor的所有权。对于 monitorenter 和 monitorexit 指令,底层实现是调用c++代码,实际使用操作系统的mutex(互斥量)来实现锁机制和操作系统的condition(条件变量)来实现同步机制(就是等待wait,通知nodify)
使用synchronized同步代码块的时候,确实是使用monitorenter 和monitorexit 两个指令实现的,但是对于synchronized用于同步方法来说,是有一些差异的,会比同步代码块多一项检查方法的标志位的步骤;
具体来说,用于同步方法时,会先检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor(执行 monitorenter ),获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
那么ACC_SYNCHRONIZED 访问标志位在哪里呢? JVM是在方法访问标识符(flags)上添加了一个名叫ACC_SYNCHRONIZED 的字段,具体在jvm内存中是保存在方法区中 (关于代码在内存中的分布可以参考我的另一篇博文: java代码在jvm内存中的具体分布 )
上述同步代码块和同步方法的差异可以通过对编译后的.class文件进行反编译后查看(使用 javap -c -v
命令):
-
对于同步代码:
反编译后的结果为:可以看到在
pringlin
方法的前后确实存在monitorenter和monitorexit指令
-
对于同步方法:
反编译后:在方法的标识位存在 ACC_SYNCHRONIZED 标识(内存中位于方法区),先判断标识再执行monitorenter
Synchronized锁的优化
从JDK5引入了现代操作系统新增加的CAS原子操作( JDK5中并没有对synchronized关键字做优化,而是体现在J.U.C中,所以在该版本concurrent包有更好的性能 ),从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。由于此关键字的优化使得性能极大提高,同时语义清晰、操作简单、无需手动关闭,所以推荐在允许的情况下尽量使用此关键字。
synchronized锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,目的是为了提高获得锁和释放锁的效率。
那么synchronized真的不可降级吗?
事实上,像 HotSpot JVM 实现其实是支持锁降级的,但是锁频繁升降级的话对性能就会造成很大影响。重量级锁降级发生于垃圾回收的 STW 阶段,此时Java线程都会暂停在“安全点(SafePoint),降级对象为仅仅能被 VMThread 访问而没有其他 JavaThread 访问的对象。VMThread通过对所有Monitor的遍历,删除这些符合条件的Monitor对象,同时会把对应对象的对象头的MarkWord恢复到重量级锁之前的状态。不过既然Monitor对象都被删除了,锁都不存在了,其实可以认为就是不可降级的一种表现。
在 JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking
来禁用偏向锁。
Synchronized锁升级的过程
在详细描述锁的状态转换过程前,先来看一下偏向锁状态被引入的目的是什么。
偏向锁被引入的目的:为了在没有多线程竞争的情况下尽量减少不必要的轻量级锁执行(减少CAS操作)。因为轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令。CAS操作会延迟本地调用,因此偏向锁的想法是一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。
CAS的全称为Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。
根据MESI协议,CPU Core1和Core2可能会同时把主存中某个位置的值Load到自己的L1 Cache中,当Core1在自己的L1 Cache中修改这个位置的值时,会通过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现自己L1 Cache中的值失效(称为Cache命中缺失)则会通过总线从内存中加载该地址最新的值,大家通过总线的来回通信称为“Cache一致性流量”,因为总线被设计为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,称为“Cache一致性”,从这个层面来说,锁设计的终极目标便是减少Cache一致性流量(尽可能在并发过程中减少cache不一致的时间)
而CAS恰好会导致Cache一致性流量,如果有很多线程都共享同一个对象,当某个Core CAS成功时必然会引起总线风暴,这就是所谓的本地延迟,本质上偏向锁就是为了消除CAS,降低Cache一致性流量。
当然,在一些高端处理器上,可能架构有些不同,没有总线,没有公用主存,每个Core有自己的内存,针对这种结构以上的CAS讨论则不成立。
轻量级锁的目的:轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁侧则是在只有一个线程执行同步块时进一步提高性能。
锁升级的具体过程:
值得注意的是:重量级锁的获取是需要进入内核态的,而其他状态均在用户态实现;
另外可以看到,jvm初始启动的前4秒,new出来的对象是没有启动偏向锁的(偏向锁此时无效),称之为偏向锁延迟,那么为什么要设计这个延迟呢?这是一个启动时间回归的解决方案,也就是说这样做,可以使得JVM启动更快。
为什么启动会更快?因为JVM在启动期间会采取大量安全点来消除偏差。 JVM在启动期间用到的锁,包括初始化很多类的过程中用的锁,都会经过偏向锁逻辑,如果没有偏向延迟,就会带来更多的STW,导致JVM启动时间过长。
2 JDK显示锁
-
基于Lock接口实现的锁:基于Lock接口实现的锁主要有ReentrantLock。
-
基于ReadWriteLock接口实现的锁:基于ReadWriteLock接口实现的锁主要有ReentrantReadWriteLock。
-
基于AQS基础同步器实现的锁:基于AQS基础同步器实现的锁主要有CountDownLatch,Semaphore,ReentrantLock,ReentrantReadWriteLock等。
-
基于自定义API操作实现的锁: 不依赖于上述三种方式来直接封装实现的锁,最典型是JDK1.8版本中提供的StampedLock。
从一定程度上说,Java显式锁都是基于AQS基础同步器实现的锁,其中JDK1.8版本中提供的StampedLock是对ReentrantReadWriteLock读写锁的一种改进。
对于JDK显示锁,这里就不展开了,每一项实现都有大量的细节。
3 总结
单纯从Java对其实现的方式上来看,java中的锁可以划分为基于JVM实现的内置锁和基于JDK层面实现的显示锁。
-
Java内置锁:基于Java语法层面(JVM)实现的锁,主要是根据Java语义来实现,最典型的应用就是synchronized。
-
Java显式锁:基于JDK层面实现的锁,主要是根据基于Lock接口和ReadWriteLock接口,以及统一的AQS基础同步器等来实现,最典型的有ReentrantLock。
最后再着重看一下synchronized和ReentrantLock的对比
-
两者都是可重入锁 重入一次锁的计数器都自增1,锁的计数器下降为0时才能释放锁。
-
synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成)
- ReentrantLock 相比 synchronized 增加了一些高级功能
①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
- 等待可中断.通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
- ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
- ReentrantLock类线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的。用ReentrantLock类结合Condition实例可以实现“选择性通知”
- 使用选择。除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持(Java 5开始引入JUC)。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放
参考资料:
- Java对象的内存布局 - JaJian
- 谈谈JVM内部锁升级过程 - 掘金 (juejin.cn)
- 深入分析Synchronized原理(阿里面试题) - aspirant - 博客园 (cnblogs.com)
- 如何正确理解Java领域中的并发锁我们应该具体掌握到什么程度
- synchronized锁升级降级问题
- java synchronized - 高压锅里的大萝卜 - 博客园 (cnblo)