无锁、偏向锁、轻量级锁、重量级锁

无锁、偏向锁、轻量级锁、重量级锁

1、偏向锁、轻量级锁、重量级锁适用于不同的并发场景

  • 偏向锁:无实际的锁竞争,且将来只有第一个申请锁的线程会使用锁。偏向锁只有初始化时需要一次CAS
  • 轻量级锁:无实际的锁竞争,多个线程交替使用锁,允许短时间的锁竞争。轻量级锁每次申请、释放锁都至少需要一次CAS
  • 重量级锁:有实际的锁竞争,且锁竞争时间长。

2、锁升级、撤销的流程图

3、内置锁和显示锁

内置锁:也就是Synchronized锁

  • 由Synchronized实现的内置锁才能锁升级,从偏向锁-轻量级锁-重量级锁
  • 执行完同步代码,会自动的释放锁
  • 基于JVM实现,可以对Synchronized锁进行增加锁粒度和减低锁粒度(修饰实例方法、修饰静态方法、修饰代码块
  • 相对显示锁来说,内置锁还是过重,因为内置锁是一个互斥锁,不仅读写互斥并且读读也互斥,最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁。
  • synchronized关键字不能继承,父类方法中加了synchronized,在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,需要重新加锁。
  • 内置锁是非公平锁,线程在竞争synchronized锁时并不遵循先到等待队列就先获得锁,如果一个线程来请求锁,刚好该锁被释放了,那么这个线程可能会跳过在等待队列中的其它线程直接获得该锁。
  • 内置锁是可重入锁,如果已经获取了一个锁对象,在还没释放时又要执行该锁对象的另一个代码块或方法,则不需要再次给这个对象加锁就可以直接执行。
  • sychronized 作用于实例方法时,锁对象是this
  • sychronized 作用于静态方法时,锁对象是Class对象
  • sychronized 作用于代码块时,锁对象是sychronized(obj)中的obj

显示锁:ReentrantLock,必须手动的释放锁

4、内置锁与显示锁的区别:

1、锁的释放
显示锁必须调用unlock方法才能释放锁,内置锁只要运行到同步代码块之外就会释放锁
2、公平性
显示锁可以指定公平策略,默认为不公平锁
内置锁不可以选择公平策略,只能是不公平锁
3、可中断申请
显示锁提供可中断申请(lock.lockInterruptibly();可中断申请, 在申请锁的过程中如果当前线程被中断, 将抛出InterruptedException异常)
内置锁不可中断。(在申请锁时被其它线程持有,那么当前线程后挂起,挂起其间不可中断)
4、可尝试申请、可定时申请
显示锁提供尝试型申请方法(Lock.tryLock和Lock.tryLock(long time, TimeUnit unit)),
内置锁不提供这种特性
5、是否可以精确唤醒特定线程
显示锁可以通过Condition对象(由显示锁派生出来),调用Condition.singal或Condition.singalAll方法可以唤醒在该Condition对象上等待的线程。以此来唤醒指定线程。
内置锁的notify或notifyAll方法唤醒在其上等待的线程,但无法指定特定线程。
总结,内置锁够解决大部分需要的场景,只有在需要额外的灵活时,比如公平、可中断、可尝试、可定时、可唤醒特定线程时,我们才考虑用显示锁。
6、偏向锁

img

流程讲解

当JVM启用了偏向锁模式(JDK6以上默认开启),新创建对象的Mark Word中的Thread Id为0,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。

偏向锁逻辑
1.线程A第一次访问同步块时,先检测对象头Mark Word中的标志位是否为01,依此判断此时对象锁是否处于无所状态或者偏向锁状态(匿名偏向锁);

2.然后判断偏向锁标志位是否为1,如果不是,则进入轻量级锁逻辑(使用CAS竞争锁),如果是,则进入下一步流程;

3.判断是偏向锁时,检查对象头Mark Word中记录的Thread Id是否是当前线程ID,如果是,则表明当前线程已经获得对象锁,以后该线程进入同步块时,不需要CAS进行加锁,只会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,用来统计重入的次数(如图为当对象所处于偏向锁时,当前线程重入3次,线程栈帧中Lock Record记录)。

偏向锁重入

退出同步块释放偏向锁时,则依次删除对应Lock Record,但是不会修改对象头中的Thread Id;

注:偏向锁撤销是指在获取偏向锁的过程中因不满足条件导致要将锁对象改为非偏向锁状态,而偏向锁释放是指退出同步块时的过程。

4.如果对象头Mark Word中Thread Id不是当前线程ID,则进行CAS操作,企图将当前线程ID替换进Mark Word。如果当前对象锁状态处于匿名偏向锁状态(可偏向未锁定),则会替换成功(将Mark Word中的Thread id由匿名0改成当前线程ID,在当前线程栈中找到内存地址最高的可用Lock Record,将线程ID存入),获取到锁,执行同步代码块;

5.如果对象锁已经被其他线程占用,则会替换失败,开始进行偏向锁撤销,这也是偏向锁的特点,一旦出现线程竞争,就会撤销偏向锁;

6.偏向锁的撤销需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的),暂停持有偏向锁的线程,检查持有偏向锁的线程状态(遍历当前JVM的所有线程,如果能找到,则说明偏向的线程还存活),如果线程还存活,则检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁;

注:每次进入同步块(即执行monitorenter)的时候都会以从高往低的顺序在栈中找到第一个可用的Lock Record,并设置偏向线程ID;每次解锁(即执行monitorexit)的时候都会从最低的一个Lock Record移除。所以如果能找到对应的Lock Record说明偏向的线程还在执行同步代码块中的代码。

7.如果持有偏向锁的线程未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向,如果不允许重偏向,则撤销偏向锁,将Mark Word设置为无锁状态(未锁定不可偏向状态),然后升级为轻量级锁,进行CAS竞争锁;

8.如果允许重偏向,设置为匿名偏向锁状态,CAS将偏向锁重新指向线程A(在对象头和线程栈帧的锁记录中存储当前线程ID);

9.唤醒暂停的线程,从安全点继续执行代码。

以上便是偏向锁的整个逻辑了。

5、MarkWord的结构

image-20211118192225123

6、批量重偏向和批量撤销

1)、批量重偏向与批量撤销

渊源:从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。
于是,就有了批量重偏向与批量撤销的机制。

撤销是偏向锁升级为轻量级锁或者是重量级锁的意思

2)、解决场景

批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。撤销了20次了,就要重新偏向其他线程了
批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。撤销了40了,可能已经不适合偏向锁了,所以就直接升级为轻量级锁了

3)、原理

以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。
每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。
每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。
当达到重偏向阈值后,假设该class计数器继续增长,当其达到
批量撤销的阈值后(默认40)
,JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

4)、代码实现

前提是关闭延迟加载:开启偏向延迟,-XX:BiasedLockingStartupDelay=0

4.1)、批量重偏向
package com.robin.demospring.test;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

public class A {
   

}

package com.robin.demospring.test;

import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.LockSupport;

@Slf4j
public class Test1 {
   
    static Thread t1;
    static Thread t2;
    static int loopFlag = 20;

    public static void main(String[] args) {
   
        final List<A> list = new ArrayList<>();
        t1 = new Thread() {
   
            @Override
            public void run() {
   
                for (int i = 0; i < loopFlag; i++) {
   
                    A a = new A();
                    list.add(a);
                    log.debug("加锁前" + i + " " + ClassLayout.parseInstance(a).toPrintable<
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无敌的黑星星

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值