Java线程学习笔记之线程协作(通信)

线程协作

在Java线程的使用中,仅仅有线程同步是不够的,还需要线程与线程协作(即通信),生产者/消费者问题是一个经典的线程同步以及通信的案例。下面我们通过他来理解线程协作。
该问题描述了两个共享固定大小缓冲区的线程,即所谓的“生产者”和“消费者”在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。要解决该问题,就必须让生产者在缓冲区满时进入等待(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入等待,等到生产者往缓冲区添加数据之后,再唤醒消费者,通常采用线程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入等待,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。下面是JDK5之前传统线程的通信方式。问题代码如下:

package thread_test;

import java.util.ArrayList;
import java.util.List;
/**定义一个工作区类*/
public class WorkArea {

    List<Object> dataBuffer = new ArrayList<Object>();//装数据的共享缓存区
    /**
     * 取数据
     */
    public synchronized Object getData(){
        while(dataBuffer.size() == 0){
            try{
                System.out.println("消费者线程: "+Thread.currentThread().getName()+" 未检查到数据,进入等待...");
                wait();
                System.out.println("消费者线程: "+Thread.currentThread().getName()+" 被唤醒并获得锁,继续执行...");
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
        Object data = dataBuffer.get(0);
        dataBuffer.clear();//清空缓存区S
        System.out.println("消费者线程: "+Thread.currentThread().getName()+"拿到数据,释放锁,结束运行");
        notify();//唤醒阻塞队列的某线程到就绪队列
        return data;
    }
    /**
     * 写入数据
     */
    public synchronized void putData(Object data){
        while(dataBuffer.size() > 0){
            try{
                System.out.println("生产者线程: "+Thread.currentThread().getName()+" 检查到数据,进入等待...");
                wait();
                System.out.println("生产者线程: "+Thread.currentThread().getName()+" 被唤醒并获得锁,继续执行...");
            }catch(InterruptedException e){
                e.printStackTrace();
            }
        }
        dataBuffer.add(data);
        System.out.println("生产者线程: "+Thread.currentThread().getName()+"写入数据,释放锁,结束运行");
        notify();//唤醒阻塞队列的某个线程到就绪队列
    }
    /**
     * 生产者线程
     */
    static class Producer implements Runnable{
        private WorkArea workArea;
        private Object data = new Object();

        public Producer(WorkArea workArea) {
            this.workArea = workArea;
        }
        @Override
        public void run() {
            workArea.putData(data);
        }
    }
    /**
     * 消费者线程
     */
    static class Customer implements Runnable{
        private WorkArea workArea;

        public Customer(WorkArea workArea) {
            this.workArea = workArea;
        }
        @Override
        public void run() {
            workArea.getData();
        }
    }

    public static void main(String[] args){
        WorkArea workArea = new WorkArea();
        for(int i=1;i<=3;i++){
            new Thread(new Customer(workArea)).start();
            new Thread(new Producer(workArea)).start();
        }
    }
}

输出结果:

消费者线程: Thread-0 未检查到数据,进入等待...
生产者线程: Thread-1写入数据,释放锁,结束运行
消费者线程: Thread-0 被唤醒并获得锁,继续执行...
消费者线程: Thread-0拿到数据,释放锁,结束运行
消费者线程: Thread-2 未检查到数据,进入等待...
消费者线程: Thread-4 未检查到数据,进入等待...
生产者线程: Thread-3写入数据,释放锁,结束运行
消费者线程: Thread-2 被唤醒并获得锁,继续执行...
消费者线程: Thread-2拿到数据,释放锁,结束运行
消费者线程: Thread-4 被唤醒并获得锁,继续执行...
消费者线程: Thread-4 未检查到数据,进入等待...
生产者线程: Thread-5写入数据,释放锁,结束运行
消费者线程: Thread-4 被唤醒并获得锁,继续执行...
消费者线程: Thread-4拿到数据,释放锁,结束运行

wait()、notify() 和 notifyAll() 方法

Object 类定义了 wait()、notify() 和 notifyAll() 方法。要执行这些方法,必须拥有相关对象的锁。
Wait() 会让调用线程休眠,直到用 Thread.interrupt() 中断它、过了指定的时间、或者另一个线程用 notify() 或 notifyAll() 唤醒它。
当对某个对象调用 notify() 时,如果有任何线程正在通过 wait() 等待该对象,那么就会唤醒其中一个线程T,从对象的等待集中删除线程 T,并重新进行线程调度。然后,该线程以常规方式与其他线程竞争,以获得在该对象上同步的权利(即获得该对象的锁)。当对某个对象调用 notifyAll() 时,会唤醒所有正在等待该对象的线程。
在上面的代码中,调用对象的wait()方法时,会放在while循环中,而不是if循环,这是因为在没有被通知、中断或超时的情况下,线程还可以唤醒一个所谓的虚假唤醒 (spurious wakeup)。虽然这种情况在实践中很少发生,但是应用程序必须通过以下方式防止其发生,即对应该导致该线程被提醒的条件进行测试,如果不满足该条件,则继续等待。也就是把它放在while循环中。还有,wait和notify方法必须工作于synchronized内部,且这两个方法只能由锁对象来调用。因为调用该方法的线程必须拥有此对象监视器

sleep()和wait()的区别

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。
sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。
在调用sleep()方法的过程中,线程不会释放对象锁。
而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备,在前一个线程释放锁后,准备就绪队列中的线程开始竞争,获取对象锁进入运行状态。
在等待锁定池中的线程,如果不是时间到了,自动唤醒或者被notify唤醒,那么会永远处于等待状态(你可以把上面代码中的两个notify()注释掉,运行代码,就会发现凡是进入等待状态的线程都没有被唤醒继续执行任务,也不会打印出’结束运行’字段)。而synchronized在一个线程释放掉该锁后,处于准备就绪队列中的线程会竞争获得该对象锁,而未获得的线程仍然处于该队列中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章