synchronized 学习

 下面的代码,在命令行执行 javac,然后再执行javap -v -p,就可以看到它具体的字节码。

public class SynchronizedTest {
    
   public void methodA(){
      synchronized (this){

      }

   }
   public synchronized void methodB(){

   }

    public synchronized static void methodC(){

    }
}

编译后

E:\LearningProject2022\cloud-consumer-order80\src\test\java>javap -v SynchronizedTest.class
Classfile /E:/LearningProject2022/cloud-consumer-order80/src/test/java/SynchronizedTest.class
  Last modified 2022-3-18; size 465 bytes
  MD5 checksum 98ca0cd6cd90749035cbee8278a80719
  Compiled from "SynchronizedTest.java"
public class SynchronizedTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#17         // java/lang/Object."<init>":()V
   #2 = Class              #18            // SynchronizedTest
   #3 = Class              #19            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               methodA
   #9 = Utf8               StackMapTable
  #10 = Class              #18            // SynchronizedTest
  #11 = Class              #19            // java/lang/Object
  #12 = Class              #20            // java/lang/Throwable
  #13 = Utf8               methodB
  #14 = Utf8               methodC
  #15 = Utf8               SourceFile
  #16 = Utf8               SynchronizedTest.java
  #17 = NameAndType        #4:#5          // "<init>":()V
  #18 = Utf8               SynchronizedTest
  #19 = Utf8               java/lang/Object
  #20 = Utf8               java/lang/Throwable
{
  public SynchronizedTest();
    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 1: 0

  public void methodA();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_1
         5: monitorexit
         6: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit
        12: aload_2
        13: athrow
        14: return
      Exception table:
         from    to  target type
             4     6     9   any
             9    12     9   any
      LineNumberTable:
        line 5: 0
        line 7: 4
        line 9: 14
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 9
          locals = [ class SynchronizedTest, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public synchronized void methodB();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 12: 0

  public static synchronized void methodC();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 16: 0
}
SourceFile: "SynchronizedTest.java"

 

可以看到,同步代码块在字节码是通过 monitorenter 和monitorexit 两个指令进行控制的。

同步方法在字节码的体现上,它只给方法加了一个 flag:ACC_SYNCHRONIZED

monitorenter指令介绍

每个对象都与一个monitor 相关联。当且仅当拥有所有者时(被拥有),monitor才会被锁定。执行到monitorenter指令的线程,会尝试去获得对应
的monitor,如下: 每个对象维护着一个记录着被锁次数的计数器, 对象未被锁定时,该计数器为0。线程进入monitor(执行monitorenter指令)时,会把计数器设置为1. 当同一个线程再次获得该对象的锁的时候,计数器再次自增. 当其他线程想获得该monitor的时候,就会阻塞,直到计数器为0才能成功。

monitorexit指令介绍

monitor的拥有者线程才能执行 monitorexit指令。

线程执行monitorexit指令,就会让monitor的计数器减一。如果计数器为0,表明该线程不再拥有monitor。其他线程就允许尝试去获得该monitor了

ACC_SYNCHRONIZED介绍

方法级别的同步是隐式的,作为方法调用的一部分。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。

当调用一个设置了ACC_SYNCHRONIZED标志的方法,执行线程需要先获得monitor锁,然后开始执行方法,方法执行之后再释放monitor锁,当方法不管
是正常return还是抛出异常都会释放对应的monitor锁。 在这期间,如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。 如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

 

虽然显示效果不同,但他们都是通过 monitor 来实现同步的。

其中在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 用来记录该对象被线程获取锁的次数,这也说明了synchronized是可重入的
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;  // 指向持有ObjectMonitor对象的线程
    _WaitSet      = NULL;  // 处于wait状态的线程,会被加入到_WaitSet,调用了wait方法之后会进入这里
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

每个 Java 对象在 JVM 的对等对象的头中保存锁状态,指向 ObjectMonitor。

ObjectMonitor 保存了当前持有锁的线程引用,EntryList 中保存目前等待获取锁的线程,WaitSet 保存 wait 的线程。

还有一个计数器count,每当线程获得 monitor 锁,计数器 +1,当线程重入此锁时,计数器还会 +1。当计数器不为 0 时,其它尝试获取 monitor 锁的线程将会被保存到EntryList中,并被阻塞。

当持有锁的线程释放了monitor 锁后,计数器 -1。当计数器归位为 0 时,所有 EntryList 中的线程会尝试去获取锁,但只会有一个线程会成功,没有成功的线程仍旧保存在 EntryList 中。

 

详细流程:

  1. 加锁时,即遇到Synchronized关键字时,线程会先进入monitor的_EntryList队列阻塞等待。
  2. 如果monitor的_owner为空,则从队列中移出并赋值与_owner
  3. 如果在程序里调用了wait()方法,则该线程进入_WaitSet队列。我们都知道wait方法会释放monitor锁,即将_owner赋值为null并进入_WaitSet队列阻塞等待。这时其他在_EntryList中的线程就可以获取锁了。
  4. 当程序里其他线程调用了notify/notifyAll方法时,就会唤醒_WaitSet中的某个线程,这个线程就会再次尝试获取monitor锁。如果成功,则就会成为monitor的owner。
  5. 当程序里遇到Synchronized关键字的作用范围结束时,就会将monitor的owner设为null,退出。

 

保证可见性

monitorenter 指令之后会有一个 Load 屏障,执行refresh处理器缓存操作,把别的处理器修改过的最新的值加载到自己的高速缓存中,

monitorexit 指令之后会有一个 Store 屏障,让线程把自己修改的变量都执行flush处理器缓存操作,刷到高速缓存或是主内存中

保证有序性

在 monitorenter 指令和 Load 屏障之后,会加一个 Acquire屏障,这个屏障的作用是禁止读操作和读写操作之间发生指令重排,

在 monitorexit 指令前加一个Release屏障,也是禁止写操作和读写操作之间发生重排序。


 

锁的优化

相比于 JDK 1.5,在 JDK 1.6 中 HotSopt 虚拟机对 Synchronized 内置锁的性能进行了很多优化,包括自适应的自旋、锁消除、锁粗化、偏向锁、轻量级锁等。

自适应的自旋锁

在 JDK 1.6 中引入了自适应的自旋锁来解决长时间自旋的问题。

比如,如果最近尝试自旋获取某一把锁成功了,那么下一次可能还会继续使用自旋,并且允许自旋更长的时间;但是如果最近自旋获取某一把锁失败了,那么可能会省略掉自旋的过程,以便减少无用的自旋,提高效率。

锁消除

经过逃逸分析之后,如果发现某些对象不可能被其他线程访问到,那么就可以把它们当成栈上数据,栈上数据由于只有本线程可以访问,自然是线程安全的,也就无需加锁,所以会把这样的锁给自动去除掉。

锁粗化

按理来说,同步块的作用范围应该尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,缩短阻塞时间,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。

但是加锁解锁也需要消耗资源,如果存在一系列的连续加锁解锁操作,可能会导致不必要的性能损耗。

锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。

偏向锁/轻量级锁/重量级锁

JVM 默认会优先使用偏向锁,如果有必要的话才逐步升级,这大幅提高了锁的性能。

 

锁升级

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁

随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级

从JDK 1.6中默认是开启偏向锁,可以通过-XX:-UseBiasedLocking来禁用偏向锁

偏向锁

在只有一个线程使用了锁的情况下,偏向锁能够保证更高的效率。

具体过程是这样的:当第一个线程第一次访问同步块时,会先检测对象头 Mark Word 中的标志位 Tag 是否为 01,以此判断此时对象锁是否处于无锁状态或者偏向锁状态。

线程一旦获取了这把锁,就会把自己的线程 ID 写到 MarkWord 中,在其他线程来获取这把锁之前,锁都处于偏向锁状态。

当下一个线程参与到偏向锁竞争时,会先判断 MarkWord 中保存的线程 ID 是否与这个线程 ID 相等,如果不相等,会立即撤销偏向锁,升级为轻量级锁。

轻量级锁

当锁处于轻量级锁的状态时,就不能够再通过简单地对比标志位 Tag 的值进行判断,每次对锁的获取,都需要通过自旋。

当然,自旋也是面向不存在锁竞争的场景,比如一个线程运行完了,另外一个线程去获取这把锁;但如果自旋失败达到一定的次数,锁就会膨胀为重量级锁。

重量级锁

重量级锁,这种情况下,线程会挂起,进入到操作系统内核态,等待操作系统的调度,然后再映射回用户态。系统调用是昂贵的,所以重量级锁的名称由此而来。

如果系统的共享变量竞争非常激烈,锁会迅速膨胀到重量级锁,这些优化就名存实亡。

如果并发非常严重,可以通过参数-XX:-UseBiasedLocking 禁用偏向锁,理论上会有一些性能提升,但实际上并不确定。

 


 

 

 

 

 去创建的对象里获取对象头的锁,非静态方法是竞争同一个对象的锁

 

 

 

 

 

 静态方法是基于类的,虽然是两个方法,但是对应同一个类,静态方法是对类进行上锁的。

 

 

 

 

 

 this 代表当前对象,锁的this其实是锁的当前对象,和第一个例子一个道理。

 

 

多线程访问同步方法的几种情况:

 

两个线程同时访问一个对象的同步方法。

 

由于同步方法锁使用的是this对象锁,同一个对象的this锁只有一把,两个线程同一时间只能有一个线程持有该锁,所以该方法将会串行运行。

 

两个线程访问的是两个对象的同步方法。

 

由于两个对象的this锁互不影响,Synchronized将不会起作用,所以该方法将会并行运行。

 

两个线程访问的是Synchronized的静态方法。

 

Synchronized修饰的静态方法获取的是当前类模板对象的锁,该锁只有一把,无论访问多少个该类对象的方法,都将串行执行。

 

同时访问同步方法与非同步方法

 

非同步方法不受影响。

 

访问同一个对象的不同的普通同步方法。

 

由于this对象锁只有一个,不同线程访问多个普通同步方法将串行运行。

 

同时访问静态Synchronized和非静态Synchronized方法

 

静态Synchronized方法的锁为class对象的锁,非静态Synchronized方法锁为this的锁,它们不是同一个锁,所以它们将并行运行。

 

 

https://mp.weixin.qq.com/s/zAEwHgim-rXbo4-OjmKG2Q

 

posted @ 2022-03-18 15:47  Nausicaa0505  阅读(39)  评论(0编辑  收藏  举报