Java并发编程艺术学习笔记(六)
Java中的并发工具类
CountDownLatch、CyclicBarrier、Semaphore工具提供了一种并发流程控制的手段,Exchanger工具类提供了在线程间交换数据的一种方式。
一.等待多线程完成的CountDownLatch
CountDownLatch允许一个或者多个线程等待其他线程完成操作。
一般采用的方法都是采用join()方法,join用于当前执行线程等待join线程执行结束,实现原理就是不断检查join线程是否存活,如果join线程存活就让当前线程永远等待。
而JDK1.5后的并发包提供的CountDownLatch也可以实现join功能,并且比join的功能更多。
CountDownLatch的构造函数接受一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。
每次调用countDown方法,N就会减少1,await方法则会阻塞当前线程,直到N变为0。
二.同步屏障CyclicBarrier
可循环的屏障。让一组线程到达一个屏障(可以称为同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才能继续运行。
Ⅰ.CyclicBarrier简介
默认的构造方法是CyclicBarrier(int parties),参数就是屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
其中还有个更高级的构造函数CyclicBarrier(int parties,Runnable barrierAction),用来在线程到达屏障时,优先执行barrierAction。
Ⅱ.CyclicBarrier的应用场景
主要用于多线程计算数据,最后合并计算结果的场景。
Ⅲ.CyclicBarrier和CountDownLatch的区别
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。因此CyclicBarrier可以处理更加复杂的业务场景,而CyclicBarrier还提供了其他有用方法,例如getNumberWaiting可以获得CyclicBarrier阻塞的线程数量,isBroken()可以了解阻塞的线程是否被中断。
三.控制并发线程数的Semaphore
Semaphore是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
1.应用场景
Semaphore主要用来做流量控制,特别是公共资源有限的应用场景,例如数据库的连接。acquire获取一个许可证,release方法可以释放一个许可证。
2.其他方法
1️⃣intavailablePermits():返回这个信号量当前可用的许可证数。
2️⃣intgetQueueLength():返回正在等待获取许可证的线程数。
3️⃣booleanhasQueuedThreads():是否有线程正在等待获取许可证。
4️⃣void reducePermits(int reduction):减少reduction的许可证。
5️⃣Collection getQueuedThreads():返回所有等待获取许可证的线程集合。
四.线程间交换数据的Exchanger
Exchanger是一个用来线程之间协作的工具类,主要用于线程之间的数据交换。它提供了一个同步点,在这个同步点,两个线程之间可以交换彼此的数据。如果第一个线程先执行exchange()方法,他会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就会交换数据。
Exchanger可以用于遗传算法,也可以用于校对工作。
Java中的线程池
Java中的线程池是运用场景最多的并发框架,合理运用线程池可以带来3个好处:
第一.降低资源消耗。
第二.提高响应速度。
第三.提高线程的可管理性。
一.线程池的实现原理
当提交一个新任务到线程池中,线程池的处理流程如下:
(1)线程池判断核心线程池的线程是否都在执行任务。如果不是,就创建一个新的工作线程来执行任务。如果是就进入下个流程。
(2)判断工作队列是否满了,如果没有满就放在工作队列,如果满了就进入下个流程。
(3)判断线程池是否满了,如果没有就执行线程任务,如果满了就按照策略处理无法执行的任务。
二.线程池的使用
Ⅰ.线程池的创建
new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,milliseconds,runnableTaskQueue,handler)
需要几个参数:
(1)corePoolSize:线程池的基本大小。
(2)runnableTaskQueue:用来保存等待执行的任务的阻塞队列,可以由多种选择。
(3)maximumPoolSize:线程池最大数量,线程池允许创建的最大线程数,如果队列满了,并且已经创建的线程数小于最大线程数,线程池就不会再创建新的线程执行任务。如果用的是无界的任务队列就没有意义。
(4)ThreadFactory:用于创建线程的工厂。
(5)RejectedExecutionHandler:饱和策略。
二.向线程池提交任务
分别是execute()和submit()方法。
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
submit()方法用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过这个future类型可以判断任务是否执行成功。
三.关闭线程池
可以通过shutdown和shutdownNow来关闭线程池。原理是遍历线程池中的工作进程,然后逐个调用interrupt()方法中断线程,因此需要注意的是无法响应中断的任务将永远无法终止。shutdown只是将线程池的状态设置成shutdown状态,然后中断所有没有执行任务的线程。shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有正在执行或者暂停的线程。
只要调用任何一个关闭线程的方法,isShutdown方法就会返回true,当所有任务都关闭后,才表示线程池关闭成功,这时候调用isTerminaed才会返回true。
四.合理地配置线程池
从几个角度来分析:
(1)任务的性质:CPU密集型、IO密集型或者混合型任务。
(2)任务的优先级:高中低。
(3)任务的执行时间:长中短。
(4)任务的依赖性:是否依赖其他系统资源,例如数据库连接。
CPU密集型的任务配置尽可能小的线程,IO密集型就需要多配置几个线程。优先级不同的可以通过优先级队列PriorityBlockingQueue来处理,可以让优先级高的任务先执行(注意优先级队列会导致饥饿)。执行时间不同的任务可以交给不同规模的线程池来处理,或者使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提高SQL后需要等待数据库返回结果,等待时间越长,CPU空闲时间就越长,那么线程数就应该设置的大,这样更好的利用CPU。
建议使用有界队列来作为阻塞队列。
五.线程池的监控
takeCount:线程池中需要执行的任务数量。
completedTaskCount:线程池已经完成的任务数量。
largestPoolSize:线程池曾经创建的最大线程数量。
getPoolSize:线程池的线程数量。
getActiveCount:获取活动的线程数。
Executor框架
Java线程既是工作单元也是执行单元。从JDK5开始,把工作单元和执行机制分离开,工作单元包括Runnable和Callable,而执行机制由Executor框架提供。
一.Executor框架简介
Ⅰ.Executor框架的两级调度模型
在Hotspot中Java线程被映射为本地操作系统线程,Java线程启动时会创建一个本地操作系统线程;当java线程终止时,这个操作系统线程会被回收,操作系统会调度所有的线程并且将他们分配给可用的CPU。
上层Java多线程程序将应用分成若干任务,然后使用Executor框架将这个任务映射到固定数量的线程;在底层操作系统内核将这些线程映射到硬件处理器上。
可以看到上层的调度主要通过Executor框架,而下层的调度是由操作系统的内核完成的。
Ⅱ.Executor框架的结构与成员
1.Executor框架的结构
主要由三部分构成:
(1)任务。被执行任务要实现的接口:Runnable接口和Callable接口。
(2)任务的执行。包括任务执行机制的核心接口Executor,以及继承于Executor的ExecutorService接口。两个实现类ThreadPoolExecutor和ScheduledThreadPoolExecutor。
(3)异步计算的结果。包括Future和实现Future接口的FutureTask类。
主线程首先创建实现Runnable或者Callable接口的任务对象,工具类Executors可以把一个Runnable对象封装成Callable对象,然后把Runnable对象直接交给ExecutorService执行,或者提交执行。如果用submit执行那么结果将会返回到一个实现future接口的对象,目前的JDK都是FutureTask对象。
2.Executor框架的成员
(1)ThreadPoolExecutor
通常使用工厂类Executors来创建,可以创建三种类型的ThreadPoolExecutor:
1️⃣FixedThreadPool:创建固定线程数的FixedThreadPool,适用于满足资源管理的需求而需要限制线程数量的场景。
2️⃣SingleThreadExecutor:创建单个线程的SingleThreadExecutor。
3️⃣CachedThreadPool:创建一个会根据需求创建新线程的API,是个大小无界的线程池。
(2)ScheduledThreadPoolExecutor
可以提供两种类型的ScheduledThreadPoolExecutor:
1️⃣ScheduledThreadPoolExecutor:包含若干个线程的。
2️⃣SingleThreadScheduledExecutor:包含一个线程。
这两个都适合执行周期任务。
(3)Future接口
Future接口和实现Future接口的FutureTask类都用来表示异步计算的结果。
需要注意的是返回的不一定是FutureTask对象还可能是实现Future接口的对象。
(4)Runnable接口和Callable接口
这个实现类的区别是Runnable不会返回结果,而Callable会返回结果。
除了实现Callable接口的对象外,还可以通过工厂类Executors将一个Runnable包装成一个Callable。
ThreadPoolExecutor详解
Executor框架最核心的类是ThreadPoolExecutor,是线程池的实现类。主要由以下的4个组件构成:
1️⃣corePool:核心线程池的大小。
2️⃣maximumPool:最大线程池的大小。
3️⃣BlockingQueue:用来暂时保存任务的工作队列。
4️⃣RejectedExecutionHandler:达到最大线程池大小并且工作队列满了时候调用。
5️⃣通过工具类可以创建三种ThreadPoolExecutor。
Ⅰ.FixedThreadPool详解
可重用固定线程数的线程池。corePool的数量和maximumPool的数量是相同的都是一开始设置的线程数大小。
几点说明:
1.当运行的线程数小于corePoolSize时将创造新线程来执行任务。
2.当corePoolSize满后(完成预热),将任务加入LinkedBlockingQueue。
3.线程执行完1中的任务后,会循环从BlockingQueue队列中获取任务来执行。
FixedThreadPool使用的是无界队列LinkedBlockingQueue,使用无界工作线程将会有以下的影响:
1.线程池中的线程数不会超过corePoolSize。
2.由于1因此maximumPoolSize将会是个无效参数。
3.由于1和2,所以keepAliveTime将会是个无效参数。
4.由于使用无界队列,运行中的FixedThreadPool不会拒绝任务。
Ⅱ.SingleThreadExecutor详解
corePoolSize和maximumPoolSize都被设置成1,几点说明:
1.如果当前没有线程运行,就创建一个新的线程来执行任务。
2.线程池完成预热后,将任务加入LinkedBlockingQueue。
3.执行完1中的任务后,会在一个无限循环中反复从LinkedBlockingQueue中获取任务来执行。
Ⅲ.CachedThreadPool详解
corePoolSize设置成0,maximumPoolSize被设置成integer.max_value,及最大的线程数是无界的,将keepAliveTime设置成60L,意味着空闲线程等待新任务的最大时间为60秒。CachedThreadPool使用了没有容量的SynchronousQueue作为线程池的工作队列。几点说明:
1.首先是将任务放入队列中,由于这是个传输者队列,必须有空闲线程在获取任务才能把任务传输到线程池中,否则将执行步骤2。
2.当maximumPool为空的时候或者线程池中没有空闲线程的时候,这时候线程池中会创建一个新线程来执行任务。
3.新创建的线程执行完毕后会执行poll从传输者队列获取任务60秒,如果60秒没有获得任务将会把这个线程关闭。
三.ScheduledThreadPoolExecutor详解
主要用于在给定的延迟之后执行任务或者定期执行任务。可以在构造函数中指定多个对应的后台线程数。
Ⅰ.ScheduledThreadPoolExecutor的运行机制
基于JDK6,DelayQueue是一个无界队列,所以maximumPooledSize没有什么意义。执行主要分为两大部分:
(1)调用scheduleAtFixedRate()方法或者scheduleWithFixedDelay(),会向DelayQueue添加一个实现了RunnableScheduledFutur接口的ScheduledFutureTask。
(2)线程池从DelayQueue中获取ScheduledFutureTask,然后执行任务。
跟ThreadPoolExecutor相比做了如下的修改:
(1)使用DelayQueue作为任务队列。
(2)获取任务的方式不同。
(3)执行周期任务后,增加了额外的处理。
Ⅱ.ScheduledThreadPoolExecutor的实现
ScheduledFutureTask主要包括了3个成员变量,如下:
1️⃣long型成员变量time,表示这个任务要被执行的具体时间。
2️⃣long成员变量sequenceNumber,表示这个任务被加入线程池中的序号。
3️⃣long型成员变量period,表示任务执行的间隔周期。
如果两个任务的执行时间相同,那么先提交的任务先被执行。
线程执行周期任务的4个步骤:
1️⃣线程从DelayQueue中获取已到期的ScheduledFutureTask,到期任务指的是ScheduledFutureTask的time大于等于当前的时间。
2️⃣线程执行这个ScheduledFutureTask。
3️⃣线程修改time变量为下次要执行的时间。
4️⃣线程将这个ScheduledFutureTask放回DelayQueue中。
四.FutureTask详解
Ⅰ.FutureTask简介
FutureTask除了实现Future接口外,还实现了Runnable接口。因此FutureTask可以交给Executor执行,也可以直接调用FutureTask.run(),根据run方法被执行的时机,可以处于下面3种状态:
(1)未启动。没有调用run方法之前。
(2)已启动。run方法被执行的过程中。
(3)已完成。执行完正常结束或者被取消或者抛出异常。
当处在1或者2状态时,执行get()方法将导致调用线程阻塞;处在3状态时,执行方法将会立刻返回结果或者抛出异常。
Ⅱ.FutureTask的使用
可以将FutureTask交给Executor执行,也可以通过submit返回一个FutureTask,然后执行get或者cancel方法,除此之外还可以单独使用FutureTask。
当一个线程需要等待另外一个线程把某个任务执行完后它才能继续执行,这个时候就可以使用FutureTask。
Ⅲ.FutureTask的实现
实现是基于AQS,每个AQS实现的同步器都会包含两种类型的操作:
(1)至少一个acquire操作。
(2)至少一个release操作。
FutureTask的内部子类Sync,AQS作为基础类提供给Future的内部子类Sync,具体就是Sync实现了AQS的tryAcquireShared(int)和tryReleaseShared(int)方法。
FutureTask.get()方法会调用AQS.acquireSharedinterruptibly(int arg)方法,这个方法的执行过程如下:
(1)这个方法首先调用tryAcquireShared方法来判断acquire操作是否成功,成功的条件就是state是RAN或者CANCELLED。
(2)如果成功get方法就立刻返回,如果失败就到线程等待队列等待其他线程执行release操作。
(3)当其他线程执行release操作(run或者cancel)唤醒当前线程后,当前线程执行返回1,当前线程将离开线程等待队列并唤醒它的后继线程。
(4)最后返回计算的结果或者抛出异常。
FutureTask.run()的执行过程如下:
(1)执行在构造函数中指定的函数
(2)以原子方式更新同步状态。
(3)AQS.releaseShared(int arg)
(4)调用FutureTask.done