java多线程---总结(2)

ThreadPoolExecutor

官方API解释线程池的好处:

(1)通过重用线程池中的线程,来减少每个线程创建和销毁的性能开销。

(2)对线程进行一些维护和管理,比如定时开始,周期执行,并发数控制等等。

一、Executor

       Executor是一个接口,跟线程池有关的基本都要跟他打交道。下面是常用的ThreadPoolExecutor的关系。

     Executor接口很简单,只有一个execute方法。

     ExecutorService是Executor的子接口,增加了一些常用的对线程的控制方法,之后使用线程池主要也是使用这些方法。

     AbstractExecutorService是一个抽象类。ThreadPoolExecutor就是实现了这个类。

二、ThreadPoolExecutor

     ThreadPoolExecutor类是线程池中最核心的一个类,因此如果要透彻地了解Java中的线程池,必须先了解这个类。

1、ThreadPoolExecutor类的四个构造方法。

复制代码
public class ThreadPoolExecutor extends AbstractExecutorService {
    .....
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}
复制代码

       构造方法参数讲解 

参数名 作用
corePoolSize 核心线程池大小
maximumPoolSize 最大线程池大小
keepAliveTime 线程池中超过corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)使得核心线程有效时间
TimeUnit keepAliveTime时间单位
workQueue 阻塞任务队列
threadFactory 新建线程工厂
RejectedExecutionHandler 当提交任务数超过maxmumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理

 2、ThreadPoolExecutor类中有几个非常重要的方法

//主要是这四个方法
execute()
submit()
shutdown()
shutdownNow()

(1)execute()

   execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

   源码

复制代码
 public void execute(Runnable command) {
         /*如果提交的任务为null  抛出空指针异常*/
        if (command == null)
            throw new NullPointerException();

        int c = ctl.get();
        /*如果当前的任务数小于等于设置的核心线程大小,那么调用addWorker直接执行该任务*/
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        /*如果当前的任务数大于设置的核心线程大小,而且当前的线程池状态时运行状态,那么向阻塞队列中添加任务*/
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        /*如果向队列中添加失败,那么就新开启一个线程来执行该任务*/
        else if (!addWorker(command, false))
            reject(command);
    }
复制代码

它的主要意思就是:

    任务提交给线程池之后的处理策略,这里总结一下主要有4点
当线程池中的线程数小于corePoolSize 时,新提交的任务直接新建一个线程执行任务(不管是否有空闲线程) 当线程池中的线程数等于corePoolSize 时,新提交的任务将会进入阻塞队列(workQueue)中,等待线程的调度 当阻塞队列满了以后,如果corePoolSize < maximumPoolSize ,则新提交的任务会新建线程执行任务,直至线程数达到maximumPoolSize 当线程数达到maximumPoolSize 时,新提交的任务会由(饱和策略)管理

(2)submit()

        submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。

(3)shutdown()和shutdownNow()

       如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;

  如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;

还有很多其他的方法:

  比如:getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法,有兴趣的朋友可以自行查阅API。

 

三.使用示例

复制代码
public class Test {
    public static void main(String[] args) {  
        //核心线程数5,最大线程数10,阻塞队列采用ArrayBlockingQueue,做多排队5个
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(5));
         
        for(int i=0;i<15;i++){
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);
            System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
            executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount());
        }
        executor.shutdown();
    }
}


class MyTask implements Runnable {
   private int taskNum;
    
   public MyTask(int num) {
       this.taskNum = num;
   }
    
