线程间通信

阿福聊编程
我是阿福,公众号JavaClub作者,一个在后端技术路上摸盘滚打的程序员,在进阶的路上,共勉!
文章已收录在 JavaSharing 中,包含Java技术文章,面试指南,资源分享。

掌握的技术点如下:

  • 使用wait/notify实现线程间的通信
  • 线程的生命周期
  • 生产者/消费者模式的实现
  • 方法join的使用
  • ThreadLocal类的使用

线程间通信

3.1 使用wait/notify实现线程间的通信

3.1.1 等待/通知机制的实现

什么是等待/通知机制

等待/通知机制在我们生活中比比皆是,比如在就餐时就会出现,如下图所示:

  • 厨师做完一道菜的时间不确定,所以厨师将菜品放到“菜品传递台”上的时间也不确定。
  • 服务员取到菜的时间取决于厨师,所以服务员就有“等待”(wait)的状态。
  • 厨师将菜放到“菜品传递台”上,其实就相当于一种通知(notify),这是服务员才能拿到菜交给就餐者。

这个过程就出现了“等待/通知”机制。

使用专业术语讲

等待/通知机制,是指线程A调用了对象O的wait()方法进入等待状态,而线程B调用了对象O的notify()/notifyAll()方法,线程A收到通知后退出等待队列,进入可运行状态,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()方法和notify()/notifyAll()方法的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

等待/通知机制的实现

wait()方法的作用

是使当前线程进入阻塞状态,同时在调用wait()方法之前线程必须获得该对象的对象级别锁,即只能在同步方法或同步代码块中调用wait()方法。在执行wait()方法之后,当前线程释放锁

notify() notifyAll()方法的作用:

就是用来通知那些等待该对象的对象锁的其他线程,如果有多个线程等待,则由线程规划器随机挑选其中一个呈wait()状态的线程,对其发起通知notify,并使它获取该对象的对象锁。

需要说明的是:在执行notify()方法之后,当前线程不会马上释放该对象锁,呈wait()状态的线程也不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出synchronized
代码块,当前线程才会释放锁,而呈wait()状态所在的线程才可以获取对象锁。

强调notify(),notifyAll()也是在同步方法或者是同步代码块中调用,即在调用之前必须获得该对象的对象级别锁

用一句话总结一下wait和notify: wait使线程停止运行,而notify使停止的线程继续运行

下面代码实现一个示例:

创建MyList.java,代码如下:

public class MyList {
    private static List list=new ArrayList();
    public static void add(){
        list.add("anyString");
    }
    public static int size(){
        return list.size();
    }
}

自定义线程类 MyThread1.java, MyThread2.javaMyThread3.java代码如下:

public class MyThread1 extends Thread {
    private Object lock;

