美文网首页oo
从生产者消费者开始讲线程

从生产者消费者开始讲线程

作者: 阿莫米德 | 来源:发表于2017-02-27 22:52 被阅读22次
    先说多线程的好处:
    • 多线程技术使程序的响应速度更快,用户界面可以在进行其它工作的同时一直处于活动状态;
    • 当前没有进行处理的任务时可以将处理器时间让给其它任务;
    • 占用大量处理时间的任务可以定期将处理器时间让给其它任务;
    • 可以随时停止任务;
    • 可以分别设置各个任务的优先级以优化性能。

    创建线程主要的方法有两个:

    1、继承Thread

    1)定义一个类A 继承于Java.lang.Thread类
    2)在A类中覆盖Thread类中的run方法。在run方法中我们编写需要执行的操作,run方法里的是线程执行体。
    3)在main方法中创建线程对象,并启动线程的
    A a = new A(); a.start();
    可以看出,这个方法中,线程和线程所要执行的任务是绑定在一起的,无法共享资源。

    2、实现Runable接口

    1)定义一个类A实现java.lang.Runable接口。类A只是一个普通的类。
    2)在A类中覆盖Runable接口中的run方法,编写需要执行的操作。
    3)在main方法中,创建线程对象,并启动线程。
    Thread t = new Thread(new A()); t.start()
    在这个方法中,一个任务可以开辟多个线程(即一个类被传进多个线程实例),这些线程执行的是同一个任务,即资源共享。

    生产者消费者进程

    很明显,生产者和消费者是两个不同的进程,但是它们共享一个资源,所以要用第二种方法来实现。
    作为被共享的资源,资源类要先被创建。

    ///资源类
    public class ShareResource {
        String name;
        int kg;
    //生产者要用到的push方法
        public void push(String name, int kg) {
                this.name = name;
                Thread.sleep(10);
                this.kg = kg;
        }
        public void pop() {
                System.out.println("食物名称:" + name + "重量" + kg);
            }
    }
        ```
    接着Resource的实例作为参数传递进Producer类和Consumer类。
    

    //消费者进程
    public class Consumer implements Runnable {
    private ShareResource resource = null;
    Consumer(ShareResource resource) {
    this.resource = resource;
    } //重载一个带参数的构造器,将共享的资源传进来
    public void run() {
    for (int i = 0; i < 50; i++) {
    resource.pop();
    }
    }
    }
    //生产者进程
    public class Producer implements Runnable {
    private ShareResource resource = null;
    Producer(ShareResource resource) {
    this.resource = resource;
    } //重载一个带参数的构造器,将共享的资源传进来
    public void run() {
    for (int i = 0; i < 50; i++) {
    if (i % 2 == 1) {
    resource.push("prok", 5);
    } else {
    resource.push("rice", 3);
    }
    }
    }
    }

    最后是一个测试方法
    

    public class Producer_Consumer {
    public static void main(String[] args) {
    ShareResource resource = new ShareResource();
    Thread producer = new Thread(new Producer(resource));
    Thread consumer = new Thread(new Consumer(resource));
    producer.start();
    consumer.start();
    }
    }

    这样一个基本的生产者消费者框架就出来了,但是即使运行了,也存在线程安全问题,下面要讲线程的同步。
    
    #####线程的同步 
    线程的同步就是,虽然不能控制线程的执行顺序,但是我们可以控制哪些代码必须在一起执行,比如生产者进程要用的push()方法中,物品的名称和重量的赋值行为必须绑定在一起,如果不绑定在一起,中间断开了会出错。比如:原resource的值为猪肉 10kg,现在有一个新的值 羊肉 5kg 要被传入,当羊肉赋值给name之后,5kg还没来得及赋值,消费者线程已经开始执行pop操作,这时消费者取走的就时羊肉 10kg。 这就产生了错误。所以要引入synchronized 修饰词。 
    **三种机制的模式都为 “加锁--修改公共变量--释放锁”** 
    1.同步代码块 当要执行的代码被synchronized{}包含在内之后,里面的代码就称为一次原子操作。 代码会一次执行完name 和kg的赋值后自动释放锁。
    

    public void push(String name, int kg) {
    synchronized(this){
    this.name = name;
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }
    this.kg = kg;
    }
    }

    2.同步方法 顾名思义,在方法前面加上synchronized修饰符。也是执行完毕之后自动释放锁。但是,千万不能用synchronized来修饰run方法,如下面是消费者进程的run方法,这样,一旦消费者线程开始执行,就会一次性取出50次,失去了线程存在的意义。
    

    public synchronized void run() {
    for (int i = 0; i < 50; i++) {
    resource.pop();
    }
    }

    3.lock机制 
    其中最常用的是RetrantLock可重入锁.  RetrantLock是Lock接口的一个实现类。可重入锁由最近成功获得锁,并且还没有释放该锁的线程所拥有。
    官方推荐我们的最佳用法如下,因为Lock机制不会自动解锁,所以必须在用完之后手动解锁,我们将要运行的代码写进try{ }中。
    

    class X {
    private final ReentrantLock lock = new ReentrantLock();//此处为创建一个锁对象
    // ...

    public void m() {
    lock.lock(); // block until condition holds
    try {
    // ... method body
    } finally {
    lock.unlock()
    }
    }
    }

    但是加入锁机制也不能使得程序按照我们的想法运行起来。因为生产者消费者还需要进行通信,生产者生产出一个产品之后通知消费者来取,生产一个消费一个的顺序。
    **线程的通信**
    线程的通信有两种方式,一种方式基于synchronized同步方法,另一种基于锁机制。
    1. 基于synchronized同步方法  
    这里主要用的是wait() 和notify()方法。这两个方法都只能被同步锁对象来调用,所以必须使用synchronized来修饰,并定义了一个boolean的isEmpty变量,来判断是否要进入该方法。
    

    public class Resource {
    String name;
    int kg;
    private final ReentrantLock lock = new ReentrantLock();
    boolean isEmpty = true;
    public void push(String name, int kg) {
    synchronized (this) {
    try {
    while (!isEmpty) {
    this.wait();
    }
    this.name = name;
    Thread.sleep(10);
    this.kg = kg;
    isEmpty = false;
    this.notify();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    synchronized public void pop() {
    try {
    while (isEmpty) {
    this.wait();
    }
    System.out.println("食物名称:" + name + "重量" + kg);
    isEmpty=true;
    this.notify();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }

    第二种方法用到Lock机制。Lock能完成Synchronized所实现的所有功能。但是Lock机制要与Condition绑定在一起使用。此时用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()。
    

    public class Resource {
    String name;
    int kg;
    private final ReentrantLock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();
    boolean isEmpty = true;

    public void push(String name, int kg) {
            try {
                while (!isEmpty) {
                    condition.await();
                }
                lock.lock();
                this.name = name;
                Thread.sleep(10);
                this.kg = kg;
                isEmpty = false;
                condition.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally{
                lock.unlock();
            }
    }
    
     public void pop() {
        try {
            while (isEmpty) {
                condition.await();
            }
            lock.lock();
            System.out.println("食物名称:" + name + "重量" + kg);
            isEmpty = true;
            condition.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    

    }

    运行结果如下:
    

    食物名称:rice重量3
    食物名称:prok重量5
    食物名称:rice重量3
    食物名称:prok重量5
    食物名称:rice重量3
    食物名称:prok重量5
    食物名称:rice重量3
    食物名称:prok重量5
    食物名称:rice重量3
    食物名称:prok重量5
    食物名称:rice重量3

    在这个生产者消费者的例子中,Lock机制相比于Synchronized的优势不明显,因为这里只有两个线程,一个阻塞之后唤醒的必然是另外一个。所以只需要new出一个condition对象。但是线程中,不管是signal()还是nofify()方法都是从线程池中随机挑选出一个线程来唤醒,如果存在多个生产者和消费者,那么生产者唤醒的就未必是消费者了,也有可能是另一个生产者。这个时候只需要new两个condition对象。
    如下 muxProducer 和muxConsumer,这样当生产者阻塞了唤醒的一定是消费者,消费者阻塞了唤醒的一定是生产者。
    

    public class Resource {
    String name;
    int kg;
    private final ReentrantLock lock = new ReentrantLock();
    public Condition muxProducer = lock.newCondition();
    public Condition muxConsumer = lock.newCondition();
    boolean isEmpty = true;

    public void push(String name, int kg) {
        lock.lock();
            try {
                while (!isEmpty) {
                    muxProducer.await();
                }
                this.name = name;
                Thread.sleep(10);
                this.kg = kg;
                isEmpty = false;
                muxConsumer.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally{
                lock.unlock();
            }
    }
    
     public void pop() {
        lock.lock();
        try {
            while (isEmpty) {
                muxConsumer.await();
            }
            System.out.println(Thread.currentThread().getName()+"取得食物:" + name + "重量" + kg);
            isEmpty = true;
            muxProducer.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    

    }

    **这里一定要注意, lock.lock();一定要写在muxProducer.await();
    之前。不然就要报错。因为任务如果要执行等待或者进行唤醒,前提是必须拥有锁。**
    运行结果为:
    

    Thread-0生产出米饭3kg
    Thread-2取得食物:rice重量3
    Thread-1生产出米饭3kg
    Thread-2取得食物:rice重量3
    Thread-1生产出猪肉5kg
    Thread-3取得食物:prok重量5
    Thread-1生产出米饭3kg
    Thread-2取得食物:rice重量3
    Thread-1生产出猪肉5kg
    Thread-3取得食物:prok重量5

    相关文章

      网友评论

      本文标题:从生产者消费者开始讲线程

      本文链接:https://www.haomeiwen.com/subject/iastgttx.html