synchronized实现原理

0. 前言

形成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多个线程共同操做共享数据。所以为了解决线程安全问题,咱们可能须要这样一个方案,当存在多个线程操做共享数据时,须要保证同一时刻有且只有一个线程在操做共享数据,其余线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥,即能达到互斥访问的目的。换句话说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其余线程只能处于等待的状态,直到当前线程处理完毕释放该锁。在 Java 中,关键字 synchronized能够保证在同一个时刻,只有一个线程能够执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操做),同时咱们还应该注意到synchronized另一个重要的做用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其余线程所看到(保证可见性,彻底能够替代Volatile功能),这点确实也是很重要的。java

1. synchronized的三种应用方式

synchronized关键字最主要有如下3种应用方式,下面分别介绍c++

  • 修饰实例方法,做用于当前实例加锁,进入同步代码前要得到当前实例的锁
  • 修饰静态方法,做用于当前类对象加锁,进入同步代码前要得到当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要得到给定对象的锁

1.1 synchronized做用于实例方法

所谓的实例对象锁就是用synchronized修饰实例对象中的实例方法,注意实例方法不包括静态方法数组

public class AccountingSync implements Runnable{
    //共享资源(临界资源)
    static int i=0;

    /** * synchronized 修饰实例方法 */
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<100;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        System.out.println(i);//200
    }
    
}

复制代码

上述代码中,咱们开启两个线程操做同一个共享资源即变量i,因为i++操做并不具有原子性,该操做是先读取值,而后写回一个新值,至关于原来的值加上1,分两步完成。若是第二个线程在第一个线程读取旧值和写回新值期间读取i的值,那么第二个线程就会与第一个线程一块儿看到同一个值,并执行相同值的加1操做,这也就形成了线程安全失败。所以对于increase方法必须使用synchronized修饰,以便保证线程安全。此时咱们应该注意到synchronized修饰的是实例方法increase,在这样的状况下,当前线程的锁即是实例对象,注意Java中的线程同步锁能够是任意对象。从代码执行结果来看确实是正确的,假若咱们没有使用synchronized关键字,其最终输出结果就极可能小于200,这即是synchronized关键字的做用。这里咱们还须要意识到,当一个线程正在访问一个对象的 synchronized 实例方法,那么其余线程不能访问该对象的其余 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁以后,其余线程没法获取该对象的锁,因此没法访问该对象的其余synchronized实例方法,可是其余线程仍是能够访问该实例对象的其余非synchronized方法。固然若是是一个线程 A 须要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另外一个线程 B 须要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是容许的,由于两个实例对象锁并不一样相同,此时若是两个线程操做数据并不是共享的,线程安全是有保障的,遗憾的是若是两个线程操做的是共享数据,那么线程安全就有可能没法保证了,以下代码将演示出该现象:安全

public class AccountingSyncBad implements Runnable{
    static int i=0;
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncBad());
        //new新实例
        Thread t2=new Thread(new AccountingSyncBad());
        t1.start();
        t2.start();
        System.out.println(i);//146
    }
}
复制代码

上述代码与前面不一样的是咱们同时建立了两个新实例AccountingSyncBad,而后启动两个不一样的线程对共享变量i进行操做,但很遗憾操做结果是146而不是指望结果200,由于上述代码犯了严重的错误,虽然咱们使用synchronized修饰了increase方法,但却new了两个不一样的实例对象,这也就意味着存在着两个不一样的实例对象锁,所以t1和t2都会进入各自的对象锁,也就是说t1和t2线程使用的是不一样的锁,所以线程安全是没法保证的。解决这种困境的的方式是将synchronized做用于静态的increase方法,这样的话,对象锁就当前类对象,因为不管建立多少个实例对象,但对于的类对象只有一个,全部在这样的状况下对象锁就是惟一的。下面咱们看看如何使用将synchronized做用于静态的increase方法。数据结构

1.2 synchronized做用于静态方法

