多线程之间的通信-wait方法和notify方法解析

死锁(deadLock):

有两个线程,A线程获取到a资源,B线程获取到b资源,每个线程都必须获取到a,b两个资源才可以继续往下执行,但在此种情况下,A线程和B线程都不愿意放开自己手中的资源,此为死锁。

 

wait方法

wait是object类中的方法,而且被fianl修饰。wait方法可以被所有的类所继承并且不能被重写。

wait方法会造成当前线程等待,直到有其他的线程调用同一对象的notify方法或者notifyAll()方法。

当前线程必须拥有这个对象的锁,才可以执行wait方法。那么什么时候才能确保一个线程拥有对象的锁呢? 当把wait方法放在synchronized方法或者synchronized块里面时才可以确保当前线程获取到了对象的锁,因为synchronized方法或者synchronized块只允许一个线程访问。

synchronized (obj) {
         while (<condition does not hold>)
             obj.wait();
         ... // Perform action appropriate to condition
     }

当执行wait方法之后,当前线程会释放掉对象的锁并且等待,直到有其他的线程唤醒那些正在等待对象的锁的线程,唤醒的方法不是notify方法就是notifyAll方法。等待对象锁的线程会一直等待,直到它重新获得锁的拥有权后再继续往下执行。

notify方法

notify方法也是Object类中的方法,并且是final的,可以被所有的类所继承并且不能被重写。

notify方法会唤醒一个正在等待对象锁的线程。如果有多个线程都在等待这个对象的锁,那么他们其中的一个会被选择唤醒,这个选择是任意的。

被唤醒的线程是不能立刻执行的,直到当前线程放弃对象的锁。被唤醒的线程会与其他的线程竞争谁先获得对象的锁。notify方法应该被拥有对象的锁的线程所调用,所以notify方法也是应该放在synchronized方法或者synchronized块中的。

总结

wait方法和notify方法都是Object类中的方法,而且都是final的,可以被所有的类所继承并且无法重写。这两个方法要求在调用时线程已经获得对象的锁,因此对这两个方法的调用应该放在synchronized方法或者块中。当线程调用wait方法,它会释放对象的锁。

 案例

通过控制多条线程在控制台打印010101010101010.......当打印出0时,调用增加数字的线程加1;当打印1时,再调用减少数字的线程让数字减少1,结果是总是在01010之间循环。

Sample类  

/*
 * 定义同步增加数字和减少数字的方法
 */
package com.test1;

public class Sample {

	int number;
	
	public synchronized void increaseNumber(){
		//当数字等于1时,此时应该让增加的线程等待,直到有其他线程唤醒再继续执行		
		while (1 == number) {

			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
		}
			//代码执行到这里,说明数字已经被减少为0,并将增加方法的线程唤醒
			number++;
			System.out.println(number);
			//数字已经增加为1,此时可以去唤醒数字减少的线程
			notify();	
		
		
	}
	
	public synchronized void decreaseNumber(){
		//当数字等于0时,应该让减少数字的方法等待,直到有其他的线程唤醒它在继续执行
		while (0 == number) {
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
			//线程执行到这里,说明数字已经增加为1,并将减少方法的线程唤醒。
			number--;
			System.out.println(number);
			//数字已经减少为0,此时可以去唤醒数字减少的线程。
			notify();	
		
		
	}
}

IncreaseThread 增加数字的线程

package com.test1;

public class IncreaseThread extends Thread {

	private Sample sample;
	
	public IncreaseThread(Sample sample){
		
		this.sample = sample;
	}
	
	@Override
	public void run() {
	
		for(int i = 0 ; i < 20 ; i++){
			try {
				Thread.sleep((long)(Math.random() * 1000));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			sample.increaseNumber();
		}
	}
	
}

DecreaseThread 减少数字的线程

package com.test1;

public class DecreaseThread extends Thread {

	private Sample sample;
	
	public DecreaseThread (Sample sample){
		
		this.sample = sample;
		
	}
	
	@Override
	public void run() {
		for(int i = 0 ; i < 20 ; i++){
			try {
				Thread.sleep((long)(Math.random() * 1000));
			} catch (InterruptedException e) {
				e.printStackTrace();
			};	
			
			sample.decreaseNumber();
		}
	}
}

 MainTest  测试方法

public class MainTest {

	public static void main(String[] args) {
		
		Sample sample = new Sample();
		
		Thread t1 = new IncreaseThread(sample);
		Thread t2 = new DecreaseThread(sample);
		
		Thread t3 = new IncreaseThread(sample);
		Thread t4 = new DecreaseThread(sample);
		
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		
	}
}

需要注意的是,我们在测试类中分别定义了两个增加数字的线程和两个减少数字的线程,此时需要将增加数字的同步方法和减少数字的同步方法中的判断改为while循环。

因为如果将while改为if判断,线程会出现问题,打印的数字会乱掉。

我们把可能会出现数字混乱的其中一种情况做一个分析,前提是先将while改为if。为了描述方便,字母A和B代表增加数字的线程,字母C和D代表减少数字的线程,假设刚开始number==0,首先可能是C线程先获得对象的锁,发现判断为0==number,判断为真,所以线程等待,释放掉对象的锁,然后可能D线程又获得对象的锁,发现0==number,判断也为真,然后线程进入等待,释放掉对象的锁。接着A线程可能获得了对象的锁,判断if(0!=number) ,为false,所以数字number++,此时数字变为1了,接着去唤醒其他正在等待的线程。先不考虑B线程的情况下,此时C、D线程都在等待,假设C线程获得了对象的锁,因为我们使用的是if判断,所以C线程没有对number的值进行判断,继续执行wait方法后面的代码,将number--,此时数字变为0,接着唤醒其他的线程,然后假设D线程可能获得了对象的锁,此时D线程也没有对number的值进行判断,而是直接执行wait方法后面的代码,所以数字由0变为-1,这时数字的值就与我们的预期不符了,线程失去了控制。是什么原因导致的呢?造成这样的原因是在获得对象的锁的线程每次进入同步方法以后没有对数字的值进行判断,有可能是两个减少数字的线程相继获得了对象的锁,就造成数字减了两次。那么解决办法就是当我们把if判断改为while,在每次线程进入同步方法时,都会对数字的值进行判断,如果不符合条件,就不再往下执行,而是让其他线程先去判断再执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值