【java】synchronized关键字详解
目录
- 一、线程同步与线程安全问题
- 线程不安全Demo
- 线程不安全的原因
- 二、synchronized关键字
- 关键字锁粒度
- 修饰对象
- 修饰代码块
- 修饰方法
- 修饰静态方法
- 修饰类
- synchronized 锁总结
- synchronized加锁原理
- MarkWord
- synchronized锁升级
- synchronized锁原理
- synchronized关键字总结
- 其他同步方式
- 乐观锁
- Lock类的加锁、解锁
- 分布式锁(redis)
一、线程同步与线程安全问题
线程不安全Demo
我们下面就看一个出现线程不安全情况的示例
class UnsafeThreadDemo {private int count;public MultiUnSafeDemo(int count) {this.count = count;}public void getAndIncrement() {count++;}public static void main(String[] args) throws InterruptedException {final MultiUnSafeDemo counter = new MultiUnSafeDemo(0);Thread []threads = new Thread[100];for(int i=0; i<threads.length; i++) {threads[i] = new Thread(() -> {for(int j=0; j<1000; j++) {counter.getAndIncrement();}});threads[i].start();}for(int i=0; i<threads.length; i++) {threads[i].join();}System.out.println("count: "+counter.count);}
}
运行结果,结果显示count并非100000,那又是为什么呢?
线程不安全的原因
问题出现在count++ 并非原子操作,i++分为三个步骤
- 读取当前值
- 当前值+1
- 写回内存
线程之间的切换,可能发生在任意过程,e.g. 线程a已经加完1,但是发生线程切换,当前值被原由的线程b的值覆盖,导致实际执行100个线程的总和并非100*1000,发生线程切换的位置可能导致实际值比理论值小。
二、synchronized关键字
java中提供了一些解决线程安全问题的工具,有Lock类,有synchronized关键字,这里先介绍synchronized关键字
关键字锁粒度
修饰对象
synchronized 可以修饰对象,例如通过new ClassName() 创建的 对象,例如在代码的共享变量区域使用synchronized(this) ,可以保障线程安全,如果在第一部分的示例中使用synchronized(this) 覆盖 count++的代码块,可以保障最终的结果正确输出100000:
public class MultiUnSafeDemo {private int count;public MultiUnSafeDemo(int count) {this.count = count;}public void getAndIncrement() {synchronized (this) {count++;}}public static void main(String[] args) throws InterruptedException {final MultiUnSafeDemo counter = new MultiUnSafeDemo(0);Thread []threads = new Thread[100];for(int i=0; i<threads.length; i++) {threads[i] = new Thread(() -> {for(int j=0; j<1000; j++) {counter.getAndIncrement();}});threads[i].start();}for(int i=0; i<threads.length; i++) {threads[i].join();}System.out.println("count: "+counter.count);}
}
此时,由于代码的getAndIncrement方法被对象调用,因此synchronized (this) 等价于对象锁,锁的是调用这个方法的对象。通过this对象调用方法的synchornized,实现非原子性的**count++**操作的同步。
修饰代码块
除了修饰对象,可以将synchronized关键字修饰代码中共享变量的区域,保证操作共享变量的一致性。
public class Cache {private final Object lock = new Object();private Map<String, String> data = new HashMap<>();public void put(String key, String value) {synchronized (lock) { // 锁定私有对象,避免暴露锁data.put(key, value);}}
}
上述示例中,synchronized关键字修饰lock这个类的变量,实现操作data这个hashMap的线程同步性
修饰方法
可以将synchornized关键字添加到方法前,调用方法的时候会实现多线程的同步
public synchronized void getAndIncrement() {count++;}
效果和在方法内部增加synchronized () {} 代码块相同,都是锁定这个实例
优点:实现简单,方便
缺点:可能将方法中不需要同步的部分也控制住,降低了程序的并发性。
修饰静态方法
等价于修饰类,因为静态方法的生命周期和类相同,因此在静态方法上加锁,锁定的是类所有实例,在一台机器上的对象实例都串行执行
修饰类
通过synchronized (XXXName.Class) 实现锁对象
public class DatabaseConnection {public void init() {// 同步代码块,锁定类的 Class 对象synchronized (DatabaseConnection.class) {// 初始化数据库连接(全局唯一操作)}}
}
锁的类型为 类对象,适用场景为全局:
- 全局锁
synchronized 锁总结
Synchronized基于类、对象、方法、静态方法的使用修饰,有以下几种形式,整理为:
锁类型 | 锁对象 | 同步范围 | 适用场景 |
---|---|---|---|
对象锁 | 对象实例,以及对象调用方法 | 当前实例的线程 | 非静态的示例方法 |
类锁 | Class对象 | 所有实例的对象 | 静态变量保护或全局操作,例如数据库连接池的初始化 |
同步代码块 | 显示指定的对象 | 代码块包含的区域 | 同步耗时操作中的关键部分 |
synchronized加锁原理
MarkWord
每个 Java 对象在内存中由 对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding) 组成。synchronized 的锁信息存储在对象头中,具体分为两部分:
Mark Word:记录对象的哈希码、GC 分代年龄、锁状态标志等。
Klass Pointer:指向对象所属类的元数据。
锁状态标志 通过 Mark Word 的特定比特位表示,支持以下状态:
- 无锁(001)
- 偏向锁(101)
- 轻量级锁(00)
- 重量级锁(10)
synchronized锁升级
为了提高性能,JVM 会根据线程竞争情况动态调整锁的级别,从低开销到高开销逐步升级:
(1) 偏向锁(Biased Locking)
**适用场景:**单线程访问同步代码块。
原理:
对象头记录第一个获取锁的线程 ID(Thread ID),后续该线程无需通过 CAS 加锁和解锁。
(2) 轻量级锁(Lightweight Locking)
适用场景:
多线程交替执行,无实际竞争。
原理:
线程通过 CAS 操作 将对象头的 Mark Word 替换为指向线程栈中锁记录(Lock Record)的指针。
若成功,线程获得轻量级锁;若失败(有其他线程竞争),升级为重量级锁。
(3) 重量级锁(Heavyweight Locking)
适用场景:
多线程高竞争。
原理:
通过操作系统的 互斥量(Mutex) 实现,未获取锁的线程会被阻塞,进入内核态等待唤醒。
代价:涉及用户态到内核态的切换,性能开销较大。
synchronized锁原理
synchronized关键字修饰方法或者代码块的时候,增加了synchronized修饰的代码块在反编译为字节码的时候,都增加了monitorenter和monitorexit,monitorenter进入临界区,申请锁定资源。执行完同步代码后,monitorexit退出临界区。
synchornized修饰方法的时候,使用ACC_SYNCHRONIZED标记被修饰的方法
每个 Java 对象关联一个 监视器锁(Monitor),其本质是一个 互斥锁(Mutex)。Monitor 的实现依赖操作系统,核心操作包括:
-
进入区(Entry Set):线程尝试获取锁,若失败则进入阻塞队列。
-
拥有者(Owner):持有锁的线程。
-
等待区(Wait Set):调用 wait() 的线程释放锁并进入等待状态。
synchronized关键字总结
对象头与锁状态:通过 Mark Word 记录锁状态,支持偏向锁、轻量级锁、重量级锁的升级。
锁升级机制:根据线程竞争动态调整锁级别,平衡性能与安全性。
字节码实现:依赖 monitorenter 和 monitorexit 指令,同步方法使用 ACC_SYNCHRONIZED 标记。
JVM 优化:锁消除和锁粗化减少不必要的同步开销。
适用场景:偏向锁和轻量级锁适用于低竞争场景,重量级锁应对高并发竞争。
其他同步方式
乐观锁
通过版本号,根据写入的时候的版本号和当前期望是否一致判断是否可以执行写入操作,是一种无锁的同步
Lock类的加锁、解锁
Lock的lock方法适用于单实例,单JVM的同步锁,支持可重入锁、自旋锁、中断锁
分布式锁(redis)
使用redis的单线程,根据Lua脚本和setnx保障分布式环境中的全局原子性以及超时设置