synchronized的锁升级过程和详细的代码演示

在了解这个知识之前先了解Java对象的内容布局

一、锁升级的过程

在JDK1.0、1.2synchronized就是一个重量级的锁,效率不高,后来JDK对synchronized做了优化,在上锁的时候有一个锁升级的过程,
偏向锁-》轻量级锁(无锁,自旋锁、自适应自旋)-》重量级锁
如果大家不理解以上锁的类型可以看这篇文章重量级锁、自旋锁、轻量级锁、偏向锁、悲观、乐观锁等各种锁(转)

  1. 偏向锁:顾名思义就是偏向于第一个访问的线程。也就是说在无竞争的环境下,第一个线程访问的同步代码块,那么就会,会采用CAS机制,把这个同步代码块标记为有人在执行了并把这个线程ID写到锁对象的MarkWord中,这个锁就会偏向这个线程,下次有线程访问的时候就会判断是否有人在执行了,并且线程的ID是是之前访问过的线程访问,那么他就会直接进进入同步代码块中并执行,这样就会少一次cas的开销。偏向锁会有一个延迟,程序刚启动的大约三四秒内不会出现偏向锁。 解决方法有两个:
    (1)加sleep 5s再试
    (2)-XX:BiasedLockingStartupDelay=0 关闭关闭延迟开启偏向锁,JDK6默认开启偏向锁
    计算过hashcode值的对象不会加偏向锁,因为对象头没有空间放线程id了。
  2. 轻量级锁:轻量级锁体现轻量的点就在于自旋。当有任意的一个线程与持有偏向锁的线程进行资源竞争,那么锁就会升级成轻量级锁。每个线程的线程栈中都有一个lock record对象,哪个线程把自己的lock record的指针记录在锁对象头中,哪个线程就持有轻量级,抢的方式就是自旋(CAS)。如果自旋一定的次数还没拿到锁,jdk1.6之前默认有的线程自旋超过10次或者在那自旋等着的线程数超过整个cpu核数的二分之一,那么锁就会升级成重量级锁。jdk1.6之后加入了自适应自旋,自旋次数JVM自己控制。
  3. 重量级锁:jvm层面的两个标识,加锁解锁都会阻塞其他线程。

二、synchronized加锁的锁状态在markword中的布局

在这里插入图片描述
(1)无锁状态

public class NoLock {
    private static Object o = new Object();
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

在这里插入图片描述
问:可能有人问了,为什么上面图中锁状态最后两位表示锁标记位而打印出来的是前面的两位?
答:这就涉及到了大端模式和小端模式了,大端模式是高位字节存储在底地址段,低位字节存储在高地址段,小端模式是低位字节存储在底地址段,高位字节存储在高地址段,如:

0x 00 00 00 00 00 00 00 01 表示MarkWord的8个字节,最左边就是高位字节,最右边就是地位字节
小端模式在内存中存储顺序:
0x 01 00 00 00 00 00 00 00
大端模式在内存中存储顺序:
0x 00 00 00 00 00 00 00 01 

大家实在不懂的可以看一下这篇文章字节序–大端模式和小端模式

问:分代年龄最多可以改到多少?
答:我们从上图可以看到分代年龄(就是一个对象被垃圾回收多少次就会被移到老年代)占4bit,所以网上很多人说把分代年龄改成30多,20多来防止老年代被快速填满是错误的,因为4bit最大是15,所以分代年龄最大可以被调到15次。

如果锁对象在无锁状态下调用hashcode方法,MardWord中会存储hashcode的信息:

public class NoLock {
    private static Object o = new Object();
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        o.hashCode();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

在这里插入图片描述

(2)偏向锁状态

public class DeflectionLock {
    private static Object o = new Object();
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        lock();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
    private static void lock() {
        synchronized (o) {

        }
    }
}

在这里插入图片描述
问:根据上面我们的分析,应该出现偏向锁的,为什么没有出现呢?
答:偏向锁会有一个延迟,程序刚启动的三四秒内不会出现偏向锁

public class DeflectionLock {
    private static Object o;

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        lock();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

