leetcode1116 多信号量嵌套控制下的DCL检查
先吐槽一下网上对原子性的解释,总是说原子性是不可分割的。
那只是在单核心下的语义,单核心下不可分割的操作意味着执行过程中不会有其它线程执行,从而导致变量污染,也就是原子性操作涉及的共享变量是安全的。
所以多线程下原子性的语义也应该是 :原子性操作涉及的共享变量是安全的,不会有其它线程修改。
也就是说保证原子性,是保证“某段代码”以及“对所有这段代码中牵扯到的共享变量的操作”是线程排他的。
在单线程环境下存在数据依赖或者控制依赖的一段指令,多线程下我们必须保证其原子性。原因很简单,如果它们不是原子的,在它们执行的间隙,其它线程改变共享变量的值会导致变量污染。
但当外部控制是信号量而不是锁时,无法保证内部逻辑的原子性。所以我们进行完一系列控制后,在真正执行任务前需要返回来判断外部控制是否还是有效的,并在内部控制保证任务的原子性,这便是DCL的思路。
题目如上,题目不难,是一个通过信号量通信的典型例子。
题目中几个点比较具有代表意义:
1. 对于共享变量 n 的累减与 current 的累加是一个典型的基于共享变量当前值计算下一步值的操作,通常情况下是一个读操作与一个写操作的组合。因为这两个操作在单线程环境下存在数据依赖,所以在多线程情况下我们应该保证其原子性,防止在单线程的两个操作之间,其它线程修改了共享变量的值造成变量污染。
但是在本题中,每个线程需要自己的信号量放行后才可以执行共享变量 n 的累减与 current 的累加操作,且同一时间只有一个信号量是 true ,所以我们在保证线程对信号量的访问正确性的情况下,信号量等于是共享变量 n 的累减与 current 的累加的锁,同一时间只有一个线程可以执行,天然的保证了其原子性。
2. n 与 三个信号量共同构成了线程执行任务的条件。
当我们遇到类似的结构时:
while( 条件1 ){
while( 条件2 ){
执行任务;
}
}
任务的执行需要多个共享变量放行,我们需要尤其注意。
在满足了外层条件并等待在内层条件满足的时间段中,外层条件很可能被其它线程改变。因此在满足内层条件后我们一定要对外层条件再进行一次检查。除非内层条件的等待与外层条件的修改是互斥的,但代价非常大。因为我们要互斥的,不是 条件1 的写操作与 条件2 的读操作,而是 条件1 的写操作与等待条件2直到其为真。
比如:
int lock1=10; int lock2=0; private synchronized void setLock1(int i){ lock1=i; } while(lock1!=0){ synchronized(this){ while(lock2==0){ 等待; }
开启lock3; } }
使 lock1 的写操作与等待在 lock2 上的所有操作共用一把锁。逻辑上很合理,一个线程基于 lock1==1 做事,那么我做完之前,谁都不允许去修改 lock1 。
如果说,所有线程都只使用这两把锁,这样做没有什么问题。但如果 lock2 控制的任务牵扯到与其它线程的协作的话,情况就复杂了起来。
比如此时还有一个线程:
while(lock1!=0){ synchronized(this){ while(lock3==0){ 等待; }
开启 lock2;
} }
需要锁的顺序为: lock1-->lock3-->unlock2 ,而上一个线程的任务需要锁的顺序为:lock1-->lock2-->unlock3。
lock3开启lock2关闭的情况下,第一个线程先获取lock1,则整个程序便死锁了,因此我们需要更细粒度的互斥关系。
lock1 不应该是锁,而应该是信号量,它代表的是线程可用资源而不是保证线程互斥,那么多个线程可以进入 lock1 控制的区域。但是这样依然不够,lock3-->unlock2 与
lock2-->unlock3 存在循环依赖,依然会造成死锁。所以我们必须让进入 lock2 与进入 lock3 也是互斥的。
而在 lock1 控制的区域内部,执行 lock2 加锁区域 与 执行 lock3 加锁区域是互斥的,而且只有在这两个区域内可以进行 lock1 的修改操作,那么不管进入 lock2 还是进入 lock3,再进行 lock1 的取值,其结果都将是线程安全的。
class ZeroEvenOdd {
//待打印数据个数 private volatile int n;
//打印 0 的信号量 private volatile boolean isZero=true;
//打印 偶数 的信号量 private volatile boolean isEven=false;
//打印 奇数 的信号量 private volatile boolean isOdd=false;
//当前打印到的数 private volatile int current=0; public ZeroEvenOdd(int n) { this.n = n; } // printNumber.accept(x) outputs "x", where x is an integer. public final void zero(IntConsumer printNumber) throws InterruptedException { while(n!=0){ while(!isZero){ if(n==0){ return; } Thread.yield(); } if(n==0){ break; } printNumber.accept(0); isZero=false; if((current&1)==0){ isOdd=true; }else{ isEven=true; } } } public final void even(IntConsumer printNumber) throws InterruptedException { while(n!=0){ while(!isEven){ if(n==0){ return; } Thread.yield(); } current=current+1; printNumber.accept(current); n--; isEven=false; isZero=true; } } public final void odd(IntConsumer printNumber) throws InterruptedException { while(n!=0){ while(!isOdd){ if(n==0){ return; } Thread.yield(); } current=current+1; printNumber.accept(current); n--; isOdd=false; isZero=true; } } }