在多线程中线程的执行顺序是依靠哪个线程先获得到CUP的执行权谁就先执行,虽然说可以通过线程的优先权进行设置,但是他只是获取CUP执行权的概率高点,但是也不一定必须先执行。在这种情况下如何保证线程按照一定的顺序进行执行,今天就来一个大总结,分别介绍一下几种方式。
- 通过Object的wait和notify
- 通过Condition的awiat和signal
- 通过一个阻塞队列
- 通过两个阻塞队列
- 通过SynchronousQueue
- 通过线程池的Callback回调
- 通过同步辅助类CountDownLatch
- 通过同步辅助类CyclicBarrier
1、通过Object的wait和notify
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(); } } }
执行结果如下:
三、通过一个阻塞队列
上面的两个方法实现起来代码比较繁琐,如果通过阻塞队列来实现会更加简洁,这里采用常用的容量为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等等。
运行效果如下:
四、通过两个阻塞队列