【多线程】(六)Java并发编程深度解析:常见锁策略、CAS、synchronized原理、线程安全集合类和死锁详解

文章目录

  • 一、常见锁策略
    • 1.1 乐观锁和悲观锁
    • 1.2 读写锁
    • 1.3 重量级锁和轻量级锁
    • 1.4 自旋锁
    • 1.5 公平锁和非公平锁
    • 1.6 可重入锁和不可重入锁
  • 二、CAS
    • 2.1 什么是CAS
    • 2.2 CAS的实现原理
    • 2.3 CAS应用
    • 2.4 ABA问题
  • 三、synchronized原理
    • 3.1 synchronized锁的特点
    • 3.2 加锁工作过程
    • 3.3 锁消除和锁粗化
  • 四、JUC(java.util.concurrent)的常见类和接口
    • 4.1 ReentrantLock锁
    • 4.2 原子类
    • 4.3 线程池
    • 4.4 信号量SemaPhore
    • 4.5 Callable接口
    • 4.6 CountDownLatch
  • 五、线程安全的集合类
    • 5.1 多线程环境使用ArrayList
    • 5.2 多线程环境使用队列
    • 5.3 多线程环境使用哈希表Hashtable和ConcurrentHashMap
  • 六、死锁
    • 6.1 死锁的概念
    • 6.2 死锁的产生情况
    • 6.3 死锁的产生条件
    • 6.4 死锁的避免


一、常见锁策略

1.1 乐观锁和悲观锁

乐观锁:

假设多个线程之间的冲突是低概率事件,因此在读取数据时不会加锁,只有在更新数据时进行加锁和冲突检测。常用的乐观锁包括版本号控制和CAS(Compare 按到 Swap)操作。

悲观锁:

假设多个线程之间的冲突是高概率事件,因此在读取数据时也会加锁,其他线程需要等待锁的释放synchronized 初始使用乐观锁策略,当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略。而ReentrantLock一直使用悲观锁策略。

1.2 读写锁

在多线程环境中,多个线程同时读取数据不会产生线程安全问题;但是存在写入操作的时候,写入与读取、写入与写入这些操作之间都有可能引发线程安全问题。如果对这两个场景都进行加锁操作的话,势必会产生极大的性能损耗,尤其是存在大量的读操作情景下,所有就引入了读写锁。

读写锁允许多个线程同时读取共享数据,此时不会加锁;但是当存在写操作时,所有的线程都需要等待写锁的释放读写锁适用于读锁远远多于写锁的情况,可以提高并发性能。常见的读写锁实现有ReentrantReadWriteLock

总而言之,一个线程对于数据的访问,包括以下情况:

  • 多个线程读取同一个数据,此时没有线程安全问题,不需要加锁;
  • 多个线程对同一个数据进行读取和写入操作,存在线程安全问题,需要加锁;
  • 多个线程对同一个数据进行写操作,存在线程安全问题,需要加锁。

1.3 重量级锁和轻量级锁

重量级锁:
加锁机制重度依赖于操作系统提供的mutex,它会涉及线程的上下文切换以及内核态与用户态之间的切换,对系统性能的开销较大。重量级锁适用于多个线程竞争同一个锁的情况。

轻量级锁:
轻量级锁是为了在没有多个线程竞争的情况下提高性能而设计的锁机制。它使用了CAS操作来避免进程的上下文切换内核态与用户态之间的切换。对系统的开销较小。但是当多个线程竞争同一个锁时,轻量级锁会升级为重量级锁

1.4 自旋锁

自旋锁(Spin Lock)是一种基于忙等待的锁,一个线程在没有竞争到锁的时候不会立即阻塞,而是通过循环不断的尝试去获取锁。自旋锁适用于锁占用时间短,线程冲突率低的情况。

自旋锁伪代码:

while (抢锁(lock) == 失败) {}

自旋锁是一种典型的 轻量级锁 的实现方式。

  • 优点:没有放弃 CPU,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁。
  • 缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗 CPU 资源。(而挂起等待的时候是不消耗 CPU 的)

1.5 公平锁和非公平锁

公平锁:
公平锁是按照线程的申请顺序来获取锁的,即和排队一样先到先得。当一个线程释放锁的时候,等待时间最长的线程将获取锁的访问权。公平锁保证了锁的获取是按照线程的排队顺序来获取的。

非公平锁:
非公平锁不保证线程的获取顺序是按照线程的申请顺序进行的,它允许新来的线程在等待线程中插队,比如优先级队列。有可能导致先来的线程一直无法获取到锁,但是非公平锁相比于公平锁具有更高的吞吐量

总之,公平锁和非公平锁都有各自的优势和适用场景。公平锁可以避免饥饿问题,但会导致线程切换频繁,性能相对较低非公平锁在性能上具有优势,但可能会导致某些线程长时间无法获取到锁

1.6 可重入锁和不可重入锁

可重入锁:

可重入锁允许同一个线程多次获取同一个锁。如果一个线程已经获得了某个锁,那么它可以再次获取该锁,而不会被自己所持有的锁所阻塞。可重入锁可以避免死锁,并简化了编程模型

