synchronized底层原理实现

实现synchronized的基础

  • java对象头
  • Monitor

对象在内存中的布局

hotspot虚拟机中,对象在内存中的布局分为三个区域

  • 对象头
  • 实例数据
  • 对齐填充
    这里只讲对象头
对象头的结构

在这里插入图片描述
Class Metadata Address是指向类元数据的指针,JVM通过这个指针来确定这个对象是哪个类的实例。
Mark Word用于存储对象自身的运行时数据,是实现轻量级锁和偏向锁的关键。
在这里插入图片描述
由于对象头的信息是与自身数据无关的额外存储成本,因此,考虑到JVM的空间效率Mark Word被设计为非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态,复用自己的存储空间,如32位的JVM上,除了上图中Mark Word默认的存储结构外,还有如下变化的结构,其中轻量级锁和偏向锁是Java6后对synchronized锁进行优化后新增加的。
主要说重量级锁,也就是synchronized锁,锁的标志位为10,其中指针指向的是Monitor的起始地址,每一个对象都有一个Monitor对象与之关联。对象与Monitor的关联实现有多种方式。Monitor可以和对象一起创建和销毁,或当线程试图获取锁时自动生成。但当一个Monitor被持有后,它变处于锁定状态

Monitor

每个Java对象天生自带了一把看不见的锁,叫做内部锁或Monitor锁。
可以把它理解为一个同步工具,也可以描述为一种同步机制,通常被描述为一个对象。

在hotspot虚拟机中Monitor是由ObjectMonitor来实现的,它是通过c++来实现的。
查看源码
在这里插入图片描述
源码地址

http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/741cd0f77fac/src/share/vm/runtime/objectMonitor.hpp

其中有等待池_WaitSet和锁池_EntryList,它们用来保存ObjectWaiter的列表,每个对象锁的线程都会被封装成ObjectWaiter来保存到里面。其中有个字段_owner,是指向持有ObjectMonitor对象的线程,当多个线程同时访问同一段同步代码时,首先会进入_EntryList中,当线程获取到对象的Monitor后,进入Object区域,并把Object的_owner设置为当前线程,其中_count就会加1,若线程调用wait方法,则释放当前持有的Monitor,_owner被恢复为null,_count减1,ObjectWaiter实例会进入到_waitSet集合中等待被唤醒。若当前线程执行完毕,将释放Monitor锁,并复位对应变量的值,以便其他线程进入获取Monitor锁。
在这里插入图片描述
上图即使ObjectMonitor的结构。Monitor对象存在于每个Java对象的对象头中,synchronized锁就是通过这种方式去获取锁的。这也是java对象中任意对象都可以为锁的原因。

查看字节码

public class SyncBlockAndMethod {
    public void syncsTask(){
        synchronized (this){
            System.out.println("Hello");
        }
    }
    public synchronized void syncTask(){
        System.out.println("Hello again");
    }
}

使用javac命令编译出.class文件

javac .java文件路径

使用javap命令反编译.class文件,查看字节码

javap -verbose .class文件路径

编译后文件

  Compiled from "SyncBlockAndMethod.java"
