Java多线程:交替打印FooBar
文章目录
问题
方法
1.Semaphore
Semaphore常用于控制同时访问特定资源的线程数。
通过构造方法Semaphore(int permits)
构造Semaphore,permits表示可用的许可证数量,例如Semaphore(10)
即用于控制最多10个线程同时访问特定资源。
如果线程要访问一个资源就必须先获得对应信号量Semaphore的许可证。线程使用Semaphore的acquire()
方法来获取一个许可证,如果信号量内部计数器大于0,信号量减1,然后允许共享这个资源;否则,如果信号量的计数器等于0,信号量将会把线程置入休眠直至计数器大于0。当信号量使用完时,必须释放,线程调用Semaphore的release()
方法归还许可证。
Java多线程中的Semaphore信号量和当初我们学习操作系统时接触的信号量原理是类似的,我们也可以使用它完成线程间的同步
当我们准备打印Foo时,我们须保证Bar已经打印,因此我们需要一个表示Bar已经打印完成的信号量,记为barFinish,在打印Foo之前,我们去获取barFinish对应的许可证,由于我们最开始需要先打印Foo,且只有一个打印Foo的线程需要进行许可证获取,因此barFinish的初始值设为1。
同理,当我们准备打印Bar时,必须保证Foo已经打印,因此需要一个表示Foo已经打印完成的信号量,记为fooFinish,在打印Bar之前,我们去获取fooFinish对应的许可证,由于我们最开始需要先打印Foo,即在第一个Foo打印出来之前先禁止打印Bar,且只有一个打印Bar的线程需要进行许可证获取,因此fooFinish的初始值设为0。
在Foo打印完成或Bar打印完成之后,需要通知另一个线程,即对应信号量的release操作,在foo打印完后,我们释放fooFinish信号量的许可证,通知打印bar,同理,在bar打印完后,我们释放barFinish信号量的许可证,通知打印foo。
代码:
class FooBar {
private int n;
public FooBar(int n) {
this.n = n;
}
private Semaphore fooFinish = new Semaphore(0);
private Semaphore barFinish = new Semaphore(1);
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
barFinish.acquire();
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
fooFinish.release();
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
fooFinish.acquire();
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
barFinish.release();
}
}
}
2.CyclicBarrier
CyclicBarrier从字面意思上理解是可循环的Barrier,它的作用是:让一组线程到达一个Barrier(屏障,即同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截(阻塞)的线程才会继续运行。从作用上来看,CyclicBarrier与CountDownLatch很相似,主要的区别是CyclicBarrier可重复使用,而CountDownLatch是一次性的。
CyclicBarrier默认的构造方法是CyclicBarrier(int parties)
,参数表示屏障拦截的线程数。线程调用CyclicBarrier的await()
方法告知CyclicBarrier其已经到达屏障,然后线程被阻塞。
在CyclicBarrier的实现中,若计数器减为0,将调用nextGeneration方法将屏障转到下一代,在该方法中会将所有线程唤醒,将计数器的值重新设为parties,因此CyclicBarrier是可以重复使用的,当所有线程都到达同步点,计数器为0时,线程全被唤醒,之后计数器的值又会复原。
因为共有2个线程,CyclicBarrier的计数值设为2。我们需要使用CyclicBarrier实现线程间的相互等待,打印foo之前需要等待bar打印完成,打印bar之前需要等待foo打印完成,第一次需要先打印foo,因此我们可以在打印foo之后调用CyclicBarrier的await方法等待打印bar的线程,在打印bar之前调用await方法等待foo打印完成。但这还不够,因为线程都达到同步点后,有可能打印foo的线程比打印bar的线程快,在打印bar之前又打印了一次foo,因此连续打印了两个foo。因此我们还需要创建一个volatile变量作为条件变量,以保证在都到达同步点后,bar打印一定在foo打印之前,将该条件变量设为barExecuting
,或许会想到在打印bar之前的await()方法后,加上barExecuting = true
,表示bar线程要执行打印bar了,foo线程先等等,同时在打印foo之前加上while判断,若barExecuting = true
,则执行空循环等待。但这不行,有可能还没执行改变barExecuting
状态语句,foo就打印完了,因此在打印完foo之后,将barExecuting = true
,用foo线程自己来控制自己,这样就能保证一定不会出现连续的两个foo了。打印完bar之后,将barExecuting = false
,通知foo线程bar已经打印完了,foo线程退出空循环。
代码
class FooBar {
private int n;
public FooBar(int n) {
this.n = n;
}
private CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
private volatile boolean barExecuting = false;
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
while (barExecuting) {
}
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
barExecuting = true;
try {
cyclicBarrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
try {
cyclicBarrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
barExecuting = false;
}
}
}
3.Thread.yield()
Thread.yield()表示线程礼让,线程礼让是指在某个特定的时间点,让线程暂停抢占CPU资源的行为,运行状态/就绪状态——>阻塞状态,将CPU资源让给其他线程来使用
假如线程甲和线程乙在交替执行,某个时间点线程甲做出了礼让,所以在这个时间节点线程乙拥有了CPU资源,执行业务逻辑,但不代表线程甲一直暂停执行。线程甲只是在特定的时间节点礼让,过了时间节点,线程甲再次进入就绪状态,和线程乙争夺CPU资源
正确的思路是,当现在不应该是foo线程运行时,foo线程进行礼让,不应该是bar线程运行时,bar线程进行礼让。因此我们还需要创建一个volatile变量作为条件变量,以判断现在到底是foo线程该执行还是bar线程该执行,设该条件变量为fooCanExecute
,表示foo线程是否该执行,初值为true,在打印foo之前判断fooCanExecute
是否为true,若为true则打印foo,同时将fooCanExecute = false
,若打印前判断fooCanExecute
为false,则进行线程礼让。同理,在打印bar之前判断fooCanExecute
是否为false,若为false则打印bar,同时fooCanExecute = true
,若打印前判断fooCanExecute
为true,进行线程礼让。需注意循环变量i的恢复操作。
代码
class FooBar {
private int n;
public FooBar(int n) {
this.n = n;
}
private volatile boolean fooCanExecute = true;
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
if (fooCanExecute) {
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
fooCanExecute = false;
}
else {
Thread.yield();
i--;
}
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
if (!fooCanExecute) {
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
fooCanExecute = true;
} else {
Thread.yield();
i--;
}
}
}
}
4.ReentrantLock
打印foo或bar的代码区域作为不能两个线程同时执行的区域,将其用ReentrantLock上锁,但这还不够,因为若foo线程速度更快,可能会连续打印foo。因此还需要volatile变量作为条件变量,设为fooCanExecute
,初始为true,打印foo前判断其是否为true,打印完后将其变为false,bar线程的处理与其相反
由于ReentrantLock的可重复上锁和解锁的性质,下述代码执行过程中,某线程可能会不断上锁解锁,导致开销很大,因此该代码提交后会超时,这样使用ReentrantLock不是一种较好的实现方式
代码:
class FooBar {
private int n;
public FooBar(int n) {
this.n = n;
}
private volatile boolean fooCanExecute = true;
private ReentrantLock lock = new ReentrantLock(true);
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n;) {
lock.lock();
try {
if (fooCanExecute) {
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
fooCanExecute = false;
i++;
}
} finally {
lock.unlock();
}
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n;) {
lock.lock();
try {
if (!fooCanExecute) {
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
fooCanExecute = true;
i++;
}
} finally {
lock.unlock();
}
}
}
}
5.BlockingQueue
阻塞队列BlockingQueue是一个支持阻塞的插入和移出方法的队列,阻塞的插入指当队列满时,队列会阻塞插入元素的线程,直到队列不满。阻塞的移出是指当队列为空时,获取元素的线程会被阻塞,等待队列变为非空。
阻塞队列的put(e)
和take()
方法即对应阻塞的插入和移出
根据上述性质,我们可以知道阻塞队列很适合用于生产者消费者的场景。
我们也可以用阻塞队列完成本题,我们将打印foo和打印bar看作两个任务,创建两个阻塞队列表示任务队列,由于第一次需先打印foo,因此表示打印foo任务的阻塞队列先置入一个元素,表示bar任务的阻塞队列初始为空,在打印foo和bar之前,我们调用阻塞的移出方法,从任务队列中取元素,若能取到,则执行打印,否则阻塞。在打印完成后,向对方的任务队列中插入元素
JDK提供了7种阻塞队列,这里使用用链表实现的LinkedBlockingQueue
代码:
class FooBar {
private int n;
public FooBar(int n) {
this.n = n;
}
private LinkedBlockingQueue<Integer> fooQ = new LinkedBlockingQueue<Integer>() {{add(0);}};
private LinkedBlockingQueue<Integer> barQ = new LinkedBlockingQueue<>();
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n;i++) {
fooQ.take();
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
barQ.put(0);
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n;i++) {
barQ.take();
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
fooQ.put(0);
}
}
}
6.Synchronized + wait()/notify() 等待/通知机制
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。
等待/通知机制是依托于同步机制的,一般先使用synchronized获取对象O的锁,再调用wait()或notify()
单用wait()/notify()达不到效果,在打印前调用wait(),打印后调用notify(),两个线程都会在wait()处阻塞。因此我们还需要创建volatile变量表示条件变量,设为canBar
,表示bar线程是否能执行,初值为false,在打印foo前先判断canBar
是否为false(用if或while判断均可),若为false,则打印foo,将canBar
设为true,之后调用notifyAll()唤醒等待的bar线程。若canBar
为true,则调用wait()等待。Bar线程中的处理相反即可
代码:
class FooBar {
private int n;
public FooBar(int n) {
this.n = n;
}
private Object lock = new Object();
private volatile boolean canBar = false;
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n;i++) {
synchronized (lock) {
while (canBar) {
lock.wait();
}
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
canBar = true;
lock.notifyAll();
}
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n;i++) {
synchronized (lock) {
while (!canBar) {
lock.wait();
}
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
canBar = false;
lock.notifyAll();
}
}
}
}
7.LockSupport
LockSupport工具类定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能。
LockSupport定义了一组以park开头的方法用来阻塞当前线程,如LockSupport.park()
表示阻塞当前线程,以及LockSupport.unpark(Thread thread)
唤醒一个被阻塞的线程。
由于LockSupport的唤醒方法需要线程作为参数,因此创建一个ConcurrentHashMap保存线程,key为String,value为Thread,调用唤醒方法时参数从ConcurrentHashMap中取。
同样也需借助volatile变量作为条件变量以判断当前打印是否能执行,设为canBar
,初值为false。在打印foo之前,判断canBar
是否为false,若为false,则打印foo,canBar
设为true,并调用unpark方法唤醒bar线程。若打印前判断canBar
为true,则调用park方法阻塞自己。Bar线程的处理同理,相反即可。
代码
class FooBar {
private int n;
public FooBar(int n) {
this.n = n;
}
private volatile boolean canBar = false;
private Map<String, Thread> map = new ConcurrentHashMap<>();
public void foo(Runnable printFoo) throws InterruptedException {
map.put("foo", Thread.currentThread());
for (int i = 0; i < n;i++) {
while (canBar) {
LockSupport.park();
}
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
canBar = true;
LockSupport.unpark(map.get("bar"));
}
}
public void bar(Runnable printBar) throws InterruptedException {
map.put("bar", Thread.currentThread());
for (int i = 0; i < n;i++) {
while (!canBar) {
LockSupport.park();
}
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
canBar = false;
LockSupport.unpark(map.get("foo"));
}
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/153741.html