   @Override
   public void run() {
       System.out.println("正在执行task "+taskNum);
       try {
           Thread.currentThread().sleep(4000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
       System.out.println("task "+taskNum+"执行完毕");
   }
}
复制代码

运行结果:

 运行结果随机一种可能

通过案例总结:

      当线程数小于核心线程数(5)时会创建新线程,如果要执行的线程大于5,就先把任务放入队列中,如果队列最大容量5已经满了,那会在创建线程,直到最大达到最大线程数10。

注意

      这里如果创建超过15个,比如将for循环中改成执行20个任务,就会抛出任务拒绝异常了。因为你的队列和最大线程数才15,如果有20个任务就会抛异常。

不过在java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池

Executors.newCachedThreadPool();        //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE
Executors.newSingleThreadExecutor();   //创建容量为1的缓冲池
Executors.newFixedThreadPool(int);    //创建固定容量大小的缓冲池

      下面是这三个静态方法的具体实现;

复制代码
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
复制代码

从它们的具体实现来看,它们实际上也是调用了ThreadPoolExecutor,只不过参数都已配置好了。

  newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue;

  newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,也使用的LinkedBlockingQueue;

  newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

  实际中,如果Executors提供的三个静态方法能满足要求,就尽量使用它提供的三个方法,因为自己去手动配置ThreadPoolExecutor的参数有点麻烦,要根据实际任务的类型和数量来进行配置。

 

四、用线程池和不用线程池的区别是什么?

复制代码
public class ThreadCondition implements Runnable {


@Test
public void testThreadPool(){
   Runtime run=Runtime.getRuntime();//当前程序运行对象
    run.gc();//调用垃圾回收机制,减少内存误差
    Long freememroy=run.freeMemory();//获取当前空闲内存
    Long protime=System.currentTimeMillis();
    for(int i=0;i<10000;i++){
      new Thread(new ThreadCondition()).start();
    }
    System.out.println("独立创建"+10000+"个线程需要的内存空间"+(freememroy-run.freeMemory()));
    System.out.println("独立创建"+10000+"个线程需要的系统时间"+(System.currentTimeMillis()-protime));


    System.out.println("---------------------------------");
    Runtime run2=Runtime.getRuntime();//当前程序运行对象
    run2.gc();//调用垃圾回收机制,减少内存误差
    Long freememroy2=run.freeMemory();//获取当前空闲内存
    Long protime2=System.currentTimeMillis();
   ExecutorService service=Executors.newFixedThreadPool(2);
    for(int i=0;i<10000;i++){
     service.execute(new ThreadCondition()) ;
    } 
    System.out.println("线程池创建"+10000+"个线程需要的内存空间"+(freememroy2-run.freeMemory()));
    service.shutdown();
   
    System.out.println("线程池创建"+10000+"个线程需要的系统时间"+(System.currentTimeMillis()-protime2));




}

@Override
public void run() {
//null
}

}
复制代码

运行结果:

这也就说明了,线程池的优势。

ThreadLocal

 什么是ThreadLocal?

        顾名思义它是local variable(线程局部变量)。它的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

从线程的角度看,就好像每一个线程都完全拥有该变量。

注意:ThreadLocal不是用来解决共享对象的多线程访问问题的。

一、多线程共享成员变量

     在多线程环境下,之所以会有并发问题,就是因为不同的线程会同时访问同一个共享变量,同时进行一系列的操作。

1、例如下面的形式

复制代码
//这个意思很简单,创建两个线程,a线程对全局变量+10,b线程对全局变量-10
public class MultiThreadDemo {

    public static class Number {
        private  int value = 0;

        public   void increase() throws InterruptedException {
        //这个变量对于该线程属于局部变量
                value = 10;
            Thread.sleep(10);
            System.out.println("increase value: " + value);
        }

        public    void decrease() throws InterruptedException {
        //同样这个变量对于该线程属于局部变量       
              value = -10;
            Thread.sleep(10);
            System.out.println("decrease value: " + value);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final Number number = new Number();
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    number.increase();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread b = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    number.decrease();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        a.start();
        b.start();
    }
}
复制代码

思考:可能运行的结果:

 运行结果

为了验证我上面的原因分析,我修改下代码:

复制代码
 public    void decrease() throws InterruptedException {
          //我在decrease()新添加这个输出,看下输出结果
            System.out.println("increase value: " + value);
              value = -10;
            Thread.sleep(10);
            System.out.println("decrease value: " + value);
        }
复制代码

再看运行结果:(和上面分析的一样)

思考:如果在 private volatile  int value = 0;在这里加上volatile关键字结果如何?

 volatile结果

所以总的来说:

      a线程和b线程会操作同一个 number 中 value,那么输出的结果是不可预测的,因为当前线程修改变量之后但是还没输出的时候,变量有可能被另外一个线程修改.

当如如果要保证输出我当前线程的值呢?

     其实也很简单:在 increase() 和 decrease() 方法上加上 synchronized 关键字进行同步,这种做法其实是将 value 的 赋值 和 打印 包装成了一个原子操作,也就是说两者要么同时进行,要不都不进行,中间不会有额外的操作。

 

二、多线程不共享全局变量

     上面的例子我们可以看到a线程操作全局变量,b在去去全局成员变量是a已经修改过的。

      如果我们需要 value 只属于 increase 线程或者 decrease 线程,而不是被两个线程共享,那么也不会出现竞争问题。

1、方式一

     很简单,为每一个线程定义一份只属于自己的局部变量。

复制代码
 public void increase() throws InterruptedException {
     //为每一个线程定义一个局部变量,这样当然就是线程私有的
     int value = 10;
     Thread.sleep(10);
     System.out.println("increase value: " + value);
  }
复制代码

    不论 value 值如何改变,都不会影响到其他线程,因为在每次调用 increase 方法时,都会创建一个 value 变量,该变量只对当前调用 increase 方法的线程可见。

2、方式二

    借助于上面这种思想,我们可以创建一个map,将当前线程的 id 作为 key,副本变量作为 value 值,下面是一个实现

复制代码
public class SimpleImpl {

    //这个相当于工具类
    public static class CustomThreadLocal {
        //创建一个Map
        private Map<Long, Integer> cacheMap = new HashMap<>();

        private int defaultValue ;

        public CustomThreadLocal(int value) {
            defaultValue = value;
        }

        //进行封装一层,其实就是通过key得到value
        public Integer get() {
            long id = Thread.currentThread().getId();
            if (cacheMap.containsKey(id)) {
                return cacheMap.get(id);
            }
            return defaultValue;
        }
       //同样存放key,value
        public void set(int value) {
            long id = Thread.currentThread().getId();
            cacheMap.put(id, value);
        }
    }
   //这个类引用工具类,当然也可以在这里写map。
    public static class Number {
        private CustomThreadLocal value = new CustomThreadLocal(0);

        public void increase()  {
            value.set(10);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("increase value: " + value.get());
        }

        public void decrease()  {
            value.set(-10);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("decrease value: " + value.get());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final Number number = new Number();
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                    number.increase();     
            }
        });

        Thread b = new Thread(new Runnable() {
            @Override
            public void run() {              
                    number.decrease();            
            }
        });

        a.start();
        b.start();
    }
}
复制代码

思考,运行结果如何?

//运行结果(其中一种):
increase value: 0
decrease value: -10

