【JUC】二十八、synchronized锁升级之偏向锁

文章目录

  • 1、偏向锁出现的背景
  • 2、从共享对象的内存结构看偏向锁
  • 3、偏向锁的持有
  • 4、启动偏向锁
  • 5、sleep暂停来启动偏向锁
  • 6、偏向锁的撤销
  • 7、总体流程
  • 8、SinceJava15 偏向锁的废除

1、偏向锁出现的背景

如果一个线程连续几次抢到锁,仍然重复加锁解锁,就会导致用户态和内核态频繁切换,这显然是有改进空间的。如之前买票的例子:

public class SaleTick {

    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        },"t1").start();

        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        },"t2").start();

        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        },"t3").start();
    }
}


//资源类
class Ticket {

    private int number = 50;
    Object lockObject = new Object();

    public void sale() {

        synchronized (lockObject) {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出票,剩余票数" + number--);
            }
        }
    }
}

发现一个线程一直在抢到锁:

Hotspot 的作者发现,大多数情况下:多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一个线程多次获得的情况,偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步代码块时提高性能。

偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也即偏向锁在资源没有竞争情况下消除了同步语句,懒的连CAS操作都不做了,直接提高程序性能。

举个例子:生活中,第一次去店铺A吃牛肉汤,老板会问你的口味,然后接下来几天,天天都去吃这家店,那老板以后看到来的是你,就不会再问了,直接给你按口味做就是了。

2、从共享对象的内存结构看偏向锁

从对象结构来看,偏向锁时,被锁对象请求头Mark word的前54位都存当前线程的指针,末尾的三位则改成了101,即代表偏向锁。

3、偏向锁的持有

当线程A第一次竞争到对象锁时,修改共享对象Mark Word里的偏向线程ID,在没有其他线程竞争的情况下,后续这个线程再进入这个同步代码块时,不需要再次加锁解锁,只需判断对象Mark Word里的ID是不是指向自己

  • 是,就直接执行,且直到有其他线程过来发生竞争才释放锁
  • 不是,说明发生了竞争,就尝试通过CAS修改Mark Word里的线程ID为自身ID

上面CAS时:

  • 如果修改成功,说明线程B来改时,之前偏向的线程A刚结束,此时,仍为偏向锁,偏向B
  • 如果修改失败,升级轻量锁,保证所有线程重新公平竞争

注意点:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的

偏向锁的操作不会直接捅到操作系统,不涉及用户到内核来回转换。以自定义的Account对象的对象头为例:

此时,线程A执行到synchronized同步代码块时,JVM通过CAS操作把线程指针ID记录到Account对象的Mark word 中,并修改偏向标识,线程A获得锁成功。注意,执行完同步代码块后,锁并未释放,等线程A二次来时,JVM判断account的Mark Word里面是否还有线程A的ID,有,就继续执行,因为之前没有释放锁,这里自然不用重新获取锁,也就不涉及用户态和内核态的来回切换

4、启动偏向锁

终端执行以下,查看偏向锁的配置信息:

java -XX:+PrintFlagsInitial | grep BiasedLock*

可以看到偏向锁默认打开,以及启动偏向锁的延迟时长(默认延迟4秒,我这里JDK版本较高,不是4)

写实例Demo:

可以看到只有一个线程在操作对象o ⇒ 应该是偏向锁 ⇒ 却发现是轻量锁000

这是因为偏向锁延时4秒开启,期间自然是下一级:轻量锁。

偏向锁在JDK1.6之后就默认开启,但启动时间有延迟,想立刻启动,可通过添加JVM参数将延迟改为0:

  • -XX:+UseBiasedLocking 开启偏向锁
  • -XX:-UseBiasedLocking 关闭偏向锁,此时会直接跳入轻量锁
  • -XX:BiasedLockingStartupDelay=0 关闭延迟

添加JVM参数,这里关闭延时,正常显示101,即偏向锁:

5、sleep暂停来启动偏向锁

除了以上添加JVM参数关闭延时来立刻启动偏向锁,也可通过另一种方式:程序执行前等4秒,以保证开启了偏向锁

再对比下,偏向锁开启后,使用synchronized锁时的对象o和不使用synchronized时的对象o的区别:

可以看到二者锁状态均为101,但前面o对象未使用synchronized锁,所以线程ID为空,而后者则带了线程ID。

6、偏向锁的撤销

共享对象o的Mark Word一直指向线程A的ID,线程A也一直拿着这个对象锁。直到第二个线程开始来抢夺锁时,线程A的好日子结束:

  • 偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。

  • 且撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行(客人还在你店里吃饭,你总不能一到打烊时机就掀桌子)

  • 如果第一个线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级(升级为轻量锁),且此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁

  • 第一个线程执行刚好完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向。直白说就是,另一线程t2来竞争时,偏向的线程t1刚好执行完,那大家就重新竞争。当然,也有可能t1出代码块后,run方法结束,直接走了,那就偏向t2就行

7、总体流程

8、SinceJava15 偏向锁的废除

JDK15:Disable and Deprecate Biased Locking.
//2020.9.15

Prior to JDK 15, biased locking is always enabled and available. With this JEP, biased locking will no longer be enabled when HotSpot is started unless -XX:+UseBiasedLocking is set on the command line.

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2023年12月26日
下一篇 2023年12月26日

相关推荐