Java并发编程 -- synchronized

一. synchronized的使用

1.1 修饰实例方法(对象锁)

将 synchronized 放在普通方法的返回值前面。

  • 作用范围:锁住的是当前对象实例(this)。
  • 适用场景:保护某个对象的实例变量不被多个线程同时修改。
java 复制代码
// 只有获取了当前对象锁的线程,才能执行这个方法
public synchronized void increment() {
    count++;
}

1.2 修饰静态方法(类锁)

将 synchronized 放在 static 方法上。

  • 作用范围:锁住的是当前类的 Class 对象(如 MyClass.class)。因为类对象在 JVM 中是全局唯一的,所以它能限制所有线程对该类静态变量的并发访问。
  • 适用场景:保护全局的静态变量。
java 复制代码
// 无论创建多少个对象,同一时刻只有一个线程能执行此方法
public static synchronized void staticIncrement() {
    staticCount++;
}

1.3 修饰同步代码块(自定义锁)

将需要同步的代码包裹在 synchronized(对象) { ... } 中。

  • 作用范围:锁住的是括号中显式指定的对象。
  • 适用场景:当方法中只有部分代码需要保证线程安全时,使用代码块可以缩小锁的粒度,避免其他不相关的代码也被锁住,从而提升并发性能。
java 复制代码
public void doSomething() {
    // 这里的代码是不受保护的,多个线程可以同时执行
    System.out.println("准备阶段...");

    // 只有这部分代码受锁保护
    synchronized (this) { 
        count++;
    }
}

1.4 锁对象的高级用法(传入外部对象)

java 复制代码
// 定义一个专门用来加锁的私有对象
private final Object lock = new Object();

public void safeMethod() {
    synchronized (lock) {
        // 只有拿到 lock 对象的线程才能进入这里
        // 这种方式比直接锁 this 更安全,避免外部代码意外获取你的锁
    }
}

二. synchronized的原理

2.1 对象在内存上的存储

对象在堆中的内存布局,指的是对象在堆中存放时的各个组成部分,主要分为以下几个部分:

对象头包含标记字段和元数据指针

标记字段 相对比较复杂。在不同的对象状态(有无锁、是否处于垃圾回收的标记中)下存放的内容是不同的,同时在64位(又分为是否开启指针压缩)、32位虚拟机中的布局都不同。以64位开启指针压缩为例:

  • hashcode:25位的对象标识Hash码
  • age:对象分代年龄占4位
  • biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁
  • thread:持有偏向锁的线程ID,占23位
  • epoch:偏向时间戳,占2位
  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位

我们可以通过lock的标识,来判断是哪一种锁的等级

  • 后三位是001表示无锁
  • 后三位是101表示偏向锁
  • 后两位是00表示轻量级锁
  • 后两位是10表示重量级锁

Klass pointer元数据的指针 指向方法区中保存的InstanceKlass对象:

2.2 Monitor(监视器)

Monitor 被翻译为监视器,是由jvm提供,c++语言实现。

monitor从字节码的原理出发如下图:

  • monitorenter 上锁开始的地方
  • monitorexit 解锁的地方
  • 其中被monitorenter和monitorexit包围住的指令就是上锁的代码
  • 有两个monitorexit的原因,第二个monitorexit是为了防止锁住的代码抛异常后不能及时释放锁

在使用了synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁

monitor主要就是跟这个对象产生关联图如下:

Monitor内部具体的存储结构:

  • Owner:存储当前获取锁的线程的,只能有一个线程可以获取
  • EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
  • WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
    具体的流程:
  • 代码进入synchorized代码块,先让lock(对象锁)关联的monitor,然后判断Owner是否有线程持有
  • 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功
  • 如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平)
  • 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待

2.3 synchronized锁升级

2.3.1 CAS自旋

CAS的全称是: Compare And Swap(比较再交换 ),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。

CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令。

CAS 操作包含三个操作数:

  • V(内存地址):需要修改的变量的内存地址。
  • E(Expected,期望值):线程认为该变量当前应该有的值。
  • N(New,新值):线程希望将该变量更新为的新值。

执行逻辑:当且仅当内存地址 V 的当前值与期望值 E 相等时,才将 V 的值更新为 N。如果不相等,说明有其他线程已经修改过该变量,当前操作失败(通常会失败重试,即"自旋")。

2.3.2 偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。

1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。

2.通过CAS指令将Lock Record的线程id存储在对象头的mark word中,同时也设置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁。

3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作,性能相对轻量级锁更好一些。

2.3.3 轻量级锁

当有第二个线程尝试获取这把已经被偏向的锁时,偏向锁会被撤销,并升级为轻量级锁。

1.创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word。

  1. 让锁记录中 Object reference 指向锁对象,并尝试用 CAS 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
  1. 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁。

如果 cas 失败,有两种情况:

  • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程

锁膨胀:如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

  • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数。
2.3.4 重量级锁

如果自旋等待超过了设定的阈值(JVM 会根据历史情况自适应调整自旋次数),或者同时有多个线程(通常是3个及以上)在激烈竞争同一把锁,轻量级锁就会膨胀为重量级锁。

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针。

相关推荐
snow@li1 小时前
Spring 项目 Java 访问修饰符 + 非访问修饰符全景梳理详解
java·后端·spring
大模型念念1 小时前
Spring 框架入门:从零开始构建你的第一个应用
java·后端·spring
雨落在了我的手上1 小时前
Java数据结构(九):栈和队列
java·开发语言·数据结构
春生野草2 小时前
个人笔记--大顶堆和基数排序
java·数据结构·算法
高铭杰2 小时前
Golang sync.Mutex实现原理学习
java·学习·golang
头发还在的女程序员12 小时前
医院陪诊管理系统怎么选择?——2026 年选型避坑与架构参考
java·开发语言·陪诊系统·陪诊app·医院陪诊陪护
CodeStats12 小时前
【Spring事务】Spring事务注解 @Transactional 完整体系:从 MySQL 隔离级别到 MyBatis 原理详解
java·spring·mybatis·事务·transactional
我命由我1234513 小时前
Android 开发问题:为 PDFView 设置一个带有黑色边框的背景 drawable,但边框没有生效
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
都叫我大帅哥15 小时前
从Python到Java:为什么企业级Agent最终会选择Java?
java·ai编程