之前的一篇文章,我给出了关于多
线程应用的几个
例子:
都是基于Java里面Lock锁实现的,分别是:
(1)两个线程轮流打印奇数和偶数
(2)多个线程模拟买票
(3)模拟生产者
消费者
今天再抛砖引玉,思考一下如何在多个线程中,轮流打印特定顺序的信息多少次。
这类问题其实并不难,只要掌握了Java里面线程协作和锁的知识,就可以轻而易举的搞定:
根据这些,我们来假设一个场景,使用三个线程轮流打印ABC字符串3次。
解决思路:
首先需要声明3个线程,我们可以分别叫A线程,B线程,C线程:
在这里面:
A线程仅仅负责打印A。
B线程仅仅负责打印B。
C线程仅仅负责打印C。
但是呢,
他们必须是有顺序,也就是说A打印完之后,才能打印B,B打印完后才行打印C,这就涉及线程协作和通信的知识了,A线程打印完毕之后,要通知B线程打印,B线程打印完之后要通知C线程打印,如果有多轮的话,C线程打印完毕之后,还要通知A线程。以及控制多轮次数的终结,不能让程序陷入死
循环之中。
在仔细理一下:
(1)首先三个线程启动后,一定是A线程先打印。如果是其他线程先启动,则必须等待,线程间的通信,我们用共享变量来解决。(本质是通过共享
内存)
(2)A运行的时候,B和C都在等待
(3)B运行的时候,A和C都在等待
(4)C运行的时候,A和B都在等待
(5)A运行结束通知B运行
(6)B运行结束通知C运行
(7)C运行结束通知A运行
(8)同时,如果要控制几轮打印,则需要在运行时控制循环次数,因为C线程是每一轮的结束标志,循环次数的加和要在C线程里面做。
ok,主要的逻辑基本理清了,我们看下如何用代码实现,先看核心的类:
定义了共享的监视器对象,计数器,共享变量,然后定义了三个方法分别负责打印A,B,C,功能的实现主要用了synchronized + 监视器的wait,notifyAll方法。
class="java" name="code">```
static class PrintABC{
final Object monitor=new Object();
volatile int count=1;//轮次计数,从1开始,为了保证可见性,这里需要用volatile修饰
String id="A";//贡献的
int printCount ;
public PrintABC(int printCount) {
this.printCount = printCount;
}
public void printA() throws InterruptedException {
while (count < printCount) {
synchronized (monitor) {
while (!id.equals("A")) {
monitor.wait();
}
System.out.println(Thread.currentThread().getName() + "打印: " + id);
id = "B";
monitor.notifyAll();
}
}
}
public void printB() throws InterruptedException {
while (count < printCount) {
synchronized (monitor) {
while (!id.equals("B")) {
monitor.wait();
}
System.out.println(Thread.currentThread().getName() + "打印: " + id);
id = "C";
monitor.notifyAll();
}
}
}
public void printC() throws InterruptedException {
while (count < printCount +1) {//最后一次终结线程,需要多加一次
synchronized (monitor) {
while (!id.equals("C")) {
monitor.wait();
}
System.out.println(Thread.currentThread().getName() + "打印: " + id+"\n");
id = "A";
count=count+1;
monitor.notifyAll();
}
}
}
}
```
然后,我们看下,main方法如何编写:
```
public static void main(String[] args) {
PrintABC printABC=new PrintABC(3);
Thread t1=new Thread(()->{
try {
printABC.printA();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.setName("A线程");
Thread t2=new Thread(()->{
try {
printABC.printB();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t2.setName("B线程");
Thread t3=new Thread(()->{
try {
printABC.printC();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t3.setName("C线程");
t2.start();
t3.start();
t1.start();
}
```
这里我们设置了3,也就是仅仅打印3轮,就终止程序。结果如下:
```java
A线程打印: A
B线程打印: B
C线程打印: C
A线程打印: A
B线程打印: B
C线程打印: C
A线程打印: A
B线程打印: B
C线程打印: C
```
至此,这个问题就搞定了,我们思考下,因为这里面采用的是Java的内置锁synchronized来实现的,synchronized关键词虽然使用起来非常简单,但是由于它出现的早,本身也有一些缺点,细心的朋友可能已经
发现,上面的通信代码处用的是:
```
monitor.notifyAll();
```
注意这个地方,明明我们只需要唤醒一个线程,为什么需要notifyAll()而不是用notify(),这么做的主要目的是因为synchronized的监视器唤醒的线程是随机的,没办法精确到某个线程,所以它必须唤醒所有的线程,然后重新参与锁的竞争,这样就导致部分线程调度没必要的被交换了一次。
这个地方恰内置锁synchronized的一个弊端,这也是为什么在jdk5之后引入的Lock这样高级锁
接口,其相比synchronized在加锁的时候,主要优点是:
(1)提供了公平和非公平调度
(2)可中断
(3)可提供非阻塞
(4)可
超时
(5)提供了Condition更细粒度的,锁唤醒条件
队列
本文中的例子,完全可以用Lock接口+Condition来达到更细粒度的锁控制,也就是A线程执行完之后,仅仅只唤醒B线程,没有必要把C线程也唤醒,感兴趣的朋友可以思考下怎么实现。
有什么问题可以扫码关注微信公众号:我是攻城师(woshigcs) 关注公众号的朋友,可以加入我们的:攻城师互助交流群,一起学习!