多线程之并发锁
Synchronized
特性:
- 可重入,持有该锁的线程可以再次获取锁
- 不可中断:获取了Synchronized锁之后就必须要等其释放锁,响应不了中断
- 灵活性不高:使用Synchronized锁只能是进入到代码块内执行完了才释放锁
原理:
Synchronized的实现主要依赖于Java虚拟机(JVM)中的monitor机制。每个对象在JVM中都有一个关联的监视器锁(monitor lock)。
当线程要进入Synchronized代码块时必须获取对象的menitor,如果menitor已经被其它线程给占有了,那么就会陷入阻塞状态,当一个线程持有menitor,menitor数加1,再次持有menitor就会再次加1,从而实现了可重入的功能。
public class Main {
static volatile Integer num=0;
static volatile Integer n=0;
public static void main(String[] args) {
synchronized (num) {
for(int i=0;i<10;i++)num++;
System.out.println(num);
}
}
}
反编译结果如下:
5: monitorenter
6: iconst_0
7: istore_2
8: iload_2
9: bipush 10
11: if_icmpge 40
14: getstatic #7 // Field num:Ljava/lang/Integer;
17: astore_3
18: getstatic #7 // Field num:Ljava/lang/Integer;
21: invokevirtual #13 // Method java/lang/Integer.intValue:()I
24: iconst_1
25: iadd
26: invokestatic #19 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
29: putstatic #7 // Field num:Ljava/lang/Integer;
32: aload_3
33: pop
34: iinc 2, 1
37: goto 8
40: getstatic #23 // Field java/lang/System.out:Ljava/io/PrintStream;
43: getstatic #7 // Field num:Ljava/lang/Integer;
46: invokevirtual #29 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
49: aload_1
50: monitorexit
可以看到当执行monitorenter的时候进入代码块中,执行到monitorexit时退出代码块。
JVM对synchronized锁的优化
JVM 对 synchronized
关键字的优化主要体现在以下几个方面:偏向锁、轻量级锁、重量级锁、锁粗化、锁消除 和 自适应自旋。
1. 偏向锁
偏向锁是针对无竞争场景下的优化。当一个线程首次获得锁时,JVM 会将这个锁对象的标记设置为偏向该线程。此后,如果同一线程再次进入同步块,无需执行任何同步操作,而是直接使用偏向锁,从而避免了大量的加锁和解锁操作。这种优化在单线程的情况下效果显著。
-
原理:锁对象的 Mark Word(对象头的一部分)会记录偏向的线程 ID。偏向锁状态下,线程不需要执行 CAS 操作来获取锁,只需检查 Mark Word 中的线程 ID 是否是当前线程即可。如果是,则直接进入同步块。
-
撤销:当另一个线程尝试获取偏向锁时,偏向锁会被撤销,并升级为轻量级锁或重量级锁。
2. 轻量级锁
轻量级锁是针对线程竞争不激烈的场景进行的优化。线程尝试获取轻量级锁时,会将锁对象的 Mark Word 拷贝到当前线程的栈帧中,并通过 CAS 操作尝试将锁对象的 Mark Word 指向线程的锁记录。如果成功,则获取锁;如果失败,说明有其他线程正在竞争该锁,此时锁会升级为重量级锁。
-
原理:轻量级锁的核心在于减少了线程切换的成本。通过自旋操作,线程在短时间内尝试获取锁,而不是立即进入阻塞状态,从而避免了频繁的线程上下文切换。
-
自旋优化:JVM 会让轻量级锁的线程在短时间内进行自旋,尝试获取锁而不进入阻塞状态。如果在自旋期间锁被释放,线程可以立即获得锁并继续执行;否则,线程将进入阻塞状态。
3. 重量级锁
重量级锁是最传统的锁实现。当线程竞争激烈或偏向锁和轻量级锁无法满足要求时,锁会升级为重量级锁。此时,JVM 会通过操作系统的 Mutex(互斥锁)来阻塞线程,直到锁被释放。
- 原理:重量级锁依赖操作系统的同步机制,线程会被挂起和唤醒,导致上下文切换开销较大。因此,JVM 会尽量避免锁升级到重量级锁,除非在高竞争场景下无法避免。
4. 锁消除
锁消除是一种编译时的优化。通过逃逸分析,JVM 可以检测出某些同步块中的锁对象是否仅在单线程内部使用。如果某个锁对象没有逃逸出当前线程,即只在单线程中使用,那么该锁的加锁和解锁操作就没有实际意义,JVM 会在编译阶段直接将这些锁操作消除。
- 示例:在方法内部创建的局部对象通常不会被其他线程访问,因此同步块中的锁可以被消除。
public void method() {
Object lock = new Object();
synchronized (lock) {
// 临时对象锁定,但该锁对象不会逃逸到其他线程
}
}
- 效果:锁消除减少了不必要的同步开销,提升了代码执行效率。
5. 锁粗化
锁粗化是 JVM 在编译时进行的一种优化策略。当 JVM 发现多个连续的同步块都对同一个对象加锁时,会将这些同步块合并为一个更大范围的同步块,从而减少频繁加锁和解锁的开销。
-
原理:如果多个操作本质上是一个整体操作,但由于代码结构导致多次加锁解锁,JVM 会将这些加锁操作合并为一次更大范围的加锁,从而提高性能。
-
示例:在一个循环中,每次迭代都对同一个对象进行加锁操作,JVM 可能会将整个循环体用一个更大的锁来保护。
for (int i = 0; i < 1000; i++) {
synchronized (lock) {
// 每次循环都加锁,JVM 可能会将其优化为在整个循环外部加锁
}
}
6. 自适应自旋
自适应自旋是对轻量级锁的一种优化。传统的自旋锁会在固定的循环次数后放弃,而自适应自旋会根据前一次自旋的结果和锁持有的时间来动态调整自旋时间。
-
原理:如果某个线程刚刚释放了锁,那么自适应自旋可能会让后续线程自旋更长时间,以便它们有更大概率获得锁;反之,如果线程持续未获得锁,自旋时间会逐渐缩短,减少 CPU 资源的浪费。
-
效果:自适应自旋能够更智能地决定自旋时间,避免过长或过短的自旋时间,从而提升锁竞争的性能。
Lock接口
Lock
接口是 Java 中的一个高级锁机制,它比 synchronized
更灵活。Lock
接口定义了五个核心方法:
void lock()
void lockInterruptibly() throws InterruptedException
boolean tryLock()
boolean tryLock(long time, TimeUnit unit) throws InterruptedException
void unlock()
下面我结合代码分别讲解这五种方法的使用。
1. lock()
lock()
是最常用的方法,用于获取锁。如果锁被其他线程持有,则当前线程会等待,直到锁被释放。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private final Lock lock = new ReentrantLock();
private int counter = 0;
public void increment() {
lock.lock(); // 获取锁
try {
counter++; // 关键代码段
} finally {
lock.unlock(); // 确保最终释放锁
}
}
}
说明: 在这个例子中,lock.lock()
确保了 counter
的自增操作在多线程环境下是线程安全的。finally
块中调用 unlock()
保证了锁一定会被释放。
2. lockInterruptibly()
lockInterruptibly()
与 lock()
类似,但它允许在等待锁的过程中响应中断。如果当前线程被中断,它会抛出 InterruptedException
。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockInterruptiblyExample {
private final Lock lock = new ReentrantLock();
public void performTask() {
try {
lock.lockInterruptibly(); // 可中断地获取锁
try {
// 关键代码段
} finally {
lock.unlock(); // 确保释放锁
}
} catch (InterruptedException e) {
// 处理中断信号
System.out.println("Thread was interrupted while waiting for the lock.");
}
}
}
说明: lockInterruptibly()
方法特别适用于需要能中断的场景,比如当线程可能会因为某些原因需要提前退出时。
3. tryLock()
tryLock()
尝试获取锁,如果锁可用则立即返回 true
,否则返回 false
。这个方法不会阻塞当前线程。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockExample {
private final Lock lock = new ReentrantLock();
public void tryTask() {
if (lock.tryLock()) {
try {
// 关键代码段
} finally {
lock.unlock(); // 确保释放锁
}
} else {
// 锁不可用时的处理逻辑
System.out.println("Could not acquire lock, performing alternative operations.");
}
}
}
说明: tryLock()
非常适合那些不想因为锁不可用而被阻塞的操作,例如轮询或在超时前尝试获取锁。
4. tryLock(long time, TimeUnit unit)
这个方法尝试在给定的时间内获取锁。如果在指定时间内成功获取锁,返回 true
,否则返回 false
。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TimedTryLockExample {
private final Lock lock = new ReentrantLock();
public void timedTryTask() {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) { // 在2秒内尝试获取锁
try {
// 关键代码段
} finally {
lock.unlock(); // 确保释放锁
}
} else {
// 锁在指定时间内不可用时的处理逻辑
System.out.println("Could not acquire lock within 2 seconds.");
}
} catch (InterruptedException e) {
// 处理中断信号
System.out.println("Thread was interrupted while waiting for the lock.");
}
}
}
说明: tryLock(long time, TimeUnit unit)
在一些需要在有限时间内完成任务的场景中特别有用,避免了无限等待。
5. unlock()
unlock()
用于释放锁。它通常在 try-finally
块中使用,以确保在锁被持有后能正确释放。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class UnlockExample {
private final Lock lock = new ReentrantLock();
private int counter = 0;
public void decrement() {
lock.lock(); // 获取锁
try {
counter--; // 关键代码段
} finally {
lock.unlock(); // 确保释放锁
}
}
}
说明: unlock()
是确保锁最终释放的关键。即使发生异常,finally
块也会保证锁的释放,从而避免死锁。
Reentrantlock
ReentrantLock
是 Java 中 Lock
接口的一个具体实现,是一种可重入锁。可重入锁意味着同一个线程可以多次获得同一把锁而不会被阻塞。相比于 synchronized
关键字,ReentrantLock
提供了更灵活的锁定机制,并具备了一些高级功能,如公平锁、条件变量等。
1. 基本使用
ReentrantLock
提供了显式锁定和解锁机制,类似于 synchronized
,但需要手动控制锁的获取和释放。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
private int counter = 0;
public void increment() {
lock.lock(); // 获取锁
try {
counter++;
} finally {
lock.unlock(); // 确保释放锁
}
}
public int getCounter() {
return counter;
}
}
说明: 在上述代码中,lock.lock()
用于获取锁,finally
块中的 lock.unlock()
确保了即使在发生异常时,锁也能正确释放。
2. 可重入性
ReentrantLock
的一个重要特性是可重入性,即同一个线程可以多次获取同一把锁,每次获取都需要对应的释放。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void outerMethod() {
lock.lock();
try {
innerMethod(); // 可以再次获取锁
} finally {
lock.unlock();
}
}
public void innerMethod() {
lock.lock();
try {
// 执行一些操作
} finally {
lock.unlock();
}
}
}
说明: 在这个例子中,outerMethod
和 innerMethod
都会获取同一把锁,因为 ReentrantLock
是可重入的,所以不会发生死锁。
3. 公平锁 vs. 非公平锁
ReentrantLock
可以配置为公平锁或非公平锁。公平锁按照线程请求锁的顺序(FIFO)分配锁,而非公平锁可能会让某些线程"插队"。
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁
说明:
- 公平锁:更公平,但性能可能稍差,因为线程需要按照顺序等待。
- 非公平锁:可能会有更高的吞吐量,但有可能导致某些线程长期得不到锁(饥饿问题)。
4. tryLock()
和 tryLock(long time, TimeUnit unit)
这两个方法用于尝试获取锁,不像 lock()
那样会无限等待。
tryLock()
:立即尝试获取锁,如果成功返回true
,否则返回false
。tryLock(long time, TimeUnit unit)
:在指定的时间内尝试获取锁,超时未获取到返回false
。
if (lock.tryLock()) {
try {
// 获得锁后执行的代码
} finally {
lock.unlock();
}
} else {
// 没有获得锁时的处理
}
说明: 这些方法非常适用于避免长时间等待锁的场景。
5. lockInterruptibly()
lockInterruptibly()
是一个响应中断的锁获取方法。与 lock()
不同,如果当前线程被中断,它会抛出 InterruptedException
并退出等待状态。
try {
lock.lockInterruptibly();
try {
// 执行任务
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 处理中断信号
}
说明: 这种锁获取方式适用于需要能中断的场景,确保线程不会在等待锁的过程中被无限期阻塞。
6. 条件变量(Condition)
ReentrantLock
提供了条件变量,通过 newCondition()
方法可以创建多个 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 awaitMethod() throws InterruptedException {
lock.lock();
try {
System.out.println("Waiting for signal...");
condition.await(); // 释放锁并进入等待状态
System.out.println("Received signal, resuming work...");
} finally {
lock.unlock();
}
}
public void signalMethod() {
lock.lock();
try {
System.out.println("Sending signal...");
condition.signal(); // 唤醒等待的线程
} finally {
lock.unlock();
}
}
}
说明: 条件变量可以实现复杂的线程协作机制,例如生产者-消费者模型。await()
会使当前线程等待,并释放锁,直到其他线程调用 signal()
或 signalAll()
唤醒它。
7. 性能与注意事项
- 性能: 相较于
synchronized
,ReentrantLock
在高并发情况下通常能提供更好的性能,特别是使用非公平锁时。 - 死锁: 由于需要手动管理锁的获取和释放,更容易出现死锁问题,开发时需要特别注意
lock()
和unlock()
的配对。 - 使用场景: 适合需要更加灵活的锁定控制场景,比如可中断锁定、超时锁定、多个条件变量、非阻塞锁定等。
读写锁
读写锁允许多个线程同时读取共享数据,但在写入数据时,写锁会阻止其他线程进行读写操作,从而提高并发性能。
Java中的读写锁主要由ReentrantReadWriteLock
类实现。这个类提供了两个主要的锁:读锁和写锁。
读写锁的基本用法
- 读锁(
readLock()
):多个线程可以同时获取读锁,这样可以并行读取共享数据,只要没有线程持有写锁。 - 写锁(
writeLock()
):当一个线程持有写锁时,其他线程无法获取读锁或写锁,这样可以保证写操作的独占性。
锁的降级
锁的降级指的是一个线程在持有写锁的情况下,获取读锁。这样做的好处是,可以在写操作完成后,继续进行读取操作,而不需要再次申请写锁。
示例代码
以下是一个简单的示例,展示了如何使用ReentrantReadWriteLock
以及锁的降级:
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockExample {
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private String data = "";
// 写操作
public void write(String newData) {
rwLock.writeLock().lock();
try {
System.out.println("Writing data: " + newData);
this.data = newData;
// 降级到读锁
rwLock.readLock().lock();
} finally {
// 释放写锁
rwLock.writeLock().unlock();
}
}
// 读操作
public String read() {
rwLock.readLock().lock();
try {
System.out.println("Reading data: " + data);
return data;
} finally {
rwLock.readLock().unlock();
}
}
public static void main(String[] args) {
ReadWriteLockExample example = new ReadWriteLockExample();
// 写操作
example.write("Hello, World!");
// 读操作
example.read();
}
}
locksupport
LockSupport
是 Java 并发包中的一个工具类,提供了一些低级别的线程挂起和唤醒操作的方法。它主要用于在实现复杂的同步机制时,提供线程挂起和恢复的基本支持。
LockSupport
的主要方法
-
LockSupport.park()
使当前线程挂起,直到被另一个线程通过调用unpark()
方法唤醒。这个方法会释放当前线程持有的任何监视器(即锁),从而允许其他线程对资源进行访问。LockSupport.park();
-
LockSupport.parkNanos(long nanos)
使当前线程挂起指定的纳秒时间,时间到达后线程会被唤醒。这个方法提供了对挂起时间的精确控制。LockSupport.parkNanos(1000); // 挂起 1000 纳秒
-
LockSupport.parkUntil(long deadline)
使当前线程挂起直到指定的时间点(以毫秒为单位的绝对时间),时间到达后线程会被唤醒。LockSupport.parkUntil(System.currentTimeMillis() + 1000); // 挂起到当前时间 + 1000 毫秒
-
LockSupport.unpark(Thread thread)
唤醒指定的线程。如果该线程在调用park()
时被挂起,它将被唤醒。如果该线程在未被挂起的情况下调用unpark()
,它不会立即生效,但会记录该线程的唤醒请求,当它下一次调用park()
时将立即返回。LockSupport.unpark(thread);
使用场景
LockSupport
类主要用于实现高级的同步原语,如自定义的锁、信号量或其他线程协调机制。它提供了比传统的 Object.wait()
和 Object.notify()
更加灵活和高效的线程挂起和唤醒机制。
示例代码
以下是一个简单的示例,展示了如何使用 LockSupport
来实现一个基本的线程同步:
import java.util.concurrent.locks.LockSupport;
public class LockSupportExample {
private static final Object lock = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println("Thread 1 is going to park");
LockSupport.park();
System.out.println("Thread 1 is unparked");
});
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(1000); // Ensure thread1 is parked
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Thread 2 is unpark");
LockSupport.unpark(thread1);
});
thread1.start();
thread2.start();
}
}
注意事项
-
park()
和unpark()
的配对
在使用park()
和unpark()
时,确保每次调用park()
都会有相应的unpark()
调用,否则线程可能会永久挂起。 -
避免滥用
尽管LockSupport
提供了底层的线程控制功能,但它并不是为普通的同步操作设计的,通常更高层次的同步工具(如ReentrantLock
和Semaphore
)能够提供更直观和易于使用的接口。
AQS队列同步器
AQS(AbstractQueuedSynchronizer)是Java并发包java.util.concurrent
中的一个关键抽象类,它为实现锁和同步器(如ReentrantLock、CountDownLatch、Semaphore等)提供了基础设施。AQS通过一个FIFO队列来管理获取锁资源的线程,并通过CAS操作和volatile变量来确保并发操作的安全性。
1. 三个修改同步状态的方法
AQS提供了三个方法来直接修改同步状态,它们是protected的,用于供自定义同步器调用。
-
getState()
:- 获取当前的同步状态。返回值为int类型。
-
setState(int newState)
:- 直接设置同步状态为指定的值。这是一个直接操作,通常需要确保调用时保持线程安全。
-
compareAndSetState(int expect, int update)
:- 使用CAS(Compare-And-Swap)操作来更新同步状态。如果当前状态等于预期值
expect
,则将其更新为update
,并返回true
。否则返回false
。
- 使用CAS(Compare-And-Swap)操作来更新同步状态。如果当前状态等于预期值
2. 五个可重写的方法
AQS为实现自定义同步器提供了五个可以被重写的方法。通过重写这些方法,可以定义如何获取和释放同步状态。
-
tryAcquire(int arg)
:- 尝试以独占模式获取同步状态。需要实现者定义如何获取同步状态,返回
true
表示获取成功,false
表示获取失败。
- 尝试以独占模式获取同步状态。需要实现者定义如何获取同步状态,返回
-
tryRelease(int arg)
:- 尝试以独占模式释放同步状态。需要实现者定义如何释放同步状态,返回
true
表示释放成功,false
表示释放失败。
- 尝试以独占模式释放同步状态。需要实现者定义如何释放同步状态,返回
-
tryAcquireShared(int arg)
:- 尝试以共享模式获取同步状态。返回值为
>= 0
表示获取成功,负数表示获取失败。
- 尝试以共享模式获取同步状态。返回值为
-
tryReleaseShared(int arg)
:- 尝试以共享模式释放同步状态。返回
true
表示同步状态完全释放,可以唤醒后续等待的线程。
- 尝试以共享模式释放同步状态。返回
-
isHeldExclusively()
:- 判断当前同步器是否被独占。通常在实现独占锁时用来检查锁是否被当前线程持有。
3. 九个模板方法
-
acquire(int arg)
:- 独占模式获取同步状态,如果获取失败,将进入等待队列,直到获取成功。
-
acquireInterruptibly(int arg)
:- 以响应中断的方式获取独占模式的同步状态。如果当前线程被中断,则抛出
InterruptedException
。
- 以响应中断的方式获取独占模式的同步状态。如果当前线程被中断,则抛出
-
acquireShared(int arg)
:- 以共享模式获取同步状态,如果获取失败,将进入等待队列,直到获取成功。
-
acquireSharedInterruptibly(int arg)
:- 以响应中断的方式获取共享模式的同步状态。如果当前线程被中断,则抛出
InterruptedException
。
- 以响应中断的方式获取共享模式的同步状态。如果当前线程被中断,则抛出
-
release(int arg)
:- 以独占模式释放同步状态,并唤醒后续等待的线程。
-
releaseShared(int arg)
:- 以共享模式释放同步状态,并唤醒后续等待的线程。
-
tryAcquireNanos(int arg, long nanosTimeout)
:- 以独占模式获取同步状态,并支持超时。如果在指定时间内未能获取同步状态,则返回
false
。
- 以独占模式获取同步状态,并支持超时。如果在指定时间内未能获取同步状态,则返回
-
tryAcquireSharedNanos(int arg, long nanosTimeout)
:- 以共享模式获取同步状态,并支持超时。如果在指定时间内未能获取同步状态,则返回
false
。
- 以共享模式获取同步状态,并支持超时。如果在指定时间内未能获取同步状态,则返回
-
hasQueuedThreads()
:- 判断是否有线程在等待获取同步状态。
1. 双向链表(FIFO Queue)
AQS 使用一个双向链表来维护等待线程的队列。这些线程在尝试获取锁或其他同步状态时,如果当前锁不可用,就会被加入到这个队列中进行等待。链表中的每个节点(Node)表示一个线程,节点结构如下:
- prev: 指向前一个节点。
- next: 指向后一个节点。
- thread: 保存当前节点对应的线程。
- waitStatus: 表示当前节点的状态,比如是否已取消、是否需要唤醒等。
2. volatile int state
- state 变量用来表示同步状态。它是一个
volatile
变量,因此能够保证在多个线程之间的可见性。具体含义因使用的同步工具不同而异,例如,在ReentrantLock
中,state 表示持有锁的次数,在Semaphore
中,state 表示可用的许可数量。
3. 头节点(Head)与尾节点(Tail)
- head 和 tail:AQS 使用两个指针
head
和tail
来维护队列的头节点和尾节点。head
节点是一个哨兵节点(dummy node),通常代表正在持有锁的线程;tail
节点指向当前最后一个等待的线程节点。
AQS的工作流程
- 当一个线程尝试获取锁,如果获取失败,它就会被封装成一个 Node,并加入到等待队列的尾部。
- 如果锁可用,头节点的线程就会被唤醒并从队列中移除,然后将当前节点的
next
节点设置为新的头节点。
死锁
死锁是多线程编程中常见的一个问题,它发生在两个或多个线程互相等待对方持有的资源,从而导致所有线程都无法继续执行的情况。死锁是一种循环等待的状态,其中每个线程都在等待另一个线程释放资源。
死锁出现的四个必要条件:
-
互斥条件(Mutual Exclusion):
- 资源必须在一个时刻只能被一个线程所使用。这意味着资源是排他使用的,比如一个锁只能被一个线程持有。
-
占有且等待条件(Hold and Wait):
- 一个已经持有某种资源的线程在没有释放该资源之前,可以申请新的资源。如果新的资源暂时不能得到满足,它就会等待而不会释放已拥有的资源。
-
不可抢占条件(No Preemption):
- 已分配给线程的资源不能被抢占,只有该线程主动释放资源才可以让其他线程使用。
-
循环等待条件(Circular Wait):
- 存在一个线程集合{P1, P2, …, PN},其中P1等待P2持有的资源,P2等待P3持有的资源,…,PN等待P1持有的资源,形成了一个等待环路。
死锁示例
假设有一个系统中有两个线程T1和T2,以及两个资源R1和R2。
- T1获得了R1,然后尝试获取R2。
- T2获得了R2,然后尝试获取R1。
在这种情况下,T1等待R2,而T2等待R1,于是两个线程都陷入无限等待的状态,这就是一个典型的死锁情况。
避免死锁的方法
避免死锁的主要方法包括:
-
破坏互斥条件:
- 如果可以,尽量使用共享资源而不是排他资源。
-
破坏占有且等待条件:
- 要求线程一次性请求所有需要的资源,或者在请求新资源之前释放已持有的资源。
-
破坏不可抢占条件:
- 允许资源被抢占,例如,如果一个线程需要的资源被另一个线程占用,它可以强制抢占那个资源。
-
破坏循环等待条件:
- 为资源分配一个全局顺序,要求线程按照这个顺序请求资源。
实践中的预防措施
在实践中,还有一些其他的方法可以帮助预防死锁的发生:
- 使用超时:为资源请求设置超时,如果请求超时则放弃资源。
- 使用死锁检测算法:周期性地检测是否存在死锁,并采取相应的措施解决死锁。
- 使用锁顺序:为所有的锁定义一个固定的顺序,并且总是按照这个顺序获取和释放锁。
- 使用工具和监控:使用工具来监控应用程序的锁状态,帮助发现潜在的死锁风险。
在设计并发程序时,应该始终考虑到死锁的可能性,并采取适当的措施来避免或处理这种状况。