本文对Java的内存管理模型、volatile关键字和锁机制进行详细阐述,包括synchronized关键字、Lock接口及其实现类ReentrantLock、AQS等的实现原理和常见方法。
一、JMM(Java内存模型)
1. 介绍
JMM定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。
2. 模型
- JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存
二、Volatile关键字
1. 介绍
- Volatile关键字主要用于修饰成员变量(实例变量或类变量),使得对该变量的读写操作直接与主内存交互,而不是通过线程本地的工作内存进行缓存。
- Volatile关键字提供了一种轻量级的同步机制,用于保证多线程环境下数据的可见性(但不能保证数据的原子性,Synchronized关键字既保证可见性,又保证原子性)
2. Volatile禁止指令重排
(1)指令重排
指令重排是一种编译器和处理器优化技术,它改变指令执行的顺序,以提高性能。在单线程环境下,这种优化不会影响程序的正确性。但是在多线程环境下,指令重排可能导致意想不到的结果,破坏程序的正确性。
(2)Volatile如何禁止指令重排
- 通过在读写 volatile 变量时插入内存屏障(Memory Barriers)来实现
- 写屏障(Write Barrier):确保在写 volatile 变量之前的所有写操作在内存中完成。
- 读屏障(Read Barrier):确保在读 volatile 变量之后的所有读操作在内存中完成
- volatile关键字一般加在写变量最后一个,读变量第一个。
class Example {
private volatile boolean flag = false;
private int a = 0;
public void writer() {
a = 1; // 1
flag = true; // 2
}
public void reader() {
if (flag) { // 3
int i = a; // 4
}
}
}
上述示例中:
- 在writer方法中,写入a = 1(1)操作不能被重排序到写入flag = true(2)之后。因为flag是volatile,在写flag的时候会插入一个StoreStore屏障,确保在flag被写入之前,所有之前的写操作都完成了。
- 在reader方法中,读取flag(3)操作不能被重排序到读取a(4)之前。因为flag是volatile,在读flag的时候会插入一个LoadLoad屏障,确保在flag被读取之后,所有之前的读操作都完成了。
三、锁
1. 悲观锁和乐观锁
(1)悲观锁:认为访问共享资源时一定会发生线程安全问题,因此在访问时必须加锁。
(2)乐观锁:认为访问共享资源时不一定会发生线程安全问题,因此不会加锁,当提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改即可。
(3)二者区别
特性 | 悲观锁 | 乐观锁 |
---|---|---|
锁定机制 | 操作数据前先加锁,其他线程无法操作 | 操作数据时不加锁,提交时检查冲突 |
使用场景 | 适用于写多读少,冲突概率高的场景 | 适用于读多写少,冲突概率低的场景 |
性能 | 由于频繁加锁和释放锁,性能较低 | 不加锁,性能较高 |
实现方式 | 使用数据库锁机制(如行锁、表锁) | 使用版本号或时间戳进行冲突检测 |
数据一致性 | 一致性强,通过锁机制防止并发修改 | 一致性依赖于冲突检测和重试机制 |
并发性 | 并发性差,锁定会阻塞其他线程 | 并发性好,线程可以同时进行操作 |
死锁风险 | 存在死锁风险 | 不存在死锁风险 |
典型应用 | 行锁、表锁、页锁和意向锁(数据库)、ReentrantLock等 | 版本控制系统、CAS(Compare and Swap)操作 |
2. synchronized 关键字
(1)介绍
synchronized 关键字是一种对象锁,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁。
(2)底层实现原理
① Monitor(对象监视器)
- Monitor(监视器)是一种同步机制,用于管理多线程之间的互斥访问和并发控制。Monitor是JVM提供的,但是是基于c++实现的。
- Monitor的结构
- Owner:存储当前获取锁的线程的,只能有一个线程可以获取
- EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
- WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
② synchronized关键字实现原理
线程获得锁需要使用对象(锁)关联monitor监视器,即获得monitor的持有权。
(3)使用方法
- 同步方法:当一个线程执行同步方法时,它会自动获得该方法所属对象的锁。其他线程在获得该锁之前无法执行该方法。
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
- 同步块:同步块用于在一个方法内同步部分代码,提供了比同步方法更细粒度的控制。
public class SynchronizedBlockExample {
private final Object lock = new Object();
private int count = 0;
public void increment() {
synchronized (lock) {
count++;
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
- 静态同步方法:静态同步方法锁定的是类对象,对于所有该类的实例都有效。
public class StaticSynchronizedExample {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static synchronized int getCount() {
return count;
}
}
3. synchronized锁升级
Monitor实现的锁属于重量级锁,因为monitor是使用c++实现的,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁。
(1)对象的内存结构
注:填充为8的整数倍是方便字节对齐提高程序运行效率 ,而且对于压缩指针也更加的方便。
(2)轻量级锁
在无竞争的情况下,轻量级锁使用CAS操作来实现锁的获取和释放,提高运行效率。
① 加锁流程
- 在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
- 通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
- 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。(仍需CAS操作)
- 如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
② 解锁过程
- 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
- 如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
- 如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。
(3)偏向锁
与轻量级锁不同的是,偏向锁只第一次获得锁时使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后该线程再获取锁时,只需要判断锁对象mark word中是否是自己的线程id即可。
注:轻量级锁和偏向锁都是在没有竞争的情况下使用,一旦发生了竞争会立刻升级为重量级锁。
4. Lock接口:ReentrantLock
Lock接口提供了一种比传统的同步块和方法更灵活的锁机制。Lock 接口有多种实现,其中最常用的是 ReentrantLock。
(1)Lock接口常见实现类
实现类 | 描述 |
---|---|
ReentrantLock | 可重入的互斥锁,具有与使用 synchronized 方法和语句相同的基本行为和语义,但功能更强大。 |
ReentrantReadWriteLock.ReadLock | 读锁,可被多个读线程同时持有,只要没有写线程持有写锁。 |
ReentrantReadWriteLock.WriteLock | 写锁,独占锁,只有写线程可以持有,读线程和其他写线程都被阻塞。 |
StampedLock | 一种新的读写锁实现,支持乐观读锁和写锁的互斥锁操作,提高并发性和性能。 |
LockSupport | 提供了基本的线程阻塞原语,用于创建锁和其他同步工具。 |
(2)ReentrantLock
① ReentrantLock是一个可重入且独占式的锁,还支持轮询、超时、中断、公平锁和非公平锁等。
② 特点
- 可重入性:一个线程可以多次获得该锁,而不会导致死锁。这意味着同一个线程可以进入锁保护的代码块多次,必须确保在离开代码块时相应的次数解锁。
- 公平性:可以选择公平锁或者非公平锁。公平锁按照线程请求的顺序获取锁,非公平锁则没有这种顺序,可能导致某些线程长时间等待。
- 灵活性:提供了比 synchronized 块更灵活的锁获取方式,如可中断的锁获取、超时的锁获取。
③ 实现原理
- ReentrantLock使用CAS+AQS来实现,使用用一个整数state表示锁的状态,使用一个先进先出的队列(CLH)管理被阻塞的线程。
- 当线程需要获得锁时,使用CAS操作修改锁的状态,修改成功则获得锁;修改失败则加入CLH队列管理。
④ 常用方法
方法 | 描述 |
---|---|
lock() | 获取锁。如果锁不可用,则当前线程将被禁用以进行线程调度,并处于休眠状态,直到获得锁。 |
unlock() | 释放锁。 |
tryLock() | 尝试获取锁。如果锁在调用时未被另一个线程持有,则获取锁并立即返回 true ;否则返回 false 。 |
tryLock(long time, TimeUnit unit) | 尝试在给定的等待时间内获取锁。如果锁在给定时间内可用,则获取锁并返回 true ;否则返回 false 。 |
lockInterruptibly() | 如果当前线程未被中断,则获取锁。如果锁不可用,则当前线程出于线程调度目的将被禁用并处于休眠状态,直到发生以下两种情况之一:锁由当前线程获得;或者其他某个线程中断当前线程。 |
newCondition() | 返回一个绑定到此 Lock 实例的新 Condition 实例。 |
5. synchronized与ReentrantLock的比较
特性 | synchronized | ReentrantLock |
---|---|---|
锁类型 | 隐式锁 | 显式锁 |
获取锁的方式 | 自动 | 需要显式调用 lock() 方法 |
释放锁的方式 | 自动 | 需要显式调用 unlock() 方法 |
可重入性 | 是 | 是 |
公平锁支持 | 否 | 是(通过构造函数可以指定) |
条件变量支持 | 否 | 是(通过 newCondition() 方法) |
获取锁中断 | 否 | 是(通过 lockInterruptibly() 方法) |
超时锁获取 | 否 | 是(通过 tryLock(long time, TimeUnit unit) 方法) |
性能开销 | 较低(JVM 内部优化) | 较高 |
锁的范围 | 代码块或方法 | 灵活,可锁代码块 |
代码复杂度 | 低 | 较高 |
推荐使用场景 | 简单的同步需求 | 复杂的同步需求,如需要公平锁、超时等 |
6. AQS(Abstract Queued Synchronizer,抽象队列同步器)
(1)介绍
AQS是Java 并发包 (java.util.concurrent.locks) 中的一个核心框架,是构建各种锁和同步组件的基础框架。
(2)实现原理
- AQS 使用一个 int 类型的变量 state 表示同步状态。
- AQS 内部维护了一个 FIFO 队列,用于管理获取锁失败的线程。线程在争夺锁失败后会被封装成队列节点(Node)并加入到等待队列中。
- 队列中每个节点包含线程引用和状态标志
(3)常用方法
方法 | 描述 |
---|---|
acquire(int arg) | 独占模式获取锁,如果获取失败则进入等待队列。 |
release(int arg) | 独占模式释放锁,如果释放成功则唤醒等待队列中的下一个节点。 |
acquireShared(int arg) | 共享模式获取锁,如果获取失败则进入等待队列。 |
releaseShared(int arg) | 共享模式释放锁,如果释放成功则唤醒等待队列中的下一个节点。 |
addWaiter(Node mode) | 将当前线程封装成节点并加入等待队列的尾部。 |
unparkSuccessor(Node node) | 唤醒等待队列中的下一个节点。 |
(4)常见实现类
实现类 | 描述 |
---|---|
ReentrantLock | 可重入独占锁。允许线程重复获取同一把锁。 |
ReentrantReadWriteLock | 可重入读写锁。支持多个线程同时读取数据,但写操作是互斥的。 |
Semaphore | 信号量。控制同时访问特定资源的线程数。 |
CountDownLatch | 倒计时闩。一个或多个线程等待其他线程完成操作后再继续执行。 |
CyclicBarrier | 循环栅栏。一组线程相互等待,达到同步点后再继续执行。 |
Phaser | 阶段器。管理多个参与者线程的同步,支持动态注册和注销。 |
StampedLock | 读写锁的改进版本,支持乐观读取和写锁。 |
Condition | 条件变量。与锁配合使用,允许线程等待某个条件变为真。 |