Java多线程:交替打印字符串
文章目录
问题
方法
1.CyclicBarrier
首先需要知道,每个数字对应调用四个方法中的某一个打印语句。对于四个方法,我们可以使其依次遍历1~n,但我们需要满足从小到大按序打印,由于线程运行的速度有快有慢,那么我们必须得建立一个基准,我们可以使用CyclicBarrier让四个线程每次从同一起跑线出发。也就是说我们只需要让四个线程调用的四个方法同时判断同一个数字即可,先判断完的等待后判断完的,四个方法都判断完后再判断下一个数字,利用CyclicBarrier可以实现这一点
代码:
class FizzBuzz {
private int n;
public FizzBuzz(int n) {
this.n = n;
}
public CyclicBarrier cyclicBarrier = new CyclicBarrier(4);
// printFizz.run() outputs "fizz".
public void fizz(Runnable printFizz) throws InterruptedException {
int i;
for (i = 1;i <= n;i++) {
if (i % 3 == 0 && i % 5 != 0) {
printFizz.run();
}
try {
cyclicBarrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
// printBuzz.run() outputs "buzz".
public void buzz(Runnable printBuzz) throws InterruptedException {
int i;
for (i = 1;i <= n;i++) {
if (i % 5 == 0 && i % 3 != 0) {
printBuzz.run();
}
try {
cyclicBarrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
// printFizzBuzz.run() outputs "fizzbuzz".
public void fizzbuzz(Runnable printFizzBuzz) throws InterruptedException {
int i;
for (i = 1;i <= n;i++) {
if (i % 5 == 0 && i % 3 == 0) {
printFizzBuzz.run();
}
try {
cyclicBarrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
// printNumber.accept(x) outputs "x", where x is an integer.
public void number(IntConsumer printNumber) throws InterruptedException {
int i;
for (i = 1;i <= n;i++) {
if (i % 5 != 0 && i % 3 != 0) {
printNumber.accept(i);
}
try {
cyclicBarrier.await();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
2.Semaphore
首先需要知道,每个数字对应调用四个方法中的某一个打印语句。对于四个方法,我们可以使其依次遍历1~n,但我们需要满足从小到大按序打印。由于线程运行的速度有快有慢,那么我们必须得建立一个基准,这里我们可以使用信号量让四个线程以执行number方法的线程为基准,当某个线程运行速度快于基准线程时,若它要进行打印,我们让其等待基准线程向其发送信号,才能进行打印。同时需要注意,我们也得控制基准线程的步伐,基准线程需等待其他线程打印完成之后,才能进行下一次判断,否则若基准线程过快,可能其他线程还没打印,基准线程就打印了多次。
我们使用信号量fizzbuzzRun,buzzRun,fizzRun
分别表示用于控制fizzbuzz,buzz,fizz
方法对应线程速度的信号量,初始值为0,在打印前,必须获得对应的信号量,而信号量由number方法release,因此若这三个线程速度较快,也不会提前输出,而是会等待number方法对应的线程向其发送信号。使用信号量number
用于控制number
方法对应线程的速度,初始值为1,在对当前遍历的数字进行整除判断前,需要获得number
信号量,number
信号量一方面是在其他三个线程打印完成后release的,另一方面是在number
方法打印完成后release的,也就是进行一次打印后都要释放number
信号量,这样才能保证number
方法能顺利进行下一个数字的判断。
代码:
class FizzBuzz {
private int n;
public FizzBuzz(int n) {
this.n = n;
}
public Semaphore fizzbuzzRun = new Semaphore(0);
public Semaphore buzzRun = new Semaphore(0);
public Semaphore fizzRun = new Semaphore(0);
public Semaphore number = new Semaphore(1);
// printFizz.run() outputs "fizz".
public void fizz(Runnable printFizz) throws InterruptedException {
int i;
for (i = 1;i <= n;i++) {
if (i % 3 == 0 && i % 5 != 0) {
fizzRun.acquire();
printFizz.run();
number.release();
}
}
}
// printBuzz.run() outputs "buzz".
public void buzz(Runnable printBuzz) throws InterruptedException {
int i;
for (i = 1;i <= n;i++) {
if (i % 5 == 0 && i % 3 != 0) {
buzzRun.acquire();
printBuzz.run();
number.release();
}
}
}
// printFizzBuzz.run() outputs "fizzbuzz".
public void fizzbuzz(Runnable printFizzBuzz) throws InterruptedException {
int i;
for (i = 1;i <= n;i++) {
if (i % 5 == 0 && i % 3 == 0) {
fizzbuzzRun.acquire();
printFizzBuzz.run();
number.release();
}
}
}
// printNumber.accept(x) outputs "x", where x is an integer.
public void number(IntConsumer printNumber) throws InterruptedException {
int i;
for (i = 1;i <= n;i++) {
number.acquire();
if (i % 5 != 0 && i % 3 != 0) {
printNumber.accept(i);
number.release();
} else if (i % 5 == 0 && i % 3 == 0) {
fizzbuzzRun.release();
} else if (i % 5 == 0) {
buzzRun.release();
} else {
fizzRun.release();
}
}
}
}
3.Thread.yield()
思路与使用Semaphore的方法类似。对于fizz方法,我们使其只遍历1 ~ n中可以被3整除且不被15整除的数,同理,对于buzz方法,使其遍历1 ~ n中可以被5整除且不被15整除的数,fizzbuzz方法遍历可以被15整除的数。
以number方法对应的线程作为基准,当其他3个线程较快时,只有收到number方法对应线程发送的信号才能进行打印,否则循环调用Thread.yield()进行等待。number方法依次遍历1 ~ n,当number方法对应的线程较快时,只有当其他三个线程中的一个打印完成,向number方法对应的线程发送信号,number线程才能进行当前遍历数字的判断,否则循环调用Thread.yield()进行等待。我们可以用一个volatile条件变量state作为信号,当state为不同的值时,不同的线程可以进行操作,当某个线程当前不允许操作时,循环调用Thread.yield()进行等待。
设state = 0
表示number线程能进行当前遍历数字的整除判断等操作,注意在number线程打印完之后,不需要重新设置state = 0
,因为此时state已经为0。在使用Semaphore的方法2中,我们有释放信号量的操作,是因为如果不释放,下一次number线程获取信号量会发生阻塞,而此时volatile变量并不是信号量,需要注意两者之间的区别。
代码:
class FizzBuzz {
private int n;
public FizzBuzz(int n) {
this.n = n;
}
public volatile int state = 0;
// printFizz.run() outputs "fizz".
public void fizz(Runnable printFizz) throws InterruptedException {
int i;
for (i = 3;i <= n;i += 3) {
if (i % 15 == 0) {
continue;
}
while (state != 3) {
Thread.yield();
}
printFizz.run();
state = 0;
}
}
// printBuzz.run() outputs "buzz".
public void buzz(Runnable printBuzz) throws InterruptedException {
int i;
for (i = 5;i <= n;i += 5) {
if (i % 15 == 0) {
continue;
}
while (state != 5) {
Thread.yield();
}
printBuzz.run();
state = 0;
}
}
// printFizzBuzz.run() outputs "fizzbuzz".
public void fizzbuzz(Runnable printFizzBuzz) throws InterruptedException {
int i;
for (i = 15;i <= n;i += 15) {
while (state != 15) {
Thread.yield();
}
printFizzBuzz.run();
state = 0;
}
}
// printNumber.accept(x) outputs "x", where x is an integer.
public void number(IntConsumer printNumber) throws InterruptedException {
int i;
for (i = 1;i <= n;i++) {
while (state != 0) {
Thread.yield();
}
if (i % 3 != 0 && i % 5 != 0) {
printNumber.accept(i);
} else if (i % 3 == 0 && i % 5 == 0) {
state = 15;
} else if (i % 3 == 0) {
state = 3;
} else {
state = 5;
}
}
}
}
4.ReentrantLock + Condition
Condition定义了等待(await()
) / 通知(signal() / signalAll()
)两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。 Condition对象是由Lock对象(调用Lock对象的newCondition()
方法)创建出来的,也就是说,Condition是依赖于Lock对象的
当Condition对象调用await()
方法后,当前线程会释放锁并在此等待, 而其他线程调用Condition对象的signal()
方法,通知当前线程后,当前线程才从await()
方法返回,并且在返回前已经获取了锁。
思路还是和方法2、3中的类似,以number线程作为基准,不过这次我们用ReentrantLock + Condition实现,这里有两种实现方式:一是使用3个Condition,二是使用同一个Condition再加上一个volatile条件变量state
先看使用3个Condition的实现,由一个锁创建出3个Condition分别控制number线程以外的三个线程是否要进行打印,在打印之前,调用Condition的await方法等待number线程发送信号。number线程根据当前遍历的数字的整除情况,调用对应Condition的signal方法唤醒线程。
代码1:
class FizzBuzz {
private int n;
public FizzBuzz(int n) {
this.n = n;
}
public Lock lock = new ReentrantLock();
Condition conditionFizz = lock.newCondition();
Condition conditionBuzz = lock.newCondition();
Condition conditionFizzBuzz = lock.newCondition();
// printFizz.run() outputs "fizz".
public void fizz(Runnable printFizz) throws InterruptedException {
int i;
for (i = 3;i <= n;i += 3) {
if (i % 15 == 0) {
continue;
}
lock.lock();
try {
conditionFizz.await();
printFizz.run();
} finally {
lock.unlock();
}
}
}
// printBuzz.run() outputs "buzz".
public void buzz(Runnable printBuzz) throws InterruptedException {
int i;
for (i = 5;i <= n;i += 5) {
if (i % 15 == 0) {
continue;
}
lock.lock();
try {
conditionBuzz.await();
printBuzz.run();
} finally {
lock.unlock();
}
}
}
// printFizzBuzz.run() outputs "fizzbuzz".
public void fizzbuzz(Runnable printFizzBuzz) throws InterruptedException {
int i;
for (i = 15;i <= n;i += 15) {
lock.lock();
try {
conditionFizzBuzz.await();
printFizzBuzz.run();
} finally {
lock.unlock();
}
}
}
// printNumber.accept(x) outputs "x", where x is an integer.
public void number(IntConsumer printNumber) throws InterruptedException {
int i;
for (i = 1;i <= n;i++) {
lock.lock();
try {
if (i % 3 != 0 && i % 5 != 0) {
printNumber.accept(i);
} else if (i % 3 == 0 && i % 5 == 0) {
conditionFizzBuzz.signalAll();
} else if (i % 3 == 0) {
conditionFizz.signalAll();
} else {
conditionBuzz.signalAll();
}
} finally {
lock.unlock();
}
}
}
}
初学JUC时,可能会对上述代码产生几个疑问:
- 四个线程对同一个lock上锁,每次只有一个线程能执行,若是number线程外的线程执行了,不会死锁吗?
不会死锁,需要注意,当Condition对象调用await()
方法后,当前线程会释放锁并在此等待,因此阻塞后,其他线程将获得它所释放的锁而得以执行 - 若number线程速度快,会不会当其他三个线程中的一个还没打印完成时,number线程打印了两次?
不可能出现这种情况,需要注意, 当number线程调用Condition对象的signal()
方法,通知被阻塞线程后,被阻塞线程从await()
方法返回,并且在返回前已经获取了锁。也就是说,若number线程执行了Condition对象的signal()
方法,通知了被阻塞线程,被阻塞线程将获得锁,在其没有释放之前,number线程的下一次遍历操作会被阻塞,因此number线程一定是等待其唤醒的线程打印完并释放锁后,才会执行接下来的操作。
我们再来看看使用同一个Condition再加上一个volatile条件变量state的实现。和使用Thread.yield()的方法3类似,我们可以用一个volatile条件变量state作为信号,当state为不同的值时,不同的线程可以进行操作,用while循环判断state是否满足要求,若不满足,则调用Condition对象的await()
方法。
代码2:
class FizzBuzz {
private int n;
public FizzBuzz(int n) {
this.n = n;
}
public Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public volatile int state = 0;
// printFizz.run() outputs "fizz".
public void fizz(Runnable printFizz) throws InterruptedException {
int i;
for (i = 3;i <= n;i += 3) {
if (i % 15 == 0) {
continue;
}
lock.lock();
try {
while (state != 3) {
condition.await();
}
printFizz.run();
state = 0;
condition.signalAll();
} finally {
lock.unlock();
}
}
}
// printBuzz.run() outputs "buzz".
public void buzz(Runnable printBuzz) throws InterruptedException {
int i;
for (i = 5;i <= n;i += 5) {
if (i % 15 == 0) {
continue;
}
lock.lock();
try {
while (state != 5) {
condition.await();
}
printBuzz.run();
state = 0;
condition.signalAll();
} finally {
lock.unlock();
}
}
}
// printFizzBuzz.run() outputs "fizzbuzz".
public void fizzbuzz(Runnable printFizzBuzz) throws InterruptedException {
int i;
for (i = 15;i <= n;i += 15) {
lock.lock();
try {
while (state != 15) {
condition.await();
}
printFizzBuzz.run();
state = 0;
condition.signalAll();
} finally {
lock.unlock();
}
}
}
// printNumber.accept(x) outputs "x", where x is an integer.
public void number(IntConsumer printNumber) throws InterruptedException {
int i;
for (i = 1;i <= n;i++) {
lock.lock();
try {
while (state != 0) {
condition.await();
}
if (i % 3 != 0 && i % 5 != 0) {
printNumber.accept(i);
} else if (i % 3 == 0 && i % 5 == 0) {
state = 15;
} else if (i % 3 == 0) {
state = 3;
} else {
state = 5;
}
condition.signalAll();
} finally {
lock.unlock();
}
}
}
}
初学JUC时,将代码2与代码1对比,可能会对上述代码产生疑问:
- 为什么要设置
state = 0
的情况,当state != 0
时,number线程调用condition.await()
进行等待,number线程外的三个线程在打印完后要将state设为0,并调用condition.signalAll()
方法唤醒阻塞线程?在代码1之后,不是说了从condition.await()
返回时,线程已经获得锁了吗,那么number线程不是不会进行下一次遍历了吗?
注意这里等待同一个condition的线程可能不只一个,若其中一个线程被唤醒,但发现state不满足条件,它又会调用condition.await()
阻塞自己,此时会释放锁,而该释放的锁有可能会被number线程抢到,若不进行state = 0
的判断,可能会出现number线程多次打印的情况,当number线程抢到锁时,由于此时state != 0
,number线程将阻塞自己,同时释放锁,经过一系列试探的过程,最终state值所对应的正确的线程将得到锁并打印,同时唤醒等待的number线程
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/153737.html