文章目录
- Java多线程进阶:死锁与面试题解析
- 一、并发编程的噩梦——死锁
- 1. 什么是死锁?四个缺一不可的条件
- 2. 如何避免死锁?从破坏循环等待开始
- 二、并发编程面试题全景解析
- 1. 锁与同步机制
- 2. CAS 与原子操作
- 3. JUC 工具与线程池
- 4. 线程安全集合
- 5. 综合问题
- 本文核心要点总结 (Key Takeaways)
Java多线程进阶:死锁与面试题解析
学到这里,我们总算来到了 Java 多线程学习之旅的最后一站。在前面的笔记里,我们一起探索了锁策略、synchronized
的底层细节、JUC 工具包以及各种并发集合。现在,是时候挑战两个“终极 BOSS”了:
- 死锁:并发编程中最让人头疼的噩梦。
- 面试题:检验我们学习成果的试金石。
这篇笔记的目标很明确:首先,彻底搞懂死锁的成因和避免策略;然后,整理一份相对全面的并发面试题库。这既是对整个系列知识的最终巩固,也希望能帮助自己(以及其他可能读到这篇笔记的朋友)更自信地应对面试的挑战。
一、并发编程的噩梦——死锁
1. 什么是死锁?四个缺一不可的条件
死锁,简单来说,就是多个线程同时被阻塞,它们中的一个或全部都在等待对方持有的资源。因为大家都在等对方先放手,结果就导致所有线程都被无限期地阻塞,程序也就卡住无法正常结束了。
这里我的理解是,可以用一个“吃饺子”的场景来帮助消化这个概念:
滑稽老哥和不吃香菜一起去吃饺子,但桌上只有一瓶酱油和一瓶醋。
- 滑稽老哥手快,先拿起了酱油瓶(这相当于持有了锁 A)。
- 与此同时,不吃香菜拿起了醋瓶(持有了锁 B)。
接下来,滑稽老哥想蘸醋(请求锁 B),而不吃香菜想用酱油(请求锁 A)。如果两人谁也不肯先把自己的瓶子放下给对方用,那就僵持住了——这就是一个典型的死锁。
从这个例子可以总结出,死锁的产生必须同时满足以下四个缺一不可的条件:
- 互斥使用:一个资源一次只能被一个线程使用(酱油瓶一次只能一人拿)。
- 不可抢占:线程不能强行从另一个线程手中夺取资源,只能等待其主动释放(不能从对方手里抢瓶子)。
- 请求和保持:线程在持有至少一个资源的同时,又去请求其他资源(拿着酱油瓶,又想要醋瓶)。
- 循环等待:存在一个线程等待链,T1等T2,T2等T3,…,Tn等T1,形成环路(滑稽老哥等不吃香菜,不吃香菜又在等滑稽老哥)。
2. 如何避免死锁?从破坏循环等待开始
要避免死锁,理论上只需破坏上述四个条件中的任意一个即可。但在实际编程中,我们最容易、也最常入手的是破坏“循环等待”条件。
最常用的一种死锁预防技术就是锁排序。这个方法思路很简单:假设有 N 个线程尝试获取 M 把锁, 我们可以对这 M 把锁进行统一编号 (例如根据它们的 hashCode
)。然后定下规矩,所有线程在需要获取多把锁时,都必须严格按照固定的、从小到大的编号顺序来获取。这样一来,请求锁的方向都是一致的,就从根本上避免了形成等待环路。
可能产生死lock的代码:
下面这个例子很典型,两个线程以相反的顺序申请锁,就可能导致死锁。
Object lock1 = new Object();
Object lock2 = new Object();// 线程1: 尝试先锁 lock1,再锁 lock2
new Thread(() -> {synchronized (lock1) {System.out.println("线程1 持有 lock1, 尝试获取 lock2...");try { Thread.sleep(100); } catch (InterruptedException e) {}synchronized (lock2) {System.out.println("线程1 成功获取两把锁");}}
}).start();// 线程2: 尝试先锁 lock2,再锁 lock1 (顺序相反,非常危险!)
new Thread(() -> {synchronized (lock2) {System.out.println("线程2 持有 lock2, 尝试获取 lock1...");synchronized (lock1) {System.out.println("线程2 成功获取两把锁");}}
}).start();
破坏循环等待后的安全代码:
只要我们约定好,所有线程都先获取 lock1
, 再获取 lock2
,问题就解决了。
Object lock1 = new Object();
Object lock2 = new Object();// 两个线程都遵循先锁1再锁2的顺序
new Thread(() -> {synchronized (lock1) {synchronized (lock2) {System.out.println("线程1 成功");}}
}).start();new Thread(() -> {synchronized (lock1) {synchronized (lock2) {System.out.println("线程2 成功");}}
}).start();
二、并发编程面试题全景解析
这部分内容既是对整个多线程系列学习的复习,也是对常见面试问题的检验。
1. 锁与同步机制
1. 如何理解乐观锁和悲观锁?具体如何实现?
- 悲观锁:我的理解是,它非常“悲观”,总是假设会发生并发冲突。所以,在每次对数据进行操作前,它都会先加锁,确保在自己操作的这段时间里,别人碰不了数据。这种方式的实现,通常依赖于底层的同步机制,比如 Java 中的
synchronized
关键字和ReentrantLock
类。 - 乐观锁:它则非常“乐观”,总是假设不会发生并发冲突。所以,它操作数据时不加锁,而是在准备提交更新的时候,才去检查数据在操作期间有没有被其他线程修改过。如果没被改过,就成功更新;如果被改了,就放弃或者重试。它的典型实现是 CAS(Compare-and-Swap)机制,为了防止 ABA 问题,通常还会配合一个版本号字段来一起检查。
2. 介绍一下读写锁?
读写锁(ReadWriteLock
)是一种很巧妙的锁,它把锁的功能一分为二:分为“读锁”和“写锁”,特别适合用在“读多写少”的场景。
- 读锁 vs 读锁:不互斥。大家都是读数据,不会有冲突,所以允许多个线程同时持有读锁,并发读取。
- 写锁 vs 写锁:互斥。为了保证数据写入的原子性,一次只能有一个线程持有写锁。
- 读锁 vs 写锁:互斥。当有线程在写数据时,其他线程不能读,反之亦然。这保证了读线程不会读到修改了一半的“脏数据”。
Java 中的ReentrantReadWriteLock
就是它的标准实现。
3. 什么是自旋锁?为什么要使用它,缺点是什么?
- 什么是自旋锁:当一个线程想获取锁但发现锁被占用了,它不会立刻放弃 CPU 进入阻塞状态,而是在原地进行一个忙等待的循环(“自旋”),不断地尝试获取锁。
- 为什么使用:它基于一个假设——锁被占用的时间通常很短。如果这个假设成立,那么通过自旋等待,线程可以避免进入和退出阻塞状态的巨大开销(这涉及到用户态和内核态的切换以及线程调度),一旦锁被释放,就能立刻抢到,响应速度更快。
- 缺点:如果锁被占用的时间很长,自旋的线程就会持续空转,白白浪费 CPU 资源。所以它是个双刃剑,用对地方才高效。
4. synchronized
是可重入锁吗?
是的,必须是。可重入锁(也叫递归锁)指的是,同一个线程在已经持有锁的情况下,可以再次成功获取该锁,而不会自己把自己锁死。
它的内部实现机制大致是:锁本身会记录下当前是哪个线程持有着它,并且还有一个计数器。当这个线程再次请求这把锁时,计数器会递增。每次执行完一次同步代码块释放锁时,计数器会递减。只有当计数器减到 0 时,这个锁才会被真正释放,其他线程才能获取。
5. 什么是偏向锁?
偏向锁是 JVM 对 synchronized
的一种极致优化。它的核心思想是,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。
为了处理这种情况,偏向锁并不会真的加锁,而是在对象头里记录一个“偏向”的线程 ID。如果后续访问该锁的始终是这一个线程,那么它进出同步块时就几乎没有任何开销,连 CAS 操作都不需要。这大大降低了无竞争情况下的同步成本。当然,一旦有其他线程参与竞争,偏向锁状态就会被撤销,升级为轻量级锁。
6. synchronized
的实现原理是什么?
synchronized
的实现核心是一个从偏向锁、轻量级锁到重量级锁的锁升级过程,这是 JVM 为了在不同竞争情况下都能有较好性能而做的优化。
- 偏向锁:当没有竞争时,锁会“偏向”于第一个获取它的线程。此时,仅在对象头记录线程ID,开销最低。
- 轻量级锁:当出现另一个线程竞争时,偏向锁会升级为轻量级锁。这时,线程会通过自旋+CAS的方式尝试获取锁,避免了线程阻塞带来的开销。
- 重量级锁:如果自旋一定次数后,竞争依然激烈,锁就会“膨胀”为重量级锁。此时它会依赖操作系统的
mutex
互斥量来实现。获取不到锁的线程会被挂起,进入等待队列,等待操作系统唤醒。
除此之外,JVM 还会进行锁消除(如果判断一段代码不可能有共享数据竞争,就直接去掉锁)和锁粗化(将多个连续的加解锁操作合并为一个更大的锁)等优化。
2. CAS 与原子操作
1. 讲解一下你自己理解的 CAS 机制。
我理解的 CAS(Compare-and-Swap,比较并交换)是一种非常底层的原子操作,很多无锁编程都依赖它。你可以把它想象成一个需要三个参数的指令:
- 要操作的内存地址 V
- 我预期的这个地址上的旧值 A
- 我想要更新成的新值 B
执行这条指令时,CPU 会原子性地做一件事:检查地址 V 上的当前值是否等于我预期的旧值 A。当且仅当它俩相等时,才会把地址 V 上的值更新为新值 B。整个“比较再更新”的过程是一步完成的,不会被其他线程中断,这就是它实现原子性的关键。
2. ABA问题怎么解决?
ABA 问题是 CAS 的一个经典漏洞。意思是,一个值原来是 A,被其他线程改成了 B,然后又改回了 A。我的 CAS 操作在检查时,会发现值仍然是 A,就误以为它没变过,然后执行更新。
要解决这个问题,光比较值是不够的,还需要引入一个版本号或时间戳。每次更新值的时候,都把版本号加一。这样,CAS 操作就变成了比较“值 + 版本号”。
- 原来的流程:
A -> B -> A
- 加入版本号后:
(A, v1) -> (B, v2) -> (A, v3)
当我拿着(A, v1)
去做 CAS 时,发现现在是(A, v3)
,版本号对不上,就知道中间发生过变化,从而避免了错误的操作。Java 中的AtomicStampedReference
类就是用来解决这个问题的。
3. AtomicInteger
的实现原理是什么?
它的核心原理就是基于 CAS 的自旋循环。以 incrementAndGet()
方法为例,它内部并没有用锁,而是执行了这样一个循环:
- 读取:先读取
AtomicInteger
内部volatile
修饰的当前值(oldValue
)。 - 计算:在本地计算出新值(
newValue = oldValue + 1
)。 - 交换 (CAS):调用底层的
compareAndSet(oldValue, newValue)
方法,尝试原子性地用newValue
替换oldValue
。 - 判断与重试:如果
compareAndSet
返回true
,说明在我计算的这段时间里,没有其他线程修改过值,更新成功,退出循环。如果返回false
,说明值已经被其他线程改了,我的oldValue
已经过时了。这时,循环会继续,重新执行第 1 步,直到 CAS 操作成功为止。
3. JUC 工具与线程池
1. 线程同步的方式有哪些?
我目前学到的主要有这几种:
- 最基础的:使用
synchronized
关键字,简单粗暴。 - 更灵活的:使用 JUC 包下的
Lock
接口实现,比如ReentrantLock
。 - 控制协作的:使用 JUC 包下的一些同步工具类,比如
Semaphore
(信号量)、CountDownLatch
(倒计时门闩)等。 - 无锁方式:使用原子类,比如
AtomicInteger
,通过 CAS 保证单个变量操作的线程安全。
2. 为什么有了 synchronized
还需要 JUC 下的 Lock
?
因为以 ReentrantLock
为代表的 Lock
接口,提供了 synchronized
不具备的、更高级和更灵活的功能,让我们可以对锁进行更精细的控制:
- 可中断的锁获取:
lockInterruptibly()
允许在等待锁的过程中响应中断。 - 可限时的锁获取:
tryLock(time, unit)
可以尝试在指定时间内获取锁,超时就放弃,避免死等。 - 公平性选择:可以在创建时指定为公平锁,保证线程先来先得,避免饥饿。
- 灵活的线程通信:可以绑定多个
Condition
对象,实现对不同条件的线程进行分组等待和精确唤醒,而synchronized
只有一个等待队列。
3. 信号量听说过吗?之前都用在过哪些场景下?
信号量(Semaphore
)我理解它是一个控制“许可证”数量的计数器,用来限制能同时访问某个特定资源的线程数量。
acquire()
:线程尝试获取一个许可证,成功则计数器减一。如果许可证发完了(计数器为0),线程就得阻塞等待。release()
:线程在用完资源后,释放一个许可证,计数器加一,可能会唤醒一个正在等待的线程。
它最常见的用途就是流量控制或资源池管理。比如,我们有一个数据库连接池,里面只有10个连接,那就可以用一个初始值为10的 Semaphore
来控制,确保最多只有10个线程能同时拿到连接。
4. 解释一下 ThreadPoolExecutor
构造方法的参数的含义。
这是创建线程池最核心的构造方法,每个参数都得搞清楚:
corePoolSize
: 核心线程数。线程池创建后,即使线程是空闲的,也会长期保留这么多线程。maximumPoolSize
: 最大线程数。当任务队列满了,线程池最多能再创建多少个“临时”线程,总线程数不能超过这个值。keepAliveTime
: 临时线程的空闲存活时间。当线程数超过corePoolSize
时,那些多出来的临时线程如果空闲了这么久还没接到新任务,就会被销毁。unit
:keepAliveTime
的时间单位。workQueue
: 任务阻塞队列。当核心线程都在忙时,新来的任务会先被存放在这个队列里排队。threadFactory
: 线程工厂。用来创建新线程,可以自定义线程的名字、是否为守护线程等。handler
: 拒绝策略。当任务队列已满,并且线程数也达到了maximumPoolSize
时,用来处理新提交任务的策略(比如抛异常、丢弃任务等)。
5. Java创建线程池的接口是什么?参数LinkedBlockingQueue
的作用是什么?
- 创建线程池的核心接口是
ExecutorService
。我们通常不直接实现它,而是通过Executors
这个工厂类提供的静态方法(如newFixedThreadPool
)或者直接构造ThreadPoolExecutor
类的实例来创建。 LinkedBlockingQueue
在线程池中通常被用作任务队列(workQueue)。它的特点是一个无界的链式阻塞队列。当所有核心线程都在忙碌时,新提交的任务就会被无限地添加到这个队列中,等待核心线程空闲后前来领取并执行。
4. 线程安全集合
1. ConcurrentHashMap
的读是否要加锁,为什么?
读操作(get
方法)几乎不加锁,这也是它性能高的关键之一。
ConcurrentHashMap
为了最大化读操作的并发性能,采取了非常精巧的设计。它通过将共享变量(比如哈希桶中 Node 节点的 val
和 next
指针)声明为 volatile
,来利用 volatile
的内存可见性语义。这确保了当一个线程修改了某个节点后,这个修改能立刻对其他读线程可见。这样,读线程总能看到最新的数据,从而实现了高效的无锁读取。
2. 介绍下 ConcurrentHashMap
的锁分段技术?
锁分段技术是 Java 1.7 版本中 ConcurrentHashMap
提高并发度的核心策略。它的思路是,不对整个哈希表加一把大锁,而是将整个表在逻辑上分成若干个“段”(Segment),每个 Segment 本身就像一个小型的、线程安全的 Hashtable
,拥有自己独立的锁。
当需要操作某个 key 时,只需根据 key 的哈希值定位到它所属的那个 Segment,然后只锁定这一个 Segment 即可,其他 Segment 的操作完全不受影响。这相当于把一把大锁拆分成了多把小锁,大大提高了并发写入的效率。
不过这里有个重点需要记一下:这个技术已经在 Java 1.8 中被优化替换了。在 Java 1.8 及以后的版本中,ConcurrentHashMap
取消了 Segment 的设计,改为使用 synchronized
锁住哈希桶的头节点,再加上 CAS 操作来辅助,实现了粒度更细的锁定,并发性能通常更好。
3. Hashtable
、HashMap
、ConcurrentHashMap
之间的区别?
这三个是面试老朋友了,它们的关键区别在于线程安全和性能:
HashMap
: 线程不安全。如果用在多线程环境下,需要手动在外部加锁。它的优点是性能最高,并且 key 和 value 都允许为null
。Hashtable
: 线程安全。它实现安全的方式非常粗暴,就是给几乎所有public
方法都加上了synchronized
,锁住的是整个Hashtable
对象。这导致并发效率极低,现在基本不推荐使用了。另外,它的 key 和 value 都不允许为null
。ConcurrentHashMap
: 线程安全,并且是为高并发场景设计的。在 JDK 1.8 中,它通过synchronized
锁住哈希桶的头节点 + CAS +volatile
变量来保证安全,实现了细粒度的锁,并发性能非常高。和Hashtable
一样,它的 key 和 value 也都不允许为null
。
5. 综合问题
1. 谈谈死锁是什么,如何避免死锁?
- 死锁定义:我理解的死锁,就是两个或多个线程在执行过程中,因为争夺资源而陷入一种互相等待的僵局。如果没有外力干涉,它们谁也无法继续往下执行。
- 四个必要条件:互斥使用、不可抢占、请求与保持、循环等待。这四个条件必须同时满足才会发生死锁。
- 如何避免:最核心的方法就是想办法破坏这四个必要条件之一。在编程实践中,我们最常用的手段是通过锁排序来破坏“循环等待”条件,即规定好所有线程都必须以一个固定的、相同的顺序来获取一系列的锁。
2. volatile
关键字的用法?
volatile
关键字在我看来主要有两个关键作用:
- 保证内存可见性:这是它最核心的功能。它能确保一个线程对
volatile
变量的修改,能够立刻被其他线程“看到”。底层原理是它会强制线程每次都从主内存中读取变量的值,而不是依赖自己工作内存中的缓存。 - 禁止指令重排序:
volatile
还能充当一个内存屏障,防止编译器和处理器为了优化性能而随意改变代码的执行顺序。这一点在实现某些底层并发算法时至关重要,比如经典的双重检查锁定(DCL)单例模式。
3. Java多线程是如何实现数据共享的?
JVM 的内存模型把内存分成了几个区域,其中**堆内存(Heap)和方法区(Method Area)**是所有线程共享的区域。所以,只要我们把一个对象(无论是类的实例还是静态变量)创建在这两个共享区域里,那么这个对象的引用就可以被多个线程同时持有和访问,这样就实现了线程间的数据共享。
4. Java线程共有几种状态?状态之间怎么切换的?
根据 Thread.State
枚举,Java 线程有 6 种状态:
NEW
(新建): 线程对象被创建,但还没调用start()
方法。RUNNABLE
(可运行): 这是个复合状态,包含了操作系统线程状态中的“就绪”(Ready)和“运行中”(Running)。调用start()
后线程就进入这个状态,等待 CPU 调度。BLOCKED
(阻塞): 线程在等待进入一个synchronized
同步块时,因为获取不到监视器锁而被阻塞。WAITING
(无限期等待): 线程需要等待其他线程执行特定的唤醒动作。调用Object.wait()
、Thread.join()
、LockSupport.park()
会进入此状态。TIMED_WAITING
(限时等待): 和WAITING
类似,但它不会无限等下去,会在指定时间后自动唤醒。调用Thread.sleep(time)
、Object.wait(time)
等方法会进入此状态。TERMINATED
(终止): 线程的run()
方法执行完毕,线程生命周期结束。
5. 在多线程下,如果对一个数进行叠加,该怎么做?
要保证线程安全,主要有两种方法:
- 加锁:最直接的方法,使用
synchronized
关键字或ReentrantLock
来保护这个叠加操作,确保同一时间只有一个线程能执行i++
。 - 使用原子类:更推荐的方式是使用
java.util.concurrent.atomic
包下的原子类,比如AtomicInteger
。它的addAndGet()
或incrementAndGet()
方法是基于 CAS 操作实现的,属于无锁操作,在并发量大的情况下性能通常比加锁要好。
6. Servlet是否是线程安全的?
Servlet 本身的设计是单实例多线程的。也就是说,Web 容器(比如 Tomcat)通常只会为每个 Servlet 类创建一个实例。当多个 HTTP 请求同时访问这个 Servlet 时,容器会为每个请求分配一个独立的线程,这些线程会并发地执行同一个 Servlet 实例的 service()
方法。
因此,结论是:
- 如果 Servlet 中定义了成员变量(实例变量),并且
service()
方法对这些成员变量进行了读写操作,那么就会存在线程安全问题。 - 如果 Servlet 中只使用了局部变量(在
service()
方法内部定义的变量),那么它是线程安全的,因为每个线程都有自己独立的栈空间来存放局部变量。
7. Thread
和Runnable
的区别和联系?
Runnable
是一个接口,里面只有一个run()
方法。它代表的是一个“任务”或“要做什么事”。Thread
是一个类,它代表一个执行任务的线程实体,是真正“干活的人”。- 联系:
Thread
类本身也实现了Runnable
接口。创建线程时,可以将一个Runnable
对象作为任务传递给Thread
的构造函数。 - 区别与选择: 推荐使用实现
Runnable
接口的方式。因为它将“任务”和“执行者”解耦了,一个Runnable
任务可以被不同的Thread
执行。而如果通过继承Thread
类的方式,由于 Java 的单继承限制,这个类就不能再继承其他类了,灵活性较差。
8. 多次start
一个线程会怎么样?
一个 Thread
对象的 start()
方法只能被调用一次。
- 第一次调用
start()
会成功启动线程,使其进入RUNNABLE
状态,并由 JVM 调度执行其run()
方法。 - 之后对同一个线程对象再次调用
start()
,无论线程是否已经执行完毕,都会抛出IllegalThreadStateException
异常。
9. 有synchronized
两个方法,两个线程分别同时调用,请问会发生什么?
这要看 synchronized
修饰的是什么类型的方法,以及两个线程调用的是否是同一个对象的实例方法。
- 修饰非静态方法:此时锁是当前对象实例 (
this
)。- 如果两个线程调用的是同一个对象的这两个同步方法,它们会互斥,一个线程执行时另一个必须等待锁。
- 如果两个线程调用的是不同对象的这两个同步方法,它们之间没有关系,不会互斥,可以并发执行。
- 修饰静态方法:此时锁是当前类的 Class 对象 (
Xxx.class
)。- 这种情况下,锁是全局唯一的。无论两个线程操作的是不是同一个对象实例,只要它们调用的是这个类的任何静态
synchronized
方法,都会互斥。
- 这种情况下,锁是全局唯一的。无论两个线程操作的是不是同一个对象实例,只要它们调用的是这个类的任何静态
10. 进程和线程的区别?
这是个基础但非常重要的问题,我的理解是:
- 资源单位 vs 调度单位:进程是操作系统进行资源分配的最小单位(比如内存空间)。线程是 CPU 调度的最小单位,是真正执行计算的。
- 包含关系:一个进程可以包含一个或多个线程。线程是进程的一部分,不能独立存在。
- 内存共享:进程之间的内存空间是相互独立的。而同一进程内的所有线程共享该进程的内存空间(如堆、方法区),但每个线程有自己独立的栈和程序计数器。
- 开销:创建、销毁和切换进程的开销远大于线程,因此线程也被称为“轻量级进程”。
本文核心要点总结 (Key Takeaways)
- 锁的本质是权衡:并发编程中的所有锁策略,都是在性能开销与数据安全之间寻找平衡点。没有普适的“最优解”,只有“最适合”特定场景的方案。
synchronized
是智能的:现代 JVM 中的synchronized
远非一个简单的互斥锁,它内部集成了偏向锁、轻量级锁(自旋)、重量级锁的动态升级机制,以及锁消除、锁粗化等优化,努力在各种场景下都提供接近最优的性能。- CAS是无锁并发的基石:CAS(比较并交换)作为一种 CPU 级别的原子指令,是 JUC 中许多高性能并发类(如
AtomicInteger
,ConcurrentHashMap
)的实现基础,它用乐观的非阻塞方式,在很多场景下替代了传统的悲观阻塞式加锁。 - JUC是并发编程的瑞士军刀:
java.util.concurrent
包提供了一套丰富、模块化的并发工具(如ReentrantLock
,Semaphore
,CountDownLatch
, 线程池等),它们是解决复杂并发问题的强大武器库。 - 死锁可防可控:深刻理解死锁产生的四个必要条件,并在编码实践中(最常见的是通过“锁排序”)主动破坏其中之一,是预防这个严重并发问题的关键。
是 JUC 中许多高性能并发类(如AtomicInteger
,ConcurrentHashMap
)的实现基础,它用乐观的非阻塞方式,在很多场景下替代了传统的悲观阻塞式加锁。 - JUC是并发编程的瑞士军刀:
java.util.concurrent
包提供了一套丰富、模块化的并发工具(如ReentrantLock
,Semaphore
,CountDownLatch
, 线程池等),它们是解决复杂并发问题的强大武器库。 - 死锁可防可控:深刻理解死锁产生的四个必要条件,并在编码实践中(最常见的是通过“锁排序”)主动破坏其中之一,是预防这个严重并发问题的关键。