深入学习重点分析java基础---第二章:java并发 synchronized
1.由浅入深首先要有 乐观锁、悲观锁的概念
乐观锁:CAS(比较并替换) 乐观锁从乐观的角度看待并发问题,也就是乐观锁默认不存在并发问题,只是线程去修改数据的时候发现数据已经被修改了,才会返回修改失败的响应,乐观锁允许线程自旋尝试获取锁
悲观锁:synchronized、reentranLock以及数据库for update等,悲观锁默认会有线程争抢资源.所以每次线程操作前会尝试抢占锁,如果锁已经被占用就阻塞当前线程,并等待持有锁的线程释放锁、
2.可重入锁
synchroized和reentranLock都是可重入锁,可重入锁的意思是,当一个线程获取了该锁后,还能再次获取该锁,并且将重入数加1,每次线程释放锁 该计数减1
为什么要设计为可重入锁?当线程已经持有该对象的monitor锁时再次访问受限资源时monitor计数器会加1,当访问结束jvm释放monitor锁时,monitor计数器会减1,计数器为0则完全释放该对象的monitor锁
3.synchronized的锁升级
jdk1.6对synchronized进行了非常大的优化,使得synchronized与reentranLock性能非常接近,甚至在高并发情况下效率高与reentranLock
整个升级过程为
无锁 --------> 偏向锁 --------> 轻量级锁 --------> 重量级锁
首先没有线程获取对象的monitor锁时,对象处于无锁状态
当线程获取对象的monitor锁时,会在对象头和栈帧的锁记录里存储当前线程的ID,作为偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁只需测试Mark Word里线程ID是否为当前线程。
如果测试成功,表示线程已经获得了锁。如果测试失败,则需要判断偏向锁的标识。如果标识被设置为0(表示当前是无锁状态),则使用CAS竞争锁;如果标识设置成1(表示当前是偏向锁状态)
则尝试使用CAS将对象头的偏向锁指向当前线程,触发偏向锁的撤销。偏向锁只有在竞争出现才会释放锁。当其他线程尝试竞争偏向锁时,程序到达全局安全点后(没有正在执行的代码),它会查看Java
对象头中记录的线程是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程的栈帧信息,如果还是需要继续持有这个锁对象
那么暂停当前线程,撤销偏向锁,升级为轻量级锁,如果线程1不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
需要注意的是对象头中有一个epoch值,该值会记录对象偏向锁改变的次数,可以理解为偏向锁的版本号
这个epoch有什么用呢?就是当虚拟机认为此类对象被撤销的次数超过一定次数 ( -XX:BiasedLockingBulkRebiasThreshold,默认为 20)之后认为该换代了
也就是每次锁对象被撤销偏向的时候都会记录着,当超过阈值之后,对象对应的类里面的epoch就会升级,也就是+1。
当类对象被撤销的次数超过一定次数 (-XX:BiasedLockingBulkRevokeThreshold,默认值为 40),JVM会认为此类对象都不适合偏向,会撤销所有此类实例的偏向锁,并且之后的加锁直接轻量级,没偏向了
当对象处于轻量级锁状态时,线程在获取monitor时,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的MarkWord复制到锁记录中,即Displaced Mark Word。然后线程会尝试使用CAS将对象头中的Mark Word
替换为指向锁记录的指针。如果成功,当前线程获得锁。如果失败,表示其他线程在竞争锁,当前线程使用自旋来获取锁。当自旋次数达到一定次数时,锁就会升级为重量级锁。
轻量级锁解锁时,会使用CAS操作将Displaced Mark Word替换回到对象头,如果成功,表示没有竞争发生。如果失败,表示当前锁存在竞争,锁已经被升级为重量级锁,则会释放锁并唤醒等待的线程。
4.各种锁状态下对象头中的数据
synchronized的锁状态存在Java对象头里,Java对象头里的Mark Word默认存储对象的HashCode、分代年龄和锁标记位。在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。32位JVM的Mark Word可能变化存储为以下5种数据:
图1
可以看到当对象处于无锁状态之外的任何状态时,都没有存储HashCode,那么此时如何获取对象的HashCode呢
首先这里的HashCode指的是jvm中的 identity hash code 与类中自定义的hashCode()方法不是一回事,如果没有自定义hashCode()方法,也就是调用的Object对象的hashCode方法时返回的才是identity hash code
这里需要注意几点
- 当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;
- 当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;
- 重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。或者简单说就是重量锁可以存下identity hash code。
5.synchroized底层实现原理
synchroized在未发生激烈竞争的情况下不会进入到重量级锁状态,此前都是使用自旋的方式阻塞线程,而大家都知道自旋锁只在用户态就能做到,只是会消耗cpu资源
而synchroized进入到重量级锁之后则是切换到内核态阻塞线程,从图1中可以看到重量级锁只存储了指向互斥量(重量级锁)的指针
这里的指针指向的其实就是上面提到过多次的monitor锁的起始地址
每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态
在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor() { _header = NULL; _count = 0; //记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; //_owner指向持有ObjectMonitor对象的线程 _WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象);
整个monitor运行的机制过程如下:
_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1
若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
具体见下图:
因此,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因
同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因
ps:文中有借鉴以下博客中的内容,感谢几位博主分享了这么多优质内容