线程交互是指两个线程之间通过通信联系对锁的获取与释放,从而达到较好的线程运行结果,避免引起混乱的结果。一般来说synchronized块的锁会让代码进入同步状态,即一个线程运行的同时让其它线程进行等待,那么如果需要进行实现更复杂的交互,则需要学习以下几个方法:
void notify():唤醒在此对象监视器上等待的单个线程。
void notifyAll():唤醒在此对象监视器上等待的所有线程。
void wait():让占用了这个同步对象的线程,临时释放当前的占用,并且等待。
wait()方法是使当前线程临时暂停,释放锁,并进入等待,其功能类似于sleep()方法,但是wait()需要释放锁,而sleep()不需要释放锁。以下通过例子说明wait()方法实现线程间的交互,举例子之前需要强调的是wait()方法一定存在于synchronized作用范围内,否则报错,因为它是同步对象上的方法。首先定义一个学生类:
1.理解wait()和notify()的相互关系
首先该类有两个同步方法,存钱和取钱,首先需要保证当取钱取到只剩0元时,当前取钱的线程应该进入等待状态,此时调用同步对象的notify状态,我们为了观察notify()方法的作用,首先不使用它,进行对比:
package person;
import java.util.concurrent.locks.*;
public class Student {
public int k=0;
public int total;
public int perMonth;
public String name;
Lock lock=new ReentrantLock();
public synchronized void saveMoney(Student s) {
// TODO Auto-generated method stub
s.total=s.total+2*(s.perMonth);
System.out.println(s.name+"存了"+2*s.perMonth+"当前总共拥有"+s.total+"元");
}
public synchronized void getMoney(Student s)
{
if(total<=0)
{
try
{
this.wait();//让占有this的取钱过程,暂时释放对this的占有,并等待,等待其它线程的调用同步对象的notify方法来唤醒它。
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
s.total=s.total-s.perMonth;
System.out.println(s.name+"取了"+s.perMonth+"当前总共拥有"+s.total+"元");
}
}
测试程序如下,这个测试程序在下文中不改变:
package waytobuildthread;
import person.Student;
public class RunnableTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
final Object obj=new Object();
final Student s=new Student();
final Student jack=new Student();
s.total=1000;
s.perMonth=200;
s.name="Tom";
System.out.println("Tom最开始拥有"+s.total+"元");
Thread[] save=new Thread[6];
Thread[] get=new Thread[10];
for(int i=0;i<10;i++)
{
Thread t1= new Thread(){
public void run(){
s.getMoney(s);;
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
get[i]=t1;
}
for(int i=0;i<6;i++)
{
Thread t2= new Thread(){
public void run(){
s.saveMoney(s);
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
t2.start();
save[i]=t2;
}
//等待线程结束
for(Thread t:save)
{
try
{
t.join();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
//等待线程结束
for(Thread t:get)
{
try
{
t.join();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
System.out.println("最后Tom总共拥有"+s.total+"元");
}
}
运行结果如下:
Tom最开始拥有1000元
Tom取了200当前总共拥有800元
Tom取了200当前总共拥有600元
Tom取了200当前总共拥有400元
Tom取了200当前总共拥有200元
Tom取了200当前总共拥有0元
Tom存了400当前总共拥有400元
Tom存了400当前总共拥有800元
Tom存了400当前总共拥有1200元
Tom存了400当前总共拥有1600元
Tom存了400当前总共拥有2000元
Tom存了400当前总共拥有2400元
从上面结果可以看出,当取钱取到只剩0元时,取钱线程便释放对this的占有,进入等待,但是由于没有唤醒等待线程的方法notify()方法,所以取钱线程便不会被唤醒,因此剩余的取钱线程不会被执行。那么下面我们在存钱方法中添加notify()方法,用于唤醒等待中的取钱方法:
package person;
import java.util.concurrent.locks.*;
public class Student {
public int k=0;
public int total;
public int perMonth;
public String name;
Lock lock=new ReentrantLock();
public synchronized void saveMoney(Student s) {
// TODO Auto-generated method stub
s.total=s.total+2*(s.perMonth);
System.out.println(s.name+"存了"+2*s.perMonth+"当前总共拥有"+s.total+"元");
this.notify();//用于唤醒暂停中的线程
}
public synchronized void getMoney(Student s)
{
if(total<=0)
{
try
{
this.wait();//让占有this的取钱过程,暂时释放对this的占有,并等待,等待其它线程的调用同步对象的notify方法来唤醒它。
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
s.total=s.total-s.perMonth;
System.out.println(s.name+"取了"+s.perMonth+"当前总共拥有"+s.total+"元");
}
}
运行测试程序,结果如下:
Tom最开始拥有1000元
Tom取了200当前总共拥有800元
Tom取了200当前总共拥有600元
Tom取了200当前总共拥有400元
Tom取了200当前总共拥有200元
Tom取了200当前总共拥有0元
Tom存了400当前总共拥有400元
Tom存了400当前总共拥有800元
Tom取了200当前总共拥有600元
Tom存了400当前总共拥有1000元
Tom取了200当前总共拥有800元
Tom取了200当前总共拥有600元
Tom取了200当前总共拥有400元
Tom存了400当前总共拥有800元
Tom存了400当前总共拥有1200元
Tom取了200当前总共拥有1000元
Tom存了400当前总共拥有1400元
最后Tom总共拥有1400元
对比两者关系可以看出notify的作用,上面例子说明当取钱取到只剩0元时,便会进入暂停,此时等待着的存钱线程便会被执行,每次执行之后便会唤醒暂停着的线程,使其进入等待状态。这就是notify()的作用。
2.深入交互
以上只是对取钱进行了临时暂停,释放占有,那么接下来如果对存钱过程也添加wait()方法又会怎样的呢?以下便是对其进行了两个同步方法都添加了暂停与唤醒功能,如下所示:
package person;
import java.util.concurrent.locks.*;
public class Student {
public int k=0;
public int total;
public int perMonth;
public String name;
Lock lock=new ReentrantLock();
public synchronized void saveMoney(Student s) {
// TODO Auto-generated method stub
if(total>=2000)
{
try
{
this.wait();
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
s.total=s.total+2*(s.perMonth);
System.out.println(s.name+"存了"+2*s.perMonth+"当前总共拥有"+s.total+"元");
this.notify();
}
public synchronized void getMoney(Student s)
{
if(total<=0)
{
try
{
this.wait();
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
s.total=s.total-s.perMonth;
System.out.println(s.name+"取了"+s.perMonth+"当前总共拥有"+s.total+"元");
this.notify();
}
}
运行结果如下:
Tom最开始拥有1000元
Tom取了200当前总共拥有800元
Tom取了200当前总共拥有600元
Tom取了200当前总共拥有400元
Tom取了200当前总共拥有200元
Tom取了200当前总共拥有0元
Tom存了400当前总共拥有400元
Tom取了200当前总共拥有200元
Tom取了200当前总共拥有0元
Tom取了200当前总共拥有-200元
Tom存了400当前总共拥有200元
Tom取了200当前总共拥有0元
Tom存了400当前总共拥有400元
Tom存了400当前总共拥有800元
Tom存了400当前总共拥有1200元
Tom取了200当前总共拥有1000元
Tom存了400当前总共拥有1400元
最后Tom总共拥有1400元
观察上述结果,会发现结果并未如我们期待那样,出现了一个负值,这是为什么呢因为当取钱取到只剩0时,接下去又调用了一个notify()方法,可能唤醒了另一个取钱线程,即继续取钱,所以会出现负值,那么怎么解决这一问题呢?很简单,就是将if改为while,表示如果账户余额如果不大于0,则不会取钱。则一直等待。同理存钱有上限,所以如果余额不小于上限,那么也会一直等待。因此,修改如下所示:
package person;
import java.util.concurrent.locks.*;
public class Student {
public int k=0;
public int total;
public int perMonth;
public String name;
Lock lock=new ReentrantLock();
public synchronized void saveMoney(Student s) {
// TODO Auto-generated method stub
while(total>=2000)
{
try
{
this.wait();
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
s.total=s.total+2*(s.perMonth);
System.out.println(s.name+"存了"+2*s.perMonth+"当前总共拥有"+s.total+"元");
this.notify();//唤醒那些等待着取钱的过程,可以醒过来了。
}
public synchronized void getMoney(Student s)
{
while(total<=0)
{
try
{
this.wait();//让占有this的取钱过程,暂时释放对this的占有,并等待,等待其它线程的调用同步对象的notify方法来唤醒它。
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
s.total=s.total-s.perMonth;
System.out.println(s.name+"取了"+s.perMonth+"当前总共拥有"+s.total+"元");
this.notify();
}
}
测试结果如下:
Tom最开始拥有1000元
Tom取了200当前总共拥有800元
Tom取了200当前总共拥有600元
Tom取了200当前总共拥有400元
Tom取了200当前总共拥有200元
Tom取了200当前总共拥有0元
Tom存了400当前总共拥有400元
Tom取了200当前总共拥有200元
Tom存了400当前总共拥有600元
Tom取了200当前总共拥有400元
Tom取了200当前总共拥有200元
Tom存了400当前总共拥有600元
Tom取了200当前总共拥有400元
Tom存了400当前总共拥有800元
Tom取了200当前总共拥有600元
Tom存了400当前总共拥有1000元
Tom存了400当前总共拥有1400元
最后Tom总共拥有1400元
根据上述结果可以知道,我们在进行多线程交互时,如果要对共享数据有约束条件,需要注意对wait()方法与notify()方法使用的时机与条件。
总之,wait()、notify()、notifyAll()三个方法必须在synchronized代码块中使用,否则会报错,另外,如果使用了wait()方法,如果想再次使用该方法,当前同步方法,则需要使用notify()唤醒。