      按照常理来讲应该是一个10,一个-10,怎么都想不通会出现0,也没有想明白是哪个地方引起的这个线程不同步,毕竟我这里两个线程各放各的key和value值,而且key也不一样

为什么出现有一个不存在key值,而取出默认值0。

     其实原因就在HashMap是线程不安全的,并发的时候设置值,可能导致冲突,另一个没设置进去。如果这个改成Hashtable,就发现永远输出10和-10两个值。

 

三、ThreadLocal

     其实上面的方式二实现的功能和ThreadLocal像,只不过ThreadLocal肯定更完美。

1、了解ThreadLocal类提供的几个方法

   public T get() { }
   public void set(T value) { }
   public void remove() { }
   protected T initialValue() { }

 

    get()方法:获取ThreadLocal在当前线程中保存的变量副本。

    set()方法:用来设置当前线程中变量的副本。

    remove()方法:用来移除当前线程中变量的副本。

    initialValue()方法:是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法,下面会详细说明。

这里主要看get和set方法源码

复制代码
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
复制代码

通过这个可以总结出:

  (1)get和set底层还是一个ThreadLocalMap实现存取值

  (2)我们在放的时候只放入value值,那么它的key其实就是ThreadLocal类的实例对象(也就是当前线程对象)

2、小案例

复制代码
public class Test {
    //创建两个ThreadLocal对象
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();   
     