不可重入锁:

不可重入锁指一旦一个线程获得了该锁,再次尝试获取锁时会被阻塞。不可重入锁通常需要显式地释放锁才能再次获取它,否则会造成死锁

大多数锁机制都是可重入的,包括 synchronized 关键字和 ReentrantLock可重入锁提供了更便利的锁操作,并避免了某些潜在的编程错误。不可重入锁一般较少使用,因为它需要手动释放锁,并容易导致死锁问题。

二、CAS

2.1 什么是CAS

CAS(Compare and Swap)是一种并发控制机制,用于实现多线程环境下的原子操作。它是通过比较内存中的值与期望的值,如果相等则交换,否则不做任何操作CAS是一种乐观锁技术,不需要使用传统的锁机制来保证线程安全。

例如,假设内存中的原始数据为 A,旧的预期值为 B,需要修改的新值为 C:

  1. 比较 A 与 B 是否相等。(比较)
  2. 如果结果相等,则将 B 写入 A。(交换)
  3. 返回操作是否成功。

以下是一段 CAS 的伪代码:

需要注意的是,下面写的代码不是原子的,真实的 CAS 是一个原子的硬件指令完成的。这个伪代码只是辅助理解 CAS 的工作流程。

boolean CAS(address, expectedValue, swapValue) {if (&address == expectedValue) {  // 比较内存地址中的值与期望值&address = swapValue;  // 如果相等,则将交换值写入内存地址return true;  // 操作成功,返回 true}return false;  // 操作失败,返回 false
}
  • 这段代码描述了CAS的基本思想。它首先比较内存地址中的值与期望值,如果相等,则将交换值写入内存地址,并返回 true 表示操作成功。如果不相等,则不做任何操作,并返回 false 表示操作失败。

  • 当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。

2.2 CAS的实现原理

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来说:

  • Java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
  • unsafeCAS 依赖的是 JVM 针对不同的操作系统实现的 Atomic::cmpxchg(比较并交换);
  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 CPU 硬件提供的 lock 机制保证其原子性。

总而言之,CAS的实现原理是通过硬件和软件层面的配合来实现的。硬件提供了原子指令和锁机制,而软件层面的JVM使用了底层的CAS操作实现,依赖于处理器和操作系统提供的特性来保证CAS操作的原子性。

2.3 CAS应用

1. 实现原子类
标准库中提供了 java.util.concurrent.atomic 包,里面的类都是基于CAS这种方式来实现的。典型的就是 AtomicInteger 类,其中:

  • getAndIncrement 相当于 i++ 操作;
  • incrementAndGet 相当于 ++i 操作;
  • getAndDecrement 相当于 i-- 操作;
  • decrementAndGet 相当于 --i 操作。

例如,getAndIncrement的伪代码实现:

class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue + 1) != true) {oldValue = value;}return oldValue;}
}

2. 实现自旋锁

自旋锁是基于 CAS 实现的更灵活的锁,其伪代码如下:

public class SpinLock {private Thread owner = null;public void lock() {// 通过 CAS 看当前锁是否被某个线程持有.// 如果这个锁已经被别的线程持有, 那么就自旋等待.// 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock () {this.owner = null;}
}

2.4 ABA问题

1. 什么是ABA问题

ABA问题是CAS操作的一个潜在问题。ABA问题指的是,在CAS操作中,如果一个值原来是A,后来变成了B,然后又变回了A,那么CAS操作就可能会误判为成功。这是因为CAS只比较了值,并没有考虑过程中的变化。例如下面的情况:

假设存在两个线程 t1 和 t2,有一个共享变量 num,初始值为 A。

  1. 接下来,线程 t1 想使用 CAS 把 num 值改成 Z,那么就需要:

    • 先读取 num 的值,记录到 oldNum 变量中;
    • 然后使用 CAS 判定当前 num 的值是否为 A,如果为 A,就修改成 Z。
  2. 但是,在 t1 执行这两个操作之间,t2 线程把 num 的值从 A 改成了 B,又从 B 改成了 A。

  3. 到这一步,t1 线程无法区分当前这个变量始终是 A,还是经历了一个变化过程,但与oldNum比较的值是相等的,就进行了交换。


2. ABA问题引发的BUG

大部分的情况下,t2线程这样的一个反复横跳改动,对于 t1 是否修改 num 是没有影响的,但是不排除一些特殊情况

假如张三有100元存款,想从ATM机中取出50元,当按下取钱按钮的时候因为网络延迟,导致张三以为没有按成功,因此又按了一次按钮,此时就创建了两个线程,来并发的来执行取50元这个操作

在正常情况下,我们所期望的就是一个线程执行 -50 成功;而另一个线程执行 -50 失败。
如果使用 CAS 的方式来执行这个扣款过程就有可能出现问题。

正常过程:

