多线程学习总结(三):线程间通信

多线程学习总结(三):线程间通信

最近看了《Java编程多线程核心技术》这本书,在此接着上一篇继续总结以下知识点:

通知/等待机制
join方法的使用
ThreadLocal与InheritableThreadLocal类的使用


一、通知/等待机制

在多线程程序编写时,经常需要让多个线程进行协作通信,其中最常见的一种机制就是通知等待机制,该机制的主要思想是在多个使用同一对象锁的同步方法或代码块内,在某些情况下让当前线程陷入等待,另外一些情况下则通知线程继续执行,以此确保每个线程都在适当的情况下执行,在不合适时不执行,举个例子:生产者消费者模式,当生产者线程判断操作栈已满时就需要陷入等待,同时通知消费者去消费,而消费者线程判断操作栈已空时则陷入等待,通知生产者去生产…,多个线程就通过这种机制实现了相互通信,相互协作。
通知/等待机制就是通过如下两个方法实现的:

wait();//让当前线程陷入等待
notify();//通知一个陷入等待的线程继续执行
notify();//通知所有陷入等待的线程继续执行

注意:这三个方法均是Object类中的方法,在同步方法或代码块中,由锁对象进行调用,若不在同步方法或代码块中调用,则会抛出IllegalMonitorStateException异常。
看如下例子

//定义消费者线程类
class Consumer extends Thread {
    private List list;

    public Consumer(List list) {
        this.list = list;
    }

    public void consume(Object obj){
        try{
            synchronized (list){
            //list里有数据时,从list中取一个,通知别的线程,无数据时陷入等待,等待生产者线程往list中写入数据
                if(list.size()>0){
                    list.remove(0);
                    System.out.println("仓库目前商品个数为:"+list.size());
                    list.notify();
                }else{
                    System.out.println(Thread.currentThread().getName()+"仓库里面商品没了");
                    list.wait();
                }
            }
        }catch(InterruptedException e){
            e.printStackTrace();
        }

    }
    public void run(){
        while(true){
            consume("*");
        }

    }
}
//定义生产者线程类
class Producer extends Thread {
    private List list;
    private int maxSize;

    public Producer(List list, int maxSize) {
        this.list = list;
        this.maxSize = maxSize;
    }

    public void product(Object obj){
        try{
            synchronized (list){
            //list长度不大于最大长度时,往list中放一个值,通知别的线程,大于等于最大值时陷入等待,等待消费者线程从list中取数据
                if(list.size()<this.maxSize){
                    list.add(obj);
                    System.out.println(Thread.currentThread().getName()+"仓库目前商品个数为:"+list.size());
                    list.notify();
                }else{
                    System.out.println(Thread.currentThread().getName()+"仓库满了");
                    list.wait();
                }
            }
        }catch(InterruptedException e){
            e.printStackTrace();
        }

    }
    public void run(){
        while(true){
            product("*");
        }

    }
}
//测试方法
public void test(){
        List list=new LinkedList();
        List<Thread> threadList=new ArrayList<>();
        //在这里开五个生产者线程和十个消费者线程,去操作一个最大长度为5的list
        for(int i=0;i<10;i++){
            if(i<5){
                Producer p=new Producer(list,5);
                p.setName("p"+i);
                threadList.add(p);
            }
            Consumer c=new Consumer(list);
            c.setName("c"+i);
            threadList.add(c);
        }
        //Lambda表达式的方式启动所有线程
        threadList.forEach(Thread::start);
    }

运行结果如下
这里写图片描述
从结果可以看到,生产者消费者在不断交替运行,(如果亲自进行了该实验,运行一段时间后,便会发现该程序会停下来,陷入假死状态,关于这一点,看下面注意事项第四点)。
以上便是wait/notify机制的简单应用,通过synchronized和wait/notify的配合使用,让线程协作有序高效的进行。
除了上面提到的必须在同步方法或代码块中,由锁对象进行调用,还有其它一些关于wait()和notify()两个方法的注意事项和知识点:
1.wait()在调用之后会停下当前的线程,并释放锁;而notify()调用后会通知别的线程,但当前线程并不会立马释放掉锁。
2.除了wait()之外还有一个可以带参数的wait(long)方法,它的用法是等待一段时间,如果这段时间里该线程没有被唤醒,则自动唤醒该线程。
3.要注意sleep(long)与wait(long)方法之间的区别,sleep方法不释放锁,而wait方法调用后即会释放锁;其次:前者是Thread类中的方法,后者是Object类中的方法。
4.notify()的作用是通知唤醒一个别的线程,这样就有可能在程序运行时唤醒一个可能并不需要唤醒的线程,比如上面例子中,假如所有消费者线程都陷入等待,而操作栈满之前的最后一次调用notify()方法时又唤醒了一个生产者线程,则程序将陷入假死状态。在程序设计中这种状态是必须要想办法避免的,一种简单的方法便是使用notifyAll()方法替代notify()方法,前者会唤醒所有同一对象监视器的线程,这样便能保证不会让需要运行的线程陷入无谓的等待。

