JAVA 锁—— synchronized

在这里插入图片描述

32 位机器上java对象头中,markWord 示意图如上所示,64 位机器扩展前面标识位数,如 hashcode(25 -> 31),线程ID(23 -> 54)

如果启用了偏向锁:

  • synchronized添加偏向锁:只有1个线程加锁的情况下,此时一定没有竞争
  • synchronized添加轻量级锁:超过1个线程,且交替加锁,此时没有竞争
  • synchronized添加重量级锁:超过1个线程,同时加锁,或加锁时,其它线程没释放锁,此时发生了竞争

一、基本概念

注:下面代码大部分基于 JDK 8 分析,后续有 JDK23 的分析。

1、偏向锁

为方便观察 Java 内存对象,我们使用JOL工具,详见Java对象的内存分布(一)

1.1、代码

	/**
	*  程序运行前最好等待5秒,开启偏向锁。
	*  因为 jvm 启动也有加锁需求,防止 jvm 启动时受偏向锁影响,比如锁升级带来消耗,
	*  故而 jvm 完全启动后(大约4s,通过参数 -XX:BiasedLockingStartupDelay=0 进行调整),才会应用偏向锁。
	*/
    public static  void main(String[] args) throws Exception{
        Thread.sleep(5000l); 
        Object lock = new Object();
        System.out.println("---第一次----");
        System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());

        synchronized (lock) {
            System.out.println("加锁中:" + ClassLayout.parseInstance(lock).toPrintable());
        }


        System.out.println("加锁后:" + ClassLayout.parseInstance(lock).toPrintable());
        System.out.println("---第二次----");
        System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());

        synchronized (lock) {
            System.out.println("加锁中:" + ClassLayout.parseInstance(lock).toPrintable());
        }

        System.out.println("加锁后:" + ClassLayout.parseInstance(lock).toPrintable());
    }

1.2 运行结果

在这里插入图片描述

  • 大端模式:高位字节存放于内存的低地址端,低位字节存放于内存的高地址端,便于人类阅读。
  • 小端模式:低位字节存放于内存的低地址端,高位字节存放于内存的高地址端,便于机器处理。
存储模式示例(0x12345678)
大端(Big-Endian)0x12 0x34 0x56 0x78
小端(Little-Endian)0x78 0x56 0x34 0x12

1.3、注意事项

Object.hashCode()方法和System.identityHashCode()会让对象不能使用偏向锁,所以如果想使用偏向锁,那就最好重写hashCode方法。

  • 无锁和偏向锁占用相同位置,不像轻量级锁和重量级锁可以将原位置信息拷贝到其它地方进行备份,所以当对象已经存储了hashcode之后,加锁时会跳过偏向锁。
  • 偏向锁不会释放,即解锁后,锁对象头MarkWord不变。
  • JDK [6, 15),偏向锁默认开启,从 JDK 15 开始,默认关闭,可以通过 -XX:+UseBiasedLocking 开启,从JDK 18 开始彻底移除偏向锁。

2、轻量级锁

2.1、代码

	/**
	* 开启偏向锁后,2个线程交替加锁,偏向锁升级为轻量级锁。
	*/
    public static  void main(String[] args) throws Exception{
        Thread.sleep(5000l); // 等待5秒,开启偏向锁
        Object lock = new Object();
        System.out.println("---第一次----");
        System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());

        synchronized (lock) {
            System.out.println("加锁中:" + ClassLayout.parseInstance(lock).toPrintable());
        }


        System.out.println("加锁后:" + ClassLayout.parseInstance(lock).toPrintable());
        System.out.println("---第二次----");


        Thread thread = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程2:");
                System.out.println("加锁中:" + ClassLayout.parseInstance(lock).toPrintable());
            }

            System.out.println("线程2:");
            System.out.println("加锁后:" + ClassLayout.parseInstance(lock).toPrintable());
        });

        thread.start();
    }

2.2、运行结果

在这里插入图片描述

2.3、锁细节

轻量级锁加锁时,markWord中会存储指向栈中锁记录的指针,栈中锁记录存储的就是未加锁时原来的markWord,解锁时方便还原回去。

