您的位置:首页 > 编程语言 > Java开发

java线程之间的通信

2014-04-16 17:19 302 查看
本文讲解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的属性,因为这样一方面很危险,第二可读性差,第三代码冗杂。即要坚持面向对象的思想。


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息