Java内部锁 synchronized详解(偏向锁和轻量级锁)

3 篇文章 0 订阅


JDK6,研究人员引入了偏向锁和轻量级锁,因为Sun 程序员发现大部分程序大多数时间都不会发生多个线程同时访问竞态资源的情况,每次线程都加锁解锁,每次这么搞都要操作系统在用户态和内核态之前来回切,太耗性能了

一、偏向锁

-XX:+UseBiasedLocking 开启偏向锁
-XX:-UseBiasedLocking 关闭偏向锁

Java 对象头

锁的类型和状态和对象头的Mark Word息息相关
对象存储在堆中,主要分为三部分内容,对象头、对象实例数据和对齐填充(数组对象多一个区域:记录数组长度)。

Mard Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
Klass Word 存储指向对象所属类(元数据)的指针,JVM通过这个确定这个对象属于哪个类

Monitor

每个对象都有一个与之关联的 Monitor 对象;

1、JDK 6 以前 synchronized具体实现逻辑:

1、当有二个线程A、线程B都要进行操作的时候 ,发现方法上加了synchronized锁,这时线程调度到A线程执行,A线程就抢先拿到了锁。拿到锁的步骤为:

  • 1.1 将 MonitorObject 中的 _owner设置成 A线程;
  • 1.2 将 mark word 设置为 Monitor 对象地址,锁标志位改为10;
  • 1.3 将B线程阻塞放到 ContentionList 队列;

2、JVM 每次从Waiting Queue 的尾部取出一个线程放到OnDeck作为候选者,但是如果并发比较高,Waiting Queue会被大量线程执行CAS操作,为了降低对尾部元素的竞争,将Waiting Queue 拆分成ContentionList 和 EntryList 二个队列, JVM将一部分线程移到EntryList 作为准备进OnDeck的预备线程。另外说明几点:

  • 1、所有请求锁的线程首先被放在ContentionList这个竞争队列中;
  • 2、Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
  • 3、任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
  • 4、当前已经获取到所资源的线程被称为 Owner;
  • 5、处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的);

3、作为Owner 的A 线程执行过程中,可能调用wait 释放锁,这个时候A线程进入 Wait Set , 等待被唤醒。

2、由于 synchronized 重量级锁有以下二个问题,:

1、依赖底层操作系统的 mutex 相关指令实现,加锁解锁需要在用户态和内核态之间切换,性能损耗非常明显。

2、研究人员发现,大多数对象的加锁和解锁都是在特定的线程中完成。也就是出现线程竞争锁的情况概率比较低。他们做了一个实验,找了一些典型的软件,测试同一个线程加锁解锁的重复率,如下图所示,可以看到重复加锁比例非常高。早期JVM
有 19% 的执行时间浪费在锁上。

3、因此JDK 6 之后做了改进,引入了偏向锁和轻量级锁:

1、首先A 线程访问同步代码块,使用CAS 操作将 Thread ID 放到 Mark Word 当中;
2、如果CAS 成功,此时线程A就获取了锁
3、如果线程CAS 失败,证明有别的线程持有锁,线程B 来CAS 就失败的,这个时候启动偏向锁撤销 (revoke bias);
4、锁撤销流程:

  • 让 A线程在全局安全点阻塞(类似于GC前线程在安全点阻塞);
  • 遍历线程栈,查看是否有被锁对象的锁记录(Lock Record),如果有Lock Record,需要修复锁记录和Markword,使其变成无锁状态;
  • 恢复A线程 ;
  • 将是否为偏向锁状态置为 0 ,开始进行轻量级加锁流程;

5、当锁升级为轻量级锁之后,如果依然有新线程过来竞争锁,首先新线程会自旋尝试获取锁,尝试到一定次数(默认10次)依然没有拿到,锁就会升级成重量级锁。

如果确定竞态资源会被高并发的访问,建议通过 -XX:-UseBiasedLocking 参数关闭偏向锁,偏向锁的好处是并发度很低的情况下,同一个线程获取锁不需要内存拷贝的操作,免去了轻量级锁的在线程栈中建Lock Record,拷贝Mark Down的内容,也免了重量级锁的底层操作系统用户态到内核态的切换,因为前面说了,需要使用系统指令。另外Hotspot 也做了另一项优化,基于锁对象的epoch 批量偏移和批量撤销偏移,这样大大降低了偏向锁的CAS和锁撤销带来的损耗

二、synchronized锁状态

1、无锁

无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点是修改操作会在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。

2、偏向锁

引入偏向锁的主要目的是:为了在无多线程竞争的情况下尽量减少不必须要的轻量级锁执行路径。
其实在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获取,所以引入偏向锁就可以减少很多不必要的性能开销和上下文切换。

偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。

当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。

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

3、轻量级锁

引入轻量级锁的主要目的是:在多线程竞争不激烈的前提下,通过自旋减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
需要注意的是轻量级锁并不是取代重量级锁,而是在大多数情况下同步块并不会出现严重的竞争情况,所以引入轻量级锁可以减少重量级锁对线程的阻塞带来的开销。

所以偏向锁是认为环境中不存在竞争情况,而轻量级锁则是认为环境中竞争不激烈,所以轻量级锁一般都只会有少数几个线程竞争锁对象,其他线程只需要稍微等待(自旋)下就可以获取锁,但是自旋次数有限制,如果自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

4、重量级锁

重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源,导致性能低下。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值