AQS框架之ReentrantLock以及synchronized锁记录
AQS(AbstractQueuedSynchronizer)框架之——ReentrantLock源码分析
最近在学习AQS框架的ReentrantLock源码,通过debug加锁解锁,观察到lock持有锁时的几种情况,遂通过作图来加深印象。
注:该情况下的代码是通过公平锁实现。
AQS说明:
重要的属性:head(头)、state(锁状态,0:未加锁 1:加锁)、exclusiveOwnerThread(当前持有的线程)、tail(尾)
其中Node参数的重要属性:prev(前驱结点)、next(后继节点)、Thread(赋予的线程)
加锁解锁说明
1、加锁前lock锁的状态
2、加锁一次lock锁的状态
3、第一个线程未被释放,再次启动一个线程时lock锁的状态
4、再次启动一个新线程,进入队列排队时的状态
5、最后一种情况
当lock锁持有的线程释放的瞬间,进行t2加锁操作时lock锁的状态,暂时无法精准的复现。
ReentrantLock的公平锁与非公平锁
公平锁:即每个线程获取锁是根据顺序来的,如果锁正在被持有,则需要排队等着锁释放。类似于我们平时去食堂排队打饭,排到你打饭,你就可以独自享用打菜大妈的单独服务。
非公平锁:所有的线程看运气获取锁,无关于调用lock()方法的顺序。
附上公平锁与非公平锁的流程图
synchronized底层原理深究
对象头是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word和Class Metadata Address组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。
JDK6前 :锁只有两种状态——无锁状态、重量级锁
JDK6及之后 :锁状态——无锁状态、偏向锁、轻量级锁、重量级锁
锁膨胀
锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:无锁→偏向锁→轻量级锁→重量级锁,并且膨胀方向不可逆(在某种特别极端情况下是可逆的,此处可直接理解为不可逆)。
偏向锁
一句话总结它的作用:减少统一线程获取锁的代价。在大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得,那么此时就是偏向锁。
核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
轻量级锁
轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。
重量级锁
重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。如方法test1和方法test2,两者的执行效率是相同的,因为obj对象为私有属性,不存在竞争关系。
private void test1(){
Object obj = new Object();
synchronized (obj){
System.out.println("方法1");
}
}
private void test2(){
Object obj = new Object();
System.out.println("方法2");
}
锁粗化
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围(直接加类锁),避免反复加锁和释放锁。比如下面test3经过锁粗化优化之后就和test4执行效率一样。
private void test3(){
synchronized (ReentrantLockDemo.class){
for(int i = 0; i < 1000; i++){
System.out.println("输出" + i);
}
}
}
private void test4(){
for(int i = 0; i < 1000; i++){
synchronized (ReentrantLockDemo.class){
System.out.println("输出" + i);
}
}
}
自旋锁(pthread_spin_lock)与自适应自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段——一直去请求锁。
自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。
自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。
互斥锁(pthread_mutex_lock)
互斥锁:线程去请求锁,如果请求不到,则会将该线程park掉,进入阻塞状态。