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关键字结果如何?
所以总的来说:
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()一起集成到了阻塞队列中实现。