一位前輩發給我的原題為:
評測題目: 2個線程,一個線程輸出1-100這個範圍内的所有奇數,一個輸出1-100内所有的偶數。要求這些資料最終按照 1.2.3.4....48,49,51,50,53,52,55,54,.....98,97,100,99 這個順序輸出。
解
不是說自己擅自修改題目:
1-49是按順序列印,有規律可言
51,50,..這裡是奇數在前,偶數在後。但是後面變成了偶數在前奇數在後了。最後一部分不知道從哪裡開始沒有規律的。
分析:
看到題目,想到是利用多線程之間的通信,然後想到這兩個線程需要知道對方在什麼位置了。
Java實作線程的方式大體上有兩種:
- 繼承Thread,每個線程對象變量私有
- 實作Runable, 線程之間可以共享變量,自由度更高。
個人選擇了Runnable。
線程間的通信:
- 共享變量,想到利用Java集合類Queue接口的實作。本質上也是基于共享變量。
- 線程間wait,notify,Condition的await與signal。
思路核心:每一個線程都知道另外一個線程在做什麼,知道它到什麼位置了。
代碼
- 使用鎖是為了保證一次隻有一個線程可以列印數。以防出現奇怪的問題。
- numberA和nuumberB都添加了volatile關鍵字,每次都讀取到最新的資料。雖然線程少基本不出現問題,但是以防萬一。
- 線程A列印numberA,線程B列印numberB,間隔為2
1-49時:
依次列印:
A發現B比自己往前一個數,列印A,并且加2. 否則不處理
B發現A比自己超前一個數,列印B,并且加2,否則不處理
跳出來的時候:A=51,B=50,為什麼會這樣呢?
鎖會慢一些,當A=49,B=48的時候,兩個都進入了循環,但是同一時刻隻有B一個能進行列印與加2操作,然後A獲得鎖,列印,跳出循環。
50~100時:
A = 51,B = 50
A比B大1個數的時候,列印A,跟随上一步比較友善,A加2,否則不處理。
A比B大3個數的時候,列印B,然後B加2,否則不處理。
當B列印98的時候,A=101。B加2為100,再次列印B。
public class PrintNumber {
// 一個線程隻列印奇數 1-100, A線程
// 一個線程隻列印偶數 1-100, B線程
// 前1-49個數 A在前,B在後
// 50~100 B在後,A在前
//分析:線程之間通信,A知道B,B知道A。
public static void main(String[] args) {
NumberVariable numberVariable = new NumberVariable(1, 2);
Thread a = new Thread(numberVariable, "A");
Thread b = new Thread(numberVariable, "B");
a.start();
b.start();
}
static class NumberVariable implements Runnable {
/**
* 鎖對象,隻有持有鎖的線程才能列印數字。
*/
final Object lock = new Object();
volatile int numberA;
volatile int numberB;
public NumberVariable(int numberA, int numberB) {
this.numberA = numberA;
this.numberB = numberB;
}
/**
* 版本1:
* 分為兩個循環,加鎖是為了保證同一時刻隻有一個線程在列印數字,友善個人了解。
* 循環1:列印 1 2 3 4 5 ...48, 49,順序列印
* A與B隻能保證間隔為1,設定初始變量的時候已經設定間隔為1了。
* 如果:B先進來,B大于A不會列印
* 如果:A先進來,A剛好小于B,A列印,然後A變為3。 接下來B可以列印。然後B變為4,重複剛才的步驟。
* 當B為46的時候,A隻能為47,A變為49。然後B變為48。
* <p>
* 循環1跳出去的結果為A:51,B50。
* <p>
* 循環2:
* 1. 假設A先進來,A = 49,B = 50. 然後列印A,A = 53, B = 50,A保持不列印的狀态
* 2. 假設B先進來,A = 49,B = 50, B不列印,如果B比A小3,那麼已經列印過A的51值,列印50,B = 52
* ...
* A = 99 , B = 98才會列印A的值99, 此時A = 101
* A = 101, B = 98列印B98, B = 100
*/
public void version1() {
while (numberA <= 49 && numberB <= 48) {
//保證隻有一個線程在列印數字。
synchronized (lock) {
if ("A".equals(Thread.currentThread().getName())) {
//先判斷再列印數字,在A數字比B數字大1的時候才列印
if ((numberB - numberA) == 1) {
System.out.println("目前線程:" + Thread.currentThread().getName() + numberA);
numberA = numberA + 2;
}
} else if ("B".equals(Thread.currentThread().getName())) {
if ((numberA - numberB) == 1) {
System.out.println("目前線程:" + Thread.currentThread().getName() + numberB);
numberB = numberB + 2;
}
}
}
}
while (numberA <= 101 && numberB <= 100) {
//保證隻有一個線程在列印數字。
synchronized (lock) {
if ("A".equals(Thread.currentThread().getName())) {
//先判斷再列印數字,在A數字比B數字大1的時候才列印
if (numberA - numberB == 1 && numberA != 101) {
System.out.println("目前線程:" + Thread.currentThread().getName() + numberA);
numberA = numberA + 2;
}
} else if ("B".equals(Thread.currentThread().getName())) {
if ((numberA - numberB) == 3) {
System.out.println("目前線程:" + Thread.currentThread().getName() + numberB);
numberB = numberB + 2;
if (numberB == 100) {
System.out.println("目前線程:" + Thread.currentThread().getName() + numberB);
numberB = numberB + 2;
}
}
}
}
}
}
/**
* 考慮使用wait方式。
*/
public void run() {
version1();
}
}
}
額外(非解題)
想着使用集合類,wait,notify來寫的,但是寫的時候好像用不到,兩者之間已經互相知道了,再去用wait,notify通信有些多餘。
另外一種方式:
- 隊列,保證隊列的有序,1,2,3,4,然後才能列印5.
- 奇數列印之後wait,然後通知偶數線程列印。偶數線程列印之後wait通知奇數線程列印。并且把數推入隊列
簡化一下思路:隻按順序列印,不分成兩部分了。想到的僞代碼
odd:
while(currentEvenNumber < 101)
if queue.last % 2 == 0,queue.last = (currentEven - 1)
print currentEvenNumber && queue.last = currentEvenNumber;
currentEvenNumber = currentEvenNumber + 2;
oddCondition.wait();
evenCondition.signal();
even:
while(currentOddNumber < 100)
if queue.last % 2 == 1,queue.last = (currentOdd - 1)
print currentOddNumber && queue.last = currentOddNumber;
currentOddNumber = currentOddNumber + 2;
evenCondition.wait();
oddCondition.signal();
既然僞代碼都寫了,真實的代碼也幹一下吧。
代碼比較粗糙,這個肚子餓了,簡答實作一下,非題目。
public class PrintNumer2 {
private static ReentrantLock lock = new ReentrantLock();
private static Condition evendition = lock.newCondition();
private static Condition odddition = lock.newCondition();
private static LinkedList<Integer> integers = new LinkedList<Integer>();
public static void main(String[] args) {
integers.add(0);
new ThreadA().start();
new ThreadB().start();
}
static class ThreadA extends Thread{
private int num = 1;
@Override
public void run() {
super.run();
while (num < 101){
if (integers.getLast() % 2 == 0 && integers.getLast() == (num - 1)){
lock.lock();
System.out.println(Thread.currentThread().getName() + ":" + num);
integers.add(num);
num += 2;
evendition.signal();
try {
odddition.await();
} catch (InterruptedException e) {
// e.printStackTrace();
}
lock.unlock();
}
}
lock.lock();
evendition.signal();
lock.unlock();
}
}
static class ThreadB extends Thread{
private int num = 2;
@Override
public void run() {
super.run();
while (num < 101){
if (integers.getLast() % 2 == 1 && integers.getLast() == (num - 1)){
lock.lock();
System.out.println(Thread.currentThread().getName() + ":" + num);
integers.add(num);
num += 2;
odddition.signal();
try {
evendition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
}
}
lock.lock();
odddition.signal();
lock.unlock();
}
}
}
正常運作

image.png
最後
修改了一點點題目,勉強實作了要求,更好的解法歡迎留言。