Java 多线程 :入门- 线程间协作:挂起当前线程(wait)与通知其他线程继续执行(notify notifyAll)

Posted ajjiangxin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 多线程 :入门- 线程间协作:挂起当前线程(wait)与通知其他线程继续执行(notify notifyAll)相关的知识,希望对你有一定的参考价值。

首先,之前我理解起来比较混沌的是到底谁是‘锁’这个问题,因为线程间协作的核心就是锁的交换通过每个线程的“获得锁”与“释放锁”来实现。

锁,也叫“互斥”,是一种机制,通过控制一个对象在一定代码段(或方法内)同时只能被一个线程所访问,来实现所谓的(对于这个特定对象的)“线程安全”。

 

1.先看一个从网上扒来的最基本款示例,原文 http://www.cnphp6.com/archives/62258,写的很棒很清晰,我这里略微改了一两句:

public class TestNotifyByFlag {
    private String flag[] = {"true"};class NotifyThread extends Thread{
        public NotifyThread(String name){
            super(name);
        }
        public void run(){            
            synchronized(flag){
                flag[0] = "false";
                flag.notifyAll();
                System.out.println("Message1: NotifyThread仍然持有锁,因为代码段还未执行完毕。");
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("Message2: 这一句在synchronized代码段之外,此时锁已经释放了,所以这一句几时能运行就不一定了。");
        }
    }
    
    class WaitThread extends Thread{
        public WaitThread(String name){
            super(name);
        }
        
        public void run(){
            synchronized(flag){
                while(flag[0] != "false"){
                    System.out.println(getName()+" beginWaiting!");
                    long startToWait = System.currentTimeMillis();
                    try{
                        flag.wait();
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
                    long waitTime = System.currentTimeMillis() - startToWait;
                    System.out.println("waitTime: "+waitTime);
                }
                System.out.println(getName()+" end waiting!");
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("Main Thread Run!");
        TestNotifyByFlag test = new TestNotifyByFlag();
        NotifyThread notify = test.new NotifyThread("notifty01");
        WaitThread wait01 = test.new WaitThread("wait01");
        WaitThread wait02 = test.new WaitThread("wait02");
        WaitThread wait03 = test.new WaitThread("wait03");
        wait01.start();
        wait02.start();
        wait03.start();    
        try{
            Thread.sleep(5000);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        notify.start();
    }
}

结果:

Main Thread Run!
wait01 beginWaiting!
wait02 beginWaiting!
wait03 beginWaiting!
Message1: NotifyThread仍然持有锁,因为代码段还未执行完毕.
waitTime: 4999
wait03 end waiting!
waitTime: 4999
wait02 end waiting!
waitTime: 5000
wait01 end waiting!
Message2: 这一句在synchronized代码段之外,此时锁已经释放了.


 

补充与解读:

1)大意:先运行三个WaitThread,分别进入wait(),挂起,等待,并开始计时;5秒后运行一个NotifyThread,notifyAll()所有挂起的线程,被通知的三个WaitThread接到通知,继续进行(继续运行各自的run()函数),并输出等待时长。

2)显然,这里的“锁”是flag这个String[]类型的属性,基于这个对象传递挂起与通知的状态。其中,三个WaitThread线程,谁先进入synchronized(flag){..}代码段,谁先获得这个flag锁,又因为这个线程手里掌握着flag锁,才可以在这个线程内,由flag调用wait(), 即把拿到手的flag锁从当前线程释放掉,并持续监听flag将来可能传来的通知。三个WaitThread依次完成这个动作(executor不保证具体一定哪个线程最先开始)。补充一句,因为synchronized声明的锁是flag,所以代码段内只要是wait(), notify(), notifyAll()三种函数,都只能由该锁来触发(毕竟,从逻辑上来说,东西在谁手里,谁才可以扔掉,且你手里拿着的是一个斧头,你不能说扔掉的是一把锯子吧,拿和放的物件必须是一致的)否则,但凡这三个函数不是由flag触发,就会报错:IllegalMoniterStateException,即有synchronized(X)声明的X(moniter)状态已经发生改变,或者说此时再通过X这个引用已经不能再触发原有的那个moniter对象,那么当然不能释放原有moniter含有的锁。

3)此时,由于三个WaitThread都依次捡起又释放掉flag,那么flag此时是空闲的,所以才有可能被一个NotifyThread再次获取,并由flag调用notifyAll(),即flag通知所有正在监听其状态的线程:你们可以继续了。注意:

------- 多次试验,Message1都会先于(即将被激活的)WaitThread来运行,说明NotifyThread在调用NotifyAll()之后,并未马上将锁释放,而是老老实实运行完synchronized代码段,然后再释放;

------- 由于锁已经释放,Message2具体何时执行,就完全取决于executor的安排了,这里加入sleep,让结果更加直观,即一旦释放了锁,当前函数(代码段)的原子性也就不复存在了(中断了);

4)这里有一个点,flag不用String而用String[],原文说的原因:

...(若flag是String)对在同步块中对flag进行了赋值操作,使得flag引用的对象改变,这时候再调用notify方法时,因为没有控制权所以抛出异常。
我们可以改进一下,将flag改成一个JavaBean,然后更改它的属性不会影响到flag的引用。我们这里改成数组来试试,也可以达到同样的效果...

意思是“flag = “xxx” 这个动作的结果是:flag(指向的对象)变了,即synchronized(flag)与之后的flag.notifyAll()不是同一个对象了,所以就IllegalMoniterStateException了,可见synchronized(X)这一句里,synchronized标注的是这个X(作为指向jvm堆内存某个对象的地址,不可以变化,因为这个“锁”是锁在这一句声明时的X所指向的对象上的的,X一旦指向不同的对象,新的X就不具备该锁了),注:X不可以是int boolean等原始类型(无法通过编译)。

 

 

 

 

2.再看《Java编程思想》里的例子:

public class TestNotifyByInterruption {
...
    class Car{
        private boolean waxOn = false;
        public synchronized void waxed(){
            waxOn = true;
            notifyAll();
        }
        public synchronized void buffed(){
            waxOn = false;
            notifyAll();
        }
        public synchronized void waitForWaxing() throws InterruptedException{
            while(waxOn == false){
                wait();
            }
        }
        public synchronized void waitForBuffing() throws InterruptedException{
            while(waxOn == true){
                wait();
            }
        }
    }
    
