synchronized是Java中用于解决并发问题的核心关键字,它通过确保多个线程对共享资源的互斥访问,来避免线程安全问题(如竞态条件、数据不一致等)。
synchronized的核心特性
- 原子性(Atomicity):确保一个或多个操作要么全部执行成功,要么全部执行失败。在synchronized代码块中的代码是不可中断的,同一时刻只有一个线程能执行。
- 可见性(Visibility):保证一个线程对共享变量的修改,对于其他后续进入同步代码块的线程是可见的。当线程释放锁时,会将私有内存中的变量值刷新回主内存。
- 有序性(Ordering):虽然编译器和处理器为了优化会进行指令重排,但synchronized可以保证多线程程序在逻辑上的执行顺序,即"同步块内的代码在执行上具有先后顺序"。
- 可重入性:可重入性是指一个线程已经获取到某个锁后,再次请求该锁时可以直接获取,无需重新竞争。synchronized是可重入锁,其内部通过计数器记录锁的持有次数(初始为0,获取锁时加1,释放锁时减1,计数器为0时锁才被真正释放)。这一特性避免了线程在递归调用同步方法/代码块时出现死锁。例如,一个同步方法A调用另一个同步方法B(两者锁对象相同),线程获取A的锁后,调用B时可直接获取锁。
synchronized的使用方式
synchronized的使用灵活,可修饰不同的代码结构,核心是明确"锁对象"------线程竞争的是锁对象,只有获取到锁对象的线程才能执行同步代码,常见使用方式有3种。
- 修饰实例方法(对象锁),语法:
public synchronized void methodName() { ... }
- 锁对象:当前类的实例对象(this)。
- 特点:不同实例对象的锁相互独立,即多个线程访问同一个实例的同步实例方法时会竞争锁;访问不同实例的同步实例方法时,因锁对象不同,不会竞争。
- 示例:同一User实例的add()方法被多线程调用时互斥,不同User实例的add()方法可并行执行。
- 修饰静态方法(类锁)语法:
public static synchronized void methodName() { ... }
- 锁对象:当前类的Class对象(每个类在JVM中只有一个Class对象,是全局唯一的)。
- 特点:类锁是全局锁,无论创建多少个类的实例,所有线程访问该类的同步静态方法时,都会竞争同一个Class对象锁。
- 注意:类锁与对象锁相互独立,即同步静态方法和同步实例方法的锁对象不同,线程访问时不会竞争。
- 修饰代码块(自定义锁对象),语法:
synchronized (锁对象) { ... 同步代码 ... }
- 锁对象:可自定义,支持两种类型:① 实例对象(this或其他实例);② Class对象(类名.class)。
- 特点:粒度最细,可精准控制需要同步的代码片段(而非整个方法),减少锁竞争,提高程序性能。
- 常见场景:
- 锁当前实例:
synchronized (this) { ... },效果与修饰实例方法一致,但仅同步代码块内的逻辑。
- 锁Class对象:
synchronized (User.class) { ... },效果与修饰静态方法一致。
- 锁自定义对象:
private Object lock = new Object(); synchronized (lock) { ... },通过独立的锁对象,避免与其他同步逻辑竞争锁,灵活性最高。
synchronized的锁机制
- Java 6及以后对synchronized进行了大幅优化,引入了"偏向锁、轻量级锁、重量级锁"三种锁状态,目的是根据线程竞争的激烈程度动态切换锁状态,平衡性能与线程安全。锁机制的核心是"对象头"------Java对象在内存中的布局包括对象头、实例数据、对齐填充,其中对象头存储了锁的状态信息(Mark Word)、类元数据指针等。
- 三种锁状态的优先级:无锁 < 偏向锁 < 轻量级锁 < 重量级锁,随着线程竞争的加剧,锁会从低级别向高级别升级,且升级过程不可逆(一旦升级为重量级锁,无法回退为轻量级锁或偏向锁)。
- 无锁(No Lock):初始状态。
- 偏向锁(Biased Lock):当只有一个线程访问同步块时,直接在对象头记录线程ID,下次该线程进入时无需CAS操作,性能极高。
- 轻量级锁(Lightweight Lock):当出现竞争,但竞争不激烈时,通过CAS自旋来尝试获取锁,避免线程阻塞。
- 重量级锁(Heavyweight Lock):当自旋超过一定次数或竞争非常激烈时,升级为重量级锁。此时未获取到锁的线程会进入阻塞(Blocked)状态,交给操作系统管理。
锁优化补充
- 锁消除:JVM的即时编译器(JIT)在运行时,会对一些"不可能存在竞争的锁"进行消除。例如,局部变量作为锁对象(每个线程都有独立的局部变量,无共享),JVM会直接删除该synchronized修饰,避免不必要的锁开销。
- 锁粗化:当多个连续的synchronized代码块使用同一个锁对象时,JVM会将这些代码块合并为一个大的同步代码块,减少锁的获取/释放次数(每次获取/释放锁都有开销)。例如,循环内多次调用同步方法,JVM可能将锁粗化到循环外部。
CAS (Compare And Swap,比较并交换) 是并发编程中实现原子操作的核心算法,是一种乐观锁的实现策略。
- CAS的工作原理,包含三个核心参数:
- 内存地址 V (Memory Location):变量在内存中的实际值。
- 期望值 A (Expected Value):线程认为该变量当前应该是什么值。
- 新值 B (New Value):线程想要更新成的值。
- 执行逻辑:当且仅当内存地址 V 的值等于期望值 A 时,处理器才会将 V 的值更新为 B。否则,说明该变量已被其他线程修改,当前线程什么都不做,通常会进入自旋(死循环重试)。
- CAS的优缺点
优点:
- 非阻塞性:CAS 是一种非阻塞算法(Non-blocking),它不需要像 synchronized 那样挂起和恢复线程。
- 性能高:在低、中度竞争的情况下,由于减少了线程上下文切换的开销,效率远高于重量级锁。
缺点:
- 循环时间长(自旋开销):如果高并发下竞争激烈,CAS 会频繁失败并不断自旋,这会给 CPU 带来巨大的计算压力。
- 只能保证一个共享变量的原子操作:对于多个变量的操作,仍需使用 synchronized 或 ReentrantLock。
- ABA 问题(最经典的缺点)。
- 什么是 ABA 问题?
- 如果变量初始值为A,在线程1准备修改它的过程中,线程2快速地将其改成了B,然后又改回了A。 现象:线程1观察到值依然是A,认为它没变过,于是CAS成功。 风险:虽然数值没变,但变量的状态(或对象内部的属性)可能已经发生了变化,导致逻辑错误。
- Java提供了AtomicStampedReference类,通过引入版本号(Stamp)来解决: 每次变量更新时,不仅更新值,还增加一个版本号。只有值和版本号都一致,CAS才会成功。
- 还可以设置时间戳来解决。
- CAS 在Java中的实现
- 在Java中,CAS主要由 sun.misc.Unsafe 类提供支持。该类中的方法(如 compareAndSwapInt)是 native 的,直接调用硬件底层的指令。
ReentrantLock(可重入锁)
- ReentrantLock是 Java java.util.concurrent.locks 包下的可重入锁实现,基于 AQS(抽象队列同步器)构建,是 synchronized 的 "增强版"------ 既保留了 synchronized 的可重入特性,又提供了更灵活的同步控制能力。
两者对比:
| 特性 |
synchronized |
ReentrantLock |
| 实现层面 |
JVM 层面(关键字),由 C++ 实现 |
JDK 层面(API),由 Java 编写(基于 AQS) |
| 锁的释放 |
自动释放(代码执行完或异常后) |
手动释放(必须在 finally 中调用 unlock()) |
| 灵活性 |
低(不可中断,无超时机制) |
高(支持尝试获取、超时获取、可中断获取) |
| 公平性 |
只支持非公平锁 |
支持公平锁与非公平锁(默认非公平) |
| 等待队列 |
只能关联 1 个 等待队列(wait/notify) |
可以绑定 多个 Condition(精细化唤醒) |
ReentrantLock的特有高级功能
- 响应中断 (lockInterruptibly)
- synchronized一旦进入阻塞等待,除非拿到锁,否则无法被中断。而ReentrantLock允许线程在等待锁的过程中响应Thread.interrupt(),从而避免死等。
- 超时机制 (tryLock)
- 线程可以尝试获取锁,如果锁被占用,立即返回 false 或者等待一段时间后返回,而不是一直阻塞。这在预防死锁时非常有用。
- 公平锁 (Fairness)
- 公平锁:按照线程请求锁的顺序分配,先到先得。
- 非公平锁(默认):允许"插队"。如果新来的线程正好碰到锁释放,它可以直接抢占,性能通常比公平锁高。
- 多个 Condition 对象
- 通过lock.newCondition(),你可以创建多个等待集。例如在阻塞队列中,可以定义 notFull 和 notEmpty 两个条件,实现比 notifyAll 更精准的线程唤醒。
基本使用示例:
java
复制代码
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
// 创建非公平锁(默认),若需公平锁:new ReentrantLock(true)
private static final ReentrantLock lock = new ReentrantLock();
public static void doTask() {
// 1. 普通获取锁(不可中断)
lock.lock();
try {
// 临界区代码(线程安全)
System.out.println(Thread.currentThread().getName() + " 执行任务");
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 必须在finally中释放锁,否则锁永远无法释放
lock.unlock();
}
}
// 超时获取锁示例
public static void tryLockWithTimeout() {
try {
// 尝试在2秒内获取锁,获取成功返回true,失败返回false
if (lock.tryLock(2, java.util.concurrent.TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() + " 超时获取锁成功");
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 超时获取锁失败");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) {
// 测试普通获取锁
new Thread(ReentrantLockDemo::doTask, "线程1").start();
new Thread(ReentrantLockDemo::doTask, "线程2").start();
// 测试超时获取锁
new Thread(ReentrantLockDemo::tryLockWithTimeout, "线程3").start();
}
}