死锁及其必要条件
死锁是指在多个进程(或线程)并发执行时,它们互相等待对方释放资源,导致这些进程(或线程)无法继续执行的情况。简单来说,死锁发生时,系统中的进程陷入了“相互等待”的状态,导致程序无法继续运行。
死锁的发生通常需要满足以下四个必要条件(称为死锁的四个必要条件):
-
互斥条件(Mutual Exclusion): 至少有一个资源是以排他方式分配的,即资源一次只能被一个进程占用。如果其他进程请求该资源,它们必须等待,直到占用资源的进程释放它。
-
持有并等待(Hold and Wait): 一个进程已经持有了至少一个资源,并且正在等待获取其他进程占用的资源。也就是说,进程在等待资源的同时并没有释放自己已经持有的资源。
-
不剥夺条件(No Preemption): 已经分配给进程的资源,不能被强制剥夺。也就是说,进程释放资源只能通过它自己主动释放,而不是由系统强制剥夺。
-
循环等待条件(Circular Wait): 存在一组进程,它们形成一个闭环,其中每个进程都在等待下一个进程持有的资源。例如,进程A等待进程B的资源,进程B等待进程C的资源,进程C又等待进程A的资源。
死锁的预防和解决
要避免死锁,可以采取一些措施,例如:
-
避免互斥条件:对于某些资源,尽量允许多个进程并行访问,减少资源冲突。
-
避免持有并等待:要求进程在开始执行前申请所有需要的资源,避免在执行过程中进行资源请求。
-
避免循环等待:对资源分配进行排序或规定资源申请的顺序,确保进程之间不会形成循环等待。
如果死锁发生,常见的解决方法包括:
-
终止进程:通过终止某些进程来打破死锁。
-
资源剥夺:强制从某些进程中回收资源,以便解除死锁。
死锁是并发编程中的一个经典问题,通常在设计系统时需要特别注意资源的分配和管理。
synchronized 和 Lock
synchronized 和 Lock 都是用于在多线程环境中实现线程同步的工具,但它们在使用方式、特性和灵活性上有一些显著的不同。下面是这两者的主要区别:
1. 语法和使用方式
-
synchronized:-
synchronized是一种 关键字,用于修饰方法或代码块。它通过 隐式地锁定一个对象 来保证线程安全。 -
使用方式:
-
修饰实例方法:
synchronized修饰一个实例方法时,锁的是该方法所属的实例对象。 -
修饰静态方法:
synchronized修饰静态方法时,锁的是该类的 Class 对象。 -
修饰代码块:指定一个特定的对象来进行加锁,锁定指定对象的监视器。
public synchronized void someMethod() { // 被同步的代码 }或者:
public void someMethod() { synchronized (someObject) { // 被同步的代码 } } -
-
-
Lock:-
Lock是java.util.concurrent.locks包中的一个接口,通常配合其实现类(如ReentrantLock)使用。它提供了比synchronized更为灵活和精细的锁机制。 -
使用方式:
-
通过
lock()方法手动获取锁,使用unlock()方法释放锁,通常使用try-finally语句块来确保锁的释放。
Lock lock = new ReentrantLock(); lock.lock(); // 获取锁 try { // 被同步的代码 } finally { lock.unlock(); // 释放锁 } -
-
2. 灵活性
-
synchronized:-
较为简单:用法非常直观,容易理解。
-
只能锁住整个方法或某个代码块,灵活性不如
Lock。 -
自动释放锁:当进入同步代码块或方法时会自动获取锁,退出时会自动释放锁(不需要显式调用释放锁)。
-
-
Lock:-
更灵活:除了基本的加锁和解锁操作,
Lock还提供了一些附加功能,如:-
tryLock():尝试获取锁,如果无法立即获取锁,则返回false(可以设置超时等待)。 -
lockInterruptibly():支持在等待锁时响应中断。 -
ReentrantLock提供的条件变量等。
-
-
需要手动释放锁,如果忘记释放锁,会导致死锁。
-
3. 可重入性
-
synchronized:-
synchronized默认是 可重入的,也就是说同一个线程可以多次获得同一个锁。
-
-
Lock:-
Lock(例如ReentrantLock)也是可重入的,能够在同一线程内多次获得同一把锁而不会造成死锁。
-
4. 性能
-
synchronized:-
由于 JVM 对
synchronized做了优化(如锁消除、锁粗化等),其性能在现代 JVM 上已经非常高效。尽管如此,在高并发情况下,它的性能可能会受到影响,尤其是竞争激烈时。
-
-
Lock:-
Lock的性能比synchronized要 更好,特别是在某些复杂的并发场景下(例如尝试锁、可中断锁等),Lock提供了更好的性能调优机会。
-
5. 支持中断
-
synchronized:-
synchronized不能响应中断,线程在等待锁时不会被中断,必须一直等到锁被释放。
-
-
Lock:-
Lock支持中断。例如,ReentrantLock提供了lockInterruptibly()方法,允许在等待锁的过程中响应中断,这在某些应用场景下非常有用。
-
6. 死锁风险
-
synchronized:-
synchronized本身较为简单,因此容易理解并避免死锁。但在复杂的多锁场景下,死锁的风险仍然存在,特别是在多个线程按不同的顺序请求多个资源时。
-
-
Lock:-
Lock提供了更高的灵活性,但这也意味着可能会更容易引发死锁,尤其是在使用多个锁时。如果使用不当,死锁的风险较大。因此,需要特别小心锁的管理和释放。
-
7. 其他功能
-
Lock:-
通过
Lock的实现类ReentrantLock,可以实现一些额外的功能,如 公平锁(即按照请求锁的顺序分配锁)、条件变量(通过Condition来实现线程间的协调)等。
-
总结:
-
如果你需要 简单易用 的同步机制,且只有 简单的锁定需求,
synchronized足够使用。 -
如果你需要 更灵活的控制,比如 尝试锁、中断锁、公平锁等,或者在高并发场景中需要 更高的性能,则应使用
Lock(如ReentrantLock)。
可重入锁、公平锁、中断锁
1. 可重入锁(Reentrant Lock)
可重入锁是指同一个线程可以多次获得同一把锁,而不会发生死锁。换句话说,如果一个线程已经持有了某个锁,它可以再次获得这个锁而不会被阻塞。
具体原理:
-
当一个线程进入某个同步块或方法时,它获得了某个锁,锁会记录谁持有了它。
-
如果同一个线程尝试再次进入这个锁保护的代码块,它不会被阻塞,而是可以继续执行。
-
这个特性确保了程序的灵活性,避免了因递归调用或多次访问同一资源时死锁的风险。
典型例子:ReentrantLock
ReentrantLock 是一个典型的可重入锁。其实现支持锁的递归获取,也就是说,同一个线程可以多次锁定同一把锁,直到所有锁都被释放。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final Lock lock = new ReentrantLock();
public void outerMethod() {
lock.lock(); // 获取锁
try {
innerMethod(); // 内部方法可以再次获取锁
} finally {
lock.unlock(); // 释放锁
}
}
public void innerMethod() {
lock.lock(); // 可以再次获取锁
try {
// 执行代码
} finally {
lock.unlock(); // 释放锁
}
}
}
优点:
-
避免死锁:同一线程可以进入多个临界区,不会被自己锁住。
-
递归调用:在递归或嵌套方法中非常有用,避免了由于锁未释放而导致的阻塞。
注意:
-
过多的可重入锁使用可能导致逻辑上的复杂性,应该合理管理锁的释放。
2. 公平锁(Fair Lock)
公平锁是指锁的获取遵循 先进先得(FIFO)的原则,即先请求锁的线程会先获得锁,避免某些线程一直无法获得锁(即 饥饿 问题)。在高并发的情况下,公平锁有助于避免线程的饥饿。
具体原理:
-
公平锁通过维护一个等待队列来保证线程获取锁的顺序。
-
如果多个线程请求锁,公平锁会按照请求的顺序来分配锁,即优先让最早请求锁的线程获得锁。
典型例子:ReentrantLock 的公平锁
ReentrantLock 提供了一个构造函数,可以指定是否为公平锁,默认为非公平锁(即锁的分配顺序是随机的)。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairLockExample {
private final Lock fairLock = new ReentrantLock(true); // true表示公平锁
public void method() {
fairLock.lock();
try {
// 执行操作
} finally {
fairLock.unlock();
}
}
}
优点:
-
避免饥饿问题:公平锁可以确保每个请求锁的线程都有机会获得锁。
-
适用于严格顺序要求的场景:例如,队列管理、并发任务调度等需要遵循请求顺序的场景。
缺点:
-
性能开销较大:维护一个队列来管理请求锁的线程,会带来一定的性能开销,尤其是在高并发情况下,公平锁可能比非公平锁的性能差。
-
潜在的线程切换:公平锁需要线程依次进入队列,可能会导致线程频繁的上下文切换,影响性能。
3. 中断锁(Interruptible Lock)
中断锁是指在等待获取锁的过程中,如果线程被中断,则能够响应并退出等待。这样,线程在获取锁时能够被外部中断,避免线程长时间阻塞。
具体原理:
-
在传统的
synchronized锁和非中断锁中,线程一旦开始等待锁,就会一直阻塞,直到获得锁或者超时。中断锁允许线程在等待锁的过程中响应中断信号,从而避免线程因获取锁而被无限期阻塞。 -
ReentrantLock提供了一个方法lockInterruptibly(),允许线程在等待锁的过程中被中断。
典型例子:ReentrantLock 的中断锁
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class InterruptibleLockExample {
private final Lock lock = new ReentrantLock();
public void method() {
try {
lock.lockInterruptibly(); // 可中断地获取锁
try {
// 执行操作
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 响应中断
// 处理中断的逻辑
}
}
}
优点:
-
提高响应性:线程在等待锁时能够响应中断,适用于需要高响应性的场景,例如处理任务中断、优先级调度等。
-
避免死锁:如果一个线程正在等待锁时,另一个线程可能会发送中断信号,帮助及时释放资源,避免死锁。
缺点:
-
增加代码复杂性:需要捕获和处理
InterruptedException异常,可能会增加代码的复杂性和错误处理。 -
可能的任务放弃:线程中断后可能会放弃任务,如果不正确处理中断逻辑,可能会导致部分任务丢失或不执行。
总结:
-
可重入锁:允许同一线程多次获取同一把锁,避免死锁和递归调用的阻塞。
-
公平锁:保证线程按请求顺序获得锁,避免线程饥饿,但可能带来性能开销。
-
中断锁:允许线程在等待锁时响应中断,提高程序的灵活性和响应性,适用于需要快速响应的场景。
什么是AQS锁?
AQS (Abstract Queued Synchronizer) 是 Java 并发包 java.util.concurrent 中的一个框架,用于构建锁和其他同步原语。AQS 提供了一种基于 队列 的同步机制,允许开发者轻松实现自定义同步工具,如 独占锁、共享锁、读写锁 等。
AQS 的基本原理
AQS 是一个 抽象类,其核心思想是通过一个 FIFO 队列 来管理线程的请求和同步状态。它利用一个 状态变量 来控制线程是否可以继续执行,状态变量通常是一个整数,表示同步器的当前状态。
AQS 提供了 独占模式(即一次只有一个线程能获得锁)和 共享模式(即多个线程可以共享某些资源)的实现。常见的同步工具,如 ReentrantLock、CountDownLatch、Semaphore 等,都是基于 AQS 实现的。
AQS 的关键构件
-
同步队列(FIFO 队列):
-
AQS 维护了一个 等待队列,当多个线程尝试获取同步器时,如果同步器已经被占用,线程就会被放入队列中等待。
-
线程请求锁时,首先进入队列,然后通过 AQS 提供的控制逻辑来判断是否可以获得锁。如果不能,则等待;一旦锁被释放,队列中的下一个线程就会获得锁。
-
-
状态变量:
-
AQS 通过一个
int型的 state 变量来表示同步器的状态。状态的值通常表示当前锁的占用情况。 -
对于 独占模式(如
ReentrantLock),state表示锁是否被持有以及持有锁的线程数。 -
对于 共享模式(如
Semaphore),state可以表示可用的资源数量。
-
-
阻塞队列:
-
线程在无法获取同步器时会被阻塞,并加入到一个 阻塞队列 中。
-
线程被唤醒时,AQS 会根据同步状态判断是否允许线程继续执行。
-
AQS 主要方法
AQS 提供了几个关键的方法,供子类(如 ReentrantLock)实现具体的同步逻辑:
-
acquire(int arg)和release(int arg):这些方法是 独占锁 的核心方法,用来获取和释放锁。-
acquire(int arg):尝试获取锁,如果当前无法获得锁,则当前线程进入等待队列,直到锁被释放。 -
release(int arg):释放锁,如果有线程在等待队列中,则唤醒下一个线程。
-
-
tryAcquire(int arg)和tryRelease(int arg):这些方法用于尝试获取和释放锁,子类可以实现这些方法来控制锁的具体获取和释放逻辑。 -
acquireShared(int arg)和releaseShared(int arg):这些方法是 共享锁 的核心方法,用来获取和释放共享资源(如读写锁中的读取锁)。-
acquireShared(int arg):尝试获取共享资源,如果无法获取,当前线程会进入等待队列。 -
releaseShared(int arg):释放共享资源,并唤醒等待队列中的线程。
-
-
tryAcquireShared(int arg)和tryReleaseShared(int arg):这些方法用于尝试获取和释放共享资源。 -
getState()和setState(int newState):用于获取和设置同步器的状态值。状态值的具体含义取决于子类的实现。
AQS 的实现模式
AQS 可以通过两种模式来实现同步:
-
独占模式(Exclusive Mode):
-
在独占模式下,只有一个线程可以持有锁,其他线程必须等待。
-
例如
ReentrantLock就是一个典型的独占锁,它允许线程获取锁后独占执行,其他线程必须等待锁被释放。
-
-
共享模式(Shared Mode):
-
在共享模式下,多个线程可以共享锁或资源,只有在所有线程都释放资源时,才会允许其他线程访问。
-
例如
Semaphore和CountDownLatch就是基于共享模式实现的,同一时刻可以有多个线程访问共享资源。
-
AQS 的应用实例
以下是几个基于 AQS 实现的常见同步工具:
-
ReentrantLock:-
ReentrantLock是 AQS 的一个典型应用,使用 AQS 的独占模式来实现锁的获取与释放。 -
它提供了 公平锁 和 非公平锁 选项,通过 AQS 的队列来管理等待的线程。
-
-
Semaphore:-
Semaphore是一个基于共享模式的工具,表示可用的资源数量。它允许多个线程共享对有限资源的访问,每次请求资源时,线程会等待直到有足够的资源可用。
-
-
CountDownLatch:-
CountDownLatch是一个计数器,当计数器的值减至零时,所有等待的线程将被释放。它也是基于 AQS 的共享模式实现的。
-
-
CyclicBarrier:-
CyclicBarrier用于将一组线程同步到同一个点,在所有线程到达屏障点时,线程才会继续执行。它通过 AQS 的队列来管理等待的线程,直到所有线程都到达屏障。
-
AQS 的优势
-
高效性:通过队列管理线程,可以减少上下文切换和竞争。
-
灵活性:AQS 支持自定义同步工具的实现,能够处理各种复杂的同步需求。
-
扩展性:由于 AQS 是一个抽象类,它可以被子类继承并实现具体的同步逻辑,非常适合开发各种同步工具。
AQS 的设计缺点
-
复杂性:AQS 提供了非常灵活的同步机制,但也使得开发者在使用时需要对其工作原理有深入的了解,避免错误的使用方式。
-
死锁风险:和其他同步工具一样,使用 AQS 构建的同步工具如果没有正确管理锁的获取和释放,可能会引发死锁。
总结
AQS 是 Java 并发工具包中的一个重要框架,它通过队列和状态管理为开发者提供了一个灵活、高效的同步机制。通过继承 AQS,开发者可以轻松实现多种复杂的同步工具,如锁、信号量、计数器等。理解 AQS 的工作原理和正确使用它可以大大提升并发编程的能力。
详细介绍一下常见的AQS锁
AQS(Abstract Queued Synchronizer,抽象队列同步器)是Java中用于实现锁机制和同步器的一种工具类,位于java.util.concurrent.locks包下。它为构建锁提供了一种通用框架,特别适用于实现可重入锁、共享锁、读写锁等复杂同步器。AQS的核心思想是通过一个FIFO队列管理线程的排队状态。接下来我将详细介绍一些常见的基于AQS的锁实现。
1. ReentrantLock(可重入锁)
ReentrantLock是最常见的基于AQS实现的独占锁。它允许一个线程多次获取锁,并确保不会发生死锁。每当线程释放锁时,锁的计数会减一,直到计数为零时,锁才会真正释放,允许其他线程获取锁。
-
特点:
-
支持公平锁和非公平锁。公平锁会保证线程按照请求锁的顺序获取锁,而非公平锁则可能让后请求的线程优先获得锁。
-
可中断,支持在等待锁的过程中响应中断。
-
支持条件变量,可以通过
Condition类控制线程的等待和通知。
-
-
应用场景:
-
用于需要可中断、可重入的独占锁场景,尤其是在多线程环境下需要高度控制锁竞争时。
-
2. ReentrantReadWriteLock(读写锁)
ReentrantReadWriteLock是基于AQS的读写锁实现,允许多个线程同时读共享资源,但在写线程访问时,必须排他性地获得锁。该锁分为两个部分:一个是读锁,一个是写锁。写锁是独占的,获取写锁的线程会阻塞其他线程的读写操作;而读锁是共享的,多个线程可以同时获取读锁。
-
特点:
-
读锁:多个线程可以同时获得读锁,适用于读取操作比较频繁的场景。
-
写锁:写锁是独占的,一旦一个线程持有写锁,其他线程无法获取读锁或写锁。
-
也支持公平锁和非公平锁。
-
适用于读多写少的场景,能够提高并发性能。
-
-
应用场景:
-
读多写少的场景,像缓存、共享数据结构等。
-
3. CountDownLatch(倒计时器)
CountDownLatch是一个基于AQS的同步器,它通过一个计数器控制多个线程的协作。计数器的初始值通常是某个线程数目,每当一个线程完成某个操作时,计数器减一。当计数器为零时,其他线程才能继续执行。
-
特点:
-
一次性使用,一旦计数器归零,无法重置。
-
适用于某些事件的等待,比如等待一组线程完成任务后再执行某个操作。
-
-
应用场景:
-
用于并行计算的结果汇总,或等待多个线程完成某项任务之后执行后续操作。
-
4. CyclicBarrier(循环栅栏)
CyclicBarrier也是基于AQS的同步器,它允许一组线程互相等待,直到所有线程都达到某个屏障点。与CountDownLatch不同,CyclicBarrier允许线程在执行完毕后重新初始化计数器,能够多次使用。
-
特点:
-
允许线程在多次迭代中同步执行,每次达到屏障点时,所有线程都必须等待,之后一起继续执行。
-
可以设定一个
Runnable任务,在所有线程都到达屏障点后执行。
-
-
应用场景:
-
用于实现某些需要分阶段执行的任务(如并行计算中的多个阶段),或多个线程在相同阶段协调执行。
-
5. Semaphore(信号量)
Semaphore是基于AQS的一个计数信号量,用于控制对共享资源的访问。它允许多个线程同时访问一定数量的资源。信号量通过一个计数器来实现并发控制,每当一个线程获取信号量时,计数器减一,当计数器为零时,其他线程必须等待。
-
特点:
-
适用于限制对共享资源的访问数量,如数据库连接池、线程池等。
-
支持公平锁和非公平锁。
-
-
应用场景:
-
用于实现资源池或限制并发量的场景。
-
6. Exchanger(交换器)
Exchanger是一个基于AQS的同步工具,用于在两个线程之间交换数据。两个线程可以在exchange()方法处交换对象,直到两个线程都调用了该方法并交换了数据。
-
特点:
-
线程在调用
exchange()方法时会阻塞,直到两个线程都到达交换点。 -
用于线程之间交换信息的场景,尤其是生产者-消费者模型中。
-
-
应用场景:
-
适用于两个线程之间的协作,如线程间的生产者消费者模型。
-
总结
AQS提供了一个非常强大和灵活的框架,可以帮助我们实现各种同步工具。在实际应用中,常见的基于AQS的锁包括:
-
ReentrantLock:可重入的独占锁,适用于各种需要高并发控制的场景。
-
ReentrantReadWriteLock:读写锁,适合读多写少的场景。
-
CountDownLatch 和 CyclicBarrier:主要用于线程间协调。
-
Semaphore:用于控制对资源的并发访问。
-
Exchanger:用于线程间交换数据。
这些锁和同步工具可以根据不同的业务场景选择使用,合理的选择和应用能够显著提高程序的并发性能和可维护性。
2621

被折叠的 条评论
为什么被折叠?



