线程之间通讯
每个线程都是独立运行的个体,线程通讯能让多个线程之间协同工作。
- Object类中的wait/notify方法可以实现线程间通讯
- wait/notify必须与synchronized一起使用,因为使用这些方法之前必须获得monitor锁
- wait释放锁,且只释放当前对象锁,若有多个对象,不会影响到其他对象的锁
- notify不释放锁
wait方法被唤醒的四种情况
- 另一个线程调用这个wait方法加锁对象的notify方法且刚好被唤醒的是本线程
- 另一个线程调用这个wait方法加锁对象的notifyAll方法
- 过了wait(long timeout)规定的超时时间,如果传入0就是永久等待
- 线程自身调用了interrupt()
栗子1:while方式实现线程间通讯
启动一个线程调用put方法往全局变量list中添加元素,当添加了5个元素后,通知另一个线程调用get方法获取list中元素并输出
1 | public class ThreadDemo17 { |
执行结果:
1 | 线程t1添加第1个元素 |
疑问:
1.一个线程修改全局变量,另外一个线程获取变量,不是会产生脏读吗?为什么没有加synchronized保证同步执行?
因为有全局变量canGet的控制,只有通过线程1改变了canGet的值,线程2才能get,并不会引起脏读。
这里需要对canGet变量加上volatile关键字,保证可见性。在个数为5的时候,线程t1已经更新了canGet=true,但是还未更新到主内存中,则线程t2取到的值不是最新的值。
2.当线程1,线程2运行结束后,list的size是多少?
10,当线程1添加到5个元素后,通知线程2,线程2获取元素并输出,然后就break退出循环了。但是线程1还在继续添加元素,直到线程1的循环10都结束。所以,最后list的size肯定是10,只不过线程2只是输出了前5个元素,就退出循环,不再输出了。
栗子2:wait/notify实现线程间通讯
启动一个线程调用put方法往全局变量list中添加元素,当添加了5个元素后,通知另一个线程调用get方法获取list中元素并输出。先调用get方法,先保证get方法运行。
1 | public class ThreadDemo18 { |
运行结果:
线程t1先启动,获取lock锁,进入get方法执行。执行到lock.wait时,释放锁,等待通知。
t1释放锁,线程t2获得锁,进入put方法执行,添加5个元素后,发出通知,但是notify不释放锁,会继续执行完t1后才释放锁。
当t1添加完10个元素,t1执行完成,释放锁。线程t2接着执行wait后的输出语句,输出list中的10个元素。
1 | 线程t1业务处理,发现有需要的数据没准备好,则发起等待 |
notify唤醒
- notify方法只应该被拥有该对象的monitor的线程调用
- 要等刚才执行notify的线程退出被synchronized保护的代码并释放monitor
notify与notifyAll的区别
notifyAll 使所有原来在该对象上等待被notify的线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
notify 只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁。
栗子1:只调用一次notify
1 | public class ThreadDemo19 { |
执行结果:
run2,run3方法先运行,等待通知。sleep1s后,run1方法运行,使用notify发送通知,只会发送一个通知。
run2收到通知,执行完毕。run3没有收到通知,还在等待着notify/notifyAll才可以继续运行。
1 | 进入run2方法.. |
如果run2方法修改成如下:
1 | public synchronized void run2() { |
执行结果:
在run2方法中再次发送了notify通知,此时只有run3一个方法在等待通知,自然就是run3接收到通知,执行完毕。可以看到,notify类似于链式操作。
1 | 进入run2方法.. |
栗子2:使用notifyAll
1 | public class ThreadDemo19 { |
执行结果:
使用notifyAll通知,所有的等待通知的线程都会收到通知。
1 | 进入run2方法.. |
守护进程与用户进程
- main函数所在线程是一个用户线程
- 只要有一个用户线程还没结束,jvm进程就不会结束
- 父线程结束后,子线程还可以继续存活,子线程的生命周期不受父线程影响
线程上下文切换
当前线程使用完时间片后就会进入就绪状态,让出cpu执行权给其他线程,此时就是从当前线程的上下文切换到了其他线程。
当发生上下文切换的时候需要保存执行现场,待下次执行时进行恢复。
所以频繁的,大量的上下文切换会造成一定资源开销。
sleep方法
- Thread类中的一个静态方法
- 暂时让出执行权,不参与CPU调度,但是不释放锁,包括synchronized和lock
- sleep之后,本身进入阻塞状态,时间到了就进入就绪状态,一旦获取到CPU时间片,则继续执行
- 清除中断状态
一句话总结:Sleep方法可以让线程进入TIME_WAITING状态,并且不占用CPU资源,但是不释放锁,直到规定时间后再执行,休眠期间如果被中断,会抛出异常且清除中断标志
栗子1:两个线程交替打印1-100,使用wait/notify
1 | // 两个线程执行自己的for循环,用判断是否奇偶的方式来控制打印顺序 |
1 | // 共享变量i,控制好i初始值,直接打印,打印完唤醒其他线程打印,不需要判断是否奇偶 |
栗子2:两个线程交替打印1-100,使用volatile修饰的变量控制
用volatile修饰的变量来交替执行线程,需要while(true)死循环检测变量是否改变.
当输出完结果后,线程不会正常结束,会被阻塞
1 | public class OddExample2 { |
栗子3:两个线程交替打印1-100,使用synchronized
1 | public class OddExample3 { |
面试题1:为什么wait方法需要在同步代码块内使用,而sleep方法不需要
sleep方法主要是在线程内使用的,不涉及多个线程的通信。
wait方法放在同步代码块主要了为了防止死锁的发生。因为有了synchronized关键字,能保证在同一时刻,只会有一个线程执行代码快中的代码,避免了先notify,后wait这种情况。
面试题2:wait/notify方法为什么定义在Object类,而不是Thread类?
wait/notify是需要在同步代码块内使用的,而同步代码快涉及到锁,锁是保存在对象的对象头的。任意对象都可以作为锁。另一方面,经常在同一个代码块中有多个锁配合使用,因此wait/notify定义在Object类是比较灵活的。
面试题3:调用Thread.wait()方法会发生什么情况?
Thread也是一个对象,也可以调用wait方法。但是Thread类在线程结束后,会自动调用notifyAll方法。如果使用Thread对象作为锁,调用了Thread.wait方法,会对设计好的业务有影响。
面试题4:wait、sleep方法的相同点,不同点
- 相同点
- 阻塞
- 响应中断
- 不同点
- 所属类
- 释放锁:wait释放锁,sleep不释放锁
- 同步方法中:wait需要在同步方法中使用,sleep不需要
- 指定时间:sleep方法使用必须指定时间,而wait可以不指定时间