并发编程原理与实战(二十六)深入synchronized底层原理实现

锁在保证线程安全的过程中起着关键性的作用。在第二篇文章和第十五篇文章中,我们对锁以及synchronized关键字已经有了一定的了解。本文对synchronized进一步学习,深入分析其底层的实现原理。

synchronized使用

先来看看官方对synchronized使用说明。

同步语句

The synchronized statement (§14.19) computes a reference to an object; it then
attempts to perform a lock action on that object's monitor and does not proceed
further until the lock action has successfully completed. After the lock action has
been performed, the body of the synchronized statement is executed. If execution
of the body is ever completed, either normally or abruptly, an unlock action is
automatically performed on that same monitor.

当执行 synchronized 修饰的语句块时,首先计算对象的引用,随后尝试‌在该对象的监视器‌上执行锁定操作,并在锁定成功后才继续执行后续代码。如果同步块内的代码执行完成,无论正常结束或异常中断,在该监视器上将会自动执行一个解锁操作。

同步方法

A synchronized method (§8.4.3.6) automatically performs a lock action when it is
invoked; its body is not executed until the lock action has successfully completed. If
the method is an instance method, it locks the monitor associated with the instance
for which it was invoked (that is, the object that will be known as this during
execution of the body of the method). If the method is static, it locks the monitor
associated with the Class object that represents the class in which the method is
defined. If execution of the method's body is ever completed, either normally or
abruptly, an unlock action is automatically performed on that same monitor.

一个synchronized修饰符的方法被调用时将会执行一个锁定操作,synchronized代码块中的代码不会执行直到锁定操作成功完成。如果方法是一个实例方法,锁定的是当前实例关联的监视器(即方法体内的 this 对象)。如果方法是一个静态方法,锁定的该方法所属类的 Class 对象关联的监视器。如果方法体执行完成,无论正常结束或异常中断,在该监视器上将会自动执行一个解锁操作。

基于上述语法说明,才有了常见的synchronized的三种用法,回顾下代码,如下:

public class SynchronizedTest {
    public static void main(String[] args) {
        System.out.println("synchronized class");
    }
	//修饰普通方法
    public synchronized void output(String name) {
        for(int i=0; i<5; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name);
        }
    }
	//修饰代码块
    public void output2(String name) {
        synchronized(this) {
            for(int i=0; i<5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(name);
            }
        }
    }
	//修饰静态方法
    public static synchronized void output3(String name) {
        for(int i=0; i<5; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name);
        }
    }
}

死锁处理机制

在使用synchronized加锁时,开发者并不需要代码显示的控制死锁的问题。

The Java programming language neither prevents nor requires detection of
deadlock conditions. Programs where threads hold (directly or indirectly) locks
on multiple objects should use conventional techniques for deadlock avoidance,
creating higher-level locking primitives that do not deadlock, if necessary.
Other mechanisms, such as reads and writes of volatile variables and the use
of classes in the java.util.concurrent package, provide alternative ways of
synchronization.

Java编程语言既不阻止也不要求检测‌死锁现象。若线程直接或间接持有多个对象锁,开发者需采用以下策略:使用常规的死锁避免技术,必要时创建‌高级无死锁锁原语‌。‌替代同步方案‌的其他机制,‌比如volatile 变量‌的读写操作,java.util.concurrent 包中的并发工具类,提供了其他可选的同步方案。

底层实现原理

用到synchronized关键字就绕不开对象的监视器‌。关于对象监视器‌,或许还有很多疑问。

(1)什么是对象的监视器‌?

(2)如何查看对象监视器‌的状态信息?

(3)synchronized是如何上锁和解锁的?我们已经学习了Lock相关的锁,这类锁上锁和解锁都有显式Lock和unLock操作,那么synchronized是如何实现的?

(4)synchronized既然是一个独占锁,那么其他抢锁的等待者记录在哪里?

(5)synchronized既然可重入,那么重入计数记录在哪里?

(6)持有监视器锁的线程线程信息记录在哪里?

