线程安全分类及虚拟机锁优化
1. 线程安全问题
线程安全问题是多线程之间访问共享数据时的数据安全问题;
线程安全按数据操作安全等级可以分为 5 大类:
不可变、绝对线程安全、相对线程安全、线程兼容、线程对立
线程安全的同步措施:互斥同步、非阻塞同步、无同步措施
线程安全的锁优化:适应性自旋、锁消除、锁粗化、轻量级锁、偏向锁
2. 线程安全-线程安全的类别
2.1 不可变
比如 final 修饰的对象、常量,是不会改变的,在多个线程之中永远是一致的状态。不存在线程安全问题。
public final int value = 1;
2.2 绝对线程安全
是指对于一个类,不管运行时环境如何,调用者都不需要给出额外的同步措施。
绝对线程安全的成本最大,不惜效率保证所有的操作是安全的。即便是 java.util.Vector 是一个线程安全的容器,但类 Vector 仍然不是绝对的线程安全。存在并发环境下,操作同一个对象时出现不正确的操作。
2.3 相对线程安全
是指对这个对象单独的操作是线程安全的,调用端不再需要额外的同步措施;
例如类 Vector 的所有方法都有关键 synchronized 修饰
因此,相对线程安全的对象,在一些特定顺序的连续调用,需要调用端做额外的同步措施,来保证调用的正确性。* 在 Java 中大多数线程安全的类归类于相对线程安全;如 Vector、HashTable
示例
import java.util.Vector;
/**
* @description: 绝对线程安全
* 绝对线程安全,是指对于某个类,在调用端不再需要额外的同步措施
* 如下例子如果不加额外的同步措施,运行后会抛出异常
* Exception in thread "Thread-34340" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 9
* at java.util.Vector.remove(Vector.java:831)
*
* 尽管 Vector 的方法都是线程安全的方法,多线程环境下,不在方法调用端做额外的同步措施,存在某个线程恰好删除了一个元素,导致序号 i 不可用,就会出现越界异常。
* 需要 synchronized (vector) {} 在调用端额外做同步措施
**/
public class ThreadSafeAbsolute {
private static Vector<Integer> vector = new Vector<Integer>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread removeThread = new Thread(new Runnable() {
@Override
public void run() {
// 额外的同步措施
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
}
});
Thread printThread = new Thread(new Runnable() {
@Override
public void run() {
// 额外的同步措施
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
System.out.println(vector.get(i));
}
}
}
});
removeThread.start();
printThread.start();
// 不要开启过多线程,会导致线程假死
while (Thread.activeCount() > 20) {}
}
}
}
2.4 线程兼容
是指对象本身不是线程安全的,但通过在调用端额外的做同步措施,能保证对象在并发环境下操作是线程安全的。这样的类或对象是属于线程兼容的。
Java API 中的大部分类是线程兼容的;如 ArrayList、HashMap
2.5 线程对立
是指无论作什么同步措施都不能保证对象在并发环境下是线程安全的。
例:线程类 Thread 的 suspend() 和 resume() 方法,两个线程同时持有一个线程对象,一个尝试中断线程,一个尝试唤起线程,并发进行下无论调用时是否做了同步措施,该线程都存在死锁风险。
如果中断的线程就是即将要被唤醒的线程,就会发生死锁,以上两个方法被废弃也是这个原因。
3. 同步措施-线程安全的实现方法
3.1 互斥同步
多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用。
临界区、互斥量、信号量都是主要的互斥实现方式;
- 临界区
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件。 - 互斥量
一种锁机制,多个线程竞争临界区的资源,一个线程已获取临界区资源,其他线程就无法获取临界区资源,需等待该线程释放掉临界区资源。 - 信号量
一个线程同步结构,用于在线程间传递信号,以避免出现信号丢失。
一种信号机制,某个任务完成后可通过信号量通知到其他的任务执行后续的动作。
synchronized 的互斥同步原理
Java 最基本的互斥实现就是关键字 synchronized;
被该关键字修饰的同步块,前后会生成两个字节码指令 monitorenter 和 monitorexit;
在执行 monitorenter 指令前首先尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有对象的锁,则把锁的计数器加一;相应的在执行 monitorexit 指令时,则将锁定额计数器减一;当锁的计数器为零时,则释放锁;
如果获取锁失败,则当前线程就要阻塞等待,直到对象锁被另一个线程释放。
ReentrantLock 的互斥同步
相比 synchronized 增加了一些高级功能,主要有等待可中断、可实现公平锁、锁可绑定多个条件;
通过方法 lock() 和 unlock() 配合 try/finally 使用
- 等待可中断
是指当前持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,去处理其他的操作。 - 公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间来依次获得锁;
非公平锁,在锁被释放时,任何一个等待锁的线程都有机会获得锁。
synchronized 是非公平锁,ReentrantLock 默认也是非公平锁,可以通过构造方法要求使用公平锁; - 绑定多个条件
是指 ReentrantLock 对象可以同时绑定多个 Condition 对象;需要和多个条件关联时,则需要多次调用 newCondition() 方法即可。
在 synchronized 下锁的对象 wait() 和 notify() notifyAll() 方法可以实现一个隐含的条件;如要支持多个条件,则需要额外添加一个锁;
JDK 1.6 之后,synchronized 和 ReentrantLock 性能差不多,推荐使用 synchronized
状态转换耗时操作
Java 线程是映射到操作系统的原生线程上,如果要阻塞或唤醒一个线程,都需要操作系统来完成,这就需要从用户态转换到核心态中,状态转换需要耗费很多处理器时间。
3.2 非阻塞同步
是指在并发环境下,先进行操作,如果没有其他线程争用共享数据,操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施,比如重试机制。
这种策略属于乐观并发策略,大多乐观并发策略的实现不需要把线程挂起。
互斥同步相对的是一种悲观并发策略,认为只要不做正确的同步措施,操作就是不安全的,无论共享数据是否真的存在争用,都要进行加锁。
冲突检测指令
- 测试并设置 Test-and-Set
- 获取并增加 Fetch-and-Increment
- 交换 Swap
- 比较并交换 Compare-and-Swap (CAS)
- 加载链接/条件存储 Load-Linker/Store-Conditional (LL/SC)
3.2.1 CAS 指令
Java 中的比较并交换实现;
比较并交换 CAS 指令需要三个操作数:内存地址、旧值、新值;
比较并交换是指,当且仅当内存地址的值符合旧值时,处理器用新值更新内存地址的值;否则不更新内存地址的值。上述操作是原子操作。
伪代码
if (address -> value == oldValue) {
address -> value = newValue;
}
Java 的交换并替换操作通过 sun.misc.Unsafe 实现,一般的需要通过反射或者 Java API 来间接调用
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
/**
* getAndSetInt 内部是一个无限循环,
* 通过 CAS 指令不断尝试将一个新值 var4 赋给当前地址,如果失败了,则不断尝试,直到比对成功,再将新值替换掉旧值
*/
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}
如 java.util.concurrent.atomic.AtomicInteger 的部分方法使用了 Unsafe 的 CAS 操作。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
3.2.2 ABA 问题
- ABA 问题描述
在 CAS 语义上存在一个漏洞,如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它的值仍然是 A 值,CAS 操作会认为它从来没改变过。实质上这段期间如果他的值被改为 B 值,然后又被改为 A 值,就不会被 CAS 检测到这种变化。 - ABA 问题处理
JUC 包提供了一个带有原子标记的类 AtomicStampedReference,可以通过控制变量值的版本来保证 CAS 的正确性。或者通过互斥同步措施来处理。
模拟ABA问题日志,T1、T2两个线程模拟AtomicInteger操作;T3、T4两个线程模拟AtomicStampedReference操作。通过记录版本号来规避潜在风险
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABADemo {
private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(100, 0);
private static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
abaProblem();
abaSolution();
}
private static void abaSolution() throws InterruptedException {
int[] stampHolder = {atomicStampedRef.getStamp()}; // 获取初始版本号
// 线程 T3:尝试更新值
Thread t3 = new Thread(() -> {
while (true) {
Integer value = atomicStampedRef.getReference();
Integer finalValue = atomicStampedRef.getReference();
int finalStamp = atomicStampedRef.getStamp();
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". T3 updated value from 100 to 101. start");
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". Current value: " + finalValue + ", stamp: " + finalStamp);
if (value.equals(100)) {
if (atomicStampedRef.compareAndSet(value, 101, stampHolder[0], stampHolder[0] + 1)) {
finalValue = atomicStampedRef.getReference();
finalStamp = atomicStampedRef.getStamp();
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". Current value: " + finalValue + ", stamp: " + finalStamp);
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". T3 updated value from 100 to 101. end");
break;
}
}
}
});
// 线程 T4:模拟 ABA 情况
Thread t4 = new Thread(() -> {
try {
Thread.sleep(50); // 让 T1 先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". T4 performed ABA operation. start");
// 将值从 100 改为 101 再改回 100
atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
Integer finalValue = atomicStampedRef.getReference();
int finalStamp = atomicStampedRef.getStamp();
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". Current value: " + finalValue + ", stamp: " + finalStamp);
atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
finalValue = atomicStampedRef.getReference();
finalStamp = atomicStampedRef.getStamp();
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". Current value: " + finalValue + ", stamp: " + finalStamp);
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". T4 performed ABA operation. end");
});
t3.start();
t4.start();
t3.join();
t4.join();
// 最终检查结果
Integer finalValue = atomicStampedRef.getReference();
int finalStamp = atomicStampedRef.getStamp();
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". Final value: " + finalValue + ", stamp: " + finalStamp);
}
private static void abaProblem() throws InterruptedException {
// 线程 T1:尝试更新值
Thread t1 = new Thread(() -> {
while (true) {
Integer value = atomicInteger.get();
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". T1 updated value from 100 to 101. start");
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". Current value: " + value);
if (value.equals(100)) {
if (atomicInteger.compareAndSet(value, 101)) {
value = atomicInteger.get();
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". Current value: " + value);
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". T1 updated value from 100 to 101. end");
break;
}
}
}
});
// 线程 T2:模拟 ABA 情况
Thread t2 = new Thread(() -> {
try {
Thread.sleep(50); // 让 T1 先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". T2 performed ABA operation. start");
// 将值从 100 改为 101 再改回 100
atomicInteger.compareAndSet(100, 101);
Integer finalValue = atomicInteger.get();
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". Current value: " + finalValue);
atomicInteger.compareAndSet(101, 100);
finalValue = atomicInteger.get();
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". Current value: " + finalValue);
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". T2 performed ABA operation. end");
});
t1.start();
t2.start();
t1.join();
t2.join();
// 最终检查结果
Integer finalValue = atomicInteger.get();
System.out.println("currentTimeMillis:" + System.currentTimeMillis() + ". Final value: " + finalValue);
}
}
执行日志
currentTimeMillis:1733638244406. T1 updated value from 100 to 101. start
currentTimeMillis:1733638244407. Current value: 100
currentTimeMillis:1733638244407. Current value: 101
currentTimeMillis:1733638244407. T1 updated value from 100 to 101. end
currentTimeMillis:1733638244468. T2 performed ABA operation. start
currentTimeMillis:1733638244468. Current value: 101
currentTimeMillis:1733638244468. Current value: 100
currentTimeMillis:1733638244468. T2 performed ABA operation. end
currentTimeMillis:1733638244468. Final value: 100
currentTimeMillis:1733638244470. T3 updated value from 100 to 101. start
currentTimeMillis:1733638244470. Current value: 100, stamp: 0
currentTimeMillis:1733638244470. Current value: 101, stamp: 1
currentTimeMillis:1733638244470. T3 updated value from 100 to 101. end
currentTimeMillis:1733638244532. T4 performed ABA operation. start
currentTimeMillis:1733638244532. Current value: 101, stamp: 1
currentTimeMillis:1733638244532. Current value: 100, stamp: 2
currentTimeMillis:1733638244532. T4 performed ABA operation. end
currentTimeMillis:1733638244532. Final value: 100, stamp: 2
3.3 无同步方案
对于不涉及到共享数据的方法,自然是不存在线程安全的问题的。无需额外的同步措施去保证操作的安全性。
可重入代码、线程本地存储
- 可重入代码
是指可以在代码执行的任何时刻中断它,转而去执行另外一段代码,在控制权返回时,原来的程序不会出现任何错误。
可重入代码不依赖存储在 Java 堆上的数据或公共的系统资源,用到的状态量都是由参数传入,不调用非可重入的方法; - 线程本地存储
如果一段代码中所需的数据必须与其他代码共享,如果共享数据的代码可以限制在一个线程中执行,这样无需额外的同步措施也不会出现数据争用问题。
Java语言中,如果一个变量要被多线程访问,可以使用volatile关键字声明它为“易变 的”;如果一个变量要被某个线程独享,Java中就没有类似C++中__declspec(thread)[3]这样 的关键字,不过还是可以通过java.lang.ThreadLocal类来实现线程本地存储的功能。每一个线 程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组以 ThreadLocal.threadLocalHashCode为键,以本地线程变量为值的K-V值对,ThreadLocal对象就 是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含了一个独一无二的 threadLocalHashCode值,使用这个值就可以在线程K-V值对中找回对应的本地线程变量。
4. 锁优化-虚拟机的锁优化技术
锁优化技术常见的有适应性自旋、锁消除、锁粗化、轻量级锁、偏向锁
自旋锁在存在竞争的情况下,让当前线程自旋(忙循环)等待后,尝试获取对象的锁。
锁消除是对于不存在共享资源的代码块去除同步措施;
锁粗化是对于频繁加锁解锁操作适当扩大加锁范围;
轻量级锁在无竞争的情况下,使用 CAS 操作做同步;
偏向锁在无竞争的情况下,不再任何同步操作;
4.1 自旋锁
是指当前线程获取锁时,锁已被其他线程持有,当前线程不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,需要让线程执行一个忙循环,这个忙循环称为自旋。这种技术称为自旋锁。
自旋的次数默认为 10 次;可以使用参数 -XX:PreBlockSpin 来更改。
自旋锁的弊端,占用处理器资源,如果自旋次数较多占用时间过长,还没有等到持有锁的线程释放锁,就比较浪费处理器资源。
4.2 锁消除
是指处理器在即时编译器运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。
锁消除的主要依据来源于逃逸分析的数据支持。如果同步代码中,堆上的所有数据都不会逃逸出去,就不会被其他线程访问到,同步加锁措施就无需进行。
4.3 锁粗化
频繁的加锁操作存在一定的性能消耗;如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至出现在循环体内的加锁操作,则虚拟机会扩大加锁的范围,粗化锁的作用范围,避免这种不必要的性能消耗。
原则上是建议锁足够细化,保证同步的操作数量足够小,只在共享数据的实际作用域才进行同步。
4.4 轻量级锁
轻量级锁是相对的概念,传统的锁都是重量级锁,轻量级锁的引用是为了减少重量级锁使用操作系统的互斥量的性能消耗。
4.5 偏向锁
消除数据在无竞争情况下的同步原语。
如果说轻量级锁是在无竞争的情况下使用 CAS 操作去除同步使用的互斥量;
偏向锁就是在无竞争的情况下把整个同步都消除掉
使用参数 -XX:-UseBiasedLocking 来禁止偏向锁优化
4.6 扩展
- HotSpot 虚拟机的对象内存布局
对象头分为两部分信息
一部分存储对象自身的运行时数据,如哈希码、GC 分代年龄等,这部分数据被称为 Mark Word;
另一部分存储指向方法区对象类型数据的指针,如果是数组对象,还有一个额外的部分存储数组的长度。
Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储更多的信息。
例如在 32 位的 HotSpot 虚拟机中对象未被锁定的状态下,Mark Word 的 32bit 空间中,25bit 存储对象的哈希码,4bit 存储对象的分代年龄,2bit 存储锁标志位,1bit 固定为0.
锁标志位
| 存储内容 | 标志位 | 状态 |
|---|---|---|
| 对象哈希码、对象分代年龄 | 01 | 未锁定 |
| 指向锁记录的指针 | 00 | 轻量级锁定 |
| 指向重量级锁的指针 | 10 | 重量级锁定(膨胀) |
| 空,不需要记录信息 | 11 | GC标记 |
| 偏向线程ID、偏向时间戳、对象分代年龄 | 01 |
- 轻量级锁的加锁过程
在代码进入同步块的时候,如果同步对象没有被锁定,锁标志位为 01,虚拟机首先在当前线程的战阵中建立一个名为所记录的空间,用于存储锁对象目前的 Mark Word 的拷贝,这个拷贝被称为 Displaced Mark Word。
然后虚拟机将使用 CAS 操尝试将对象的 Mark Word 更新为指向锁记录的指针,即指向 Displaced Mark Word;
如果这个更新操作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位转变为 00,即表示当前对象处理轻量级锁锁定状态。
如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果指向则说明当前线程已经拥有了该对象的锁,就可以直接进入同步块继续执行;否则说明该对象已经被其他线程抢占。如果有两条以上的线程争用同一个锁,轻量级锁要膨胀为重量级锁,锁标志位变为 10,Mark Word 存储的就是指向重量级锁(互斥量)的指针,后面的线程要进入阻塞状态
如果没有锁竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销;
如果存在锁竞争,还是用轻量级锁,除了互斥量的开销,还要做 CAS 操作,此时轻量级锁比重量级锁还要慢。
- 偏向锁技术
如上表,对象头的 MarkWord 在偏向锁和未锁定状态下的标志位都是 01,区别是存储内容不同。未锁定状态下,Mark Word 存储对象哈希码、对象分代年龄;可偏向状态下,存储的是偏向线程 ID、偏向时间戳、对象分代年龄;
虚拟机通过设置参数 -XX:+UseBiasedLocking 可以启动偏向锁;当锁对象第一次被线程获取的时候,虚拟机将会把对象头的 Mark Word 的标志位设置为 01,此时为偏向模式,同时使用 CAS 操作把这个锁的线程 ID 记录在 Mark Word 中;如果 CAS 操作成功持有偏向锁的线程每次进入这个锁的同步代码块时,虚拟机都可以不再进行额外的同步操作。
当存在另外一个线程尝试获取这个锁时,偏向模式结束;根据锁对象当前被锁定状态,撤销偏向恢复到未锁定或者轻量级锁的状态。然后按照轻量级锁加锁过程进行处理。
5. 常用锁
- Synchronized:适合大多数基本的同步需求,自动管理锁的获取和释放,简单易用。
- ReentrantLock:提供了更多功能和更好的性能,特别是在高竞争环境下,适合需要更灵活锁特性的场景。
- Read/Write Lock:适合读多写少的场景,提高了读操作的并发性能,特别适用于缓存系统和配置管理。
- StampedLock:适用于高并发读场景,通过乐观读减少锁开销,特别适合实时数据分析和金融交易系统。
5.1 Synchronized
5.1.1 分析
- 是JVM内置锁,Java 提供的最简单的线程同步机制。
- 独占性:同一时间只有一个线程可以访问被 synchronized 修饰的方法或代码块。
- 自动释放:当持有锁的线程执行完毕或抛出异常时,锁会自动释放。
- 局限性:灵活性较低,无法实现一些高级特性(如可中断等待、公平锁等)
5.1.2 应用场景
银行账户转账
5.1.3 场景描述
多个线程同时尝试对同一个银行账户进行转账操作。为了避免数据不一致的问题,需要确保每次转账都是原子性的。
5.1.4 示例代码块
public class SyncExample {
private final Object lock = new Object();
// 同步方法
public synchronized void syncMethod() {
System.out.println(Thread.currentThread().getName() + " is executing syncMethod.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 同步代码块
public void syncBlock() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " is executing syncBlock.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
5.2 ReentrantLock
5.2.1 分析
- 显式锁:需要手动获取和释放锁。
- 灵活性:提供了比 synchronized 更多的功能,如可中断锁等待、公平锁、尝试非阻塞获取锁等。
- 性能:在某些情况下性能优于 synchronized,尤其是在高竞争的情况下。
必须手动管理:开发者需要确保在所有路径上正确地释放锁,否则可能导致死锁。 - 提供了比 synchronized 更多的功能,如可中断锁等待、公平锁、尝试非阻塞获取锁等。比JVM内置锁性能更高
5.2.2 应用场景:需要更灵活锁特性的场景,如可中断锁等待、公平锁
5.2.3 场景描述:
- 任务调度系统:在一个多线程的任务调度系统中,某些任务可能需要等待特定条件满足才能继续执行。使用 ReentrantLock 可以允许任务在等待时被中断,从而提高系统的响应性和可靠性。
- 公共资源管理:在管理有限的公共资源(如数据库连接、文件句柄)时,使用公平锁可以确保资源分配的顺序性,避免饥饿问题。
5.2.4 示例
基础用法
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
public void method() {
lock.lock(); // 获取锁
try {
System.out.println(Thread.currentThread().getName() + " is executing method.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock(); // 确保锁被释放
}
}
}
可中断锁等待
如果希望线程在等待锁时可以被中断,可以使用 lockInterruptibly() 方法
public void interruptibleMethod() throws InterruptedException {
lock.lockInterruptibly(); // 获取锁,且可中断
try {
// 执行临界区代码
} finally {
lock.unlock(); // 确保锁被释放
}
}
尝试非阻塞获取锁
public boolean tryMethod() {
if (lock.tryLock()) {
try {
// 执行临界区代码
return true;
} finally {
lock.unlock(); // 确保锁被释放
}
} else {
// 锁不可用,执行其他逻辑
return false;
}
}
条件变量支持Condition
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void awaitCondition() throws InterruptedException {
lock.lock();
try {
// 等待特定条件满足
condition.await();
} finally {
lock.unlock();
}
}
public void signalCondition() {
lock.lock();
try {
// 唤醒一个等待的线程
condition.signal();
} finally {
lock.unlock();
}
}
}
简单的任务调度系统
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TaskScheduler {
private final Queue<Runnable> taskQueue = new LinkedList<>();
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
public void submitTask(Runnable task) {
lock.lock();
try {
taskQueue.offer(task);
notEmpty.signal(); // 唤醒等待的任务处理线程
} finally {
lock.unlock();
}
}
public void processTasks() throws InterruptedException {
lock.lock();
try {
while (taskQueue.isEmpty()) {
notEmpty.await(); // 等待任务提交
}
Runnable task = taskQueue.poll();
lock.unlock(); // 提前释放锁,避免持有锁期间执行耗时操作
try {
task.run(); // 执行任务
} finally {
lock.lock(); // 重新获取锁
}
} finally {
lock.unlock();
}
}
}
5.3 Read/Write Lock
5.3.1 分析
- 读写分离:允许多个线程同时读取共享资源,但在写入时确保独占访问。
- 适合读多写少场景:当读操作远多于写操作时,使用读写锁可以显著提高并发性能。
- 细粒度控制:提供了更细粒度的锁控制,允许读操作并发进行,而写操作互斥。
- 局限性:如果写操作过于频繁,可能导致读操作被长时间阻塞。
5.3.2 应用场景
读多写少的场景,如缓存系统、配置管理
5.3.3 场景描述
- 缓存系统:在一个分布式缓存系统中,大量的客户端请求会频繁地读取缓存数据,但只有少数请求会更新缓存。使用读写锁可以在读操作并发进行的同时,确保写操作的独占性。
- 配置管理:在应用程序中,配置文件可能会被多个线程频繁读取,但很少会被修改。使用读写锁可以提高读操作的并发性能,同时保证写操作的安全性。
5.3.4 示例
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private int value = 0;
public void read() {
rwLock.readLock().lock(); // 获取读锁
try {
System.out.println(Thread.currentThread().getName() + " reads: " + value);
} finally {
rwLock.readLock().unlock(); // 释放读锁
}
}
public void write(int newValue) {
rwLock.writeLock().lock(); // 获取写锁
try {
value = newValue;
System.out.println(Thread.currentThread().getName() + " writes: " + value);
} finally {
rwLock.writeLock().unlock(); // 释放写锁
}
}
}
5.4 StampedLock
5.4.1 分析
- 结合了读写锁和乐观读锁:提供了三种模式:读锁、写锁和乐观读。
- 适用于高并发读场景:乐观读不加锁,只有在检测到数据变更时才会重试或升级为读锁。
- 性能优化:减少了不必要的锁开销,特别适合读操作远远超过写操作的场景。
- 局限性:使用 StampedLock 需要更复杂的逻辑来处理锁的获取和释放,以及处理可能的锁转换
5.4.2 应用场景
高并发读场景,如实时数据分析、金融交易系统
5.4.3 场景描述
- 实时数据分析:在一个大数据分析平台上,大量用户会频繁查询最新的统计数据,但数据更新频率较低。使用 StampedLock 的乐观读模式可以在大部分情况下不加锁地读取数据,只有在检测到数据变更时才升级为读锁或写锁。
- 金融交易系统:在高频交易系统中,订单簿的读取远多于写入。使用 StampedLock 可以显著提高读操作的性能,同时确保写操作的原子性。
5.4.4 示例
import java.util.concurrent.locks.StampedLock;
public class StampedLockExample {
private final StampedLock stampedLock = new StampedLock();
private int value = 0;
public int readValueOptimistic() {
long stamp = stampedLock.tryOptimisticRead(); // 尝试乐观读
int v = value;
if (!stampedLock.validate(stamp)) { // 检查是否有其他线程修改过数据
stamp = stampedLock.readLock(); // 升级为读锁
try {
v = value;
} finally {
stampedLock.unlockRead(stamp); // 释放读锁
}
}
return v;
}
public void writeValue(int newValue) {
long stamp = stampedLock.writeLock(); // 获取写锁
try {
value = newValue;
System.out.println(Thread.currentThread().getName() + " writes: " + value);
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
}
Powered By niaonao
以上内容参照深入理解Java虚拟机一书

浙公网安备 33010602011771号