    public MyThread1(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                if (MyList.size() != 5) {
                    System.out.println("开始 wait time=" + System.currentTimeMillis());
                    lock.wait();
                    System.out.println("结束 wait time=" + System.currentTimeMillis());
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

public class MyThread2 extends Thread {

    private Object lock;

    public MyThread2(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            for (int i = 0; i < 10; i++) {
                MyList.add();
                if (MyList.size() == 5) {
                    lock.notify();
                    System.out.println(" 已发出通知");
                }
                System.out.println("添加了" + (i + 1) + " 个元素!!");
            }
        }
    }
}

创建测试类 Test.java

public class Test {
    public static void main(String[] args) {
       Object lock=new Object();
       MyThread1 myThread1=new MyThread1(lock);
       myThread1.start();
       MyThread2 myThread2=new MyThread2(lock);
       myThread2.start();
    }
}

程序代码运行结果如下:

开始 wait time=1618832467129
添加了1 个元素!!
添加了2 个元素!!
添加了3 个元素!!
添加了4 个元素!!
已发出通知
添加了5 个元素!!
添加了6 个元素!!
添加了7 个元素!!
添加了8 个元素!!
添加了9 个元素!!
添加了10 个元素!!
结束 wait time=1618832467130

从运行的结果来看,这也说明notify()方法执行后不是立即释放锁。


3.2 线程的生命周期

线程生命周期转换图

线程的状态

线程从创建,运行到结束总是处于五种状态之一:新建状态,就绪状态,运行状态,阻塞状态,死亡状态。
  • 新建状态 :线程对象被创建后就进入了新建状态,Thread thread = new Thread();
  • 就绪状态(Runnable):也被称之为“可执行状态”,当线程被new出来后,其他的线程调用了该对象的start()方法,即thread.start(),此时线程位于“可运行线程池”中,只等待获取CPU的使用权,随时可以被CPU调用。进入就绪状态的进程除CPU之外,其他运行所需的资源都已经全部获得。
  • 运行状态(Running):线程获取CPU权限开始执行。注意:线程只能从就绪状态进入到运行状态。
  • 阻塞状态(Bloacked):阻塞状态是线程因为某种原因放弃CPU的使用权,暂时停止运行,知道线程进入就绪状态后才能有机会转到运行状态。

阻塞的情况分三种:

(1)、等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池中”。进入这个状态后是不能自动唤醒的,必须依靠其他线程调用notify()或者notifyAll()方法才能被唤醒。
(2)、同步阻塞:运行的线程在获取对象的(synchronized)同步锁时,若该同步锁被其他线程占用,则JVM会吧该线程放入“锁池”中。

(3)、其他阻塞:通过调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新回到就绪状态。

  • 死亡状态(Dead):线程执行完成或者因异常退出run方法,该线程结束生命周期。

阻塞线程方法的说明:

  • wait(), notify(),notifyAll()这三个方法是结合使用的,都属于Object中的方法,wait的作用是使当前线程释放它所持有的锁进入等待状态(释放对象锁),而notify和notifyAll则是唤醒当前对象上的等待线程。
  • sleep() 和 yield()方法是属于Thread类中的sleep()的作用是让当前线程休眠(正在执行的线程主动让出CPU,然后CPU就可以去执行其他任务),即当前线程会从“运行状态”进入到阻塞状态”,但仍然保持对象锁。当延时时间过后该线程重新阻塞状态变成就绪状态,从而等待CPU的调度执行。
  • yield()的作用是让步,它能够让当前线程从运行状态进入到就绪状态”,从而让其他等待线程获取执行权,但是不能保证在当前线程调用yield()之后,其他线程就一定能获得执行权,也有可能是当前线程又回到“运行状态”继续运行。

wait () , sleep()的区别:

1、 sleep()睡眠时,保持对象锁,仍然占有该锁,而wait()释放对象锁.
2、 wait只能在同步方法和同步代码块里面使用,而sleep可以在任何地方使用。
3、 sleep必须捕获异常,而wait不需要捕获异常


3.3 生产者/消费者模式的实现

生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。生产者生成一定量的数据放到缓冲区中,然后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的情况,此时进程都在等待唤醒。

解决生产者/消费者问题的方法可分为两类

(1)采用某种机制保护生产者和消费者之间的同步;
(2)在生产者和消费者之间建立一个管道。第一种方式有较高的效率,并且易于实现,代码的可控制性较好,属于常用的模式。第二种管道缓冲区不易控制,被传输数据对象不易于封装等,实用性不强。因此本文只介绍同步机制实现的生产者/消费者问题。

同步问题核心在于

如何保证同一资源被多个线程并发访问时的完整性。常用的同步方法是采用信号或加锁机制,保证资源在任意时刻至多被一个线程访问。Java语言在多线程编程上实现了完全对象化,提供了对同步机制的良好支持。在Java中一共有四种方法支持同步,其中前三个是同步方法,一个是管道方法。

(1)wait() / notify()方法

(2)await() / signal()方法

(3)BlockingQueue阻塞队列方法

(4)PipedInputStream / PipedOutputStream

下面我们通过 wait() / notify()方法实现生产者和消费者模式:

代码场景:

当缓冲区已满时,生产者线程停止执行,放弃锁,使自己处于等状态,让其他线程执行;
当缓冲区已空时,消费者线程停止执行,放弃锁,使自己处于等状态,让其他线程执行。

当生产者向缓冲区放入一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态;
当消费者从缓冲区取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。

代码实现:

创建仓库Storage.java 代码:

public class Storage {
    // 仓库容量
    private final int MAX_SIZE = 10;
    // 仓库存储的载体
    private LinkedList<Object> list = new LinkedList<>();

    public void produce() {
        synchronized (list) {
            while (list.size() + 1 > MAX_SIZE) {
                System.out.println("【生产者" + Thread.currentThread().getName()
                        + "】仓库已满");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.add(new Object());
            System.out.println("【生产者" + Thread.currentThread().getName()
                    + "】生产一个产品,现库存" + list.size());
            list.notifyAll();
        }
    }

    public void consume() {
        synchronized (list) {
            while (list.size() == 0) {
                System.out.println("【消费者" + Thread.currentThread().getName()
                        + "】仓库为空");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.remove();
            System.out.println("【消费者" + Thread.currentThread().getName()
                    + "】消费一个产品,现库存" + list.size());
            list.notifyAll();
        }
    }

}

创建生产者线程Producer.java,消费者线程Consumer.java,代码如下:

public class Producer implements Runnable {

    private Storage storage;

    public Producer(){}

    public Producer(Storage storage){
        this.storage = storage;
    }

    @Override
    public void run(){
        while(true){
            storage.produce();
        }
    }

}
public class Consumer implements Runnable{
    private Storage storage;

    public Consumer(){}

    public Consumer(Storage storage){
        this.storage = storage;
    }

    @Override
    public void run(){
        while(true){
            storage.consume();
        }
    }

}

创建测试类TestPc.java

public class TestPc {

    public static void main(String[] args) {
        Storage storage = new Storage();
        Thread p1 = new Thread(new Producer(storage));
        p1.setName("张三");
        p1.start();
        Thread c1=new Thread(new Consumer(storage));
        c1.start();
        c1.setName("李四");

    }


}

程序运行的部分结果:

【消费者李四】消费一个产品,现库存8
【消费者李四】消费一个产品,现库存7
【消费者李四】消费一个产品,现库存6
【消费者李四】消费一个产品,现库存5
【消费者李四】消费一个产品,现库存4
【生产者张三】生产一个产品,现库存5
【生产者张三】生产一个产品,现库存6
【生产者张三】生产一个产品,现库存7
【生产者张三】生产一个产品,现库存8
【生产者张三】生产一个产品,现库存9
【生产者张三】生产一个产品,现库存10
【生产者张三】仓库已满
【消费者李四】消费一个产品,现库存9
【消费者李四】消费一个产品,现库存8
【消费者李四】消费一个产品,现库存7

3.4 方法join的使用

在很多情况下,主线程创建并启动子线程,如果子线程中进行大量的运算,主线程往往早于子线程结束。这时主线程要等待子线程完成之后再结束。比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法。

join()方法就是等待线程对象销毁。

创建测试MyJoinThread.java代码:


public class MyJoinThread extends Thread{

    @Override
    public void run() {
        int secondValue= (int) (Math.random() * 10000);
        System.out.println(secondValue);
        try {
            Thread.sleep(secondValue);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

创建测试类TestJoin.java代码:

public class TestJoin {
    public static void main(String[] args) throws InterruptedException {
        MyJoinThread myJoinThread=new MyJoinThread();
        myJoinThread.start();
        //myJoinThread.join();
        System.out.println("我想当myJoinThread对象执行完毕我再执行,答案是不确定的");
    }
}

代码的运行结果:

我想当myJoinThread对象执行完毕我再执行,答案是不确定的
9618

把myJoinThread.join()代码注释去掉运行代码执行结果如下:

82
我想当myJoinThread对象执行完毕我再执行,答案是不确定的

所以得出结论是:join()方法使所属线程对象myJoinThread正常执行run()方法中的任务,而使当前线程main进行无限的阻塞,等待myJoinThread销毁完再继续执行main线程后面的代码。

方法join()具有使线程排队运行的作用,有点类似同步运行的效果。

join()和synchronized的区别是:join()在内部使用wait()方法进行等待,而synchronized关键字使用的是“对象监听器”的原理做的同步。

方法join()与sleep(long)的区别

方法join(long)的功能在内部使用的是wait(long)方法实现的,所用join(long)方法具有释放锁的特点。

方法join(long)源代码如下:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

从源代码中可以了解到,当执行wait(long)方法后,当前线程的锁被释放,那么其他线程可以调用此线程中的同步方法了。

而Thread.sleep(long)方法不释放锁。


3.5 ThreadLocal类的使用

我们知道变量值的共享可以使用public static 变量的形式,如果想实现每一个线程都有自己的共享变量该如何解决呢? JDK中提供ThreadLocal正是解决这样的问题。

类ThreadLocal主要解决的就是为每个线程绑定自己的值,可以将ThreadLocal类比喻成全局存放数据的盒子,盒子中可以存储每一个线程的私有数据。

创建run.java类,代码如下:

public class run {
  private static  ThreadLocal threadLocal=new ThreadLocal();

    public static void main(String[] args) {
       if (threadLocal.get()==null){
           System.out.println("从未放过值");
           threadLocal.set("我的值");
       }
        System.out.println(Thread.currentThread().getName()+"线程:"+threadLocal.get());
    }
    

}

代码的运行结果:

从未放过值
main线程:我的值

从图中运行结果来看,第一次调用threadLocal对象的get方法返回为null,通过调用set()赋值后值打印在控制台上,类ThreadLocal解决的是变量在不同线程间的隔离性,也就是不同的线程拥有自己的值,不同线程的值可以存放在ThreadLocal类中进行保存的。

验证线程变量的隔离性

创建ThreadLocalTest项目,类 Tools.java代码如下:

public class Tools {
    public static ThreadLocal local=new ThreadLocal();
}

创建线程类 MyThread1.java ,MyThread2.java代码如下:

public class MyThread1 extends Thread {

    @Override
    public void run() {
        for (int j = 0; j < 5; j++) {
            Tools.local.set(j+1);
            System.out.println(Thread.currentThread().getName()+"get value:"+Tools.local.get());
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class MyThread2 extends Thread {


    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            Tools.local.set(i+1);
            System.out.println(Thread.currentThread().getName()+"get value:"+Tools.local.get());
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

创建run.java 测试类

public class run {

    public static void main(String[] args) {
       MyThread1 myThread1=new MyThread1();
       myThread1.setName("myThread1线程");
       myThread1.start();
       MyThread2 myThread2=new MyThread2();
       myThread2.setName("myThread2线程");
       myThread2.start();
    }
}

程序运行结果:

myThread1线程get value:1
myThread2线程get value:1
myThread2线程get value:2
myThread1线程get value:2
myThread1线程get value:3
myThread2线程get value:3
myThread2线程get value:4
myThread1线程get value:4
myThread1线程get value:5
myThread2线程get value:5

虽然2个线程都向local中set()数据值,但每个线程还是能取到自己的数据。


文章参考:

《Java多线程编程核心技术》
https://blog.csdn.net/ldx1998...
https://blog.csdn.net/MONKEY_...

看到这里今天的分享就结束了,如果觉得这篇文章还不错,来个分享、点赞、在看三连吧,让更多的人也看到~

欢迎关注个人公众号 「JavaClub」,定期为你分享一些技术干货。

阅读 2.1k
57 声望
40 粉丝
0 条评论
57 声望
40 粉丝
文章目录
宣传栏