Java并发基础05. 传统线程同步通信技术

Wesley13
• 阅读 726

先看一个问题:

有两个线程,子线程先执行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 文档中也是这么干的。

线程间通信就总结到这吧~若有错误,欢迎指正,我们一起进步。

点赞
收藏
评论区
推荐文章
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
java 面试知识点笔记(十三)多线程与并发
java线程池,利用Exceutors创建不同的线程池满足不同场景需求:1.newSingleThreadExecutor() 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。2.
Wesley13 Wesley13
3年前
java_线程池
血一样的教训,今天上午参加了一家现场面试java。在这之前,我一直认为我的java基础还是可以的,而今天一问三不知。现在将面试的问题整理出来一、说说java中的线程池?  1.线程池:线程池是线程的集合,不用自己创建线程,把线程直接给线程池,由线程池处理。   2.过程:首先,使用线程池可以重复利用已有的线程继续执行任务,避免线程在
Wesley13 Wesley13
3年前
java多线程的3种实现方式
多线程相关的问题1.什么是进程?​正在执行的程序2.什么是线程?​进程的子单位,一个能够完成独立功能的执行路径3.为什么需要开启多线程?当执行某些耗时操作的任务的时候需要开启多线程,防止线程阻塞能够让两个任务看起来像是在同时执行
Wesley13 Wesley13
3年前
Java基础教程——线程池
启动新线程,需要和操作系统进行交互,成本比较高。使用线程池可以提高性能——线程池会提前创建大量的空闲线程,随时待命执行线程任务。在执行完了一个任务之后,线程会回到空闲状态,等待执行下一个任务。(这个任务,就是Runnable的run()方法,或Callable的call()方法)。Java5之前需要手动实现线程池,Java5之
Stella981 Stella981
3年前
Python进程、线程、协程的对比
1\.执行过程每个线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在进程中,由进程提供多个线程执行控制。每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。协程,又称微线程,Coroutine。执行过程中,在子程序内部可中断,然后转而
Wesley13 Wesley13
3年前
Java 多线程,线程池,
1\.创建线程池的方法之三://对于每个任务,如果有空闲的线程可用,立即让他执行任务,//没有空闲的线程则创建一个线程。ExecutorServicepoolExecutors.newCachedThreadPool();//固定大小的线程池,任务数空闲线程数,得不到服务的任务
Stella981 Stella981
3年前
Executor框架详解
任务是一组逻辑工作单元,线程是使任务异步执行的机制。下面分析两种通过创建线程来执行任务的策略。1将所有任务放在单个任务中串行执行;2为每个任务创建单独的线程来执行实际上两种方式都存在一些严重的缺陷。串行执行的问题在于其糟糕的响应和吞吐量;而为每个任务单独创建线程的问题在于资源管理的复杂性,容易造成资源的浪费和过度消耗,影响系统的稳定性。为了提
Wesley13 Wesley13
3年前
Java多线程之任务执行
Java多线程之任务执行一、在线程中执行任务1.串行的执行任务在应用程序中可以通过多种策略来调度任务,而其中的策略能够更好的利用潜在的并发性。_最简单的策略就是在单个线程中串行的执行各项任务。_public class SingleThreadWebServer {
京东云开发者 京东云开发者
11个月前
ThreadPoolExecutor线程池内部处理浅析 | 京东物流技术团队
我们知道如果程序中并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束时,会因为频繁创建线程而大大降低系统的效率,因此出现了线程池的使用方式,它可以提前创建好线程来执行任务。本文主要通过java的ThreadPoolExecutor来查看线程池