1.简介
Java 中的锁是用于控制多线程对共享资源访问的一种机制,以防止数据的不一致性和脏读。Java 提供了多种锁机制,包括内置的同步机制(synchronized)和在 java.util.concurrent.locks
包中提供的显式锁(如 ReentrantLock
)等。
- Synchronized
synchronized
关键字是 Java 中最基本的同步机制。它可以用于修饰方法或代码块,保证同一时间只有一个线程可以执行该方法或代码块内的代码。
- 方法同步:将
synchronized
关键字加在方法上,锁住的是调用该方法的对象。 - 代码块同步:通过
synchronized
关键字和一个锁对象来同步代码块,锁住的是给定的对象。
- ReentrantLock
ReentrantLock
是java.util.concurrent.locks
包中的一个类,提供了比synchronized
更灵活的锁定机制。它允许同一个线程多次获得锁,并且支持公平锁和非公平锁。
- 公平锁:按照线程请求锁的顺序来获取锁。
- 非公平锁:不按照请求锁的顺序来分配锁,这可能导致某些线程永远获取不到锁。
使用 ReentrantLock
时,需要显式地锁定和解锁:
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
// 访问或修改共享资源
} finally {
lock.unlock(); // 释放锁
}
- ReadWriteLock
ReadWriteLock
是一个读写锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这样可以提高并发性能,特别是在读操作远远超过写操作的场景中。
ReadWriteLock
有两个锁:一个读锁 (readLock()
) 和一个写锁 (writeLock()
)。
-
Condition
Condition
是与ReentrantLock
配合使用的,可以实现线程间的协调通信,比如实现生产者-消费者模式。Condition
提供了类似Object
的wait
、notify
和notifyAll
方法。 -
StampedLock
StampedLock
是 Java 8 引入的,提供了一种乐观的读锁机制,适用于读多写少的并发场景。它也支持读锁和写锁,以及一种称为“乐观读”的锁模式。
总的来说,Java 中的锁机制可以根据不同的应用场景和需求来选择,以实现高效且安全的并发访问控制。
2.各种锁使用详情
2.1.Synchronized锁
Synchronized
关键字在 Java 中是实现同步的一种基本方式,用于防止多个线程同时访问某个区域的代码。它可以应用于方法和代码块上,根据应用的位置,这两种方式有着不同的特点和用途。
2.1.1.Synchronized 方法锁
当你在方法声明上使用 synchronized
关键字时,这个方法称为同步方法。对于实例方法,锁定的是调用该方法的对象实例(this);对于静态同步方法,锁定的是这个方法所在的Class对象本身,因为静态方法是属于类的,不属于任何对象实例。
优点:
- 简单易用,只需在方法前加上
synchronized
关键字。 - 自动锁管理:进入同步方法时自动加锁,退出方法时自动解锁,即使方法中途出现异常。
缺点:
- 粒度较粗:当一个线程访问同步方法时,其他线程对同一对象中所有其他同步方法的访问将被阻塞,可能会导致不必要的等待。
- 灵活性较低:无法对读操作和写操作采用不同的锁策略,比如读写锁。
代码示例
class Counter {
private int count = 0;
// 使用synchronized关键字修饰方法
public synchronized void increment() {
count++; // 增加计数
}
public synchronized int getCount() {
return count; // 返回当前计数
}
}
在这个例子中,increment
方法和getCount
方法都被synchronized
关键字修饰。这意味着在同一时刻,只能有一个线程执行这些方法中的任何一个。如果有多个线程尝试同时访问这些方法,它们将会排队,直到前一个线程完成执行。
2.1.2.Synchronized 块锁
synchronized
也可以用来同步代码块而非整个方法。这时,你需要指定一个锁对象。对于代码块同步,可以选择任何对象作为锁对象(监视器对象),根据这个锁对象的不同,同步代码块的作用范围也会有所不同。
优点:
- 粒度更细:可以减小锁的范围,只在需要同步的代码区域上加锁,提高了效率。
- 灵活性高:可以根据需要对不同的代码块使用不同的锁对象,更加灵活地控制同步的范围和粒度。
缺点:
- 使用复杂度较高:需要手动指定锁对象,且必须确保正确管理锁对象,以避免死锁等问题。
- 管理成本高:相比于同步方法,需要更仔细地考虑锁的选择和同步块的范围。
代码示例
class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized(lock) { // 在特定对象上加锁
count++; // 这部分代码是同步的
}
// lock释放后,其他线程可以进入synchronized块
}
public int getCount() {
synchronized(lock) {
return count;
}
}
}
在这个例子中,我们只对修改count
变量的部分代码进行同步。这是通过在一个特定的对象(在这里是lock
对象)上使用synchronized
代码块来实现的。这种方式提供了更细粒度的锁控制,允许在同一个方法中的其他部分继续并发执行。
2.1.3.总结
- 使用
synchronized
方法时,是在方法级别上加锁,适用于简单的同步需求,但可能导致较大范围的性能影响。 - 使用
synchronized
块时,可以更精确地控制同步的范围和锁对象,提高程序的并发性能和灵活性,但需要更谨慎地管理锁对象和同步块的范围。
选择哪种方式取决于具体的需求和场景。如果同步操作仅涉及方法内部的少数几行代码,推荐使用 synchronized
块;如果整个方法都需要同步,使用 synchronized
方法更为简便。
2.2.ReentrantLock锁
ReentrantLock
是 Java 中 java.util.concurrent.locks
包提供的一种高级同步机制,用于替代传统的同步方法和同步块(synchronized
)。ReentrantLock
提供了更复杂的锁操作,允许更灵活的线程互斥处理和更细粒度的锁控制。如其名所示,“Reentrant” 意味着这种锁具有可重入的特性,即同一个线程可以多次获得同一个锁而不会导致死锁。
2.2.1.特性
- 可重入性:线程可以重复获取已经持有的锁,这避免了死锁的发生。
- 中断响应:
ReentrantLock
提供了一种能力,允许在等待锁的过程中中断线程。 - 尝试锁定:提供了一个尝试获取锁的方法,该方法无论锁是否可用,都会立即返回获取结果。
- 公平锁选项:可以设置为公平锁,确保等待时间最长的线程首先获得锁。
- 锁绑定多个条件:一个
ReentrantLock
可以同时绑定多个Condition
实例。
2.2.2.使用方法
使用 ReentrantLock
需要显式地创建一个 ReentrantLock
实例,然后在代码中显式地锁定和解锁。
ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock(); // 获取锁
try {
// 执行临界区代码
} finally {
lock.unlock(); // 释放锁
}
}
2.2.3.比较 synchronized
和 ReentrantLock
- 灵活性:
ReentrantLock
提供了比synchronized
更多的功能和灵活性,例如尝试锁定、公平锁设置、以及处理中断等待的能力。 - 性能:在 Java 5 以后,
synchronized
的性能得到了显著提升,使得两者的性能差异不再那么明显。性能差异主要取决于具体的JVM实现和应用场景。 - 易用性:
synchronized
更易用,因为它不需要显式地获取和释放锁,而是自动管理。ReentrantLock
需要开发者手动管理锁的获取和释放,增加了编程复杂度。 - 功能丰富:
ReentrantLock
提供了一些synchronized
无法提供的高级功能,如条件变量(Condition
)、可中断的锁获取等。
2.2.4.选择指南
- 当需要使用高级锁功能,如尝试锁定、定时锁定、公平锁、或者多个条件变量等时,应该使用
ReentrantLock
。 - 对于大多数基本的同步需求,
synchronized
是足够的,它简单而且自动管理锁的获取和释放。
ReentrantLock
是一个强大的同步工具,但它的使用应该根据具体场景和需求来决定,考虑到额外的灵活性是否真正需要,以及是否值得为此增加代码的复杂性。
2.2.5.特性代码
2.2.5.1.公平锁示例
公平锁是指线程获取锁的顺序与线程请求锁的顺序相同。
import java.util.concurrent.locks.ReentrantLock;
public class FairLockExample {
private final ReentrantLock lock = new ReentrantLock(true); // 创建一个公平锁
public void printJob(Object document) {
lock.lock();
try {
// 模拟文档打印
System.out.println(Thread.currentThread().getName() + ": Printing a document");
// 模拟打印时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void runDemo() {
FairLockExample printer = new FairLockExample();
Thread t1 = new Thread(() -> printer.printJob(new Object()), "Thread 1");
Thread t2 = new Thread(() -> printer.printJob(new Object()), "Thread 2");
Thread t3 = new Thread(() -> printer.printJob(new Object()), "Thread 3");
t1.start();
t2.start();
t3.start();
}
}
2.2.5.2.尝试锁定(tryLock)示例
tryLock
方法尝试获取锁,如果锁立即可用,则获取锁并返回 true
;如果锁不可用,则返回 false
,这允许程序在无法获取锁时不会无限期地等待。
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TryLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void attemptLock() {
try {
// 尝试在1秒内获取锁
boolean lockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
if (lockAcquired) {
try {
System.out.println(Thread.currentThread().getName() + ": Lock acquired, performing job.");
// 模拟任务执行时间
Thread.sleep(2000);
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + ": Unable to acquire lock, doing something else.");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void runDemo(){
TryLockExample example = new TryLockExample();
Thread t1 = new Thread(example::attemptLock, "Thread 1");
Thread t2 = new Thread(example::attemptLock, "Thread 2");
t1.start();
t2.start();
}
}
Thread 1: Lock acquired, performing job.
Thread 2: Unable to acquire lock, doing something else.
2.2.5.3.结合条件变量(Condition)示例
条件变量允许一个或多个线程等待某些特定条件的发生,并允许其他线程在条件发生时通知它们。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ": Condition wait");
condition.await(); // 等待
System.out.println(Thread.currentThread().getName() + ": Condition satisfied");
} finally {
lock.unlock();
}
}
public void conditionSignal() throws InterruptedException {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ": Signal condition");
condition.signal(); // 通知等待的线程
} finally {
lock.unlock();
}
}
public static void runDemo() throws InterruptedException {
ConditionExample example = new ConditionExample();
Thread waiter = new Thread(() -> {
try {
example.conditionWait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Waiter Thread");
Thread signaler = new Thread(() -> {
try {
// 确保waiter先执行等待
Thread.sleep(2000);
example.conditionSignal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Signaler Thread");
waiter.start();
signaler.start();
}
}
Waiter Thread: Condition wait
Signaler Thread: Signal condition
Waiter Thread: Condition satisfied
2.2.5.4.公平锁复杂案例
公平锁选项:可以设置为公平锁,确保等待时间最长的线程首先获得锁。和 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 实例。
代码展示了如何在 ReentrantLock
中使用公平锁选项以及如何绑定多个 Condition
实例来实现一个简单的场景:一个公平的生产者-消费者模型,其中生产者和消费者线程根据等待时间的长短公平地获取锁,同时利用两个条件实例分别控制生产和消费的逻辑。
package LockCode.ReentrantLocCode;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class FairProducerConsumerExample {
private final Queue<Integer> queue = new LinkedList<>();
private final int MAX_SIZE = 5;
private final ReentrantLock lock = new ReentrantLock(true); // 使用公平锁
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
// 生产者方法
public void produce(int item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == MAX_SIZE) {
System.out.println(Thread.currentThread().getName() + " waiting, queue full");
notFull.await(); // 等待队列非满
}
queue.add(item);
System.out.println(Thread.currentThread().getName() + " produced " + item);
notEmpty.signalAll(); // 通知消费者队列非空
} finally {
lock.unlock();
}
}
// 消费者方法
public void consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
System.out.println(Thread.currentThread().getName() + " waiting, queue empty");
notEmpty.await(); // 等待队列非空
}
int item = queue.poll();
System.out.println(Thread.currentThread().getName() + " consumed " + item);
notFull.signalAll(); // 通知生产者队列非满
} finally {
lock.unlock();
}
}
public static void runDemo() {
FairProducerConsumerExample example = new FairProducerConsumerExample();
// 创建生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
example.produce(i);
Thread.sleep(100); // 模拟生产时间
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer");
// 创建消费者线程
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
example.consume();
Thread.sleep(100); // 模拟消费时间
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Consumer");
producer.start();
consumer.start();
}
}
2.2.5.4.1.代码解释
- 公平锁:在创建
ReentrantLock
实例时,通过传递true
参数来启用公平锁。这确保了等待锁最长时间的线程将获得锁的优先权。 - 多个条件变量:使用两个
Condition
实例notEmpty
和notFull
来分别控制队列非空和非满的条件,从而实现生产者和消费者之间的协调。 - 生产者和消费者逻辑:生产者在队列满时等待,消费者在队列空时等待。当生产者生产一个元素后,它会通过
notEmpty.signalAll()
唤醒等待的消费者;同样,消费者消费一个元素后,通过notFull.signalAll()
唤醒等待的生产者。 - 公平的等待和唤醒:由于使用了公平锁,所以即使在多线程竞争激烈的情况下,所有线程也将有机会按照请求锁的顺序获得锁。
2.2.5.4.2.好处
这种方式的好处是显而易见的:
- 公平性:确保长时间等待的线程不会被新到达的线程饿死。
- 效率:通过使用条件变量避免
2.3.公平锁和非公平锁
在并发编程中,锁(Lock)的公平性是指当多个线程竞争锁时,锁的分配机制是否考虑了线程等待的顺序。Java ReentrantLock
类在创建时可以指定锁是公平的(Fair)还是非公平的(Nonfair)。这个选择决定了等待相同锁的线程获得锁的顺序。
2.3.1.公平锁(Fair Lock)
公平锁意味着在分配锁时,将考虑线程等待的顺序。具体来说,如果锁是公平的,那么在多个线程竞争锁时,等待时间最长的线程将获得锁。这种方式保证了所有线程都将获得执行的机会,避免了“饿死”(某些线程永远获取不到锁)的问题。
2.3.1.1.优点:
- 公平性:所有线程都有机会按顺序获得锁,确保了长时间等待的线程不会被忽视。
- 避免饥饿:所有试图获取锁的线程最终都会按照请求的顺序获得锁,没有线程会被无限期地阻塞。
2.3.1.2.缺点:
- 性能:公平锁通常会导致更大的延迟和更低的吞吐量。每次锁释放时,都需要在等待的线程中找到等待时间最长的线程,这可能会增加管理锁的开销。
2.3.2.非公平锁(Nonfair Lock)
非公平锁在分配锁时不考虑线程等待的顺序。如果锁是非公平的,那么当锁变为可用时,任何请求它的线程都有机会获得锁,而不管其等待时间如何。这可能意味着某些线程可以立即重新获取锁,而其他已经等待较长时间的线程则继续等待。
2.3.2.1.优点:
- 性能:非公平锁通常提供更好的性能。它减少了锁分配的管理开销,因为不需要检查等待队列中的线程顺序,只需判断锁是否可用即可尝试获取。
- 吞吐量:在高竞争的环境下,非公平锁可能允许更多的线程在较短时间内完成工作,从而提高吞吐量。
2.3.2.2.缺点:
- 饥饿:在极端情况下,如果锁的请求非常高,一些线程可能会遭遇饥饿,即它们可能需要非常长的时间才能获得锁,甚至可能永远得不到锁。
- 不可预测性:非公平锁的行为更不可预测,难以确定哪个线程将获得锁。
2.3.3.总结
- 选择公平锁还是非公平锁取决于具体的应用场景和性能要求。如果对响应时间的公平性有严格要求,或者想避免线程饥饿问题,可以考虑使用公平锁。然而,如果性能和吞吐量是主要关注点,非公平锁可能是更好的选择。
- 默认行为:值得注意的是,
ReentrantLock
的默认行为是非公平的。如果需要公平性,必须在创建锁时通过传递true
给构造函数来明确请求公平锁。
2.3.4.代码样例说明
package LockCode.ReentrantLocCode;
import java.util.concurrent.locks.ReentrantLock;
public class FairAndNonfairLocks {
public static void runDemo() throws InterruptedException {
testLock("Fair Lock", true);
Thread.sleep(1000); // 间隔1秒,区分公平锁和非公平锁的输出
System.out.println("------------------------------------");
testLock("Nonfair Lock", false);
}
private static void testLock(String testName, boolean isFair) {
ReentrantLock lock = new ReentrantLock(isFair);
Runnable task = () -> {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " attempting to acquire " + testName);
lock.lock();
try {
System.out.println(threadName + " acquired " + testName);
// 模拟任务执行时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
System.out.println(threadName + " released " + testName);
}
};
// 创建并启动5个线程
for (int i = 1; i <= 5; i++) {
Thread thread = new Thread(task, "Thread " + i);
thread.start();
}
}
}
Thread 1 attempting to acquire Fair Lock
Thread 2 attempting to acquire Fair Lock
Thread 1 acquired Fair Lock
Thread 3 attempting to acquire Fair Lock
Thread 4 attempting to acquire Fair Lock
Thread 5 attempting to acquire Fair Lock
Thread 1 released Fair Lock
Thread 2 acquired Fair Lock
Thread 2 released Fair Lock
Thread 3 acquired Fair Lock
Thread 4 acquired Fair Lock
Thread 3 released Fair Lock
Thread 4 released Fair Lock
Thread 5 acquired Fair Lock
Thread 5 released Fair Lock
------------------------------------
Thread 1 attempting to acquire Nonfair Lock
Thread 1 acquired Nonfair Lock
Thread 4 attempting to acquire Nonfair Lock
Thread 5 attempting to acquire Nonfair Lock
Thread 2 attempting to acquire Nonfair Lock
Thread 3 attempting to acquire Nonfair Lock
Thread 1 released Nonfair Lock
Thread 5 acquired Nonfair Lock
Thread 5 released Nonfair Lock
Thread 4 acquired Nonfair Lock
Thread 4 released Nonfair Lock
Thread 2 acquired Nonfair Lock
Thread 2 released Nonfair Lock
Thread 3 acquired Nonfair Lock
Thread 3 released Nonfair Lock
2.3.4.1.代码解释
- 这个程序定义了一个名为
testLock
的方法,它接收一个锁的名称和一个布尔值来决定使用公平锁还是非公平锁。 ReentrantLock
对象根据isFair
参数创建,true
代表公平锁,false
代表非公平锁。- 在
testLock
方法中,创建了一个任务,该任务尝试获取锁,执行一段时间(模拟通过Thread.sleep(100)
),然后释放锁。 - 对于每种锁类型,我们启动5个线程来执行这个任务,通过观察输出可以看到线程获取和释放锁的顺序。
2.3.4.2.预期观察
- 公平锁(Fair Lock):线程将按照请求锁的顺序获得锁。你应该能看到“Thread 1”、“Thread 2”依次获得锁,显示锁分配的顺序性和公平性。
- 非公平锁(Nonfair Lock):线程获取锁的顺序可能与它们请求锁的顺序不同。有可能看到后启动的线程比先启动的线程更早获取锁,显示锁分配的非顺序性和非公平性。
2.3.4.3.注意事项
- 实际的运行结果可能因JVM调度、线程启动时间差异等因素而有所不同,特别是在非公平锁的情况下。
- 公平锁通常会有更高的开销,因为维护线程顺序需要更多的管理和调度工作。在实际应用中,选择使用公平锁还是非公平锁应根据具体需求和性能测试结果决定。
2.4.ReadWriteLock锁
ReadWriteLock
接口在 Java 中提供了一种高级的线程同步机制,允许多个读操作并发进行,而写操作则需要独占访问。这种锁是为了提高在读多写少的场景下的性能和并发性。ReadWriteLock
分为两部分:读锁(Read Lock)和写锁(Write Lock)。这里我们重点介绍读锁。
2.4.1.读锁(Read Lock)
读锁是一种共享锁,允许多个线程同时持有锁进行读操作。当一个线程持有读锁时,其他请求读锁的线程也可以获取读锁并执行读操作,但任何请求写锁的线程必须等待,直到所有读锁都释放。这反映了一个基本原则:只要没有线程在写入,多个线程可以同时读取而不会引起冲突。
2.4.2.特性
- 共享访问:多个线程可以同时持有读锁,实现高效的并发读取。
- 锁降级:持有写锁的线程可以获取读锁,然后释放写锁,这是一种锁的降级策略,用于保持数据的可见性。
- 公平性选择:
ReentrantReadWriteLock
的实现提供了公平和非公平的选择。公平模式下,线程将按照请求锁的顺序来获取锁,而非公平模式则允许插队,可能会提高性能但牺牲了公平性。
2.4.3.使用场景
读锁适用于数据读取远多于修改的场景,例如缓存系统。在这些场景中,读锁可以显著提高并发性能,因为它允许多个线程同时读取数据而不阻塞彼此。
2.4.4.示例代码
下面是一个使用 ReadWriteLock
中读锁的简单示例:
package LockCode.ReadLockCode;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadLockExample {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private int value;
// 使用读锁来保护读取操作
public int readValue() {
readWriteLock.readLock().lock(); // 获取读锁
try {
return value;
} finally {
readWriteLock.readLock().unlock(); // 释放读锁
}
}
// 使用写锁来保护写入操作
public void writeValue(int newValue) {
readWriteLock.writeLock().lock(); // 获取写锁
try {
this.value = newValue;
} finally {
readWriteLock.writeLock().unlock(); // 释放写锁
}
}
public static void runDemo() {
ReadLockExample example = new ReadLockExample();
// 创建读线程
Thread readThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Read value: " + example.readValue());
}
});
// 创建写线程
Thread writeThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
example.writeValue(i);
System.out.println("Write value: " + i);
}
});
readThread.start();
writeThread.start();
}
}
Read value: 0
Write value: 0
Read value: 0
Write value: 1
Write value: 2
Write value: 3
Write value: 4
Read value: 1
Read value: 4
Read value: 4
2.4.5.注意事项
- 尽管多个线程可以同时持有读锁,但如果有一个线程正在等待获取写锁,则其他线程尝试获取读锁可能会被阻塞。这是为了避免写锁饥饿,即确保请求写锁的线程最终能获取锁。
- 锁的升级(从读锁升级到写锁)是不被允许的,尝试这样做会导致死锁。
2.5.ReentrantReadWriteLock锁
下面的代码示例展示了ReentrantReadWriteLock
的四个关键特性:读读共享、写写互斥、读写互斥和锁降级。在这个示例中,我们模拟了一个共享资源的访问,以展示不同情况下锁的行为。
2.5.1.示例代码
package LockCode.ReadWriteLockCode;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockFeaturesDemo {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private int sharedResource = 0;
// 读操作,展示读读共享
public void readOperation() {
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " starts reading.");
Thread.sleep(1000); // 模拟读操作耗时
System.out.println(Thread.currentThread().getName() + " finished reading with value: " + sharedResource);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
// 写操作,展示写写互斥和读写互斥
public void writeOperation(int newValue) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " starts writing.");
Thread.sleep(1000); // 模拟写操作耗时
sharedResource = newValue;
System.out.println(Thread.currentThread().getName() + " finished writing.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
// 锁降级,从写锁降级到读锁
public void lockDowngrading(int newValue) {
rwLock.writeLock().lock();
try {
sharedResource = newValue;
System.out.println(Thread.currentThread().getName() + " updated value to: " + sharedResource);
// 在释放写锁之前获取读锁
rwLock.readLock().lock();
} finally {
rwLock.writeLock().unlock(); // 注意先释放写锁
}
try {
// 此时持有读锁,可以安全地读取值
System.out.println(Thread.currentThread().getName() + " reads value after downgrading: " + sharedResource);
} finally {
rwLock.readLock().unlock();
}
}
public static void runDemo() throws InterruptedException {
ReadWriteLockFeaturesDemo demo = new ReadWriteLockFeaturesDemo();
// 启动两个读线程,展示读读共享
new Thread(demo::readOperation, "Reader1").start();
new Thread(demo::readOperation, "Reader2").start();
Thread.sleep(100); // 确保读线程先启动
// 启动写线程,展示写写互斥和读写互斥
new Thread(() -> demo.writeOperation(100), "Writer").start();
Thread.sleep(3000); // 等待上述操作完成
// 展示锁降级
new Thread(() -> demo.lockDowngrading(200), "Downgrader").start();
}
}
Reader1 starts reading.
Reader2 starts reading.
Reader1 finished reading with value: 0
Reader2 finished reading with value: 0
Writer starts writing.
Writer finished writing.
Downgrader updated value to: 200
Downgrader reads value after downgrading: 200
2.5.2.特性解释
- 读读共享:
readOperation
方法通过读锁展示了如果一个线程正在进行读操作,其他线程也可以获得读锁进行读操作,体现了共享性。 - 写写互斥和读写互斥:
writeOperation
方法展示了写操作必须独占地进行,无论是另一个写操作还是读操作都不能同时进行。 - 锁降级:
lockDowngrading
方法展示了如何从写锁降级到读锁。线程首先获取写锁进行写操作,然后在不释放写锁的情况下获取读锁,接着释放写锁,最后释放读锁。这样做可以保持对变量的读取权限,即使写锁已经释放。
2.5.3.注意事项
- 锁的升级(从读锁升级到写锁)是不允许的,尝试这样做会导致死锁。
- 锁降级是一种高级特性,通常用
2.6.StampedLock锁
StampedLock
是 Java 8 引入的一种新的锁机制,位于 java.util.concurrent.locks
包中。与 ReentrantReadWriteLock
相比,StampedLock
提供了更高的并发性。它是一种锁的实现,支持读锁、写锁和乐观读。StampedLock
的主要特点是它的锁方法会返回一个表示锁状态的票据(stamp),这个票据在释放锁或检查锁是否有效时使用。
2.6.1.特性
- 读写锁:支持传统的读写锁模式。
- 乐观读:一种非阻塞的读锁,可以提高系统的吞吐量。乐观读锁在读取时不会阻塞写者,但需要通过票据验证读取的数据是否在读取过程中被修改。
- 不支持重入:与
ReentrantLock
和ReentrantReadWriteLock
不同,StampedLock
不支持锁的重入。 - 不支持条件变量:
StampedLock
不提供条件变量的支持,不能使用Condition
等待/通知模式。
2.6.2.使用场景
- 当数据的读取频率远高于修改时,可以通过乐观读来提高并发性。
- 在您可以容忍偶尔需要重新尝试读取的场景下使用乐观读,以换取整体吞吐量的提升。
- 对于更新操作较少,读取操作非常频繁的数据结构。
2.6.3.示例代码
下面的代码示例展示了如何使用 StampedLock
实现一个简单的线程安全的点(Point
)类,包括使用写锁进行数据修改和使用乐观读取来提高读取操作的并发性。
import java.util.concurrent.locks.StampedLock;
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
// 构造函数
public Point(double x, double y) {
this.x = x;
this.y = y;
}
// 使用写锁修改点的坐标
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp); // 释放写锁
}
}
// 使用乐观读锁读取点的坐标
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // 尝试获取乐观读锁
double currentX = x, currentY = y;
if (!sl.validate(stamp)) { // 检查在获取读锁后是否有其他写锁被获取
stamp = sl.readLock(); // 获取一个悲观读锁
try {
currentX = x; // 重新读取变量
currentY = y;
} finally {
sl.unlockRead(stamp); // 释放读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
public class StampedLockExample {
public static void runDemo() {
Point point = new Point(1, 1);
// 修改点的坐标
point.move(2, 2);
// 读取点的坐标
System.out.println(point.distanceFromOrigin());
}
}
2.6.4.注意事项
- 使用
StampedLock
时,特别需要注意乐观读锁的使用方法。乐观读可能需要在数据实际被读取后重新检查锁的有效性。 StampedLock
不支持条件变量和锁重入,这在某些情况下可能限制了它的使用场景。- 如果乐观读后发现锁已经不再有效,就需要通过获取一个悲观读锁来保证数据的一致性。
- 由于
StampedLock
不支持重入,不当的锁管理可能导致死
2.7.LockSupport锁
LockSupport
是 Java 并发工具包 (java.util.concurrent.locks
) 中的一个工具类,提供了基本的线程阻塞和唤醒功能。它是创建锁和其他同步类的基础。与 Object
类的 wait()
和 notify()
方法相比,LockSupport
提供的阻塞和唤醒操作更为灵活和简单,因为它不要求线程持有任何锁。
主要方法
LockSupport
主要提供了以下几个静态方法:
park()
: 用于阻塞当前线程。如果调用park()
时,某个许可(permit)已经可用(通过之前调用unpark(Thread thread)
),那么park()
会立即返回,并且消耗掉这个许可;否则,它会使当前线程进入阻塞状态。unpark(Thread thread)
: 用于唤醒一个被park()
阻塞的线程。它实际上是将一个许可(permit)提供给指定的线程,如果这个线程已经被park()
阻塞,它将被唤醒;如果还未被阻塞,那么这个许可会被保存起来,当这个线程将来调用park()
时,它将由于这个许可而立即返回。parkNanos(long nanos)
: 阻塞当前线程,最多不超过给定的时间(以纳秒为单位)。parkUntil(long deadline)
: 阻塞当前线程,直到某个时间(以毫秒为单位的绝对时间)。
许可(Permit)的概念
LockSupport
使用了一种名为“许可(permit)”的概念,每个线程都有一个与之关联的许可(虽然这些许可并没有实际的许可对象存在)。许可的数量最多只有一个。调用 unpark
将使得线程的许可变为可用,而调用 park
则会消耗掉这个许可(使其不再可用),如果许可已经不可用,线程将阻塞。
示例代码
下面的示例演示了如何使用 LockSupport
来控制线程的阻塞与唤醒:
import java.util.concurrent.locks.LockSupport;
public class LockSupportExample {
public static void runDemo() throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " is parked.");
// 阻塞当前线程
LockSupport.park();
// 当前线程被唤醒后执行
System.out.println(Thread.currentThread().getName() + " is unparked.");
});
thread.start();
// 保证线程先执行 park Thread.sleep(1000);
// 唤醒线程
System.out.println("main thread unparks " + thread.getName());
LockSupport.unpark(thread);
}
}
注意事项
LockSupport
不要求在park
和unpark
之间存在锁或同步块,提供了比Object
的wait
/notify
更加灵活的线程同步方式。park
方法可以无条件地返回,因此通常需要循环检查条件来决定是否继续等待。- 相比于
Thread.suspend()
和Thread.resume()
,LockSupport.park()
和LockSupport.unpark(Thread thread)
不容易导致死锁,因为许可的最大数量限制为一,且unpark
操作是安全的,即使线程还未进入park
状态。
LockSupport
类是实现高级同步机制的基础,例如在 java.util.concurrent
包中的 Semaphore
、ReentrantLock
、`
CountDownLatch等高级并发工具内部都有使用到
LockSupport`。
2.8.Volatile关键字
volatile
关键字在 Java 并发编程中扮演着重要的角色,虽然它本身不是一种锁机制,但它提供了变量的可见性和有序性保证,从而成为实现轻量级同步的一种手段。使用 volatile
声明的变量可以确保所有线程都能看到共享变量的最新值。
2.8.1.可见性
在多线程环境中,为了性能优化,每个线程可能会把共享变量从主内存复制到线程的本地内存中。如果一个线程修改了这个变量的值,而这个新值没有被及时写回主内存中,那么其他线程可能就看不到这个修改。volatile
关键字可以保证被它修饰的变量不会被线程缓存,所有的读写都直接操作主内存,因此保证了变量修改的可见性。
2.8.2.有序性
在 Java 中,编译器和处理器可能会对操作进行重排序,以优化程序性能。但在某些情况下,这种重排序可能会破坏多线程程序的正确性。volatile
变量具有“禁止指令重排序”语义:对 volatile
变量的读写操作之前的所有操作都不会被重排序到读写操作之后。
2.8.3.不保证原子性
尽管 volatile
提供了变量操作的可见性和有序性保证,但它并不保证操作的原子性。例如,自增操作 count++
(它包含读取 count
、增加 count
、写回 count
三个步骤)就不是原子的,即使 count
被声明为 volatile
。
2.8.4.使用场景
- 状态标志:
volatile
常用于标志变量,如线程运行状态标志。 - 双重检查锁定(Double-Check Locking):用于实现单例模式时,保证实例的唯一性和线程安全。
- 一次性安全发布:确保对象被安全地发布,所有线程都能看到对象的初始化完成状态。
2.8.5.示例代码
使用 volatile
实现线程间的信号通信:
public class VolatileExample {
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!flag) { // 等待flag变为true
// do something
}
System.out.println("Flag is true now!");
}).start();
Thread.sleep(1000); // 模拟主线程其他操作
flag = true; // 修改flag的值,通知其他线程
}
}
2.8.6.注意事项
- 使用
volatile
时需要明确其目的是提供变量的可见性和有序性,而非原子性。 - 对于多个变量或复合操作的原子性要求,应该使用锁(如
synchronized
或java.util.concurrent.locks
包中的锁)或原子变量类(如AtomicInteger
)。
2.9.Semaphore信号量
Semaphore
是 Java 并发包 (java.util.concurrent
) 中提供的一个同步工具类,它用于控制对共享资源的访问数量。虽然 Semaphore
本质上不是锁,但它可以用作一种特殊的锁机制,用于限制对某段资源的同时访问数,也就是说,它可以限制同时执行一段代码的线程数。
2.9.1.工作原理
Semaphore
内部维护了一组许可(permits),许可的数量可以在 Semaphore
初始化时指定。线程在访问共享资源之前必须先从 Semaphore
获取许可,如果 Semaphore
内的许可被其他线程全部取走,那么尝试获取许可的线程将被阻塞,直到其他线程释放许可或者被中断。
2.9.2.主要方法
Semaphore(int permits)
: 构造一个Semaphore
实例,初始化许可数量。void acquire()
: 从Semaphore
获取一个许可,如果无可用许可则当前线程将被阻塞。void release()
: 释放一个许可,将其返回给Semaphore
。void acquire(int permits)
: 获取指定数量的许可。void release(int permits)
: 释放指定数量的许可。
2.9.3.使用场景
- 控制资源访问:当资源有限时,如数据库连接、IO通道等,使用
Semaphore
可以限制资源的并发访问量。 - 实现限流:在高并发场景下限制任务的并发执行数,防止过载。
2.9.4.示例代码
以下示例演示如何使用 Semaphore
控制对某个资源的并发访问:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
// 假设只允许3个线程同时访问资源
private static final Semaphore semaphore = new Semaphore(3);
static class Task implements Runnable {
private String name;
Task(String name) {
this.name = name;
}
@Override
public void run() {
try {
// 获取许可
semaphore.acquire();
System.out.println(name + " is accessing the shared resource");
Thread.sleep(1000); // 模拟资源访问需要的时间
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可
semaphore.release();
System.out.println(name + " released the permit");
}
}
}
public static void main(String[] args) {
// 创建并启动多个线程
for (int i = 0; i < 10; i++) {
new Thread(new Task("Thread " + i)).start();
}
}
}
2.9.5.注意事项
- 在使用
Semaphore
时,需要确保每次成功获取许可的线程最终都会释放许可,最好在finally
代码块中释放许可,以避免死锁。 Semaphore
可以配置为公平(fair)模式,通过构造函数Semaphore(int permits, boolean fair)
来指定,公平模式下,线程获取许可的顺序将按照请求的顺序进行。
2.10.CyclicBarrier 和 CountDownLatch
CyclicBarrier
和 CountDownLatch
是 Java 并发包 java.util.concurrent
中提供的两种重要的同步辅助类。它们虽然不是锁,但在多线程编程中被广泛用于协调线程间的同步或等待某些条件满足。
2.10.1.CountDownLatch
CountDownLatch
是一个同步辅助类,用于让一个或多个线程等待一组事件的发生。它维护一个计数器,该计数器在构造时被初始化(只能设置一次),每当一个事件发生时,计数器的值就减一。调用 CountDownLatch
的 await()
方法的线程会阻塞,直到计数器的值变为零,这时所有等待的线程都会被释放继续执行。
2.10.1.1.使用场景
- 等待初始化完成:在应用程序启动时,等待必要的服务初始化完成。
- 等待任务完成:等待并行执行的任务全部完成。
2.10.1.2.示例代码
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int threads = 3;
CountDownLatch latch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " started.");
// 模拟任务执行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
latch.countDown(); // 完成任务,计数器减一
System.out.println(Thread.currentThread().getName() + " finished.");
}).start();
}
latch.await(); // 等待所有线程完成
System.out.println("All threads have finished.");
}
}
2.10.2.CyclicBarrier
CyclicBarrier
是另一个同步辅助类,它允许一组线程相互等待,直到所有线程都到达了一个共同的障碍点(Barrier)后才继续执行。与 CountDownLatch
不同,CyclicBarrier
是可以重用的。
2.10.2.1.使用场景
- 同步任务:在完成一组相互依赖的并行任务时,确保所有任务在进行下一步之前都完成了当前步骤。
2.10.2.2.示例代码
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int threads = 3;
CyclicBarrier barrier = new CyclicBarrier(threads, () -> {
// 当所有线程都到达barrier时执行
System.out.println("All threads have reached the barrier.");
});
for (int i = 0; i < threads; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " started.");
try {
Thread.sleep(1000);
barrier.await(); // 等待其他线程
System.out.println(Thread.currentThread().getName() + " crossed the barrier.");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
2.10.3.对比
- 初始化方式:
CountDownLatch
的计数器在对象创建时设置并且之后不能更改,而CyclicBarrier
的计数器可以通过重置来重用。 - 用途:
CountDownLatch
适用于一个或多个线程等待其他线程完成操作的场景,CyclicBarrier
适用于多个线程互相等待至某个公共点的场景。 - 可重用性:
CyclicBarrier
可以重用,而CountDownLatch
不能。
3.具体细节整理汇总
3.1.死锁
死锁是指两个或多个线程在执行过程中,因为互相等待对方持有的资源而无限期地阻塞的一种情况。简单来说,就是每个线程持有一部分资源,同时又等待其他线程释放它所需要的资源,导致所有相关线程都进入等待状态,无法继续执行。
死锁通常发生在以下四个条件同时满足时:
- 互斥条件:资源不能被多个线程同时占用。
- 持有并等待条件:一个线程至少持有一个资源,并且正在等待获取额外的资源,这些资源被其他线程持有。
- 非抢占条件:资源只能由持有它的线程自愿释放,不能被强行抢占。
- 循环等待条件:存在一种线程资源的循环等待链,每个线程都持有下一个线程所需要的资源。
解决死锁的方法通常包括预防、避免和检测与恢复:
- 预防是指通过破坏死锁的四个必要条件中的至少一个来防止死锁的发生。
- 避免涉及到系统的资源分配策略,确保系统始终处于安全状态。
- 检测与恢复是指系统定期检测死锁的存在,并通过一些机制(如资源抢占、线程重启)来解除死锁。
3.2.竞态
竞态条件发生在两个或多个线程访问共享数据,并且至少有一个线程对数据进行写操作时。如果没有适当的同步,线程之间的交互可能导致数据的不一致性。简而言之,竞态条件是由于多个线程以不可预知的顺序访问共享数据而导致的程序执行结果不正确的情况。
竞态条件的一个典型例子是检查后操作(check-then-act),即一个线程检查了某个条件(如共享变量是否满足某个值),然后在这个条件的基础上执行操作,但在检查和操作之间,另一个线程已经改变了这个条件。
解决竞态条件的主要方法是通过使用同步机制(如锁、信号量等)来确保对共享资源的访问是串行的,或者设计无锁数据结构和算法来避免竞态条件。
总的来说,死锁和竞态条件都是多线程编程中需要特别注意的问题,合理地设计线程同步和资源分配策略是避免这些问题的关键。
3.3. Thread.currentThread().interrupt();
调用 Thread.currentThread().interrupt()
用于重新设置当前线程的中断状态。当线程中的一个阻塞操作(如 Object.wait()
、Thread.join()
或 Thread.sleep()
)因为中断(interrupt)被唤醒并抛出 InterruptedException
时,该线程的中断状态会被清除(即,中断状态被重置为 false
)。如果你捕获到 InterruptedException
,通常意味着某个外部操作想要中断当前线程的执行。在捕获异常后,调用 Thread.currentThread().interrupt()
可以再次设置中断状态,这样做有几个目的:
3.3.1. 保留中断请求
它保留了中断请求的信息。这对于那些可能不立即检查中断状态但稍后会检查它的代码路径来说是重要的。通过重新设置中断状态,你确保后续的代码能够通过检查 Thread.interrupted()
或 Thread.isInterrupted()
来了解到当前线程已被请求中断。
3.3.2. 传递中断信号
在多层嵌套调用中,最底层的方法可能会捕获到中断异常,并不总是直接能够处理它(比如,它不知道如何适当地响应这个中断)。通过重新设置中断状态,它允许中断信号可以被传递给调用栈上的更高层方法,这些方法可能更适合做出响应决定。
3.3.3. 支持清理操作和有序关闭
在捕获到 InterruptedException
后,可能需要执行一些清理操作,然后再结束线程。重新设置中断状态后,你可以选择在完成必要的清理后再检查中断状态,从而有序地关闭线程或者选择进一步的动作。
3.3.4.示例
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
// 线程正常执行任务
Thread.sleep(1000); // 可能抛出 InterruptedException
}
} catch (InterruptedException e) {
// 捕获异常,重新设置中断状态,允许线程退出或者在更高层处理中断
Thread.currentThread().interrupt();
}
// 进行一些必要的清理操作
}
在这个示例中,如果 Thread.sleep()
抛出 InterruptedException
,我们捕获这个异常并重新设置中断状态,然后线程可以继续到达清理代码或者通过检查中断状态来优雅地终止。
3.4.锁的重入
锁的重入,或称为可重入锁(Reentrant Lock),是指同一个线程可以多次获得同一把锁。在多线程环境中,如果一个线程已经持有了某个锁,那么这个线程再次请求这个锁时,可以再次获得这个锁而不会被锁阻塞。这种机制避免了一个线程在等待它自己持有的锁而产生死锁。
3.4.1.重入锁的特点
- 避免死锁:允许线程再次获取已经持有的锁,避免了线程等待自己持有的锁而产生死锁的情况。
- 计数机制:可重入锁通常通过计数机制来实现。当线程首次获取锁时,计数器为1。每当这个线程再次获取到这把锁时,计数器就增加1;每当线程释放锁时,计数器减1。当计数器回到0时,锁被释放。
- 保持公平性:在实现时可以选择是否公平。公平的可重入锁意味着锁将按照线程请求的顺序来分配,而非公平锁则允许“插队”。
3.4.2.使用场景
- 递归调用:在递归函数中,如果访问了同步资源,线程可以再次获得已经持有的锁。
- 扩展功能:当一个已经获取锁的方法被另一个需要相同锁的方法调用时,这种设计避免了死锁。
- 调用链中的同步方法:在调用链中,一个同步方法直接或间接地调用另一个需要同一把锁的同步方法。
3.4.3.示例
以Java中的ReentrantLock
为例,演示可重入锁的基本使用:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void outerMethod() {
lock.lock();
try {
System.out.println("Lock acquired in outerMethod");
innerMethod();
} finally {
lock.unlock();
}
}
public void innerMethod() {
lock.lock();
try {
System.out.println("Lock acquired in innerMethod");
// perform some operations
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
example.outerMethod();
}
}
在这个例子中,outerMethod
获取了锁,然后调用了 innerMethod
,后者也尝试获取同一把锁。由于锁是可重入的,所以 innerMethod
可以在不被阻塞的情况下获取锁,并继续执行。如果锁不是可重入的,那么 innerMethod
将会因为尝试获取已被自己持有的锁而被阻塞,导致死锁。
3.4.5.结论
可重入锁是一种避免死锁的同步机制,特别适用于递归调用、扩展功能和调用链中需要同步访问资源的场景。Java中的ReentrantLock
和Synchronized
关键字都实现了锁的重入性。