Java并发之synchronized深入理解

  前言

  如果某一个资源被多个线程共享(临界资源),为了避免因为资源抢占导致资源数据错乱,我们需要对线程做同步操作,那么synchronized就是实现线程同步的Java关键字,synchronized可以说在并发编程中是必不可少的部分,现在就来看一下synchronized的使用和底层原理。

  一、synchronized的特性

  1.1 原子性

  所谓原子性就是指一个操作或者多个操作,要么全部并且执行过程不会被任何因素打断,要么就都不执行。

  例如, 在Java中,对基本数据类型的变量读取和赋值操作是原子性操作,即这些操作是不可能被中断的,要么执行要么不执行。但是像i++、i+=1等操作    字符就不是原子性的,他们在字节码层面分成读取、计算、赋值几步操作,原值在这些步骤还没完成时就有可能已经被赋值了,那么最后赋值写入的数据就是脏数据,无法保证原子性。

  被synchronized修饰的方法,或者代码块的所有操作都是原子性,因为在执行操作之前必须获得类或者对象的锁,直到执行完才能释放,这中间的过程无法打断,即保证了原子性。

  1.2 可见性

  可见性是指多个线程访问一个共享资源时,该资源的状态、值信息等对于其他线程是可见的。

  synchronized和volatile具有可见性,其中synchronized对一个类或者对象加锁时,一个线程如果要访问该类或者对象必须先获得它的锁,而这个锁的状态对于其他线程都是可见的,并且在释放锁之前会对变量修改刷新到主存中,保证资源变量的可见性,如果某个线程占用了该锁,其他线程就必须在锁池中等待锁的释放。

  而volatile的实现类似,被volatile修饰的变量,每当值需要修改时都会立即更新主存,主存时共享的,所有线程可见,所以确保了其他线程读取的变量永远是最新值,保证可见性。

  1.3 有序性

  有序性指程序执行的顺序按照代码先后执行。

  synchronized和voolatile都具有有序性,Java允许编译器和处理器对代码指令进行重排以提高执行效率,但是指令重排满足 as-if-serial 并不会影响单线程的执行结果,可是会影响的是多线程并发执行的结果。

  synchronized保证了每个时刻都只有一个线程访问同步代码块,就确定不管如何指令重排,也只有一个线程执行同步代码,保证了有序性。

  1.4 可重入性

  synchronized和reentrantlock都是可重入锁。当一个线程试图操作一个有其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。简单点讲,就是一个线程拥有了锁,仍然还可以重新获得同一把锁。

  二、synchronized的用法

  synchronized可以修饰静态方法,成员方法、同时还可以直接定义代码块,但归根结底他上锁的字眼只有两类:对象或者类。

  2.1 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

  注意修饰的是实例方法不包括静态方法,如下:

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);
    }
    /**
     * 输出结果:
     * 2000000
     */
}

  上述代码中,我们开启两个线程操作同一个共享资源变量i,由于i++;操作不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的值,那么第二个线程就会获得旧值,并执行加1操作,这就会造成数据异常,即线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。此时synchronized修饰的是实例方法increase,在这样的情况下,当前线程的的锁便是实例对象instance,代码执行结果来看确实是正确的,倘若我们没有使用synchronized关键字,其最终输出结果就很可能小于2000000,这便是synchronized关键字的作用。

  这里我们需要意识到,当一个线程正在访问一个对象的synchronized实例方法,那么其他线程不能访问该对象其他synchronized方法,毕竟一个都想只有一把锁,当有线程获得该对象锁之后,其他线程无法获取该对象锁,所以无法访问对象的其他synchronized实例方法,但是其他线程还是可以访问实例对象的其他非synchronized方法,当然如果是一个线程A需要访问实例对象obj1的synchronized方法f1,另外一个线程B需要访问实例对象obj2的synchronized方法f2,这样是允许的,因为两个实例对象锁并不相同,此时如果两个线程操作的数据并非共享的,线程安全是有保障的,那如果两个线程操作的是共享数据,那么线程安全就有可能无法得到保证。如下代码可能出现这种现象:

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();
        //join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

  上述代码与前面不同在于同时创建了两个实例AccountingSyncBad,然后启动两个线程对共享变量i进行操作,操作结果不是期望的结果2000000,而是小于2000000的值,虽然使用了synchronized修饰实例方法increase,但是却new了两个不同的实例对象,这也就意味着获取的锁也不是不同的两把,因此线程安全无法保证。将increase改为静态方法,可以解决这个问题。

  2.2 修饰静态方法,作用于当前类对象加锁,进去同步代码前要获得当前类对象的锁

  当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属与任何一个实例对象,是类成员,因此通过class对象锁可以控制静态成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象,因为访问静态方法占用的锁是属于当前类对象,而访问非静态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();

        t1.join();t2.join();
        System.out.println(i);
    }
}

  注意到ynchronized关键字修饰的是静态increase方法,而increase4Obj方法是实例方法,两者的锁对象不同,两个线程分别访问将不会产生互斥现象,应该要意识到这种情况下会发生线程安全问题。

  2.3 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象锁。

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

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

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

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

