synchronized是Java的一个关键字,解决的是多个线程之间访问共享资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行,同时它还可以保证变量的内存可见性。本文将对sychronized的使用方法、sychronzied的特点以及底层原理做详细阐释。

只有使用synchronized时,其他线程等待锁时处于阻塞状态,其他Lock、AQS、LockSupport等线程都是处于等待状态。

一、使用

线程进入被synchonized修饰的区域时,需要先获取锁,若此时锁被其他线程持有,则当前线程处于阻塞状态等待锁的释放。synchronized 用于修饰代码块、类的实例方法 和 静态方法时,锁分别如下:

  • 修饰代码块时,锁是括号里的对象
  • 修饰静态方法时,锁是当前类的Class对象
  • 修饰实例方法时,锁是当前实例对象。

按照锁是类的实例对象还是类的Class对象将锁分为对象锁和类锁

1.1 对象锁

形式一:synchronized修饰实例方法,锁是当前类的实例对象

public class Test {
    public synchronized void Method1(){
        ...
    }
}

形式二:synchronized修饰代码块,锁是括号里的对象

public class Test {
    public void Method2(){
        synchronized (this){
           ...
        }
    }
}

线程进入synchronized修饰的实例方法或者代码块时,获取该对象的锁,若此对象的对象锁已经被其他线程占用,则需等待此锁被被释放再进入。

Java的所有对象都含有一把互斥锁,这个锁由JVM自动获取和释放。同一个类不同对象的锁之间互不干涉。

1.2 类锁

形式一:synchronized修饰静态方法,锁是当前类的Class对象

public class Test {
    public static synchronized void Method1(){
        ...
    }
}

形式二:synchronized修饰代码块,锁是括号里的Class对象

public class Test {
    public void Method2(){
        synchronized (Test.class){
           ...
        }
    }
}

一个静态方法被声明为synchronized,此类所有的实例化对象在调用此方法时,共用一把锁。

二、特点

2.1 原子性、可见性和有序性

synchronized保证原子性、可见性和有序性。

可见性说明:当释放锁时,所有写入都会写回内存;当获得锁时,会从内存中读取最新的数据

2.2 可重入性

对同一个线程在获得锁后,在调用其他需要同样锁的代码可以直接调用。示例如下:

public class Test{

    public synchronized void method1() {
        method2();
    }

    public synchronized void method2(){

    }
}

原理是JVM会记录锁的持有线程和持有数量。

调用synchronized代码时检查对象是否已经被锁,是则检查是否被当前线程锁定,若是计数加一,不是则将该线程加入等待队列。离开synchronized代码时计数减1,计数减到0时,释放锁

三、实现原理

3.1 synchronized实现原理

synchronized实现原理是获取到锁之后锁的计数器+1,同步代码执行完锁的计数器-1,如果获取失败就阻塞式等待锁的释放。同步代码的识别方式是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。

public class SynchronizedDemo {

    public static void main(String[] args) {
        method();
    }
    
    private static synchronized void method(){

    }
}

javap指令对上述代码进行反编译(部分信息省略)。从反编译的指令可以看到同步块是由monitorenter指令进入,然后monitorexit释放锁,在执行monitorenter之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器加1。当执行monitorexit指令时,锁的计数器也会减1。当获取锁失败时会被阻塞,一直等待锁被释放。

但是为什么会有两个monitorexit呢?其实第二个monitorexit是来处理异常的,仔细看反编译的字节码,正常情况下第一个monitorexit之后会执行goto指令,而该指令转向的就是23行的return,也就是说正常情况下只会执行第一个monitorexit释放锁,然后返回。而如果在执行中发生了异常,第二个monitorexit就起作用了,它是由编译器自动生成的,在发生异常时处理异常然后释放掉锁。

