Synchronized底层实现

 

详细 参考

      https://cxyroad.blog.csdn.net/article/details/113777629

2.重量级锁

重量级锁是我们常说的传统意义上的锁,其利用操作系统底层的同步机制去实现Java中的线程同步。

重量级锁的状态下,对象的mark word为指向一个堆中monitor对象的指针。

一个monitor对象包括这么几个关键字段:cxq(下图中的ContentionList),EntryList ,WaitSet,owner。

其中cxq ,EntryList ,WaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。 

3.轻量级锁

JVM的开发者发现在很多情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。

线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建一个Lock Record,其包括一个用于存储对象头中的 mark word(官方称之为Displaced Mark Word)以及一个指向对象的指针。下图右边的部分就是一个Lock Record。

加锁过程:

1.在线程栈中创建一个Lock Record,将其obj(即上图的Object reference)字段指向锁对象。

2.直接通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。如果失败,进入到步骤3。

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分(Displaced Mark Word)为null,起到了一个重入计数器的作用。然后结束。

4.走到这一步说明发生了竞争,需要膨胀为重量级锁。

解锁过程:

1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。

2.如果Lock Record的Displaced Mark Word为null,代表这是一次重入,将obj设置为null后continue。

3.如果Lock Record的Displaced Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为Displaced Mark Word。如果成功,则continue,否则膨胀为重量级锁。

4.偏向锁

Java是支持多线程的语言,因此在很多二方包、基础库中为了保证代码在多线程的情况下也能正常运行,也就是我们常说的线程安全,都会加入如synchronized这样的同步语义。但是在应用在实际运行时,很可能只有一个线程会调用相关同步方法。比如下面这个demo:

  1.  
    import java.util.ArrayList;
  2.  
    import java.util.List;
  3.  
    public class SyncDemo1 {
  4.  
       public static void main(String[] args) {
  5.  
           SyncDemo1 syncDemo1 = new SyncDemo1();
  6.  
           for (int i = 0; i < 100; i++) {
  7.  
               syncDemo1.addString("test:" + i);
  8.  
           }
  9.  
       }
  10.  
       private List<String> list = new ArrayList<>();
  11.  
       public synchronized void addString(String s) {
  12.  
           list.add(s);
  13.  
       }
  14.  
    }

在这个demo中为了保证对list操纵时线程安全,对addString方法加了synchronized的修饰,但实际使用时却只有一个线程调用到该方法,对于轻量级锁而言,每次调用addString时,加锁解锁都有一个CAS操作;对于重量级锁而言,加锁也会有一个或多个CAS操作(这里的’一个‘、’多个‘数量词只是针对该demo,并不适用于所有场景)。

在JDK1.6中为了提高一个对象在一段很长的时间内都只被一个线程用做锁对象场景下的性能,引入了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只会执行几个简单的命令,而不是开销相对较大的CAS命令。我们来看看偏向锁是如何做的。

对象创建

当JVM启用了偏向锁模式(1.6以上默认开启),当新创建一个对象的时候,如果该对象所属的class没有关闭偏向锁模式(什么时候会关闭一个class的偏向模式下文会说,默认所有class的偏向模式都是是开启的),那新创建对象的mark word将是可偏向状态,此时mark word中的thread id(参见上文偏向状态下的mark word格式)为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。

加锁过程

case 1:当该对象第一次被线程获得锁的时候,发现是匿名偏向状态,则会用CAS指令,将mark word中的thread id由0改成当前线程Id。如果成功,则代表获得了偏向锁,继续执行同步块中的代码。否则,将偏向锁撤销,升级为轻量级锁。

case 2:当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后(细节见后面的文章),会往当前线程的栈中添加一条Displaced Mark Word为空的Lock Record中,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下,synchronized关键字带来的性能开销基本可以忽略。

case 3.当其他线程进入同步块时,发现已经有偏向的线程了,则会进入到撤销偏向锁的逻辑里,一般来说,会在safepoint中去查看偏向的线程是否还存活,如果存活且还在同步块中则将锁升级为轻量级锁,原偏向的线程继续拥有锁,当前线程则走入到锁升级的逻辑里;如果偏向的线程已经不存活或者不在同步块中,则将对象头的mark word改为无锁状态(unlocked),之后再升级为轻量级锁。

由此可见,偏向锁升级的时机为:当锁已经发生偏向后,只要有另一个线程尝试获得偏向锁,则该偏向锁就会升级成轻量级锁。当然这个说法不绝对,因为还有批量重偏向这一机制。

解锁过程

当有其他线程尝试获得锁时,是根据遍历偏向线程的lock record来确定该线程是否还在执行同步块中的代码。因此偏向锁的解锁很简单,仅仅将栈中的最近一条lock record的obj字段设置为null。需要注意的是,偏向锁的解锁步骤中并不会修改对象头中的thread id。

下图展示了锁状态的转换流程:

 

 

 

CAS

https://blog.csdn.net/qq_37171817/article/details/106122274

 

posted @ 2021-05-28 13:47  abcdefghijklmnop  阅读(41)  评论(0编辑  收藏  举报