    private static void lock() {
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

在这里插入图片描述
问:为什么全局静态锁对象o要在mian方法里面初始化而不是在创建的时候初始化,而且还没进入到同步代码块中,相应的锁状态就已经是偏向锁了?
答:jdk6默认开启偏向锁,但是是输入延时开启,也就是说,程序刚启动创建的对象是不会开启偏向锁的,几秒后后创建的对象才会开启偏向锁的。当类加载器将类加载到JVM中的时候就会创建静态变量,及对象是不会开启偏向锁。而且我们可以看到当加完偏向锁后,MardWord中的线程ID是不为空的。

public class DeflectionLock {
    private static Object o;

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        o = new Object();
        o.hashCode();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        lock();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

    private static void lock() {
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

在这里插入图片描述
我们可以看到 计算过hashcode值的对象不会加偏向锁,而是直接加了轻量级锁。

(3)轻量级锁

public class LightweightLocking {
    private static Object o;
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000L);
        o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        new Thread(LightweightLocking::lock).start();
        // 加个睡眠是怕上面的线程没有执行完成而形成线程争用而升级为重量级锁
        Thread.sleep(3000L);
        lock();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

    private static void lock() {
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

在这里插入图片描述
在这里插入图片描述
在第一个线程获取到偏向锁并把自己的线程ID写到锁对象的MarkWord中,当主线程进入同步代码块发现不是自己的线程ID,锁就升级到了轻量级锁,这里我只是简单的来说,其实还是很复杂的,过程大家可以看下面的这张图,或者看这篇作者的文章偏向锁。退出synchronized代码块之后又会变成无锁的状态,因为轻量级锁会有一个锁撤销的过程
在这里插入图片描述
(4)重量级锁

public class HeavyweightLock {
    private static Object o;
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000L);
        o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
        new Thread(HeavyweightLock::lock).start();
        lock();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }

    private static void lock() {
        synchronized (o) {
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

在这里插入图片描述
在这里插入图片描述

以上就是synchronized的锁升级的详细过程,如果大家感觉有所收获的话,麻烦素质三连哈~

### Java 升级机制与原理深入分析 Java升级机制是为了优化多线程环境下对象头的加效率而设计的一种策略。这种机制通过不同的状态(无、偏向、轻量级、重量级),动态调整的粒度,从而在不同场景下平衡性能资源消耗。 --- #### 1. **的状态及其作用** 的状态主要包括四种:无、偏向、轻量级重量级。每种状态对应着不同的应用场景性能权衡。 - **无** 当没有任何线程竞争时,对象处于无状态。此时的对象头仅存储类元信息指针年龄字段,尚未被任何线程定[^3]。 - **偏向** 偏向是一种针对单一线程多次获取同一把的优化方式。当第一个线程尝试获取某个对象的时,JVM 将该对象标记为“偏向于”当前线程,并记录线程 ID 到对象头中。后续同一线程再次获取时无需经过 CAS 操作,直接判断是否属于同一个线程即可完成解验证[^1]。 - **轻量级** 如果多个线程同时争抢某一把,则会进入轻量级阶段。在此状态下,JVM 使用自旋技术让线程不断尝试通过 CAS 更新标志位直到成功为止。这种方式避免了立即切换到操作系统层面的竞争,减少了上下文切换带来的开销[^3]。 - **重量级** 若自旋失败或超出了预设次数仍未获得,则退化成重量级模式。此时线程会被挂起并交由 OS 进行调度处理,直至目标可用后再唤醒继续运行。 --- #### 2. **升级过程** 升级遵循以下路径:`无 -> 偏向 -> 轻量级 -> 重量级`。以下是各阶段的具体触发条件及行为说明: - **从无到偏向** 第一次有线程试图访问一个未加的对象时,JVM 自动为其分配偏向,并将线程 ID 记录下来。此后只要仍是原线程操作此对象就不会发生进一步升级[^1]。 - **从偏向到轻量级** 出现第二个线程也想对该对象加的情况下,原有偏向失效转而采用轻量级形式。这里涉及撤销偏斜动作并通过 CAS 实施新逻辑[^3]。 - **从轻量级到重量级** 对于持续无法取得的线程来说,如果超过一定时间仍未能借助自旋转赢取所有权的话,那么就不得不降格至重量级层次。这意味着要付出更高代价——使线程陷入阻塞队列等待通知释放[^3]。 需要注意的是,整个升级流程并非绝对必要;某些情况下也可能越级跳过中间环节直达最终形态取决于实际运行状况以及 JVM 参数设定等因素影响[^4]。 --- #### 3. **升级的意义** 引入这样的分级体系主要是为了应对不同类型的应用需求,在保证功能正确性的前提下追求更高的执行效能: - 提升低竞争环境下的程序表现力; - 控制高并发情形中的系统负担; - 平滑过渡各种可能发生的冲突情况而不至于立刻恶化性能指标。 不过值得注意的一点是,随着硬件架构的发展尤其是 CPU 缓存一致协议的进步,传统意义上的膨胀概念重要性有所下降,但在复杂分布式计算领域仍然具有重要意义[^5]。 --- ```java // 示例代码展示 synchronized ReentrantLock 的区别 public class LockUpgradeDemo { private final Object monitor = new Object(); public void syncMethod() { synchronized (monitor) { // 使用内置监视器实现同步控制 System.out.println(Thread.currentThread().getName() + ": Inside Sync Method."); } } public void reentrantLockMethod() { java.util.concurrent.locks.Lock lock = new java.util.concurrent.locks.ReentrantLock(); try { lock.lock(); // 手动显式地申请 System.out.println(Thread.currentThread().getName() + ": Inside Reentrant Lock Method."); } finally { lock.unlock(); // 确保无论异常与否都能正常释放 } } } ``` 上述例子简单演示了两种常见类型的用法差异,其中 `ReentrantLock` 支持更多高级特性比如公平性设置、可中断等待等功能扩展[^4]。 ---
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值