假如markWord中存储了hashcode,使用时先访问markWord发现加了轻量级锁,顺着栈指针找到栈中锁记录,即可找到hashcode


3、重量级锁

3.1、代码

    public static  void main(String[] args) throws Exception{
        Thread.sleep(5000l); // 等待5秒,开启偏向锁
        Object lock = new Object();
        System.out.println("---第一次----");
        System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());

        synchronized (lock) {
            System.out.println("加锁中:" + ClassLayout.parseInstance(lock).toPrintable());
        }


        System.out.println("解锁后:" + ClassLayout.parseInstance(lock).toPrintable());
        System.out.println("---第二次----");

        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程2:[" + Thread.currentThread() +  "] 加锁中:" 
                	+ ClassLayout.parseInstance(lock).toPrintable());
                try {
                    Thread.sleep(5000L); // 这里持锁5s,确保线程3加锁发生竞争;
                } catch (java.lang.Exception e) {

                }
            }
        });

        Thread thread3 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程3:[" + Thread.currentThread() +  "] 加锁中:" 
                	+ ClassLayout.parseInstance(lock).toPrintable());
            }
        });

        thread2.start();
        thread3.start();

        Thread.sleep(8000L); // 等待8s,确保线程锁释放;
        System.out.println("解锁后:" + ClassLayout.parseInstance(lock).toPrintable());
    }

3.2、运行结果

在这里插入图片描述

3.3、Monitor

  • 偏向锁、轻量级锁只要发生竞争,就会升级为重量级锁,注意,这里一步到位,不会自旋。

  • 升级为重量级锁后,其它线程自旋多次失败后,会进入 cxq 列表(相当于栈)中自旋,自旋达到阈值后,仍未获取锁,则进入阻塞状态。

  • 重量级锁加锁时,markWord中会存储指向 Monitor 的指针,Monitor 会存储未加锁时原来的markWord,解锁后还原回去。【JDK8实验如此,JDK23实验不一样】

    假如markWord中存储了hashcode,使用时先访问markWord发现加了重量级锁,顺着重量级锁指针找到Monitor,即可找到hashcode

截止目前为止,上述代码均在 JDK 8上讨论;下面讨论 JDK23

  • 重量级锁加锁时,markWord中会存储指向 Monitor 的指针,Monitor 会存储未加锁时原来的markWord,解锁时markWord不变。【JDK23实验,代码不变,运行结果如下图所示】
    在这里插入图片描述
ObjectMonitor细节如下:

在这里插入图片描述

由上图可知, wait()notify()notifyAll() 只能在重量级锁中调用,换言之,在偏向锁和轻量级锁中调用这3个方法时,会升级为重量级锁。

CAS详见CAS基础概念


3.4、重量级锁会降级为轻量级锁吗?

    public static  void main(String[] args) throws Exception{
        Thread.sleep(5000l); // 等待5秒,开启偏向锁
        Object lock = new Object();
        System.out.println("---第一次----");
        System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());

        synchronized (lock) {
            System.out.println("加锁中:" + ClassLayout.parseInstance(lock).toPrintable());
        }


        System.out.println("解锁后:" + ClassLayout.parseInstance(lock).toPrintable());
        System.out.println("---第二次----");

        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程2:[" + Thread.currentThread() +  "] 加锁中:" 
                	+ ClassLayout.parseInstance(lock).toPrintable());
                try {
                    Thread.sleep(5000L); // 这里持锁5s,确保线程3加锁发生竞争;
                } catch (java.lang.Exception e) {

                }
            }
        });

        Thread thread3 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程3:[" + Thread.currentThread() +  "] 加锁中:" 
                	+ ClassLayout.parseInstance(lock).toPrintable());
            }
        });

        thread2.start();
        thread3.start();

        Thread.sleep(8000L); // 等待8s,确保线程锁释放;
        System.out.println("解锁后:" + ClassLayout.parseInstance(lock).toPrintable());

        Thread thread4 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程4:[" + Thread.currentThread() +  "] 加锁中:" 
                	+ ClassLayout.parseInstance(lock).toPrintable());
            }
        });
        thread4.start();