当synchronized做用于静态方法时,其锁就是当前类的class对象锁。因为静态成员不专属于任何一个实例对象,是类成员,所以经过class对象锁能够控制静态 成员的并发操做。须要注意的是若是一个线程A调用一个实例对象的非static synchronized方法,而线程B须要调用这个实例对象所属类的静态 synchronized方法,是容许的,不会发生互斥现象,由于访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁,看以下代码多线程

public class AccountingSyncClass implements Runnable{
    static int i=0;

    /** * 做用于静态方法,锁是当前class对象,也就是 * AccountingSyncClass类对应的class对象 */
    public static synchronized void increase(){
        i++;
    }

    /** * 非静态,访问时锁不同不会发生互斥 */
    public synchronized void increase4Obj(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncClass());
        //new新实例
        Thread t2=new Thread(new AccountingSyncClass());
        //启动线程
        t1.start();
        t2.start();
        System.out.println(i);
    }
}

复制代码

因为synchronized关键字修饰的是静态increase方法,与修饰实例方法不一样的是,其锁对象是当前类的class对象。注意代码中的increase4Obj方法是实例方法,其对象锁是当前实例对象,若是别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不一样,但咱们应该意识到这种状况下可能会发现线程安全问题(操做了共享静态变量i)。并发

1.3 synchronized同步代码块

除了使用关键字修饰实例方法和静态方法外,还能够修饰同步代码块,在某些状况下,咱们编写的方法体可能比较大,同时存在一些比较耗时的操做,而须要同步的代码又只有一小部分,若是直接对整个方法进行同步操做,可能会得不偿失,此时咱们可使用同步代码块的方式对须要同步的代码进行包裹,这样就无需对整个方法进行同步操做了,同步代码块的使用示例以下:app

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        //省略其余耗时操做....
        //使用同步代码块对变量i进行同步操做,锁对象为instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                    i++;
              }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        System.out.println(i);
    }
}
复制代码

从代码看出,将synchronized做用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,若是当前有其余线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操做。固然除了instance做为对象外,咱们还可使用this对象(表明当前实例)或者当前类的class对象做为锁,以下代码:ide

//this,当前实例对象锁
synchronized(this){
    for(int j=0;j<100;j++){
        i++;
    }
}

//class对象锁
synchronized(AccountingSync.class){
    for(int j=0;j<100;j++){
        i++;
    }
}
复制代码

3. synchronized底层语义原理

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 不管是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)仍是隐式同步都是如此。在 Java 语言中,同步用的最多的地方多是被 synchronized 修饰的同步方法。同步方法 并非由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。下面先来了解一个概念Java对象头,这对深刻理解synchronized实现原理很是关键。函数

3.1 理解Java对象头与Monitor

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。以下:

对象存储布局

  • 实例数据:存放类的属性数据信息,包括父类的属性信息。
  • 对齐填充:因为虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
  • 对象头:Java对象头通常占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),可是若是对象是数组类型,则须要3个机器码,由于JVM虚拟机能够经过Java对象的元数据信息肯定Java对象的大小,可是没法从数组的元数据来确认数组的大小,因此用一块来记录数组长度。

为了表示对象的属性、方法等信息,不得不须要结构描述。Hotspot VM 使用对象头部的一个指针指向 Class 区域的方式来找到对象的 Class 描述,以及内部的方法、属性入口。以下图所示:

虚拟机位数 头对象结构 说明
32/64bit Mark Word 存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳、对象分代年龄等;Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽可能多的信息,它会根据本身的状态复用本身的存储空间
32/64bi Class Metadata Address 类型指针指向对象的类元数据,JVM经过这个指针肯定该对象是哪一个类的实例。
32/64bi Array Length 若是对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。由于虚拟机能够经过普通 Java 对象的元数据信息肯定 Java 对象的大小,可是从数组的元数据中没法肯定数组的大小。这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32bit 和 64bit。

例如,在 32 位的 HotSpot 虚拟机中,若是对象处于未被锁定的状态下,那么 Mark Word 的 32bit 空间中的 25bit 用于存储对象哈希码,4bit 用于存储对象分代年龄,2bit 用于存储锁标志位,1bit 固定为 0,以下表所示:

无锁状态
因为对象头的信息是与对象自身定义的数据没有关系的额外存储成本,所以考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象自己的状态复用本身的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有以下可能变化的结构:

在64位虚拟机下,Mark Word是64bit大小的,其存储结构以下:

对象头的最后两位存储了锁的标志位,01是初始状态,未加锁,其对象头里存储的是对象自己的哈希码,随着锁级别的不一样,对象头里会存储不一样的内容。偏向锁存储的是当前占用此对象的线程ID;而轻量级则存储指向线程栈中锁记录的指针。从这里咱们能够看到,“锁”这个东西,多是个锁记录+对象头里的引用指针(判断线程是否拥有锁时将线程的锁记录地址和对象头里的指针地址比较),也多是对象头里的线程ID(判断线程是否拥有锁时将线程的ID和对象头里存储的线程ID比较)。

其中轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增长的,稍后咱们会简要分析。这里咱们主要分析一下重量级锁也就是一般说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每一个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor能够与对象一块儿建立销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构以下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

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,用来保存ObjectWaiter对象列表( 每一个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其余线程进入获取monitor(锁)。以下图所示

由此看来,monitor对象存在于每一个Java对象的对象头中(存储的指针的指向),synchronized锁即是经过这种方式获取锁的,也是为何Java中任意对象能够做为锁的缘由,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的缘由(关于这点稍后还会进行分析),有了上述知识基础后,下面咱们将进一步分析synchronized在字节码层面的具体语义实现。

3.2 synchronized代码块底层原理

如今咱们从新定义一个synchronized修饰的同步代码块,在代码块中操做共享变量i,以下

public class SyncCodeBlock {

   public int i;

   public void syncTask(){
       //同步代码块
       synchronized (this){
           i++;
       }
   }
}
复制代码

编译上述代码并使用javap反编译后获得字节码以下(这里咱们省略一部分没有必要的信息):

Classfile /***/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
  Last modified 2018-07-25; size 426 bytes
  MD5 checksum c80bc322c87b312de760942820b4fed5
  Compiled from "SyncCodeBlock.java"
public class com.hc.concurrencys.SyncCodeBlock
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
  //........省略常量池中数据
  //构造函数
  public com.hc.concurrencys.SyncCodeBlock();
    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
  //===========主要看看syncTask方法实现================
  public void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter  //注意此处,进入同步方法
         4: aload_0
         5: dup
         6: getfield      #2             // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2            // Field i:I
        14: aload_1
        15: monitorexit   //注意此处,退出同步方法
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit //注意此处,退出同步方法
        22: aload_2
        23: athrow
        24: return
      Exception table:
      //省略其余字节码.......
}
SourceFile: "SyncCodeBlock.java"
复制代码

咱们主要关注字节码中的以下代码

3: monitorenter  //进入同步方法
//..........省略其余 
15: monitorexit   //退出同步方法
16: goto          24
//省略其余.......
21: monitorexit //退出同步方法
复制代码

从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程能够成功取得 monitor,并将计数器值设置为 1,取锁成功。若是当前线程已经拥有 objectref 的 monitor 的持有权,那它能够重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。假若其余线程已经拥有 objectref 的 monitor 的全部权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其余线程将有机会持有 monitor 。值得注意的是编译器将会确保不管方法经过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而不管这个方法是正常结束仍是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然能够正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理全部的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也能够看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

3.3 synchronized方法底层原理

方法级的同步是隐式,即无需经过字节码指令来控制的,它实如今方法调用和返回操做之中。JVM能够从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED访问标志是否被设置,若是设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),而后再执行方法,最后再方法完成(不管是正常完成仍是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其余任何线程都没法再得到同一个monitor。若是一个同步方法执行期间抛出了异常,而且在方法内部没法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法以外时自动释放。下面咱们看看字节码层面如何实现:

public class SyncMethod {

   public int i;

   public synchronized void syncTask(){
           i++;
   }
}
复制代码

使用javap反编译后的字节码以下:

Classfile /***/src/main/java/com/zejian/concurrencys/SyncMethod.class
  Last modified 2017-6-2; size 308 bytes
  MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
  Compiled from "SyncMethod.java"
public class com.hc.concurrencys.SyncMethod
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool;

   //省略不必的字节码
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法标识ACC_PUBLIC表明public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}
SourceFile: "SyncMethod.java"
复制代码