  1. 存款为100元,线程1 获取到当前存款的值为100,期望更新为50;线程2 也获取到当前存款的值为100,期望更新为50。
  2. 线程1 使用CAS先执行扣款成功,存款被改成了50。
  3. 线程2 也使用CAS尝试扣款,发现此时的存款50与获取的旧值100不相等,因此执行失败。

异常过程:

  1. 存款为100元,线程1 获取到当前存款的值为100,期望更新为50;线程2 也获取到当前存款的值为100,期望更新为50。
  2. 线程1 使用CAS先执行扣款成功,存款被改成了50。
  3. 在 线程2 执行扣款操作之前,张三的朋友还钱给张三,向他的账户转了50元,此时张三的余额就又变成100元了。
  4. 线程2 尝试执行扣款操作,发现此时余额与刚才获取的旧值100相等,于是扣款50元,余额也变成50了。

在这个异常过程中,两次都扣款成功了,但是张三却只拿到了50元,另外50缺丢失了,这就是ABA问题所引发的BUG。

3. ABA问题的解决方式

给要修改的值,引入版本号。在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期。

  • CAS 操作在读取旧值的同时,也要读取版本号;
  • 真正修改的时候:
    • 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
    • 如果当前版本号高于读到的版本号,就操作失败(认为数据已经被修改过了)。

例如针对上面的场景:

假如张三有100元存款,想从ATM机中取出50元,当按下取钱按钮的时候因为网络延迟,导致张三以为没有按成功,因此又按了一次按钮,此时就创建了两个线程,来并发的来执行取50元这个操作

  1. 存款为100元,线程1 获取到当前存款的值为100,版本号为1,期望更新为50;线程2 也获取到当前存款的值为100,版本号为1,期望更新为50。
  2. 线程1 使用CAS先执行扣款成功,存款被改成了50,版本号修改为2。
  3. 在 线程2 执行扣款操作之前,张三的朋友还钱给张三,向他的账户转了50元,此时张三的余额就又变成100元了,版本号更新为3。
  4. 线程2 尝试执行扣款操作,发现此时余额与刚才获取的旧值100相等,但是旧版本号1与当前版本号3不相等,于是扣款失败。

三、synchronized原理

3.1 synchronized锁的特点

结合前文的锁策略,可以总结出synchronized 具有以下特性(只考虑 JDK 1.8):

  1. 乐观锁和悲观锁切换:在开始阶段,synchronized锁是乐观锁,它假设线程之间的冲突是低概率事件,不会立即阻塞其他线程。但如果发生锁冲突,synchronized锁会转换为悲观锁,即锁定对象并阻塞其他线程,以保证线程安全。

  2. 轻量级锁和重量级锁转换synchronized锁开始时使用轻量级锁实现,它通过CAS操作来避免线程的上下文切换和内核态与用户态之间的切换,对系统性能开销较小。然而,如果锁被持有的时间较长,轻量级锁会升级为重量级锁,这会涉及到线程的上下文切换和内核态与用户态之间的切换,对系统性能开销较大。

  3. 自旋锁策略:在实现轻量级锁的过程中,synchronized锁可能会使用自旋锁策略。自旋锁是一种基于忙等待的锁,线程在获取锁时不会立即阻塞,而是通过循环不断尝试获取锁,减少线程上下文切换的开销。

  4. 不公平锁synchronized锁是一种不公平锁,它不保证线程获取锁的顺序与线程的申请顺序一致。当多个线程同时竞争锁时,具有更高优先级的线程有较大概率先获取到锁。

  5. 可重入锁synchronized锁是一种可重入锁,也称为递归锁。可重入锁允许同一个线程多次获取同一个锁而不会被阻塞,避免了死锁的发生。同一个线程在持有锁的时候,可以多次进入由该锁保护的代码块。

需要注意的是,以上特点适用于JDK 1.8及之前的版本。在JDK 9及以后的版本中,synchronized锁的实现做了一些改进,例如引入了偏向锁、轻量级偏向锁等机制,以进一步提高性能和并发能力。

3.2 加锁工作过程

在Java中,synchronized锁的工作过程可以分为以下几个阶段,JVM会根据情况进行逐步升级:

  1. 无锁状态:开始时,对象的锁状态是无锁状态。多个线程可以并发地访问该对象,不需要进行加锁操作。

  2. 偏向锁状态:当一个线程第一次访问一个同步块时,JVM会将锁升级为偏向锁状态。此时,JVM会将线程ID记录在对象的锁记录(Lock Record)中,并标记为偏向锁。之后,该线程再次进入同步块时,不需要重新竞争锁,直接获取锁,提高了性能。

  3. 轻量级锁状态:如果一个线程尝试获取偏向锁时,发现锁记录的线程ID不是自己的,表示存在竞争。JVM会尝试将偏向锁升级为轻量级锁状态。在轻量级锁状态下,JVM会通过CAS(Compare and Swap)操作尝试获取锁,如果成功则进入临界区,如果失败则进入自旋等待状态。