public class com.gclhaha.javabasic.jvm.thread.SyncBlockAndMethod
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #26            // Hello again
   #6 = Class              #27            // com/gclhaha/javabasic/jvm/thread/SyncBlockAndMethod
   #7 = Class              #28            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               syncsTask
  #13 = Utf8               StackMapTable
  #14 = Class              #27            // com/gclhaha/javabasic/jvm/thread/SyncBlockAndMethod
  #15 = Class              #28            // java/lang/Object
  #16 = Class              #29            // java/lang/Throwable
  #17 = Utf8               syncTask
  #18 = Utf8               SourceFile
  #19 = Utf8               SyncBlockAndMethod.java
  #20 = NameAndType        #8:#9          // "<init>":()V
  #21 = Class              #30            // java/lang/System
  #22 = NameAndType        #31:#32        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello
  #24 = Class              #33            // java/io/PrintStream
  #25 = NameAndType        #34:#35        // println:(Ljava/lang/String;)V
  #26 = Utf8               Hello again
  #27 = Utf8               com/gclhaha/javabasic/jvm/thread/SyncBlockAndMethod
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/Throwable
  #30 = Utf8               java/lang/System
  #31 = Utf8               out
  #32 = Utf8               Ljava/io/PrintStream;
  #33 = Utf8               java/io/PrintStream
  #34 = Utf8               println
  #35 = Utf8               (Ljava/lang/String;)V
{
  public com.gclhaha.javabasic.jvm.thread.SyncBlockAndMethod();
    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

  public void syncsTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         7: ldc           #3                  // String Hello
         9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        12: aload_1
        13: monitorexit
        14: goto          22
        17: astore_2
        18: aload_1
        19: monitorexit
        20: aload_2
        21: athrow
        22: return
      Exception table:
         from    to  target type
             4    14    17   any
            17    20    17   any
      LineNumberTable:
        line 9: 0
        line 10: 4
        line 11: 12
        line 12: 22
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 17
          locals = [ class com/gclhaha/javabasic/jvm/thread/SyncBlockAndMethod, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public synchronized void syncTask();
    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 Hello again
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 14: 0
        line 15: 8
}
SourceFile: "SyncBlockAndMethod.java"

其中查看syncsTask
在这里插入图片描述
monitorenter指向同步代码块的开始位置,首先获取PrintStream类,传入Hello,调用println打印。monitorexit指明同步代码块结束的位置。当monitorenter指令时,当前线程将试图获取对象锁,以及ObjectRef所对应的持有权,当ObjectRef的count为0时,线程就可以获取monitor,并将计数器设置为1,表示取锁成功。如果当前线程在之前已经拥有的ObjectRef的持有权,它可以重入这个monitor。
在这里插入图片描述
在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个方法调用synchronized方法中,在其方法内部调用另一个synchronized方法,是允许的。例如:

 public void syncsTask(){
     synchronized (this){
         System.out.println("Hello");
         synchronized (this){
             System.out.println("World");
         }
     }
 }

如果其他线程先于当前线程拥有objectRef的monitor所有权,当前线程将会阻塞在monitorEnter这里,直到持有该锁的线程执行完毕,即monitorexit执行,将释放monitor锁,并设置计数器为0,其他线程将有机会持有monitor。为了保证在方法异常完成时monitorextermonitorexit依然可以配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可以处理所有异常,它的作用就是去执行monitorexit指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时,被释放monitor的指令。

接下来看看syncTask方法

  public synchronized void syncTask();
    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 Hello again
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 14: 0
        line 15: 8

可以看到ACC_SYNCHRONIZED访问标志来区分一个方法是不是同步方法,方法调用时,调用指令会检查ACC_SYNCHRONIZED访问标志是否被设置,方法运行将生成monitor,无论方法正常还是异常结束,都会释放monitor,在方法执行期间,执行线程持有了monitor,其他线程无法再获得同一个monitor。如果一个同步方法运行期间抛出异常,并且在方法内部无法处理此异常,这个同步方法的所持有的monitor,将在同步方法把异常抛到同步方法外时自动释放。

synchronized进化

在早期Java版本中,synchronized属于重量级锁,依赖Mutex Lock实现。线程之间切换需要从用户态转换到核心态,开销较大。但在Java6以后,synchronized性能得到了很大的提升。其中有:

  • Adaptive Spinning 自适应自旋
  • Lock Eliminate 锁消除
  • Lock Coarsening 锁粗化
  • Lightweight Locking 轻量级锁
  • Biased Locking 偏向锁
  • ……
    这些都是为了在线程之间更高效的解决竞争问题,从而提高程序的执行效率。

自旋锁和自适应自旋锁

自旋锁

许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得。可以让没有获取锁的线程多等待一会儿,但不放弃cpu的执行时间,这就是自旋。
通过让线程执行忙循环,等待锁的释放,不让出cpu。类似while(true)一样去等待持有锁的线程释放锁,但又不像sleep一样去放弃cpu的执行时间。
自旋锁在Java4就已被引入,只是当时默认是关闭的,在Java6后默认为开启状态。在本质上,自旋锁和阻塞并不相同,不考虑对多处理器的要求,如果锁占的时间非常短,那么自旋锁的性能就会很好,相反,如果锁被其他线程长时间占用,会带来许多性能上的开销。自旋始终会占用cpu的时间片,如果占用时间太长,自旋线程会白白消耗资源,所以等待的时间要有一定的限度,如果自旋超过限定的尝试次数限制后,仍然没有成功获取到锁,就应该用传统的方式去挂起线程。在jdk中,可以使用PreBlockSpin参数来更改。

自适应自旋锁

自旋的次数不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程在运行中,那么JVM会认为该锁自旋获取到锁的可能性比较大,会自动增加等待时间,比如增加到50次循环。相反,如果对于某一个锁自旋很少成功获取到锁,那在以后要获取这个锁时,将可能省略掉自旋过程,以免浪费处理器资源。有了自适应自旋,JVM对程序的锁的预测会越来越精准。

锁消除

更彻底的优化

JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。可以节省毫无意义的锁请求时间。

public class StringBufferWithoutSync {
    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) {
        StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
        for (int i = 0; i < 1000; i++) {
            withoutSync.add("aaa","bbb");
        }
    }
}

锁粗化

在加同步锁的时候,尽可能的将同步块限制在尽量小的范围,只在共享作用域中才进行同步,这是为了使得需要同步的数据尽可能的小,在等待锁同步的进程中,得以尽可能快的拿到锁。大部分情况下是完美准确的,但是存在一连串对同一个对象反复加锁和解锁,甚至加锁操作在循环体中,即使没有锁的竞争,进行互斥锁操作,也会导致不必要的性能损耗。所以可以用一下方式来解决:

  • 通过扩大锁的范围,避免反复加锁和解锁。
public class CoarseSync {
    public static String copyString100Times(String target) {
        int i = 0;
        StringBuffer sb = new StringBuffer();
        while (i < 100) {
            sb.append(target);
            i++;
        }
        return sb.toString();
    }
}

如上代码,while代码只会被加一次锁。

synchronized的四种状态

  • 无锁、偏向锁、轻量级锁、重量级锁
    锁会随着竞争情况逐渐升级
    在这里插入图片描述
偏向锁:减少同一线程获取锁的代价
  • 大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得
    在这里插入图片描述
    不适合锁竞争比较激烈的多线程场合
轻量级锁

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

锁的内存语义

在这里插入图片描述

偏向锁、轻量级锁、重量级锁的汇总

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值