【JDK 8 重量级锁会降级为轻量级锁】

在这里插入图片描述

【JDK 23 重量级锁不会降级为轻量级锁】

在这里插入图片描述


二、补充知识

1、hashcode对锁的影响

1.1、偏向锁状态中,首次调用锁的hashcode后,偏向锁会直接升级为重量级锁。

    public static  void main(String[] args) throws Exception{
        Thread.sleep(5000l); // 等待5秒,开启偏向锁
        Object lock = new Object();
        System.out.println("---第一次----");
        System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());

        synchronized (lock) {
            System.out.println("加锁中-未调用hashcode :" + ClassLayout.parseInstance(lock).toPrintable());
            lock.hashCode();
            System.out.println("加锁中-已调用hashcode:" + ClassLayout.parseInstance(lock).toPrintable());
        }


        System.out.println("解锁后:" + ClassLayout.parseInstance(lock).toPrintable());
    }

在这里插入图片描述

1.2、轻量级锁状态中,首次调用锁的hashcode后,锁升级为重量级锁?

    public static  void main(String[] args) throws Exception{
        Object lock = new Object();
        System.out.println("---第一次----");
        System.out.println("加锁前:" + ClassLayout.parseInstance(lock).toPrintable());

        synchronized (lock) {
            System.out.println("加锁中-未调用hashcode :" + ClassLayout.parseInstance(lock).toPrintable());
            lock.hashCode();
            System.out.println("加锁中-已调用hashcode:" + ClassLayout.parseInstance(lock).toPrintable());
        }


        System.out.println("解锁后:" + ClassLayout.parseInstance(lock).toPrintable());
    }

【JDK 8 升级重量级锁】

在这里插入图片描述
【JDK 23 维持轻量级锁】
在这里插入图片描述


唯一不变就是变化,JDK也在不断的演进,昨天还正确的观点,今天就错误了;今天错误的观点也可能明天就正确了。

所以看到任何观点,都要保持怀疑态度啊,每个人都有自己的观点,切忌坐井观天,故步自封,一定要跳出去,用发展的视角看问题。

