java并发编程之美读书笔记-3

java并发编程之美读书笔记-3

  1. 线程池主要解决两个问题:一是当执行大量异步任务时线程池能够提供较好的性能。在不使用线程池时,每当需要执行异步任务时直接new一个线程来运行,而线程的创建和销毁是需要开销的。线程池里面的线程是可复用的,不需要每次执行异步任务时都重新创建和销毁线程。二是线程池提供了一种资源限制和管理的手段,比如可以限制线程的个数,动态新增线程等。每个ThreadPoolExecutor也保留了一些基本的统计数据,比如当前线程池完成的任务数目等。
    另外,线程池也提供了许多可调参数和可扩展性接口,以满足不同情境的需要,程序员可以使用更方便的Executors的工厂方法,比如newCachedThreadPool(线程池线程个数最多可达Integer.MAX_VALUE,线程自动回收)、newFixedThreadPool(固定大小的线程池)和newSingleThreadExecutor(单个线程)等来创建线程池,当然用户还可以自定义。
    线程池状态含义如下。
    ● RUNNING:接受新任务并且处理阻塞队列里的任务。
    ● SHUTDOWN:拒绝新任务但是处理阻塞队列里的任务。
    ● STOP:拒绝新任务并且抛弃阻塞队列里的任务,同时会中断正在处理的任务。● TIDYING:所有任务都执行完(包含阻塞队列里面的任务)后当前线程池活动线程数为0,将要调用terminated方法。
    ● TERMINATED:终止状态。terminated方法调用完成以后的状态。
    线程池状态转换列举如下。
    ● RUNNING -> SHUTDOWN :显式调用shutdown()方法,或者隐式调用了finalize()方法里面的shutdown()方法。
    ● RUNNING或SHUTDOWN)-> STOP :显式调用shutdownNow()方法时。
    ● SHUTDOWN -> TIDYING :当线程池和任务队列都为空时。
    ● STOP -> TIDYING :当线程池为空时。
    ● TIDYING -> TERMINATED:当terminated()hook方法执行完成时。
    线程池参数如下。
    ● corePoolSize:线程池核心线程个数。
    ● workQueue:用于保存等待执行的任务的阻塞队列,比如基于数组的有界ArrayBlockingQueue、基于链表的无界LinkedBlockingQueue、最多只有一个元素的同步队列SynchronousQueue及优先级队列PriorityBlockingQueue等。
    ● maximunPoolSize:线程池最大线程数量。
    ● ThreadFactory:创建线程的工厂
    。● RejectedExecutionHandler:饱和策略,当队列满并且线程个数达到maximunPoolSize后采取的策略,比如AbortPolicy(抛出异常)、CallerRunsPolicy(使用调用者所在线程来运行任务)、DiscardOldestPolicy(调用poll丢弃一个任务,执行当前任务)及DiscardPolicy(默默丢弃,不抛出异常)● keeyAliveTime:存活时间。如果当前线程池中的线程数量比核心线程数量多,并且是闲置状态,则这些闲置的线程能存活的最大时间。
    ● TimeUnit:存活时间的时间单位。
    线程池类型如下
    ● newFixedThreadPool :创建一个核心线程个数和最大线程个数都为nThreads的线程池,并且阻塞队列长度为Integer.MAX_VALUE。keeyAliveTime=0说明只要线程个数比核心线程个数多并且当前空闲则回收。
    ● newSingleThreadExecutor:创建一个核心线程个数和最大线程个数都为1的线程池,并且阻塞队列长度为Integer.MAX_VALUE。keeyAliveTime=0说明只要线程个数比核心线程个数多并且当前空闲则回收。
    ● newCachedThreadPool :创建一个按需创建线程的线程池,初始线程个数为0,最多线程个数为Integer.MAX_VALUE,并且阻塞队列为同步队列。keeyAliveTime=60说明只要当前线程在60s内空闲则回收。这个类型的特殊之处在于,加入同步队列的任务会被马上执行,同步队列里面最多只有一个任务。
  2. execute方法的作用是提交任务command到线程池进行执行,ThreadPoolExecutor的实现实际是一个生产消费模型,当用户添加任务到线程时相当于生产者生产元素,workers线程工作集中的线程直接执行任务或者从任务队列里面获取任务时则相当于消费者消费元素。
  3. schedule(Runnable command, long delay, TimeUnit unit)方法
    该方法的作用是提交一个延迟执行的任务,任务从提交时间算起延迟单位为unit的delay时间后开始执行。提交的任务不是周期性任务,任务只会执行一次
    scheduleWithFixedDelay(Runnable command, longinitialDelay, long delay, TimeUnit unit)方法
    该方法的作用是,当任务执行完毕后,让其延迟固定时间后再次运行(fixed-delay任务)。其中initialDelay表示提交任务后延迟多少时间开始执行任务command, delay表示当任务执行完毕后延长多少时间后再次运行command任务,unit是initialDelay和delay的时间单位。任务会一直重复运行直到任务运行中抛出了异常,被取消了,或者关闭了线程池。
    scheduleAtFixedRate(Runnable command, long initialDelay,long period, TimeUnit unit)方法
    该方法相对起始时间点以固定频率调用指定的任务(fixed-rate任务)。当把任务提交到线程池并延迟initialDelay时间(时间单位为unit)后开始执行任务command。然后从initialDelay+period时间点再次执行,而后在initialDelay + 2 * period时间点再次执行,循环往复,直到抛出异常或者调用了任务的cancel方法取消了任务,或者关闭了线程池。scheduleAtFixedRate的原理与scheduleWithFixedDelay类似
    总结:相对于fixed-delay任务来说,fixed-rate方式执行规则为,时间为initdelday +n*period时启动任务,但是如果当前任务还没有执行完,下一次要执行任务的时间到了,则不会并发执行,下次要执行的任务会延迟执行,要等到当前任务执行完毕后再执行。
  4. 在日常开发中经常会遇到需要在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。在CountDownLatch出现之前一般都使用线程的join()方法来实现这一点,但是join方法不够灵活,不能够满足不同场景的需要,所以JDK开发组提供了CountDownLatch这个类。
    用法:创建一个CountDownLatch实例
