?
? ? 生产者与消费者是个很好的线程通信的例子,生产者在一个循环中不断生产共享数据,而消费者则不断消费生产者生产的共享数据。程序必须保证有共享数据,如果没有,消费者必须等待生产新的共享数据。两者之间的数据关系如下:
1) 生产者生产前,如果共享数据没有被消费,则生产等待;生产者生产后,通知消费者消费。
2)消费者消费前,如果共享数据已经被消费完,则消费者等待;消费者消费后,通知生产者生产。
? ? 为了解决生产者和消费者的矛盾,引入了等待/通知(wait/notify)机制。
?
class="java" name="code">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 方法循环消费元素,并将元素从共享队列删除。?
?
共享队列类是用于保存生产者生产、消费者消费的共享数据。共享队列有两个域: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方法唤醒生产者线程进行生产。
? 下面是生产者与消费者程序的主程序。
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方法必须放在同步块或同步方法中。?
?
6. 线程死锁
? ? ? 为了保证数据安全使用 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()会强制终止线程,但是该线程也不会释放对象锁。?
?