本文讲解java线程间的通信,通过wait(),notify(),notifyAll().来实现。程序通过生产者Producer和消费者Consumer模式的例子来展开。
本文通过对程序示例的创建和改进过程,实现对以下三点的理解:
1. 实现线程同步
【有一个缓冲区,存放着一种记录结构 [name , sex] , 生产者不停地向缓冲区生产 张三 男 , 李四 女 ; 消费者不停地 消费 张三 男 , 李四 女, 若生产者向缓冲区 刚产生完 张三 , 还没生产 男 时线程被切换到消费者,那么消费者从缓冲区取出的 就是 张三 女 , 就发生线程不同步了 】
2. 实现线程之间的通信
要实现的是:生产者每生产一个 对象(张三 男)或者(李四 女) 后,就轮到消费者 消费一个对象(张三 男)或者(李四 女) ,消费者消费完一个对象后,再由生产者生产一个,再消费一个,如此循环。 即要设置不同线程调用run()方法的先后顺序,就要实现线程之间的通信。用wait(),notify(),notifyAll()方法。
3. 对wait(),notify(),notifyAll()主语的深层次理解。
注意: wait(), notify()的主语是synchronized(o) 中的监视器对象o . 程序一般不写主语,表示是this.wait(),this.notify(),表示监视器对象是当前的类对象。如果synchronized(o) 中的 监视器对象o不是 this , 则一定要写o.wait(),o.notify().
---------------------------------------------------------------------------------------------------------------------
1. TestThread14.java
package com.thread; public class TestThread14 { public static void main(String[] args) { Q q = new Q(); Producer producer = new Producer(q); Consumer consumer = new Consumer(q); new Thread(producer).start(); new Thread(consumer).start(); } } class Producer implements Runnable { Q q; public Producer(Q q) { this.q = q; } public void run() { int flag = 0; while (true) { if (flag == 1) { q.name = "张三 "; // 加上这句使得 线程不同步的情况更容易出现 try { Thread.sleep(1); } catch (Exception e) { e.printStackTrace(); } q.sex = "男"; } else { q.name = "李四"; q.sex = "女"; } flag = (flag + 1) % 2; } } } class Consumer implements Runnable { Q q; public Consumer(Q q) { this.q = q; } public void run() { while (true) { System.out.print(q.name); System.out.println(":" + q.sex); } } } class Q { String name = "unknown"; String sex = "unknown"; }
运行结果:
张三 变成 女,标明线程不同步。解决办法,在两个类中要同步的代码块,加上同一个监视器对象synchronized(q) .见下面程序2.
2. TestThread14
package com.thread; public class TestThread14 { public static void main(String[] args) { Q q = new Q(); Producer producer = new Producer(q); Consumer consumer = new Consumer(q); new Thread(producer).start(); new Thread(consumer).start(); } } class Producer implements Runnable { Q q; public Producer(Q q) { this.q = q; } public void run() { int flag = 0; while (true) { synchronized (q) { if (flag == 1) { q.name = "张三 "; // 加上这句使得 线程不同步的情况更容易出现 try { Thread.sleep(1); } catch (Exception e) { e.printStackTrace(); } q.sex = "男"; } else { q.name = "李四"; q.sex = "女"; } } flag = (flag + 1) % 2; } } } class Consumer implements Runnable { Q q; public Consumer(Q q) { this.q = q; } public void run() { while (true) { synchronized (q) { System.out.print(q.name); System.out.println(":" + q.sex); } } } } class Q { String name = "unknown"; String sex = "unknown"; }
运行结果: 张三 男,李四 女 结果正确。 name 和 sex 已经实现同步。
注意:此处很特殊,是要将两个类中的代码块同步。但是方法是一样的,即:给两个类中要同步的代码块加上同一个监视器对象。本例中,类Producer和类Consumer共同拥有对象Q q, 所以,用q 作为他们共有的监视器,确保两个类中需要同步的代码块同步。
以上已经实现线程同步,但如何实现线程间的通信?即如何实现 生产者 生产一个 张三 男,消费者 打印一个 张三男,然后生产者再生产一个,消费者打印一个,依次执行。这就需要两个线程之间的通信。
3. TestThread14.java
package com.thread; public class TestThread14 { public static void main(String[] args) { Q q = new Q(); Producer producer = new Producer(q); Consumer consumer = new Consumer(q); new Thread(producer).start(); new Thread(consumer).start(); } } class Producer implements Runnable { Q q; public Producer(Q q) { this.q = q; } public void run() { int flag = 0; while (true) { synchronized (q) { if (q.bFull) { // 如果 缓冲区q有数据,则 生产者暂停生产 try{q.wait();}catch(Exception e){e.printStackTrace();} } if (flag == 1) { q.name = "张三 "; try {Thread.sleep(1);} catch (Exception e) {e.printStackTrace();} q.sex = "男"; } else { q.name = "李四"; q.sex = "女"; } //生产完毕,缓冲区标识有数据 q.bFull = true; // 生产完毕后,缓冲区有数据,释放监视器q,通知等待监视器q的线程(消费者) q.notify(); } flag = (flag + 1) % 2; } } } class Consumer implements Runnable { Q q; public Consumer(Q q) { this.q = q; } public void run() { while (true) { synchronized (q) { if (!q.bFull) { //若缓冲区是空的,则消费者暂停读取数据。 try{q.wait();}catch(Exception e){e.printStackTrace();} } System.out.print(q.name); System.out.println(":" + q.sex); // 消费者读取完数据后,标识缓冲区为空 q.bFull = false; // 缓冲区为空,消费者释放监视器q,通知等待监视器q的线程(生产者) q.notify(); } } } } class Q { String name = "unknown"; String sex = "unknown"; boolean bFull = false; }
运行结果:正确,生产者生产一条记录,消费者消费一条记录,再生产一条,再消费一条。
注意: 线程通信应使用q.wait(),q.notify(). 要使用共同的监视器对象q。
----------------------------------------------------------------------------------------------------------------
上面的代码设计很乱,缓冲区Q的数据是通过Q外的类进行操作的,很危险,且代码冗杂。更好的设计是:把生产者生产数据及消费者消费数据封装到缓冲区Q中。
push(String name , String sex) ; get() 方法。
注意新的封装仍然要实现两点:
1). 线程同步 (不能取到 数据 张三 女 , 或者 李四 男)
2). 线程通信 (生产1条记录,消费1条记录,依次循环,不能乱。比如生产了3条记录,消费1条记录等等。)
4.TestThread14
package com.thread; public class TestThread14 { public static void main(String[] args) { Q q = new Q(); Producer producer = new Producer(q); Consumer consumer = new Consumer(q); new Thread(producer).start(); new Thread(consumer).start(); } } class Producer implements Runnable { Q q; public Producer(Q q) { this.q = q; } public void run() { int flag = 0; while (true) { if (flag == 0) { q.push("张三", "男"); }else { q.push("李四", "女"); } flag = (flag+1)%2; } } } class Consumer implements Runnable { Q q; public Consumer(Q q) { this.q = q; } public void run() { while (true) { q.get(); } } } class Q { String name = "unknown"; String sex = "unknown"; boolean bFull = false; public synchronized void push(String name , String sex){ if (bFull) { //若缓冲区是满的,则生产者暂停生产。等待 监视器this. try{wait();}catch(Exception e){e.printStackTrace();} } this.name = name; try{Thread.sleep(1);}catch(Exception e){e.printStackTrace();} this.sex = sex; // 生产完毕后,标识缓冲区为满 bFull = true; // 生产完成后,释放 监视器this的锁旗标,通知等待this锁旗标的其他线程 notify(); } public synchronized void get(){ if (!bFull) { // 若缓冲区为空,则消费者暂时不读取,等待 监视器this的锁旗标 try{wait();}catch(Exception e){e.printStackTrace();} } System.out.print(name); System.out.println(":" + sex); //消费完毕后,缓冲区为空 bFull = false; //消费完毕后,缓冲区为空,消费者释放监视器this的所旗标,通知等待监视器this锁旗标的其他线程 notify(); } }
程序结果:
注意: 对比 程序4 和 程序3 ,功能一样,设计不一样。
最重要的区别:实现同步的监视器对象不同(程序4的监视器是q对象,而程序5的监视器是this),使得程序4 逻辑更清楚,代码更简洁。
总结:一个类A的属性尽量在A类体内操作,尽量不要让其他类B直接操作类A的属性,因为这样一方面很危险,第二可读性差,第三代码冗杂。即要坚持面向对象的思想。