public void method();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class com/wangst/study/bingfa/SynchronizedDemo
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #3                  // Field i:I
         8: iconst_1
         9: iadd
        10: putstatic     #3                  // Field i:I
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return
      Exception table:
         from    to  target type
             5    15    18   any
            18    21    18   any
      LineNumberTable:
        line 7: 0
        line 8: 5
        line 9: 13
        line 10: 23
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      24     0  this   Lcom/wangst/study/bingfa/SynchronizedDemo;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 18
          locals = [ class com/wangst/study/bingfa/SynchronizedDemo, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

注:从网上查询资料得到 "对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。 对于同步代码块。JVM采用monitorentermonitorexit两个指令来实现同步",但是经测试,同步方法和同步代码块都是用monitorenter和moniorexit指令来实现的,没有出现ACC_SYNCHRONIZED标记符。我用的是JDK1.8,猜想与jdk版本有关,没有继续测试,请读者注意识别。可参考: https://www.hollischuang.com/archives/1883

3.2 锁的实现机制

前面讲了synchronized如何通过获取锁、释放锁来实现同步,接下来讲锁是如何实现的

java并发07-细说synchronized

对象头:对象头是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark WordClass Metadata Address组成。

  • Mark Word存储对象的hashCode、锁状态标志、线程持有的锁、GC分代年龄、偏向线程ID、偏向时间戳,
  • Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。

实例变量:存放类的属性数据信息,包括父类的属性信息
填充数据:由于虚拟机要求对象起始地址必须是 8 字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

当在对象上加锁时,数据是记录在对象头中。当执行 synchronized 同步方法或同步代码块时,会在对象头中记录锁标记,锁标记指向的是 monitor 对象(也称为管程或监视器锁) 的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。Monitor的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。

锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。

在 Java 虚拟机(HotSpot)中,monitor 是由 ObjectMonitor 实现的。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;  //锁计数器
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor 中有两个队列,_WaitSet _EntryList,以及_Owner 标记。其中_WaitSet 是用于管理等待队列(wait)线程的,_EntryList 是用于管理锁池阻塞线程的,_Owner 标记用于记录当前执行线程。线程状态图如下

java并发07-细说synchronized

当多线程并发访问同一个同步代码时,首先会进入_EntryList,当线程获取锁标记后,monitor中的_Owner 记录此线程,并在 monitor中的计数器执行递增计算(+1),代表锁定,其他线程在_EntryList 中继续阻塞。若执行线程调用 wait 方法,则 monitor 中的计数器执行赋值为 0 计算,并将_Owner 标记赋值为 null,代表放弃锁,执行线程进如_WaitSet 中阻塞。若执行线程调用 notify/notifyAll 方法,_WaitSet 中的线程被唤醒,进入_EntryList 中阻塞,等待获取锁标记。若执行线程的同步代码执行结束,同样会释放锁标记,monitor 中的_Owner 标记赋值为 null,且计数器赋值为 0 计算。

monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。

四、JVM对synchronized的优化

Java的开发团队一直在对synchronized优化,其中最大的一次优化就是在jdk6的时候,新增了两个锁状态,通过锁消除、锁粗化、自旋锁等方法使用各种场景,给synchronized性能带来了很大的提升。

4.1 锁膨胀

上面讲到锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。

4.1.1 偏向锁

一句话总结它的作用:减少同一线程获取锁的代价。在大多数情况下,锁不仅存在多线程竞争,而且总是由同一线程多次获得,为了让线程获取锁的代价更低就引入了偏向锁。偏向锁使用了一种等到竞争出现才释放锁的机制。当其他线程尝试竞争偏向锁时持有偏向锁的线程才会释放锁。

核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。

4.1.2 轻量级锁

轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。

4.1.3 重量级锁

重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。

重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。

4.2 锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。比如下面代码的method1和method2的执行效率是一样的,因为object锁是私有变量,不存在所得竞争关系。

public class SynchronizedDemo {
    public void method1(){
        Object object = new Object();
        synchronized (object){
            //执行同步代码
            System.out.println("hello world.");
        }
    }

    //优化后的方法,和上面method1执行效率一样
    public void method2(){
        Object object = new Object();
        System.out.println("hello world");
    }
}

4.3 锁粗化

锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。比如下面method3经过锁粗化优化之后就和method4执行效率一样了。

   public void method3(){
        for (int i=0; i<10000; ++i){
            synchronized (SynchronizedDemo.class){
                System.out.println("hello world");
            }
        }
    }
    
    //锁粗化,和上面执行效率一样
    public void method4(){
        synchronized (SynchronizedDemo.class){
            for (int i=0; i<10000; ++i){
                System.out.println("hello world");
            }
        }
    }

4.4 自旋锁与自适应自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。

自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

五、Synchronized原理流程图

java并发07-细说synchronized

参考资料

https://blog.csdn.net/weixin_36759405/article/details/83034386

https://www.cnblogs.com/haimishasha/p/10978848.html#autoid-2-0-0

https://www.hollischuang.com/archives/1883

https://blog.csdn.net/cx105200/article/details/80220937

https://cloud.tencent.com/developer/article/1465413