前言: 在多線程中線程的執行順序是依靠哪個線程先獲得到CUP的執行權誰就先執行,雖然說可以通過線程的優先權進行設置,但是他只是獲取CUP執行權的概率高點,但是也不一定必須先執行。在這種情況下如何保證線程按照一定的順序進行執行,今天就來一個大總結,分別介紹一下幾種方式。 一、通過Object的wait ...
前言:
在多線程中線程的執行順序是依靠哪個線程先獲得到CUP的執行權誰就先執行,雖然說可以通過線程的優先權進行設置,但是他只是獲取CUP執行權的概率高點,但是也不一定必須先執行。在這種情況下如何保證線程按照一定的順序進行執行,今天就來一個大總結,分別介紹一下幾種方式。
一、通過Object的wait和notify
二、通過Condition的awiat和signal
三、通過一個阻塞隊列
四、通過兩個阻塞隊列
五、通過SynchronousQueue
六、通過線程池的Callback回調
七、通過同步輔助類CountDownLatch
八、通過同步輔助類CyclicBarrier
一、通過Object的wait和notify
寫一個測試了Test,加上main方法,在寫一個內部類Man進行測試。main方法如下,他進行創建兩個線程,傳進去Runnable對象。
public static boolean flag = false;
public static int num = 0;
public static void main(String[] args) {
Man man = new Man();
new Thread(() -> {
man.getRunnable1();
}).start();
new Thread(() -> {
man.getRunnable2();
}).start();
}
getRunnable1和getRunnable2分別表示兩個需要執行的任務,在兩個線程中進行,方法1用於數據的生產,方法二用於數據的獲取,數據的初始值為num = 0,為了保證生產和獲取平衡需要使用wait和notify方法,這兩個方法的使用必須是要加鎖的,因此使用synchronized進行加鎖使用,為了演示這個效果,我們加上一個sleep方法模擬處理時間,如下:
public static class Man {
public synchronized void getRunnable1() {
for (int i = 0; i < 20; i++) {
while (flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生產出:" + (++num) + "個");
flag = true;
notify();
}
}
public synchronized void getRunnable2() {
for (int i = 0; i < 20; i++) {
while (!flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//模擬載入時間
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("取出出:" + (num--) + "個");
System.out.println("------------------");
flag = false;
notify();
}
}
}
分析它的載入流程,從方法1進行分析,由於flag的初始條件為false,所以方法1不進入等待,直接進行生產,生產完成成之後,更新flag的值為true,同時notify下一個方法2的wait方法,使其變為喚醒狀態。這時候由於方法1加鎖了,無法執行方法1其他部分,當方法1執行完畢,方法1才有可能執行,但是方法1的flag已經為true,進入到wait裡面又處於阻塞狀態,所以這時候只能執行方法2了。由於方法2被喚醒了,阻塞解除,接下來就獲取數據,當獲取完畢又再次讓flag變為false,notify方法1解除阻塞,再次執行方法1,就這樣不斷的迴圈,保證了不同線程的有序執行,直到程式終止。
運行效果如下:
二、通過Condition的awiat和signal
上面第一個的實現是一個阻塞,一個等待的方式保證線程有序的執行,但是不能進行兩個線程之間進行通信,而接下來介紹的Condition就具備這樣的功能。要獲取Condition對象首先先得獲取Lock對象,他是在jdk1.5之後增加的,比synchronized性能更好的一種鎖機制。和上面的類似,拷貝一份代碼,看看main方法:
public static boolean flag = false;
public static int num = 0;
public static void main(String[] args) {
Man man = new Man();
new Thread(() -> {
man.getRunnable1();
}).start();
new Thread(() -> {
man.getRunnable2();
}).start();
}
情況和第一個實現方法分析一致,這裡不重覆了。主要看內部類Man中的方法1和方法2。先手創建鎖對象,把synchronized改為使用Lock加鎖,其次通過Lock創建Condition對象,替換掉Object類的wait方法為Condition的await方法,最後換掉notify方法為signal方法即可,執行原理和上面分析一致,代碼如下:
public static class Man {
public static ReentrantLock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
public void getRunnable1() {
lock.lock();
try {
for (int i = 0; i < 20; i++) {
while (flag) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生產出:" + (++num) + "個");
flag = true;
condition.signal();
}
} finally {
lock.lock();
}
}
public void getRunnable2() {
lock.lock();
try {
for (int i = 0; i < 20; i++) {
while (!flag) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("取出出:" + (num--) + "個");
System.out.println("------------------");
flag = false;
condition.signal();
}
} finally {
lock.unlock();
}
}
}
執行結果如下:
這是我的iOS開發交流群:519832104不管你是小白還是大牛歡迎入駐,可以一起分享經驗,討論技術,共同學習成長!
另附上一份各好友收集的大廠面試題,需要iOS開發學習資料、面試真題,進群即可自行下載!
點擊此處,立即與iOS大牛交流學習
三、通過一個阻塞隊列
上面的兩個方法實現起來代碼比較繁瑣,如果通過阻塞隊列來實現會更加簡潔,這裡採用常用的容量為64的ArrayBlockingQueue來實現。main方法如下:
public static void main(String[] args) {
Man man = new Man();
new Thread(() -> {
man.getRunnable1();
}).start();
new Thread(() -> {
man.getRunnable2();
}).start();
}
主要來看Man中的方法1和方法2,方法1中生產數據,這裡把生產的數據存進隊列裡面,同時方法2進行取數據,如果方法1放滿了或者方法2取完了就會被阻塞住,等待方法1生產好了或者方法2取出了,然後再進行。代碼如下:
public static class Man {
ArrayBlockingQueue queue = new ArrayBlockingQueue<Integer>(64);
public void getRunnable1() {
for (int i = 0; i < 8; i++) {
System.out.println("生產出:" + i + "個");
try {
queue.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("---------------生產完畢-----------------");
}
public void getRunnable2() {
for (int i = 0; i < 8; i++) {
try {
int num = (int) queue.take();
System.out.println("取出出:" + num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
很明顯使用阻塞隊列代碼精煉了很多,在這還可以發現這個阻塞隊列是具有緩存功能的,想很多Android中網路訪問框架內部就是使用這個進行緩存的,例如Volley、Okhttp等等。
運行效果如下:
四、通過兩個阻塞隊列
使用一個阻塞隊列能夠實現線程同步的功能,兩個阻塞隊列也可以實現線程同步。原理是ArrayBlockingQueue他是具有容量的,如果把他的容量定位1則意味著他只能放進去一個元素,第二個方進行就會就會被阻塞。按照這個原理進行來實現,定義兩個容量為1的阻塞隊列ArrayBlockingQueue,一個存放數據,另一個用於控制次序。main方法和上面一致,主要來看看Man類中的兩個方法:
static class Man {
//數據的存放
ArrayBlockingQueue queue1 = new ArrayBlockingQueue<Integer>(1);
//用於控製程序的執行
ArrayBlockingQueue queue2 = new ArrayBlockingQueue<Integer>(1);
{
try {
//queue2放進去一個元素,getRunnable2阻塞
queue2.put(22222);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void getRunnable1() {
new Thread(() -> {
for (int j = 0; j < 20; j++) {
try {
//queue1放進一個元素,getRunnable1阻塞
queue1.put(j);
System.out.println("存放 線程名稱:" + Thread.currentThread().getName() + "-數據為-" + j);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
//queue2取出元素,getRunnable2進入
queue2.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
public void getRunnable2() {
new Thread(() -> {
for (int j = 0; j < 20; j++) {
try {
//queue2放進一個元素,getRunnable2阻塞
queue2.put(22222);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
//queue1放進一個元素,getRunnable1進入
int i = (int) queue1.take();
System.out.println("獲取 線程名稱:" + Thread.currentThread().getName() + "-數據為-" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
再次提醒queue2用於控製程序的執行次序,並無實際含義。最後看看運行效果,存一個、取一個很清晰,如下:
五、通過SynchronousQueue
SynchronousQueue不同於一般的數據等線程,而是線程等待數據,他是一個沒有數據緩衝的BlockingQueue,生產者線程對其的插入操作put必須等待消費者的移除操作take,反過來也一樣。通過這一特性來實現一個多線程同步問題的解決方案,代碼如下:
/**
* 使用阻塞隊列SynchronousQueue
* offer將數據插入隊尾
* take取出數據,如果沒有則阻塞,直到有數據在獲取到
*/
public static void test() {
SynchronousQueue queue = new SynchronousQueue();
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
queue.offer(9);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
try {
int take = (int) queue.take();
System.out.println(take);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
子線程中進行設置數據,而主線程獲取數據,如果子線程沒執行完畢,子線程沒有執行完畢主線程就會被阻塞住不能執行下一步。
六、通過線程池的Callback回調
線上程的創建中,有一種創建方法可以返回線程結果,就是callback,他能返回線程的執行結果,通過子線程返回的結果進而在主線程中進行操作,也是一種同步方法,這種同步在Android中特別適用,例如Android中的AsyncTask源碼中任務的創建部分。代碼如下:
private static void test() {
ExecutorService executorService = Executors.newFixedThreadPool(5);
Future<Boolean> submit = executorService.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return false;
}
});
try {
if (submit.get()) {
System.out.println(true);
} else {
System.out.println(false);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
七、通過同步輔助類CountDownLatch
CountDownLatch是一個同步的輔助類,允許一個或多個線程,等待其他一組線程完成操作,再繼續執行。他類實際上是使用計數器的方式去控制的,在創建的時候傳入一個int數值每當我們調用countDownt()方法的時候就使得這個變數的值減1,而對於await()方法則去判斷這個int的變數的值是否為0,是則表示所有的操作都已經完成,否則繼續等待。可以理解成倒計時鎖。
public class Test7 {
public static void main(String[] args) {
//啟動兩個線程,分別執行完畢之後再執行主線程
CountDownLatch countDownLatch = new CountDownLatch(2);
//線程1執行
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "線程執行完畢");
countDownLatch.countDown();
});
//線程2執行
Thread thread2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "線程執行完畢");
countDownLatch.countDown();
});
thread1.start();
thread2.start();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//執行主線程
System.out.println("主線程執行完畢");
}
}
結果如下:
八、通過同步輔助類CyclicBarrier
CyclicBarrier是一個同步的輔助類,和上面的CountDownLatch比較類似,不同的是他允許一組線程相互之間等待,達到一個共同點,再繼續執行。可看成是個障礙,所有的線程必須到齊後才能一起通過這個障礙。
public class Test8 {
public static void main(String[] args) {
//啟動兩個線程,分別執行完畢之後再執行主線程
CyclicBarrier barrier = new CyclicBarrier(2, () -> {
//執行主線程
System.out.println("主線程執行完畢");
});
//線程1執行
Thread thread1 = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "線程執行完畢");
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
//線程2執行
Thread thread2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "線程執行完畢");
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
}
}
運行結果: