Java中如何保證執行緒順序執行

六層樓發表於2021-05-14

只要瞭解過多執行緒,我們就知道執行緒開始的順序跟執行的順序是不一樣的。如果只是建立三個執行緒然後執行,最後的執行順序是不可預期的。這是因為在建立完執行緒之後,執行緒執行的開始時間取決於CPU何時分配時間片,執行緒可以看成是相對於的主執行緒的一個非同步操作。

public class FIFOThreadExample {
    public synchronized static void foo(String name) {
        System.out.print(name);
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> foo("A"));
        Thread thread2 = new Thread(() -> foo("B"));
        Thread thread3 = new Thread(() -> foo("C"));
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

輸出結果:ACB/ABC/CBA...

那麼我們該如何保證執行緒的順序執行呢?

如何保證執行緒的順序執行?

1. 使用Thread.join()實現

Thread.join()的作用是讓父執行緒等待子執行緒結束之後才能繼續執行。以上述例子為例,main()方法所在的執行緒是父執行緒,在其中我們建立了3個子執行緒A,B,C,子執行緒的執行相對父執行緒是非同步的,不能保證順序性。而對子執行緒使用Thread.join()方法之後就可以讓父執行緒等待子執行緒執行結束後,再開始執行父執行緒,這樣子執行緒執行被強行變成了同步的,我們用Thread.join()方法就能保證執行緒執行的順序性。

public class FIFOThreadExample {
    
    public static void foo(String name) {
        System.out.print(name);
    }

    public static void main(String[] args) throws InterruptedException{
        Thread thread1 = new Thread(() -> foo("A"));
        Thread thread2 = new Thread(() -> foo("B"));
        Thread thread3 = new Thread(() -> foo("C"));
        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
        thread3.start();
    }
}

輸出結果:ABC

2. 使用單執行緒執行緒池來實現

另一種保證執行緒順序執行的方法是使用一個單執行緒的執行緒池,這種執行緒池中只有一個執行緒,相應的,內部的執行緒會按加入的順序來執行。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FIFOThreadExample {

    public static void foo(String name) {
        System.out.print(name);
    }

    public static void main(String[] args) throws InterruptedException{
        Thread thread1 = new Thread(() -> foo("A"));
        Thread thread2 = new Thread(() -> foo("B"));
        Thread thread3 = new Thread(() -> foo("C"));
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.submit(thread1);
        executor.submit(thread2);
        executor.submit(thread3);
        executor.shutdown();
    }
}

輸出結果:ABC

3. 使用volatile關鍵字修飾的訊號量實現

上面兩種的思路都是讓保證執行緒的執行順序,讓執行緒按一定的順序執行。這裡介紹第三種思路,那就是執行緒可以無序執行,但是執行結果按順序執行。
你應該可以想到,三個執行緒都被建立並start(),這時候三個執行緒隨時都可能執行run()方法。因此為了保證run()執行的順序性,我們肯定需要一個訊號量來讓執行緒知道在任意時刻能不能執行邏輯程式碼。
另外,因為三個執行緒是獨立的,這個訊號量的變化肯定需要對其他執行緒透明,因此volatile關鍵字也是必須要的。

public class TicketExample2 {

    //訊號量
    static volatile int ticket = 1;
    //執行緒休眠時間
    public final static int SLEEP_TIME = 1;

    public static void foo(int name){
        //因為執行緒的執行順序是不可預期的,因此需要每個執行緒自旋
        while (true) {
            if (ticket == name) {
                try {
                    Thread.sleep(SLEEP_TIME);
                    //每個執行緒迴圈列印3次
                    for (int i = 0; i < 3; i++) {
                        System.out.println(name + " " + i);
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //訊號量變更
                ticket = name%3+1;
                return;

            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> foo(1));
        Thread thread2 = new Thread(() -> foo(2));
        Thread thread3 = new Thread(() -> foo(3));
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

執行結果:
1 0
1 1
1 2
2 0
2 1
2 2
3 0
3 1
3 2

4. 使用Lock和訊號量實現

此種方法的思想跟第三種方法是一樣的,都是不考慮執行緒執行的順序而是考慮用一些方法控制執行緒執行業務邏輯的順序。這裡我們同樣用一個原子型別訊號量ticket,當然你可以不用原子型別,這裡我只是為了保證自增操作的執行緒安全。然後我們用了一個可重入鎖ReentrantLock。用來給方法加鎖,當一個執行緒拿到鎖並且標識位正確的時候開始執行業務邏輯,執行完畢後喚醒下一個執行緒。
這裡我們不需要使用while進行自旋操作了,因為Lock可以讓我們喚醒指定的執行緒,所以改成if就可以實現順序的執行。

public class TicketExample3 {
    //訊號量
    AtomicInteger ticket = new AtomicInteger(1);
    public Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private Condition[] conditions = {condition1, condition2, condition3};

    public void foo(int name) {
        try {
            lock.lock();
            //因為執行緒的執行順序是不可預期的,因此需要每個執行緒自旋
            System.out.println("執行緒" + name + " 開始執行");
            if(ticket.get() != name) {
                try {
                    System.out.println("當前標識位為" + ticket.get() + ",執行緒" + name + " 開始等待");
                    //開始等待被喚醒
                    conditions[name - 1].await();
                    System.out.println("執行緒" + name + " 被喚醒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(name);
            ticket.getAndIncrement();
            if (ticket.get() > 3) {
                ticket.set(1);
            }
            //執行完畢,喚醒下一次。1喚醒2,2喚醒3
            conditions[name % 3].signal();
        } finally {
            //一定要釋放鎖
            lock.unlock();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        TicketExample3 example = new TicketExample3();
        Thread t1 = new Thread(() -> {
            example.foo(1);
        });
        Thread t2 = new Thread(() -> {
            example.foo(2);
        });
        Thread t3 = new Thread(() -> {
            example.foo(3);
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

輸出結果:
執行緒2 開始執行
當前標識位為1,執行緒2 開始等待
執行緒1 開始執行
1
執行緒3 開始執行
當前標識位為2,執行緒3 開始等待
執行緒2 被喚醒
2
執行緒3 被喚醒
3

上述的執行結果並非唯一,但可以保證列印的順序一定是123這樣的順序。

參考文章

java 多執行緒 實現多個執行緒的順序執行 - Hoonick - 部落格園 (cnblogs.com)
Java lock鎖的一些細節_筆記小屋-CSDN部落格
VolatileCallSite (Java Platform SE 8 ) (oracle.com)
java保證多執行緒的執行順序 - james.yj - 部落格園 (cnblogs.com)

相關文章