多线程学习总结(三):线程间通信
最近看了《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编程多线程核心技术》一书
系列文章
多线程学习总结(一):基础知识
多线程学习总结(二):同步与并发
多线程学习总结(三):线程间通信
多线程学习总结(四):可重入锁和读写锁