1. 生产者与消费者
生产者与消费者是个很好的线程通信的例子,生产者在一个循环中不断生产共享数据,而消费者则不断消费生产者生产的共享数据。程序必须保证有共享数据,如果没有,消费者必须等待生产新的共享数据。两者之间的数据关系如下:
1) 生产者生产前,如果共享数据没有被消费,则生产等待;生产者生产后,通知消费者消费。
2)消费者消费前,如果共享数据已经被消费完,则消费者等待;消费者消费后,通知生产者生产。
为了解决生产者和消费者的矛盾,引入了等待/通知(wait/notify)机制。
class Producer extends Thread {
Queue q;
Producer(Queue q) {
this.q = q;
}
public void run() {
for (int i = 1; i < 5; i++) {
q.put(i);
}
}
}
class Consumer extends Thread {
Queue q; // 声明队列q
Consumer(Queue q){
this.q = q; // 队列q初始化
}
public void run() {
while (true) {// 循环消费元素
q.get(); // 获取队列中的元素
}
}
}
Producer 是一个生产者类,该生产者类提供一个以共享队列作为参数的构造方法,它的run 方法循环产生新的元素,并将元素添加于共享队列;Consumer 是一个消费者类,该消费者类提供一个以共享队列作为参数的构造方法,它的 run 方法循环消费元素,并将元素从共享队列删除。
2.共享队列
共享队列类是用于保存生产者生产、消费者消费的共享数据。共享队列有两个域:value(元素的数目)、isEmpty(队列的状态)。共享队列提供了put和 get 两个方法。
class Queue {
int value = 0; // 声明,并初始化整数类型数据域value
boolean isEmpty = true; // 声明,并初始化布尔类型数据域isEmpty,用于判断队列的状态
// 生产者生产方法
public synchronized void put(int v) {
// 如果共享数据没有被消费,则生产者等待
if (!isEmpty) {
try {
System.out.println("生产者等待");
wait(); // 进入等待状态
} catch (Exception e) // 捕获异常
{
e.printStackTrace(); // 异常信息输出
}
}
value += v; // value值加v
isEmpty = false; // isEmpty赋值为false
System.out.println("生产者共生产数量:" + v);
notify();
}
public synchronized int get() {
if (isEmpty) {
try {
System.out.println("消费者等待");
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
value--;
if (value < 1) {
isEmpty = true;
}
System.out.println("消费者消费一个,剩余:" + value);
notify();
return value;
}
}
生产者调用put方法生产共享数据,如果共享数据不为空,生产者线程进入等待状态;否则将生成新的数据,然后调用notify方法唤醒消费者线程进行消费;
消费者调用get方法消费共享数据,如果共享数据为空,消费者进入等待状态,否则将消费共享数据,然后提调用notify方法唤醒生产者线程进行生产。
3. 运行生产者与消费者
下面是生产者与消费者程序的主程序。
public class ThreadCommunication {
public static void main(String[] args) {
Queue q = new Queue();
Producer p = new Producer(q);
Consumer c = new Consumer(q);
c.start();
p.start();
}
}
注意:考虑到程序的安全性,多数情况下使用 notifiAll(),除非明确可以知道唤醒哪一个线程。wait方法调用的前提条件是当前线程获取了这个对象的锁,也就是说 wait方法必须放在同步块或同步方法中。
为了保证数据安全使用 synchronized同步机制,当线程进入堵塞状态(不可运行状态和等待状态)时,其他线程无法访问那个加锁对象(除非同步锁被解除),所以
一个线程会一直处于等待另一个对象的状态,而另一个对象又会处于等待下一个对象的状态,以此类推,这个线程“等待”状态链会发生很糟糕的情形,即封闭环状态(也就是说最后那个对象在等待第一个对象的锁)。此时,所有的线程都陷入毫无止境的等待状态中,无法继续运行,这种情况就称为“死锁”。虽然这种情况发生的概率很小,一旦出现,程序的调试变得困难而且查错也是一件很麻烦的事情。
下面举一个死锁的例子。
public class ThreadLocked implements Runnable {
public static boolean flag = true; // 起一个标志作用
private static Object A = new Object(); // 声明,并初始化静态Object数据域A
private static Object B = new Object(); // 声明,并初始化静态Object数据域B
public static void main(String[] args) throws InterruptedException {
Runnable r1 = new ThreadLocked(); // 创建,并初始化ThreadLocked对象r1
Thread t1 = new Thread(r1); // 创建线程t1
Runnable r2 = new ThreadLocked(); // 创建,并初始化ThreadLocked对象r2
Thread t2 = new Thread(r2); // 创建线程t2
t1.start(); // 启动线程t1
t2.start(); // 启动线程t2
}
public void AccessA() {
flag = false; // 初始化域flag
// 同步代码快
synchronized (A) { // 声明同步块,给对象A加锁
System.out.println("线程t1 : 我得到了A的锁"); // 输出字符串信息
try {
// 让当前线程睡眠,从而让另外一个线程可以先得到对象B的锁
Thread.sleep(1000); // 休眠
} catch (InterruptedException e) { // 捕获异常
e.printStackTrace(); // 异常信息输出
}
System.out.println("线程t1 : 我还想要得到B的锁");
// 在得到A锁之后,又想得到B的锁
// 同步块内部嵌套同步块
synchronized (B) { // 声明内部嵌套同步块,指定对象B的锁
System.out.println("线程t1 : 我得到了B的锁"); // 输出字符串信息
}
}
}
public void AccessB() {
flag = true; // 修改flag的值
// 同步代码块
synchronized (B) { // 指定同步块,给B加锁
System.out.println("线程t2 : 我得到了B的锁"); // 输出字符串信息
try {
// 让当前线程睡眠,从而让另外一个线程可以先得到对象A的锁
Thread.sleep(1000); // 休眠
} catch (InterruptedException e) { // 捕获异常InterruptedException
e.printStackTrace(); // 异常信息输出
}
System.out.println("线程t2 : 我还想要得到A的锁"); // 字符串信息输出
// 在得到B锁之后,又想得到A的锁
// 同步块内部嵌套内部快
synchronized (A) { // 指定同步块,给A加锁
System.out.println("线程t2 : 我得到了A的锁"); // 输出字符串信息
}
}
}
public void run() {
if (flag){ // 当flag为true,执行下面语句
AccessA(); // 调用AccessA方法
} else {
AccessB(); // 调用AccessB方法
}
}
}
程序 ThreadLocked.java中创建了两个线程 t1 和 t2,并且声明两个方法:AccessA和 AccessB。在运行过程中,线程t1 先获得了 A 的锁,然后又要求获得 B 的锁;而 t2
先获得B 的锁,然后又要求获得 A的锁,此时便进入了无休止的相互等待状态,即死锁。
Java 语言本身并没有提供防止死锁的具体方法,但是在具体程序设计时必须要谨慎,以防止出现死锁现象。通常在程序设计中应注意,不要使用 stop()、suspend()、resume()以及 destroy()方法。 stop()方法不安全,它会解除由该线程获得的所有对象锁,而且可能使对象处于不连贯状态,如果其他线程此时访问对象,而导致的错误很难检查出来。suspend()/resume ()方法也极不安全,调用 suspend()方法时,线程会停下来,但是该线程并没有放弃对象的锁,导致其他线程并不能获得对象锁。调用destroy()会强制终止线程,但是该线程也不会释放对象锁。