赞
踩
接上文:Java并发知识梳理(中):读写锁,Condition与Signal等待机制,LockSupport工具,并发容器,ThreadLocal及内存泄漏,BlockingQueue
在实际使用中,线程是很占用系统资源的,如果对线程管理不善很容易导致系统问题。因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主要有如下好处:
当一个并发任务提交给线程池,线程池分配线程去执行任务的过程如下图所示:
从图可以看出,线程池执行所提交的任务过程主要有这样几个阶段:
创建线程池主要是ThreadPoolExecutor类来完成,ThreadPoolExecutor的有许多重载的构造方法,通过参数最多的构造方法来理解创建线程池有哪些需要配置的参数。ThreadPoolExecutor的构造方法为:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
下面对参数进行说明:
prestartCoreThread()
或者 prestartAllCoreThreads()
,线程池创建的时候所有的核心线程都会被创建并且启动。线程池执行逻辑
通过ThreadPoolExecutor创建线程池后,提交任务后执行过程是怎样的,下面来通过源码来看一看。execute方法源码如下:
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); /* * Proceed in 3 steps: * * 1. If fewer than corePoolSize threads are running, try to * start a new thread with the given command as its first * task. The call to addWorker atomically checks runState and * workerCount, and so prevents false alarms that would add * threads when it shouldn't, by returning false. * * 2. If a task can be successfully queued, then we still need * to double-check whether we should have added a thread * (because existing ones died since last checking) or that * the pool shut down since entry into this method. So we * recheck state and if necessary roll back the enqueuing if * stopped, or start a new thread if there are none. * * 3. If we cannot queue task, then we try to add a new * thread. If it fails, we know we are shut down or saturated * and so reject the task. */ int c = ctl.get(); //如果线程池的线程个数少于corePoolSize则创建新线程执行当前任务 if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } //如果线程个数大于corePoolSize或者创建线程失败,则将任务存放在阻塞队列workQueue中 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); }
ThreadPoolExecutor的execute方法执行逻辑请见注释。下图为ThreadPoolExecutor的execute方法的执行示意图:
execute方法执行逻辑有这样几种情况:
需要注意的是,线程池的设计思想就是使用了核心线程池corePoolSize,阻塞队列workQueue和线程池maximumPoolSize,这样的缓存策略来处理任务,实际上这样的设计思想在需要框架中都会使用。
关闭线程池,可以通过shutdown
和shutdownNow
这两个方法。它们的原理都是遍历线程池中所有的线程,然后依次中断线程。shutdown
和shutdownNow
还是有不一样的地方:
shutdownNow
首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表;shutdown
只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程可以看出shutdown方法会将正在执行的任务继续执行完,而shutdownNow会直接中断正在执行的任务。调用了这两个方法的任意一个,isShutdown
方法都会返回true,当所有的线程都关闭成功,才表示线程池成功关闭,这时调用isTerminated
方法才会返回true。
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()
方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。
参考文献
《Java并发编程的艺术》
ThreadPoolExecutor源码分析,很详细
ScheduledThreadPoolExecutor可以用来在给定延时后执行异步任务或者周期性执行任务,相对于任务调度的Timer来说,其功能更加强大,Timer只能使用一个后台线程执行任务,而ScheduledThreadPoolExecutor则可以通过构造函数来指定后台线程的个数。ScheduledThreadPoolExecutor类的UML图如下:
ThreadPoolExecutor
,也就是说ScheduledThreadPoolExecutor拥有execute()和submit()提交异步任务的基础功能,关于ThreadPoolExecutor可以看这篇文章。但是,ScheduledThreadPoolExecutor类实现了ScheduledExecutorService
,该接口定义了ScheduledThreadPoolExecutor能够延时执行任务和周期执行任务的功能;ScheduledThreadPoolExecutor有如下几个构造方法:
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }; public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory); }; public ScheduledThreadPoolExecutor(int corePoolSize, RejectedExecutionHandler handler) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), handler); }; public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory, handler); }
可以看出由于ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,它的构造方法实际上是调用了ThreadPoolExecutor,对ThreadPoolExecutor的介绍可以可以看这篇文章,理解ThreadPoolExecutor构造方法的几个参数的意义后,理解这就很容易了。可以看出,ScheduledThreadPoolExecutor的核心线程池的线程个数为指定的corePoolSize,当核心线程池的线程个数达到corePoolSize后,就会将任务提交给有界阻塞队列DelayedWorkQueue,对DelayedWorkQueue在下面进行详细介绍,线程池允许最大的线程个数为Integer.MAX_VALUE,也就是说理论上这是一个大小无界的线程池。
ScheduledThreadPoolExecutor实现了ScheduledExecutorService
接口,该接口定义了可延时执行异步任务和可周期执行异步任务的特有功能,相应的方法分别为:
//达到给定的延时时间后,执行任务。这里传入的是实现Runnable接口的任务, //因此通过ScheduledFuture.get()获取结果为null public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit); //达到给定的延时时间后,执行任务。这里传入的是实现Callable接口的任务, //因此,返回的是任务的最终计算结果 public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit); //是以上一个任务开始的时间计时,period时间过去后, //检测上一个任务是否执行完毕,如果上一个任务执行完毕, //则当前任务立即执行,如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后立即执行 public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); //当达到延时时间initialDelay后,任务开始执行。上一个任务执行结束后到下一次 //任务执行,中间延时时间间隔为delay。以这种方式,周期性执行任务。 public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
ScheduledThreadPoolExecutor最大的特色是能够周期性执行异步任务,当调用schedule,scheduleAtFixedRate和scheduleWithFixedDelay方法
时,实际上是将提交的任务转换成的ScheduledFutureTask类,从源码就可以看出。以schedule方法为例:
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
delayedExecute(t);
return t;
}
可以看出,通过decorateTask
会将传入的Runnable转换成ScheduledFutureTask
类。线程池最大作用是将任务和线程进行解耦,线程主要是任务的执行者,而任务也就是现在所说的ScheduledFutureTask。紧接着,会想到任何线程执行任务,总会调用run()
方法。为了保证ScheduledThreadPoolExecutor能够延时执行任务以及能够周期性执行任务,ScheduledFutureTask重写了run方法:
public void run() {
boolean periodic = isPeriodic();
if (!canRunInCurrentRunState(periodic))
cancel(false);
else if (!periodic)
//如果不是周期性执行任务,则直接调用run方法
ScheduledFutureTask.super.run();
//如果是周期性执行任务的话,需要重设下一次执行任务的时间
else if (ScheduledFutureTask.super.runAndReset()) {
setNextRunTime();
reExecutePeriodic(outerTask);
}
}
从源码可以很明显的看出,在重写的run方法中会先if (!periodic)
判断当前任务是否是周期性任务,如果不是的话就直接调用run()方法
;否则的话执行setNextRunTime()
方法重设下一次任务执行的时间,并通过reExecutePeriodic(outerTask)
方法将下一次待执行的任务放置到DelayedWorkQueue
中。
因此,可以得出结论:ScheduledFutureTask
最主要的功能是根据当前任务是否具有周期性,对异步任务进行进一步封装。如果不是周期性任务(调用schedule方法)则直接通过run()
执行,若是周期性任务,则需要在每一次执行完后,重设下一次执行的时间,然后将下一次任务继续放入到阻塞队列中。
在ScheduledThreadPoolExecutor中还有另外的一个重要的类就是DelayedWorkQueue。为了实现其ScheduledThreadPoolExecutor能够延时执行异步任务以及能够周期执行任务,DelayedWorkQueue进行相应的封装。DelayedWorkQueue是一个基于堆的数据结构,类似于DelayQueue和PriorityQueue。在执行定时任务的时候,每个任务的执行时间都不同,所以DelayedWorkQueue的工作就是按照执行时间的升序来排列,执行时间距离当前时间越近的任务在队列的前面。
为什么要使用DelayedWorkQueue呢?
定时任务执行时需要取出最近要执行的任务,所以任务在队列中每次出队时一定要是当前队列中执行时间最靠前的,所以自然要使用优先级队列。
DelayedWorkQueue是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的,由于它是基于堆结构的队列,堆结构在执行插入和删除操作时的最坏时间复杂度是 O(logN)。
DelayedWorkQueue的数据结构
//初始大小
private static final int INITIAL_CAPACITY = 16;
//DelayedWorkQueue是由一个大小为16的数组组成,数组元素为实现RunnableScheduleFuture接口的类
//实际上为ScheduledFutureTask
private RunnableScheduledFuture<?>[] queue =
new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
private final ReentrantLock lock = new ReentrantLock();
private int size = 0;
可以看出DelayedWorkQueue底层是采用数组构成的,关于DelayedWorkQueue可以看这篇博主的文章,很详细。
关于DelayedWorkQueue我们可以得出这样的结论:DelayedWorkQueue是基于堆的数据结构,按照时间顺序将每个任务进行排序,将待执行时间越近的任务放在在队列的队头位置,以便于最先进行执行。
现在我们对ScheduledThreadPoolExecutor的两个内部类ScheduledFutueTask和DelayedWorkQueue进行了了解,实际上这也是线程池工作流程中最重要的两个关键因素:任务以及阻塞队列。现在我们来看下ScheduledThreadPoolExecutor提交一个任务后,整体的执行过程。以ScheduledThreadPoolExecutor的schedule方法为例,具体源码为:
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
//将提交的任务转换成ScheduledFutureTask
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
//延时执行任务ScheduledFutureTask
delayedExecute(t);
return t;
}
方法很容易理解,为了满足ScheduledThreadPoolExecutor能够延时执行任务和能周期执行任务的特性,会先将实现Runnable接口的类转换成ScheduledFutureTask。然后会调用delayedExecute
方法进行执行任务,这个方法也是关键方法,来看下源码:
private void delayedExecute(RunnableScheduledFuture<?> task) { if (isShutdown()) //如果当前线程池已经关闭,则拒绝任务 reject(task); else { //将任务放入阻塞队列中 super.getQueue().add(task); if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task)) task.cancel(false); else //保证至少有一个线程启动,即使corePoolSize=0 ensurePrestart(); } }
delayedExecute
方法的主要逻辑请看注释,可以看出该方法的重要逻辑会是在ensurePrestart()
方法中,它的源码为:
void ensurePrestart() {
int wc = workerCountOf(ctl.get());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
可以看出该方法逻辑很简单,关键在于它所调用的addWorker方法
,该方法主要功能:新建Worker类
,当执行任务时,就会调用被Worker所重写的run方法
,进而会继续执行runWorker
方法。在runWorker
方法中会调用getTask
方法从阻塞队列中不断的去获取任务进行执行,直到从阻塞队列中获取的任务为null的话,线程结束终止。addWorker方法是ThreadPoolExecutor类中的方法,对ThreadPoolExecutor的源码分析可以看这篇文章,很详细。
ScheduledThreadPoolExecutor继承了ThreadPoolExecutor类,因此,整体上功能一致,线程池主要负责创建线程(Worker类),线程从阻塞队列中不断获取新的异步任务,直到阻塞队列中已经没有了异步任务为止。但是相较于ThreadPoolExecutor来说,ScheduledThreadPoolExecutor具有延时执行任务和可周期性执行任务的特性,ScheduledThreadPoolExecutor重新设计了任务类ScheduleFutureTask
,ScheduleFutureTask重写了run
方法使其具有可延时执行和可周期性执行任务的特性。另外,阻塞队列DelayedWorkQueue
是可根据优先级排序的队列,采用了堆的底层数据结构,使得与当前时间相比,待执行时间越靠近的任务放置队头,以便线程能够获取到任务进行执行;
线程池无论是ThreadPoolExecutor还是ScheduledThreadPoolExecutor,在设计时的三个关键要素是:任务,执行者以及任务结果。它们的设计思想也是完全将这三个关键要素进行了解耦。
执行者
任务的执行机制,完全交由Worker类
,也就是进一步了封装了Thread。向线程池提交任务,无论为ThreadPoolExecutor的execute方法和submit方法,还是ScheduledThreadPoolExecutor的schedule方法,都是先将任务移入到阻塞队列中,然后通过addWork方法新建了Work类,并通过runWorker方法启动线程,并不断的从阻塞对列中获取异步任务执行交给Worker执行,直至阻塞队列中无法取到任务为止。
任务
在ThreadPoolExecutor和ScheduledThreadPoolExecutor中任务是指实现了Runnable接口和Callable接口的实现类。ThreadPoolExecutor中会将任务转换成FutureTask
类,而在ScheduledThreadPoolExecutor中为了实现可延时执行任务和周期性执行任务的特性,任务会被转换成ScheduledFutureTask
类,该类继承了FutureTask,并重写了run方法。
任务结果
在ThreadPoolExecutor中提交任务后,获取任务结果可以通过Future接口的类,在ThreadPoolExecutor中实际上为FutureTask类,而在ScheduledThreadPoolExecutor中则是ScheduledFutureTask
类
在Executors框架体系中,FutureTask用来表示可获取结果的异步任务。FutureTask实现了Future接口,FutureTask提供了启动和取消异步任务,查询异步任务是否计算结束以及获取最终的异步任务的结果的一些常用的方法。通过get()
方法来获取异步任务的结果,但是会阻塞当前线程直至异步任务执行结束。一旦任务执行结束,任务不能重新启动或取消,除非调用runAndReset()
方法。在FutureTask的源码中为其定义了这些状态:
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
另外,在《java并发编程的艺术》一书,作者根据FutureTask.run()方法的执行的时机,FutureTask分为了3种状态:
下图总结了FutureTask的状态变化的过程:
由于FutureTask具有这三种状态,因此执行FutureTask的get方法和cancel方法,当前处于不同的状态对应的结果也是大不相同。这里对get方法和cancel方法做个总结:
get方法
当FutureTask处于未启动或已启动状态时,执行FutureTask.get()方法将导致调用线程阻塞。如果FutureTask处于已完成状态,调用FutureTask.get()方法将导致调用线程立即返回结果或者抛出异常
cancel方法
当FutureTask处于未启动状态时,执行FutureTask.cancel()方法将此任务永远不会执行;
当FutureTask处于已启动状态时,执行FutureTask.cancel(true)方法将以中断线程的方式来阻止任务继续进行,如果执行FutureTask.cancel(false)将不会对正在执行任务的线程有任何影响;
当FutureTask处于已完成状态时,执行FutureTask.cancel(…)方法将返回false。
对Future的get()方法和cancel()方法用下图进行总结
FutureTask除了实现Future接口外,还实现了Runnable接口。因此,FutureTask可以交给Executor执行,也可以由调用的线程直接执行(FutureTask.run())。另外,FutureTask的获取也可以通过ExecutorService.submit()方法返回一个FutureTask对象,然后在通过FutureTask.get()或者FutureTask.cancel方法。
**应用场景:**当一个线程需要等待另一个线程把某个任务执行完后它才能继续执行,此时可以使用FutureTask。假设有多个线程执行若干任务,每个任务最多只能被执行一次。当多个线程试图执行同一个任务时,只允许一个线程执行任务,其他线程需要等待这个任务执行完后才能继续执行。
参考文献
《java并发编程的艺术》
在并发编程中很容易出现并发安全的问题,有一个很简单的例子就是多线程更新变量i=1,比如多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchronized进行控制来达到线程安全的目的(关于synchronized可以看这篇文章)。但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。实际上,在J.U.C下的atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在java中则是使用CAS操作具体实现。
能够弄懂atomic包下这些原子操作类的实现原理,就要先明白什么是CAS操作。
什么是CAS?
使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。
CAS的操作过程
CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程
CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。
Synchronized VS CAS
元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。
CAS的问题
ABA问题
因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。
自旋时间过长
使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。
atomic包提高原子更新基本类型的工具类,主要有这些:
这几个类的用法基本一致,这里以AtomicInteger为例总结常用的方法
还有一些方法,可以查看API,不再赘述。为了能够弄懂AtomicInteger的实现原理,以getAndIncrement方法为例,来看下源码:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
可以看出,该方法实际上是调用了unsafe实例的getAndAddInt方法,unsafe实例的获取时通过UnSafe类的静态方法getUnsafe获取:
private static final Unsafe unsafe = Unsafe.getUnsafe();
Unsafe类在sun.misc包下,Unsafer类提供了一些底层操作,atomic包下的原子操作类的也主要是通过Unsafe类提供的compareAndSwapInt,compareAndSwapLong等一系列提供CAS操作的方法来进行实现。下面用一个简单的例子来说明AtomicInteger的用法:
public class AtomicDemo {
private static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void main(String[] args) {
System.out.println(atomicInteger.getAndIncrement());
System.out.println(atomicInteger.get());
}
}
输出结果:
1
2
例子很简单,就是新建了一个atomicInteger对象,而atomicInteger的构造方法也就是传入一个基本类型数据即可,对其进行了封装。对基本变量的操作比如自增,自减,相加,更新等操作,atomicInteger也提供了相应的方法进行这些操作。但是,因为atomicInteger借助了UnSafe提供的CAS操作能够保证数据更新的时候是线程安全的,并且由于CAS是采用乐观锁策略,因此,这种数据更新的方法也具有高效性。
AtomicLong的实现原理和AtomicInteger一致,只不过一个针对的是long变量,一个针对的是int变量。而boolean变量的更新类AtomicBoolean类是怎样实现更新的呢?核心方法是compareAndSet
t方法,其源码如下:
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
可以看出,compareAndSet方法的实际上也是先转换成0,1的整型变量,然后是通过针对int型变量的原子更新方法compareAndSwapInt来实现的。可以看出atomic包中只提供了对boolean,int ,long这三种基本类型的原子更新的方法,参考对boolean更新的方式,原子更新char,doule,float也可以采用类似的思路进行实现。
atomic包下提供能原子更新数组中元素的类有:
这几个类的用法一致,就以AtomicIntegerArray来总结下常用的方法:
可以看出,AtomicIntegerArray与AtomicInteger的方法基本一致,只不过在AtomicIntegerArray的方法中会多一个指定数组索引位i。下面举一个简单的例子:
public class AtomicDemo {
// private static AtomicInteger atomicInteger = new AtomicInteger(1);
private static int[] value = new int[]{1, 2, 3};
private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);
public static void main(String[] args) {
//对数组中索引为1的位置的元素加5
int result = integerArray.getAndAdd(1, 5);
System.out.println(integerArray.get(1));
System.out.println(result);
}
}
输出结果:
7
2
通过getAndAdd方法将位置为1的元素加5,从结果可以看出索引为1的元素变成了7,该方法返回的也是相加之前的数为2。
如果需要原子更新引用类型变量的话,为了保证线程安全,atomic也提供了相关的类:
这几个类的使用方法也是基本一样的,以AtomicReference为例,来说明这些类的基本用法。下面是一个demo
public class AtomicDemo { private static AtomicReference<User> reference = new AtomicReference<>(); public static void main(String[] args) { User user1 = new User("a", 1); reference.set(user1); User user2 = new User("b",2); User user = reference.getAndSet(user2); System.out.println(user); System.out.println(reference.get()); } static class User { private String userName; private int age; public User(String userName, int age) { this.userName = userName; this.age = age; } @Override public String toString() { return "User{" + "userName='" + userName + '\'' + ", age=" + age + '}'; } } } 输出结果: User{userName='a', age=1} User{userName='b', age=2}
首先将对象User1用AtomicReference进行封装,然后调用getAndSet方法,从结果可以看出,该方法会原子更新引用的user对象,变为User{userName='b', age=2}
,返回的是原来的user对象User{userName='a', age=1}
。
如果需要更新对象的某个字段,并在多线程的情况下,能够保证线程安全,atomic同样也提供了相应的原子操作类:
要想使用原子更新字段需要两步操作:
newUpdater
来创建一个更新器,并且需要设置想要更新的类和属性;public volatile
进行修饰;这几个类提供的方法基本一致,以AtomicIntegerFieldUpdater为例来看看具体的使用:
public class AtomicDemo { private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age"); public static void main(String[] args) { User user = new User("a", 1); int oldValue = updater.getAndAdd(user, 5); System.out.println(oldValue); System.out.println(updater.get(user)); } static class User { private String userName; public volatile int age; public User(String userName, int age) { this.userName = userName; this.age = age; } @Override public String toString() { return "User{" + "userName='" + userName + '\'' + ", age=" + age + '}'; } } } 输出结果: 1 6
从示例中可以看出,创建AtomicIntegerFieldUpdater
是通过它提供的静态方法进行创建,getAndAdd
方法会将指定的字段加上输入的值,并且返回相加之前的值。user对象中age字段原值为1,加5之后,可以看出user对象中的age字段的值已经变成了6。
在多线程协作完成业务功能时,有时候需要等待其他多个线程完成任务之后,主线程才能继续往下执行业务功能,在这种的业务场景下,通常可以使用Thread类的join方法,让主线程等待被join的线程执行完之后,主线程才能继续往下执行。当然,使用线程间消息通信机制也可以完成。其实,java并发工具类中为我们提供了类似“倒计时”这样的工具类,可以十分方便的完成所说的这种业务场景。
为了能够理解CountDownLatch,举一个很通俗的例子,运动员进行跑步比赛时,假设有6个运动员参与比赛,裁判员在终点会为这6个运动员分别计时,可以想象没当一个运动员到达终点的时候,对于裁判员来说就少了一个计时任务。直到所有运动员都到达终点了,裁判员的任务也才完成。这6个运动员可以类比成6个线程,当线程调用CountDownLatch.countDown方法时就会对计数器的值减一,直到计数器的值为0的时候,裁判员(调用await方法的线程)才能继续往下执行。
下面来看些CountDownLatch的一些重要方法。
先从CountDownLatch的构造方法看起:
public CountDownLatch(int count)
构造方法会传入一个整型数N,之后调用CountDownLatch的countDown
方法会对N减一,知道N减到0的时候,当前调用await
方法的线程继续执行。
CountDownLatch的方法不是很多,将它们一个个列举出来:
下面用一个具体的例子来说明CountDownLatch的具体用法:
public class CountDownLatchDemo { private static CountDownLatch startSignal = new CountDownLatch(1); //用来表示裁判员需要维护的是6个运动员 private static CountDownLatch endSignal = new CountDownLatch(6); public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(6); for (int i = 0; i < 6; i++) { executorService.execute(() -> { try { System.out.println(Thread.currentThread().getName() + " 运动员等待裁判员响哨!!!"); startSignal.await(); System.out.println(Thread.currentThread().getName() + "正在全力冲刺"); endSignal.countDown(); System.out.println(Thread.currentThread().getName() + " 到达终点"); } catch (InterruptedException e) { e.printStackTrace(); } }); } System.out.println("裁判员发号施令啦!!!"); startSignal.countDown(); endSignal.await(); System.out.println("所有运动员到达终点,比赛结束!"); executorService.shutdown(); } } 输出结果: pool-1-thread-2 运动员等待裁判员响哨!!! pool-1-thread-3 运动员等待裁判员响哨!!! pool-1-thread-1 运动员等待裁判员响哨!!! pool-1-thread-4 运动员等待裁判员响哨!!! pool-1-thread-5 运动员等待裁判员响哨!!! pool-1-thread-6 运动员等待裁判员响哨!!! 裁判员发号施令啦!!! pool-1-thread-2正在全力冲刺 pool-1-thread-2 到达终点 pool-1-thread-3正在全力冲刺 pool-1-thread-3 到达终点 pool-1-thread-1正在全力冲刺 pool-1-thread-1 到达终点 pool-1-thread-4正在全力冲刺 pool-1-thread-4 到达终点 pool-1-thread-5正在全力冲刺 pool-1-thread-5 到达终点 pool-1-thread-6正在全力冲刺 pool-1-thread-6 到达终点 所有运动员到达终点,比赛结束!
该示例代码中设置了两个CountDownLatch,第一个endSignal
用于控制让main线程(裁判员)必须等到其他线程(运动员)让CountDownLatch维护的数值N减到0为止。另一个startSignal
用于让main线程对其他线程进行“发号施令”,startSignal引用的CountDownLatch初始值为1,而其他线程执行的run方法中都会先通过 startSignal.await()
让这些线程都被阻塞,直到main线程通过调用startSignal.countDown();
,将值N减1,CountDownLatch维护的数值N为0后,其他线程才能往下执行,并且,每个线程执行的run方法中都会通过endSignal.countDown();
对endSignal
维护的数值进行减一,由于往线程池提交了6个任务,会被减6次,所以endSignal
维护的值最终会变为0,因此main线程在latch.await();
阻塞结束,才能继续往下执行。
另外,需要注意的是,当调用CountDownLatch的countDown方法时,当前线程是不会被阻塞,会继续往下执行,比如在该例中会继续输出pool-1-thread-4 到达终点
。
CyclicBarrier也是一种多线程并发控制的实用工具,和CountDownLatch一样具有等待计数的功能,但是相比于CountDownLatch功能更加强大。
为了理解CyclicBarrier,这里举一个通俗的例子。开运动会时,会有跑步这一项运动,我们来模拟下运动员入场时的情况,假设有6条跑道,在比赛开始时,就需要6个运动员在比赛开始的时候都站在起点了,裁判员吹哨后才能开始跑步。跑道起点就相当于“barrier”,是临界点,而这6个运动员就类比成线程的话,就是这6个线程都必须到达指定点了,意味着凑齐了一波,然后才能继续执行,否则每个线程都得阻塞等待,直至凑齐一波即可。cyclic是循环的意思,也就是说CyclicBarrier当多个线程凑齐了一波之后,仍然有效,可以继续凑齐下一波。CyclicBarrier的执行示意图如下:
当多个线程都达到了指定点后,才能继续往下继续执行。这就有点像报数的感觉,假设6个线程就相当于6个运动员,到赛道起点时会报数进行统计,如果刚好是6的话,这一波就凑齐了,才能往下执行。**CyclicBarrier在使用一次后,下面依然有效,可以继续当做计数器使用,这是与CountDownLatch的区别之一。**这里的6个线程,也就是计数器的初始值6,是通过CyclicBarrier的构造方法传入的。
下面来看下CyclicBarrier的主要方法:
//等到所有的线程都到达指定的临界点 await() throws InterruptedException, BrokenBarrierException //与上面的await方法功能基本一致,只不过这里有超时限制,阻塞等待直至到达超时时间为止 await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException //获取当前有多少个线程阻塞等待在临界点上 int getNumberWaiting() //用于查询阻塞等待的线程是否被中断 boolean isBroken() //将屏障重置为初始状态。如果当前有线程正在临界点等待的话,将抛出BrokenBarrierException。 void reset()
另外需要注意的是,CyclicBarrier提供了这样的构造方法:
public CyclicBarrier(int parties, Runnable barrierAction)
可以用来,当指定的线程都到达了指定的临界点的时,接下来执行的操作可以由barrierAction传入即可。
一个例子
下面用一个简单的例子,来看下CyclicBarrier的用法,我们来模拟下上面的运动员的例子。
public class CyclicBarrierDemo { //指定必须有6个运动员到达才行 private static CyclicBarrier barrier = new CyclicBarrier(6, () -> { System.out.println("所有运动员入场,裁判员一声令下!!!!!"); }); public static void main(String[] args) { System.out.println("运动员准备进场,全场欢呼............"); ExecutorService service = Executors.newFixedThreadPool(6); for (int i = 0; i < 6; i++) { service.execute(() -> { try { System.out.println(Thread.currentThread().getName() + " 运动员,进场"); barrier.await(); System.out.println(Thread.currentThread().getName() + " 运动员出发"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }); } } } 输出结果: 运动员准备进场,全场欢呼............ pool-1-thread-2 运动员,进场 pool-1-thread-1 运动员,进场 pool-1-thread-3 运动员,进场 pool-1-thread-4 运动员,进场 pool-1-thread-5 运动员,进场 pool-1-thread-6 运动员,进场 所有运动员入场,裁判员一声令下!!!!! pool-1-thread-6 运动员出发 pool-1-thread-1 运动员出发 pool-1-thread-5 运动员出发 pool-1-thread-4 运动员出发 pool-1-thread-3 运动员出发 pool-1-thread-2 运动员出发
从输出结果可以看出,当6个运动员(线程)都到达了指定的临界点(barrier)时候,才能继续往下执行,否则,则会阻塞等待在调用await()
处
CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的:
Semaphore可以理解为信号量,用于控制资源能够被并发访问的线程数量,以保证多个线程能够合理的使用特定资源。Semaphore就相当于一个许可证,线程需要先通过acquire方法获取该许可证,该线程才能继续往下执行,否则只能在该方法出阻塞等待。当执行完业务功能后,需要通过release()
方法将许可证归还,以便其他线程能够获得许可证继续执行。
Semaphore可以用于做流量控制,特别是公共资源有限的应用场景,比如数据库连接。假如有多个线程读取数据后,需要将数据保存在数据库中,而可用的最大数据库连接只有10个,这时候就需要使用Semaphore来控制能够并发访问到数据库连接资源的线程个数最多只有10个。在限制资源使用的应用场景下,Semaphore是特别合适的。
下面来看下Semaphore的主要方法:
//获取许可,如果无法获取到,则阻塞等待直至能够获取为止 void acquire() throws InterruptedException //同acquire方法功能基本一样,只不过该方法可以一次获取多个许可 void acquire(int permits) throws InterruptedException //释放许可 void release() //释放指定个数的许可 void release(int permits) //尝试获取许可,如果能够获取成功则立即返回true,否则,则返回false boolean tryAcquire() //与tryAcquire方法一致,只不过这里可以指定获取多个许可 boolean tryAcquire(int permits) //尝试获取许可,如果能够立即获取到或者在指定时间内能够获取到,则返回true,否则返回false boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException //与上一个方法一致,只不过这里能够获取多个许可 boolean tryAcquire(int permits, long timeout, TimeUnit unit) //返回当前可用的许可证个数 int availablePermits() //返回正在等待获取许可证的线程数 int getQueueLength() //是否有线程正在等待获取许可证 boolean hasQueuedThreads() //获取所有正在等待许可的线程集合 Collection<Thread> getQueuedThreads()
另外,在Semaphore的构造方法中还支持指定是够具有公平性,默认的是非公平性,这样也是为了保证吞吐量。
一个例子
下面用一个简单的例子来说明Semaphore的具体使用。我们来模拟这样一样场景。有一天,班主任需要班上10个同学到讲台上来填写一个表格,但是老师只准备了5支笔,因此,只能保证同时只有5个同学能够拿到笔并填写表格,没有获取到笔的同学只能够等前面的同学用完之后,才能拿到笔去填写表格。该示例代码如下:
public class SemaphoreDemo { //表示老师只有10支笔 private static Semaphore semaphore = new Semaphore(5); public static void main(String[] args) { //表示50个学生 ExecutorService service = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { service.execute(() -> { try { System.out.println(Thread.currentThread().getName() + " 同学准备获取笔......"); semaphore.acquire(); System.out.println(Thread.currentThread().getName() + " 同学获取到笔"); System.out.println(Thread.currentThread().getName() + " 填写表格ing....."); TimeUnit.SECONDS.sleep(3); semaphore.release(); System.out.println(Thread.currentThread().getName() + " 填写完表格,归还了笔!!!!!!"); } catch (InterruptedException e) { e.printStackTrace(); } }); } service.shutdown(); } } 输出结果: pool-1-thread-1 同学准备获取笔...... pool-1-thread-1 同学获取到笔 pool-1-thread-1 填写表格ing..... pool-1-thread-2 同学准备获取笔...... pool-1-thread-2 同学获取到笔 pool-1-thread-2 填写表格ing..... pool-1-thread-3 同学准备获取笔...... pool-1-thread-4 同学准备获取笔...... pool-1-thread-3 同学获取到笔 pool-1-thread-4 同学获取到笔 pool-1-thread-4 填写表格ing..... pool-1-thread-3 填写表格ing..... pool-1-thread-5 同学准备获取笔...... pool-1-thread-5 同学获取到笔 pool-1-thread-5 填写表格ing..... pool-1-thread-6 同学准备获取笔...... pool-1-thread-7 同学准备获取笔...... pool-1-thread-8 同学准备获取笔...... pool-1-thread-9 同学准备获取笔...... pool-1-thread-10 同学准备获取笔...... pool-1-thread-4 填写完表格,归还了笔!!!!!! pool-1-thread-9 同学获取到笔 pool-1-thread-9 填写表格ing..... pool-1-thread-5 填写完表格,归还了笔!!!!!! pool-1-thread-7 同学获取到笔 pool-1-thread-7 填写表格ing..... pool-1-thread-8 同学获取到笔 pool-1-thread-8 填写表格ing..... pool-1-thread-1 填写完表格,归还了笔!!!!!! pool-1-thread-6 同学获取到笔 pool-1-thread-6 填写表格ing..... pool-1-thread-3 填写完表格,归还了笔!!!!!! pool-1-thread-2 填写完表格,归还了笔!!!!!! pool-1-thread-10 同学获取到笔 pool-1-thread-10 填写表格ing..... pool-1-thread-7 填写完表格,归还了笔!!!!!! pool-1-thread-9 填写完表格,归还了笔!!!!!! pool-1-thread-8 填写完表格,归还了笔!!!!!! pool-1-thread-6 填写完表格,归还了笔!!!!!! pool-1-thread-10 填写完表格,归还了笔!!!!!!
根据输出结果进行分析,Semaphore允许的最大许可数为5,也就是允许的最大并发执行的线程个数为5,可以看出,前5个线程(前5个学生)先获取到笔,然后填写表格,而6-10这5个线程,由于获取不到许可,只能阻塞等待。当线程pool-1-thread-4
释放了许可之后,pool-1-thread-9
就可以获取到许可,继续往下执行。对其他线程的执行过程,也是同样的道理。从这个例子就可以看出,Semaphore用来做特殊资源的并发访问控制是相当合适的,如果有业务场景需要进行流量控制,可以优先考虑Semaphore。
Exchanger是一个用于线程间协作的工具类,用于两个线程间能够交换。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。具体交换数据是通过exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据。
Exchanger除了一个无参的构造方法外,主要方法也很简单:
//当一个线程执行该方法的时候,会等待另一个线程也执行该方法,因此两个线程就都达到了同步点
//将数据交换给另一个线程,同时返回获取的数据
V exchange(V x) throws InterruptedException
//同上一个方法功能基本一样,只不过这个方法同步等待的时候,增加了超时时间
V exchange(V x, long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException
一个例子
Exchanger理解起来很容易,这里用一个简单的例子来看下它的具体使用。我们来模拟这样一个情景,在青春洋溢的中学时代,下课期间,男生经常会给走廊里为自己喜欢的女孩子送情书,相信大家都做过这样的事情吧 :)。男孩会先到女孩教室门口,然后等女孩出来,教室那里就是一个同步点,然后彼此交换信物,也就是彼此交换了数据。现在,就来模拟这个情景。
public class ExchangerDemo { private static Exchanger<String> exchanger = new Exchanger(); public static void main(String[] args) { //代表男生和女生 ExecutorService service = Executors.newFixedThreadPool(2); service.execute(() -> { try { //男生对女生说的话 String girl = exchanger.exchange("我其实暗恋你很久了......"); System.out.println("女孩儿说:" + girl); } catch (InterruptedException e) { e.printStackTrace(); } }); service.execute(() -> { try { System.out.println("女生慢慢的从教室你走出来......"); TimeUnit.SECONDS.sleep(3); //男生对女生说的话 String boy = exchanger.exchange("我也很喜欢你......"); System.out.println("男孩儿说:" + boy); } catch (InterruptedException e) { e.printStackTrace(); } }); } } 输出结果: 女生慢慢的从教室你走出来...... 男孩儿说:我其实暗恋你很久了...... 女孩儿说:我也很喜欢你......
这个例子很简单,也很能说明Exchanger的基本使用。当两个线程都到达调用exchange方法的同步点的时候,两个线程就能交换彼此的数据。
生产者-消费者模式是一个十分经典的多线程并发协作的模式,弄懂生产者-消费者问题能够让我们对并发编程的理解加深。所谓生产者-消费者问题,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据,为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:
在实现生产者消费者问题时,可以采用三种方式:
1.使用Object的wait/notify的消息通知机制;
2.使用Lock的Condition的await/signal的消息通知机制;
3.使用BlockingQueue实现。本文主要将这三种实现方式进行总结归纳。
Java 中,可以通过配合调用 Object 对象的 wait() 方法和 notify()方法或 notifyAll() 方法来实现线程间的通信。在线程中调用 wait() 方法,将阻塞当前线程,直至等到其他线程调用了调用 notify() 方法或 notifyAll() 方法进行通知之后,当前线程才能从wait()方法出返回,继续执行下面的操作。
wait
该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用 wait()之前,线程必须要获得该对象的对象监视器锁,即只能在同步方法或同步块中调用 wait()方法。调用wait()方法之后,当前线程会释放锁。如果调用wait()方法时,线程并未获取到锁的话,则会抛出IllegalMonitorStateException异常,这是以个RuntimeException。如果再次获取到锁的话,当前线程才能从wait()方法处成功返回。
notify
该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,如果调用 notify()时没有持有适当的锁,也会抛出 IllegalMonitorStateException。
该方法任意从WAITTING状态的线程中挑选一个进行通知,使得调用wait()方法的线程从等待队列移入到同步队列中,等待有机会再一次获取到锁,从而使得调用wait()方法的线程能够从wait()方法处退出。调用notify后,当前线程不会马上释放该对象锁,要等到程序退出同步块后,当前线程才会释放锁。
notifyAll
该方法与 notify ()方法的工作方式相同,重要的一点差异是:
notifyAll 使所有原来在该对象上 wait 的线程统统退出WAITTING状态,使得他们全部从等待队列中移入到同步队列中去,等待下一次能够有机会获取到对象监视器锁。
1.notify早期通知
notify 通知的遗漏很容易理解,即 threadA 还没开始 wait 的时候,threadB 已经 notify 了,这样,threadB 通知是没有任何响应的,当 threadB 退出 synchronized 代码块后,threadA 再开始 wait,便会一直阻塞等待,直到被别的线程打断。比如在下面的示例代码中,就模拟出notify早期通知带来的问题:
public class EarlyNotify { private static String lockObject = ""; public static void main(String[] args) { WaitThread waitThread = new WaitThread(lockObject); NotifyThread notifyThread = new NotifyThread(lockObject); notifyThread.start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } waitThread.start(); } static class WaitThread extends Thread { private String lock; public WaitThread(String lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { try { System.out.println(Thread.currentThread().getName() + " 进去代码块"); System.out.println(Thread.currentThread().getName() + " 开始wait"); lock.wait(); System.out.println(Thread.currentThread().getName() + " 结束wait"); } catch (InterruptedException e) { e.printStackTrace(); } } } } static class NotifyThread extends Thread { private String lock; public NotifyThread(String lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " 进去代码块"); System.out.println(Thread.currentThread().getName() + " 开始notify"); lock.notify(); System.out.println(Thread.currentThread().getName() + " 结束开始notify"); } } } }
示例中开启了**两个线程,一个是WaitThread,另一个是NotifyThread。NotifyThread会先启动,先调用notify方法。然后WaitThread线程才启动,调用wait方法,但是由于通知过了,wait方法就无法再获取到相应的通知,因此WaitThread会一直在wait方法出阻塞,这种现象就是通知过早的现象。**针对这种现象,解决方法,一般是添加一个状态标志,让waitThread调用wait方法前先判断状态是否已经改变了没,如果通知早已发出的话,WaitThread就不再去wait。对上面的代码进行更正:
public class EarlyNotify { private static String lockObject = ""; private static boolean isWait = true; public static void main(String[] args) { WaitThread waitThread = new WaitThread(lockObject); NotifyThread notifyThread = new NotifyThread(lockObject); notifyThread.start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } waitThread.start(); } static class WaitThread extends Thread { private String lock; public WaitThread(String lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { try { while (isWait) { System.out.println(Thread.currentThread().getName() + " 进去代码块"); System.out.println(Thread.currentThread().getName() + " 开始wait"); lock.wait(); System.out.println(Thread.currentThread().getName() + " 结束wait"); } } catch (InterruptedException e) { e.printStackTrace(); } } } } static class NotifyThread extends Thread { private String lock; public NotifyThread(String lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " 进去代码块"); System.out.println(Thread.currentThread().getName() + " 开始notify"); lock.notifyAll(); isWait = false; System.out.println(Thread.currentThread().getName() + " 结束开始notify"); } } } }
这段代码只是增加了一个isWait
状态变量,NotifyThread调用notify方法后会对状态变量进行更新,在WaitThread中调用wait方法之前会先对状态变量进行判断,在该示例中,调用notify后将状态变量isWait
改变为false,因此,在WaitThread中while对isWait判断后就不会执行wait方法,从而避免了Notify过早通知造成遗漏的情况。
总结:在使用线程的等待/通知机制时,一般都要配合一个 boolean 变量值(或者其他能够判断真假的条件),在 notify 之前改变该 boolean 变量的值,让 wait 返回后能够退出 while 循环(一般都要在 wait 方法外围加一层 while 循环,以防止早期通知),或在通知被遗漏后,不会被阻塞在 wait 方法处。这样便保证了程序的正确性。
2.等待wait的条件发生变化
如果线程在等待时接受到了通知,但是之后等待的条件发生了变化,并没有再次对等待条件进行判断,也会导致程序出现错误。
下面用一个例子来说明这种情况
public class ConditionChange { private static List<String> lockObject = new ArrayList(); public static void main(String[] args) { Consumer consumer1 = new Consumer(lockObject); Consumer consumer2 = new Consumer(lockObject); Productor productor = new Productor(lockObject); consumer1.start(); consumer2.start(); productor.start(); } static class Consumer extends Thread { private List<String> lock; public Consumer(List lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { try { //这里使用if的话,就会存在wait条件变化造成程序错误的问题 if (lock.isEmpty()) { System.out.println(Thread.currentThread().getName() + " list为空"); System.out.println(Thread.currentThread().getName() + " 调用wait方法"); lock.wait(); System.out.println(Thread.currentThread().getName() + " wait方法结束"); } String element = lock.remove(0); System.out.println(Thread.currentThread().getName() + " 取出第一个元素为:" + element); } catch (InterruptedException e) { e.printStackTrace(); } } } } static class Productor extends Thread { private List<String> lock; public Productor(List lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " 开始添加元素"); lock.add(Thread.currentThread().getName()); lock.notifyAll(); } } } } 会报异常: Exception in thread "Thread-1" Thread-0 list为空 Thread-0 调用wait方法 Thread-1 list为空 Thread-1 调用wait方法 Thread-2 开始添加元素 Thread-1 wait方法结束 java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
异常原因分析:在这个例子中一共开启了3个线程,Consumer1,Consumer2以及Productor。首先Consumer1调用了wait方法后,线程处于了WAITTING状态,并且将对象锁释放出来。因此,Consumer2能够获取对象锁,从而进入到同步代块中,当执行到wait方法时,同样的也会释放对象锁。因此,productor能够获取到对象锁,进入到同步代码块中,向list中插入数据后,通过notifyAll方法通知处于WAITING状态的Consumer1和Consumer2线程。consumer1得到对象锁后,从wait方法出退出,删除了一个元素让List为空,方法执行结束,退出同步块,释放掉对象锁。这个时候Consumer2获取到对象锁后,从wait方法退出,继续往下执行,这个时候Consumer2再执行lock.remove(0);
就会出错,因为List由于Consumer1删除一个元素之后已经为空了。
**解决方案:**通过上面的分析,可以看出Consumer2报异常是因为线程从wait方法退出之后没有再次对wait条件进行判断,因此,此时的wait条件已经发生了变化。解决办法就是,在wait退出之后再对条件进行判断即可。
public class ConditionChange { private static List<String> lockObject = new ArrayList(); public static void main(String[] args) { Consumer consumer1 = new Consumer(lockObject); Consumer consumer2 = new Consumer(lockObject); Productor productor = new Productor(lockObject); consumer1.start(); consumer2.start(); productor.start(); } static class Consumer extends Thread { private List<String> lock; public Consumer(List lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { try { //这里使用if的话,就会存在wait条件变化造成程序错误的问题 while (lock.isEmpty()) { System.out.println(Thread.currentThread().getName() + " list为空"); System.out.println(Thread.currentThread().getName() + " 调用wait方法"); lock.wait(); System.out.println(Thread.currentThread().getName() + " wait方法结束"); } String element = lock.remove(0); System.out.println(Thread.currentThread().getName() + " 取出第一个元素为:" + element); } catch (InterruptedException e) { e.printStackTrace(); } } } } static class Productor extends Thread { private List<String> lock; public Productor(List lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " 开始添加元素"); lock.add(Thread.currentThread().getName()); lock.notifyAll(); } } } }
上面的代码与之前的代码仅仅只是将 wait 外围的 if 语句改为 while 循环即可,这样当 list 为空时,线程便会继续等待,而不会继续去执行删除 list 中元素的代码。
总结:在使用线程的等待/通知机制时,一般都要在 while 循环中调用 wait()方法,因此xuy配合使用一个 boolean 变量(或其他能判断真假的条件,如本文中的 list.isEmpty()),满足 while 循环的条件时,进入 while 循环,执行 wait()方法,不满足 while 循环的条件时,跳出循环,执行后面的代码。
3. “假死”状态
现象:如果是多消费者和多生产者情况,如果使用notify方法可能会出现“假死”的情况,即唤醒的是同类线程。
原因分析:假设当前多个生产者线程会调用wait方法阻塞等待,当其中的生产者线程获取到对象锁之后使用notify通知处于WAITTING状态的线程,如果唤醒的仍然是生产者线程,就会造成所有的生产者线程都处于等待状态。
解决办法:将notify方法替换成notifyAll方法,如果使用的是lock的话,就将signal方法替换成signalAll方法。
总结
在Object提供的消息通知机制应该遵循如下这些条件:
基本的使用范式如下:
// The standard idiom for calling the wait method in Java
synchronized (sharedObject) {
while (condition) {
sharedObject.wait();
// (Releases lock, and reacquires on wakeup)
}
// do action based upon condition e.g. take or put into queue
}
利用wait/notifyAll实现生产者和消费者代码如下:
public class ProductorConsumer { public static void main(String[] args) { LinkedList linkedList = new LinkedList(); ExecutorService service = Executors.newFixedThreadPool(15); for (int i = 0; i < 5; i++) { service.submit(new Productor(linkedList, 8)); } for (int i = 0; i < 10; i++) { service.submit(new Consumer(linkedList)); } } static class Productor implements Runnable { private List<Integer> list; private int maxLength; public Productor(List list, int maxLength) { this.list = list; this.maxLength = maxLength; } @Override public void run() { while (true) { synchronized (list) { try { while (list.size() == maxLength) { System.out.println("生产者" + Thread.currentThread().getName() + " list以达到最大容量,进行wait"); list.wait(); System.out.println("生产者" + Thread.currentThread().getName() + " 退出wait"); } Random random = new Random(); int i = random.nextInt(); System.out.println("生产者" + Thread.currentThread().getName() + " 生产数据" + i); list.add(i); list.notifyAll(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } static class Consumer implements Runnable { private List<Integer> list; public Consumer(List list) { this.list = list; } @Override public void run() { while (true) { synchronized (list) { try { while (list.isEmpty()) { System.out.println("消费者" + Thread.currentThread().getName() + " list为空,进行wait"); list.wait(); System.out.println("消费者" + Thread.currentThread().getName() + " 退出wait"); } Integer element = list.remove(0); System.out.println("消费者" + Thread.currentThread().getName() + " 消费数据:" + element); list.notifyAll(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } } 输出结果: 生产者pool-1-thread-1 生产数据-232820990 生产者pool-1-thread-1 生产数据1432164130 生产者pool-1-thread-1 生产数据1057090222 生产者pool-1-thread-1 生产数据1201395916 生产者pool-1-thread-1 生产数据482766516 生产者pool-1-thread-1 list以达到最大容量,进行wait 消费者pool-1-thread-15 退出wait 消费者pool-1-thread-15 消费数据:1237535349 消费者pool-1-thread-15 消费数据:-1617438932 消费者pool-1-thread-15 消费数据:-535396055 消费者pool-1-thread-15 消费数据:-232820990 消费者pool-1-thread-15 消费数据:1432164130 消费者pool-1-thread-15 消费数据:1057090222 消费者pool-1-thread-15 消费数据:1201395916 消费者pool-1-thread-15 消费数据:482766516 消费者pool-1-thread-15 list为空,进行wait 生产者pool-1-thread-5 退出wait 生产者pool-1-thread-5 生产数据1442969724 生产者pool-1-thread-5 生产数据1177554422 生产者pool-1-thread-5 生产数据-133137235 生产者pool-1-thread-5 生产数据324882560 生产者pool-1-thread-5 生产数据2065211573 生产者pool-1-thread-5 生产数据253569900 生产者pool-1-thread-5 生产数据571277922 生产者pool-1-thread-5 生产数据1622323863 生产者pool-1-thread-5 list以达到最大容量,进行wait 消费者pool-1-thread-10 退出wait
参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法:
针对wait方法
void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常;
long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时;
boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位
boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间
针对notify方法
void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程
也就是说wait—>await,notify---->Signal。另外,关于lock中condition消息通知的原理解析可以看这篇文章。
如果采用lock中Conditon的消息通知原理来实现生产者-消费者问题,原理同使用wait/notifyAll一样。直接上代码:
public class ProductorConsumer { private static ReentrantLock lock = new ReentrantLock(); private static Condition full = lock.newCondition(); private static Condition empty = lock.newCondition(); public static void main(String[] args) { LinkedList linkedList = new LinkedList(); ExecutorService service = Executors.newFixedThreadPool(15); for (int i = 0; i < 5; i++) { service.submit(new Productor(linkedList, 8, lock)); } for (int i = 0; i < 10; i++) { service.submit(new Consumer(linkedList, lock)); } } static class Productor implements Runnable { private List<Integer> list; private int maxLength; private Lock lock; public Productor(List list, int maxLength, Lock lock) { this.list = list; this.maxLength = maxLength; this.lock = lock; } @Override public void run() { while (true) { lock.lock(); try { while (list.size() == maxLength) { System.out.println("生产者" + Thread.currentThread().getName() + " list以达到最大容量,进行wait"); full.await(); System.out.println("生产者" + Thread.currentThread().getName() + " 退出wait"); } Random random = new Random(); int i = random.nextInt(); System.out.println("生产者" + Thread.currentThread().getName() + " 生产数据" + i); list.add(i); empty.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } } static class Consumer implements Runnable { private List<Integer> list; private Lock lock; public Consumer(List list, Lock lock) { this.list = list; this.lock = lock; } @Override public void run() { while (true) { lock.lock(); try { while (list.isEmpty()) { System.out.println("消费者" + Thread.currentThread().getName() + " list为空,进行wait"); empty.await(); System.out.println("消费者" + Thread.currentThread().getName() + " 退出wait"); } Integer element = list.remove(0); System.out.println("消费者" + Thread.currentThread().getName() + " 消费数据:" + element); full.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } } } 输出结果: 消费者pool-1-thread-9 消费数据:1146627506 消费者pool-1-thread-9 消费数据:1508001019 消费者pool-1-thread-9 消费数据:-600080565 消费者pool-1-thread-9 消费数据:-1000305429 消费者pool-1-thread-9 消费数据:-1270658620 消费者pool-1-thread-9 消费数据:1961046169 消费者pool-1-thread-9 消费数据:-307680655 消费者pool-1-thread-9 list为空,进行wait 消费者pool-1-thread-13 退出wait 消费者pool-1-thread-13 list为空,进行wait 消费者pool-1-thread-10 退出wait 生产者pool-1-thread-5 退出wait 生产者pool-1-thread-5 生产数据-892558288 生产者pool-1-thread-5 生产数据-1917220008 生产者pool-1-thread-5 生产数据2146351766 生产者pool-1-thread-5 生产数据452445380 生产者pool-1-thread-5 生产数据1695168334 生产者pool-1-thread-5 生产数据1979746693 生产者pool-1-thread-5 生产数据-1905436249 生产者pool-1-thread-5 生产数据-101410137 生产者pool-1-thread-5 list以达到最大容量,进行wait 生产者pool-1-thread-1 退出wait 生产者pool-1-thread-1 list以达到最大容量,进行wait 生产者pool-1-thread-4 退出wait 生产者pool-1-thread-4 list以达到最大容量,进行wait 生产者pool-1-thread-2 退出wait 生产者pool-1-thread-2 list以达到最大容量,进行wait 生产者pool-1-thread-3 退出wait 生产者pool-1-thread-3 list以达到最大容量,进行wait 消费者pool-1-thread-9 退出wait 消费者pool-1-thread-9 消费数据:-892558288
由于BlockingQueue内部实现就附加了两个阻塞操作。即当队列已满时,阻塞向队列中插入数据的线程,直至队列中未满;当队列为空时,阻塞从队列中获取数据的线程,直至队列非空时为止。关于BlockingQueue更多细节可以看这篇文章。可以利用BlockingQueue实现生产者-消费者为题,阻塞队列完全可以充当共享数据区域,就可以很好的完成生产者和消费者线程之间的协作。
public class ProductorConsumer { private static LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>(); public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(15); for (int i = 0; i < 5; i++) { service.submit(new Productor(queue)); } for (int i = 0; i < 10; i++) { service.submit(new Consumer(queue)); } } static class Productor implements Runnable { private BlockingQueue queue; public Productor(BlockingQueue queue) { this.queue = queue; } @Override public void run() { try { while (true) { Random random = new Random(); int i = random.nextInt(); System.out.println("生产者" + Thread.currentThread().getName() + "生产数据" + i); queue.put(i); } } catch (InterruptedException e) { e.printStackTrace(); } } } static class Consumer implements Runnable { private BlockingQueue queue; public Consumer(BlockingQueue queue) { this.queue = queue; } @Override public void run() { try { while (true) { Integer element = (Integer) queue.take(); System.out.println("消费者" + Thread.currentThread().getName() + "正在消费数据" + element); } } catch (InterruptedException e) { e.printStackTrace(); } } } } 输出结果: 消费者pool-1-thread-7正在消费数据1520577501 生产者pool-1-thread-4生产数据-127809610 消费者pool-1-thread-8正在消费数据504316513 生产者pool-1-thread-2生产数据1994678907 消费者pool-1-thread-11正在消费数据1967302829 生产者pool-1-thread-1生产数据369331507 消费者pool-1-thread-9正在消费数据1994678907 生产者pool-1-thread-2生产数据-919544017 消费者pool-1-thread-12正在消费数据-127809610 生产者pool-1-thread-4生产数据1475197572 消费者pool-1-thread-14正在消费数据-893487914 生产者pool-1-thread-3生产数据906921688 消费者pool-1-thread-6正在消费数据-1292015016 生产者pool-1-thread-5生产数据-652105379 生产者pool-1-thread-5生产数据-1622505717 生产者pool-1-thread-3生产数据-1350268764 消费者pool-1-thread-7正在消费数据906921688 生产者pool-1-thread-4生产数据2091628867 消费者pool-1-thread-13正在消费数据1475197572 消费者pool-1-thread-15正在消费数据-919544017 生产者pool-1-thread-2生产数据564860122 生产者pool-1-thread-2生产数据822954707 消费者pool-1-thread-14正在消费数据564860122 消费者pool-1-thread-10正在消费数据369331507 生产者pool-1-thread-1生产数据-245820912 消费者pool-1-thread-6正在消费数据822954707 生产者pool-1-thread-2生产数据1724595968 生产者pool-1-thread-2生产数据-1151855115 消费者pool-1-thread-12正在消费数据2091628867 生产者pool-1-thread-4生产数据-1774364499 生产者pool-1-thread-4生产数据2006106757 消费者pool-1-thread-14正在消费数据-1774364499 生产者pool-1-thread-3生产数据-1070853639 消费者pool-1-thread-9正在消费数据-1350268764 消费者pool-1-thread-11正在消费数据-1622505717 生产者pool-1-thread-5生产数据355412953
可以看出,使用BlockingQueue来实现生产者-消费者很简洁,这正是利用了BlockingQueue插入和获取数据附加阻塞操作的特性。
关于生产者-消费者实现的三中方式,到这里就全部总结出来,如果觉得不错的话,请点赞,也算是给我的鼓励,在此表示感谢!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。