Java多线程并发编程
一、线程池
1.1、什么是线程池
线程池是一种多线程的处理方式,利用已有线程对象继续服务新的任务(按照一定的执行策略),而不是频繁地创建销毁线程对象,由此提高服务的吞吐能力,减少CPU的闲置时间。具体组成部分包括:
(1)、线程池管理器(ThreadPool)用于创建和管理线程池,包括创建线程池、销毁线程池,添加新任务。
(2)、工作线程(Worker)线程池中的线程,闲置的时候处于等待状态,可以循环回收利用。
(3)、任务接口(Task)每个任务必须实现的接口类,为工作线程提供调用,主要规定了任务的入口、任务完成的收尾工作、任务的状态。
(4)、等待队列(Queue)存放等待处理的任务,提供缓冲机制。
1.2、线程池的种类
(1)、FixedThreadPool:固定数量的线程池,线程池中的线程数量是固定的,不会改变。
(2)、SingleThreadExecutor:单一线程池,线程池中只有一个线程。
(3)、CachedThreadPool:缓存线程池,线程池中的线程数量不固定,会根据需求的大小进行改变。
(4)、ScheduledThreadPool:计划任务调度的线程池,用于执行计划任务,比如每隔5分钟怎么样。
ThreadPoolExecutor构造函数中参数的含义:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
(1)、corePoolSize:线程池中核心线程数的数目
(2)、maximumPoolSize:线程池中最多能容纳多少个线程
(3)、keepAliveTime:当现在线程数目大于corePoolSize时,超过keepAliveTime时间后,多出corePoolSize的那些线程将被终结。
(4)、unit:keepAliveTime的单位
(5)、workQueue:当任务数量很大,线程池中线程无法满足时,提交的任务会被放到阻塞队列中,线程空闲下来则会不断从阻塞队列中取数据。
(6)、threadFactory:执行程序创建新线程时使用的工厂。
(7)、handler:由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
RejectedExecutionHandler拒绝的4种策略:
(1)、AbortPolicy:如果不能接受任务了,则抛出异常。
(2)、CallerRunsPolicy:如果不能接受任务了,则让调用的线程去完成。
(3)、DiscardOldestPolicy:如果不能接受任务了,则丢弃最老的一个任务,由一个队列来维护。
(4)、DiscardPolicy:如果不能接受任务了,则丢弃任务。
1.3、携带结果的任务Callable和Future/FutureTask
(1)、Callable:解决Runnable接口不能返回一个值或受检查的异常,可以采用Callable接口实现一个任务。
(2)、Future:表示异步计算的结果,可以对于具体的Runnable或者Callable任务进行查询是否完成,查询是否取消,获取执行结果,取消任务等操作。
(3)、FutureTask:是一个RunnableFuture<V>,而RunnableFuture实现了Runnbale又实现了Futrue<V>这两个接口。
1.4、Fork/Join框架
Fork/Join框架是Java7提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
Fork/Join类似MapReduce算法,两者区别是:Fork/Join 只有在必要时如任务非常大的情况下才分割成一个个小任务,而 MapReduce总是在开始执行第一步进行分割。看来,Fork/Join更适合一个JVM内线程级别,而MapReduce适合分布式系统。
(1)、工作窃取算法
工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。工作窃取的运行流程图如下:
那么为什么需要使用工作窃取算法呢?
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。
工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。
(2)、如何设计一个Fork/Join框架?
第一步分割任务。首先我们需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小。
第二步执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。
Fork/Join使用两个类来完成以上两件事情:
1)、ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类:
RecursiveAction:用于没有返回结果的任务。
RecursiveTask :用于有返回结果的任务。
2)、ForkJoinPool :ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。
(2)、Fork/Join框架的实现原理
ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责存放程序提交给ForkJoinPool的任务,而ForkJoinWorkerThread数组负责执行这些任务。
ForkJoinTask的fork方法实现原理:当我们调用ForkJoinTask的fork方法时,程序会调用ForkJoinWorkerThread的pushTask方法异步的执行这个任务,然后立即返回结果。pushTask方法把当前任务存放在ForkJoinTask 数组queue里。然后再调用ForkJoinPool的signalWork()方法唤醒或创建一个工作线程来执行任务。
ForkJoinTask的join方法实现原理:Join方法的主要作用是阻塞当前线程并等待获取结果。
(4)、Fork/Join框架的异常处理
ForkJoinTask在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过ForkJoinTask的getException方法获取异常。
二、JDK并发包
2.1、同步控制工具类
(1)、Semaphore
计数信号量,常用于限制可以访问某些资源(物理或逻辑的)线程数目。是一个共享锁,允许N个线程同时进入临界区, 但是超出许可范围的只能等待;如果N = 1, 则类似于lock
(2)、ReentrantLock
简而言之, 就是自由度更高的synchronized, 主要具备以下优点
可重入:单线程可以重复进入,但要重复退出
可中断:lock.lockInterruptibly()
可限时:超时不能获得锁,就返回false,不会永久等待构成死锁
公平锁:先来先得,public ReentrantLock(boolean fair),默认锁不公平的, 根据线程优先级竞争
(3)、Condition
类似于 Object.wait()和Object.notify(), 需要与ReentrantLock结合使用.
(4)、ReadWriteLock
读写分离锁, 可以大幅提升系统并行度.
读-读不互斥:读读之间不阻塞。
读-写互斥:读阻塞写,写也会阻塞读。
写-写互斥:写写阻塞。
(5)、CountDownLatch倒数计时器
一种典型的场景就是火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检查。
只有等所有检查完毕后,引擎才能点火。这种场景就非常适合使用CountDownLatch。它可以使得点火线程,
等待所有检查线程全部完工后,再执行
(6)、CyclicBarrier循环栅栏
Cyclic意为循环,也就是说这个计数器可以反复使用。比如,假设我们将计数器设置为10。那么凑齐
第一批10个线程后,计数器就会归零,然后接着凑齐下一批10个线程
(7)、LockSupport
一个线程阻塞工具, 可以在任意位置让线程阻塞。与wait/notify比较,如果unpark发生在park之前, 并不会导致线程冻结,也不需要获取锁
LockSupport比Object的wait/notify有两大优势:
1)、LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦
2)、unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序
2.2、并发容器
(1)、Collections.synchronizedMap
其本质是在读写map操作上都加了锁,因此不推荐在高并发场景使用
(2)、ConcurrentHashMap
内部使用分区Segment来表示不同的部分, 每个分区其实就是一个小的hashtable,各自有自己的锁,只要多个修改发生在不同的分区,他们就可以并发的进行。把一个整体分成了16个Segment,最高支持16个线程并发修改
(3)、BlockingQueue
阻塞队列, 主要用于多线程之间共享数据;当一个线程读取数据时,如果队列是空的,则当前线程会进入等待状态;如果队列满了,当一个线程尝试写入数据时,同样会进入等待状态;适用于生产消费者模型,因为BlockingQueue在put、take等操作有锁,因此非高性能容器;如果需要高并发支持的队列,则可以使用ConcurrentLinkedQueue,他内部运用了大量无锁操作
(4)、CopyOnWriteArrayList
CopyOnWriteArrayList通过在新增元素时,复制一份新的数组出来,并在其中写入数据,之后将原数组引用指向到新数组,其Add操作是在内部通过ReentrantLock进行锁保护,防止多线程场景复制多份数组,而Read操作内部无锁,直接返回数组引用,并发下效率高,因此适用于读多写少的场景