从字节码中能够看出,synchronized修饰的方法并无monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM经过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这即是synchronized锁在同步代码块和同步方法上实现的基本原理。同时咱们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,由于监视器锁(monitor)是依赖于底层的操做系统的Mutex Lock(互斥锁)来实现的,而操做系统实现线程之间的切换时须要从用户态转换到核心态,这个状态之间的转换须要相对比较长的时间,时间成本相对较高,这也是为何早期的synchronized效率低的缘由。庆幸的是在Java6以后Java官方对从JVM层面对synchronized较大优化,因此如今的synchronized锁效率也优化得很不错了,Java6以后,为了减小得到锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,接下来咱们将简单了解一下Java官方在JVM层面对synchronized锁的优化。

4. Java虚拟机对synchronized的优化

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁能够从偏向锁升级到轻量级锁,再升级的重量级锁,可是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,关于重量级锁,前面咱们已详细分析过,下面咱们将介绍偏向锁和轻量级锁以及JVM的其余优化手段,这里并不打算深刻到每一个锁的实现和转换过程更多地是阐述Java虚拟机所提供的每一个锁的核心优化思想,毕竟涉及到具体过程比较繁琐。

4.1 偏向锁

偏向锁是Java 6以后加入的新锁,它是一种针对加锁操做的优化手段,通过研究发现,在大多数状况下,锁不只不存在多线程竞争,并且老是由同一线程屡次得到,所以为了减小同一线程获取锁(会涉及到一些CAS操做,耗时)的代价而引入偏向锁。偏向锁的核心思想是,若是一个线程得到了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再作任何同步操做,即获取锁的过程,这样就省去了大量有关锁申请的操做,从而也就提供程序的性能。因此,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续屡次是同一个线程申请相同的锁。可是对于锁竞争比较激烈的场合,偏向锁就失效了,由于这样场合极有可能每次申请锁的线程都是不相同的,所以这种场合下不该该使用偏向锁,不然会得不偿失,须要注意的是,偏向锁失败后,并不会当即膨胀为重量级锁,而是先升级为轻量级锁。下面咱们接着了解轻量级锁。

4.2 轻量级锁

假若偏向锁失败,虚拟机并不会当即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6以后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁可以提高程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。须要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,若是存在同一时间访问同一锁的场合,就会致使轻量级锁膨胀为重量级锁。

4.3 自旋锁

轻量级锁失败后,虚拟机为了不线程真实地在操做系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数状况下,线程持有锁的时间都不会太长,若是直接挂起操做系统层面的线程可能会得不偿失,毕竟操做系统实现线程之间的切换时须要从用户态转换到核心态,这个状态之间的转换须要相对比较长的时间,时间成本相对较高,所以自旋锁会假设在不久未来,当前的线程能够得到锁,所以虚拟机会让当前想要获取锁的线程作几个空循环(这也是称为自旋的缘由),通常不会过久,多是50个循环或100循环,在通过若干次循环后,若是获得锁,就顺利进入临界区。若是还不能得到锁,那就会将线程在操做系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是能够提高效率的。最后没办法也就只能升级为重量级锁了。

4.4 适应性自旋锁

DK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数再也不是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

线程若是自旋成功了,那么下次自旋的次数会更加多,由于虚拟机认为既然上次成功了,那么这次自旋也颇有可能会再次成功,那么它就会容许自旋等待持续的次数更多。反之,若是对于某个锁,不多有自旋可以成功,那么在之后要或者这个锁的时候自旋的次数会减小甚至省略掉自旋过程,以避免浪费处理器资源。

4.5 锁消除

消除锁是虚拟机另一种锁的优化,这种优化更完全,Java虚拟机在JIT编译时(能够简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),经过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,经过这种方式消除没有必要的锁,能够节省毫无心义的请求锁时间,以下StringBuffer的append是一个同步方法,可是在add方法中的StringBuffer属于一个局部变量,而且不会被其余线程所使用,所以StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

