• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Java中的锁的升级过程:无锁->偏向锁->轻量级锁->重量级锁

武飞扬头像
juejin
帮助144

前言

  • 今天来看看Java里边的锁升级过程,以及各种锁之间的比较,悲观乐观。

四种锁的Markword

image.png image.png

优先程度

  • 偏向锁->轻量级锁-(先自旋不行再膨胀)>重量级锁(不会自旋直接阻塞)

🎯轻量级锁

只是栈中一个锁对象,不是monitor这种重量级

轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的,所以不会出现阻塞的情况),那么可以使用轻量级锁来进行优化。轻量级锁对使用者是透明的,即语法仍然是synchronized,假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
  1. 每次指向到synchronized代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的Mark Word(用来改变对象的lock record编码)和对象引用reference (表示指向哪个对象)

image.png

  1. 让锁记录中的Object reference指向对象,并且尝试用CAS(compare and sweep)替换Object对象的Mark Word(表示加锁) , 将对象的Mark Word更新为指向Lock Record的指针,并将Mark Word 的值存入锁记录中 (等同于将Lock Record里的owner指针指向对象的Mark Word。)

image.png

  1. 如果cas替换成功,那么对象的对象头储存的就是锁记录的地址和状态01,如下所示

image.png

  1. 如果cas失败,有两种情况
    1. 如果是其它线程已经持有了该Object的轻量级锁,那么表示有竞争,将进入锁膨胀阶段
    2. 如果是自己的线程已经执行了synchronized进行加锁,那么那么再添加一条 Lock Record 作为重入的计数

且此时新的一条Lock Record中,对象的MarkWord为null(相当于被前一个抢了)

image.png

  1. 当线程退出synchronized代码块的时候,如果获取的是取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

image.png

  1. 当线程退出synchronized代码块的时候,如果获取的锁记录取值不为 null,那么**使用cas将Mark Word的值恢复给对象 **
    1. 成功则解锁成功
    2. 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

总结

  • 加锁和解锁都是用CAS来交换Lock Record

🎯锁膨胀

如果在尝试加轻量级锁的过程中,cas操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。

  1. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

image.png

  1. 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    1. 即为对象申请Monitor锁,让Object指向重量级锁地址,然后自己进入Monitor 的EntryList 变成BLOCKED阻塞状态

image.png

  1. 当Thread-0 推出synchronized同步块时,使用cas将Mark Word的值恢复给对象头,失败,那么会进入重量级锁的解锁过程,即按照Monitor的地址找到Monitor对象,将Owner设置为null,唤醒EntryList 中的Thread-1线程

总流程

image.png

🎯自旋优化

为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

image.png

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁

  1. 自旋重试成功的情况

image.png

  1. 自旋重试失败的情况,自旋了一定次数还是没有等到持锁的线程释放锁

image.png

自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。Java 7 之后不能控制是否开启自旋功能

自适应自旋锁

自旋锁在JDK1.4.2中引入,使用-XX: UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

在自旋锁中 另有三种常见的锁形式: TicketLock、CLHlock和MCSlock

🎯偏向锁

在轻量级的锁中,我们可以发现,如果同一个线程对同一2对象进行重入锁时,也需要执行CAS操作,这是有点耗时的,所以java6开始引入了偏向锁,只有第一次使用CAS时将对象的Mark Word头设置为入锁线程ID,之后这个入锁线程再进行重入锁时,发现线程ID是自己的,那么就不用再进行CAS来加锁和解锁了

image.png

偏向状态

image.png

一个对象的创建过程
  1. 如果开启了偏向锁(默认是开启的),那么对象刚创建之后,Mark Word 最后三位的值101,并且这是它的Thread,epoch,age都是0,在加锁的时候进行设置这些的值.
  2. 偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:-XX:BiasedLockingStartupDelay=0来禁用延迟
  3. 注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中

加上虚拟机参数-XX:BiasedLockingStartupDelay=0进行测试

public static void main(String[] args) throws InterruptedException {
Test1 t = new Test1();
    //加锁前
test.parseObjectHeader(getObjectHeader(t));
    //加锁后
synchronized (t){
test.parseObjectHeader(getObjectHeader(t));
}
    //释放锁后
test.parseObjectHeader(getObjectHeader(t));
} 

//输出结果如下,三次输出的状态码都为101
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
    
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
    
biasedLockFlag (1bit): 1
LockFlag (2bit): 01 

禁用偏向锁

image.png

测试禁用:如果没有开启偏向锁,那么对象创建后最后三位的值为001,这时候它的hashcode,age都为0,hashcode是第一次用到hashcode时才赋值的。在上面测试代码运行时在添加 VM 参数-XX:-UseBiasedLocking禁用偏向锁(禁用偏向锁则优先使用轻量级锁),退出synchronized状态变回001

  1. 测试代码:虚拟机参数-XX:-UseBiasedLocking
  2. 输出结果如下,最开始状态为001,然后加轻量级锁变成00,最后恢复成001
biasedLockFlag (1bit): 0
LockFlag (2bit): 01
LockFlag (2bit): 00
biasedLockFlag (1bit): 0
LockFlag (2bit): 01 

撤销偏向锁-hashcode方法

