Android 线程、线程池的使用(二):CAS无锁机制;死锁;线程状态;乐观锁和悲观锁究竟是什么。

2 阅读7分钟

目录

  1. 线程的状态
  2. 死锁
  3. CAS是什么,应用场景,原理,局限性以及乐观锁和悲观锁。

一、线程的状态

1. NEW(新建)

  • 描述:线程被创建但尚未启动。
  • 触发条件:通过new Thread()实例化后,未调用start()方法。
  • 转换:调用start()方法后进入RUNNABLE状态。

2. RUNNABLE(可运行)

  • 描述:线程正在运行或等待CPU时间片。
    • 就绪(Ready) :等待操作系统分配CPU时间。
    • 运行中(Running) :正在执行代码。
  • 触发条件
    • 调用start()方法后。
    • 从等待状态(如BLOCKEDWAITING)恢复后。
  • 可能转换
    • 等待锁时进入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,形成一个“等对方”的环。

如何避免呢,只要破坏其中一个点,就可以了。

  1. 比如循环等待:按固定顺序拿锁,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
            // 干活
        }
    }
});
  1. 使用ReentrantLock的tryLock方法,这个方法是尝试拿锁,如果拿不到,就会返回false,而不是阻塞等待。
  2. 一次性申请所有资源:线程在开始执行前申请所有需要的资源。
// 示例:用 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

为什么使用呢?

  1. 如果我的逻辑很简单,比如就i++,为了怎么简单的操作,增加syn,太重了。对于这种操作,也没有更加轻量级的操作呢?所以CAS出现了.

传统的锁机制(如 synchronized)通过阻塞线程来保证安全,但会带来性能开销。

CAS 提供了一种无锁的替代方案​​,用于在多线程环境中安全地更新共享变量。CAS 机制​​:线程通过循环(自旋)不断尝试 CAS 操作,直到成功更新变量,无需阻塞。

CAS 的优势​​:

  • ​无锁化​​:避免线程阻塞和上下文切换,提升高并发场景下的性能。
  • ​轻量级​​:适用于简单原子操作(如计数器累加)。

3.1 应用场景:计数器累加

不需要我们去实现,java就提供了一些CAS的原子类。Atomic开头

​1. Java 原子类(AtomicIntegerAtomicReference)​

Java 的 java.util.concurrent.atomic 包中的原子类基于 CAS 实现。

AtomicInteger counter = new AtomicInteger(0);

// 线程安全的自增操作
counter.incrementAndGet(); // 内部通过 CAS 实现

3.2 原理

“先检查再更新”——在修改值之前,先检查当前值是否符合预期,如果符合则更新,否则放弃操作。

通俗比喻
假设你和朋友同时修改一份共享文档,系统会先检查你看到的文档内容是否和当前内容一致。如果一致,允许你修改;如果不一致,提示你重新加载后再试。

以更新一个变量 value 为例:

  1. 读取当前值oldValue = value
  2. 计算新值newValue = oldValue + 1
  3. 提交更新:只有当 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就可以实现。

类型​​核心思想​​实现方式​​适用场景​
​悲观锁​认为数据会被其他线程/事务修改,​​先加锁再操作​​(默认冲突会发生)。synchronizedReentrantLock、数据库行锁高并发写操作、数据竞争激烈的场景
​乐观锁​认为数据冲突概率低,​​先操作再检查冲突​​(默认冲突不会发生)。CAS、版本号机制(如数据库乐观锁)高并发读操作、数据竞争较少的场景
​1. 悲观锁的实现​
  • ​Java 多线程​​:通过 synchronizedReentrantLock(显式锁)直接加锁。
// 使用 synchronized(悲观锁)
public synchronized void updateValue() {
    // 操作共享数据
}

​悲观锁​​:加锁 → 操作 → 释放锁(锁是操作的前提)。

​2. 乐观锁的实现​
  • ​CAS 原子操作​​:通过 CPU 指令(如 cmpxchg)实现无锁更新。
// 使用 AtomicInteger(基于 CAS)
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 内部通过 CAS 实现

乐观锁​​:操作 → 检查冲突 → 提交或重试(锁是冲突后的处理)。