    class WaxOn implements Runnable{
        private Car car;
        public WaxOn(Car car){
            this.car = car;
        }
        public void run(){
            try{
                while(!Thread.interrupted()){
                    print("Wax On!");
                    TimeUnit.MILLISECONDS.sleep(500);
                    car.waxed();
                    car.waitForBuffing();
                }
            }catch(InterruptedException e){
                print("WaxOn Exiting via interrupt");
            }
            print("Ending WaxOn Task");
        }
    }
    
    class WaxOff implements Runnable{
        private Car car;
        public WaxOff(Car car){
            this.car = car;
        }
        public void run(){
            try{
                while(!Thread.interrupted()){
                    car.waitForWaxing();
                    print("Wax Off!");
                    TimeUnit.MILLISECONDS.sleep(500);
                    car.buffed();
                }
            }catch(InterruptedException e){
                print("WaxOff Exiting via interrupt");
            }
            print("Ending WaxOff Task");
        }
    }
    
    
    public static void main(String[] args) throws InterruptedException {
        TestNotifyByInterruption test = new TestNotifyByInterruption();
        Car car = test.new Car();
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(test.new WaxOff(car));
        exec.execute(test.new WaxOn(car));
        TimeUnit.MILLISECONDS.sleep(5000);
        exec.shutdownNow();
    }

}

结果:

Wax On!
Wax Off!
Wax On!
Wax Off!
Wax On!
Wax Off!
Wax On!
Wax Off!
Wax On!
Wax Off!
WaxOn Exiting via interrupt
WaxOff Exiting via interrupt
Ending WaxOff Task
Ending WaxOn Task

补充与解读:

1)这里,WaxOn与WaxOff的这两个线程是同步存在的,这两个线程交替地的某一个被挂起而某一个被通知执行是由二者共同调用的一个Car对象的一些synchronized修饰的函数内的wait()与notifyAll()决定的,即在这个例子里,“锁”的关系通过car这个Car类型的实例来传递(car对象就是WaxOn与WaxOff各自的moniter),具体被锁住的是car这个对象的一些方法,或者再换句话说,car对象的任一一个synchronized方法被线程A调用执行时,这个方法就被锁住,但整个car对象并未被锁住,它的其他方法仍可以被其他对象同时访问。

