Java synchronized

synchronized是Java语言里互斥锁的一种实现。解决了原子性、可见性的问题。

synchronized 是基于底层操作系统的 Mutex Lock 实现的,每次获取和释放锁操作都会带来用户态和内核态的切换,从而增加系统性能开销。因此,在锁竞争激烈的情况下,synchronized 同步锁在性能上就表现得非常糟糕,它也常被大家称为重量级锁。

synchronized用法

synchronized 实现同步锁的方式有两种,一种是修饰方法,一种是修饰方法块。以下就是通过 Synchronized 实现的两种同步方法加锁的方式:

public class SynchronizedTest {

    public static void main(String[] args){

    }

    /**
     * 同步实例方法,锁实例对象
     */
    public synchronized void test() {
    }

    /**
     * 同步类方法,锁类对象
     */
    public synchronized static void test1() {
    }

    /**
     * 同步代码块
     */
    public void test2() {
        // 锁类对象
        synchronized (SynchronizedTest.class) {
            // 锁实例对象
            synchronized (this) {

            }
        }
    }

}
javac  SynchronizedTest.java  //先运行编译class文件命令

javap -v SynchronizedTest.class //再通过javap打印出字节文件
[weikeqin@weikeqin sync ]$javap -v SynchronizedTest.class
Classfile /Users/weikeqin/WorkSpaces/wkq/java-study/src/main/java/cn/wkq/java/juc/sync/SynchronizedTest.class
  Last modified Dec 19, 2021; size 580 bytes
  MD5 checksum 437d370a7051efd6bc8c94390116abce
  Compiled from "SynchronizedTest.java"
public class SynchronizedTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#19         // java/lang/Object."<init>":()V
   #2 = Class              #20            // SynchronizedTest
   #3 = Class              #21            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               main
   #9 = Utf8               ([Ljava/lang/String;)V
  #10 = Utf8               test
  #11 = Utf8               test1
  #12 = Utf8               test2
  #13 = Utf8               StackMapTable
  #14 = Class              #20            // SynchronizedTest
  #15 = Class              #21            // java/lang/Object
  #16 = Class              #22            // java/lang/Throwable
  #17 = Utf8               SourceFile
  #18 = Utf8               SynchronizedTest.java
  #19 = NameAndType        #4:#5          // "<init>":()V
  #20 = Utf8               SynchronizedTest
  #21 = Utf8               java/lang/Object
  #22 = Utf8               java/lang/Throwable
{
  public SynchronizedTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 11: 0

  public synchronized void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 17: 0

  public static synchronized void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 23: 0

  public void test2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=5, args_size=1
         0: ldc           #2                  // class SynchronizedTest
         2: dup
         3: astore_1
         4: monitorenter
         5: aload_0
         6: dup
         7: astore_2
         8: monitorenter
         9: aload_2
        10: monitorexit
        11: goto          19
        14: astore_3
        15: aload_2
        16: monitorexit
        17: aload_3
        18: athrow
        19: aload_1
        20: monitorexit
        21: goto          31
        24: astore        4
        26: aload_1
        27: monitorexit
        28: aload         4
        30: athrow
        31: return
      Exception table:
         from    to  target type
             9    11    14   any
            14    17    14   any
             5    21    24   any
            24    28    24   any
      LineNumberTable:
        line 30: 0
        line 32: 5
        line 34: 9
        line 35: 19
        line 36: 31
      StackMapTable: number_of_entries = 4
        frame_type = 255 /* full_frame */
          offset_delta = 14
          locals = [ class SynchronizedTest, class java/lang/Object, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
        frame_type = 68 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 6
}
SourceFile: "SynchronizedTest.java"
[weikeqin@weikeqin sync ]$

通过输出的字节码,你会发现:Synchronized 在修饰同步代码块时,是由 monitorenter 和 monitorexit 指令来实现同步的。

JVM 使用了 ACC_SYNCHRONIZED 访问标志来区分一个方法是否是同步方法。当方法调用时,调用指令将会检查该方法是否被设置 ACC_SYNCHRONIZED 访问标志。如果设置了该标志,执行线程将先持有 Monitor 对象,然后再执行方法。在该方法运行期间,其它线程将无法获取到该 Mointor 对象,当方法执行完成后,再释放该 Monitor 对象。

synchronized 结构

在 JDK1.6 JVM 中,对象实例在堆内存中被分为了三个部分:对象头、实例数据和对齐填充。
object

其中 Java 对象头由 Mark Word、指向类的指针以及数组长度三部分组成。

Mark Word 记录了对象和锁有关的信息。Mark Word 在 64 位 JVM 中的长度是 64bit,我们可以一起看下 64 位 JVM 的存储结构是怎么样的。

object_markword

JVM 中的同步是基于进入和退出管程(Monitor)对象实现的。每个对象实例都会有一个 Monitor,Monitor 可以和对象一起创建、销毁。Monitor 是由 ObjectMonitor 实现,而 ObjectMonitor 是由 C++ 的 ObjectMonitor.hpp 文件实现,如下所示:

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 ;
}

1、_owner:初始时为 NULL。当有线程占有该 monitor 时 owner 标记为该线程的 ID。当线程释放 monitor 时 owner 恢复为 NULL。owner 是一个临界资源 JVM 是通过 CAS 操作来保证其线程安全的。
2、_cxq:竞争队列所有请求锁的线程首先会被放在这个队列中(单向)。_cxq 是一个临界资源 JVM 通过 CAS 原子指令来修改_cxq 队列。
3、每当有新来的节点入队,它的 next 指针总是指向之前队列的头节点,而_cxq 指针会指向该新入队的节点,所以是后来居上。
4、_EntryList: _cxq 队列中有资格成为候选资源的线程会被移动到该队列中。
5、_WaitSet: 等待队列因为调用 wait 方法而被阻塞的线程会被放在该队列中。

