Java中的多线程-05

前言

本来是想接着上面的那个写的,不过想到每次都写的非常的长也是没什么道理可言的。这次就多拆分一下下。

线程间的通信

前面我们已经说了不少的多线程了,但是多个线程之间到底是如何通信呢?这个问题我们之前就提出来了。我们使用多个线程都拥有的实例变量,并且使用了线程同步的机制,使得我们可以使用多个线程一起输出0~100。这算是线程通信的一个方面。但是如果我们需要的是要两个线程一个一个输出该如何做呢?有人说那好办,我让一个线程输出玩之后使用yield方法,放弃他的资源,让另一个线程进入。如此下去不就可以实现多线程轮流输出了嘛。但是我们很明白的是,yield方法虽然会放弃当前线程的资源,但是这并不意味这另一个线程就会抢到执行的机会。放弃资源的线程还是有机会抢到线程的,所以说这个方式是错误的。

至于如何实现这个功能,就需要新的方法。那就是我们之前说过的方法。wait()notify()方法。

下面是有关的方法的说明。

wait():
    // 令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,
    // 而当 前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,
    // 唤醒后等待重新获得对监视器的所有 权后才能继续执行

notify():
    // 唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll():
    // 唤醒正在排队等待资源的所有线程结束等待.

注意:

  • 注意上面的方法都提到了同步资源这个事。因为这几个方法只有在同步代码块或者说值同步方法中才可以调用。如果不在其中的话,会抛出java.lang.IllegalMonitorStateException
  • yieldsleep方法不同的是,上面的三个方法是由同步监视器来调用的。而同步监视器可以是任何的对象。这就说明了上面的三个方法肯定是Object中声明的方法。

那么我们回到之前的问题中,如何让两个线程一个接着一个的输出0-100中的所有的数。

class WaitThread implements Runnable{
    private int i = 1;
    private final Object mObject = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (mObject) {
                mObject.notify();
                if (i <= 100) {
                    System.out.println(Thread.currentThread().getName()+": "+i);
                    ++i;
                } else {
                    break;
                }
                try {
                    mObject.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

这里我们没有使用this作为同步监视器,就是为了更好的说明waitnotify的方法是由同步监视器来调用的。如果是把this作为同步监视器的话,那么我们只需要使用wait()notify(),可以省略了调用的对象this

首先线程一进入了同步代码块,第九行使用了mObject.notify()唤醒一个因为wait()而进入阻塞状态的线程。但是此时没有线程因为wait进入阻塞状态,所以此时改行代码是无效的。等到输出了1之后,使用了mObject.wait()方法,该线程进入了阻塞状态。注意这个阻塞状态是sleep的阻塞状态是不一样的,此时线程会暂停,但是线程还是会释放同步监视器。因为线程一释放了同步监视器,线程二得到了同步监视器,进入了同步代码块。此时执行了mObject.notify(),刚才因为使用了wait()方法而进入阻塞状态的线程一被唤醒。但是唤醒之后是不会立马执行的。线程一会等在原地等待得到了锁。等到线程二也执行到了mObject.wait()方法,线程二暂停,线程一得到了锁,继续执行之后的代码。然后重复操作。

通过上面的操作就可以实现交互输出的功能。不过有一点需要注意的是,使用wait方法之后,线程虽然释放了锁,但是线程还是处于同步代码块中。

上面我们已经说明了waitnotify方法的使用。还有一个notifyAll方法我们还没有进行说明。其实和名字也是可以简单的的看出了的。notify是从因wait阻塞中的线程中随机取一个唤醒。上面的问题中其实只有一个线程,所以说唤醒的肯定是唤醒另一个线程。notifyAll方法是将所以的因为wait方法而进入阻塞状态的所有的线程。不过需要注意的是,所谓的唤醒并不是直接就执行了,而是进入抢夺锁的线程池中抢夺锁,抢到了锁之后从之前因为wait阻塞的断点处继续执行。

生产者与消费者

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处 取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图 生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通 知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如 果店中有产品了再通知消费者来取走产品

上面是多线程中的一个经典的例题。这个问题中会出现一下的两个问题。

  • 生产者比消费者快时,消费者会漏掉一些数据没有取到。
  • 消费者比生产者快时,消费者会取相同的数据

如何解决这个问题,我们就需要使用上面学的线程通信的知识来解决。

首先是雇员的实现,雇员有从生产者中那儿取得产品,像消费者送产品的职责。

class Clerk {
    private int product = 0;

    public synchronized void addProductor() {
        if (product >= 20) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            product++;
            System.out.println("ADD--->product = " + product);
            notifyAll();
        }
    }

    public synchronized void getProductor() {
        if (this.product <= 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("GET--->product = " + product);
            product--;
            notifyAll();
        }
    }
}

添加产品和取得产品的方法中都是相似的。里面都有wait方法和notifyAll方法。当生产的产品达到20件的时候,生产者的线程将会被阻塞,但是当产品小于20的时候,会调用notifyAll方法唤醒之前沉睡的生产者线程,不过为什么是notifyAll呢?那是因为生产者可能是不止一个的。消费者也是同样的道理。通过这种方式我们就解决了上面我们提出的问题。

class Productor implements Runnable {

    private Clerk mClerk;

    public Productor(Clerk clerk) {
        this.mClerk = clerk;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(new Random().nextInt(100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            mClerk.addProductor();
        }
    }
}

class Consumer implements Runnable {

    private Clerk mClerk;

    public Consumer(Clerk clerk) {
        this.mClerk = clerk;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.mClerk.getProductor();
        }
    }
}
public class Demo8 {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Thread thread = new Thread(new Productor(clerk));
        Thread thread1 = new Thread(new Consumer(clerk));
        Thread thread2 = new Thread(new Consumer(clerk));
        thread.start();
        thread1.start();
        thread2.start();
    }
}

上面的代码中,我并没有设置什么时候停止运行,也就是生产者会一直生产,消费者也会一直消费下去。

一般来书生产者是有一个终止条件的,之前学爬虫的时候也学到了使用多线程的方式进行爬虫。虽然没有谈及线程通信,但是用到了生产者与消费者的模式。其中生产者一直爬取所需要的url,而消费者可以通过url进行下载数据。通过多线程加上这样效率高的模式,爬虫的效率可以提升好几倍。

总结

上面就是waitnotify[All]方法的使用,以及使用这两个方法进行简单的线程之间的通信。至此,多线程的学习基本上就要告一段落了。


一枚小菜鸡