测试 hashCode:当调用对象的hashcode方法的时候就会撤销这个对象的偏向锁,因为使用偏向锁时没有位置存**hashcode**的值了 而轻量级锁存在lockRecord,重量级锁存在monitor

  1. 测试代码如下,使用虚拟机参数-XX:BiasedLockingStartupDelay=0 ,确保我们的程序最开始使用了偏向锁!但是结果显示程序还是使用了轻量级锁。
public static void main(String[] args) throws InterruptedException {
Test1 t = new Test1();
    //撤销偏向锁
t.hashCode();
test.parseObjectHeader(getObjectHeader(t));
synchronized (t){
test.parseObjectHeader(getObjectHeader(t));
}
test.parseObjectHeader(getObjectHeader(t));
} 

输出结果
biasedLockFlag (1bit): 0
LockFlag (2bit): 01
    
LockFlag (2bit): 00
biasedLockFlag (1bit): 0
    
LockFlag (2bit): 01 

撤销偏向锁-其它线程使用对象

这里我们演示的是偏向锁撤销变成轻量级锁的过程,那么就得满足轻量级锁的使用条件,就是没有线程对同一个对象进行锁竞争,我们使用waitnotify 来辅助实现

  1. 代码,虚拟机参数-XX:BiasedLockingStartupDelay=0确保我们的程序最开始使用了偏向锁!
  2. 输出结果,最开始使用的是偏向锁,但是第二个线程尝试获取对象锁时,发现本来对象偏向的是线程一,那么偏向锁就会失效,加的就是轻量级锁
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
biasedLockFlag (1bit): 1
LockFlag (2bit): 01
LockFlag (2bit): 00
biasedLockFlag (1bit): 0
LockFlag (2bit): 01 

撤销偏向锁 - 调用 wait/notify

会使对象的锁变成重量级锁,因为wait/notify方法只有重量级锁才支持

批量重偏向

如果对象被多个线程访问,但是没有竞争,这时候偏向了线程一的对象又有机会重新偏向线程二,即可以不用升级为轻量级锁,可这和我们之前做的实验矛盾了呀,其实要实现重新偏向是要有条件的:就是超过20对象对同一个线程如线程一撤销偏向时,那么第20个及以后的对象才可以将撤销对线程一的偏向这个动作变为将第20个及以后的对象偏向线程二。

🎯乐观锁VS悲观锁

乐观锁(无锁)

CAS

优点

  • 不会出现阻塞,所有线程都处于竞争状态,适用于线程较小的情况

缺点

  • 当线程较多的时候,会不断自旋浪费cpu资源

多读用乐观锁(冲突少)

多写用悲观锁(冲突多)

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行 retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

🎯公平锁VS非公平锁

公平锁

公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁

非公平锁的优点是可以减少唤起线程的开销(比如新的线程D进来的时候刚好前边的线程A释放了锁,那么D可以直接获取锁,无需进入阻塞队列),整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

实现

ReentrantLock提供了公平和非公平锁的实现。· 公平锁:ReentrantLockpairLock =new ReentrantLock(true)。· 非公平锁:ReentrantLockpairLock =new ReentrantLock(false)。

  • 如果构造函数不传递参数,则默认是非公平锁

源码比较

image.png 通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。

image.png 再进入hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。

综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。

🎯可重入锁vs不可重入锁

不可重入锁可能会导致死锁问题

首先ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。

当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status 1,且当前线程可以再次获取锁。而非可重入锁直接去获取并尝试更新当前status的值如果status != 0的话会导致其获取锁失败,当前线程阻塞。

释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。

image.png

🎯锁消除和锁粗化

blog.csdn.net/qq_26222859…

锁消除

锁消除是发生在编译器级别的一种锁优化方式。
有时候我们写的代码完全不需要加锁,却执行了加锁操作。

锁消除是Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。

比如,StringBuffer类的append操作:

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

从源码中可以看出,append方法用了synchronized关键词,它是线程安全的。但我们可能仅在线程内部把StringBuffer当作局部变量使用,比如:

    public static String test(String str1, String str2) {
        StringBuffer sb = new StringBuffer();
        sb.append(str1);
        sb.append(str2);
        return sb.toString();
    }
}

此时不同线程调用该方法,都会创建不同的stringbuffer对象,并不会出现锁竞争等同步问题,所以此时编译器会做优化,去除不可能存在共享资源竞争的锁,这便是锁消除。

锁削除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。

锁粗化

public void doSomethingMethod(){
    synchronized(lock){
        //do some thing
    }
	//两个加锁过程中间,还有一些代码,但执行的速度很快
    
    synchronized(lock){
        //do other thing
    }
}

这两块需要同步操作的代码之间,需要做一些其它的工作,而这些工作只会花费很少的时间,那么我们就可以把这些工作代码放入锁内,将两个同步代码块合并成一个,以降低多次锁请求、同步、释放带来的系统性能消耗,合并后的代码如下:

public void doSomethingMethod(){
    //进行锁粗化:整合成一次锁请求、同步、释放
    synchronized(lock){
        //do some thing
        //做其它不需要同步但能很快执行完的工作
        //do other thing
    }
}

本篇属于是冷面大翻炒了,如有错误的地方还请指正😪

这篇文章转载于:学新通

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通
  • 本文地址: https://www.swvq.com/boutique/detail/taneheh
  • 联系方式: luke.wu●vfv.cc
系列文章
更多 icon
同类精品
更多 icon
我要评论
我的头像
精彩评论
继续加载