天天看點

多線程題分析

一位前輩發給我的原題為:

評測題目: 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。

線程間的通信:

  1. 共享變量,想到利用Java集合類Queue接口的實作。本質上也是基于共享變量。
  2. 線程間wait,notify,Condition的await與signal。

思路核心:每一個線程都知道另外一個線程在做什麼,知道它到什麼位置了。

代碼

  1. 使用鎖是為了保證一次隻有一個線程可以列印數。以防出現奇怪的問題。
  2. numberA和nuumberB都添加了volatile關鍵字,每次都讀取到最新的資料。雖然線程少基本不出現問題,但是以防萬一。
  3. 線程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

多線程題分析

最後

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