只要了解過多線程,我們就知道線程開始的順序跟執(zhí)行的順序是不一樣的汉柒。如果只是創(chuàng)建三個線程然后執(zhí)行误褪,最后的執(zhí)行順序是不可預期的。這是因為在創(chuàng)建完線程之后碾褂,線程執(zhí)行的開始時間取決于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();
}
}
輸出結(jié)果:ACB/ABC/CBA...
那么我們該如何保證線程的順序執(zhí)行呢正塌?
如何保證線程的順序執(zhí)行?
1. 使用Thread.join()實現(xiàn)
Thread.join()
的作用是讓父線程等待子線程結(jié)束之后才能繼續(xù)運行嘀略。以上述例子為例,main()
方法所在的線程是父線程,在其中我們創(chuàng)建了3個子線程A,B,C,子線程的執(zhí)行相對父線程是異步的垃帅,不能保證順序性弛针。而對子線程使用Thread.join()
方法之后就可以讓父線程等待子線程運行結(jié)束后,再開始執(zhí)行父線程村斟,這樣子線程執(zhí)行被強行變成了同步的,我們用Thread.join()
方法就能保證線程執(zhí)行的順序性。
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();
}
}
輸出結(jié)果:ABC
2. 使用單線程線程池來實現(xiàn)
另一種保證線程順序執(zhí)行的方法是使用一個單線程的線程池窥淆,這種線程池中只有一個線程,相應的巍杈,內(nèi)部的線程會按加入的順序來執(zhí)行忧饭。
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();
}
}
輸出結(jié)果:ABC
3. 使用volatile關鍵字修飾的信號量實現(xiàn)
上面兩種的思路都是讓保證線程的執(zhí)行順序,讓線程按一定的順序執(zhí)行筷畦。這里介紹第三種思路词裤,那就是線程可以無序運行,但是執(zhí)行結(jié)果按順序執(zhí)行鳖宾。
你應該可以想到吼砂,三個線程都被創(chuàng)建并start()
,這時候三個線程隨時都可能執(zhí)行run()
方法鼎文。因此為了保證run()
執(zhí)行的順序性渔肩,我們肯定需要一個信號量來讓線程知道在任意時刻能不能執(zhí)行邏輯代碼。
另外拇惋,因為三個線程是獨立的周偎,這個信號量的變化肯定需要對其他線程透明抹剩,因此volatile關鍵字也是必須要的。
public class TicketExample2 {
//信號量
static volatile int ticket = 1;
//線程休眠時間
public final static int SLEEP_TIME = 1;
public static void foo(int name){
//因為線程的執(zhí)行順序是不可預期的蓉坎,因此需要每個線程自旋
while (true) {
if (ticket == name) {
try {
Thread.sleep(SLEEP_TIME);
//每個線程循環(huán)打印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();
}
}
執(zhí)行結(jié)果:
1 0
1 1
1 2
2 0
2 1
2 2
3 0
3 1
3 2
4. 使用Lock和信號量實現(xiàn)
此種方法的思想跟第三種方法是一樣的澳眷,都是不考慮線程執(zhí)行的順序而是考慮用一些方法控制線程執(zhí)行業(yè)務邏輯的順序。這里我們同樣用一個原子類型信號量ticket
蛉艾,當然你可以不用原子類型钳踊,這里我只是為了保證自增操作的線程安全。然后我們用了一個可重入鎖ReentrantLock
伺通。用來給方法加鎖箍土,當一個線程拿到鎖并且標識位正確的時候開始執(zhí)行業(yè)務邏輯,執(zhí)行完畢后喚醒下一個線程罐监。
這里我們不需要使用while進行自旋操作了吴藻,因為Lock可以讓我們喚醒指定的線程,所以改成if就可以實現(xiàn)順序的執(zhí)行弓柱。
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();
//因為線程的執(zhí)行順序是不可預期的沟堡,因此需要每個線程自旋
System.out.println("線程" + name + " 開始執(zhí)行");
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);
}
//執(zhí)行完畢,喚醒下一次矢空。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();
}
}
輸出結(jié)果:
線程2 開始執(zhí)行
當前標識位為1,線程2 開始等待
線程1 開始執(zhí)行
1
線程3 開始執(zhí)行
當前標識位為2,線程3 開始等待
線程2 被喚醒
2
線程3 被喚醒
3
上述的執(zhí)行結(jié)果并非唯一航罗,但可以保證打印的順序一定是123這樣的順序。
參考文章
java 多線程 實現(xiàn)多個線程的順序執(zhí)行 - Hoonick - 博客園 (cnblogs.com)
Java lock鎖的一些細節(jié)_筆記小屋-CSDN博客
VolatileCallSite (Java Platform SE 8 ) (oracle.com)
java保證多線程的執(zhí)行順序 - james.yj - 博客園 (cnblogs.com)