//class对象锁
synchronized(AccountingSync.class){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

  了解完synchronized的基本含义及其使用方式后,下面我们将进一步深入理解synchronized的底层实现原理。

  三、synchronized底层语义原理

  Java虚拟机中的同步(Synchronized)基于进入和退出监视器(monitor)对象来实现的,无论是显示同步(有明确的monitorenter和monitorexit指令,即同步代码块)还是隐式同步(同步方法)都是如此。同步方法不是由显示的monitorenter和monitorexit指令来实现同步的,而是由方法调用指令上一个ACC_SYNCHRONIZED标志来告诉虚拟机这是个同步方法隐式实现的。

  3.1 理解Java对象头与Monitor

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

  实例变量:存放类的属性数据信息,包括弗雷德属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

  填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,理解到这就行。

  而对于顶部,则是Java头对象,它实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象相关信息是存储在java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:

虚拟机位数头对象结构说明
32/64bit Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bit Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

  其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等。考虑到JVM的空间效率,Mark Word 被设计成一个非固定的数据结构,以便存储更多有效的数据,他会根据对象本身的状态服用此存储空间按,如下所示:

    32位JVM的Mark Word:

    64位JVM的Mark Word:

 

  •  锁标志位(lock)
    区分锁状态,11时表示对象待GC 回收状态,只有最后2位锁标识(11)有效
  • biased_lock
    是否偏向锁,由于无锁和偏向锁得锁标识都是01,这里引入一位偏向锁标识位。
  • 分代年龄(age)
    表示对象被GC的次数,当该次数达到阈值的时候,对象会转移到老年代。
  • 对象的hashcode
    运行期间调用延迟计算,并把hashcode结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量级锁,重量级锁,hashcode会被转移到monitor中。
  • 偏向锁的线程ID
    偏向模式的时候,当某个线程持有对象锁的时候,对象这里会被置为该线程的id,在后面的操作中,同一个线程就无需再进行尝试获取锁的动作了。
  • epoch
    偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
  • ptr_to_lock_record
    轻量级锁状态下,指向栈中锁记录的指针。当锁获得是无竞争的时候,JVM使用cas操作而不是os互斥。在这种情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针。
  • ptr_to_heavyweight_monitor
    重量级锁状态下,指向对象监视器monitor的指针。

  其中轻量级锁和偏向锁是Java6对synchronized锁进行优化后新增的,而Java6之前都是以重量级锁来实现,锁标识位为10,其中mark word 保存了指向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对象的对象头中(存储的是指向monitor的指针),synchronized锁便是通过这种方式获取锁的。

  3.2 synchronized代码块底层原理

  如下代码定义了一个synchronized修饰的同步代码块,在代码块中操作共享变量i:

public class SyncCodeBlock {

   public int i;

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

  用javac编译上述代码并使用javap反编译后得到字节码如下:

Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
  Last modified 2017-6-2; size 426 bytes
  MD5 checksum c80bc322c87b312de760942820b4fed5
  Compiled from "SyncCodeBlock.java"
public class com.zejian.concurrencys.SyncCodeBlock
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
  //........省略常量池中数据
  //构造函数
  public com.zejian.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的持有权,当monitor进入计数器为0,那线程可以成功获取monitor,并将计数器设置为1,如果当前线程已持有 monitor,那么它可以重入这个monitor,计数器的值也会加1。倘若其他线程已经拥有monitor,那当前线程将阻塞,知道正在执行线程执行完毕,即monitor指令被执行,执行线程将释放monitor并设置计数器值为0,其他线程将有机会持有monitor。可以注意到编译器将会保证无论方法是通过何种方法完成,代码中调用过每条monitorenter指令都有执行其对应monitorexit指令,无论是正常结束还是异常结束。为了保证异常结束也能执行monitorexit指令,编译器回自动产生一个异常处理器,这个处理器可以处理所有异常,它的目的就是来执行monitorexit指令,从字节码中可以看出来多了一个monitor指令,正是这个异常处理。

  3.3 synchronized方法底层原理

  方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中,JVM根据方法标志去中的ACC_SYNCHRONIED判断是否同步方法,当方法调用如果设置有此标志位,线程将先去持有monitor,然后在执行方法,然后过程就与指令控制一样,看看字节码层面如何展示的:

public class SyncMethod {

   public int i;

   public synchronized void syncTask(){
           i++;
   }
}

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

Classfile /Users/zejian/Downloads/Java8_Action/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.zejian.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的优化(锁升级)

  锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。  

  4.1 偏向锁,什么是偏向锁,为什么要引入偏向锁?

  偏向锁是如果一个线程获取到了偏向锁,在没有其他线程竞争的情况下,如果下次在执行该同步块时则只需要简单判断当前偏向锁所偏向的对象是否时当前线程,如果是则不需要再进行任何获取锁与释放锁的过程,直接执行同步代码块,至于为什么引入偏向锁,是因为经过JVM的开发人员大量研究发现大多数时候都是不存在锁竞争的,通常都是一个线程在使用锁的时候没有其他线程来竞争,然而每次都要进行加锁和解锁就会额外增加一些没有必要的资源浪费,为了减少这类浪费,JVM引入了偏向锁。

  a) 偏向锁的获取以及升级过程:

  当一个线程在执行同步代码块时,他会先获取该对象头的Mark Word,通过MarkWord来判断当前虚拟机是否支持偏向锁(因为偏向锁是可以手动关闭的),如果不支持则直接进入轻量级锁获取过程。如果支持,则判断当前MarkWord中存储的ThreadID是否指向当前线程,如果指向当前线程,则直接开始执行同步块。如果没有指向当前线程,则通过CAS对象头的MarkWord中相应位置替换为当前线程ID表示当前线程获取到了偏向锁,如果CAS成功,同时将偏向锁标志置为1,执行同步块;若CAS失败,则表示存在多个线程竞争,当达到全局安全点(safepoint)的时候,暂停获取偏向锁的线程,撤销偏向锁(将偏向锁标志置为0,并且将threadid置为空,即设置为无锁状态),然后再将锁升级为轻量级锁,之后恢复刚暂停的线程,则刚刚CAS失败的线程通过自旋的方式等待轻量级锁被释放。
  偏向锁适用于没有线程竞争的同步场景。

  但它并不一定对程序有利,如果程序中大多数锁都是存在竞争的,那么偏向锁模式就显得多余,因此偏向锁可以通过一些JVM参数进行手动关闭。

  4.2 轻量级锁。什么是轻量级锁,为什么引入轻量级锁

  轻量级锁是当一个线程获取到锁后,另外一个线程也来获取该锁,这个线程并不会被直接阻塞,而是通过自旋来等待该锁被释放,所谓的自旋就是让线程执行一段无意义的循环。当然如果循环长时间执行也会带来非常大的资源浪费。因此自旋通常都是规定次数的,比如自旋100次等等,在JDK1.6中JVM加入了自适应自旋,通过之前获取锁锁等待的时间来增加或减少循环次数。那么如果知道自旋结束所还未被释放,那么此时轻量级锁膨胀为重量级锁,将后面的额线程全部阻塞,还有一种情况,如果线程2正在自旋等待线程1释放锁,此时线程3也来竞争锁,那么这时该轻量级锁膨胀为重量级锁将等待的线程全部阻塞。

  为什么要引入轻量级锁呢?原因是轻量级锁主要考虑到竞争线程并不多,并且持有对象锁的线程执行的时间也不长的情况,在未引入轻量级锁之前,一个线程刚刚被阻塞,这个锁就被其他线程(执行同步代码块时间短)释放,如果这种情况频繁发生,那么会因为频繁的阻塞以及唤醒线程给带来不必要的资源浪费。而在引入之后,在线程获取锁失败的情况下,线程并不会立即被阻塞,而是通过一段自旋,来等待获取锁,因此避免了频繁阻塞与唤醒操作带来的浪费。

  a) 轻量级锁的加锁、释放、以及膨胀过程

  现在线程1要访问同步块,在线程1访问同步块之前,JVM会在当前线程的栈帧中创建一个用于存储锁记录的空间(官方称为Displaced Mark Word),并且将对象头中的Mark Word复制到该锁记录中,并且将该对象的地址存储在锁记录的owner字段中。然后线程1尝试通过CAS将对象头中MarkWord对应的位置替换未当前栈帧中锁记录地址,如果cas成功则当前线程获锁成功,开始执行同步代码。如果cas失败,则进入自旋状态尝试获取该锁,如果自旋结束都没有获取成功,则该锁膨胀为重量级锁,并且阻塞后面的其他竞争该锁的线程。当获取锁的线程执行完毕,此时释放锁通过CAS将对象头中的信息重新替换回去,如果成功则线程成功释放锁,如果CAS失败则说明存在其他线程竞争此时锁已经膨胀为重量级锁,此时释放锁并唤醒其他被阻塞的线程。

  轻量级锁适用场景为:少量线程竞争锁对象,且线程持有锁的时间不长,追求相应速度的场景。

  但是如果存在大量的锁竞争,轻量级锁的效率比传统重量级锁更慢,因为最终都进入阻塞状态,但是轻量级锁还额外进行了CAS自旋操作。

  4.3 重量级锁

  重量级锁是如果多个线程同时竞争锁,只会有一个线程得到这把锁,其他线程获取锁失败会不自旋等待,而是直接阻塞,重量级锁的实现与对象内部的monitor监视器息息相关。前面有介绍过,这不不多加赘述了。

  五、锁优化

  5.1 自旋锁

  为什么引入自旋锁,Java的线程是映射到操作系统原生线程上的。如果要阻塞或者唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到内核态,这样会降低效率,因此在JDK1.6中对锁进行了一系列优化其中就包括自旋锁,在一个线程获取锁失败,它并不会立即阻塞线程,而是通过一段空循环,进行尝试过去锁状态。当然如果长时间进行这样无意义的循环对于CPU的浪费也是非常巨大的,因此JVM对于自旋是有次数规定的。比如循环100次啊等等。可是有存在这样一种情况,如果100次还是没有获取到锁,当前线程被阻塞,可是就在101次的时候这把锁被释放了,此时是不是很可惜呀!

  但是没关系,为了解决这种问题JVM团队又引入了自适应自旋,自适应自旋是这样的,此时获取这把锁的自旋此时就不是固定的被写死的,而是一种动态的,它可以通过之前这把锁的获得情况来自动的选择增加自旋此处或者减少自旋次数,如果之前有成功获取这把锁的线程,那么JVM会认为这把锁是能够被获取的,此时会自适应的增加一些自旋次数,当然如果之前没有一个线程成功获取这把锁,JVM为了避免无意义的循环带来的资源浪费,会选择减少自旋次数,或者说不去自旋,而直接阻塞。

  5.2 锁粗化

  如果一系列的连续操作都对一个对象进行加锁或者解锁操作,甚至加锁操作是出现在循环体中,那么即使没有线程竞争这些频繁的加锁和解锁操作也会导致不必要的性能损失。比如以下代码:public String getString(String s1,String s2){

  StringBuffer sb=new StringBuffer();
  sb.append(s1);   sb.append(s2);   
return sb.toString(); }

  看StringBuffer类的append方法是synchronized关键字修饰的,那么每次执行append都要进行加锁与解锁操作,这样无疑带来了性能损失,因此JVM会将当前加在append方法上的锁的范围进行粗化,粗话到第一个append方法之前到第二个append方法之后,这样只需要操作一次加锁,提高了效率。

  5.3 锁消除

  锁消除指在虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检查到不存在数据共享的锁进行消除的操作,比如下面代码:

public String getString(){
  StringBuffer sb=new StringBuffer();
  for(int i=0;i<10;i++){
    sb.append(i);
  }   
return sb.toString(); }

  StringBuffer的append方法是一个synchronized方法也就是说每个append都是要进行加锁和释放锁的。但通过观察上面代码,方法中所有用到的变量都是方法中的局部变量,这个方法中的所有对象都无法逃逸出这个方法之外。因此其他线程无法访问到它,也就不存在数据争用问题,因此此时JVM就会将这个方法中的锁进行消除。

  关于Synchronized先写到这,后续如果有新的体会,会持续更新。

posted @ 2022-10-26 14:55  梅晓煜  阅读(1081)  评论(0)    收藏  举报