Java J.U.C并发包(2)——synchronized 关键字解析

概述

(1)线程安全问题是并发编程中的重点,导致线程不安全的原因主要由两种:一个是存在共享数据;另一个是存在多条线程共同操作共享数据。当多个线程操作共享数据时,我们需要保证同一时刻有且仅有一个线程再操作共享数据,其他线程等待当前线程处理完后再进行操作。
(2)synchronized关键字就是保证在同一时刻,只有一个线程执行某个方法和某个代码块(因为线程和代码块中存在共享数据)另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能)
(3)使用synchronized关键字就是对代码块或者方法进行锁定,被synchronized关键字锁定的代码块,只能同时由一条线程访问该代码。synchronized关键字锁定的是对象

因为synchronized中包含很多锁的概念,因此,下面先讲解一下各类锁

并发中的各类锁

互斥锁与内置锁

互斥锁:同时刻,最多只有一个线程能够获得互斥锁,当线程A已经获得了互斥锁,线程B尝试持有A的互斥锁时必须等待或者阻塞,直到线程A释放这个互斥锁,如果线程A不释放这个互斥锁,那么线程B将永远等待下去

Java内置锁:是一种互斥锁。每个Java对象都可以用做一个实现同步的锁,这个锁称为内置锁。线程进入同步代码块或者方法的时候会自动获得该锁,在退出同步代码块或方法的时候会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。

Java的对象锁和类锁

Java的对象锁和类锁:Java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际上有着很大的区别,对象锁是用于对象实例的方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class方法上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。但是类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁的实例方法和静态方法的区别的

synchronized关键字的三种应用场景

  • 修饰实例方法:用作于当前实例加锁,进入同步代码钱需要获得实例的锁
  • 修饰静态方法:用作于当前类对象加锁,进入同步代码前需要获得当前类对象的锁
  • 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库之前要获得给定对象的锁
修饰实例方法

代码一:

    public static void main(String [] argStrings){
        final Test1 test1 = new Test1();
        new Thread(new Runnable() {
            public void run() {
                try {
                    test1.firstMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                test1.thirdMethod();

            }
        }).start();
    }
}

class Test1{
    public synchronized void firstMethod() throws InterruptedException{
        System.out.println("firstMethod");
        Thread.sleep(2000);
    }
    public void thirdMethod(){
        System.out.println("thirdMethod");
    }
}
  • 程序的执行结果是:firstMethod secondMethod 或者 secondMethod firstMethod
  • 讲解:
    • synchronized修饰实例方法时,表示某个线程执行该方法时会锁定当前对象,其他线程不可以调用该对象中含有synchronized关键字的方法,因为这些线程的这些方法要执行前提是获得该对象的锁。但是可以调用该对象中不包含synchronized关键字修饰的方法
    • 一个对象只有一把锁,当一个线程获取该对象的锁之后,其他线程就无法获得该对象的锁,所以无法访问该对象的其他synchronized实例方法。如果两个对象访问的不是共享数据,那么线程安全是有保障的,但是如果两个线程操作的是共享数据,那么线程安全是无法保证的。

修改代码后

代码二