public class StringBufferRemoveSync {

    public void add(String str1, String str2) {
        //StringBuffer是线程安全,因为sb只会在append方法中使用,不可能被其余线程引用
        //所以sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
        for (int i = 0; i < 100; i++) {
            rmsync.add("abc", "123");
        }
    }

}
复制代码

4.6 偏向锁、轻量级锁、重量级锁之间的状态转换

5. 关于synchronized 可能须要了解的关键点

5.1 synchronized的可重入性

从互斥锁的设计上来讲,当一个线程试图操做一个由其余线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求本身持有对象锁的临界资源时,这种状况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,所以在一个线程调用synchronized方法的同时在其方法体内部调用该对象另外一个synchronized方法,也就是说一个线程获得一个对象锁后再次请求该对象锁,是容许的,这就是synchronized的可重入性。以下:

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
        for(int j=0;j<100;j++){

            //this,当前实例对象锁
            synchronized(this){
                i++;
                increase();//synchronized的可重入性
            }
        }
    }

    public synchronized void increase(){
        j++;
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t1.join();
        t2.start();
        t2.join();
        System.out.println(i);
    }
}
复制代码

正如代码所演示的,在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另一个synchronized方法,再次请求当前实例锁时,将被容许,进而执行方法体代码,这就是重入锁最直接的体现,须要特别注意另一种状况,当子类继承父类时,子类也是能够经过可重入锁调用父类的同步方法。注意因为synchronized是基于monitor实现的,所以每次重入,monitor中的计数器仍会加1

重入进一步提高了加锁行为的封装性,所以简化了面向对象并发代码的开发。分析以下程序:

public class Father {  
        public synchronized void doSomething(){  
            // do something... 
        }  
    }  
      
    public class Child extends Father {  
        public synchronized void doSomething(){  
            // do something... 
            super.doSomething();  
        }  
    }  
复制代码

子类覆写了父类的同步方法,而后调用父类中的方法,此时若是没有可重入的锁,那么这段代码将产生死锁。

因为Father和Child中的doSomething方法都是synchronized方法,所以每一个doSomething方法在执行前都会获取Child对象实例上的锁。若是内置锁不是可重入的,那么在调用super.doSomething时将没法得到该Child对象上的互斥锁,由于这个锁已经被持有,从而线程会永远阻塞下去,一直在等待一个永远也没法获取的锁。重入则避免了这种死锁状况的发生。

同一个线程在调用本类中其余synchronized方法、块或父类中的synchronized方法/块时,都不会阻碍该线程的执行,由于互斥锁是可重入的。

5.2 线程中断与synchronized

正如中断二字所表达的意义,在线程运行(run方法)中间打断它,在Java中,提供了如下3个有关线程中断的方法

//中断线程(实例方法)
public void Thread.interrupt();

//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();

//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();
复制代码

当一个线程处于被阻塞状态或者试图执行一个阻塞操做时,使用Thread.interrupt()方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改成非中断状态),以下代码将演示该过程:

public class InterruputSleepThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                //while在try中,经过异常中断就能够退出run循环
                try {
                    while (true) {
                        //当前线程处于阻塞状态,异常必须捕捉处理,没法往外抛出
                        TimeUnit.SECONDS.sleep(2);
                    }
                } catch (InterruptedException e) {
                    System.out.println("Interruted When Sleep");
                    boolean interrupt = this.isInterrupted();
                    //中断状态被复位
                    System.out.println("interrupt:"+interrupt);
                }
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        //中断处于阻塞状态的线程
        t1.interrupt();

        /** * 输出结果: Interruted When Sleep interrupt:false */
    }
}
复制代码