    public static void main(String[] args) throws InterruptedException {
        final Test test = new Test();
        ExecutorService  executors= Executors.newFixedThreadPool(2);
        executors.execute(new Runnable() {        
            @Override
            public void run() {
                test.longLocal.set(Thread.currentThread().getId());
                test.stringLocal.set(Thread.currentThread().getName());
                System.out.println(test.longLocal.get());
                System.out.println(test.stringLocal.get());
            }
        });
        executors.execute(new Runnable() {        
            @Override
            public void run() {
                test.longLocal.set(Thread.currentThread().getId());
                test.stringLocal.set(Thread.currentThread().getName());
                System.out.println(test.longLocal.get());
                System.out.println(test.stringLocal.get());
            }
        });
    }
}
复制代码

思考,运行结果如何?

 运行结果

 

四、ThreadLocal的应用场景

      最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。

1、 数据库连接管理

    同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。

复制代码
public class ConnectionManager {    
    
    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {    
        @Override    
        protected Connection initialValue() {    
            Connection conn = null;    
            try {    
                conn = DriverManager.getConnection(    
                        "jdbc:mysql://localhost:3306/test", "username",    
                        "password");    
            } catch (SQLException e) {    
                e.printStackTrace();    
            }    
            return conn;    
        }    
    };    
    
    public static Connection getConnection() {    
        return connectionHolder.get();    
    }    
    
    public static void setConnection(Connection conn) {    
        connectionHolder.set(conn);    
    }    
}    
复制代码

     这样就保证了一个线程对应一个数据库连接,保证了事务。因为事务是依赖一个连接来控制的,如commit,rollback,都是数据库连接的方法。

2、Session管理

复制代码
private static final ThreadLocal threadSession = new ThreadLocal();
 
public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

Condition

 

一、Condition概述

       在线程的同步时可以使一个线程阻塞而等待一个信号,同时放弃锁使其他线程可以能竞争到锁。

       在synchronized中我们可以使用Object的wait()和notify方法实现这种等待和唤醒。

       在Lock可以实现相同的功能就是通过Condition。Condition中的await()和signal()/signalAll()就相当于Object的wait()和notify()/notifyAll()。

除此之外,Condition还是对多线程条件进行更精确的控制。notify()是唤醒一个线程,但它无法确认是唤醒哪一个线程。 但是,通过Condition,就能明确的指定唤醒读线程。

 

二、Condition和Object案例对比

       案例说明:生成者在仓库满时,进入等待状态,同时唤醒消费者线程,消费者在仓库为空时,进入等待。同时唤醒生产者线程。

1、采用await()和signal()方式

(1)测试类

复制代码
public class ConditionLockTest {

    public static void main(String[] args){

        //相当于仓库
        Depot depot=new Depot();
        
      //创建两个生产者一个消费者
        Producer producer1=new Producer(depot);
        Producer producer2=new Producer(depot);
        Consumer consumer1=new Consumer(depot);
        
      //采用线程池方式
        Executor executors=Executors.newFixedThreadPool(5);
        executors.execute(producer1);
        executors.execute(producer2);
        executors.execute(consumer1);
    }
}

//生产者
class Producer implements  Runnable {

    Depot depot;
    public Producer(Depot depot){
        this.depot=depot;
    }
    public void  run(){
     while(true){
        depot.prod();
       }
    }
}

//消费者
class Consumer implements  Runnable{
    
