【并发编程】2-Synchronized

基本概念

  1. 线程安全问题三个要素:多线程、共享资源、非原子性操作;
  2. 产生的根本原因:多条线程同时对一个共享资源进行非原子性操作;
  3. Synchronized解决线程安全问题的方式:通过互斥锁将多线程的并行执行变为单线程串行执行,同一时刻只让一条线程执行,也就是当多条线程同时执行一段被互斥锁保护的代码(临界资源)时,需要先获取锁,这时只会有一个线程获取到锁资源成功执行,其他线程将陷入等待的状态,直到当前线程执行完毕释放锁资源之后,其他线程才能执行;
  4. Synchronized可以保证可见性和有序性,但无法禁止指令重排序

Synchronized锁粒度及应用方式

Synchronized锁的三种粒度

  1. 锁粒度synchronized本质上是通过对象来加锁的,根据不同的对象类型可分为不同的锁粒度;
    1. this:当前实例锁;
    2. object:对象实例锁;
    3. class:对象锁;

应用方式

  1. 修饰实例成员方法:使用的是this锁类型,这个this代表的是当前new出来的对象;

    synchronized void method() {
        //业务代码
    }
    
  2. 修饰静态成员方法:使用的是this锁类型,但由于静态成员属于类对象,所以这个this代表的是class对象;

    synchronized static void method() {
        //业务代码
    }
    
  3. 修饰代码块:修饰代码块时,可以指定锁对象,可以将任意class类对象做为锁资源;

    synchronized(SyncIncrDemo.class) {
        //业务代码
    }
    
    1. 使用String对象加锁:因为修饰代码块时,可以将任意class类对象做为锁资源,而JAVA中字符串是以Sting对象存储使用的,并且在JVM里有个字符串常量池,用于存储字符串,那么相同值的字符串变量的引用地址可以是同一个,基于这个特性我们可以将synchronized锁粒度变细;

      如:将每一个订单的ID转化为字符串然后进行加锁,这样就能降低锁粒度提高系统并发,但在加锁时需要考虑加锁的String对象是不是同一个,需要考虑String对象使用的是堆中对象还是字符串常量池中的对象,若是堆中对象需要使用.intern()将堆中对象刷入字符串常量池中;

      /**
       * @author xrl
       * @date 2024/6/25 22:47
       */
      public class Demo1 {
          public static void main(String[] args) {
              new Thread(() ->t1(111L), "AAA").start();
              new Thread(() ->t2(111L), "BBB").start();
              new Thread(() -> t2(111L), "CCC").start();
          }
          public static void t1(Long orderId){
              // 不能使用  String lock = orderId + "; 原因是orderId是入参,而方法可能在多个地方调用
              // 所以编译器不会使用常量折叠技术对其进行优化,编译后的代码会被转化为new StringBuilder().append(orderId).append("").toString()
              String lock = new String(orderId + "").intern();
              synchronized (lock){
                  System.out.println(Thread.currentThread().getName() + "拿到锁");
      
                  try {
                      System.out.println("t1业务执行");
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
      
                  System.out.println(Thread.currentThread().getName() + "释放锁");
              }
          }
      
          public static void t2(Long orderId){
              String lock = new String(orderId + "").intern();
              synchronized (lock){
                  System.out.println(Thread.currentThread().getName() + "拿到锁");
      
                  try {
                      System.out.println("t2业务执行");
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(Thread.currentThread().getName() + "释放锁");
              }
          }
      }
      

实现原理

  1. Synchronized是基于Monitor(管程)对象实现的
    1. 获取锁:进入管程对象(显式型:monitorenter指令);
    2. 释放锁:退出管程对象(显示型:monitorexit指令);
  2. synchronized修饰方法使用时是隐式同步的,是通过调用指令,读取运行时常量池中的方法ACC_SYNCHRONIZED标识实现的,也就无法通过javap反编译看到进入/退出管程对象的指令;

JAVA对象的内存布局

  1. 分为三个区域对象头、实例数据、对齐填充

    在这里插入图片描述

    1. 对象头:存储MarkWord和类型指针(ClassMetadataAddress/KlassWord);如果为数组对象,还会存数组长度(ArrayLength

      • 主要包含unused未使用的空间HashCodeage分代年龄biased_lock是否偏向锁lock锁标记位ThreadID持有偏向锁的线程IDepoch偏向锁时间戳ptr_to_lock_record指向线程本地栈中lock_record的指针ptr_to_heavyweight_monitor指向堆中monitor对象的指针

      • 64位系统中MarkWord结构

        在这里插入图片描述

    2. 实例数据:存放当前对象的属性成员信息,以及父类属性成员信息;

    3. 对齐填充:虚拟机要求对象起始地址必须是8byte的整数倍,避免减少堆内存的碎片空间,并且方便操作系统读取;

monitor对象

  1. 概念

    1. monitor本质是一个特殊的对象,存在于堆中,并且是线程私有的
    2. 每个java对象都存在一个monitor对象与之关联,当一个monitor被某个线程持有后,便会处于锁定状态;由ObjectMonitor实现,具体代码位于HotSpot源码的ObjectMonitor.hpp文件中;
  2. 与线程之间的关联:每个线程都有一个可用的monitor record列表,同时也存在一个全局的可用列表,每一个锁住的对象,都会和monitor关联(对象头的MarkWord中的ptr_to_heavyweight_monitor,指向monitor的起始地址),同时monitor中有一个Owner字段,存放拥有该锁的线程唯一标识,表示该锁被这个线程占用;

  3. 内部结构

    在这里插入图片描述

    1. Contention List:竞争队列,**存放所有请求锁的线程(**后续1.8版本中的_cxq);
    2. Entry List:一个双向链表,存放Contention List有资格成为候选资源的线程
    3. Wait Set:哈希表,调用Object.wait()方法后,被阻塞的线程被放置在这里
    4. OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为OnDeck
    5. Owner拥有这个monitor record线程的唯一标识,为NULL说明没有被占用;
    6. !Owner:当前释放锁的线程;
    7. RcThis:表示blocked阻塞或waiting等待在该monitor record上的线程个数;
    8. Nest:用来实现重入锁的计数。
    9. Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程,唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪,然后因为竞争锁失败又被阻塞),从而导致性能严重下降。Candidate只有两种可能的值,0表示没有需要唤醒的线程;1表示要唤醒一个继任线程来竞争锁;
    10. HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
    11. EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程;
  4. 使用流程

    1. 每个获取锁或等待锁的线程都会被封装成ObjectWaiter对象;
    2. Monitor有两个队列_WaitSet_EntryList,用来保存ObjectWaiter对象列表;
    3. 当多个线程同时访问一段同步代码时,获取到锁的对象会进入到_owner区域,并将owner变量设置为当前线程的唯一标识,同时nest计数器加1,没有获取到锁的对象会加入_EntryList队列中等待;
    4. 若线程调用Object.wait()方法,会释放当前持有的monitorowenr变量回复为null,同时nest计数器减1,并将线程放入到waitSet集合中等待被唤醒;
    5. 当调用Monitor对象的notify()notifyAll() 方法来唤醒 WaitSet 中的等待线程时,会将等待线程移动到 EntryList 队列中等待获取锁的机会;

修饰代码块的原理

  1. 反编译代码

    1. 源代码

      public class SyncDemo{
          int i;
          public void incr(){
              synchronized(this){
                  i++;
              }
          }
      }
      
    2. 字节码javac SyncDemo.java javap -p -v -c SyncDemo.class

      Classfile /Users/xrl/nacos/SyncDemo.class
        Last modified 2024-5-16; size 388 bytes
        MD5 checksum 0c87d23a96c5f67c7d97908dbd338f95
        Compiled from "SyncDemo.java"
      public class SyncDemo
        minor version: 0
        major version: 52
        flags: ACC_PUBLIC, ACC_SUPER
      Constant pool:
         #1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
         #2 = Fieldref           #3.#19         // SyncDemo.i:I
         #3 = Class              #20            // SyncDemo
         #4 = Class              #21            // java/lang/Object
         #5 = Utf8               i
         #6 = Utf8               I
         #7 = Utf8               <init>
         #8 = Utf8               ()V
         #9 = Utf8               Code
        #10 = Utf8               LineNumberTable
        #11 = Utf8               incr
        #12 = Utf8               StackMapTable
        #13 = Class              #20            // SyncDemo
        #14 = Class              #21            // java/lang/Object
        #15 = Class              #22            // java/lang/Throwable
        #16 = Utf8               SourceFile
        #17 = Utf8               SyncDemo.java
        #18 = NameAndType        #7:#8          // "<init>":()V
        #19 = NameAndType        #5:#6          // i:I
        #20 = Utf8               SyncDemo
        #21 = Utf8               java/lang/Object
        #22 = Utf8               java/lang/Throwable
      {
        int i;
          descriptor: I
          flags:
      
        public SyncDemo();
          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
       /*-------synchronized修饰incr()中代码块,反汇编之后得到的字节码文件--------*/
        public void incr();
          descriptor: ()V
          flags: ACC_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 // monitorexit退出同步
              22: aload_2
              23: athrow
              24: return
            Exception table:
               from    to  target type
                   4    16    19   any
                  19    22    19   any
            LineNumberTable:
              line 5: 0
              line 6: 4
              line 7: 14
              line 8: 24
            StackMapTable: number_of_entries = 2
              frame_type = 255 /* full_frame */
                offset_delta = 19
                locals = [ class SyncDemo, class java/lang/Object ]
                stack = [ class java/lang/Throwable ]
              frame_type = 250 /* chop */
                offset_delta = 4
      }
      
      
  2. 从字节码中可知,synchronized修饰代码块,是基于进入管程monitorenter和退出管程monitorexit指令实现的,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置;
    有两条monitorexit指令的原因是解决方法异常结束时,锁释放问题;

修饰方法的原理

  1. 反编译代码

    1. 源代码

      public class SyncDemo {
          int i;
      
          public synchronized void incr() {
              i++;
          }
      }
      
    2. 字节码

      Classfile /Users/xrl/nacos/SyncDemo.class
        Last modified 2024-5-16; size 388 bytes
        MD5 checksum 0c87d23a96c5f67c7d97908dbd338f95
        Compiled from "SyncDemo.java"
      public class SyncDemo
        minor version: 0
        major version: 52
        flags: ACC_PUBLIC, ACC_SUPER
      Constant pool:
         #1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
         #2 = Fieldref           #3.#19         // SyncDemo.i:I
         #3 = Class              #20            // SyncDemo
         #4 = Class              #21            // java/lang/Object
         #5 = Utf8               i
         #6 = Utf8               I
         #7 = Utf8               <init>
         #8 = Utf8               ()V
         #9 = Utf8               Code
        #10 = Utf8               LineNumberTable
        #11 = Utf8               incr
        #12 = Utf8               StackMapTable
        #13 = Class              #20            // SyncDemo
        #14 = Class              #21            // java/lang/Object
        #15 = Class              #22            // java/lang/Throwable
        #16 = Utf8               SourceFile
        #17 = Utf8               SyncDemo.java
        #18 = NameAndType        #7:#8          // "<init>":()V
        #19 = NameAndType        #5:#6          // i:I
        #20 = Utf8               SyncDemo
        #21 = Utf8               java/lang/Object
        #22 = Utf8               java/lang/Throwable
      {
        int i;
          descriptor: I
          flags:
      
        public SyncDemo();
          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 incr();
          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:
               from    to  target type
                   4    16    19   any
                  19    22    19   any
            LineNumberTable:
              line 5: 0
              line 6: 4
              line 7: 14
              line 8: 24
            StackMapTable: number_of_entries = 2
              frame_type = 255 /* full_frame */
                offset_delta = 19
                locals = [ class SyncDemo, class java/lang/Object ]
                stack = [ class java/lang/Throwable ]
              frame_type = 250 /* chop */
                offset_delta = 4
      }
      SourceFile: "SyncDemo.java"
      xrl@xrldeMacBook-Air nacos % open/
      The file /Users/xrl/nacos/。 does not exist.
      xrl@xrldeMacBook-Air nacos % open ./ 
      xrl@xrldeMacBook-Air nacos % javac SyncDemo.java         
      xrl@xrldeMacBook-Air nacos % javap -p -v -c SyncDemo.class
      Classfile /Users/xrl/nacos/SyncDemo.class
        Last modified 2024-5-16; size 276 bytes
        MD5 checksum ef45d44f86eef93281fefeda549e1283
        Compiled from "SyncDemo.java"
      public class SyncDemo
        minor version: 0
        major version: 52
        flags: ACC_PUBLIC, ACC_SUPER
      Constant pool:
         #1 = Methodref          #4.#14         // java/lang/Object."<init>":()V
         #2 = Fieldref           #3.#15         // SyncDemo.i:I
         #3 = Class              #16            // SyncDemo
         #4 = Class              #17            // java/lang/Object
         #5 = Utf8               i
         #6 = Utf8               I
         #7 = Utf8               <init>
         #8 = Utf8               ()V
         #9 = Utf8               Code
        #10 = Utf8               LineNumberTable
        #11 = Utf8               incr
        #12 = Utf8               SourceFile
        #13 = Utf8               SyncDemo.java
        #14 = NameAndType        #7:#8          // "<init>":()V
        #15 = NameAndType        #5:#6          // i:I
        #16 = Utf8               SyncDemo
        #17 = Utf8               java/lang/Object
      {
        int i;
          descriptor: I
          flags:
      
        public SyncDemo();
          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 synchronized void incr();
          descriptor: ()V
          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 5: 0
              line 6: 10
      }
      
      
  2. 由字节码可知synchronized修饰的方法,并没有出现monitorenter指令和monitorexit指令,取得代之的是:flags: ACC_PUBLIC之后增加了一个ACC_SYNCHRONIZED标识。这个标识指明了当前方法是一个同步方法,JVM通过这个ACC_SYNCHRONIZED访问标志,来辨别一个方法是否为同步方法,从而执行相应的同步调用;

JVM对synchronized的优化

锁状态

JDK1.6之后,synchronized锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁;随着线程的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级一般是单向的,也就是说只能从低到高升级,通常不会出现锁的降级(只针对用户线程,在STW可能会发生锁降级)。

无锁状

  1. new一个对象时,会默认启动匿名偏向锁,但为了避免JVM在启动阶段大量创建对象,从而导致偏向锁竞争过多影响性能,则在JVM启动阶段会次用延迟偏向锁策略,也就是等待一定时间后再开启偏向锁,默认为4,可通过-XX:BiasedLockingStartupDelay = xx设置;
  2. 对于一个新创建的对象,由于在没有成为真正偏向锁之前,对象头markword中的线程ID会一直为空,这种被称为概念上的无锁对象,但markword的锁标识为101

偏向锁

  1. 为了减少同一线程获取锁的代价,如CAS操作带来的耗时等;
  2. 核心思想如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作;
  3. 操作流程
    1. Load-and-test,就是简单判断一下当前线程id是否与Markword中的线程id是否一致;
    2. 如果一致,则说明此线程持有的偏向锁,没有被其他线程覆盖,直接执行后续代码;
    3. 如果不一致,则要检查一下对象是否还属于可偏向状态,即检查“是否偏向锁”标志位;
    4. 如果还未偏向,则利用CAS操作来竞争锁,再次将ID放进去,即重复第一次获取锁的动作;
  4. 偏向锁主要是在锁竞争不激烈的场合对性能的提升,但是对于锁竞争比较激烈的场合,偏向锁作用就很小了,甚至成为累赘,可以通过XX:-UseBiasedLocking命令关闭;
  5. JDK15废弃;
撤销过程
  1. 在一个安全点停止拥有锁的线程;
  2. 遍历线程栈,如果存在锁记录的话,需要修复锁记录和Markword,使其变成无锁状;
  3. 唤醒当前线程,将当前锁升级成轻量级锁;
膨胀过程
  1. 当首个线程进程尝试获取锁时,会通过CAS操作,将自己的threadID设置到MarkWord中,如果设置成功,则证明拿到偏向锁
  2. 当线程再次尝试获取锁时,发现自己的线程ID和对象头中的偏向线程ID一致,则在当前线程栈的lock record锁记录中添加一个空的Displaced Mark Word(表示的是原先持有偏向锁的线程ID和相关信息被移动到哪里的标记),并不需要CAS操作;
  3. 重新偏向,当其他线程进入同步块时,发现偏向线程不是自己,则进入偏向锁撤销的逻辑;当达到全局安全点时,如果发现偏向线程挂了,那就把偏向锁撤销,并将对象头内的MarkWord修复为无锁状态,自己尝试获取偏向锁;
  4. 可如果原本的偏向线程还存活,重新偏向失败后,锁开始膨胀为轻量级锁,原来的线程仍然持有锁

轻量级锁

锁膨胀过程
  1. 根据markWork判断是否有线程持有锁,如果有则在当前线程栈中创建一个lock record复制markWord,并通过CAS将当前线程栈的lock record地址放入对象头中,如果成功则说明获取到轻量级锁
  2. 如果失败则说明锁已经被其他线程持有了,此时记录线程的重入次数(把ock recordmarkword设置为null),并进入自适应自旋
  3. 如果自旋到一定的次数后还未获取到锁,则说明目前竞争较重,则膨胀为重量级锁
自旋
  1. 自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环;
    1. 存在的问题
      1. 若加锁代码执行慢或锁竞争激烈,则需要等待较长时间,会导致CPU空转,消耗大量CPU资源;
      2. 非公平锁,若存在多个线程自旋等待同一把锁,可能对导致有些线程一致获取不到锁;
    2. 解决办法:通过-XX:PreBliockSpin给线程空循环设置一个次数,默认是10次,或者自旋线程数超过CPU核数的一半时,锁会再次膨胀升级为重量级锁;
  2. 自适应自旋锁:线程空循环次数不固定,而是会根据实际情况动态调整;
    1. 在大部分情况下,已经获取到锁的线程再次尝试获取锁,大概率能成功拿到该锁,所以虚拟机会延长这个线程的自旋次数;

重量级锁

  1. 当出现较大竞争锁膨胀为重量级锁时,对象头的markword指向堆中的monitor,此时线程会封装为一个ObjectWaiter对象,并插入到monitor_cxq竞争队列中然后挂起线程,等待锁释放;
  2. 当持有锁的线程释放后,会将_cxq竞争队列中的ObjectWaiter对象,移到EntryList中,并随机挑选一个对象也就是一个线程唤醒,被选中的线程称之为Heir Presumptive假定继承人,之后Heir Presumptive会去尝试获取锁,但在这段期间其他线程页可能尝试获取锁,所以Heir Presumptive不一定可以获取到锁,当没有获取到锁后那么它会退回到等待队列中,成为EntryList中的最后一个对象;
  3. 当线程获取到锁后,调用Object.wait()方法后,会将线程加入到WaitSet中,当被Object.notify()唤醒后,会将线程从WaitSet移动到_cxqEntryList中去,并且由于Object.wait()、Object.notify()方法主要依赖于Monitor对象实现的,所以锁对象调用这两个方法时的锁状态如果为偏向锁或轻量级锁,则会先膨胀成重量级锁;

锁膨胀过程

  1. 无锁态JVM启动后-XX:BiasedLockingStartupDelay(默认四秒)内的普通对象和四秒后的匿名偏向锁对象;
  2. 偏向锁:只有一个线程进入临界区;
    1. 偏向锁未开启:直接膨胀为轻量级锁
      1. MarkWord中锁标识位信息除外的其他所有信息copy到自己的栈内存的Lock Record中,再尝试通过CASMarkWord中的ptr_to_lock_record指向自己的栈内Lock Record,替换成功则获取到锁,没有则继续自旋;
    2. 匿名偏向锁
      1. MarkWord中锁标识位信息除外的其他所有信息copy到自己的栈内存的Lock Record中,并尝试通过CAS将自己的线程ID设置到MarkWord中;这个线程,后续再次使用这个锁时,无需进行加锁和锁释放操作,只需要在Lock Record添加一个空的MarkWord
      2. 重新偏向:当尝试获取锁时,发现线程ID与MarkWord中记录的线程ID不相同,则进入偏向锁撤销的逻辑;当达到全局安全点时,发现之前持有锁的线程执行完毕,则会发生偏向锁撤销(清除锁记录回归无锁态),然后线程可以通过CAS将自己重新设置到MarkWord
      3. 重新偏向失败:锁膨胀为轻量级锁,尝试通过CASMarkWord中的ptr_to_lock_record指向自己的栈内Lock Record,替换成功则获取到锁,没有则继续自旋;
    3. 调用Object.wait()方法,直接膨胀为重量级锁
  3. 轻量级锁多个线程交替进入临界区
    1. 当出现重度竞争、耗时过长、自旋过多等情况时会膨胀为重量级锁;
      1. 自旋线程数超过CPU核数的一半;
      2. 自旋超过-XX:PreBliockSpin默认10次;
    2. 调用Object.wait()方法,直接膨胀为重量级锁;
  4. 重量级锁:多个线程同时进入临界区;

锁状态的内存布局分析

  1. 引入架包:

    <dependency>
    	<groupId>org.openjdk.jol</groupId>
    	<artifactId>jol-core</artifactId>
    	<version>0.16</version>
    </dependency>
    
  2. MarkWord结构:

    在这里插入图片描述

  3. 运行代码:

    package com.xrl.test;
    
    import org.openjdk.jol.info.ClassLayout;
    
    /**
     * @version [v1.0]
     * @author: [xrl]
     * @create: [2024/05/21 16:14]
     **/
    public class ObjectHead {
        public String str;
    
        public static void main(String[] args) throws InterruptedException {
            /**
             无锁态:虚拟机刚启动时 new 出来的对象处于无锁状态
             **/
            ObjectHead obj = new ObjectHead();
            System.out.println(ClassLayout.parseInstance(obj).toPrintable());
            /**
             com.xrl.test.ObjectHead object internals:
             OFF  SZ               TYPE DESCRIPTION               VALUE
             0   8                    (object header: mark)     0x0000000000000001 (non-biasable; age: 0) // 对象头 non-biasable (转化为二进制最后三位表示,锁标志位状态:001)
             8   4                    (object header: class)    0xf800c105                               // ClassPointer指针
             12   4   java.lang.String ObjectHead.str            null                                    // 成员变量
             Instance size: 16 bytes                                                                     // 共占用16个字节
             Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
             */
    
    
            /**
             轻量级锁:对于真正的无锁态对象obj加锁之后的对象处于轻量级锁状态
             **/
            synchronized (obj) {
                // 查看对象内部信息
                System.out.println(ClassLayout.parseInstance(obj).toPrintable());
                /**
                 com.xrl.test.ObjectHead object internals:
                 OFF  SZ               TYPE DESCRIPTION               VALUE
                 0   8                    (object header: mark)     0x0000000309dcd9d8 (thin lock: 0x0000000309dcd9d8) // (转化为二进制最后三位表示,锁标志位状态:000)
                 8   4                    (object header: class)    0xf800c105
                 12   4   java.lang.String ObjectHead.str            null
                 Instance size: 16 bytes
                 Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
                 */
            }
    
            /**
             匿名偏向锁:休眠4S后再创建出来的对象处于匿名偏向锁状态
             **/
            Thread.sleep(4000);
            ObjectHead obj1 = new ObjectHead();
            System.out.println(ClassLayout.parseInstance(obj1).toPrintable());
            /**
             com.xrl.test.ObjectHead object internals:
             OFF  SZ               TYPE DESCRIPTION               VALUE
             0   8                    (object header: mark)     0x0000000000000005 (biasable; age: 0)  //(转化为二进制最后三位表示,锁标志位状态:101)
             8   4                    (object header: class)    0xf800c105
             12   4   java.lang.String ObjectHead.str            null
             Instance size: 16 bytes
             Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
             */
    
            /**
             重量级锁:调用wait方法之后锁对象直接膨胀为重量级锁状态
             **/
            new Thread(() -> {
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
            Thread.sleep(1);
            synchronized (obj) {
                // 查看对象内部信息
                System.out.println(ClassLayout.parseInstance(obj).toPrintable());
                /**
                 * com.xrl.test.ObjectHead object internals:
                 * OFF  SZ               TYPE DESCRIPTION               VALUE
                 *   0   8                    (object header: mark)     0x00007fe748016c0a (fat lock: 0x00007fe748016c0a) (转化为二进制最后三位表示,锁标志位状态:010)
                 *   8   4                    (object header: class)    0xf800c105
                 *  12   4   java.lang.String ObjectHead.str            null
                 * Instance size: 16 bytes
                 * Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
                 */
            }
        }
    }
    /**
     * 抛出异常原因:违法的监控状态异常。当某个线程试图等待一个自己并不拥有的对象(Obj)的监控器或者通知其他线程等待该对象(Obj)的监控器时,抛出该异常
     * Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
     * at java.lang.Object.wait(Native Method)
     * at java.lang.Object.wait(Object.java:502)
     * at com.xrl.test.ObjectHead.lambda$main$0(ObjectHead.java:53)
     * at java.lang.Thread.run(Thread.java:750)
     */
    

同步消除

  1. Java虚拟机在编译代码时,通过会对运行上下文进行扫描,从而去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的获取锁开销;

锁的可重入

  1. 一个线程获得一个对象锁后,运行他再次请求该对象锁;

  2. synchronized是基于monitor实现的,每次重入monitor中的计数器会加一;

  3. 当子类继承父类时,子类可以通过可重入锁,调用父类的同步方法;

public class Child extends Parent {
    public synchronized void childMethod() {
        System.out.println("Child method");
        // 调用另一个加锁方法,嵌套调用
        childMethod2();
        // 在子类中调用父类的同步方法
        parentMethod();
    }

    public synchronized void childMethod2() {
        System.out.println("Child method2");
        // 在子类中调用父类的同步方法
        parentMethod();
    }

    public static void main(String[] args) {
        Child child = new Child();
        child.childMethod();
    }
}

class Parent {
    public synchronized void parentMethod() {
        System.out.println("Parent method");
    }
}

其他机制

等待/唤醒机制

  1. wait()、notify()、notifyAll()这三个方法在使用时,必须处在synchronized代码块或方法中,否则会抛出IllegalMonitorStateException异常,这是由于这三个方法都依赖于monitor对象实现这也是三个方法处在Object对象中的原因,而synchronized关键字决定着一个JAVA对象会不会生成monitor对象;
  2. wait()、sleep()方法的区别:
    1. wait()方法会释放当前持有的锁,并将线程移入waitSet中;
    2. sleep()方法只会让线程休眠并不会释放锁(类似于执行for(;;){}死循环);

线程中断机制

  1. JDK1.2遗弃Thread.stop()后,JAVA就没有提供强制性停止执行中线程的方法,而是提供了协调式的方式

    //中断线程(实例方法)
    public void Thread.interrupt();
    //判断线程是否被中断(实例方法)
    public boolean Thread.isInterrupted();
    //判断是否被中断并清除当前中断状态(静态方法)
    public static boolean Thread.interrupted();
    
  2. 使用:我们可以通过调用Thread.interrupt()来进行线程中断,但由于线程中断是协调式的,所以他并不会去停止线程,而是需要我们手动进行中断检测并结束线程;并且当线程处于阻塞状态或者尝试执行一个阻塞操作时,我们调用线程中断方法,执行中断操作后会抛出InterruptedException异常;

        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(){
            @Override
            public void run(){
                try {
                    // 判断当前线程是否已中断,执行后会对中断状态进行复位
                    while (!Thread.interrupted()) {
                        System.out.println("1111");
                        // 线程阻塞,抛出异常
                        // TimeUnit.SECONDS.sleep(1);
                    }
                    System.out.println("线程中断");
                    // 输出false 说明中断状态复位
                    System.out.println(this.isInterrupted());
                } catch (Exception e) {
                    System.out.println(e);
                }
            }};
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            // 中断线程
            t1.interrupt();
        }
    
  3. synchronized与线程中断:对于synchronized而言,一个线程的执行只有两种状态,一种是获取了锁正在执行,一种是没获取到锁在阻塞额等待;那么他们即使调用中断线程的方法,也不会生效;

synchronized为什么不禁止指令重排序

  1. synchronized是通过互拆锁的方式来保证线程安全的,它的本质是将多线程并行执行变成单线程的串行执行,而指令重排序可能导致的问题是多线程环境程序乱序执行的问题,所以指令重排序对synchronized而言并不会存在乱序问题,反而可以提升串行执行时的性能;

synchronized性能不佳的原因

  1. synchronized是基于进入和退出Monitor管程实现的,而Monitor底层时依赖于操作系统的Mutex Lock,所以在其获取锁或者释放锁的时候都需要经过操作系统的调用,会涉及到频繁的用户态与内核态之间的切换,从而导致性能不佳;
  2. 但在并发竞争不高的情况下,由于synchronized几种锁状态、锁消除等技术的优化,synchronized性能并不差;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值