如上述代码所示,咱们建立一个线程,并在线程中调用了sleep方法从而使用线程进入阻塞状态,启动线程后,调用线程实例对象的interrupt方法中断阻塞异常,并抛出InterruptedException异常,此时中断状态也将被复位。这里有些人可能会诧异,为何不用Thread.sleep(2000);而是用TimeUnit.SECONDS.sleep(2);其实缘由很简单,前者使用时并无明确的单位说明,然后者很是明确表达秒的单位,事实上后者的内部实现最终仍是调用了Thread.sleep(2000);,但为了编写的代码语义更清晰,建议使用TimeUnit.SECONDS.sleep(2);的方式,注意TimeUnit是个枚举类型。除了阻塞中断的情景,咱们还可能会遇处处于运行期且非阻塞的状态的线程,这种状况下,直接调用Thread.interrupt()中断线程是不会获得任响应的,以下代码,将没法中断非阻塞状态下的线程:

public class InterruputThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    System.out.println("未被中断");
                }
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /** * 输出结果(无限执行): 未被中断 未被中断 未被中断 ...... */
    }
}
复制代码

虽然咱们调用了interrupt方法,但线程t1并未被中断,由于处于非阻塞状态的线程须要咱们手动进行中断检测并结束程序,改进后代码以下:

public class InterruputThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    //判断当前线程是否被中断
                    if (this.isInterrupted()){
                        System.out.println("线程中断");
                        break;
                    }
                }

                System.out.println("已跳出循环,线程中断!");
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /** * 输出结果: 线程中断 已跳出循环,线程中断! */
    }
}
复制代码

咱们在代码中使用了实例方法isInterrupted判断线程是否已被中断,若是被中断将跳出循环以此结束线程,注意非阻塞状态调用interrupt()并不会致使中断状态重置。综合所述,能够简单总结一下中断两种状况,一种是当线程处于阻塞状态或者试图执行一个阻塞操做时,咱们可使用实例方法interrupt()进行线程中断,执行中断操做后将会抛出interruptException异常(该异常必须捕捉没法向外抛出)并将中断状态复位,另一种是当线程处于运行状态时,咱们也可调用实例方法interrupt()进行线程中断,但同时必须手动判断中断状态,并编写中断线程的代码(其实就是结束run方法体的代码)。有时咱们在编码时可能须要兼顾以上两种状况,那么就能够以下编写:

public void run(){
    try {
    //判断当前线程是否已中断,注意interrupted方法是静态的,执行后会对中断状态进行复位
    while (!Thread.interrupted()) {
        TimeUnit.SECONDS.sleep(2);
    }
    } catch (InterruptedException e) {

    }
}
复制代码

事实上线程的中断操做对于正在等待获取的锁对象的synchronized方法或者代码块并不起做用,也就是对于synchronized来讲,若是一个线程在等待锁,那么结果只有两种,要么它得到这把锁继续执行,要么它就保存等待,即便调用中断线程的方法,也不会生效。演示代码以下:

public class SynchronizedBlocked implements Runnable{

    public synchronized void f() {
        System.out.println("Trying to call f()");
        while(true) // Never releases lock
            Thread.yield();
    }

    /** * 在构造器中建立新线程并启动获取对象锁 */
    public SynchronizedBlocked() {
        //该线程已持有当前实例锁
        new Thread() {
            public void run() {
                f(); // Lock acquired by this thread
            }
        }.start();
    }
    public void run() {
        //中断判断
        while (true) {
            if (Thread.interrupted()) {
                System.out.println("中断线程!!");
                break;
            } else {
                f();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        SynchronizedBlocked sync = new SynchronizedBlocked();
        Thread t = new Thread(sync);
        //启动后调用f()方法,没法获取当前实例锁处于等待状态
        t.start();
        TimeUnit.SECONDS.sleep(1);
        //中断线程,没法生效
        t.interrupt();
    }
}
复制代码

咱们在SynchronizedBlocked构造函数中建立一个新线程并启动获取调用f()获取到当前实例锁,因为SynchronizedBlocked自身也是线程,启动后在其run方法中也调用了f(),但因为对象锁被其余线程占用,致使t线程只能等到锁,此时咱们调用了t.interrupt();但并不能中断线程。

5.3 等待唤醒机制与synchronized

所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,不然就会抛出IllegalMonitorStateException异常,这是由于调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,咱们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字能够获取 monitor ,这也就是为何notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的缘由。

须要特别理解的一点是,与sleep方法不一样的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会立刻释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。

相关文章
相关标签/搜索