  4. 重量级锁状态:如果在轻量级锁状态下,自旋等待仍然无法获得锁,JVM会将锁升级为重量级锁状态。在重量级锁状态下,JVM会使用操作系统的互斥量(Mutex)来实现锁的操作,即线程在获取锁时会进入阻塞状态,等待操作系统的调度。

需要注意的是,锁状态的升级是逐步发生的,并且是不可逆的。一旦锁升级到更高级别的状态,就不能再降级回低级别状态。锁的升级过程是由JVM自动管理的,开发人员不需要手动干预

3.3 锁消除和锁粗化

锁消除(Lock Elimination)和锁粗化(Lock Coarsening) 是针对synchronized锁的优化技术,旨在提高并发性能和减少锁开销。

  1. 锁消除(Lock Elimination):在某些情况下,JVM可以通过静态分析和逃逸分析等技术判断出一段代码中的锁实际上是不必要的,不会引发多线程竞争问题。在这种情况下,JVM可以消除这些不必要的锁操作,从而减少锁的开销。锁消除可以在一定程度上提高并发性能。

例如,当一个对象只被单个线程访问,并且不会逃逸到其他线程中时,JVM可以判断出该对象不会引发竞争,因此可以消除相应的锁操作。

  1. 锁粗化(Lock Coarsening)锁粗化是将多个连续的锁操作合并为一个更大的锁操作,以减少锁的粒度和开销。当JVM发现一段代码中多次获取和释放锁的操作是连续的,并且没有其他的干扰操作时,可以将这些锁操作合并为一个大的锁操作。这样可以减少获取和释放锁的次数,降低锁开销。

实际开发过程中,使用细粒度锁,是期望释放锁的时候其他线程能使用锁。但是实际上可能并没有其他线程来抢占这个锁,这种情况 JVM 就会自动把锁粗化,避免频繁申请、释放锁。

四、JUC(java.util.concurrent)的常见类和接口

4.1 ReentrantLock锁

ReentrantLocksynchronized一样,都是可重入锁,但是ReentrantLock的使用更加灵活可控,原因在于它可以手动的进行加锁和解锁操作

ReentrantLock 的用法:

  • lock():加锁,如果获取不到锁就一直阻塞等待;
  • trylock(timeout):加锁,如果获取不到锁,等待指定的一段时间之后就放弃加锁;
  • unlock(): 解锁,注意配合finally代码块使用,确保一定能够解锁。

例如:

ReentrantLock lock = new ReentrantLock();lock.lock(); 
try {  // working  
} finally {  lock.unlock()  
} 

ReentrantLocksynchronized 的区别:

  • synchronized 是一个关键字,是 JVM 内部实现的(基于 C++ 实现);ReentrantLock 是标准库的一个类,在 JVM 外实现的(基于 Java 实现)。
  • synchronized 使用时不需要手动释放锁;ReentrantLock 使用时需要手动释放,使用起来更灵活,但是也容易遗漏 unlock
  • synchronized 在申请锁失败时,会一直阻塞等待;ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃等待。
  • synchronized 是非公平锁;ReentrantLock 默认是非公平锁,但是可以通过构造方法传入一个 true 开启公平锁模式。
// ReentrantLock 的构造方法
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}
  • ReentrantLock 更强大的唤醒机制。synchronized 是通过 Objectwait / notify 实现等待-唤醒,每次唤醒的是一个随机等待的线程; ReentrantLock 搭配 Condition 类实现等待-唤醒,可以更精确控制唤醒某个指定的线程。

如何选择使用哪一个锁:

  • 锁竞争激烈程度低的时候,使用 synchronized,效率更高,自动释放更方便。
  • 锁竞争激烈的时候,使用 ReentrantLock,搭配 trylock 更灵活控制加锁的行为,而不是一直阻塞等待。
  • 如果需要使用公平锁的时候,选择使用 ReentrantLock

4.2 原子类

原子类内部使用CAS(Compare and Swap) 操作来实现线程安全的原子操作,相比于加锁实现的方式,原子类的性能通常更高

以下是常见的原子类:

原子类描述
AtomicBoolean提供原子更新boolean类型的操作。
AtomicInteger提供原子更新int类型的操作。
AtomicIntegerArray提供原子更新int数组类型的操作。
AtomicLong提供原子更新long类型的操作。
AtomicReference提供原子更新引用类型的操作。
AtomicStampedReference提供带有版本标识的原子更新引用类型的操作。

这些原子类提供了在多线程环境中进行原子操作的方法,可以避免线程之间的竞争和冲突,从而实现线程安全的数据访问和更新。使用这些原子类,我们可以避免使用显式锁和同步的开销,提高并发性能和线程安全性。

例如,对于AtomicInteger,常见的方法有:

方法描述
addAndGet(int delta)将当前值与给定的增量相加,并返回相加后的结果。
decrementAndGet()将当前值减1,并返回减1后的结果。
getAndDecrement()返回当前值,并将当前值减1。
incrementAndGet()将当前值加1,并返回加1后的结果。
getAndIncrement()返回当前值,并将当前值加1。
get()获取当前值。
set(int newValue)设置为给定的值。
getAndSet(int newValue)设置为给定的值,并返回旧值。
compareAndSet(expectedValue, newValue)如果当前值等于期望值,则将其设置为给定的新值,并返回设置是否成功。

