✨Synchronized底层实现---概述

参考大佬:GitHub

概论

Synchronized简介

java中提供了两种实现同步的基础语义:Synchronized方法和Synchornized方法块

Demo:

public class SyncTest {
    public void syncBlock(){
        synchronized (this){
            System.out.println("hello block");
        }
    }
    public synchronized void syncMethod(){
        System.out.println("hello method");
    }
}

当SyncTest.java被编译成class文件时,synchronized关键字和sunchronized方法的字节码文件略有不同,我们可以用javap -v命令查看class文件对应的JVM字节码信息,部分信息如下:

{
  public void syncBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter                      // monitorenter指令进入同步块
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String hello block
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit                       // monitorexit指令退出同步块
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit                       // monitorexit指令退出同步块
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
 i

  public synchronized void syncMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED      //添加了ACC_SYNCHRONIZED标记
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String hello method
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
 
}

对于Synchronized关键字而言。在Java编译的过程中,会生成对应的monitorenter和monitorexit指令,分别对应代码块的进入和退出,有两个monitorexit的原因是为了保证抛出异常的情况下也能释放锁,所以Java为代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。而对于synichronized方法而言,javac为其生成了一个ACC_SYNCHRONIZED关键字,在JVM进行方法调用时,发现被调用的方法被ACC_SYNCHRONIZED修饰则会优先尝试获得锁。

在jvm底层中,对于这两种方法实现的原理大致相同。

锁的几种形式

传统是锁依赖于系统的同步函数,在Linux上使用mutex互斥锁,最底层是实现依赖于futex函数,这些函数都依赖于用户态和内核态的切换,进程的上下文切换,成本较高,对于加了synchronized关键字但是但是运行的时候并没有线程竞争或者两个线程接近于交替执行的状态,传统的锁机制效率就会低好多。

在jdk1.6之前,synchronized只有传统的锁机制,因此给开发者留下了synchronized关键字相对于其他同步机制性能不好的印象。

在jdk1.6之后,synchronized引入了两种新型的轻量级锁:偏向锁和轻量级锁,他们的引入是为了解决没有多线程竞争的情况下,或者基本没有竞争的情况下因为使用传统的锁机制带来的性能开销。

首先,我们先了解一下实现多种锁机制的基础:对象头

对象头

因为在Java中任意对象都可以用作锁,因此必定要有一个映射关系,存储该对象和其对应的锁信息。比如当前那个线程持有锁,那些线程在等待。首先第一种思路:用一个全局Map来记录这些映射关系,但是这样也会有线程安全问题,并且当当对象比较多的时候,文件就会格外的大。

第二种思路就是把这个映射关系保存在对象头里,对于普通对象而言,其对象头中有两类信息。因为对象头中也有一些hashcode,GC相关的数据,所以如果锁信息能够和这些信息共存的话就好了。

在JVM中,对象在内存中除了本身的数据外还会有个对象头。对于普通的对象,对象头中有两种类型的信息:markword和类型指针,另外,数组类型的还会额外存在一份记录数组长度的数据。

其中,类型指针是指向该对象所属对象的指针,markword用于存储对象的hashcode,GC分代年龄,锁状态等信息。在32位系统中markword长度为32字节,64位的系统markword长度为64位,为了能在有限的空间中存下更多的数据,他们的存储格式是不固定的。

可以看到锁信息也是存在于对象的markword中的。当对象的状态为偏向锁时,markword存储的是偏向的线程ID;当状态为轻量级锁的时候,指向的是栈中的锁记录(Lock Record),当状态为重量级锁时,指向的堆中的monitor对象的指针。

重量级锁

重量级锁是我们常说的传统意义上的锁,其利用操作系统底层的同步机制去实现Java中的线程同步。重量级锁的状态下,对象的markword为指向一个堆中monitor对象的指针。

一个monitor对象包括这几个关键字段:cxq,EntryList,WaitSet,owner

具体的实现步骤:

  1. 当一个线程尝试获得锁时如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到cxq的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将cxq中的元素移动到EntryList中去,并唤醒EntryList的队首线程。

  2. 如果一个线程在同步块中调用了ObjectWait方法,会将该线程对应的ObjectWaiter从EntryList移除并加入到WaitSet中,然后释放锁。当wait的先昵称被notify之后,会将对应的ObjectWaiter从WaitSet移动到EntryList中。

posted @ 2020-07-28 17:23  两小无猜  阅读(164)  评论(0编辑  收藏  举报