JUC源码解析: 深入解读 synchronize锁!

JUC源码解析: 深入解读 synchronize锁!

synchronize 是常用的锁,我们知道它可是用在方法上、代码块上,用法简单,但深入起来可要大费周章,下面跟着我深入一下吧。

synchronize 影响线程状态

在 Thread.Sate.class 源码中, 线程被定义为六种状态,(也可以更细分为七种, 详见我的另一篇博文 JUC源码讲解:线程状态转换)

线程进入 sync 时, 会进入 BLOCKED 状态, 而 local() 锁不会让线程进入 BLOCKED 状态, 这是一个简单的区别

退出 sync 时, 线程会进入 RUNNABLE 状态, 这个状态又可细分

RUNNABLE: 线程正在运行, 细分的两个状态会互相转换

  • RUNNING: 线程拿到了CPU时间片, 使用yield() 或没有拿到CPU时间片会进入 READY状态
  • READY: 线程没有拿到CPU时间片, 进入等待队列, 获取到时间片后会进入 RUNNING 状态

这就是 sync 对线程状态的影响了

synchronize 的三种使用方法

sync 有三种使用方式

  • 在普通方法上
  • 在静态方法上
  • 在代码块上

那么,这样使用的时候,sync 是用什么作为锁的呢?


sync 可以使用在普通方法上:

public synchronized void syncMethod() {}

这时候,要想使用普通方法一定要new一个对象,它的锁是new的那个对象的锁

例如 new Main().syncMethod(), 它使用的锁就是 new Main() 这个对象


sync 可以使用在静态方法上:

public static synchronized void staticSyncMethod() {}

它使用的锁, 是 类.class , 这个 class 在 jvm 中是唯一的

sync 可以使用在代码块上

public void method() {
    synchronized (this) {

    }
}

它锁是我们手动指定的

接下来,我们看看 jvm 里对于 sync锁是怎么标记的吧.

在资源管理器中找到我们写的这个java文件,使用 javap -v xxx.java 命令看看

像我这样,我是把结果输出到一个文件里了

javap -v Main.class >> ./1.txt

我先把输出贴出来,然后再大概讲解一下:

Classfile /D:/Code/Java/netty-study/SY-IM/target/classes/yihe/de/yy/Main.class
  Last modified 2024年3月14日; size 683 bytes
  SHA-256 checksum f32532942a1c25993483b3489e6400c45fa0e7922e391ab9251edbc7342637a6
  Compiled from "Main.java"
public class yihe.de.yy.Main
  minor version: 0
  major version: 51
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #2                          // yihe/de/yy/Main
  super_class: #3                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 5, attributes: 1
Constant pool:
   #1 = Methodref          #3.#24         // java/lang/Object."<init>":()V
   #2 = Class              #25            // yihe/de/yy/Main
   #3 = Class              #26            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lyihe/de/yy/Main;
  #11 = Utf8               syncMethod
  #12 = Utf8               staticSyncMethod
  #13 = Utf8               method
  #14 = Utf8               StackMapTable
  #15 = Class              #25            // yihe/de/yy/Main
  #16 = Class              #26            // java/lang/Object
  #17 = Class              #27            // java/lang/Throwable
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               args
  #21 = Utf8               [Ljava/lang/String;
  #22 = Utf8               SourceFile
  #23 = Utf8               Main.java
  #24 = NameAndType        #4:#5          // "<init>":()V
  #25 = Utf8               yihe/de/yy/Main
  #26 = Utf8               java/lang/Object
  #27 = Utf8               java/lang/Throwable
{
  public yihe.de.yy.Main();
    descriptor: ()V
    flags: (0x0001) 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 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lyihe/de/yy/Main;

  public synchronized void syncMethod();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 13: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lyihe/de/yy/Main;

  public static synchronized void staticSyncMethod();
    descriptor: ()V
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 15: 0

  public void method();
    descriptor: ()V
    flags: (0x0001) 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 18: 0
        line 20: 4
        line 21: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   Lyihe/de/yy/Main;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 9
          locals = [ class yihe/de/yy/Main, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 26: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  args   [Ljava/lang/String;
}
SourceFile: "Main.java"

先看看普通方法 syncMethod() 在 jvm 中是怎么标记 sync 的:

再看看静态方法 staticSyncMethod() 在jvm 中是怎么标记 sync 的:

和普通方法差不多

再看看在代码块中是怎么标记 sync 的:

找到 sync 所在的 method() 方法,我们看!

使用 monitorenter 标记 sync 的入口, monitorexit 标记 sync 的出口.

有两个 monitorexit , 是为了防止在发生异常后不能释放锁

这就是对sync三种用法的说明了

synchronize 的四种特性

sync有三种特性, 我做一下讲解:

有序性: 读读, 读写, 写读, 写写 互斥

  • sync 让一个线程对其他线程完全互斥,以保障线程间的有序性

可见性: sync 的可见性体现在两个方面

  • 锁状态对其他线程可见: 上一节提到过, 在 jvm 中,通过标记 ACC_SYNCHRONIZE, monitorenter, monitorexit 来保证锁状态对其他线程是可见的
  • 变量的可见, sync 在释放锁之前会将变量的修改刷新到共享内存中, 是可见的

原子性: 因为对其他线程的完全互斥, 保障了原子性

可重入性: sync 可以嵌套

synchronize 的 Mark Word 锁标记

synchronize 有好几种状态, 在 32位虚拟机中是这样标记的

  • 无锁状态: 锁标志位 01, 是否为偏向锁 0
  • 偏向锁: 锁标志位 01(和无锁状态相同), 是否为偏向锁 1
    • 偏向锁里会存储偏向线程的id
  • 轻量级锁: 锁标志位 00
  • 重量级锁: 锁标志位 10
  • GC标记: 锁标志位 11 (要被GC回收了,会有这个标记)

如图: 着重看 "1bit是否为偏向锁" 和 "2bit锁标志位" 两个参数就好

看偏向锁这一栏, 没有地方存指针,而是存了 线程ID 和 Epoch , 这一点尤为关键, 意味着偏向锁不能存储对象头的 hashCode, 而其他锁是有地方存的. 也就意味着, 当锁对象被隐式(父类)或显试调用了 hashcode 时, 就不能进入偏向锁! , 之后我会在锁升级的章节中着重介绍

posted @ 2024-03-22 17:17  yangruomao  阅读(40)  评论(0编辑  收藏  举报