Java Synchronized 关键字

Synchronized 是什么?

sychronied是Java中并发编程的重要关键字之一。在并发编程中synchronized一直是解决线程安全问题,它可以保证原子性,可见性,以及有序性。

  • 原子性 :保证这段代码不会被并发打断
    synchronized 通过 monitorenter / monitorexit 实现互斥。同一时刻只有一个线程能进入同一把锁保护的临界区,因此临界区内的复合操作整体对外表现为不可分割。同一个锁,同一时刻只能被一个线程持有。因此临界区代码不会被并发打断。
  • 可见性 :释放锁前的修改,对之后获取同一把锁的线程可见
    JMM 中有非常重要的 Happens-Before 规则:: 对一个锁的解锁 happens-before 于后续对同一个锁的加锁。
    synchronized 在释放锁时,会保证此前的写操作对之后获取同一锁的线程可见;线程重新获得锁后,能够看到前一个线程释放锁前的修改。
  • 有序性 : 禁止重排序破坏同步语义
    CPU 和编译器为了优化性能,可能会进行指令重排序 。在不影响单线程执行结果的情况下,底层有可能调整某些指令执行顺序。
    允许不影响程序语义的优化和重排序,但不能发生破坏 synchronized 同步语义、Happens-Before 关系的重排序。
    即: synchronized 会建立明确的同步边界,编译器和 CPU 不能进行那种破坏锁的 Happens-Before 关系和程序同步语义的重排序。
text 复制代码
线程A临界区操作   // 先于 线程A unlock
        ↓
   unlock(lock)
        ↓
 Happens-Before
        ↓
    lock(lock)
        ↓
线程B临界区操作 // 晚于 线程B lock

synchronized 可以保证原子性、可见性和有序性。原子性来自锁的互斥机制,同一把锁同一时刻只能被一个线程持有,因此同步代码块不会被多个线程同时执行。可见性来自 JMM 的 Happens-Before 规则,一个线程对锁的释放 Happens-Before 于另一个线程随后对同一把锁的获取,因此前一个线程的修改对后一个线程可见。有序性则是通过同步语义限制编译器和 CPU 的指令重排序,不能让重排序破坏锁建立的 Happens-Before 关系。


Synchronized 使用

synchronized 可以修饰方法,也能够使用 synchronized(obj){} 定义同步代码块。

  • 修饰方法
  1. 修饰实例方法
java 复制代码
public synchronized void test() { 
	// ... 
}

锁住的是当前实例对象 this。等价于

java 复制代码
public void test() { synchronized (this) { 
	// ... 
	} 
}
  1. 修饰静态方法
java 复制代码
public static synchronized void test() { 
	// ... 
}

锁住的是当前类对应的 Class 对象,等价于:

java 复制代码
public static void test() {
    synchronized (Test.class) {
        // ...
    }
}

由于一个类通常只有一个对应的 Class 对象,因此无论通过哪个实例调用,竞争的都是同一把类锁。

  1. 修饰代码块
java 复制代码
synchronized (obj) {
    // ...
}

锁住的是指定的对象 obj。

线程进入同步代码块之前,必须先获得 obj 对应的锁;退出代码块后释放锁。


三种方式对比

写法 加锁对象
synchronized 实例方法 当前实例 this
static synchronized 静态方法 当前类的 Class 对象,如 Test.class
synchronized(obj) 指定的 obj 对象

使用sychronied修饰普通方法和静态方法,其实也等价于synchronized(this){}与synchronized(class){}。


Synchronized 锁升级机制

在 JDK 6 之前,Synchronized 的实现是 重量级锁。

synchronized 的锁升级是 JDK 6 之后对 synchronized 的优化,核心目的是:在无竞争或轻竞争时,尽量避免直接使用重量级锁,减少用户态和内核态切换的开销。

java 对象在 虚拟机内存中的结构:

text 复制代码
Java 对象 = 对象头 + 实例数据 + 对齐填充

对象头 = Mark Word + Klass Pointer + Array Length(仅数组对象有)

Mark Word 存储对象自身的运行时数据,32位 四字节,64位 八字节;
Klass Pointer 指向方法区中该对象的类元数据,32 位 四字节,64位 开启压缩指针(默认)四字节 否则,八字节;
Array Length 数组对象的长度,占 4 字节。

锁状态主要记录在 Java 对象头的 Mark Word 中,升级过程通常是:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

一般认为是不可逆的,但偏向锁可以被撤销。JDK 15 之后偏向锁默认禁用,新版本中逐步移除;如果面试官问 JDK 8,则默认开启。

以32位 JVM 为例,Mark Word 如下:

锁升级流程
text 复制代码
                第一次进入 synchronized
                         ↓
                        无锁
                         ↓
                       偏向锁
                         ↓
                   出现其他线程竞争
                         ↓
                      轻量级锁
                         ↓
                   竞争进一步加剧
                         ↓
                      重量级锁

1. 无锁

对象刚创建时是无锁状态。

Mark Word 中存 hashCode、GC 年龄、锁标志位等。

以 32 位为例:锁标志位是 01,偏向位是 0。

当线程第一次 进入 synchronized, 会将锁偏向该线程,锁成为偏向锁。

2. 偏向锁

首先之所以会引入偏向锁是因为:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的操作。

在程序进入同步代码块时,会访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,若为偏向锁状态,则查看偏向锁状态下线程ID是否指向当前线程 。如果是则直接执行同步代码。

但是mark word中记录的线程ID如果不是当前线程,则通过CAS比较与交换尝试修改对象头获得锁。CAS操作成功则可以直接执行同步代码,否则表示有其他线程竞争,此时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁 ,然后被阻塞的线程继续往下执行同步代码。

**适用场景:**只有一个线程反复进入同步块。

3. 轻量级锁

轻量级锁状态下,代码进入同步块时,如果同步对象锁状态为无锁状态, 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝 ,接着虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针 。这个操作如果成功则代表获取到了锁,但是如果失败,则会检查对象Mark Word是不是指向当前线程栈帧中的锁记录,如果是,则说明本身当前线程就拥有此对象的锁,就可以直接执行同步代码。否则说明锁对象被其他线程获取,当前线程是竞争者,那么当前线程会自旋等待锁,也就是不断重试,当重试一定次数后,总不能一直重试下去吧,太耗CPU了。所以这时候就要升级为重量级锁。

**适用场景:**少量线程交替执行,竞争不激烈。

4. 重量级锁

重量级锁就是通过对象监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

**适用场景:**多线程激烈竞争。

相关推荐
金銀銅鐵2 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
福建佰胜张工2 小时前
SMC 薄型气爪 MHF2-16D1 深度解析:平行开闭、低剖面结构在自动化装配中的选型与落地
java·服务器·安全·自动化
catoop2 小时前
API Key 设计与安全生成方案:结构拆解、数据库设计与 Java 实现
java·数据库·安全
system_error_x3 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
驭渊的小故事3 小时前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客3 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1884 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn4 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su5 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
我命由我123456 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法