Java并发——Synchronized关键字和锁升级,详细分析偏向锁和轻量级锁的升级

目录java

1、Synchronized使用场景数组

2、Synchronized实现原理安全

3、锁的优化数据结构

一、锁升级多线程

二、锁粗化ide

三、锁消除布局


1、Synchronized使用场景

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<1000000;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();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

该段程序的输出为:2000000this

可是若是increase的synchronized被删除,那么极可能输出结果就会小于2000000,这是由于多个线程同时访问临界资源i,若是一个线程A对i=88的自增到89没有被B线程读取到,线程B认为i仍然是88,那么线程B对i的自增结果仍是89,那么这里就会出现问题。

Synchronized锁的3种使用形式(使用场景):

  • Synchronized修饰普通同步方法:锁对象当前实例对象;
  • Synchronized修饰静态同步方法:锁对象是当前的类Class对象;
  • Synchronized修饰同步代码块:锁对象是Synchronized后面括号里配置的对象,这个对象能够是某个对象(xlock),也能够是某个类(Xlock.class);

注意:

  • 使用synchronized修饰非静态方法或者使用synchronized修饰代码块时制定的为实例对象时,同一个类的不一样对象拥有本身的锁,所以不会相互阻塞。
  • 使用synchronized修饰类和对象时,因为类对象和实例对象分别拥有本身的监视器锁,所以不会相互阻塞。
  • 使用使用synchronized修饰实例对象时,若是一个线程正在访问实例对象的一个synchronized方法时,其它线程不只不能访问该synchronized方法,该对象的其它synchronized方法也不能访问,由于一个对象只有一个监视器锁对象,可是其它线程能够访问该对象的非synchronized方法。
  • 线程A访问实例对象的非static synchronized方法时,线程B也能够同时访问实例对象的static synchronized方法,由于前者获取的是实例对象的监视器锁,然后者获取的是类对象的监视器锁,二者不存在互斥关系。

 

2、Synchronized实现原理

一、Java对象头

首先,咱们要知道对象在内存中的布局:

已知对象是存放在堆内存中的,对象大体能够分为三个部分,分别是对象头、实例变量和填充字节。

  • 对象头的zhuyao是由MarkWord和Klass Point(类型指针)组成,其中Klass Point是是对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例,Mark Word用于存储对象自身的运行时数据。若是对象是数组对象,那么对象头占用3个字宽(Word),若是对象是非数组对象,那么对象头占用2个字宽。(1word = 2 Byte = 16 bit)
  • 实例变量存储的是对象的属性信息,包括父类的属性信息,按照4字节对齐
  • 填充字符,由于虚拟机要求对象字节必须是8字节的整数倍,填充字符就是用于凑齐这个整数倍的

经过第一部分能够知道,Synchronized不管是修饰方法仍是代码块,都是经过持有修饰对象的锁来实现同步,那么Synchronized锁对象是存在哪里的呢?答案是存在锁对象的对象头的MarkWord中。那么MarkWord在对象头中到底长什么样,也就是它到底存储了什么呢?

在32位的虚拟机中:

在64位的虚拟机中:

上图中的偏向锁和轻量级锁都是在java6之后对锁机制进行优化时引进的,下文的锁升级部分会具体讲解,Synchronized关键字对应的是重量级锁,接下来对重量级锁在Hotspot JVM中的实现锁讲解。

二、Synchronized在JVM中的实现原理

重量级锁对应的锁标志位是10,存储了指向重量级监视器锁的指针,在Hotspot中,对象的监视器(monitor)锁对象由ObjectMonitor对象实现(C++),其跟同步相关的数据结构以下:

ObjectMonitor() {
    _count        = 0; //用来记录该对象被线程获取锁的次数
    _waiters      = 0;
    _recursions   = 0; //锁的重入次数
    _owner        = NULL; //指向持有ObjectMonitor对象的线程 
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
  }

光看这些数据结构对监视器锁的工做机制仍是一头雾水,那么咱们首先看一下线程在获取锁的几个状态的转换:

线程的生命周期存在5个状态,start、running、waiting、blocking和dead

对于一个synchronized修饰的方法(代码块)来讲:

  1. 当多个线程同时访问该方法,那么这些线程会先被放进_EntryList队列,此时线程处于blocking状态
  2. 当一个线程获取到了实例对象的监视器(monitor)锁,那么就能够进入running状态,执行方法,此时,ObjectMonitor对象的_owner指向当前线程,_count加1表示当前对象锁被一个线程获取
  3. 当running状态的线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1,同时线程进入_WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程从新获取monitor对象进入_Owner区
  4. 若是当前线程执行完毕,那么也释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1

那么Synchronized修饰的代码块/方法如何获取monitor对象的呢?

在JVM规范里能够看到,不论是方法同步仍是代码块同步都是基于进入和退出monitor对象来实现,然而两者在具体实现上又存在很大的区别。经过javap对class字节码文件反编译能够获得反编译后的代码。

