浅谈Synchronized

Synchronized

Java在互斥同步方面上除了提供Lock API外,还提供Synchronized关键字来实现互斥同步原语。Synchronized是jdk内建的锁同步机制,在jdk1.6版本以前,Synchronized是java实现互斥同步的惟一方式。java

Synchronized的使用有三种方式,做为一个关键字,可做用于代码块、普通方法和静态方法,做用于代码块表示对当前代码块加锁,若是其余线程同时访问同一个对象该代码块,将会被阻塞;若是做用于普通方法,那么当有多个线程同时访问同一个对象的这个方法时,只有一个线程能执行该方法,其余线程将等待先前的线程释放才能够执行;若是做用于静态方法,那么若是多个线程访问该类的这个静态方法时,其余线程将等待先前的线程释放才能够执行。
接下来咱们经过一个例子来看它的基本用法。数据结构

/**
 * v
 * 2020/3/17 9:23 下午
 * 1.0
 */
public class SynchronizedTest {
    
    public void method1() {
        // 做用于代码块
        synchronized (this) {
            System.out.println("this is method1");
        }
    }

    // 做用于普通方法
    public synchronized void method2() {
        System.out.println("this is method2");
        
    }

    // 做用于静态方法
    public static synchronized void method3() {
        System.out.println("this is method3");
    }
    

    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();
        test.method1();
        test.method2();
        SynchronizedTest.method3();
    }
}

因为synchronized是java内建的关键字,由于synchronized的实现原理并不能从java语言层面去分析,只能经过实现java的C语言中去分析,这里限于我的能力也没办法拿出来分享一下。不过咱们能够从它的字节码来看一下,被synchronized修饰的代码块有什么不一样。jvm

这里咱们看一下上面代码编译出来的字节码,主要看一下method一、method2和method3这三个方法的字节码。布局

// class version 52.0 (52)
// access flags 0x21
public class cn/v/SynchronizedTest {

  // compiled from: SynchronizedTest.java

  ...省略
  // access flags 0x1
  public method1()V
    TRYCATCHBLOCK L0 L1 L2 null
    TRYCATCHBLOCK L2 L3 L2 null
   L4
    LINENUMBER 14 L4
    ALOAD 0
    DUP
    ASTORE 1
    MONITORENTER
   L0
    LINENUMBER 15 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "this is method1"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L5
    LINENUMBER 16 L5
    ALOAD 1
    MONITOREXIT
   L1
    GOTO L6
   L2
   FRAME FULL [cn/v/SynchronizedTest java/lang/Object] [java/lang/Throwable]
    ASTORE 2
    ALOAD 1
    MONITOREXIT
   L3
    ALOAD 2
    ATHROW
   L6
    LINENUMBER 17 L6
   FRAME CHOP 1
    RETURN
   L7
    LOCALVARIABLE this Lcn/v/SynchronizedTest; L4 L7 0
    MAXSTACK = 2
    MAXLOCALS = 3

  // access flags 0x21
  public synchronized method2()V
   L0
    LINENUMBER 21 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "this is method2"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 23 L1
    RETURN
   L2
    LOCALVARIABLE this Lcn/v/SynchronizedTest; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x29
  public static synchronized method3()V
   L0
    LINENUMBER 27 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "this is method3"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 28 L1
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0

    ...省略
}

咱们能够看出,在method1中,被synchronized修饰的同步代码块的入口和出口,分别插入了MONITORENTOR、MONITOREXIT字节码指令。然而,在Mthod2和method3中,并无任何特别的字节码指令对它们进行修饰,这是由于在Class文件的方法表中,将该方法的access_flag字段中的synchronized设置为1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Class做为锁对象。性能

在jvm中,每一个对象都有一个monitor监控器,MONITORENTER主要就是尝试获取这个monitor监视器,若是成功获取monitor,就将值+1,MONITOREXIT就是在线程离开同步代码块时,对其值-1。若是线程重入,就将值再-1,synchronized是能够重入的。ReentranLock的实现原理相似,也是内部维护一个volitile int类型的变量,经过cas操做对其加一减一来表示锁的获取和释放。学习

咱们上边说过,synchronized在修饰方法的时候,是经过在其Class文件的方法表中,将该方法的access_flag字段中的synchronized设置为1来表示该方法加锁,同时它会在常量池中增长这一标识符,获取它的monitor监视器,本质上是同样的。优化

经过如下命令输出一些附加信息后,能够看到metod二、method3的方法flags上,多了一个ACC_SYNCHRONIZED标签。
javap -v SynchronizedTest.classthis

public void method1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String this is method1
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 10: 0
        line 11: 8

  public synchronized void method2();
    descriptor: ()V
    flags: ACC_PUBLIC, 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 this is method2
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 13: 0
        line 14: 8

  public static synchronized void method3();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String this is method3
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 17: 0
        line 18: 8

Hotspot 对synchronized的优化历程

重量级锁

在jdk1.6版本以前 ,jvm对synchronized实现是须要从用户态切换到内核态的,jvm会阻塞未获取到锁的线程,在锁被释放时去唤醒被阻塞的线程。而阻塞和唤醒操做是依赖操做系统来完成的,而且monitor调用的操做系统底层的互斥量(mutex),这会形成很大的开销,所以称之为重量级锁,这就是synchronized早期实现。spa

自旋锁和自适应自旋

