Java--锁策略--上
在前面多线程相关知识点的讲解中,我们简单介绍了一下锁的特性,在这里我们深入探讨一下锁相关的知识点,在不同的场景下我们应该怎么使用锁进行编写程序~~
1.悲观锁和乐观锁
1.1悲观锁
理论原理:
悲观锁顾名思义就是一直很悲观,它假设每次并发访问共享资源都会发生冲突,因此每次操作都采取加锁的方式来保证线程安全。即便说有一个线程已经获取到了锁,其他线程仍然会被阻塞,直到锁被释放。
悲观锁的核心思想是:假想数据会被修改,因此必须加锁。
在Java中,通过 synchronized 或 ReentrantLock 来实现悲观锁。每次进入代码块的时候线程会阻塞并尝试获取锁,只有持有锁的线程可以继续执行,其他的线程会被挂起,直到锁释放。
实现逻辑
synchronized 是Java中最常见的悲观锁实现。它通过JVM的monitor实现锁的管理,使用的是重量级锁(即内核层级的加锁机制),此外, ReentrantLock也可以实现悲观锁,通过显示调用Lock()和unlock() 来确保互斥性。
在高并发下环境下,悲观锁由于线程需要等待上一个锁的释放才能继续下一个步骤,会导致线程的阻塞和上下文切换,可能引起性能瓶颈。在轻负载的情况下悲观锁的开销才会小一点。
一般用于写操作频繁的场景之下,比如数据库事务中对同一个数据的频繁修改,保障数据的一致性;以及多线程访问同一个资源的情况,并且资源更新复杂的情况,比如文件的写操作,读取的时候可能会更改文件内容。
优缺点
优点::保证了数据的一致性。
缺点:性能较差,尤其在多线程资源竞争激烈的时候,会产生大量的上下文切换和·资源浪费。
代码示例:
这里使用一个两个线程自增1000次的场景:
public class demo20 {
private static int count = 0;
private static final ReentrantLock lock = new ReentrantLock();
public static void increment(){
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Runnable task = (() -> {
for (int i = 0; i < 1000; i++) {
increment();
}
});
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("counter = "+count);
}
}
这个程序使用的就是悲观锁,lock.lock()就能体现出来(调用lock对象的lock方法,也就是说刚进入这个方法就直接被加锁了)。线程在count++之前必须先获取锁,如果锁被其他线程持有,当前线程就会被阻塞(挂起),直到获取锁为止,这就是总是假设最坏情况的策略~
1.2 乐观锁
理论原理:
乐观锁认为并发访问资源不会冲突,因此在读取的时候不会进行加锁,而是在更新时通过检测冲突来确保数据一致性。
乐观锁的核心思想就是:假想数据不会被修改,因此无需加锁,但是当更改时会进行验证。
在Java中,通常会通过 CAS(Compare-And-Swap) 来实现乐观锁。
实现机制:
使用CAS操作来实现。CAS是一种无锁操作,它通过原子操作比较内存中的数据值是否与预期的值相同,如果相同就替换,否则就表示发生了冲突,需要重试。
AtomicInteager、AtomicReference和AtomicBoolean 等都是基于CAS实现的。
CAS操作通常比悲观锁更加高效,因为它避免了线程挂起和上下文切换的开销,但是如果发生大量冲突时,CAS也会导致较低的性能。对于大多数轻量级的读写操作,乐观锁可以通过减少所竞争来提高性能。
一般适用于读多写少的场景,例如股票价格、用户信息等频繁读取的资源更新;以及高并发场景没比如高效的并发计数器。
优缺点
优点:高并发时比悲观锁性能更好,避免了加锁带来的开销,并且能高效处理无锁操作,是和读多写少的场景。
缺点:如果发生频繁冲突,CAS可能重试多次,影响性能。
代码实例:
package thread;
import java.util.concurrent.atomic.AtomicInteger;
public class demo21 {
private static final AtomicInteger counter = new AtomicInteger(0);
public static void increment() {
counter.incrementAndGet();//原子性自增操作(i+=1的线程安全版)
}
public static void main(String[] args) throws InterruptedException {
Runnable task = (() -> {
for (int i = 0; i < 1000; i++) {
increment();
}
});
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("counter:"+counter.get());
}
}
这个程序使用AtomicInteger来保证线程安全,这里面使用的是乐观锁加上CAS。
乐观锁策略:乐观锁假设并发重冲突的概率很低,他不需要先加锁,而是直接尝试更新数据,这个代码中看不到synchronized或者lock()这样的加锁代码,increment()方法执行的时候,线程是不阻塞的。
虽然说我们运用到了CAS,但是在代码中我们并没有看到CAS出现,其实CAS藏在了incrementAndGet方法的内部实现里面,虽然在写代码的时候看不到CAS,但是在调用counter.incrementAndGet()这个方法的时候,方法内部会自动调用CAS逻辑。
这里我们看一下简化版的CAS源码:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
1. 参数含义:
Object o:操作的对象(比如AtomicInteger实例)。long offset:内存偏移量。Java 通过这个偏移量直接定位到对象在内存中的value字段地址。int delta:需要增加的数值(比如1)。
详细步骤:
-
v = getIntVolatile(o, offset);- 动作:从主内存中读取
o对象在offset偏移量处的整数值,赋给变量v。 - 意义:这是获取“期望值”。此时
v代表了线程认为的当前数值。
- 动作:从主内存中读取
-
weakCompareAndSetInt(o, offset, v, v + delta)- 动作:这是 CAS 操作的具体体现。它的意思是:
- 拿着刚才读到的
v去内存里比对一下。 - 如果内存里的值等于
v(说明没人动过),就把内存里的值更新为v + delta,并返回true。 - 如果内存里的值不等于
v(说明被别的线程改过了),就什么都不做,并返回false。
- 拿着刚才读到的
- 关于
weak:weakCompareAndSet(弱比较并交换)在某些 JVM 实现和旧版本中,它可能不保证内存排序的happens-before原则(即不保证 volatile 读写的语义),但在现代 JDK(如 JDK 8+)的AtomicInteger实现中,它的效果通常等同于标准的compareAndSwapInt,都是为了性能优化。
- 动作:这是 CAS 操作的具体体现。它的意思是:
-
while (!...)(自旋)- 动作:这是一个死循环(
do-while)。 - 逻辑:
- 如果 CAS 成功(返回
true),!true为false,循环结束,方法返回。 - 如果 CAS 失败(返回
false),!false为true,循环继续。
- 如果 CAS 成功(返回
- 意义:这就是自旋锁的体现。一旦失败,线程不会阻塞,而是立刻重新执行
do里面的代码:再次读取最新值v,再次尝试 CAS。直到成功为止。
- 动作:这是一个死循环(
2. 举例
假设内存中 count 的值当前是 5,有两个线程 A 和 B 同时执行 getAndAddInt,想要加 1。
-
并发读取:
- 线程 A 读到
v = 5。 - 线程 B 也读到
v = 5。
- 线程 A 读到
-
CAS 竞争:
- 假设线程 A 的手速快一点,先执行了
weakCompareAndSetInt(..., 5, 6)。 - 内存值确实是 5,匹配成功。内存值更新为 6。线程 A 的 CAS 返回
true,循环结束,线程 A 完成。
- 假设线程 A 的手速快一点,先执行了
-
重试:
- 轮到线程 B 执行
weakCompareAndSetInt(..., 5, 6)。 - 此时内存值已经是 6 了,而线程 B 手里的期望值还是 5。
- 匹配失败!CAS 返回
false。 while条件成立,线程 B 没有被挂起,而是立刻进入下一轮循环。
- 轮到线程 B 执行
-
再次读取并尝试:
- 线程 B 再次执行
getIntVolatile,这次读到了最新的值v = 6。 - 线程 B 再次执行 CAS:
weakCompareAndSetInt(..., 6, 7)。 - 这次匹配成功了(内存确实是 6),更新为 7。线程 B 的循环结束。
- 线程 B 再次执行
3. 总结
这段代码展示了 CAS 算法的标准实现模式:
- 无锁:没有使用
synchronized,线程不会挂起。 - 自旋:通过
do-while循环,在冲突发生时不断重试。 - 原子性:依赖底层的
weakCompareAndSetInt(CPU 指令cmpxchg)保证比较和更新这两步是原子的,不可分割。
这就是为什么 AtomicInteger 代码不需要加锁也能保证线程安全的原因。
2. 重量级锁和轻量级锁
锁的核心特性“原子性”,这样的机制追溯源是CPU这样的硬件设备提供的。
- CPU提供了“原子操作指令”。
- 操作系统基于CPU的原子指令,实现了mutex互斥锁、、
- JVM基于操作系统提供的互斥锁,实现了synchronized 和 ReentrantLock等关键字和类。

synchronized不仅仅是对mutex进行封装,在synchronized内部还做了很多其他的工作。
2.1 重量级锁
理论原理:
重量级锁依赖于操作系统的底层机制,比如操作系统的互斥锁。当线程获取到锁失败时,操作系统会将线程阻塞,并进行上下文切换。当线程被唤醒时,它会重新尝试获取锁。
实现机制:
- 在Java中,synchronized是重量级锁的典型实现,它会锁通过操作系统的内核进行管理。在争用激烈时,JVM会将锁升级为重量级锁。
- 在高并发情况下,重量级锁导致线程阻塞,频繁的上下文切换和CPU切换回引发性能瓶颈。
2.2 轻量级锁:
轻量级锁是JVM提供的一种优化策略,它在没有竞争的情况下,通过自旋锁或CAS等机制避免了线程的阻塞。只有在竞争发生时,他才会将锁升级为重量级锁。
实现机制:
- 自旋锁:通过循环(自旋)不断尝试获取锁,避免线程挂起。
- CAS操作:轻量级锁通过原子操作判断锁状态,如果锁未被占用,则获取锁。、
轻量级锁的开销较小,特别是在锁竞争较少时,它能有效提升性能~
代码案例(自旋锁):
import java.util.concurrent.atomic.AtomicBoolean;
public class tty {
private static final AtomicBoolean lock = new AtomicBoolean(false);
public static void acquireLock() {
while(!lock.compareAndSet(false,true)){
//自旋等待
}
}
public static void releaseLock() {
lock.set(false);
}
/**
* 主方法,创建并启动两个线程来执行任务
* @param args 命令行参数,本程序中未使用
*/
public static void main(String[] args) {
// 使用Lambda表达式创建一个Runnable任务
Runnable task = () -> {
// 获取锁
acquireLock();
// 打印当前线程名和获取锁的信息
System.out.println(Thread.currentThread().getName()+" acquired the lock");
// 释放锁
releaseLock();
};
// 创建第一个线程,执行上述任务
Thread t1 = new Thread(task);
// 创建第二个线程,执行相同的任务
Thread t2 = new Thread(task);
// 启动第一个线程
t1.start();
// 启动第二个线程
t2.start();
}
}
1. 核心组件:AtomicBoolean
private static final AtomicBoolean lock = new AtomicBoolean(false);
- 含义:这里声明了一个静态的、不可变的
AtomicBoolean对象,命名为lock,初始值为false。 - 作用:
AtomicBoolean是一个提供原子操作的布尔值。在多线程环境下,普通的boolean变量在读写时可能会出现线程安全问题(如内存可见性问题),而AtomicBoolean内部使用了 CAS (Compare-And-Swap) 算法,保证了操作的原子性和可见性。 - 状态定义:
false:表示锁处于空闲状态(没有线程持有)。true:表示锁处于占用状态(有一个线程正在持有)。
2. 获取锁:acquireLock()
public static void acquireLock() {
while(!lock.compareAndSet(false, true)){
//自旋等待
}
}
这是自旋锁的核心逻辑。
-
compareAndSet(false, true)方法:- 这是 CAS 操作的体现。它的含义是:"原子性地检查当前的值是不是
false,如果是,就把它设置为true,并返回true;如果当前值不是false(即已经被别的线程改成了true),就什么都不做,并返回false。" - 原子性保证了这一步是不可打断的,不会出现两个线程同时看到
false并同时将其改为true的情况。
- 这是 CAS 操作的体现。它的含义是:"原子性地检查当前的值是不是
-
while循环(自旋):- 如果
compareAndSet返回true,说明抢锁成功,!true为false,循环结束,线程继续往下执行。 - 如果
compareAndSet返回false,说明锁已经被其他线程占用了(当前值是true)。此时!false为true,线程进入while循环体。 - 循环体是空的(或者只有注释),线程会在这里死循环,不断地再次尝试
compareAndSet。这就是所谓的**"自旋"**(Spinning)。线程不会进入阻塞状态(不会让出 CPU),而是一直占用 CPU 等待锁释放。
- 如果
3. 释放锁:releaseLock()
public static void releaseLock() {
lock.set(false);
- 含义:当线程完成临界区代码(需要互斥执行的代码)后,调用此方法释放锁。
lock.set(false):将AtomicBoolean的值重新设置为false。- 效果:这一步利用了
AtomicBoolean的volatile语义(set方法具有写内存屏障),保证这个修改对其他线程立即可见。其他正在while循环中自旋的线程会立刻发现lock变成了false,于是它们中的一个会通过 CAS 成功抢到锁。
4. 测试代码:main()
public static void main(String[] args) {
Runnable task = () -> {
acquireLock();
System.out.println(Thread.currentThread().getName()+" acquired the lock");
releaseLock();
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
}
- 任务定义:定义了一个 Lambda 表达式
task,它的逻辑是:先获取锁 -> 打印当前线程名 -> 释放锁。 - 线程启动:创建了两个线程
t1和t2,几乎同时启动,去执行同一个任务。 - 执行流程推演:
t1和t2同时运行。- 假设
t1稍微快一点,执行acquireLock()。此时lock为false,t1的 CAS 操作成功,lock变为true,t1进入打印语句。 t2紧接着执行acquireLock()。此时lock已经是true了,t2的 CAS 操作失败,t2进入while死循环(自旋),等待t1释放锁。t1打印完语句,执行releaseLock(),将lock设为false。t2在下一次循环中检测到lock为false,CAS 成功,抢到锁,跳出循环,执行打印语句。
总结与优缺点
功能总结:
这段代码实现了一个非阻塞的互斥锁。它确保了同一时刻只有一个线程能进入临界区(即 System.out.println 那一行),保证了线程安全。
优点:
- 无阻塞:线程没有挂起(没有调用
wait或LockSupport.park),避免了操作系统内核态和用户态切换的开销。 - 响应快:如果锁被持有的时间非常短,自旋等待能立刻获取锁,执行效率高。
缺点(重要):
- CPU 浪费:如果锁被持有的时间较长,或者竞争非常激烈,其他线程就会一直占用 CPU 进行空转(自旋),导致 CPU 负载飙升,极其浪费资源。
- 不可重入:如果一个线程已经持有锁,再次调用
acquireLock(),会因为lock已经是true而进入死循环,导致自己把自己锁死(死锁)。
适用场景:
锁持有的时间非常短,且并发竞争不激烈的场景。在实际开发中(如 Java JDK),通常会在自旋锁的基础上增加自适应策略(比如自旋一定次数后放弃),或者直接使用更高级的 ReentrantLock 或 synchronized,它们在内部已经优化了自旋与阻塞的切换逻辑。
理解自旋锁vs挂起等待锁
想象下,去追求⼀个女神.当男升向女神表白后,女神说:你是个好人,但是我有男朋友了~~
挂起等待锁:陷入沉不能自拔....过了很久很久之后,突然女神发来消息,"咱俩要不试试?"(注意,这 个很长的时间间隔里,女神可能已经换了好几个男票了).
自旋锁:死皮赖脸坚韧不拔.仍然每天持续的和女神说早安晚安.一旦女神和上一任分手,那么就能立刻 抓住机会上位.
3. 自旋锁
3.1 原理
自旋锁通过让线程忙碌等待的方式避免了线程挂起。当线程发现锁被占用时,它会反复检查锁的状态,直到成功获得锁。
3.2 使用场景
- 锁竞争不激烈,并且锁占用时间较短的场景。
- 自旋锁的典型应用是在CPU密集型的任务中,避免了阻塞带来的上下文切换。
3.3 优缺点
- 优点:避免了上下文切换的开销,在短时间内能够提升性能。
- 缺点:如果锁被长时间占用,会造成CPU资源的浪费。
4. 公平锁和非公平锁
顾名思义,公平锁和非公平锁就是看是否公平。
举个例子:假如说三个线程ABC,A现获取锁(成功),然后B获取锁(失败,阻塞等待)再然后C也尝试获取锁(失败,阻塞等待)。但是如果此时线程A释放锁了会怎么样呢?
- 公平锁情况下,遵守‘先来后到’,B比C先尝试获取,当A释放锁之后,B顺理成章拿到锁,等待B释放后C才能拿到锁。
- 非公平锁情况下,不遵守‘先来后到’,B和C都有可能获得锁。
这就好比一群男生追同一个女神.当女神和前任分手之后,先来追女神的男生上位,这就是公平锁;如果 是女神不按先后顺序挑一个自己看的顺眼的,就是非公平锁.
4.1 公平锁
公平锁保证请求锁的线程按顺序获取锁,避免线程饥饿问题
代码实现:
import java.util.concurrent.locks.ReentrantLock;
public class f1 {
private static final ReentrantLock lock = new ReentrantLock(true); // 公平锁
/**
* 主方法,创建两个线程并启动它们,每个线程都会尝试获取锁
* @param args 命令行参数,本程序中未使用
*/
public static void main(String[] args) {
// 创建一个Lambda表达式实现的Runnable任务,该任务会尝试获取锁
Runnable task = () -> {
// 加锁操作
lock.lock();
try {
// 打印当前线程名称和获取锁的信息
System.out.println(Thread.currentThread().getName() + " acquired the lock");
} finally {
// 确保锁被释放,无论是否发生异常
lock.unlock();
}
};
// 创建第一个线程,执行上述任务
Thread t1 = new Thread(task);
// 创建第二个线程,执行相同的任务
Thread t2 = new Thread(task);
// 启动第一个线程
t1.start();
// 启动第二个线程
t2.start();
}
}
1. 核心组件:ReentrantLock 与公平锁
private static final ReentrantLock lock = new ReentrantLock(true); // 公平锁
-
ReentrantLock:
- 位于
java.util.concurrent.locks包下,是 JDK 提供的基于 AQS (AbstractQueuedSynchronizer) 实现的锁。 - 它比
synchronized关键字更灵活,提供了如可中断锁、尝试非阻塞获取锁、公平锁等高级功能。
- 位于
-
new ReentrantLock(true):- 构造函数中的参数
true表示创建一个公平锁。 - 公平锁的含义:锁的分配是严格按照线程请求锁的时间顺序(先进先出,FIFO)来决定的。如果有多个线程在等待锁,排队时间最长的线程将优先获得锁。
- 对比:如果是
new ReentrantLock(false)或者无参构造,则创建的是非公平锁。非公平锁允许“插队”,性能通常比公平锁高,但可能导致某些线程长时间等待(“饥饿”现象)。
- 构造函数中的参数
2. 标准加锁范式:lock() 与 try-finally
Runnable task = () -> {
// 加锁操作
lock.lock();
try {
// 临界区代码
System.out.println(Thread.currentThread().getName() + " acquired the lock");
} finally {
// 确保锁被释放
lock.unlock();
}
};
这是使用 ReentrantLock 最重要的一点,也是与 synchronized 最大的区别之一。
-
lock.lock():- 线程主动获取锁。如果锁已经被其他线程持有,当前线程会进入休眠(阻塞)状态,直到锁被获取为止。
- 注意:与
tryLock()不同,lock()不会响应中断(除非使用线程中断机制配合),且如果不小心导致unlock()没执行,就会死锁。
-
try { ... } finally { ... }结构:- 为什么必须这样写?
ReentrantLock不会像synchronized那样在代码块结束时自动释放锁。必须手动调用unlock()。 - 风险控制:将
unlock()放在finally块中,是为了保证无论临界区代码是否发生异常(抛出 Error 或 Exception),锁都一定会被释放。如果忘记放在finally中,一旦业务代码报错跳出,锁就会永远被占用,导致系统死锁。
- 为什么必须这样写?
3. 线程执行逻辑
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
- 创建了两个线程
t1和t2,它们共享同一个lock实例。 - 两个线程几乎同时启动,竞争同一个锁。
- 由于这是一个公平锁,如果 t1 先启动并尝试获取锁(或者先进入等待队列),t2 将乖乖排队,直到 t1 执行完毕并释放锁,t2 才能获取锁。
4. 与上一段自旋锁代码的对比
为了让你更好地理解,我们将这段代码与之前的 AtomicBoolean 自旋锁做一个对比:
| 特性 | 上一段代码 (AtomicBoolean 自旋锁) | 这段代码 |
|---|---|---|
| 实现原理 | 基于 CAS 算法,通过 while 循环不断检测状态 |
基于 AQS 队列,线程获取锁失败后会阻塞(挂起) |
| CPU 消耗 | 高。线程在等待时会一直占用 CPU 进行空转。 | 低。线程等待时会释放 CPU 资源,由操作系统调度唤醒。 |
| 响应速度 | 如果锁持有时间极短,响应很快(无需上下文切换)。 | 如果锁持有时间短,可能因为上下文切换而有轻微延迟。 |
| 功能特性 | 仅提供基本的互斥访问。 | 提供公平性选择、可重入、条件变量等丰富功能。 |
| 代码安全性 | 简单场景下可用,复杂场景容易出错(如死循环)。 | 标准的 try-finally 范式,异常安全性高。 |
总结
这段代码演示了 Java 并发编程中显式锁的正确使用方法。
- 功能:它创建了一个公平的互斥锁,确保两个线程串行执行打印操作,不会发生交错。
- 关键点:
- 使用了
ReentrantLock(true)启用公平机制,保证先来后到。 - 使用了
try-finally块,这是防止死锁、保证资源释放的黄金法则。
- 使用了
- 适用场景:适用于需要高并发控制、需要公平锁机制、或者需要利用
tryLock等高级特性的复杂业务场景。在简单的同步需求下,synchronized可能更简洁,但在需要精细化控制锁时,ReentrantLock是首选。
4.2 非公平锁:
非公平锁不保证按照请求顺序获得锁,可能会导致部分线程长时间等待
代码示例:
package fff;
import java.util.concurrent.locks.ReentrantLock;
public class f2 {
private static final ReentrantLock lock = new ReentrantLock(); // 非公平锁
/**
* 主方法,演示多线程环境下的锁使用
* 创建了两个线程,它们都会尝试获取同一个锁
*/
public static void main(String[] args) {
// 使用Lambda表达式创建一个Runnable任务
Runnable task = () -> {
// 获取锁
lock.lock();
try {
// 打印当前线程名称和获取锁的信息
System.out.println(Thread.currentThread().getName() + " acquired the lock");
} finally {
// 确保锁一定会被释放,避免死锁
lock.unlock();
}
};
// 创建两个线程,使用相同的任务
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
// 启动两个线程
t1.start();
t2.start();
}
}
这段代码与上一段(f1 类)非常相似,它们都使用了 ReentrantLock 来保证线程安全,并且都遵循了标准的 lock() / try...finally / unlock() 编程范式。
唯一的关键区别在于锁的“公平性”策略。
下面我将重点讲解这个区别,以及它对程序运行的影响。
1. 核心区别:非公平锁
private static final ReentrantLock lock = new ReentrantLock(); // 非公平锁
- 构造方法:这里调用的是无参构造
new ReentrantLock()。在 Java 源码中,这等同于new ReentrantLock(false)。 - 非公平锁的含义:
- 不保证先来后到:当锁被释放时,正在等待的线程和新来的线程都有机会获取锁。
- 允许“插队”:如果有一个新线程在锁刚好释放的瞬间请求锁,它可以直接抢到锁,而不管队列中是否有等待了更久的线程。
- 默认行为:
ReentrantLock和synchronized关键字默认都是非公平的。
2. 公平锁 vs 非公平锁:深入对比
为了更好地理解,我们可以对比一下 f1(公平锁)和 f2(非公平锁)的行为:
| 特性 | f1: 公平锁 (new ReentrantLock(true)) |
f2: 非公平锁 (new ReentrantLock()) |
|---|---|---|
| 获取锁策略 | 严格遵循 FIFO(先进先出)队列。 | 允许“插队”。 |
| 线程饥饿 | 不会发生。所有等待的线程最终都能拿到锁。 | 可能发生。在高并发下,如果不断有新线程插队,排在队列后面的线程可能长时间拿不到锁。 |
| 吞吐量 | 较低。除了线程挂起/唤醒的开销,还维护了严格的队列顺序,增加了系统开销。 | 较高。减少了线程挂起的几率,不需要频繁切换线程状态,CPU 利用率更高。 |
| 适用场景 | 严格要求按顺序执行任务,或者为了防止某个线程长期等待而“饿死”。 | 绝大多数通用场景。追求更高的执行效率,对执行顺序要求不严。 |
3. 代码执行流程推演
让我们看看在这段 f2 代码中,非公平性是如何体现的:
- 启动阶段:
t1和t2启动。 - 竞争阶段:
- 假设
t1抢到了锁,开始执行System.out.println。 t2尝试获取锁失败,进入阻塞队列(Waiting 队列)等待。
- 假设
- 释放阶段:
t1执行完毕,调用unlock()释放锁。- 关键点来了:此时,
t2还在被操作系统唤醒的过程中(从阻塞态转为就绪态需要时间)。 - 如果此时恰好有一个新线程
t3(假设有的话)发起了lock()请求,非公平锁策略允许t3直接检查锁的状态,发现空闲,直接抢走锁! - 等到
t2终于被唤醒准备去拿锁时,发现锁又被t3拿走了,t2只能无奈地继续回去排队等待。
4. 为什么默认是非公平锁?
你可能会问,非公平锁会导致线程“饿死”,为什么 Java 还把它设为默认?
- 性能考量:恢复一个被挂起的线程(操作系统上下文切换)是非常昂贵的操作。
- 实际情况:在极短的时间内,让刚释放锁的线程或者新来的线程直接再次获取锁(重入或插队),比先去唤醒队列头部的线程要快得多。
- 结论:为了追求更高的系统吞吐量,牺牲了绝对的公平性。在实际应用中,线程“饿死”的概率相对较低,而性能提升带来的收益是巨大的。
总结
这段代码演示了 ReentrantLock 的默认用法(非公平锁)。
- 代码结构:与
f1完全一致,是标准的并发安全写法。 - 核心区别:去掉了构造函数中的
true参数,使用了默认的非公平策略。 - 实际影响:
- 程序运行速度通常会更快。
- 线程获取锁的顺序是不确定的,不保证先启动的线程先拿到锁。
- 在只有两个线程(
t1,t2)的简单示例中,你可能看不出区别;但在高并发场景下,这种策略能显著提升系统性能
加餐
ReentrantLock 的构造函数括号中的参数(boolean fair)决定了它是公平锁还是非公平锁。
具体规则如下:
-
new ReentrantLock(true):- 参数为
true。 - 创建的是公平锁。
- 线程获取锁的顺序严格按照请求锁的时间顺序(先来后到)。
- 参数为
-
new ReentrantLock()或new ReentrantLock(false):- 不传参数,或者参数为
false。 - 创建的是非公平锁。
- 线程获取锁的顺序是不确定的,允许“插队”。
- 不传参数,或者参数为
源码层面的解释
如果你查看 ReentrantLock 的 Java 源码,会看到如下两个构造函数:
// 1. 无参构造函数
public ReentrantLock() {
// 它内部调用了带参构造,并且默认传入了 false
sync = new NonfairSync();
}
// 2. 带参构造函数
public ReentrantLock(boolean fair) {
// 如果 fair 为 true,创建公平锁内部类对象
// 如果 fair 为 false,创建非公平锁内部类对象
sync = fair ? new FairSync() : new NonfairSync();
}
总结:
- 写
new ReentrantLock()等同于new ReentrantLock(false),这就是非公平锁。 - 只有显式地写成
new ReentrantLock(true),才是公平锁。
以上就是本篇的全部内容啦~~咱们下篇再见~~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)