Synchronized的底层实现

1.关于monitorenter和monitorexit的作用:

        我们可以抽象的理解为每个做对象拥有一个计数器和一个指向持有该锁的线程指针。

        当执行monitorenter时,如果目标锁对象的计数器为0,那么说明它没有被其他线程所持有。在这个情况下,Java虚拟机会将该锁的持有线程设置为当前线程,并且将其计数器加1.

        在目标锁对象计数器不为0的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

        当执行monitorexit时,Java虚拟机则需将对象的计数器减1.当计数器为0时,那便代表该锁已经被释放掉了。

 

2.Synchronized的优化

  ---【在同一时刻只有一个线程能够获得对象的监视器,从而进入到同步代码块或者同步方法中,即表现为互斥

2.1 锁优化的前置条件:

                 CAS,Java对象头

        2.1.1 什么是cas?

         使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁的策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。无锁操作时使用CAS(又叫做比较交换)来鉴别线程是否出现冲突,出现冲突就重试当前操作只知道没有冲突为止。

       2.1.2 CAS的操作过程

        CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V内存地址存放的实际值;O预期的值(旧值);N更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最先的值了,自然而然可以将N赋值给V,反之,V和O不同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作

一个变量时,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新啊尝试,当然也可以选择挂起线程。

        CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用吹器提供的XMPXCHG指令实现。

        元老级的synchronized(未优化)前最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒带来的性能问题,因为这是一种互斥同步。而CAS必能不是五段的将线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫作非阻塞同步。这是两者主要的区别。

        2.1.3 CAS的问题:

       1) ABA问题

因为CAS会检查旧值有没有变化,这里存在这样一个由意思的问题。比如一个就只A变为了B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。在JDK1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路就是这样的

        2)自旋会浪费大量的处理资源。

与线程阻塞相比,自旋会浪费大量的处理器资源。这是因为当前线程仍处于运行状况,只不过跑的是无用的指令。它期望在运行无用指令的过程中,锁能够被释放出来。

        举个例子:

        我们可以用等红绿灯作为例子。Java线程的阻塞相当于熄火停车,而自旋状态相当于怠速停车如果红灯的等待时间非常长,那么熄火停车相对省油一些;如果红灯的等待时间非常短,比如我们在同步代码块中只做了一个整型加法,那么在短时间内锁肯定会被释放出来,因此怠速停车更合适。

  然而,对于JVM来说,它并不能看到红灯的剩余时间,也就没法根据等待时间的长短来选择是自旋还是阻塞。JVM给出的该方案是自适应自旋,根据以往自选等待时能否获取锁,来动态调整自旋的时间(循环数)

        就我们的例子来说,如果之前不变熄火等待了苦等,那么这次不熄火的时间就长一点;如果之前不熄火没等待绿灯,那么这次不熄火的时间短一点。

        3)公平性

        自旋状态还带来另外一个副作用,不公平的锁机制。处于阻塞状态的线程,无法立刻竞争被释放的锁。然而,处于自旋状态的线程,则很有可能有限获得这把锁

    2.2.1 Java对象头

        在同步的时候是获取对象的monitor,即获取到对象的锁。那么对象的锁怎们理解?无非就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头,Java对象头里的MarkWord里默认存放的对象的HashCode,分代年龄和所标记位。32位JVM Mark Word默认存储结构为:

锁状态                     25bit           4bit                  1bit是否是偏向锁    2bit锁标志位

无锁状态   对象的hashCode     对象分代年龄        0                                        01

  如图在MarkWord会默认村反复hashcode,年龄值以及锁标志位等信息。Java SE 1.6中,锁一共有4中状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况主键升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。对象的MarkWord变化为:

00:轻量级锁

10:重量级锁

11:GC标记

01:偏向锁

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

2.2.1偏向锁:

1)偏向锁的引入:

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是有同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

偏向锁是四种状态中最乐观的一种锁:从始至终只有一个线程请求某一把锁。

2)偏向锁的获取:

        当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单的测试一下对象头的MarkWord里是否存储着指向当前线程的偏向锁。如果测试成功,比闹事线程已经获得无锁。如果测试失败,则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁:如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

3)偏向锁的撤销:

        偏向锁使用了一种等到竞争出现才释放锁的机制,所以其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

      偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在指向的字节码)。他会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的MarkWord要么重新偏向于其他线程,要么恢复到无锁或者所标记对象不适合作为偏向锁,最后唤醒暂停的线程。

4)偏向锁的关闭:

        偏向锁在JDK6之后是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartRelay=0.如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

 

2.2.3 轻量级锁:

        ·举个例子:

        深夜的十字路口,四个方向都闪黄灯的情况。由于深夜十字路口的车辆来往可能比较少,如果还设置红绿灯交替,那么很有可能出现四个方向仅有一辆车在等红灯的情况。因此,红绿灯可能被设置为闪黄灯的情况,代表车辆可以自由通过,但是司机需要注意观察。

JVM也存在类似的情况:

        多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。针对这种情况,JVM采用了轻量级锁,来避免线程的阻塞以及唤醒。

1)加锁:

线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord复制到锁记录中,官方称为Displaced Mark Word.然后线程尝试使用CAS将对象头中的MarkWord替换替换为指向锁标记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

2)解锁

轻量级解锁时,会使用原子的CAS操作静Displaced MarkWord替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁

2.2.4重量级锁:

        重量级锁是JVM中最为基础的锁实现。在这种状态下,JVM虚拟机会阻塞加锁失败的线程,并且在目标锁被脂肪的时候,唤醒这些线程。

        Java线程的阻塞以及唤醒,都是依靠操作系统来完成的。举例来说,对于符合POSIX接口的操作系统,上述操作通过平pthread的互斥锁(mutex)来实现的。此外,这些操作将涉及系统调用,需要从操作系统的用户态切换至内核态,其开销非常之大。

        为了尽量避免昂贵的线程阻塞、唤醒操作,JVM会在线程进入阻塞状态之前,以及被唤醒之后竞争不到锁的情况下,进入自旋状态,在处理器上空跑并且轮询锁是否被释放。如果此时所恰好被释放了,那么当前线程便无须进入阻塞状态,而是直接获得这把锁。

        总结:

        Java虚拟机中synchronized关键字的实现,按照代价有高到底可以分为重量级锁、轻量级锁和偏向锁三种。

  1. 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。JVM采用了自适应自旋,来避免线程在面对非常小的synchronized代码块时,仍会被阻塞、唤醒的情况。
  2. 轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原来的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
  3. 偏向锁只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录当前线程的地址,在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。
  4. 其他优化:
  1. 锁粗化:

锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次。将多个连续的锁扩展成为一个范围更大的锁。

锁消除:

锁消除即删除必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。

-----------------------------------------------------------未完待续。。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值