线程时操作系统中独立的个体,但这些个体如果不经过特殊的处理是不能成为一个整体的。线程间的通信就是使线程成为整体的比用方案之一,可以说,是线程间进行通信后系统之间的交互性会更强大,CPU利用率会得以大幅提高,同时程序员在处理的过程中可以有效把控与监督各线程任务。
public class MyList {
volatile private List list = new ArrayList<>();
public void add(){
list.add("jay chou");
}
public int size(){
return list.size();
}
}
public class ThreadA extends Thread{
private MyList list;
public ThreadA(MyList list) {
this.list = list;
}
@Override
public void run(){
try {
for (int i = 0; i < 10; i++) {
list.add();
System.out.println("添加了" + (i + 1) +"个元素");
Thread.sleep(1000);
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadB extends Thread{
private MyList list;
public ThreadB(MyList list) {
this.list = list;
}
@Override
public void run(){
try {
while(true){
if(list.size() == 5){
System.out.println(" == 5了,线程b要退出了");
throw new InterruptedException();
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class Run1 {
public static void main(String[] args) {
MyList list = new MyList();
ThreadA a = new ThreadA(list);
a.setName("a");
a.start();
ThreadB b = new ThreadB(list);
b.setName("b");
b.start();
}
}
虽然两个线程之间实现了通信,但还存在缺点:线程ThreadB不停地通过while语句轮询机制来检测某一个条件,这样会浪费CPU资源。如果轮询的时间间隔很短,更浪费CPU资源;如果轮询的时间很长,有可能会取不到想要得到的数据。
wait/notify(等待/通知)机制在生活中很常见,比如在就餐时就会出现,如下图:
厨师和服务员在”菜品传递台“上交互,在这期间会想到几个问题:
1、厨师做完一个菜的时间未知,所以厨师把菜品放到”菜品传递台“上的时间也未知。
2、服务员取到菜的时间取决于厨师,所以服务器就有”wait“的状态。
3、服务器如何能取到菜?这又要取决于厨师。厨师将菜放到”菜品传递台“上,其实就相当于一个notify,这时服务员才可以拿到菜并交给就餐者。
4、这个过程中就出现了”wait/notify“机制。
需要说明的是,前文中多个线程之间也可以实现通信,就是就是多个线程共同访问同一个变量。但那种通信机制却不是”等待/通知“,两个线程完全是主动操作同一个共享变量。 但那种通信机制却不失“等待/通知”,两个线程完全是主动操作同一个共享变量,在花费读取时间的基础上,读到的值并不确定是不是想要的。
注意:拥有相同锁的线程才可以实现wait/notify机制。所以下文都是假定操作同一个锁。
wait()是Object类的方法,它的作用是使当前执行wait()方法的线程进行等待,在wait()所在的代码行处暂停执行,并释放锁,直到接到通知或被中断为止。在调用wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用wait()方法。通过通知机制是某个线程继续执行wait()方法后面的代码时,对线程的选择是按照执行wait()方法的顺序确定的,并需要重新获得锁。如果调用wait()时没有持有适当的锁,则抛出IllegalMonitorStateException。它是RuntimeException的一个子类,因此不需要try-catch语句进行捕获异常。
notify()也要在同步方法或同步块中调用,即在调用前线程必须要获得锁,如果调用notify()没有持有适当的锁,也会抛出IllegalMonitorStateException。该方法用来通知哪些可能等待该锁的其他线程,如果有多个线程等待,则按照执行wait()方法的顺序对呈等待状态的线程发出1次通知,并使那个线程重新获取锁。需要说明的是,执行notify()方法后,当前线程不会马上释放锁,呈等待状态的线程也并不能马上获取该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步区域后,当前线程才会释放锁,而呈等待状态所在的线程才可以获取该对象锁。当第一个获得了该对象锁的等待线程运行完毕后,它会释放掉该对象锁。此时如果没有再次使用notify语句,那么其他等待状态的线程会因为没有得到通知而继续等待。
wait和notify是Object类中的方法。总结来说:wait是线程暂停运行,而notify通知暂停的线程继续运行。
wait()的作用是使当前线程暂停运行,并释放锁。
public class Test1 {
public static void main(String[] args) {
try {
String s = new String("");
s.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
出现异常的原因是没有“对象监视器”,也就是没有锁。
public class Test2 {
public static void main(String[] args) {
try {
String lock = new String();
System.out.println("synchronzied之前");
synchronized (lock){
System.out.println("进入synchronzied代码块");
lock.wait();
System.out.println("执行完wait()方法");
}
System.out.println("执行完synchronzied代码块的代码");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
此时线程已经开始等待,但不能永远等待下去,否则程序就不会继续向下运行了。使用notify()方法可以是等待状态的线程继续运行。
public class MyThread1 extends Thread{
private Object lock;
public MyThread1(Object lock) {
this.lock = lock;
}
@Override
public void run(){
try {
synchronized (lock){
System.out.println("开始 等待时间 = " + Utils.data(System.currentTimeMillis()));
lock.wait();
System.out.println("结束 等待时间 = " + Utils.data(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){
System.out.println("开始 通知时间 = " + Utils.data(System.currentTimeMillis()));
lock.notify();
System.out.println("结束 通知时间 = " + Utils.data(System.currentTimeMillis()));
}
}
}
public class Run1 {
public static void main(String[] args) {
try {
Object object = new Object();
MyThread1 t1 = new MyThread1(object);
t1.start();
Thread.sleep(3000);
MyThread2 t2 = new MyThread2(object);
t2.start();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
从运行结果来看,3秒后线程被通知唤醒。
使用wait/notify可以在某个特定条件下,销毁线程。
public class MyList {
private static List list= new ArrayList<>();
public static void add(){
list.add("1");
}
public static int size(){
return list.size();
}
}
public class ThreadA extends Thread{
private Object lock;
public ThreadA(Object lock) {
this.lock = lock;
}
@Override
public void run(){
try {
synchronized (lock){
if(MyList.size() != 5){
System.out.println("开始等待 = " + Utils.data(System.currentTimeMillis()));
lock.wait();
System.out.println("结束等待 = " + Utils.data(System.currentTimeMillis()));
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadB extends Thread{
private Object lock;
public ThreadB(Object lock) {
this.lock = lock;
}
@Override
public void run(){
try {
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));
Thread.sleep(1000);
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class Run1 {
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
ThreadA t1 = new ThreadA(lock);
t1.start();
Thread.sleep(50);
ThreadB t2 = new ThreadB(lock);
t2.start();
}
}
日志信息“结束等待”在最后输出,这也说明notify()方法执行后并不立即释放锁,这点后面会补充介绍。
关键字synchronzied可以将任何一个Object作为锁来看待,而Java为每个Object都实现了wait()和notify()方法,它们必须用在被同步的Object的临界区内。通过调用wait()方法可以使处于临界区内的线程进入等待状态,同时释放被同步对象的锁,而notify操作可以唤醒一个由于调用了wait操作而处于wait状态中的线程,使其进入就绪状态,被重新唤醒的线程会试图重新获取临界区的控制权,也就是锁,并继续执行临界区内wait之后的代码。如果发出notify操作时没有处于wait状态中的线程,那么该命令就会被忽略。
notify()方法按照执行wait方法的顺序唤醒等待同一个锁的“一个”线程,进入可运行状态。也就是notify()方法仅通知“一个”线程。而notifyAll()方法执行后,会按照执行 wait()方法的倒序依次唤醒全部的线程。
前面的代码是在自定义的类中处理业务,而业务代码要尽量放在Service类中进行处理。
public class MyList {
volatile private List list = new ArrayList<>();
public void add(){
list.add("q");
}
public int size(){
return list.size();
}
}
public class MyService {
private Object lock = new Object();
private MyList list = new MyList();
public void watiMethod(){
try {
synchronized (lock){
if(list.size() != 5){
System.out.println("开始等待 = " + Utils.data(System.currentTimeMillis()) + " " + Thread.currentThread().getName());
lock.wait();
System.out.println("结束等待 = " + Utils.data(System.currentTimeMillis()) + " " + Thread.currentThread().getName());
}
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void notifyMethod(){
try {
synchronized (lock){
System.out.println("开始通知 = " + Utils.data(System.currentTimeMillis()) + " " + Thread.currentThread().getName());
for (int i = 0; i < 10; i++) {
list.add();
if(list.size() == 5){
lock.notify();
System.out.println("发出通知,wait后面的代码还没有立即执行,因为锁没有释放");
}
System.out.println("add次数 " + (i+1));
Thread.sleep(1000);
}
System.out.println("结束通知 = " + Utils.data(System.currentTimeMillis()) + " " + Thread.currentThread().getName());
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadA extends Thread{
private MyService service;
public ThreadA(MyService service) {
this.service = service;
}
@Override
public void run(){
service.watiMethod();
}
}
public class ThreadB extends Thread{
private MyService service;
public ThreadB(MyService service) {
this.service = service;
}
@Override
public void run(){
service.notifyMethod();
}
}
public class Run1 {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA t1 = new ThreadA(service);
t1.start();
Thread.sleep(3000);
ThreadB t2 = new ThreadB(service);
t2.start();
}
}
前面介绍了与Thread有关的大部分API,这些API可以改变线程对象的状态。
1)创建了一个新的线程对象后,再调用它的start方法,系统会为此线程分配CPU资源,处于可运行状态,是一个准备运行的阶段。如果线程抢占到CPU资源,此线程就处于运行状态。
2)可运行状态和运行状态可互相切换,因为有可能线程运行一段时间后其他高优先级的线程抢占了CPU资源,这时此线程就从运行状态变成可运行状态。
线程进入可运行状态可分为以下四种情况:
1、调用sleep()方法后经过的时间超过了指定的休眠时间;
2、线程成功获得了试图同步的监视器;
3、线程正在等待某个通知,其他线程发出了通知;
4、处于挂起状态的线程调用了resume方法。
3)暂停状态结束后,线程进入可运行状态,等待系统重新分配资源。
出现阻塞的情况可分为以下5种:
1、线程调用sleep方法,主动放弃占用的处理器资源;
2、线程调用了阻塞式I/O方法,在该方法返回前,该线程被阻塞;
3、线程试图获得一个同步监视器,但该监视器正被其他线程所持有;
4、线程等待某个通知;
5、程序调用了suspend方法将该线程挂起。此方法容易导致死锁,应尽量避免使用。
4)run方法运行结束后进入销毁阶段,整个线程执行完毕。
wait()被执行后,锁会被立即释放,但执行完notify方法后,锁不会立即释放。
public class Service {
public void testMethod(Object lock){
try {
synchronized (lock){
System.out.println("开始等待:");
lock.wait();
System.out.println("结束等待");
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadA extends Thread{
private Object lock;
public ThreadA(Object lock) {
this.lock = lock;
}
@Override
public void run(){
Service service = new Service();
service.testMethod(lock);
}
}
public class ThreadB extends Thread{
private Object lock;
public ThreadB(Object lock) {
this.lock = lock;
}
@Override
public void run(){
Service service = new Service();
service.testMethod(lock);
}
}
public class Run1 {
public static void main(String[] args) {
Object locl = new Object();
ThreadA t1 = new ThreadA(locl);
t1.start();
ThreadB t2 = new ThreadB(locl);
t2.start();
}
}
如果将wait方法改成sleep方法,就成了同步效果。
public class Service {
public void testMethod(Object lock){
try {
synchronized (lock){
System.out.println("开始等待:");
Thread.sleep(4000);
System.out.println("结束等待");
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
还有一个结论要进行实验:方法notify被执行后,不立即释放锁。
public class MyService {
private Object lock = new Object();
public void testMethod(){
try {
synchronized (lock){
System.out.println("开始等待时间 : " + Utils.data(System.currentTimeMillis()) + Thread.currentThread().getName());
lock.wait();
System.out.println("结束等待时间 : " + Utils.data(System.currentTimeMillis()) + Thread.currentThread().getName());
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void testNofity(){
try {
synchronized (lock){
System.out.println("开始通知时间 : " + Utils.data(System.currentTimeMillis()) + Thread.currentThread().getName());
lock.notify();
Thread.sleep(4000);
System.out.println("结束通知时间 : " + Utils.data(System.currentTimeMillis()) + Thread.currentThread().getName());
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadA extends Thread{
private MyService service;
public ThreadA(MyService service) {
this.service = service;
}
@Override
public void run(){
service.testMethod();
}
}
public class ThreadB extends Thread{
private MyService service;
public ThreadB(MyService service) {
this.service = service;
}
@Override
public void run(){
service.testNofity();
}
}
public class Run1 {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA t1 = new ThreadA(service);
t1.start();
Thread.sleep(500);
ThreadB t2 = new ThreadB(service);
t2.start();
}
}
通过控制台打印的时间来分析,得到的结论:必须执行完notify方法所在的同步代码块后,才释放锁。
每次调用notify方法时,只通知一个线程进行唤醒,唤醒的顺序按执行wait方法的正序。
public class MyService {
private Object lock = new Object();
public void waitMethod(){
try {
synchronized (lock){
System.out.println("开始等待时间 = " + Utils.data(System.currentTimeMillis()) + Thread.currentThread().getName());
lock.wait();
System.out.println("结束等待时间 = " + Utils.data(System.currentTimeMillis()) + Thread.currentThread().getName());
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
public void notifyMethod(){
synchronized (lock){
System.out.println("开始唤醒时间 = " + Utils.data(System.currentTimeMillis()) + Thread.currentThread().getName());
lock.notify();
System.out.println("结束唤醒时间 = " + Utils.data(System.currentTimeMillis()) + Thread.currentThread().getName());
}
}
}
public class ThreadA extends Thread{
private MyService service;
public ThreadA(MyService service) {
this.service = service;
}
@Override
public void run(){
service.waitMethod();
}
}
public class ThreadB extends Thread{
private MyService service;
public ThreadB(MyService service) {
this.service = service;
}
@Override
public void run(){
service.notifyMethod();
}
}
public class Run1 {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
for (int i = 0; i < 10; i++) {
ThreadA t1 = new ThreadA(service);
t1.start();
}
Thread.sleep(1000);
ThreadB t1 = new ThreadB(service);
t1.start();
Thread.sleep(500);
ThreadB t2 = new ThreadB(service);
t2.start();
Thread.sleep(500);
ThreadB t3 = new ThreadB(service);
t3.start();
Thread.sleep(500);
ThreadB t4 = new ThreadB(service);
t4.start();
Thread.sleep(500);
ThreadB t5 = new ThreadB(service);
t5.start();
}
通过以上几个实验,得出以下三个结论:
1、执行完notify方法后,按照执行wait的顺序唤醒其他线程。notify所在的同步到代码块执行完才会释放对象的锁,其他线程继续执行wait之后的代码。
2、在执行同步代码块的过程中,遇到异常而导致县城终止时,锁也会被释放。
3、在执行同步代码块的过程中执行了锁所属对象的wait方法,这个线程会释放对象锁,等待被唤醒。
前面示例中通过多次调用notify方法来实现5个线程被唤醒,当并不能保证实际系统中仅有5个线程,就是notify方法的调用次数小于线程对象的数量,那么会出现部分线程对象没有被唤醒的情况。为了唤醒全部线程,可以使用notifyAll方法。
注意,notifyAll方法会按照执行wait方法的倒序依次对其他线程进行唤醒。对上一节的MyService.java进行修改,把notifyMethod()方法中的notify()改成notifyAll(),然后再新建Run2.java类。
public class Run2 {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
for (int i = 0; i < 10; i++) {
ThreadA t1 = new ThreadA(service);
t1.start();
}
Thread.sleep(1000);
ThreadB t2 = new ThreadB(service);
t2.start();
}
}
通过运行结果看到,唤醒的顺序是调用wait()方法的倒序。