(1)Synchronized修饰代码块:

Synchronized代码块同步在须要同步的代码块开始的位置插入monitorentry指令,在同步结束的位置或者异常出现的位置插入monitorexit指令;JVM要保证monitorentry和monitorexit都是成对出现的,任何对象都有一个monitor与之对应,当这个对象的monitor被持有之后,它将处于锁定状态。

例如,同步代码块以下:

public class SyncCodeBlock {
   public int i;
   public void syncTask(){
       synchronized (this){
           i++;
       }
   }
}

对同步代码块编译后的class字节码文件反编译,结果以下(仅保留方法部分的反编译内容):

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:
      //省略其余字节码.......

能够看出同步方法块在进入代码块时插入了monitorentry语句,在退出代码块时插入了monitorexit语句,为了保证不管是正常执行完毕(第15行)仍是异常跳出代码块(第21行)都能执行monitorexit语句,所以会出现两句monitorexit语句。

(2)Synchronized修饰方法:

Synchronized方法同步再也不是经过插入monitorentry和monitorexit指令实现,而是由方法调用指令来读取运行时常量池中的ACC_SYNCHRONIZED标志隐式实现的,若是方法表结构(method_info Structure)中的ACC_SYNCHRONIZED标志被设置,那么线程在执行方法前会先去获取对象的monitor对象,若是获取成功则执行方法代码,执行完毕后释放monitor对象,若是monitor对象已经被其它线程获取,那么当前线程被阻塞。

同步方法代码以下:

public class SyncMethod {
   public int i;
   public synchronized void syncTask(){
           i++;
   }
}

对同步方法编译后的class字节码反编译,结果以下(仅保留方法部分的反编译内容):

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
}

能够看出方法开始和结束的地方都没有出现monitorentry和monitorexit指令,可是出现的ACC_SYNCHRONIZED标志位。

3、锁的优化

一、锁升级

锁的4中状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)

(1)偏向锁:

为何要引入偏向锁?

由于通过HotSpot的做者大量的研究发现,大多数时候是不存在锁竞争的,经常是一个线程屡次得到同一个锁,所以若是每次都要竞争锁会增大不少没有必要付出的代价,为了下降获取锁的代价,才引入的偏向锁。

偏向锁的升级

当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,由于偏向锁不会主动释放锁,所以之后线程1再次获取锁的时候,须要比较当前线程的threadID和Java对象头中的threadID是否一致,若是一致(仍是线程1获取锁对象),则无需使用CAS来加锁、解锁;若是不一致(其余线程,如线程2要竞争锁对象,而偏向锁不会主动释放所以仍是存储的线程1的threadID),那么须要查看Java对象头中记录的线程1是否存活,若是没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)能够竞争将其设置为偏向锁;若是存活,那么马上查找该线程(线程1)的栈帧信息,若是仍是须要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,若是线程1 再也不使用该锁对象,那么将锁对象状态设为无锁状态,从新偏向新的线程

偏向锁的取消:

偏向锁是默认开启的,并且开始时间通常是比应用程序启动慢几秒,若是不想有这个延迟,那么可使用-XX:BiasedLockingStartUpDelay=0

若是不想要偏向锁,那么能够经过-XX:-UseBiasedLocking = false来设置;

(2)轻量级锁

为何要引入轻量级锁?

轻量级锁考虑的是竞争锁对象的线程很少,并且线程持有锁的时间也不长的情景。由于阻塞线程须要CPU从用户态转到内核态,代价较大,若是刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,所以这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

轻量级锁何时升级为重量级锁?

线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中建立的用于存储锁记录的空间(称为DisplacedMarkWord),而后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址

若是在线程1复制对象头的同时(在线程1CAS以前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,可是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁

可是若是自旋的时间太长也不行,由于自旋是要消耗CPU的,所以自旋的次数是有限制的,好比10次或者100次,若是自旋次数到了线程1尚未释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

 

*注意:了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁能够升级不能够降级,可是偏向锁状态能够被重置为无锁状态。

(3)这几种锁的优缺点(偏向锁、轻量级锁、重量级锁)

二、锁粗化

按理来讲,同步块的做用范围应该尽量小,仅在共享数据的实际做用域中才进行同步,这样作的目的是为了使须要同步的操做数量尽量缩小,缩短阻塞时间,若是存在锁竞争,那么等待锁的线程也能尽快拿到锁。 
可是加锁解锁也须要消耗资源,若是存在一系列的连续加锁解锁操做,可能会致使没必要要的性能损耗。 
锁粗化就是将多个连续的加锁、解锁操做链接在一块儿,扩展成一个范围更大的锁,避免频繁的加锁解锁操做。

三、锁消除

Java虚拟机在JIT编译时(能够简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),经过对运行上下文的扫描,通过逃逸分析,去除不可能存在共享资源竞争的锁,经过这种方式消除没有必要的锁,能够节省毫无心义的请求锁时间