<think>首先,用户的问题是基于之前的引用内容,但系统级指令要求我使用中文回答,并确保LaTeX格式正确。用户的问题是:“我想了解Javasynchronized关键字是否可重入以及递归调用是否会引发死 请问Java synchronized关键字是否支持可重入,递归调用是否会死” 从引用中,我可以看到: - 引用[1]:Javasynchronized 关键字支持重入性。通过使用内置的机制,synchronized 确保同一个线程可以多次获得同一把,从而允许在同一个保护下进行递归调用和方法嵌套。 - 引用[2]:synchronizedJava 中用于控制多个线程对共享资源访问的关键字。它通过提供互斥mutex lock)机制,确保在同一时刻只有一个线程可以执行同步代码块或方法,从而避免了数据竞争问题。可重入性。 - 引用[3]:当synchronized用于方法时,它会将整个方法变为同步方法。这意味着在同一时间内,只有一个线程可以执行该方法。对于实例方法,是当前实例对象(this);对于静态方法,是当前类的Class对象。 - 引用[4]:在 Java 中提供了同步机制,可以有效地防止资源冲突。同步机制使用 synchronized 关键字。 - 引用[5]:一个代码示例展示了可重入的使用,其中同一个线程多次进入同步块。 所以,synchronized 关键字支持可重入性,这意味着同一个线程可以多次获得同一个,而不会导致死。递归调用不会引发死,因为线程已经持有,可以重新进入同步块。 我的回答需要: 1. 使用中文回答。 2. 结构清晰,帮助用户逐步解决问题。 3. 确保回答真实可靠,基于提供的引用。 4. 在回答中引用的段落末尾自然地添加引用标识,例如:[^1] 5. 生成相关问题(在最后)。 6. 对于数学表达式,使用 $...$ 或 $$...$$ 格式。但在这个问题中,可能没有数学表达式,所以我可以忽略这个部分,除非必要。 回答结构: - 先确认问题:用户想知道 synchronized 是否支持可重入,以及递归调用是否会死。 - 逐步解释: a. 解释 synchronized 的可重入性。 b. 解释为什么递归调用不会死。 c. 提供代码示例或引用。 - 最后添加相关问题。 生成相关问题: - 基于主题,如 Java 机制、可重入、死预防等。 相关问题示例: 1. 什么是可重入?它在 Java 中如何实现? 2. 除了 synchronizedJava 中还有哪些可重入机制? 3. 如何避免死Java 多线程编程中? 4. synchronized 和 ReentrantLock 的区别是什么? 5. 可重入在递归场景中的应用。 现在,构建回答。 回答大纲: 1. 引言:直接回答问题。 2. 解释可重入性。 3. 解释递归调用和死。 4. 总结。 5. 相关问题。 确保引用:在引用内容的末尾添加引用标识。</think>### Java synchronized 关键字的可重入性及递归调用死分析 在 Java 中,`synchronized` 关键字是用于实现线程同步的核心机制,其设计支持 **可重入性(reentrancy)**,这意味着同一个线程可以多次获得同一把,而不会引发死。递归调用在 `synchronized` 保护下也不会导致死,因为线程在持有时可以安全地重新进入同步代码块或方法。下面我将逐步解释这一机制,确保回答真实可靠,基于 Java 官方文档和常见实践[^1][^2]。 #### 1. **synchronized 支持可重入性** - `synchronized` 通过内置的机制(称为 **监视器(monitor lock)**)实现可重入性。当一个线程首次获得时,JVM 会记录的持有者和计数器。当同一个线程再次请求同一把时,计数器会增加(而不是阻塞),允许线程多次进入同步区域[^1][^2]。 - **关键点**: - 可重入性避免了线程在递归或嵌套调用中自我阻塞。例如,在递归方法中,线程可以重复进入同一个同步块,而不会导致争用[^1]。 - 这种机制简化了代码设计,尤其在递归场景中,无需额外的管理[^2][^5]。 #### 2. **递归调用不会引发死的原因** - **死条件分析**:死通常需要四个条件同时满足(互斥、持有并等待、不可剥夺、循环等待)。但在 `synchronized` 的可重入机制下: - 同一个线程在递归调用中始终持有,因此不会出现“持有并等待”其他的情况。 - JVM 内部维护计数器:当线程退出最外层的同步块时,计数器才会归零,释放[^1][^5]。这确保了递归调用不会触发死。 - **代码示例**: 以下示例演示了 `synchronized` 在递归调用中的安全行为(基于引用[5]修改)。线程 `t1` 可以多次进入同一个保护的同步块,而不会死。 ```java public class ReentrantDemo { static Object lock = new Object(); // 共享对象 public static void main(String[] args) { new Thread(() -> { synchronized (lock) { // 第一次获得 System.out.println("外层同步块"); recursiveMethod(1); // 递归调用 } }, "t1").start(); } public static void recursiveMethod(int depth) { synchronized (lock) { // 同一个线程再次获得(可重入) System.out.println("递归深度: " + depth); if (depth < 3) { recursiveMethod(depth + 1); // 继续递归 } } } } ``` - **输出结果**: ``` 外层同步块 递归深度: 1 递归深度: 2 递归深度: 3 ``` - **分析**:线程 `t1` 在递归过程中多次获取 `lock` ,但不会死,因为 JVM 允许可重入[^5]。如果不可重入,线程会在第二次进入同步块时自我阻塞,导致死。 #### 3. **总结与注意事项** - **支持可重入**:`synchronized` 是 Java 中默认的可重入,适用于递归、嵌套方法调用等场景,确保线程安全[^1][^2]。 - **死风险**:尽管递归调用本身不会死,但死可能发生在多个线程和多个的交互中(例如,线程 A 持有 X 并等待 Y,线程 B 持有 Y 并等待 X)。因此,在复杂同步设计中,建议: - 避免嵌套多个不同的。 - 使用 `ReentrantLock` 类(来自 `java.util.concurrent` 包)提供更灵活的控制,如超时机制[^3][^4]。 - **最佳实践**:在递归算法中使用 `synchronized` 是安全的,但需确保同步范围最小化,以减少性能开销[^3][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值