    Depot depot;  
    public Consumer(Depot depot){
        this.depot=depot;
    }
    public void run(){
     while(true){
        depot.consum();
      }
   }
}
复制代码

(2)仓库类

复制代码
public class Depot {
   //初始仓库为0,最大为10,超过10生产者停止生产
    private int size;
    private int maxSize=10;

    private Condition prodCondition;
    private Condition consumCondition;

    private Lock lock;
    public Depot(){

        this.size=0;
        this.lock=new ReentrantLock();
        //可以看出Condition对象依赖于Lock锁
        this.prodCondition=this.lock.newCondition();
        this.consumCondition=this.lock.newCondition();
    }

    /*
     * 生产者生产方法
     */
    public void prod(){

        lock.lock();
        try{
          //如果生产超过max值,则生产者进入等待
            while(size+1>maxSize){
                try {
                    System.out.println(Thread.currentThread().getName()+"生产者进入等待状态");
                    prodCondition.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            size+=1;        
            System.out.println(Thread.currentThread().getName()+" 生产了一个 "+1+" 总共还有 "+size);
            
            //唤醒消费者线程
            consumCondition.signal(); 

        }finally {
            lock.unlock();
        }
    }

    /*
     * 消费者消费方法
     */
    public void consum(){

        lock.lock();
        try{
            //如果当前大小减去要消费的值,如果小于0的话,则进入等待
            while(size-1<0){
                try {
                     System.out.println(Thread.currentThread().getName()+" 消费者进入等待状态");
                    consumCondition.await();
                   

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            size-=1;
            System.out.println(Thread.currentThread().getName()+" 消费者消费了 "+1+" 个,总共还有 "+size);
            //唤醒生产者线程
            prodCondition.signal(); 
        }finally {
            lock.unlock();
        }
    }
}
复制代码

运行结果(截取部分图)

根据结果分析可以得出:
      生产者生产产品,当超过10个,生产者会处于等待状态,直到消费者消费者消费了一个产品,生产者才会重新唤醒。

 2、采用wait()和notifyAll()方法

 (1)仓库类代码(测试类代码不变)

复制代码
public class Depot {
   //初始仓库为0,最大为10,超过10生产者停止生产
    private int size;
    private int maxSize=10;

    public Depot(){
        this.size=0;
    }

    /*
     * 生产者生产方法
     */
    public synchronized void prod(){

        try{
          //如果生产超过max值,则生产者进入等待
            while(size+1>maxSize){
                try {
               //采用wait方法
                      wait();
                   System.out.println(Thread.currentThread().getName()+"生产者进入等待状态");         
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            size+=1;        
            System.out.println(Thread.currentThread().getName()+" 生产了一个 "+1+" 总共还有 "+size);
            
            //唤醒所有线程
             notifyAll();
         
        }finally {     
        }
    }

    /*
     * 消费者消费方法
     */
    public synchronized void consum(){

        try{
            //如果当前大小减去要消费的值,如果小于0的话,则进入等待
            while(size-1<0){
                try {
                     wait();
                     System.out.println(Thread.currentThread().getName()+" 消费者进入等待状态");
                  
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
          
            size-=1;
            System.out.println(Thread.currentThread().getName()+" 消费者消费了 "+1+" 个,总共还有 "+size);
            //唤醒所有线程
            notifyAll();
          
        }finally {
        }
    }
}    
复制代码

运行结果:

对比:

       首先可以看出两个都可以实现生产者消费者的工作,不过可以发现Condition的signal相对于Object的notify最大有点就是它可以唤醒指定的线程,

比如这里可以指定唤醒生产线程或者消费线程,而用notify是不能唤醒指定线程的,你只能通过notifyAll来唤醒所有。

阻塞队列

      再写阻塞列队之前,我写了一篇有关queue集合相关博客,也主要是为这篇做铺垫的。

      网址:【java提高】---queue集合  在这篇博客中我们接触的队列都是非阻塞队列,比如PriorityQueue、LinkedList(LinkedList是双向链表,它实现了Dequeue接口)。

      使用非阻塞队列的时候有一个很大问题就是:它不会对当前线程产生阻塞,那么在面对类似消费者-生产者的模型时,就必须额外地实现同步策略以及线程间唤醒策略,这个实现起来就非常麻烦。

一、认识BlockingQueue

       阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:

      从上图我们可以很清楚看到,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;

常用的队列主要有以下两种:

  先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。

  后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。

      阻塞队列常用于生产者和消费者的场景,生产者线程可以把生产结果存到阻塞队列中,而消费者线程把中间结果取出并在将来修改它们。

队列会自动平衡负载,如果生产者线程集运行的比消费者线程集慢,则消费者线程集在等待结果时就会阻塞;如果生产者线程集运行的快,那么它将等待消费者线程集赶上来。

作为BlockingQueue的使用者,我们再也不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。

看下BlockingQueue的核心方法

1、放入数据

  (1)put(E e):put方法用来向队尾存入元素,如果队列满,则等待。    

  (2)offer(E o, long timeout, TimeUnit unit):offer方法用来向队尾存入元素,如果队列满,则等待一定的时间,当时间期限达到时,如果还没有插入成功,则返回false;否则返回true;

2、获取数据

 (1)take():take方法用来从队首取元素,如果队列为空,则等待;

 (2)drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

 (3)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null;

 (4)poll(long timeout, TimeUnit unit):poll方法用来从队首取元素,如果队列空,则等待一定的时间,当时间期限达到时,如果取到,则返回null;否则返回取得的元素;

 

二、常见BlockingQueue

       在了解了BlockingQueue的基本功能后,让我们来看看BlockingQueue家庭大致有哪些成员?

1、ArrayBlockingQueue

      基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。

2、LinkedBlockingQueue

     基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。

3、PriorityBlockingQueue

       以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即

容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列。

4、DelayQueue

       基于PriorityQueue,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会

被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

 5、小案例

       有关生产者-消费者,上篇博客我写了基于wait和notifyAll实现过,也基于await和signal实现过,网址:https://www.cnblogs.com/qdhxhz/p/9206076.html

这里已经是第三个相关生产消费者的小案例了。

      这里通过LinkedBlockingQueue实现生产消费模式

(1)测试类

复制代码
public class BlockingQueueTest {
      
          public static void main(String[] args) throws InterruptedException {
              // 声明一个容量为10的缓存队列
             BlockingQueue<String> queue = new LinkedBlockingQueue<String>(10);
      
             //new了两个生产者和一个消费者,同时他们共用一个queue缓存队列
             Producer producer1 = new Producer(queue);
             Producer producer2 = new Producer(queue);          
             Consumer consumer = new Consumer(queue);
      
             // 通过线程池启动线程
             ExecutorService service = Executors.newCachedThreadPool();

             service.execute(producer1);
             service.execute(producer2);          
             service.execute(consumer);
      
             // 执行5s
             Thread.sleep(5 * 1000);
             producer1.stop();
             producer2.stop();
           
             Thread.sleep(2000);
             // 退出Executor
             service.shutdown();
         }
     }
复制代码

(2)生产者

复制代码
/**
  * 生产者线程
  */
 public class Producer implements Runnable {
     
     private volatile boolean  isRunning = true;//是否在运行标志
     private BlockingQueue<String> queue;//阻塞队列
     private static AtomicInteger count = new AtomicInteger();//自动更新的值
    
     //构造函数
     public Producer(BlockingQueue<String> queue) {
         this.queue = queue;
     }
  
     public void run() {
         String data = null;
         System.out.println(Thread.currentThread().getName()+" 启动生产者线程!");
         try {
             while (isRunning) {
                 Thread.sleep(1000);
                 
                //以原子方式将count当前值加1
                 data = "" + count.incrementAndGet();
                 System.out.println(Thread.currentThread().getName()+" 将生产数据:" + data + "放入队列中");
                 
               //设定的等待时间为2s,如果超过2s还没加进去返回false
                 if (!queue.offer(data, 2, TimeUnit.SECONDS)) {
                     System.out.println(Thread.currentThread().getName()+" 放入数据失败:" + data);
                 }
             }
         } catch (InterruptedException e) {
             e.printStackTrace();
             Thread.currentThread().interrupt();
         } finally {
             System.out.println(Thread.currentThread().getName()+" 退出生产者线程!");
         }
     }
  
     public void stop() {
         isRunning = false;
     }
 }
复制代码

(3)消费者

复制代码
/**
  * 消费者线程
  */
 public class Consumer implements Runnable {
     
     private BlockingQueue<String> queue;

     //构造函数
     public Consumer(BlockingQueue<String> queue) {
         this.queue = queue;
     }
  
     public void run() {
         System.out.println(Thread.currentThread().getName()+" 启动消费者线程!");

         boolean isRunning = true;
         try {
             while (isRunning) {
                //有数据时直接从队列的队首取走,无数据时阻塞,在2s内有数据,取走,超过2s还没数据,返回失败
                 String data = queue.poll(2, TimeUnit.SECONDS);
                 
                 if (null != data) {
                     System.out.println(Thread.currentThread().getName()+" 正在消费数据:" + data);
                     Thread.sleep(1000);
                 } else {
                     // 超过2s还没数据,认为所有生产线程都已经退出,自动退出消费线程。
                     isRunning = false;
                 }
             }
         } catch (InterruptedException e) {
             e.printStackTrace();
             Thread.currentThread().interrupt();
         } finally {
             System.out.println(Thread.currentThread().getName()+" 退出消费者线程!");
         }
     }     
 }
复制代码

运行结果(其中一种)

 

 三、阻塞队列的实现原理

     主要看两个关键方法的实现:put()和take()

 1、put方法

复制代码
public void put(E e) throws InterruptedException {
    
    //首先可以看出,不能放null,否在报空指针异常
    if (e == null) throw new NullPointerException();
    final E[] items = this.items;
    
    //发现采用的是Lock锁
    final ReentrantLock lock = this.lock;
    
    //如果当前线程不能获取锁则抛出异常
    lock.lockInterruptibly();
    try {
        try {
            while (count == items.length)
    //这里才是关键,我们发现它的堵塞其实是通过await()和signal()来实现的
                notFull.await();
        } catch (InterruptedException ie) {
            notFull.signal(); 
            throw ie;
        }
        insert(e);
    } finally {
        lock.unlock();
    }
}
复制代码

       当被其他线程唤醒时,通过insert(e)方法插入元素,最后解锁。

我们看一下insert方法的实现:

复制代码
private void insert(E x) {
    items[putIndex] = x;
    putIndex = inc(putIndex);
    ++count;
    notEmpty.signal();
}
复制代码

      它是一个private方法,插入成功后,通过notEmpty唤醒正在等待取元素的线程。

 2、take()方法

复制代码
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        try {
            while (count == 0)
                notEmpty.await();
        } catch (InterruptedException ie) {
            notEmpty.signal(); 
            throw ie;
        }
        E x = extract();
        return x;
    } finally {
        lock.unlock();
    }
}
复制代码

        跟put方法实现很类似,只不过put方法等待的是notFull信号,而take方法等待的是notEmpty信号。在take方法中,如果可以取元素,则通过extract方法取得元素,

下面是extract方法的实现:

复制代码
private E extract() {
    final E[] items = this.items;
    E x = items[takeIndex];
    items[takeIndex] = null;
    takeIndex = inc(takeIndex);
    --count;
    notFull.signal();
    return x;
}
复制代码

跟insert方法也很类似。

其实从这里大家应该明白了阻塞队列的实现原理,事实它和我们用Object.wait()、Object.notify()和非阻塞队列实现生产者-消费者的思路类似,只不过它这里通过await()和signal()一起集成到了阻塞队列中实现。

 

posted @ 2022-02-25 16:16  hanease  阅读(52)  评论(0编辑  收藏  举报