先看一个问题:
有两个线程, 子线程先执行 10 次, 然后主线程执行 5 次, 然后再切换到子线程执行 10, 再主线程执行 5 次...... 如此往返执行 50 次.
看完这个问题, 很明显要用到线程间的通信了, 先分析一下思路: 首先肯定要有两个线程, 然后每个线程中肯定有个 50 次的循环, 因为每个线程都要往返执行任务 50 次, 主线程的任务是执行 5 次, 子线程的任务是执行 10 次. 线程间通信技术主要用到 wait() 方法和 notify() 方法. wait() 方法会导致当前线程等待, 并释放所持有的锁, notify() 方法表示唤醒在此对象监视器上等待的单个线程. 下面来一步步完成这道线程间通信问题.
首先不考虑主线程和子线程之间的通信, 先把各个线程所要执行的任务写好:
- public class TraditionalThreadCommunication {
- public static void main(String[] args) {
- // 开启一个子线程
- new Thread(new Runnable() {
- @Override
- public void run() {
- for(int i = 1; i <= 50; i ++) {
- synchronized (TraditionalThreadCommunication.class) {
- // 子线程任务: 执行 10 次
- for(int j = 1;j <= 10; j ++) {
- System.out.println("sub thread sequence of" + j + ", loop of" + i);
- }
- }
- }
- }
- }).start();
- //main 方法即主线程
- for(int i = 1; i <= 50; i ++) {
- synchronized (TraditionalThreadCommunication.class) {
- // 主线程任务: 执行 5 次
- for(int j = 1;j <= 5; j ++) {
- System.out.println("main thread sequence of" + j + ", loop of" + i);
- }
- }
- }
- }
- }
如上, 两个线程各有 50 次大循环, 执行 50 次任务, 子线程的任务是执行 10 次, 主线程的任务是执行 5 次. 为了保证两个线程间的同步问题, 所以用了 synchronized 同步代码块, 并使用了相同的锁: 类的字节码对象. 这样可以保证线程安全. 但是这种设计不太好, 就像我在上一节的死锁中写的一样, 我们可以把线程任务放到一个类中, 这种设计的模式更加结构化, 而且把不同的线程任务放到同一个类中会很容易解决同步问题, 因为在一个类中很容易使用同一把锁. 所以把上面的程序修改一下:
- public class TraditionalThreadCommunication {
- public static void main(String[] args) {
- Business bussiness = new Business(); //new 一个线程任务处理类
- // 开启一个子线程
- new Thread(new Runnable() {
- @Override
- public void run() {
- for(int i = 1; i <= 50; i ++) {
- bussiness.sub(i);
- }
- }
- }).start();
- //main 方法即主线程
- for(int i = 1; i <= 50; i ++) {
- bussiness.main(i);
- }
- }
- }
- // 要用到的共同数据 (包括同步锁) 或共同的若干个方法应该归在同一个类身上, 这种设计正好体现了高类聚和程序的健壮性.
- class Business {
- public synchronized void sub(int i) {
- for(int j = 1;j <= 10; j ++) {
- System.out.println("sub thread sequence of" + j + ", loop of" + i);
- }
- }
- public synchronized void main(int i) {
- for(int j = 1;j <= 5; j ++) {
- System.out.println("main thread sequence of" + j + ", loop of" + i);
- }
- }
经过这样修改后, 程序结构更加清晰了, 也更加健壮了, 只要在两个线程任务方法上加上 synchronized 关键字即可, 用的都是 this 这把锁. 但是现在两个线程之间还没有通信, 执行的结果是主线程循环执行任务 50 次, 然后子线程再循环执行任务 50 次, 原因很简单, 因为有 synchronized 同步.
下面继续完善程序, 让两个线程之间完成题目中所描述的那样通信:
- public class TraditionalThreadCommunication {
- public static void main(String[] args) {
- Business bussiness = new Business(); //new 一个线程任务处理类
- // 开启一个子线程
- new Thread(new Runnable() {
- @Override
- public void run() {
- for(int i = 1; i <= 50; i ++) {
- bussiness.sub(i);
- }
- }
- }).start();
- //main 方法即主线程
- for(int i = 1; i <= 50; i ++) {
- bussiness.main(i);
- }
- }
- }
- // 要用到共同数据 (包括同步锁) 或共同的若干个方法应该归在同一个类身上, 这种设计正好体现了高雷剧和程序的健壮性.
- class Business {
- private boolean bShouldSub = true;
- public synchronized void sub(int i) {
- while(!bShouldSub) { // 如果不轮到自己执行, 就睡
- try {
- this.wait(); // 调用 wait()方法的对象必须和 synchronized 锁对象一致, 这里 synchronized 在方法上, 所以用 this
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- for(int j = 1;j <= 10; j ++) {
- System.out.println("sub thread sequence of" + j + ", loop of" + i);
- }
- bShouldSub = false; // 改变标记
- this.notify(); // 唤醒正在等待的主线程
- }
- public synchronized void main(int i) {
- while(bShouldSub) { // 如果不轮到自己执行, 就睡
- try {
- this.wait();
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- for(int j = 1;j <= 5; j ++) {
- System.out.println("main thread sequence of" + j + ", loop of" + i);
- }
- bShouldSub = true; // 改变标记
- this.notify(); // 唤醒正在等待的子线程
- }
- }
首先, 先不说具体的程序实现, 就从结构上来看, 已经体会到了这种设计的好处了: 主函数里不用修改任何东西, 关于线程间同步和线程间通信的逻辑全都在 Business 类中, 主函数中的不同线程只需要调用放在该类中对应的任务即可. 体现了高类聚的好处.
再看一下具体的代码, 首先定义一个 boolean 型变量来标识哪个线程该执行, 当不是子线程执行的时候, 它就睡, 那么很自然主线程就执行了, 执行完了, 修改了 bShouldSub 并唤醒了子线程, 子线程这时候再判断一下 while 不满足了, 就不睡了, 就执行子线程任务, 同样地, 刚刚主线程修改了 bShouldSub 后, 第二次循环来执行主线程任务的时候, 判断 while 满足就睡了, 等待子线程来唤醒. 这样逻辑就很清楚了, 主线程和子线程你一下我一下轮流执行各自的任务, 这种节奏共循环 50 次.
另外有个小小的说明: 这里其实用 if 来判断也是可以的, 但是为什么要用 while 呢? 因为有时候线程会假醒(就好像人的梦游, 明明正在睡, 结果站起来了), 如果用的是 if 的话, 那么它假醒了后, 就不会再返回去判断 if 了, 那它就很自然的往下执行任务, 好了, 另一个线程正在执行呢, 啪叽一下就与另一个线程之间相互影响了. 但是如果是 while 的话就不一样了, 就算线程假醒了, 它还会判断一下 while 的, 但是此时另一个线程在执行啊, bShouldSub 并没有被修改, 所以还是进到 while 里了, 又被睡了~ 所以很安全, 不会影响另一个线程! 官方 JDK 文档中也是这么干的.
线程间通信就总结到这吧~ 若有错误, 欢迎指正, 我们一起进步.
来源: https://www.cnblogs.com/eson15/p/10229789.html