二、join方法的使用

join()方法的作用是在程序运行时,让join()的调用者X线程正常执行,无限阻塞当前线程的执行,当X线程执行完成之后再继续执行当前线程,也就是使得线程可以按顺序进行。

public void joinTest(){
        Thread threadA=new Thread(()->{
            try{
                System.out.println("A线程开始执行");
                Thread.sleep(1000);
                System.out.println("A线程结束执行");
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        },"A");
        try{
            threadA.start();
            threadA.join();
            System.out.println("A线程执行完成后再执行");
        }catch(InterruptedException e){
            e.printStackTrace();
        }
    }
   

执行结果为
这里写图片描述
从执行结果可以看出,join()方法使得threadA执行完成后再执行后面的代码,如果注释掉threadA.join();这行代码,运行结果将变为
这里写图片描述
所以假如有一个需求是需要A、B、C三个线程顺序执行,那就可以使用join()方法。如下所示的写法

		threadA.start();
        threadA.join();
        threadB.start();
        threadB.join();
        threadC.start();
        threadC.join();

注意事项:
1、join()内部使用了wait()方法来实现等待。
2.wait(long)可以限定最大等待时间相似,join也有一个重载方法join(long),可以设置等待时间,当限定的等待时间内,join(long)方法的调用线程对象未能执行完毕,就继续执行当前线程后面的线程。如

			threadA.start();
            threadA.join(1000);
            threadB.start();

上面的代码,如果threadA未能在1000毫秒内结束,则后面的代码也要开始执行了。
三、ThreadLocal与InheritableThreadLocal类的使用

ThreadLocal的作用是让每个线程保存一份自己线程的共享变量,在自己线程内部共享,与别的线程不共享,常用的有get()和set(T value)两个方法,如下一个简单的例子看一下该类的使用。

//定义一个线程类
public class ThreadLocal1 extends Thread {
	//有一个ThreadLocal类型的成员变量
    private ThreadLocal<String> stringThreadLocal;
    public ThreadLocal1(String name,ThreadLocal<String> stringThreadLocal) {
        super(name);
        this.stringThreadLocal=stringThreadLocal;
    }
    @Override
    public void run(){
        try{
        //注意该方法并没有加锁
            for(int i=0;i<1000;i++){
                this.stringThreadLocal.set("这是"+this.getName()+"线程的私有变量"+i);
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+"==="+i+"================================"+this.stringThreadLocal.get());
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }


    }

}

//测试方法如下
public void threadLocalTest(){
        ThreadLocal<String> stringThreadLocal=new ThreadLocal<String>();
        ThreadLocal1 t1=new ThreadLocal1("t1",stringThreadLocal);
        ThreadLocal1 t2=new ThreadLocal1("t2",stringThreadLocal);
       t1.start();
        t2.start();
    }

执行结果如下图所示
这里写图片描述
分析一下运行结果,两个线程t1和t2共享了共一个ThreadLocal的实例,但是在t1和t2内部同样调用get()方法,在整个实验中我并没有加锁,但t1永远得到t1放进去的值,t2永远得到t2放进去的值,这就是ThreadLocal的作用:在线程内部共享,不同线程之间相互隔离。
InheritableThreadLocal的使用与ThreadLocal相似,不同的是前者可以让子线程里继承共享父线程set进去的变量。

以上所有内容均总结自高洪岩的《Java编程多线程核心技术》一书

系列文章

多线程学习总结(一):基础知识
多线程学习总结(二):同步与并发
多线程学习总结(三):线程间通信
多线程学习总结(四):可重入锁和读写锁

猜你喜欢

转载自blog.csdn.net/weixin_42882828/article/details/81713306
今日推荐