chapter3 线程间通信
1、等待/通知机制
1、使用sleep+死循环
线程A和B处理同一份对象,A/B 其中A对对象做修改,B通过死循环的方式监听对象的变化。这种方法虽然实现了线程间通信,但是通过轮询来检测浪费资源,轮询间隔较难控制,如果控制较少,浪费资源,间隔较大,可能监听的数据不全。
如下例:
|
|
|
|
|
|
|
|
得到结果如下:
此处需要注意一个问题,由于进程间通信,局部变量list需要添加volatile修饰,保证内存可见性。
2、等待通知机制实现
方法wait()的作用是使当前执行代码的线程进行等待,wait()方法是object的方法,在调用wait方法之前,线程必须获得该对象的对象级别锁,即只能在synchronized方法内执行。
如果调用wait时没有持有适当的锁,两会抛出IllegalMonitorStateException。
方法notify()的作用是用来通知那些可能等待该对象的对象锁的其他线程,如果有多个线程等待,由线程规划器随机挑选一个wait状态的线程,对其发通知notify,并使它等待获取该对象的对象锁。notify()方法执行完成之后并不是马上释放该对象锁,等到执行notify()方法的线程将程序执行完成,也就是退出synchronized代码块后,当前线程才会释放锁。wait所在线程才可以开始执行。如果第一个获得了该对象锁的线程已经执行完成,他会释放掉该对象锁,如此如果没有再次调用notify方法,即便该对象已经空闲,其他的wait状态等待的线程由于没有接到通知,会继续阻塞,知道对象发出notify或notifyAll。
wait使线程停止运行,notify使线程继续运行。
|
|
|
|
|
|
执行结果
|
|
3. 线程的状态
1)创建一个线程调用start,处于Runnable状态,如果线程抢占到CPU资源,此时线程处于Running状态。
2)Runnable状态和Running状态可以互相切换,主要体现在是否争抢到了CPU执行权。
进入Runnable状态大体有如下几种情况
- 调用sleep()方法,已经超过执行休眠时间
- 线程调用的阻塞IO已经返回,阻塞方法执行完毕
- 线程成功地获得了视图同步的监视器
- 线程正在等待某个通知,其他线程已经发出了通知。
- 处于挂起状态的线程调用了resume方法
3)Blocked(阻塞/暂停)状态。Blocked状态结束后进入Runnable状态
出现Blocked状态有以下几种情况
- 线程调用sleep方法
- 线程调用了阻塞式IO方法
- 试图获取一个同步监听器(synchronized锁),但该同步监听器正在被其他线程锁持有
- 线程等待某个通知
- 线程调用了suspend将线程挂起(容易死锁,不要使用)
4)run方法运行结束后进行销毁阶段,整个线程执行完毕。
每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列
4.方法wait()锁释放与notify()锁不释放
方法在执行wait等待时候,会将synchronized上的对象锁释放掉,其他的线程将得到执行权。
|
|
|
|
|
|
|
|
执行结果
|
|
由执行结果来看
线程A在执行到wait方法时,将synchronized上的lock已经释放,线程B才可以执行到wait方法。
如果将上述的wait方法换成sleep方法,lock将不会被释放, AB线程同步执行。
notify()方法同样不会释放lock
5.wait状态调用interrupt
当线程呈wait()状态时,调用线程对象的interrupt()方法,会出现InterruptedException异常。
6.通知过早
代码逻辑控制时,一定要线wait后notify,如果notify先执行,wait将一直处于挂起。
7.生产者/消费者模式实现
等待/通知模式最经典的案例就是“生产者/消费者”模式,原理都是基于wait/notify
1.一生成与一消费:操作值
定义一生产者,一消费者,生产者如果检测到已经产生值,则等待,如果为空,则生产一个值。消费者与之相反。
如下所示:
|
|
|
|
|
|
|
|
|
|
|
|
在两个Thread中,while(true)当生产者产生时,通知消费者,消费者消费后通知生产者。
所以会交替产生如下日志
|
|
2.多生产者多消费者:操作值-假死
有多个生产者消费者的情况,如果使用wait/notify,可能出现notify通知的并不是“异己”,比如生产者通知到生产者,然后生产者一直处于等待状态,消费者由于没接到通知,虽然有待消费的内容,但是无法执行。这样就出现了死锁现象。
如何解决?
将notify修改为notifyAll即可,同时唤醒所有处于wait状态的,让竞争资源(保证同步,如果不同步,执行将出现“脏读”现象)。
8. 通过管道进行线程间通信:字节流
通过pipeStream实现不同线程之间的通信
|
|
|
|
|
|
|
|
|
|
需要注意的是:pos.connect(pis);,使用connect的作用是两个stream之间长通信连接,这样才可可以将数据进行输入与输出。
9.实现方法的交替执行
除了生产者消费者外,如果为多线程可以考虑下面的实现方式
|
|
|
|
|
|
|
|
展示结果为
2. 方法join的使用
1.方法join效果
主线程创建并启动子线程,如果子线程执行时间较长,主线程早于子线程结束之前结束。如果主线程想等子线程结束后再结束,此时就需要用到join方法。join()的作用是等待线程对象销毁。
下例展示主线程先结束
|
|
如果需要在主程序中等待子程序执行完成前保持阻塞状态。可以是用join方法
|
|
方法join具有使线程排队运行的作用,有些类似同步的效果。join与synchronized的区别是:join在内部使用wait()方法进行等待,而synchronized关键字使用的是“对象监视器”原理作为同步。
2.方法join与异常
在join过程中,如果当前线程对象被中断,则当前线程出现异常。
方法join()与interrupt()方法如果遇到,则会出现异常。
3.方法join(long) 与sleep(long)的区别
方法join的功能内部是使用wait方法来实现的,所有join方法具有释放锁的特点。
3.ThreadLocal的使用
TheadLocal主要解决的就是每个线程绑定自己的值,可以将ThreadLocal比喻成全局存放数据的盒子,盒子中可以存储每个线程的私有数据。
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
如下例,主线程和子线程间使用同一个ThreadLocal,但是取值互不影响
执行结果
|
|
ThreadLocal的应用场景
最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。
|
|
|
|