Java 并发基础之内存模型( 三 )

使用场景信号量为控制并发程序的执行提供了强有力工具 , 这里列举两个场景:
互斥信号量提供了了一种很方便的方法来保证对共享变量的互斥访问 , 基本思想是

将每个共享变量(或一组相关的共享变量)与一个信号量 s (初始化为1)联系起来 , 然后用 wait/signal 操作将相应的临界区包围起来 。
二元信号量也被称为互斥锁(mutex , mutual exclusve, 也称为 binary semaphore) , wait 操作相当于加锁 , signal 相当于解锁 。一个被用作一组可用资源的计数器的信号量称为计数信号量(counting semaphore)
调度共享资源除了互斥外 , 信号量的另一个重要作用是调度对共享资源的访问 , 比较经典的案例是生产者消费者 , 伪代码如下:
emptySem = NfullSem = 0// Producerwhile(whatever) {locally generate itemwait(emptySem)fill empty buffer with itemsignal(fullSem)}// Consumerwhile(whatever) {wait(fullSem)get item from full buffersignal(emptySem)use item}POSIX 实现POSIX 标准中有定义信号量相关的逻辑 , 在 semaphore.h 中 , 为 sem_t 类型 , 相关 API:
// Intialize: sem_init(&theSem, 0, initialVal);// Wait: sem_wait(&theSem);// Signal: sem_post(&theSem);// Get the current value of the semaphore:sem_getvalue(&theSem, &result);信号量主要有两个缺点:
  • Lack of structure , 在设计大型系统时 , 很难保证 wait/signal 能以正确的顺序成对出现 , 顺序与成对缺一不可 , 否则就会出现死锁!
  • Global visiblity , 一旦程序出现死锁 , 整个程序都需要去检查
解决上述两个缺点的新方案是监控器(monitor) 。
MonitorsC. A. R. Hoare(也是 Quicksort 的作者) 在 1974 年的论文 Monitors: an operating system structuring concept 首次提出了「监控器」概念 , 它提供了对信号量互斥和调度能力的更高级别的抽象 , 使用起来更加方便 , 一般形式如下:
monitor1 . . . monitorMprocess1 . . . processN我们可以认为监控器是这么一个对象:
  • 所有访问同一监控器的线程通过条件变量(condition variables)间接通信
  • 某一个时刻 , 只能有一个线程访问监控器
Condition variables上面提到监控器通过条件变量(简写 cv)来协调线程间的通信 , 那么条件变量是什么呢?它其实是一个 FIFO 的队列 , 用来保存那些因等待某些条件成立而被堵塞的线程 , 对于一个条件变量 c 来说 , 会关联一个断言(assertion) P 。线程在等待 P 成立的过程中 , 该线程不会锁住该监控器 , 这样其他线程就能够进入监控器 , 修改监控器状态;在 P 成立时 , 其他线程会通知堵塞的线程 , 因此条件变量上主要有三个操作:
  1. wait(cv, m) 等待 cv 成立 , m 表示与监控器关联的一 mutex 锁
  2. signal(cv) 也称为 notify(cv) 用来通知 cv 成立 , 这时会唤醒等待的线程中的一个执行 。根据唤醒策略 , 监控器分为两类:Hoare vs. Mesa , 后面会介绍
  3. broadcast(cv) 也称为 notifyAll(cv) 唤醒所有等待 cv 成立的线程
POSIX 实现在 pthreads 中 , 条件变量的类型是 pthread_cond_t , 主要有如下几个方法:
// initializepthread_cond_init() pthread_cond_wait(&theCV, &someLock);pthread_cond_signal(&theCV);pthread_cond_broadcast(&theCV);使用方式在 pthreads 中 , 所有使用条件变量的地方都必须用一个 mutex 锁起来 , 这是为什么呢?看下面一个例子:
pthread_mutex_t myLock;pthread_cond_t myCV;int count = 0;// Thread Apthread_mutex_lock(&myLock);while(count < 0) {pthread_cond_wait(&myCV, &myLock);}pthread_mutex_unlock(&myLock);// Thread Bpthread_mutex_lock(&myLock);count ++;while(count == 10) {pthread_cond_signal(&myCV);}pthread_mutex_unlock(&myLock);如果没有锁 , 那么
  • 线程 A 可能会在其他线程将 count 赋值为10后继续等待
  • 线程 B 无法保证加一操作与测试 count 是否为零 的原子性
这里的关键点是 , 在进行条件变量的 wait 时 , 会释放该锁 , 以保证其他线程能够将之唤醒 。不过需要注意的是 , 在线程 B 通知(signal) myCV 时 , 线程 A 无法立刻恢复执行 , 这是因为 myLock 这个锁还被线程 B 持有 , 只有在线程 B unlock(&myLock) 后 , 线程 A 才可恢复 。总结一下:


推荐阅读