针对这些问题,只有深入其底层实现才能搞懂。

对象监视器锁

首先,先来看看什么是对象的监视器‌,下面是官方文档关于对象监视器的描述:

The Java programming language provides multiple mechanisms for
communicating between threads. The most basic of these methods is
synchronization, which is implemented using monitors. Each object in Java is
associated with a monitor, which a thread can lock or unlock. Only one thread at
a time may hold a lock on a monitor. Any other threads attempting to lock that
monitor are blocked until they can obtain a lock on that monitor. A thread t may
lock a particular monitor multiple times; each unlock reverses the effect of one
lock operation.

java编程语言提供了多种线程间通信的机制,通过监视器实现的同步是最基础的一种方法,这个是实现线程间同步的核心机制。Java中每个对象都有一个关联的监视器对象,线程可以对该对象进行加锁(lock)和解锁(unlock)操作。同一时刻仅允许一个线程持有监视器对象,其他尝试锁定该监视器的线程将被阻塞,直至获取锁成功。一个线程可多次锁定同一监视器对象,每次解锁操作抵消一次锁定。

由此可见,基于监视器实现的锁具有独占性和可重入性(多次上锁)。而synchronized正是基于对象监视器实现的锁。

查看对象监视器的状态信息

了解了什么是对象监视器之后,接下来解决如何查看对象监视器‌的状态信息这个问题。下面的代码用主线程和一个子线程分别不断地调用synchronized修饰的output()方法,然后输出线程的ID。

public class SynchronizedTest {
    public static void main(String[] args) {
        new Thread(() -> {
            for (; ; ) {
                output("t1 thread running");
            }
        }, "t1").start();

        for (; ; ) {
            output("main thread running");
        }
    }

    public static synchronized void output(String name) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name + " thread tid " + Thread.currentThread().getId() + ", thread nid " + getOsThreadId(Thread.currentThread()));
    }

    public static long getOsThreadId(Thread thread) {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        return threadMXBean.getThreadInfo(thread.getId()).getThreadId();
    }
}

输出结果

main thread running thread tid 1
main thread running thread tid 1
t1 thread running thread tid 23
t1 thread running thread tid 23
...

输出线程ID的目的是,接下来使用jstack命令查看线程堆栈信息和对象监视器的状态时用到,根据线程ID查找对应的堆栈信息。在windows的任务管理中找到该程序的PID号为10776,然后使用jstack -l 10776命令查看Java进程的线程堆栈及锁状态详情,也可以通过jstack -l 10776 > thread_dump.log命令输出到thread_dump.log文件中。。我们摘取相main线程和t1线程的堆栈信息来分析,如下图:
在这里插入图片描述
在这里插入图片描述

从main线程和t1线程的堆栈信息可以看出:

(1)线程名称和线程ID和上述程序输出结果一致。

(2)main线程在调用output()方法进入TIMED_WAITING睡眠状态时,仍然处于持有锁的状态,所以此时的t1线程处于BLOCKED阻塞状态,等待main线程释放监视器锁。这个符合我们的程序逻辑。

(3)main线程的堆栈信息中的locked <0x0000000754d98f08>是main线程持有的监视器地址,进一步说明main线程正持有监视器锁。

(4)t1线程的堆栈信息中的waiting to lock <0x0000000754d98f08>是t1线程正在等待的监视器地址,进一步说明t1线程正等待监视器锁。

显式隐式获取监视器锁

解决了什么是对象监视器以及如何查看对象监视器‌的状态信息这两个问题后,接下来分析synchronized是如何上锁和解锁的。我们使用javap -c SynchronizedTest.class命令将上述文章开头的代码对应的class文件反编译成字节码指令。