private static volatile CountDownLatch countDownLatch = new CountDownLatch(2);

在每个子线程中调用countDownLatch .countDown();
在主线程中调用countDownLatch .await();等待子线程结果汇总;
在项目实践中一般都避免直接操作线程,而是使用ExecutorService线程池来管理。使用ExecutorService时传递的参数是Runable或者Callable对象,这时候你没有办法直接调用这些线程的join()方法,这就需要选择使用CountDownLatch了。

ExecutorService executorService = Executors.newFixedThreadPool(2);
		executorService.submit(()->{
			try {
				Thread.sleep(1000);
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				countDownLatch.countDown();
				System.out.println("thread-1 is over");
			}
		});

这里总结下CountDownLatch与join方法的区别。一个区别是,调用一个子线程的join()方法后,该线程会一直被阻塞直到子线程运行完毕,而CountDownLatch则使用计数器来允许子线程运行完毕或者在运行中递减计数,也就是CountDownLatch可以在子线程运行的任何时候让await方法返回而不一定必须等到线程结束。另外,使用线程池来管理线程时一般都是直接添加Runable到线程池,这时候就没有办法再调用线程的join方法了,就是说countDownLatch相比join方法让我们对线程同步有更灵活的控制。
本节首先介绍了CountDownLatch的使用,相比使用join方法来实现线程间同步,前者更具有灵活性和方便性。另外还介绍了CountDownLatch的原理,CountDownLatch是使用AQS实现的。使用AQS的状态变量来存放计数器的值。首先在初始化CountDownLatch时设置状态值(计数器值),当多个线程调用countdown方法时实际是原子性递减AQS的状态值。当线程调用await方法后当前线程会被放入AQS的阻塞队列等待计数器为0再返回。其他线程调用countdown方法让计数器值递减1,当计数器值变为0时,当前线程还要调用AQS的doReleaseShared方法来激活由于调用await()方法而被阻塞的线程。
5. 为了满足计数器可以重置的需要,JDK开发组提供了CyclicBarrier类,并且CyclicBarrier类的功能并不限于CountDownLatch的功能。从字面意思理解,CyclicBarrier是回环屏障的意思,它可以让一组线程全部达到一个状态后再全部同时执行。这里之所以叫作回环是因为当所有等待线程执行完毕,并重置CyclicBarrier的状态后它可以被重用。之所以叫作屏障是因为线程调用await方法后就会被阻塞,这个阻塞点就称为屏障点,等所有线程都调用了await方法后,线程们就会冲破屏障,继续向下运行。
用法:创建一个实例