4.3 线程池

关于线程池可见文章:【工厂模式和线程池】。

4.4 信号量SemaPhore

Semaphore是一个计数信号量,用于控制同时访问某个资源的线程数量。它可以指定允许同时访问的线程数,并提供了acquire()release()方法用于获取和释放信号量。Semaphore常用于限制并发线程数、控制资源访问和实现线程间的互斥和同步

下面是一个Semaphore的使用案例:

import java.util.concurrent.Semaphore;class MyThread extends Thread {private Semaphore semaphore;private String name;public MyThread(Semaphore semaphore, String name) {this.semaphore = semaphore;this.name = name;}public void run() {try {// 获取信号量许可semaphore.acquire();System.out.println("Thread " + name + " is accessing the resource.");// 模拟线程访问资源的耗时操作Thread.sleep(2000);System.out.println("Thread " + name + " has finished accessing the resource.");// 释放信号量许可semaphore.release();} catch (InterruptedException e) {e.printStackTrace();}}
}public class SemaphoreExample {public static void main(String[] args) {int numThreads = 5;Semaphore semaphore = new Semaphore(2); // 创建信号量,设置许可数为2for (int i = 1; i <= numThreads; i++) {Thread thread = new MyThread(semaphore, "Thread-" + i);thread.start();}}
}

在上面的例子中,我们创建了一个Semaphore对象,并设置许可数为2。然后创建了5个线程,每个线程在执行任务之前通过semaphore.acquire()获取信号量的许可,一旦获取到许可,就可以执行任务。在执行任务时,通过Thread.sleep模拟了线程访问资源的耗时操作。完成任务后,通过semaphore.release()释放信号量的许可。

由于许可数设置为2,所以最多允许两个线程同时访问资源,其他线程需要等待获取许可。通过Semaphore,我们可以控制并发访问的线程数量,实现对共享资源的合理调度和管理。

4.5 Callable接口

Callable 是一个接口,相当于给线程封装了一个 “返回值”,方便我们借助多线程的方式计算结果并返回给主线程。

例如:使用 Callable,创建线程计算 1 + 2 + 3 + … + 1000

  1. 创建一个匿名内部类,实现 Callable 接口。Callable 带有泛型参数,泛型参数表示返回值的类型。
  2. 重写 Callablecall 方法,完成累加的过程,直接通过返回值返回计算结果。
  3. callable 实例使用 FutureTask 包装。
  4. 创建线程,线程的构造方法传入 FutureTask。此时新线程就会执行 FutureTask 内部的 Callablecall 方法完成计算。计算结果就放到FutureTask 对象中。
  5. 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕,并获取到 FutureTask 中的结果。
public class ThreadDemo {// 使用 Callable 计算 1 + 2 + 3 + ... + 1000public static void main1(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 1; i <= 1000; i++) {sum += i;}return sum;}};FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();// get会进行阻塞,直到callable执行完毕Integer res = futureTask.get();System.out.println(res);}

CallableRunnable

  • CallableRunnable 相比,其功能都是对 “任务” 的描述Callable 描述的是带有返回值的任务,而 Runnable 描述的是不带返回值的任务。
  • Callable 通常需要搭配 FutureTask 来使用。FutureTask用来保存 Callable 的返回结果,因为Callable 往往是在另一个线程中执行的,什么时候执行完并不确定。而 FutureTask 就可以负责等待结果出来的工作。

关于 FutureTask

FutureTask是一个实现了RunnableFuture接口的类,它可以用来表示一个异步计算任务。FutureTask可以通过RunnableCallable对象来构造,并且可以获取计算结果或取消任务的执行。

下面是FutureTask的一些常用方法:

方法描述
FutureTask(Callable<V> callable)构造一个FutureTask对象,接受一个Callable对象作为参数。
FutureTask(Runnable runnable, V result)构造一个FutureTask对象,接受一个Runnable对象和一个结果值作为参数。
boolean cancel(boolean mayInterruptIfRunning)尝试取消任务的执行。如果任务已经开始执行或已经完成,则无法取消。mayInterruptIfRunning参数用于指定是否中断正在执行的任务。
boolean isCancelled()判断任务是否被取消。
boolean isDone()判断任务是否已经完成。
V get()获取任务的执行结果。如果任务还未完成,则会阻塞等待直到任务完成并返回结果。
V get(long timeout, TimeUnit unit)在指定的时间内获取任务的执行结果。如果任务在指定时间内未完成,则会抛出TimeoutException异常。

需要注意的是,get()方法会阻塞等待任务的执行结果,而get(long timeout, TimeUnit unit)方法可以设置等待的超时时间,避免无限等待。另外,cancel()方法可以尝试取消任务的执行,但并不保证一定成功,因为任务可能已经开始执行或已经完成。

4.6 CountDownLatch

CountDownLatch是Java并发包(java.util.concurrent)中的一个同步辅助类,它可以用于控制多个线程之间的同步

CountDownLatch通过一个计数器来实现同步,该计数器初始化为一个正整数,表示需要等待的线程数量。当一个线程完成了自己的任务后,可以调用countDown()方法来减少计数器的值。其他线程可以通过调用await()方法来等待计数器达到零,从而实现线程的阻塞。

下面是CountDownLatch的一些常用方法:

方法描述
CountDownLatch(int count)构造一个CountDownLatch对象,指定计数器的初始值。
void countDown()计数器减1。当计数器达到0时,释放所有等待的线程。
void await()阻塞当前线程,直到计数器达到0。
boolean await(long timeout, TimeUnit unit)阻塞当前线程,直到计数器达到0或等待超时。
long getCount()获取当前计数器的值。

下面是一个简单的示例代码,演示了CountDownLatch的使用:

import java.util.concurrent.CountDownLatch;// CountDownLatch的使用——同时等待 N 个任务执行结束.
public class ThreadDemo7 {public static void main(String[] args) throws Exception {CountDownLatch latch = new CountDownLatch(10);Runnable r = new Runnable() {@Overridepublic void run() {try {Thread.sleep(1000);latch.countDown();} catch (Exception e) {e.printStackTrace();}}};for (int i = 0; i < 10; i++) {new Thread(r).start();}// 必须等到 10 人全部回来latch.await();System.out.println("比赛结束");}
}

五、线程安全的集合类

5.1 多线程环境使用ArrayList

在多线程环境中使用ArrayList是不安全的,因为ArrayList不是线程安全的数据结构。多个线程同时对同一个ArrayList进行读写操作可能会导致数据不一致或抛出异常。

如果需要在多线程环境中使用类似ArrayList的功能,可以考虑使用线程安全的替代类,如CopyOnWriteArrayList

  • CopyOnWrite容器即写时复制的容器。
  • 当往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素;
  • 添加完元素之后,再将原容器的引用指向新的容器。

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

优点:

  • 在读多写少的场景下,性能很高,不需要加锁竞争。

缺点:

  • 占用内存较多。
  • 新写入的数据不能第一时间读取到。

5.2 多线程环境使用队列

在多线程环境中,可以使用线程安全的队列类来实现线程安全的操作。常见的线程安全队列有BlockingQueue接口的实现类,如LinkedBlockingQueueArrayBlockingQueue等。

这些队列类提供了线程安全的操作方法,可以实现多线程之间的安全数据交换。例如,一个线程可以将数据添加到队列中,而另一个线程可以从队列中获取数据,而无需担心数据竞争或并发访问的问题。

5.3 多线程环境使用哈希表Hashtable和ConcurrentHashMap

HashMap 本身不是线程安全的,因此在多线程环境下,哈希表可以使用:

  1. Hashtable
  2. ConcurrentHashMap

1. Hashtable
Hashtable是Java早期提供的线程安全的哈希表实现类。它通过加锁来保证多线程环境下的安全访问

Hashtable的线程安全是通过对所有方法进行同步来实现的,这意味着同一时间只能有一个线程对Hashtable进行读写操作。虽然它提供了线程安全,但是在高并发环境下,由于所有线程都需要竞争同一把锁,可能会导致性能瓶颈

2. ConcurrentHashMap
ConcurrentHashMap是Java提供的线程安全的哈希表实现类,它是对Hashtable的改进和扩展,以 Java1.8 为例:

  • 读操作没有加锁(但使用了 volatile 保证从内存读取结果),只对写操作进行加锁。加锁的方式仍然是是用 synchronized,但是不是对整个对象加锁,而是 "锁桶" (用每个链表的头结点作为锁对象),大大降低了锁冲突的概率。
  • 充分利用 CAS 特性。比如 size 属性通过 CAS 来更新,避免出现重量级锁的情况。
  • 优化扩容方式,一句话概括即:化整为零
    • 具体来说,当需要扩容时,ConcurrentHashMap会创建一个新的数组,并且只搬迁一部分元素到新数组中。在扩容期间,新老数组同时存在,即新数组中的部分槽位存放新添加的元素,而其他槽位仍然保留老数组中的元素。

    • 后续每个对ConcurrentHashMap进行操作的线程都会参与搬迁元素的过程。每个操作负责搬运一小部分元素,将其从老数组搬迁到新数组中。这样,在整个扩容过程中,多个线程并发地进行搬迁操作,加快了扩容的速度。

    • 在搬迁过程中,新元素的插入只会发生在新数组中,而对于查找操作,需要同时查找新数组和老数组,以确保能够找到所有的元素。

    • 当搬迁完成后,最后一个元素被搬迁到新数组中,老数组就可以被删除掉了,此时只需要操作新数组即可。

通过采用"化整为零"的扩容方式,ConcurrentHashMap在扩容过程中可以实现更高的并发度,减少了对全局锁的竞争,提高了并发性能。同时,该方式也保证了在扩容期间,对ConcurrentHashMap的读写操作可以继续进行,不会造成阻塞。

六、死锁

6.1 死锁的概念

死锁是指在多线程环境下,两个或多个线程互相持有对方所需的资源,导致所有参与的线程都无法继续执行,进入一种无法解除的等待状态。也可以说,死锁是指多个线程因相互竞争资源而导致的互相等待的现象

6.2 死锁的产生情况

死锁的产生情况通常涉及多个线程以不同的顺序申请资源,并且相互之间保持着对方所需资源的锁定。当所有线程都无法满足对方的资源需求时,就会发生死锁。

一种常见的死锁情况是多个线程同时持有某些资源,并且每个线程都在等待其他线程释放它所需的资源。这种情况下,没有一个线程能够继续执行下去,从而导致死锁的发生。

6.3 死锁的产生条件

死锁通常需要满足以下四个条件才能发生:

  1. 互斥条件:资源只能同时被一个线程占用,其他线程必须等待释放。
  2. 请求与保持条件:一个线程已经持有了至少一个资源,同时还在请求其他资源。
  3. 不可剥夺条件:已经分配给线程的资源不能被强制性地剥夺。
  4. 循环等待条件:存在一个线程的资源申请序列,使得每个线程都在等待下一个线程所持有的资源。

当以上四个条件同时满足时,就会发生死锁。

6.4 死锁的避免

避免死锁是一个重要的多线程编程问题。以下是一些常用的避免死锁的策略:

  1. 破坏循环等待条件:通过对资源进行排序,使线程按照固定的顺序请求资源,避免循环依赖
  2. 破坏互斥条件:对于某些资源,可以允许多个线程同时访问,从而避免资源互斥。
  3. 破坏请求与保持条件:线程在申请资源时,一次性申请所需的所有资源,而不是逐个申请。
  4. 破坏不可剥夺条件:允许线程在持有资源的同时,释放一些不必要的资源。
  5. 使用超时机制:设置一个超时时间,在等待一段时间后,如果无法获取到所需的资源,就放弃或尝试其他策略。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/5072.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

自动驾驶多任务框架 MultiTask V3、HybridNets和YOLOP比较

目标检测和分割是自动驾驶汽车感知系统的两个核心模块。它们应该具有高效率和低延迟,同时降低计算复杂性。目前,最常用的算法是基于深度神经网络的,这保证了高效率,但需要高性能的计算平台。 在自动驾驶汽车的场景下,大多使用的都是计算能力有限的嵌入式平台,这使得难以满…

力扣 332. 重新安排行程

一、题目描述 给你一份航线列表 tickets&#xff0c;其中 tickets[i] [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。 所有这些机票都属于一个从 JFK&#xff08;肯尼迪国际机场&#xff09;出发的先生&#xff0c;所以该行程必须从 JFK 开始。…

计算机网络基础-OSI七层模型 和 TCP/IP四层模型的对比

OSI七层模型 和 TCP/IP四层模型的对比 OSI七层模型&#xff1a; 理论上的网络通信模型 记忆&#xff1a; (物、链、网、输、会、示、用) TCP/IP四层模型&#xff1a; 实际上的网络通信标准 (1) 七层网络体系结构各层的主要功能&#xff1a; 应用层&#xff1a; 最上层的&am…

Nginx配置整合:基本概念、命令、反向代理、负载均衡、动静分离、高可用

一、基本概念 1.什么是Nginx Nginx是一个高性能的HTTP和反向代理服务器&#xff0c;也是一个IMAP/POP3/SMTP代理server。其特点是占有内存少。并发能力强&#xff0c;其并发能力确实在同类型的网页server中表现较好。 http服务器 Web服务器是指驻留于因特网上某种类型计算机的程…

请问支付功能如何测试

目录 01测试思维 02支付功能的测试点 一&#xff1a;支付的分类&#xff1a; 二&#xff1a;功能测试 三&#xff1a;接口测试 四&#xff1a;安全测试 五&#xff1a;测试点 01测试思维 要分析测试点之前&#xff0c;我们先来梳理一下测试思维。总结来说&#xff0c;任…

IP首部报文字段

一、IP首部报文字段 字段如下图所示 二、每个字段的含义 版本 表示 IP 协议的版本。通信双方使用的 IP 协议版本必须一致。目前广泛使用的IP协议版本号为 4&#xff0c;即 IPv4 首部长度 这个字段所表示数的单位是 32 位字长&#xff08;1 个 32 位字长是 4 字节&#xff0…

分布式事务 Seata

分布式事务 Seata 事务介绍分布式理论Seata 介绍Seata 部署与集成Seata TC Server 部署微服务集成 Seata XA 模式AT 模式AT 模式执行过程读写隔离写隔离读隔离 实现 AT 模式 TCC 模式TCC 模式介绍实现 TCC 模式 Saga 模式Seata 四种模式对比 事务介绍 事务&#xff08;Transac…

uniapp引入echarts

作为前端在开发需求的时候经常会遇到将数据展示为图表的需求&#xff0c;之前一直用的HBuilder的图表插件uCharts&#xff0c;使用方法可以参考我的另一篇博客&#xff1a;uniapp 中使用图表&#xff08;秋云uCharts图表组件&#xff09; 但是最近发现uCharts很多功能都需要付…

Bard:一个可以描述图像的人工智能

Bard 是一个大型语言模型&#xff0c;可以对各种提示和问题进行交流和生成类似人类的文本。它接受了大量的文字和代码训练&#xff0c;可以生成文本、翻译语言、编写不同类型的创意内容&#xff0c;并以信息丰富的方式回答你的问题。 Bard 还可以识别图像。它可以识别图像中的…

数组的递归筛选

数组递归筛选 根据一个值筛选出来通过 includes 递归 const options [{name: "ikun",options: [{name: "YAY11",},],},{name: "YAY",}, ];function findValue(orgOptions,val) {let newArr1 []orgOptions.forEach(item>{if(item.options…

ASUS华硕无双15_K3502ZA工厂模式原装Win11恢复原厂OEM预装系统 带ASUS Recovey恢复功能

ASUS华硕无双15笔记本电脑12代Vivobook_ASUSLaptop K3502ZA出厂Windows11系统工厂包 自带恢复功能、所有驱动、出厂主题壁纸LOGO、Office办公软件、MyASUS等预装程序 所需要工具&#xff1a;32G或以上的U盘 文件格式&#xff1a;HDI,SWP,OFS,EDN,KIT,TLK多个底包 文件大小&…

leetcode 40. 组合总和 II

2023.7.19 此题为 组合总和 的升级版。本题的特殊之处在于 给定的candidates数组只一个无序且包含重复元素的数组&#xff0c;并且最终的解集不能包含重复的组合。 所以本题的关键在于去重。那么&#xff0c;此类题的去重分为两种&#xff0c;一种是解集内部去重&#xff0c;灵…

Spring Boot学习

Spring Boot 配置 同一目录下配置文件优先级&#xff1a;.properties > .yml > .yaml 错误&#xff1a;org.yaml.snakeyaml.error.YAMLException: java.nio.charset.MalformedInputException: Input length 1 Caused by: java.nio.charset.MalformedInputException: Inp…

VUE- 选取本地图片,自定义裁切图片比例 vue-cropper

裁切图片&#xff0c;按照比例裁切&#xff0c;分步骤 1&#xff1a;el-upload选择本地图片&#xff08;分选择本地和上传两步骤&#xff09; 2&#xff1a;在on-change回调方法中拿到el-upload选中的图片&#xff0c;显示在vueCropper上&#xff08;&#xff09;。 2.1&…

查看IP地址方法(电脑IP地址方法)

查看IP地址方法 如何识别win7还是win10系统&#xff1f; &#xff08;一&#xff09;Win7系统电脑导航栏如下&#xff1a; &#xff08;二&#xff09;Win10系统电脑导航栏如下&#xff1a; 一、win7系统查看IP地址 方法一&#xff1a;查看网络设置 点击电脑导航栏最右下…

react和vue2/3父子组件的双向绑定(sync、emit、v-model)

目录 Vue .sync&#xff08;2.3.0&#xff09; $emit &#xff08;2.3后&#xff09; 自定义组件的 v-model 2.2.0 v-modelemits(3.0取消了.sync) React 父组件回调函数 相关基础 框架 MVC &#xff08;Model View Controller&#xff09;/MVP&#xff08;Model View…

STM32 Proteus仿真可设置时间红绿灯-0075

STM32 Proteus仿真可设置时间红绿灯-0075 Proteus仿真小实验&#xff1a; STM32 Proteus仿真可设置时间红绿灯-0075 功能&#xff1a; 硬件组成&#xff1a;STM32F103C6单片机 74HC595串入并出芯片4个2位数码管显示十字路口红绿灯时间多个按键 1.包含机动车指示灯(红、黄、…

windows安装mysql8.0.23版本成功示例-免安装

windows安装mysql8.0.23版本成功示例 一.通过mysql-installer-*.msi安装包的方式1.安装准备1.1 官网下载地址1.2 选择合适的版本点击下载 2.安装mysql 二.通过mysql-8.0.23-winx64.zip免安装方式-推荐1.安装准备1.1 下载官网压缩包1.2 解压后配置文件my.ini信息1.3 配置my.ini …

STM32MP157驱动开发——按键驱动(查询方式)

文章目录 概述APP 读取按键的 4 种方法查询方式休眠-唤醒方式poll 方式异步通知方式 查询方式的按键驱动程序&#xff08;框架&#xff09;按键驱动编写思路board_xxx.cbutton_drv.cbutton_drv.hbutton_test.cMakefile编译测试 查询方式的按键驱动程序(stm32mp157)board_stm32m…

浅谈测试工程化 - 以并发自动化框架为例

目录 前言 测试工程化 一、测试需求分析 二、测试设计 三、测试实现和落地 四、测试维护 扩展 前言 测试工程化是指将软件测试过程中的各个环节进行自动化和标准化&#xff0c;以提高测试效率、质量和可持续性。在测试工程化中&#xff0c;使用并发自动化框架是一个重要…