目录
- 线程的状态
- 死锁
- CAS是什么,应用场景,原理,局限性以及乐观锁和悲观锁。
一、线程的状态
1. NEW(新建)
- 描述:线程被创建但尚未启动。
- 触发条件:通过
new Thread()
实例化后,未调用start()
方法。 - 转换:调用
start()
方法后进入RUNNABLE状态。
2. RUNNABLE(可运行)
- 描述:线程正在运行或等待CPU时间片。
-
- 就绪(Ready) :等待操作系统分配CPU时间。
- 运行中(Running) :正在执行代码。
- 触发条件:
-
- 调用
start()
方法后。 - 从等待状态(如
BLOCKED
、WAITING
)恢复后。
- 调用
- 可能转换:
-
- 等待锁时进入BLOCKED。
- 调用
wait()
、join()
或LockSupport.park()
进入WAITING。 - 调用
sleep()
或带超时的wait()
进入TIMED_WAITING。 - 执行完毕或异常终止进入TERMINATED。
3. BLOCKED(阻塞)
- 描述:线程等待获取监视器锁(synchronized锁)。
- 触发条件:尝试进入
synchronized
块/方法时,锁已被其他线程持有。 - 转换:获取锁后返回RUNNABLE状态。
4. WAITING(无限期等待)
- 描述:线程主动等待其他线程显式唤醒。
- 触发方法:
-
Object.wait()
:释放锁并等待其他线程调用notify()
/notifyAll()
。Thread.join()
:等待目标线程终止。LockSupport.park()
:挂起当前线程。
- 转换:被唤醒(如
notify()
、LockSupport.unpark()
)后返回RUNNABLE。
5. TIMED_WAITING(超时等待)
- 描述:线程等待指定时间后自动唤醒。
- 触发方法:
-
Thread.sleep(long millis)
:休眠指定时间。Object.wait(long timeout)
:带超时的等待。Thread.join(long millis)
:带超时的等待线程终止。LockSupport.parkNanos(long nanos)
:带纳秒级超时的挂起。
- 转换:
-
- 超时后自动返回RUNNABLE。
- 被提前唤醒(如
interrupt()
)返回RUNNABLE。
6. TERMINATED(终止)
- 描述:线程执行完毕或异常终止。
- 触发条件:
-
run()
方法正常结束。- 未捕获的异常导致线程终止。
- 转换:终止后不可恢复。
二、死锁
简单来说,比如有两把锁,A线程进入同步的时候拿到了1号锁,这个时候B线程进入同步的时候拿到了2号锁,然后A又要拿2号锁,就阻塞住了,要等B线程释放,但这个时候B线程又要拿1号锁,就又阻塞住了,两者都僵持住了,这个现象,就叫死锁。
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread1拿到锁1");
try { Thread.sleep(100); }
catch (InterruptedException e) {}
synchronized (lock2) { // 等锁2(此时锁2被Thread2占着)
System.out.println("Thread1拿到锁2");
}
}
}).start();
new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread2拿到锁2");
synchronized (lock1) { // 等锁1(此时锁1被Thread1占着)
System.out.println("Thread2拿到锁1");
}
}
}).start();
}
}
死锁发生必须同时满足以下条件,缺一不可:
条件 | 通俗解释 |
---|---|
互斥 | 资源(比如打印机、锁)一次只能被一个线程占用,别人用就得等。 |
占有且等待 | 线程A占着锁1不放手,同时还想抢锁2;线程B占着锁2不放手,同时想抢锁1。 |
不可剥夺 | 线程占有的资源不能被强行抢走,只能自己释放。 |
循环等待 | 线程A等线程B,线程B等线程A,形成一个“等对方”的环。 |
如何避免呢,只要破坏其中一个点,就可以了。
- 比如循环等待:按固定顺序拿锁,A和B线程同时拿1号锁,然后再拿2号锁。
// 正确写法:所有线程按相同顺序拿锁
Thread threadA = new Thread(() -> {
synchronized (lock1) { // 先拿锁1
synchronized (lock2) { // 再拿锁2
// 干活
}
}
});
Thread threadB = new Thread(() -> {
synchronized (lock1) { // 先拿锁1(和线程A顺序一致)
synchronized (lock2) { // 再拿锁2
// 干活
}
}
});
- 使用ReentrantLock的tryLock方法,这个方法是尝试拿锁,如果拿不到,就会返回false,而不是阻塞等待。
- 一次性申请所有资源:线程在开始执行前申请所有需要的资源。
// 示例:用 ReentrantLock 的 tryLock 实现
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
public void doWork() {
while (true) {
if (lock1.tryLock()) { // 尝试拿筷子
try {
if (lock2.tryLock()) { // 尝试拿碗
try {
System.out.println("吃饭!");
break;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock(); // 没拿到碗,释放筷子
}
}
// 随机睡一会,避免活锁(两个人反复抢筷子)
try { Thread.sleep((long)(Math.random()*100)); }
catch (InterruptedException e) {}
}
}
三、CAS
为什么使用呢?
- 如果我的逻辑很简单,比如就i++,为了怎么简单的操作,增加syn,太重了。对于这种操作,也没有更加轻量级的操作呢?所以CAS出现了.
传统的锁机制(如 synchronized
)通过阻塞线程来保证安全,但会带来性能开销。
CAS 提供了一种无锁的替代方案,用于在多线程环境中安全地更新共享变量。CAS 机制:线程通过循环(自旋)不断尝试 CAS 操作,直到成功更新变量,无需阻塞。
CAS 的优势:
- 无锁化:避免线程阻塞和上下文切换,提升高并发场景下的性能。
- 轻量级:适用于简单原子操作(如计数器累加)。
3.1 应用场景:计数器累加
不需要我们去实现,java就提供了一些CAS的原子类。Atomic开头
1. Java 原子类(AtomicInteger
、AtomicReference
)
Java 的 java.util.concurrent.atomic
包中的原子类基于 CAS 实现。
AtomicInteger counter = new AtomicInteger(0);
// 线程安全的自增操作
counter.incrementAndGet(); // 内部通过 CAS 实现
3.2 原理
“先检查再更新”——在修改值之前,先检查当前值是否符合预期,如果符合则更新,否则放弃操作。
通俗比喻:
假设你和朋友同时修改一份共享文档,系统会先检查你看到的文档内容是否和当前内容一致。如果一致,允许你修改;如果不一致,提示你重新加载后再试。
以更新一个变量 value
为例:
- 读取当前值:
oldValue = value
。 - 计算新值:
newValue = oldValue + 1
。 - 提交更新:只有当
value
当前仍等于oldValue
时,才将value
设置为newValue
。
(1)无锁化设计
- 自旋重试:线程在 CAS 失败后不阻塞,而是循环重试,直到成功更新。
- 轻量级同步:适合简单原子操作(如计数器、标志位)。
(2)解决数据不一致
- 基于最新值更新:每次 CAS 操作都基于当前最新值,避免脏读和覆盖。
- 线程协作:即使多线程并发操作,最终只有一个线程能成功更新。
整个“比较-交换”过程由 CPU 指令(如 cmpxchg
)保证原子性,无需加锁。
3.3 局限性
① ABA 问题
- 问题描述:变量从 A 变为 B 再变回 A,CAS 会误认为值未变化。
- 解决方案:添加版本号(如
AtomicStampedReference
)。
② 高竞争下的性能问题
- 场景:大量线程频繁修改同一变量时,CAS 重试次数激增。
- 优化手段:退化为锁(如
LongAdder
的分段累加)。
③ 只能保护单个变量
- 限制:CAS 无法直接保证多个变量的原子性操作。
- 解决方案:使用锁或合并变量(如将多个字段打包为单个对象)。
3.4 乐观锁和悲观锁
在学习之前,我在思考,乐观锁和悲观锁究竟是什么,感觉实现起来很复杂,没想到,居然是synchronized
和CAS就可以实现。
类型 | 核心思想 | 实现方式 | 适用场景 |
---|---|---|---|
悲观锁 | 认为数据会被其他线程/事务修改,先加锁再操作(默认冲突会发生)。 | synchronized 、ReentrantLock 、数据库行锁 | 高并发写操作、数据竞争激烈的场景 |
乐观锁 | 认为数据冲突概率低,先操作再检查冲突(默认冲突不会发生)。 | CAS、版本号机制(如数据库乐观锁) | 高并发读操作、数据竞争较少的场景 |
1. 悲观锁的实现
- Java 多线程:通过
synchronized
或ReentrantLock
(显式锁)直接加锁。
// 使用 synchronized(悲观锁)
public synchronized void updateValue() {
// 操作共享数据
}
悲观锁:加锁 → 操作 → 释放锁(锁是操作的前提)。
2. 乐观锁的实现
- CAS 原子操作:通过 CPU 指令(如
cmpxchg
)实现无锁更新。
// 使用 AtomicInteger(基于 CAS)
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 内部通过 CAS 实现
乐观锁:操作 → 检查冲突 → 提交或重试(锁是冲突后的处理)。