public class SynchronizedTest1 {
    public static void main(String [] argStrings){
        final Test1 test1 = new Test1();
        new Thread(new Runnable() {
            public void run() {
                try {
                    test1.firstMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                test1.thirdMethod();

            }
        }).start();
    }
}

class Test1{
    public synchronized void firstMethod() throws InterruptedException{
        System.out.println("firstMethod");
        Thread.sleep(2000);
    }
    public synchronized void thirdMethod(){
        System.out.println("thirdMethod");
    }
}
  • 程序的执行结果:一直都是:firstMethod thirdMethod

分析:
代码一:出现firstMethod secondMethod 或者 secondMethod firstMethod这个结果的原因是thirdMethod方法是非synchronized 方法,因此在执行这个方法thirdMethod时不需要获取当前对象的锁,firstMethod方法和thirdMethod方法时并行的

代码二:一直都是:firstMethod thirdMethod的结果的原因时因为两个方法都是synchronized方法,两个方法执行前提是可以获取当前对象的锁,所以两者是无法同时进行的,因为同一个对象中只有一把锁

修饰静态方法

当synchronized关键字修饰的是静态方法时,它时当前类的class对象锁

代码三: 一个方法是static,另一个方法是正常的方法

public class SynchronizedTest1 {
    public static void main(String [] argStrings){
        final Test2 test2 = new Test2();
        new Thread(new Runnable() {
            public void run() {
                try {
                    test2.firstMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                test2.thirdMethod();

            }
        }).start();
    }
}

class Test2{
    public static synchronized void firstMethod() throws InterruptedException{
        System.out.println("firstMethod");
        Thread.sleep(2000);
    }
    public synchronized void thirdMethod(){
        System.out.println("thirdMethod");
    }
}
  • 程序的执行结果是:firstMethod secondMethod 或者 secondMethod firstMethod

代码四: 两个方法都是static方法

public class SynchronizedTest1 {
    public static void main(String [] argStrings){
        final Test2 test2 = new Test2();
        new Thread(new Runnable() {
            public void run() {
                try {
                    test2.firstMethod();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                test2.thirdMethod();

            }
        }).start();
    }
}

class Test2{
    public static synchronized void firstMethod() throws InterruptedException{
        System.out.println("firstMethod");
        Thread.sleep(2000);
    }
    public static synchronized void thirdMethod(){
        System.out.println("thirdMethod");
    }
}
  • 程序的执行结果:一直都是:firstMethod thirdMethod
  • 解释:
    • synchronized修饰静态方法时,它表示锁定class对象,一个类只有一个class对象;
    • synchronized修饰实例方法时,表示锁定当前对象(this)
同时修饰实例方法和静态方法
public class TestSynchronized 
{  
    public synchronized void test1() 
    {  
              int i = 5;  
              while( i-- > 0) 
              {  
                   System.out.println(Thread.currentThread().getName() + " : " + i);  
                   try 
                   {  
                        Thread.sleep(500);  
                   } 
                   catch (InterruptedException ie) 
                   {  
                   }  
              }  
    }  
    public static synchronized void test2() 
    {  
         int i = 5;  
         while( i-- > 0) 
         {  
              System.out.println(Thread.currentThread().getName() + " : " + i);  
              try 
              {  
                   Thread.sleep(500);  
              } 
              catch (InterruptedException ie) 
              {  
              }  
         }  
    }  
    public static void main(String[] args) 
    {  
         final TestSynchronized myt2 = new TestSynchronized();  
         Thread test1 = new Thread(  new Runnable() {  public void run() {  myt2.test1();  }  }, "test1"  );  
         Thread test2 = new Thread(  new Runnable() {  public void run() { TestSynchronized.test2();   }  }, "test2"  );  
         test1.start();  
         test2.start(); 
    } 
}


test1 : 4
test2 : 4
test1 : 3
test2 : 3
test2 : 2
test1 : 2
test2 : 1
test1 : 1
test1 : 0
test2 : 0

上面代码synchronized同时修饰静态方法和实例方法,但是运行结果是交替进行的,这证明了类锁和对象锁是两个不一样的锁,控制着不同的区域,它们是互不干扰的。同样,线程获得对象锁的同时,也可以获得该类锁,即同时获得两个锁,这是允许的。

修饰代码块
class Test3{
    public void firstMethod() throws InterruptedException{
        synchronized (this) {
            System.out.println("firstMethod");
            Thread.sleep(2000);
        }
    }
    public void thirdMethod(){
        synchronized (this) {
            System.out.println("thirdMethod");
        }
    }
}

这里涉及到synchronized关键字的缺陷:
(1)当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。

(2)当然同步方法和同步代码块都会有这样的缺陷,只要用了synchronized关键字就会有这样的风险和缺陷。既然避免不了这种缺陷,那么就应该将风险降到最低。这也是同步代码块在某种情况下要优于同步方法的方面。例如在某个类的方法里面:这个类里面声明了一个对象实例,SynObject so=new SynObject();在某个方法里面调用了这个实例的方法so.testsy();但是调用这个方法需要进行同步,不能同时有多个线程同时执行调用这个方法。

(3)这时如果直接用synchronized修饰调用了so.testsy();代码的方法,那么当某个线程进入了这个方法之后,这个对象其他同步方法都不能给其他线程访问了。假如这个方法需要执行的时间很长,那么其他线程会一直阻塞,影响到系统的性能。

(4)如果这时用synchronized来修饰代码块:synchronized(so){so.testsy();},那么这个方法加锁的对象是so这个对象,跟执行这行代码的对象没有关系,当一个线程执行这个方法时,这对其他同步方法时没有影响的,因为他们持有的锁都完全不一样。

synchronized底层原理

JVM中的同步(Synchronization)是基于进入和退出管程(Monitor)对象实现的。

对象头与monitor(管程或监视器锁)

JVM中,对象在内存中有三个区域:对象头、实例数据和填充数据

实例对象在JVM中的存储情况

这里写图片描述

  • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
  • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可
  • 对象头:是实现同步的基础,其中monitor存放在对象头中。一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字节来存储对象头(如果对象是数组则会分配3个字节,多出来的1个字节记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:
虚拟机位数头对象结构说明
32/64bitMark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bitClass Metadata Address类型指针指向对象的类数据,JVM通过这个指针确定该对象是哪个类的实例

这里写图片描述

刚开始LockWord被设置为hashCode、最低三位表示LockWord所处的状态,初始状态为001表示无锁状态。Klass ptr指向字节码在虚拟机内部的对象表示的地址。Filed表示连续的对象实例字段。

其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构

锁状态25 bit4 bit1 bit是否是偏向锁2 bit 锁标志位
无锁状态对象的hashCode对象分代年龄001

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

这里写图片描述

其中重量级锁就是对象锁,锁的标志位是10,指针指向的是monitor的起始位置。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

Monitor Record是线程私有的数据结构,每个线程都有一个可用monitor record列表,同时还有一个全局的可用列表;每一个被锁住的对象都会和一个monitor record关联(对象投中的LockWord指向monitor record的起始位置,因为这个地址是8 byte对齐的所以LockWord的最低三位可以用来做状态位),同时monitor record中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。下图表示monitor record的内部结构

这里写图片描述

  • Owner:初始时为NULL,表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时有设置为NULL;
  • EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程
  • RcThis:表示blocked或waiting在该monitor record上的所有线程的个数
  • Nest:用来实现重入锁的计数
  • HashCode:保存从对象头拷贝过来的HashCode值(可能包含GC)
  • Candidate:用来避免不必要的阻塞或者等待线程的唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或者等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争所失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的指:0代表没有需要唤醒的线程;1代表唤醒一个继任线程来竞争锁

在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,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
这里写图片描述

这里写图片描述

当多个线程一起同时访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
(1)Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
(2)Entry List:Contention List中那些有资格程唯候选资源的线程被移动到Entry List中
(3)Wait Set:调用wait方法被阻塞的线程放置在这里
(4)OnDeck:任意时刻,最多只有一个线程正在竞争锁的资源,该线程被成为OnDeck
(5)Owner:当前已经获得到所资源的线程被称为Owner
(6)!Owner:当前释放锁的线程

JVM每次从队列的尾部去除一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问(注解:CAS指令在iIntel CPU上程唯CMPXCHG指令,它的作用是将制定内存地址的内容与所给的某个值比较,若相等,则将其内容替换为指令中提供的新值;若不等,则更新失败),为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中部分线程迁移到EntryList中,并制定EntryList中的某个线程为OnDeck线程(一般是最先进去的线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大提升系统的吞吐量,在JVM中,也把这种行为称之为“竞争切换”

OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进入EntryList中。

处于ContentionList、EntryList、WaitSet中的线程处于阻塞状态,该阻塞是由操作系统来完成的。

Monitor是一个同步工具,相当于操作系统中的互斥量,即值为1的信号量,它内置于每个object对象中。每个对象的monitor是唯一的,相当于一个许可证,线程只有拿到许可证才可以进行操作,没有拿到许可证需要阻塞等待,每个线程执行完后释放对象的monitor才可以被其他线程获取。

synchronized在JVM中的实现都是基于进入和退出monitor对象来实现方法同步和代码块同步的,虽然具体实现细节不一样,但是都可以通过成对的monitorEnter和MonitorExit指令来实现。monitorEnter指令插入在同步代码块的起始位置,当代码执行到该指令时,将会尝试获得该对象monitor的所有权,即尝试获得该对象的锁,而monitorExit指令则插入在方法结束和异常处,JVM保证每个monitorEnter必须有一个对应的monitorExit。

synchronized代码块底层原理

抽象的例子:

    syncrhoized(Object lock){
        同步代码...;
    }

该段代码在字节码文件中被编译为:


    monitorenter;       //获取monitor许可证,进入同步块
        同步代码...
    monitorexit;   //离开同步块后,释放monitor许可证

实际的例子

public class SyncCodeBlock {

   public int i;

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

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

public class com.concurrencys.SyncCodeBlock
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
  //此处应该是常量池中的数据(在此就省略常量池中数据)
  //构造函数
  public com.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  //说明方法是public的
    Code:
      stack=3, locals=3, args_size=1 
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter  //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   //monitorexit是指退出同步方法
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit //退出同步方法
        22: aload_2
        23: athrow
        24: return
      Exception table:
      //省略其他字节码.......
}
SourceFile: "SyncCodeBlock.java"

注意这里面,进入同步代码块和退出同步代码块时用到了两个指令,一个是monitorenter,另一个是monitorexit;此处与synchoronized修饰方法是完全不同的

synchronized方法底层原理

方法级的同步时隐式的同步方式,即无需通过字节码指令来控制同步,其中的同步操作是现在方法调用和方法返回之中,JVM可以从方法的常量池中的方法表结构(method_info Structure)中设置方法是否为同步方法,该方法表结构中可以包含方法的访问权限等信息。其中如果表中由ACC_SYNCHRONIZED,则表明该方法时同步方法。当方法调用时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程需要先持有monitor,然后再执行对应的方法,最后在方法完成时释放monitor。在方法执行期间,执行线程持有monitor,其他线程就无法获得同一个monitor,该线程执行完同步方法则释放monitor;若同步方法执行期间抛出异常,并且在同步方法中没有对这个异常的处理方法,那么这个同步方法持有的monitor会在异常抛出后释放。

public class SyncMethod {

   public int i;

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

反编译后的部分字节码文件:

public class com.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锁在同步代码块和同步方法上实现的基本原理。在Java早期版本中(jdk6之前),synchronized属于重量级锁,效率低,因为monitor是依赖于底层的操作系统 Mutex Lock来实现的,操作系统实现线程之间的切换需要从用户态转换为核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。在jdk6以后引入了轻量级锁和偏向锁后,就减少了获得锁和释放锁带来的性能消耗。

锁的类型

Java SE1.6中锁一共由4中状态:无锁状态、偏向锁状态、轻量级锁和重量级锁。锁的状态会随着竞争情况逐渐升级。锁可以升级但是不能降级,升级的目的时为了提高获得锁和释放锁的效率
锁的级数:(1)无锁 (2)偏向锁 (3)轻量级 (4)重量级 从左到右依次递增

偏向锁

引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,较少不必要的CAS操作

偏向锁的获取过程:

  • (1)访问Mark Word中偏向锁的标识是否设置为1,锁标志位是否为01,确认为可偏向状态
  • (2)如果为可偏向状态,则看线程ID是否制定当前线程,如果是,进入步骤5,否则进入步骤3
  • (3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程,然后执行步骤5;如果竞争失败,则执行步骤4
  • (4)如果CAS获取偏向锁失败,则表示由竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

通过Mark Word的结构可以知道,对象处在偏向锁时,mark word中的偏向锁标记为1,锁标志位为01;下面时一个从偏向锁升级到竞争锁的示例:线程1当前拥有偏向锁对象,线程2时需要竞争到偏向锁

  • (1)线程2来竞争锁对象
  • (2)判断当前对象头是否是偏向锁
  • (3)判断拥有偏向锁的线程1是否还存在
  • (4)线程1不存在,直接设置偏向所标志为0(线程1执行完毕后,不会主动释放偏向锁)
  • (5)使用cas替换偏向所线程ID为线程2,锁步升级,仍然为偏向锁
  • (6)线程1仍然存在,暂停线程1
  • (7)设置锁标志位为00(变为轻量级锁),偏向锁为0
  • (8)从线程1的空闲monitor record中读取一条,放在线程1的当前monitor record中
  • (9)更新mark word,将mark word指向线程1中monitor record指针
  • (10)继续执行线程1的代码
  • (11)锁升级为轻量级锁
  • (12)线程2 自旋来获取锁对象

偏向锁的释放:

偏向锁的释放在偏向锁获得的第4步骤中提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)状态

偏向锁的适用场景:

始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其他线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;在由锁的竞争时,偏向锁会多做很多额外操作,尤其时撤销偏向锁的时候会导致进入安全点,安全点会导致stop the word,导致性能下降,这种情况下应该禁用。

轻量级锁

轻量级锁是由偏向锁膨胀而来,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁征用的时候,偏向锁会升级为轻量级锁

轻量级锁的获取过程

  • (1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁的标志位为“01“状态,是否为偏向锁的值为”0“),JVM首先将在当前线程的栈中建立一个名为锁记录(Lock record)的空间,用于存储锁对象目前的mark word的拷贝,这个时候线程堆栈和对象头的状态如下图所示:

这里写图片描述

  • (2)拷贝对象头中的mark word复制到锁记录中
  • (3)拷贝成功后,JVM将使用CAS操作尝试将对象的Mark word更新为指向Lock record的指针,并将lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则zhi’xing’bu’zhou执行步骤5.
  • (4)如果这个公信动作成功了,那么这个线程就拥有了该对象的锁,并且对象mark word的锁标志位设置为“00”,即表示次对象处于轻量级锁定状态,这时候线程堆栈和对象头的状态如下图所示:

这里写图片描述

  • (5)如果这个更新操作失败了,JVM首先会检查对象mark word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标识的状态值变为“10“,mark word中存储的就是指向重量级锁的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁,自旋是为了不让线程阻塞,而采用循环去获取锁的过程
自旋锁

轻量级锁失败后,JVM为了避免线程真实地在操作系统层面挂起,还会进行一项程唯自旋锁的优化。这个是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换为和心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此JVM会让当前想要获得锁的线程做几个空循环,一般不会太久,可能是50或者100咯循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实可以提升效率。最后没办法也只能提升为重量级锁。

总结

synchronized执行过程:

  1. 检查mark word中是不是当前线程的id,如果时,表示当前线程处于偏向锁
  2. 如果不是,则使用CAS将当前线程的ID替换mark word, 如果成功则表示当前线程获得偏向锁,置偏向锁的标志位为1
  3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁
  4. 当前线程使用CAS将对象头的mark word替换为锁记录指针,如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程会尝试使用自旋来获取锁
  5. 如果自旋成功则依然处于轻量级状态;如果自旋失败,则升级为重量级锁。

参考博文

https://blog.csdn.net/javazejian/article/details/72828483
https://www.cnblogs.com/wl0000-03/p/5973039.html
https://blog.csdn.net/zqz_zqz/article/details/70233767

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值