浅谈基本的并发模式——Signaling
最近在阅读《Little Book Of Semaphores》,收获良多,总结一下书中第三章———基本的并发模式(Basic synchronization patterns)。文中主要介绍了Signaling(通知)、Rendezvous(约会)、Mutex(互斥)、Multiplex(...)、Barrier(屏障)、Queue(队列)六种模式。
Signaling(通知)
Signaling是一个线程发送信号给另一个线程表明一件事情发生,涉及到发送线程和接受线程。发送线程完成事件A时会发送信号。接受线程运行到一定位置时等待或检查事件A的完成信号,只有收到事件A的完成信号才可以继续运行。Signaling可以用于保证不同线程中代码块的执行顺序,解决Serialization Problem(一致性问题?)。
可以简单的使用AtomicBoolean来变量来实现
// AtomicBoolean版的Signaling
public class Signaling {
private AtomicBoolean state = new AtomicBoolean(false);
public void doSignal() {
state.set(true);
}
public void doWait() {
for (;;) {
if (state.get() && state.compareAndSet(true, false)) {
break;
}
}
}
}
使用AtomicBoolean实现的Signaling在doWait调用前的多次doSignal的调用时没有作用的,在使用上有很多限制。更好的实现可以使用AtomicInteger。
// AtomicInteger版的Signaling
public class Signaling {
private AtomicInteger state;
public Signaling() {
this(0);
}
public Signaling(int permits) {
state = new AtomicInteger(permission);
}
public void doSignal() {
state.incrementAndGet();
}
public void doWait() {
state.decrementAndGet();
while (state.get() < 0) {
}
}
}
测试程序:
public static void main(String[] args) {
Signaling signaling = new Signaling();
Thread a = new Thread(() -> {
System.out.println("A1");
signaling.doSignal();
System.out.println("A2");
});
Thread b = new Thread(() -> {
System.out.println("B1");
signaling.doWait();
System.out.println("B2");
});
b.start();
a.start();
}
A1和B1、A2和B2的打印顺序无法确定,由于Signaling的使用A1一定在B2之前打印。使用这种方法会使接受线程在一个变量上自旋,如果等待的时间不长还可以接受否则会造成CPU资源的浪费。
《Little Book Of Semaphores》中主要讲Semaphores(信号量)以及使用Semaphores解决并发中一些问题。我们也可以使用Java并发包中的Semaphores表明Signaling。
// Semaphores版的Signaling
public class Signaling {
private Semaphore semaphore;
public Signaling() {
this(0);
}
public Signaling(int permits) {
semaphore = new Semaphore(permits);
}
public void doSignal() {
semaphore.release();
}
public void doWait() {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
关于Signaling的使用举一个都很熟悉的问题————两个线程交替打印奇偶数(这里就不给出问题描述了)。这个问题很明显的要解决两个线程中打印代码的执行顺序,我们可以使用Signaling的思想来思考这个问题。每个线程都在循环中使用同样的方式遍历所有的数字,在偶数线程中当遇到偶数时则打印并发出通知,遇到奇数则等待通知。奇数线程则相反。
public static void main(String[] args) {
Signaling even = new Signaling();
Signaling odd = new Signaling();
final int TOTAL = 100;
Thread evenThread = new Thread(() -> {
for (int i = 0; i <= TOTAL; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
even.doSignal();
} else {
odd.doWait();
}
}
});
Thread oddThread = new Thread(() -> {
for (int i = 0; i <= TOTAL; i++) {
if (i % 2 == 1) {
System.out.println(Thread.currentThread().getName() + ":" + i);
odd.doSignal();
} else {
even.doWait();
}
}
});
evenThread.start();
oddThread.start();
}
两个线程交替打印奇偶数可以扩展到N个线程连续打印数字0到M(怎么才能清晰的描述这个问题?)。我们使用N个Signaling来控制N个线程的执行顺序,一个线程打印完通知应该打印下一个数字的线程,线程在等待打印上一个数字的线程上,线程只关心它应该打印的数字的上一个数字有没有打印。
public static void main(String[] args) {
final int NUM = 6;
final int TOTAL = 200;
Signaling[] signalings = new Signaling[NUM];
for (int i = 0; i < NUM; i++) {
signalings[i] = new Signaling();
}
Thread[] threads = new Thread[NUM];
for (int i = 0; i < NUM; i++) {
int index = i;
threads[i] = new Thread(() -> {
for (int j = 0; j <= TOTAL; j++) {
int current = j % NUM;
int prev;
if (index - 1 < 0) {
prev = NUM - 1;
} else {
prev = index - 1;
}
if (current == index) {
System.out.println(Thread.currentThread().getName() + ": " + j);
signalings[(index + 1) % NUM].doSignal();
} else if (current == prev) {
signalings[index].doWait();
}
}
System.out.println(Thread.currentThread().getName() + " end");
});
}
for (int i = 0; i < num; i++) {
threads[i].start();
}
}