当前位置: 首页 > web >正文

Java多线程

死锁及其必要条件

死锁是指在多个进程(或线程)并发执行时,它们互相等待对方释放资源,导致这些进程(或线程)无法继续执行的情况。简单来说,死锁发生时,系统中的进程陷入了“相互等待”的状态,导致程序无法继续运行。

死锁的发生通常需要满足以下四个必要条件(称为死锁的四个必要条件):

  1. 互斥条件(Mutual Exclusion): 至少有一个资源是以排他方式分配的,即资源一次只能被一个进程占用。如果其他进程请求该资源,它们必须等待,直到占用资源的进程释放它。

  2. 持有并等待(Hold and Wait): 一个进程已经持有了至少一个资源,并且正在等待获取其他进程占用的资源。也就是说,进程在等待资源的同时并没有释放自己已经持有的资源。

  3. 不剥夺条件(No Preemption): 已经分配给进程的资源,不能被强制剥夺。也就是说,进程释放资源只能通过它自己主动释放,而不是由系统强制剥夺。

  4. 循环等待条件(Circular Wait): 存在一组进程,它们形成一个闭环,其中每个进程都在等待下一个进程持有的资源。例如,进程A等待进程B的资源,进程B等待进程C的资源,进程C又等待进程A的资源。

死锁的预防和解决

要避免死锁,可以采取一些措施,例如:

  • 避免互斥条件:对于某些资源,尽量允许多个进程并行访问,减少资源冲突。

  • 避免持有并等待:要求进程在开始执行前申请所有需要的资源,避免在执行过程中进行资源请求。

  • 避免循环等待:对资源分配进行排序或规定资源申请的顺序,确保进程之间不会形成循环等待。

如果死锁发生,常见的解决方法包括:

  • 终止进程:通过终止某些进程来打破死锁。

  • 资源剥夺:强制从某些进程中回收资源,以便解除死锁。

死锁是并发编程中的一个经典问题,通常在设计系统时需要特别注意资源的分配和管理。

synchronizedLock

synchronizedLock 都是用于在多线程环境中实现线程同步的工具,但它们在使用方式、特性和灵活性上有一些显著的不同。下面是这两者的主要区别:

1. 语法和使用方式

  • synchronized

    • synchronized 是一种 关键字,用于修饰方法或代码块。它通过 隐式地锁定一个对象 来保证线程安全。

    • 使用方式:

      • 修饰实例方法:synchronized 修饰一个实例方法时,锁的是该方法所属的实例对象。

      • 修饰静态方法:synchronized 修饰静态方法时,锁的是该类的 Class 对象。

      • 修饰代码块:指定一个特定的对象来进行加锁,锁定指定对象的监视器。

      public synchronized void someMethod() {// 被同步的代码
      }
      

      或者:

      public void someMethod() {synchronized (someObject) {// 被同步的代码}
      }
      
  • Lock

    • Lockjava.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 提供了 独占模式(即一次只有一个线程能获得锁)和 共享模式(即多个线程可以共享某些资源)的实现。常见的同步工具,如 ReentrantLockCountDownLatchSemaphore 等,都是基于 AQS 实现的。

AQS 的关键构件

  1. 同步队列(FIFO 队列)

    • AQS 维护了一个 等待队列,当多个线程尝试获取同步器时,如果同步器已经被占用,线程就会被放入队列中等待。

    • 线程请求锁时,首先进入队列,然后通过 AQS 提供的控制逻辑来判断是否可以获得锁。如果不能,则等待;一旦锁被释放,队列中的下一个线程就会获得锁。

  2. 状态变量

    • AQS 通过一个 int 型的 state 变量来表示同步器的状态。状态的值通常表示当前锁的占用情况。

    • 对于 独占模式(如 ReentrantLock),state 表示锁是否被持有以及持有锁的线程数。

    • 对于 共享模式(如 Semaphore),state 可以表示可用的资源数量。

  3. 阻塞队列

    • 线程在无法获取同步器时会被阻塞,并加入到一个 阻塞队列 中。

    • 线程被唤醒时,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 可以通过两种模式来实现同步:

  1. 独占模式(Exclusive Mode)

    • 在独占模式下,只有一个线程可以持有锁,其他线程必须等待。

    • 例如 ReentrantLock 就是一个典型的独占锁,它允许线程获取锁后独占执行,其他线程必须等待锁被释放。

  2. 共享模式(Shared Mode)

    • 在共享模式下,多个线程可以共享锁或资源,只有在所有线程都释放资源时,才会允许其他线程访问。

    • 例如 SemaphoreCountDownLatch 就是基于共享模式实现的,同一时刻可以有多个线程访问共享资源。

AQS 的应用实例

以下是几个基于 AQS 实现的常见同步工具:

  1. ReentrantLock

    • ReentrantLock 是 AQS 的一个典型应用,使用 AQS 的独占模式来实现锁的获取与释放。

    • 它提供了 公平锁非公平锁 选项,通过 AQS 的队列来管理等待的线程。

  2. Semaphore

    • Semaphore 是一个基于共享模式的工具,表示可用的资源数量。它允许多个线程共享对有限资源的访问,每次请求资源时,线程会等待直到有足够的资源可用。

  3. CountDownLatch

    • CountDownLatch 是一个计数器,当计数器的值减至零时,所有等待的线程将被释放。它也是基于 AQS 的共享模式实现的。

  4. 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:读写锁,适合读多写少的场景。

  • CountDownLatchCyclicBarrier:主要用于线程间协调。

  • Semaphore:用于控制对资源的并发访问。

  • Exchanger:用于线程间交换数据。

这些锁和同步工具可以根据不同的业务场景选择使用,合理的选择和应用能够显著提高程序的并发性能和可维护性。

http://www.xdnf.cn/news/1935.html

相关文章:

  • 2.5 桥梁桥面系及附属结构施工
  • kafka课后总结
  • Spring @Transactional 自调用问题深度解析
  • 【Unity 与c++通信】Unity与c++通信注意事项,参数传递
  • websheet之 自定义函数
  • 成都种业博览会预登记火热进行中,6月8日-9日成都世纪城新国际会展中心与您不见不散!
  • [密码学实战]商用密码产品密钥体系架构:从服务器密码机到动态口令系统
  • vue前端SSE工具库|EventSource 替代方案推荐|PUSDN平行宇宙软件开发者网
  • 如何申请游戏支付平台通道接口?
  • PyTorch生成式人工智能实战(3)——分类任务详解
  • 施磊老师基于muduo网络库的集群聊天服务器(七)
  • 容器的网络类型
  • 视频噪点多,如何去除画面噪点?
  • 【基于Qt的QQMusic项目演示第一章】从界面交互到核心功能实现
  • 常见移动机器人底盘模型对比(附图)
  • Codeforces Round 1020 (Div. 3) A-D
  • 用diffusers库从单文件safetensor加载sdxl模型(离线)
  • 系统分析师-第九、十章
  • 蓝桥杯 3. 密码脱落
  • gradio 订单处理agent
  • 通过VSCode远程连接到CentOS7/Ubuntu18等老系统
  • 燃气经营从业人员有哪些类别
  • Doris vs ClickHouse:深入对比MPP数据库聚合操作的核心区别
  • Excel表格批量翻译对照翻译(使用AI助手)
  • ESG跨境电商如何为国内的跨境电商企业打开国外的市场
  • JDK 24:Java 24 中的新功能
  • SOC估算:开路电压修正的安时积分法
  • Doris表设计与分区策略:让海量数据管理更高效
  • 软测面经(私)
  • 分布式队列对消息语义的处理