Monitor 是依赖于底层的操作系统实现,存在用户态与内核态之间的切换,所以增加了性能开销。

队列协作流程图

队列协作流程图


monitor 竞争过程

1、通过 CAS 尝试把 monitor 的 owner 字段设置为当前线程。
2、如果设置之前的 owner 指向当前线程,说明当前线程再次进入 monitor,即重入锁执行 recursions ++ , 记录重入的次数。
3、如果当前线程是第一次进入该 monitor, 设置 recursions 为 1,_owner 为当前线程,该线程成功获得锁并返回。
4、如果获取锁失败,则等待锁的释放。

锁升级优化

为了提升性能,JDK1.6 引入了偏向锁、轻量级锁、重量级锁概念,来减少锁竞争带来的上下文切换,而正是新增的 Java 对象头实现了锁升级功能。
当 Java 对象被 Synchronized 关键字修饰成为同步锁后,围绕这个锁的一系列升级操作都将和 Java 对象头有关。

Java 对象头
在 JDK1.6 JVM 中,对象实例在堆内存中被分为了三个部分:对象头、实例数据和对齐填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组长度三部分组成。
Mark Word 记录了对象和锁有关的信息。Mark Word 在 64 位 JVM 中的长度是 64bit,我们可以一起看下 64 位 JVM 的存储结构是怎么样的。如下图所示:

偏向锁

获取偏向锁流程

当线程访问同步块并获取锁时处理流程如下:
1、检查 mark word 的线程 id 。
2、如果为空则设置 CAS 替换当前线程 id。如果替换成功则获取锁成功,如果失败则撤销偏向锁。
3、如果不为空则检查(对象头的 Mark Word 中去判断一下是否有偏向锁指向它的 ID) 线程 id为是否为本线程。如果是则获取锁成功(无需再进入 Monitor 去竞争对象),如果失败则撤销偏向锁。
持有偏向锁的线程以后每次进入这个锁相关的同步块时,只需比对一下 mark word 的线程 id 是否为本线程,如果是则获取锁成功。

如果发生线程竞争发生, 2、3 步失败的情况则需要撤销偏向锁。

偏向锁撤销流程
一旦出现其它线程竞争锁资源时,偏向锁就会被撤销。

1、偏向锁的撤销动作必须等待全局安全点
2、暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
3、撤销偏向锁恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态

优点

只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。

减少了获取锁和释放锁的开销。获取锁或释放锁每次操作都会发生用户态与内核态的切换。并且要进入 Monitor 去竞争对象。

比如:在for循环里加锁并count++

缺点

如果存在竞争会带来额外的锁撤销操作。


轻量级锁

当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头 Mark Word 中的线程 ID 不是自己的线程 ID,就会进行 CAS 操作获取锁,如果获取成功,直接替换 Mark Word 中的线程 ID 为自己的 ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。

轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争。

自旋锁

轻量级锁 CAS 抢锁失败,线程将会被挂起进入阻塞状态。如果正在持有锁的线程在很短的时间内释放资源,那么进入阻塞状态的线程无疑又要申请锁资源。
JVM 提供了一种自旋锁,可以通过自旋方式不断尝试获取锁,从而避免线程被挂起阻塞。这是基于大多数情况下,线程持有锁的时间都不会太长,毕竟线程被挂起阻塞可能会得不偿失。
从 JDK1.7 开始,自旋锁默认启用,自旋次数由 JVM 设置决定。

重量级锁

自旋锁重试之后如果抢锁依然失败,同步锁就会升级至重量级锁,锁标志位改为 10。在这个状态下,未抢到锁的线程都会进入 Monitor,之后会被阻塞在 _WaitSet 队列中。

锁消除

编译器在编译这个同步块的时候不会生成 synchronized 所表示的锁的申请与释放的机器码,即消除了锁的使用。

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

public class SynchronizedTest {

    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();

        for (int i = 0; i < 100000000; i++) {
            test.append("abc", "def");
        }
    }

    public void append(String str1, String str2) {
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }
}

锁粗化

锁粗化同理,虚拟机如果发现几个相邻的同步块使用的是同一个锁实例,那么虚拟机会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁”所带来的性能开销。

如果虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

public class StringBufferTest {
    StringBuffer stringBuffer = new StringBuffer();

    public void append(){
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");
    }
}

这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

synchronized使用案例

交替打印奇偶数


public class SynchronizedPrintEvenOdd {

    private static int count = 0;
    private static final Object lock = new Object();


    public static void main(String[] args) {
        Thread evenThread = new Thread(new Print(), "偶数线程");
        evenThread.start();
        
        Thread oddThread = new Thread(new Print(), "奇数线程");
        oddThread.start();
    }


    static class Print implements Runnable {

        /**
         *
         */
        @Override
        public void run() {

            while (count < 100) {
                synchronized (lock) {
                    // 打印数字 并释放锁
                    System.out.println(Thread.currentThread().getName() + "打印:" + count++);
                    // 唤醒另一个线程
                    lock.notify();

                    // 程序退出
                    if (count >= 100) {
                        return;
                    }

                    // 释放锁
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } // end synchronized
            } // end while
        }
    }

}

参考

[1] 多线程之锁优化(上):深入了解Synchronized同步锁的优化方法
[2] synchronized 实现原理
[3] 不可不说的Java“锁”事
[4] Java synchronized原理总结