private static CyclicBarrier cyclicBarrier = new CyclicBarrier(2, ()->{
	System.out.println("all tasks complete");
	//当达到屏障时候输出
	}) ;

在子线程中调用cyclicBarrier.await();会阻塞子线程,并等待到达屏障。
可复用的CyclicBarrier 实现,其中一个线程到达屏障就会阻塞,等到冲破屏障继续执行,同时屏障恢复到起始状态

executorService.submit(()->{
			try {
				System.out.println("thread-1 ----- step 1");
				cyclicBarrier.await();
				System.out.println("thread-1 ----- step 2");
				cyclicBarrier.await();
				System.out.println("thread-1 ----- step 3");
			} catch (Exception e) {
				e.printStackTrace();
			} 
		});

本节首先通过案例说明了CycleBarrier与CountDownLatch的不同在于,前者是可以复用的,并且前者特别适合分段任务有序执行的场景。然后分析了CycleBarrier,其通过独占锁ReentrantLock实现计数器原子性更新,并使用条件变量队列来实现线程同步。
6. Semaphore信号量也是Java中的一个同步器,与CountDownLatch和CycleBarrier不同的是,它内部的计数器是递增的,并且在一开始初始化Semaphore时可以指定一个初始值,但是并不需要知道需要同步的线程个数,而是在需要同步的地方调用acquire方法时指定需要同步的线程个数。
用法:创建实例

private static Semaphore semaphore = new Semaphore(0);

然后在子线程中调用semaphore.release();阻塞
最后在主线程中调用semaphore.acquire(2);指定2到达信号;
主线程调用acquire方法后返回值为0,所以可以实现复用。
本节首先通过案例介绍了Semaphore的使用方法,Semaphore完全可以达到CountDownLatch的效果,但是Semaphore的计数器是不可以自动重置的,不过通过变相地改变aquire方法的参数还是可以实现CycleBarrier的功能的。然后介绍了Semaphore的源码实现,Semaphore也是使用AQS实现的,并且获取信号量时有公平策略和非公平策略之分。
本章介绍了并发包中关于线程协作的一些重要类。首先CountDownLatch通过计数器提供了更灵活的控制,只要检测到计数器值为0,就可以往下执行,这相比使用join必须等待线程执行完毕后主线程才会继续向下运行更灵活。另外,CyclicBarrier也可以达到CountDownLatch的效果,但是后者在计数器值变为0后,就不能再被复用,而前者则可以使用reset方法重置后复用,前者对同一个算法但是输入参数不同的类似场景比较适用。而Semaphore采用了信号量递增的策略,一开始并不需要关心同步的线程个数,等调用aquire方法时再指定需要同步的个数,并且提供了获取信号量的公平性策略。使用本章介绍的类会大大减少你在Java中使用wait、notify等来实现线程同步的代码量,在日常开发中当需要进行线程同步时使用这些同步类会节省很多代码并且可以保证正确性。


版权声明:本文为tianyadaoke2020原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。