2)显然,线程A直接调用moniter来让A自己wait()或notify()他人,或者,线程A通过调用moniter内的同步方法来让A自己wait()或notify()他人,两种方式并无本质区别,只是第二例子多了一个通过while代码段(也叫“忙等待”)监视一个标志(Thread.interrupted),来实现代码段的反复调用而已

 

 

 

3.最后多举一个例子:

public class TestNotify {
class Obj{
        int num = 20;

        public int getNum() {
            return num;
        }

        public synchronized void setNum(int num) {
            System.out.println("setNum() started!");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.num = num;
            System.out.println("setNum() finished!");
        }
    }
    
    class TaskA implements Runnable{
        private Obj obj;
        public TaskA(Obj obj){
            this.obj = obj;
        }
        public void run(){
                System.out.println("TaskA: getNum():"+obj.num);
                System.out.println("TaskA: num:"+obj.num);
        }
    }
    
    
    class TaskB implements Runnable{
        private Obj obj;
        public TaskB(Obj obj){
            this.obj = obj;
        }
        public void run(){
                obj.setNum(35);
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        TestNotify test = new TestNotify();
        Obj obj = test.new Obj();
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(test.new TaskB(obj));
        exec.execute(test.new TaskB(obj));
        Thread.sleep(1000);
        exec.execute(test.new TaskA(obj));
    }
}

结果:

setNum() started!
TaskA: getNum():20
TaskA: num:20
setNum() finished!
setNum() started!
setNum() finished!

补充与解读:

------  一个TaskB要等待另一个TaskB释放锁才能继续,应为TaskB中调用了Obj的同步方法setNum(), 而TaskA不受影响,因为其访问的是Obj的非同步方法和属性

------ 更进一步(这里没有贴出代码,但也很简单),如果TaskB中有synchronized(obj)关键字,而TaskA中没有,那么TaskA也不受影响,可以(与TaskB)同时访问obj。

 

根据网上的资料:

------- synchronized代码段和仅synchronized关键字修饰的方法都可以称为对象锁,形式是synchronized(X)时,X(对象或是方法)为锁,所有具备这个表达式的线程都得等X被释放(如果不具备这个表达式的线程不受影响,可以同时访问X及X内非synchronized方法与属性),X可以为this,但是当X为对象A的方法时,其他线程调用A的其他方法或属性都是OK的,仅X方法需要等锁被释放。

------- synchronized+static关键字修饰的方法(或代码段?)称为类锁,即这个类的所有对象的所有线程都得等锁释放才能继续。

 

需要注意的就两点:

1. synchronized(X){....}与代码段内的X.wait()(或notify(), notifyAll())的两个X一定是同一个对象,这个不难理解。

2. 对于“线程A内含有X.wait()”最简单的理解方式(前提,X可以是对象,可以是方法,但线程A这个瞬间一定拥有X的锁): A线程将自己挂起,释放X,并监听关于其他线程通过X(可能)发来的任何notify通知

  相应地,含有X.notify()或X.notifyAll()的最简单理解: A线程通知所有在监听X的其他线程,一旦A自己将X释放(运行完synchronized(X)的所有代码),这些线程可以马上获取X

 

 

我的体会大概就是这些,欢迎补充和纠正。

这里是一些文章:

http://www.cnphp6.com/archives/62258

http://zhidao.baidu.com/link?url=OAxIF39R0uZrsBJTCvw2g7STdSdcKOqFuJU1z11esbJY_6IzeTJBegiLNUQ0KhgOrSUs7tQY47a8g4XlaWTAL_

http://blog.csdn.net/intlgj/article/details/6245226

以上是关于Java 多线程 :入门- 线程间协作:挂起当前线程(wait)与通知其他线程继续执行(notify notifyAll)的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程线程基础线程间的共享和协作

java并发编程 线程间协作

Java多线程之线程的状态以及线程间协作通信导致的状态变换

Java入门——多线程

Java并发编程线程间协作(上)

多线程之线程间协作的两种方式:waitnotifynotifyAll和Condition