多线程问题总览

多线程并发访问的问题

线程安全问题|race condition | data race | 系统行为一致性问题 | 原子性、有序性、可见性问题 都是讲的同一个问题,即如何解决共享资源的并发访问的正确性。

背景:

  1. CPU指令和编译器会在不影响 as_if_serail 的情况下,会进行指令重排

  2. 多级缓存架构(CPU 寄存器、三级缓存、内存)导致可见性问题

  3. CPU 提供了内存栅栏、原子操作、关中断、缓存一致性协议等能力
    4.系统中的异常分类:中断(interrpt)、陷阱(trap)、故障(fault)、终止(abort)

  4. 操作系统中的线程状态机:
    NEW(新创建、未初始化 PCB)
    READY(初始化完成、可调度)
    RUNNING(调度中,有时间片)
    BlOCKED(由于某些问题阻塞了,IO、wait、sleep)
    TERMINATED(进程已经完成执行、不会被调度)

    READY->RUNNING :分配时间片被调度
    RUNNING->READY: 时间片用完或者 yield()
    RUNNING->BLOCKED: wait、sleep、io
    BLOCKED->READY: notify、io完成、time 到了
    Java 中的线程状态多了 TIME_WAITING 和 WAITING 区分 sleep 和 wait

  5. 所有阻塞 API,本质都是条件等待(sleep、wait、io),这里可以拓展到 epoll。

操作系统层面给出的解决方案是同步原语(同步器),即设计一套算法,算法需符合互斥访问、有限等待、空闲让进的原则,确保临界区中的共享资源安全访问。
算法一
互斥锁
2.0 关中断(进入临界区的时候关闭中断,表示当前线程不响应中断信号,即不会做上下文切换,等出临界区的时候再打开)。
2.1 基于 CAS, 定一个变量 status=0 表示无锁,=1 表示有锁,然后使用 CAS 不断重试获取锁,即 while(CAS(&,0,1)),如果成功,就表示获取到了锁(非公平的,忙等的)
2.2 基于 FAA排号锁,第一两个变量,ticket:号码,serving:服务号,ticket 通过 FAA 更新,线程加锁时,通过 FAA 拿到 ticket 同时会原子性更新下一个 ticket,如果 ticket=servving 就表示获取到了锁,否者就等待。离开临界区的时候 serving++。(公平的,忙等的)
2.3 为了有序竞争锁,提升效率,不要让那么多线程忙等,可以加一个同步队列,如果没获取到锁先插入到同步队列中,并挂起。(不占 CPU 了,比如 AQS)

算法二
条件变量
解决算法二的忙等浪费CPU问题,设计一个等待队列,当条件不满足时,将线程放到等待队列中并阻塞,等条件满足时再唤醒,这里需要注意挂起前需要先放锁,同时需要原子操作,并且被唤醒后需要再进入到同步队列竞争锁,重新判定条件是否满足。
这里操作系统利用这个可以实现阻塞方法,比如 wait()/Condition.await()/sleep()/read()/write()(阻塞 IO)、accept()/recv()/lock(),这里可以进化到epoll 的 IO 多路复用。

算法三
信号量
为了解决多个线程并发访问有限共享资源的问题,可以控制 n 个访问一起进入临界区。(限流,线程池,连接池)
通过资源计数器 + 条件变量来实现,PV操作,P 申请资源(acquire,资源数-1),V 释放资源(release,资源数+1),不足的时候就放到等待队列中

算法四
读写锁
专门用于读多写少的场景,需要有两个锁状态,一个读锁、一个写锁,同时需要通过算法维护读写、写写的互斥性,具体可以参考 Java 中的读写锁实现。

算法五
管程:为了减轻开发者的负担,提出了 Monitor(管程)的概念,包含两部分内容,一个是共享数据,一个是操作共享数据的函数,有点类似于 Java 中的线程安全的类。注意 java 中的 synchronize 就是通过 JVM 的 objectMonitor 管程来实现的,其中封装了锁状态、锁的持有者、等待队列、同步队列、条件变量等结构。

Java 层面的解决方案--管程:
JVM的ObjectMonitor 属于高级管程,有同步队列和等待队列,可以管理 n 个线程的并发控制,是 synchronized(monitorenter、monitorexit) 和 Object.wait 以及 Object.notiry 的底层实现。

class ObjectMonitor {
public:
    // 1. 互斥锁(POSIX mutex)
    pthread_mutex_t _mutex;

    // 2. 条件变量(POSIX cond)—— 你要找的东西就在这!
    pthread_cond_t  _cond;

    // 3. 当前持有锁的线程
    Thread* _owner;

    // 4. 锁竞争队列:想拿锁但没拿到的线程
    ObjectWaiter* _EntryList;

    // 5. wait() 等待队列:调用了 wait() 的线程
    ObjectWaiter* _WaitSet;

    // 6. 自旋次数、重入计数、膨胀状态等……
    int _count;
    // ...
};

JVM 的Parker 也属于管程,但是极简版,只关注单个线程的挂起和唤醒,用的是操作系统的等待队列

class Parker {
    pthread_mutex_t _mutex;
    pthread_cond_t  _cond;
    int _counter;  // 一个许可
};

抽象的队列同步器
AQS = LockSupport(park,uppark)+CLH 双向队列(同步队列)+CAS(Unsafe)修改状态
独占模式: ReentrantLock(里面有条件变量,因为条件变量依赖锁,所以放里面)
共享模式: Semaphore、CountDownLatch、CyclicBarrier(Lock+Condition)
BlockingQueue
ThreadPoolExecutor

Synchoronized
monitorenter(锁升级逻辑)、monitorexit
对象头里的 marker word(不同锁类型,有不同的 marker word)
锁升级(CAS + 偏向锁(thread id)、轻量级锁(堆栈中的锁记录)、重量级锁(ObjectMonitor))

ReentrantLock、ReentrantLock.Condition、Semaphore、CountDownLatch →AQS->LockSupport-> Unsafe → Parker(管程) → POSIX同步原语(互斥锁+条件变量)
Synchronized、Object.wait()、Object.notiry() -> monitorenter、monitorexit -> ObjectMonitor(管程) -> POSIX同步原语(互斥锁+条件变量)

易混淆的问题:
同步队列和的等待队列的区别和联系?
所需的阶段不同,同步队列可以理解为等待锁释放的等待队列,这个时候线程还没有拿到锁。而等待队列这个时候线程已经拿到了锁,只不过线程还在等待某个业务条件满足,放到等待队列。最终两个队列中的线程都会通过 LockSupport.park()挂起。

posted @ 2026-04-17 16:51  StanderXon  阅读(18)  评论(0)    收藏  举报