@lemonguge
2015-07-07T01:23:37.000000Z
字数 9167
阅读 374
Concurrency
对于上一篇文章所遗留的问题,也就是笔者即将进行介绍的内容。
在第一篇介绍多线程的状态时,笔者曾经提到过这三个方法。这三个方法都是监视器的方法,在之前的同步块中,说过:“在同步块中,我们也把锁称为监视器对象。”
wait()方法和sleep()方法都可以使当前线程从运行状态变为冻结状态。注意,wait()方法会同时释放执行权和释放锁,sleep()方法释放执行权不释放锁。这句话很容易理解,如果不是释放执行权,将相当与CPU一直等待,这种现象会使电脑死机的;如果wait()方法不释放锁,由于需要被其他线程唤醒,其他线程如果拿不到锁的话那就不能被唤醒了。
notify()方法和notifyAll()方法是用来唤醒线程的。在此,重复一遍:“notify()唤醒该监视器所监视的处于冻结状态的所有线程的任意一个线程,notifyAll()唤醒该监视器所监视的所有冻结线程。”
清楚了上面介绍的这三个方法,我们可以对之前的问题进行解决。当烤鸭店生产了一只烤鸭后,便通过监视器唤醒消费者“哥们,烤鸭做好啦,过来吃”,同时使烤鸭店处于等待状态,只要消费者消费了一只烤鸭,那么监视器便唤醒烤鸭店“老板,我吃完啦,再来一只”,同时使消费者处于等待状态。
import java.io.BufferedOutputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.PrintStream;// 资源代表烤鸭class Resource{boolean flag; // 通过标志位判断烤鸭是否做好了}// 生产者代表餐厅class Producer implements Runnable{private Resource res;public Producer(Resource res) {this.res = res;}@Overridepublic void run() {while (true)synchronized (res) { // 临界区if (!res.flag) {System.out.println("Producer run..");res.flag = true;res.notify(); // 唤醒消费者,烤鸭做好啦try { // 会出现中断异常,由于异常限制,只能捕获不能声明res.wait(); // 使当前线程等待,即烤鸭刚做好,等着消费} catch (InterruptedException e) {e.printStackTrace();}} else {System.out.println("Resource exist!");}}}}// 消费者class Consumer implements Runnable{private Resource res;public Consumer(Resource res) {this.res = res;}@Overridepublic void run() {while (true)synchronized (res) { // 临界区if (res.flag) {System.out.println("Consumer eat..");res.flag = false;res.notify(); // 唤醒烤鸭店,烤鸭吃完啦try {res.wait(); // 刚刚消费烤鸭,烤鸭没做好呢,继续等待} catch (Exception e) {e.printStackTrace();}} else {System.out.println("Waitting Resource!");}}}}public class OneToOne {public static void main(String[] args) throws InterruptedException {PrintStream ps = logger();Resource res = new Resource();Producer pro = new Producer(res);Consumer con = new Consumer(res);Thread t1 = new Thread(pro,"生产者");Thread t2 = new Thread(con,"消费者");t1.start();t2.start();Thread.sleep(250);ps.close();}public static PrintStream logger(){PrintStream ps = null;try {ps = new PrintStream(new BufferedOutputStream(new FileOutputStream("log.txt")),true);} catch (FileNotFoundException e) {e.printStackTrace();}System.setOut(ps);return ps;}} /* Output:Producer run..Consumer eat..Producer run..Consumer eat.....*///:~
输出结果符合我们的期望,通过上面的示例,相信大家清楚了这三个方法的使用。接下来,我们对上面的代码进行些修改和简化,虽然功能是相同,但是之后会演示一个现象。
import java.io.BufferedOutputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.PrintStream;// 资源代表烤鸭class Resource{boolean flag; // 通过标志位判断烤鸭是否做好了}// 生产者代表餐厅class Producer implements Runnable{private Resource res;public Producer(Resource res) {this.res = res;}@Overridepublic void run() {while (true)synchronized (res) { // 临界区if (res.flag)try { res.wait(); } catch (InterruptedException e) { }System.out.println(Thread.currentThread().getName() + "@Producer run..");res.flag = true;res.notify(); // 唤醒消费者,烤鸭做好啦}}}// 消费者class Consumer implements Runnable{private Resource res;public Consumer(Resource res) {this.res = res;}@Overridepublic void run() {while (true)synchronized (res) { // 临界区if (!res.flag)try { res.wait(); } catch (Exception e) { }System.out.println(Thread.currentThread().getName() + "@Consumer eat..");res.flag = false;res.notify(); // 唤醒烤鸭店,烤鸭吃完啦}}}public class OneToOne {public static void main(String[] args) throws InterruptedException {PrintStream ps = logger();Resource res = new Resource();Producer pro = new Producer(res);Consumer con = new Consumer(res);Thread t1 = new Thread(pro,"生产者");Thread t2 = new Thread(con,"消费者");t1.start();t2.start();Thread.sleep(250);ps.close();}public static PrintStream logger(){PrintStream ps = null;try {ps = new PrintStream(new BufferedOutputStream(new FileOutputStream("log.txt")),true);} catch (FileNotFoundException e) {e.printStackTrace();}System.setOut(ps);return ps;}} ///:OK~
对于上面的代码,这是面对于一个生产者和一个消费者的情形,如果是多个生产者和多个消费者的情形,那么会出现什么现象呢。假如现在没有了烤鸭,一个生产者开始生产了烤鸭,接下来在无限循环中的第二次判断使得该生产者处于了冻结状态,由于CPU的执行切换可能导致另一个生产者获得了CPU的执行权,此时烤鸭未被消费,也将处于冻结状态。直到一个消费者消费了一只烤鸭,如果监视器随机唤醒的线程恰好为一个生产者,那么很荣幸,生产者将会生成一只烤鸭,接下来监视器如果唤醒的仍然是一个生产者,此时生产者并不会判断烤鸭是否被消费而去直接生产烤鸭,这将会导致有两只烤鸭存在!以下为代码示例:
import java.io.BufferedOutputStream;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.PrintStream;// 资源代表烤鸭class Resource{boolean flag; // 通过标志位判断烤鸭是否做好了}// 生产者代表餐厅class Producer implements Runnable{private Resource res;public Producer(Resource res) {this.res = res;}@Overridepublic void run() {while (true)synchronized (res) { // 临界区if (res.flag)try { res.wait(); } catch (InterruptedException e) { }System.out.println(Thread.currentThread().getName() + "@Producer run..");res.flag = true;res.notify(); // 唤醒消费者,烤鸭做好啦}}}// 消费者class Consumer implements Runnable{private Resource res;public Consumer(Resource res) {this.res = res;}@Overridepublic void run() {while (true)synchronized (res) { // 临界区if (!res.flag)try { res.wait(); } catch (Exception e) { }System.out.println(Thread.currentThread().getName() + "@Consumer eat..");res.flag = false;res.notify(); // 唤醒烤鸭店,烤鸭吃完啦}}}public class OneToOne {// 两个生产者与一个消费者public static void main(String[] args) throws InterruptedException {PrintStream ps = logger();Resource res = new Resource();Producer pro1 = new Producer(res);Producer pro2 = new Producer(res);Consumer con1 = new Consumer(res);Thread t1 = new Thread(pro1,"生产者1");Thread t2 = new Thread(pro2,"生产者2");Thread t3 = new Thread(con1,"消费者");t1.start();t2.start();t3.start();Thread.sleep(250);ps.close();}public static PrintStream logger(){PrintStream ps = null;try {ps = new PrintStream(new BufferedOutputStream(new FileOutputStream("log2.txt")),true);} catch (FileNotFoundException e) {e.printStackTrace();}System.setOut(ps);return ps;}} /* Output: // 输出顺序并不确定,以下log2.txt文件中的一部分...生产者2@Producer run..消费者@Consumer eat..生产者2@Producer run..生产者1@Producer run..生产者2@Producer run..生产者1@Producer run..生产者2@Producer run..生产者1@Producer run.....*///:~
假唤醒:等待线程即使没有收到正确的信号,也能够执行后续的操作。
为了防止假唤醒,保存信号的成员变量将在一个while循环里接受检查,而不是在if表达式里。这样的一个while循环叫做自旋锁。代码修改如下:
// 资源代表烤鸭class Resource{boolean flag; // 通过标志位判断烤鸭是否做好了}// 生产者代表餐厅class Producer implements Runnable{private Resource res;public Producer(Resource res) {this.res = res;}@Overridepublic void run() {while (true)synchronized (res) { // 临界区while (res.flag)try { res.wait(); } catch (InterruptedException e) { }System.out.println(Thread.currentThread().getName() + "@Producer run..");res.flag = true;res.notify(); // 随机唤醒一个冻结了的线程}}}// 消费者class Consumer implements Runnable{private Resource res;public Consumer(Resource res) {this.res = res;}@Overridepublic void run() {while (true)synchronized (res) { // 临界区while (!res.flag)try { res.wait(); } catch (Exception e) { }System.out.println(Thread.currentThread().getName() + "@Consumer eat..");res.flag = false;res.notify();}}}public class OneToOne {// 两个生产者与一个消费者public static void main(String[] args) throws InterruptedException {Resource res = new Resource();Producer pro1 = new Producer(res);Producer pro2 = new Producer(res);Consumer con1 = new Consumer(res);Thread t1 = new Thread(pro1,"生产者1");Thread t2 = new Thread(pro2,"生产者2");Thread t3 = new Thread(con1,"消费者");t1.start();t2.start();t3.start();}} /* Output: // 输出顺序并不确定,以下输出语句的一部分...生产者1@Producer run..消费者@Consumer eat..生产者2@Producer run..消费者@Consumer eat.. // 输出停止了*///:~
为什么会输出停止?自旋锁防止了第二个生产者被唤醒后,没有接受检查而直接进行生成。`while``循环会再执行一次,促使醒来的线程回到等待状态。想想这么一种情景:这两个生产者因为生产过烤鸭而处于冻结状态,接着消费者消费了一只烤鸭后而处于冻结状态,同时唤醒了随机一个生产者线程。这个生产者在生成了一个烤鸭后,唤醒一个线程后自身处于冻结状态。如果随机唤醒了的线程为另一个生产者线程,这个生产者会因为自旋锁而处于冻结状态。到现在为止,这三个线程都处于了冻结状态,这就是为什么输出停止的原因。
就像上面代码所演示的,这种输出中止的现象我们称之为死锁,记住while判断标记+notify会导致死锁,死锁表现之一为所有的线程都处于冻结状态。notify因为唤醒的是同任务的线程而没有唤醒其他线程,如果要解决这种问题,我们可以使用notifyAll,这个方法会唤醒所有线程,可以避免死锁的发生!
// 资源代表烤鸭class Resource{boolean flag; // 通过标志位判断烤鸭是否做好了}// 生产者代表餐厅class Producer implements Runnable{private Resource res;public Producer(Resource res) {this.res = res;}@Overridepublic void run() {while (true)synchronized (res) { // 临界区while (res.flag)try { res.wait(); } catch (InterruptedException e) { }System.out.println(Thread.currentThread().getName() + "@Producer run..");res.flag = true;res.notifyAll(); // 唤醒所有线程}}}// 消费者class Consumer implements Runnable{private Resource res;public Consumer(Resource res) {this.res = res;}@Overridepublic void run() {while (true)synchronized (res) { // 临界区while (!res.flag)try { res.wait(); } catch (Exception e) { }System.out.println(Thread.currentThread().getName() + "@Consumer eat..");res.flag = false;res.notifyAll();}}}public class OneToOne {// 两个生产者与一个消费者public static void main(String[] args) throws InterruptedException {Resource res = new Resource();Producer pro1 = new Producer(res);Producer pro2 = new Producer(res);Consumer con1 = new Consumer(res);Thread t1 = new Thread(pro1,"生产者1");Thread t2 = new Thread(pro2,"生产者2");Thread t3 = new Thread(con1,"消费者");t1.start();t2.start();t3.start();}} ///:OK~
死锁更常见的场景表现为不同任务的多线程互相循环等待去持有对方任务线程占有的锁(同步的嵌套)。举个例子,甲在独木桥的一端A,乙在独木桥的另一端B,独木桥不允许两个人并行。如果甲要去B端(任务),而乙要去A端(另一个任务),如果它们一起出发(两个不同任务的线程开启),甲和乙肯定会在独木桥相碰。此时甲走过的路程(锁)和乙走过的路程(另一把锁),甲如果想到对面,则需要乙退回去(释放锁);同样乙若想到对面,会需要甲退回。如果互不想让的话,那么就谁也无法前进(死锁发生)。
class Lock {// 两把锁public Object lockA = new Object(); // 甲走过的路程public Object lockB = new Object(); // 乙走过的路程}// 甲的任务,从A端出发class RunA implements Runnable {private Lock lock;public RunA(Lock lock) {this.lock = lock;}@Overridepublic void run() {while (true)synchronized (lock.lockA) {System.out.print(" 甲要去B端");synchronized (lock.lockB) {System.out.println(",于是乙退回去");}}}}// 乙的任务,从B端出发class RunB implements Runnable {private Lock lock;public RunB(Lock lock) {this.lock = lock;}@Overridepublic void run() {while (true)synchronized (lock.lockB) {System.out.print(" 乙要去A端");synchronized (lock.lockA) {System.out.println(",于是甲退回去");}}}}public class DeadLock {public static void main(String[] args) {Lock lock = new Lock();new Thread(new RunA(lock)).start();new Thread(new RunB(lock)).start();}} /* Output: // 输出顺序并不确定,以下输出语句的一部分...甲要去B端,于是乙退回去甲要去B端,于是乙退回去甲要去B端 乙要去A端 // 死锁发生*///:~