C:\Program Files\Java\jdk-21\bin>javap -verbose SynchronizedTest.class
...
{
  public com.demo.SynchronizedTest();
    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 2: 0
  ...

  public synchronized void output(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=4, args_size=2
         0: iconst_0
         1: istore_2
         2: iload_2
         3: iconst_5
         4: if_icmpge     34
         7: ldc2_w        #21                 // long 1000l
        10: invokestatic  #23                 // Method java/lang/Thread.sleep:(J)V
        13: goto          21
        16: astore_3
        17: aload_3
        18: invokevirtual #31                 // Method java/lang/InterruptedException.printStackTrace:()V
        21: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        24: aload_1
        25: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        28: iinc          2, 1
        31: goto          2
        34: return
      Exception table:
         from    to  target type
             7    13    16   Class java/lang/InterruptedException
      LineNumberTable:
        line 8: 0
        line 10: 7
        line 13: 13
        line 11: 16
        line 12: 17
        line 14: 21
        line 8: 28
        line 16: 34
      StackMapTable: number_of_entries = 4
        frame_type = 252 /* append */
          offset_delta = 2
          locals = [ int ]
        frame_type = 77 /* same_locals_1_stack_item */
          stack = [ class java/lang/InterruptedException ]
        frame_type = 4 /* same */
        frame_type = 250 /* chop */
          offset_delta = 12

  public void output2(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=6, args_size=2
         0: aload_0
         1: dup
         2: astore_2
         3: monitorenter
         4: iconst_0
         5: istore_3
         6: iload_3
         7: iconst_5
         8: if_icmpge     40
        11: ldc2_w        #21                 // long 1000l
        14: invokestatic  #23                 // Method java/lang/Thread.sleep:(J)V
        17: goto          27
        20: astore        4
        22: aload         4
        24: invokevirtual #31                 // Method java/lang/InterruptedException.printStackTrace:()V
        27: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        30: aload_1
        31: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        34: iinc          3, 1
        37: goto          6
        40: aload_2
        41: monitorexit
        42: goto          52
        45: astore        5
        47: aload_2
        48: monitorexit
        49: aload         5
        51: athrow
        52: return
      Exception table:
         from    to  target type
            11    17    20   Class java/lang/InterruptedException
             4    42    45   any
            45    49    45   any
      ...

  public static synchronized void output3(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: iconst_5
         4: if_icmpge     34
         7: ldc2_w        #21                 // long 1000l
        10: invokestatic  #23                 // Method java/lang/Thread.sleep:(J)V
        13: goto          21
        16: astore_2
        17: aload_2
        18: invokevirtual #31                 // Method java/lang/InterruptedException.printStackTrace:()V
        21: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
        24: aload_0
        25: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        28: iinc          1, 1
        31: goto          2
        34: return
      Exception table:
         from    to  target type
             7    13    16   Class java/lang/InterruptedException
     ...
}
SourceFile: "SynchronizedTest.java"

C:\Program Files\Java\jdk-21\bin>

从上面可以看出,synchronized修饰普通方法和修饰静态方法时,字节码文件方法中会有同步标志 ACC_SYNCHRONIZED,而修饰代码块时使用了生成显式的 monitorenter/monitorexit 指令。ACC_SYNCHRONIZED 是方法的访问标志位,编译时由编译器添加,无需生成显式的 monitorenter/monitorexit 指令。两种上锁和释放锁的方式是有区别的:

(1)当synchronized修饰的普通方法和修饰静态方法时方法被调用时,JVM 检查是否设置ACC_SYNCHRONIZED 标志,若设置了线程会‌隐式尝试获取锁‌:对于实例方法锁对象是当前实例(this);对于静态方法锁对象是当前类的 Class 对象(如 SynchronizedTest.class);获取锁失败时线程进入阻塞状态,直到锁被释放。‌锁释放‌时无论方法正常执行结束还是抛出异常,JVM 均‌自动释放锁‌避免死锁。

(2)当执行synchronized修饰的代码块时,底层通过monitorenter指令进行上锁,我们知道,每个java对象都有一个隐式关联一个监视器(锁),调用monitorenter 指令尝试获取该监视器的所有权;调用monitorexit 指令时释放锁,异常时也会触发释放避免死锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

帧栈

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值