Java的线程是映射到操做系统的原生线程之上的,若是要阻塞或唤醒一个线程,都须要从用户态切换到内核态之中,所以线程的状态转换须要有消费不少的处理器时间。Synchronized重量级锁是经过操做系统互斥性实现的,互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程都须要转入内核态中完成,这些操做会给操做系统带来很大的压力。同时,虚拟机的开发团队也注意到在许多的应用上,共享数据的锁定状态中会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。因些引入自旋锁的概念,就是让后面请求锁的那个线程等待一下但不放弃处理的执行时间,看看持有锁的线程是否很快就会释放,通常的操做为让线程执行一个忙循环(自旋)。操作系统

自旋锁虽然避免了线程切换的开销,可是它要占用处理器时间。所以若是锁被占用的时间很是短,那么使用自旋锁自旋等待的效果就会很好,相反,若是锁被占用的时间很是长,使用自旋锁就会浪费额外的处理器资源。所以,自旋锁须要设置一个自旋次数,默认值为10次,用户能够经过jvm参数 -XX:PreBlockSpin来进行修改。

在jdk 1.6引入了自适应的的自旋锁,自适应代表了自旋的时间再也不固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的,若是在同一锁对象上,自旋等待刚刚成功得到过锁,而且持有锁的线程正在运行中,那么虚拟机也会认为此次自旋也可能成功得到到锁,进而提升自旋的时间。

轻量级锁

在jdk1.6以后,为了下降使用synchronized时的性能损耗,引入轻量级锁的概念,也就是在实际没有锁竞争的状况下,将申请互斥量的这步操做省略。而轻量级的实现原理就是将对象头的Mark Word中后2个bit设置为00的标志位来进行控制,标志位的设置是经过cas原理来进行操做的。若是当前线程获取到对象的锁,那么会将该标志位置为00,同时在当前线程的栈帧中开辟出一块名为『锁记录』内存空间,用于存储Mark Word信息的拷贝,而后将对象头中原来存储Mark Word的区域经过CAS操做更新为指向锁记录的指针,即pointer to lock record,若是操做成功,说明当前线程获取锁成功。

上面说的是轻量级锁的加锁过程,它的解锁过程一样是经过经过CAS操做来进行操做的,操做过程与加锁相反,就是将存储在线程锁的Mark Word信息从新拷贝到对象头(java对象头的结构在后面会进行说明)的Mark Word中,若是拷贝完成,那么整个同步的过程就完成了,若是替换失败,那么说明当前有其余线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。

注意:轻量级锁能提高程序同步性能的前提条件是,对于绝大部分锁,在整个同步周期内都是不存在锁竞争的。若是没有锁竞争,就避免了使用操做系统互斥量的损耗,若是存放锁竞争,除了互斥量的损耗外,还进行CAS操做的额外开销,这种状况性能明显不如重量级锁。

偏斜锁/偏向锁

为了进一步优化synchronized的实现,提出了偏向锁的概念,目的是消耗数据在无竞争状况下的同步原语,即将全部同步操做所有省略。它的实现与轻量级锁同样,经过对象头的Mark word中的后2个bit的标志位进行实现,不不一样的是它的标志位为01。若是开启偏向锁的,对象头中Mark word的数据结构将是另外一种实现,它会保存thread ID和epoch,即持有偏向锁的线程id和偏向时间戳。在未获取到偏向锁以前,它的threadId为0,当前进入同步代码块时,将经过cas进行操做标志位设置为01,同时将当前线程的id记录在Mark Word的Thread ID位置中,若是CAS操做成功,持有偏向锁的线程之后每次进入这个锁相关的同步代码块时,虚拟机均可以再也不进行任务同步操做(locking、unlocking和Mark Word的update)。

若是当另一个线程去尝试获取这个锁时,偏向模式就宣告失结束。根据锁对象目前是否处理被锁定的状态,撤销偏向锁后恢复到未锁定(标志位为01)或轻量级锁的状态,后续的同步操做就如上面的轻量级锁那样执行。

注意:偏向锁能够提升带有同步但无竞争的程序性能,它一样是一个带有效益权衡性质的优化,若是程序中有大多数的锁老是被多个不一样的线程访问,那使用偏向锁会形成更多额外的开销,好比偏向锁的撤销,锁升级。

java对象头

在Hotspot虚拟机中,对象在内容中存储的布局能够分为3块区块:对象头,实例数据和对齐填充。而对象头又包含两部分信息,第一部分是存储自身的运行时数据,如哈希码,GC分代年龄,锁状态标志位,线程持有的锁、偏向线程ID、偏向时间戳等,这一部分称之为"Mark Word"。因为对象须要存储的运行时数据不少,但对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计为一个非固定的数据结构以便在极小的空间内存储尽可能多的信息,它会根据对象的状态复用本身的空间。

好比,在不一样的状态(未锁定、轻量级锁、重量级锁、GC标记、可偏向/偏向锁)下的Mark Word的存储内容将会不同。
image.png

对象头的另外一部分是指向对象的类元数据的指针,虚拟机经过这个指针来肯定当前对象是哪一个类的实例。并非全部的虚拟机实现都须要在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不必定要经过对象自己(这是由对象访问定位的方式来决定的:使用句柄和直接指针,hotspot使用后者)。

Mark Word在不一样状态下数据结构的变化能够参考下图
image.png

完整的对象头以下图所示,包含Mark Word和Klass pointer
image.png

本文为学习周志明老师著做《深刻理解JVM虚拟机》的学习心得。

相关文章
相关标签/搜索