1. 并发工具类
1.1 CyclicBarrier
1.1.1 CyclicBarrier简介
CyclicBarrier
是同步屏障,它的作用类似于一道门,默认情况下关闭,堵住所以线程的道路,直到所有线程就位,让所有线程一起通过。
1.1.2 实现
内部使用了ReentrantLock
和Condition
1.1.3 使用
构造方法:
CyclicBarrier(int parties)
:它将在给定数量的参与者(线程)处于等待状态时启动。parties表示拦截线程的数量CyclicBarrier(int parties, Runnable barrierAction)
:创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动屏障时执行给定的屏障操作,该操作由最后一个进入屏障的线程执行。
阻塞方法:await()
1.1.4 注意
当n
个线程放行时,又会继续拦截一批
1.2 CountDownLatch
1.2.1 CountDownLatch简介
CountDownLatch
时一个计数的闭锁,让一个线程等待其他多个线程完成某件事之后才能执行。
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后就可以恢复等待的线程继续执行了
1.2.2 实现
CountDownLatch
内部依赖Sync实现,而Sync继承AQS,CountDownLatch
是采用共享锁来实现的。
1.2.3 使用
await
方法:使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。内部使用AQS的getState方法获取计数器,如果计数器值不等于0,则会以自旋方式会尝试一直去获取同步状态。countDown
方法:递减锁存器的计数,如果计数到达零,则释放所有等待的线程。内部调用AQS的releaseShared(int arg)方法来释放共享锁同步状态。
1.3 Semaphore
1.3.1 Semaphore简介
Semaphore是一个控制访问多个共享资源的计数器,和CountDownLatch一样,其本质上是一个“共享锁”。
Semaphore维护了一个信号量许可集。线程可以获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以释放它所持有的信号量许可,被释放的许可归还到许可集中,可以被其他线程再次获取。
当信号量为1时可以当作互斥锁使用
1.3.2 实现
Semaphore
内部包含公平锁(FairSync
)和非公平锁(NonfairSync
),继承内部类Sync
,其中Sync
继承AQS
1.3.3 使用
构造方法:
Semaphore(int permits)
:创建具有给定的许可数和非公平的 Semaphore。Semaphore(int permits, boolean fair)
:创建具有给定的许可数和给定的公平设置的 Semaphore。
获取许可:acquire()
方法
信号量释放:release()
方法
2. 并发容器
2.1 ConcurrentHashMap
2.1.1 ConcurrentHashMap简介
HashMap
是一个使用非常频繁的容器,但是它是线程不安全的。在jdk8
之前put
操作甚至会产生死循环。
解决该问题的方案可以使用:
HashTable
Collections.synchronizedMap
但是效率过低,他们的方法都是对读写加锁。
所以我们采用效率较高的ConcurrentHashMap
在jdk8
之前,ConcurrentHashMap
使用的是分段锁的概念,使锁细化。
在jdk8
之后,利用了CAS + Synchronized
保证并发更新安全。
2.1.2 JDK7的HashMap
jdk7
的HashMap
实现是数组 + 链表,绿色格子代表Entry
实例。
构造方法:public HashMap(int initialCapacity, float loadFactor)
capacity:当前数组容量,始终是2^n,每次扩容是当前数组的2倍
loadFactor:负载因子,默认为0.75
threshold:扩容的阈值,等于capacity * loadFactor
put
过程:
- 数组初始化,在第一个元素插入
HashMap
时做初始化,先确定了初始数组大小 - 计算具体数组位置,根据
key
进行hash
运算 - 找到下标后判断是是否
key
重复,如果没重复放在表头 - 在插入新值之前,如果
size
达到了阈值,并且当前插入的数组位置上已经有了元素,就会触发扩容。
get
过程:
- 根据
key
算出hash
值 - 根据
hash
找到对应的数组下标 - 遍历该位置的链表,找到相等的
key
2.1.3 JDK7的ConcurrentHashMap
ConcurrentHashMap
由一个个的Segment
组成,代表一段,所以又叫分段锁。
ConcurrentHashMap
是一个Segment
数组,Segment
通过继承ReentrantLock
进行加锁,每次加锁锁住的是一个Segment
,这样保证每个Segment
是安全的。
Segment
内部和之前的HashMap
就很类似了。
初始化:public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
initialCapacity:整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment
concurrencyLevel:并发数(或者Segment 数,有很多叫法,重要的是如何理解)。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的
loadFactor:负载因子,Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使用的
put
过程:
- 根据
hash
值找到对应的Segment
- 获取该
Segment
的独占锁 - 用上面的
HashMap
的方法在找到对应的位置
get
过程:
- 计算
hash
值,找到Segment
的对应位置 Segment
中找到具体的数组位置- 顺着链表查找
2.1.4 JDK8的HashMap
jdk8
利用了红黑树,所以组成有:数组 + 链表 + 红黑树
在链表的长度超过了8之后,会将链表转化为红黑树。
jdk7
中使用Entry
代表数据节点,jdk8
使用Node
,当改成红黑树之后会使用TreeNode
put
过程:jdk8
之后是先插值在扩容
get
过程:
- 计算
hash
值,找到对应的下标 - 判断该位置是否是要找的元素
- 如果是
TreeNode
类型用红黑树方法去找,否则遍历链表
2.1.5 注意
在并发执行时,线程安全的容器只能保证自身的数据不被破坏,和数据在多个线程间是可见的,但无法保证业务的行为是否正确。
2.2 HashTable
Hashtable和ConcurrentHashMap的不同点:
- Hashtable对get,put,remove都使用了同步操作,它的同步级别是正对Hashtable来进行同步的,也就是说如果有线程正在遍历集合,其他的线程就暂时不能使用该集合了,这样无疑就很容易对性能和吞吐量造成影响,从而形成单点。而ConcurrentHashMap则不同,它只对put,remove操作使用了同步操作,get操作并不影响。
- Hashtable在遍历的时候,如果其他线程,包括本线程对Hashtable进行了put,remove等更新操作的话,就会抛出ConcurrentModificationException异常,但如果使用ConcurrentHashMap的话,就不用考虑这方面的问题了
2.3 ConcurrentSkipListMap
Skip List ,称之为跳表,它是一种可以替代平衡树的数据结构,其数据元素默认按照key值升序,天然有序。Skip list让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过“空间来换取时间”的一个算法,在每个节点中增加了向前的指针,在插入、删除、查找时可以忽略一些不可能涉及到的结点,从而提高了效率。
上面就是一个典型的跳表
3. 队列
要实现一个线程安全的队列有两种方式:阻塞和非阻塞
queue | 是否阻塞 | 是否有界 | 线程安全保证 |
---|---|---|---|
ConcurrentLinkedQueue | 非阻塞 | 无界 | CAS |
ArrayBlockingQueue | 阻塞 | 有界 | 全局锁 |
LinkedBlockingQueue | 阻塞 | 可配置 | 存取采用两把锁 |
PriorityBlockingQueue | 阻塞 | 无界 | 全局锁 |
SynchronousQueue | 阻塞 | 无界 | CAS |
3.1 ConcurrentLinkedQueue
ConcurrentLinkedQueue是一个基于链接节点的无边界的线程安全队列,遵循队列的FIFO原则,队尾入队,队首出队。采用CAS算法来实现的。
@Test
public void test() {
Queue<Object> queue = new ConcurrentLinkedQueue<>();
// 添加
queue.add("...");
// 弹出并获取返回值
queue.poll();
// 判断是否为空
queue.isEmpty();
// 得到元素个数,注意!!会遍历整个队列
queue.size();
}
3.2 阻塞队列BlockingQueue
被阻塞的情况主要有如下两种:
- 当队列满了的时候进行入队列操作
- 当队列空了的时候进行出队列操作
BlockingQueue
对插入操作、移除操作、获取元素操作提供了四种不同的方法用于不同的场景中使用:
- 抛出异常
- 返回特殊值
- 阻塞等待直到成功
- 阻塞等待知道成功或者超时
操作 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offfer(e) | put(e) | offer(e, time, unit) |
移除 | remove() | poll() | take() | poll(time, unit) |
检查 | element() | peek() | 无 | 无 |
3.3 ArrayBlockingQueue
ArrayBlockingQueue是一个由数组实现的有界阻塞队列。该队列采用FIFO的原则对元素进行排序添加的。
ArrayBlockingQueue为有界且固定,其大小在构造时由构造函数来决定,确认之后就不能再改变了。
ArrayBlockingQueue支持对等待的生产者线程和使用者线程进行排序的可选公平策略,但是在默认情况下不保证线程公平的访问,在构造时可以选择公平策略(fair = true)。公平性通常会降低吞吐量,但是减少了可变性和避免了“不平衡性”。
ArrayBlockingQueue内部使用可重入锁ReentrantLock + Condition来完成多线程环境的并发操作:
items
:定长数组,维护元素takeIndex
:队首位置putIndex
:队尾位置count
:元素个数lock
:锁,出入队都要先获取
3.4 LinkedBlockingQueue
- LinkedBlockingQueue是一个基于链表的有界(可设置)阻塞队列
- LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock
- LinkedBlockingQueue实现的队列中在生产和消费的时候,需要把枚举对象转换为Node进行插入或移除,会影响性能
- LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE
- 使用方法基本同
ArrayBlockingQueue
3.5 PriorityBlockingQueue
PriorityBlockingQueue
始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部使用二叉堆,通过使用一个二叉树最小堆算法来维护内部数组,这个数组是可扩容的,当前元素个数>=最大容量时候会通过算法扩容。值得注意的是为了避免在扩容操作时候其他线程不能进行出队操作,实现上使用了先释放锁,然后通过CAS保证同时只有一个线程可以扩容成功。
3.6 SynchronousQueue
其实并不是一个真正的队列,因为他并不会维护存储空间。
它维护一组线程,这些线程等待着把元素加入或者移除队列。
put
和get
会一直阻塞,知道另一个线程做出另一个操作。
iterator()
: 永远返回空,因为里面没东西。peek()
:永远返回null。put()
:往queue放进去一个element以后就一直wait直到有其他thread进来把这个element取走。offer()
:往queue里放一个element后立即返回,如果碰巧这个element被另一个thread取走了,offer方法返回true,认为offer成功;否则返回false。offer(2000, TimeUnit.SECONDS)
:往queue里放一个element但等待时间后才返回,和offer()方法一样。take()
:取出并且remove掉queue里的element,取不到东西他会一直等。poll()
:取出并且remove掉queue里的element,方法立即能取到东西返回。否则立即返回null。poll(2000, TimeUnit.SECONDS)
:等待时间后再取,并且remove掉queue里的element,isEmpty()
:永远是true。remainingCapacity()
:永远是0。remove()和removeAll()
:永远是false。
4. 线程池
线程池的好处:
- 降低资源消耗:通过重用已经创建的线程来降低线程创建和销毁的消耗
- 提高响应速度:任务到达时不需要等待线程创建就可以立即执行。
- 提高线程的可管理性:线程池可以统一管理、分配、调优和监控。
java的线程池支持主要通过ThreadPoolExecutor
来实现,我们使用的ExecutorService
的各种线程池策略都是基于ThreadPoolExecutor
实现的,所以ThreadPoolExecutor
十分重要。要弄明白各种线程池策略,必须先弄明白ThreadPoolExecutor。
4.1 线程池的状态
有五种状态:
Running
SHUTDOWN
STOP
TIDYING
TERMINATED
变量ctl
记录了线程池的任务数量和状态:
- 高三位表示线程池状态
- 低29位表示任务池中的任务数量
状态转化如图
4.2 构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
this.ctl = new AtomicInteger(ctlOf(-536870912, 0));
this.mainLock = new ReentrantLock();
this.workers = new HashSet();
this.termination = this.mainLock.newCondition();
if (corePoolSize >= 0 && maximumPoolSize > 0 && maximumPoolSize >= corePoolSize && keepAliveTime >= 0L) {
if (workQueue != null && threadFactory != null && handler != null) {
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
} else {
throw new NullPointerException();
}
} else {
throw new IllegalArgumentException();
}
}
参数含义:
-
corePoolSize
:核心线程的数量,当提交一个任务时,线程池会创建一个线程来执行任务,不管是否有空间的线程,直到线程数等于corePoolSize为止,刚创建时线程池为空的。 -
maximumPoolSize
:允许的最大线程数,如果阻塞队列满了,还有任务提交并且当前线程数小于maximumPoolSize
,会创建新线程来执行任务。 -
keepAliveTime
:线程空闲时间,线程执行完任务后不会立即销毁,默认情况下,只有当前线程数大于corePoolSize
才有效。 -
unit
:keepAliveTime
的单位 -
workQueue
:保存等待执行的任务的阻塞队列,等待的任务必须实现Runnable
接口。ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue -
threadFactory
:用来创建线程的工厂。 -
handler
:拒绝策略,将任务添加到线程池中,拒绝该任务所采取的策略(如果线程饱和并且阻塞队列也满了,会拒绝)AbortPolicy: 直接抛出异常(默认)
CallerRunsPolicy:调用者所在的线程来执行
DiscardPolicy:丢弃该任务
DiscardOldestPolicy:丢弃最考前的任务
自己实现:实现RejectedExecutionHandler接口
4.3 四种线程池
Executor框架提供了三种线程池,他们都可以通过工具类Executors来创建。
还有一种线程池ScheduledThreadPoolExecutor,它相当于提供了“延迟”和“周期执行”功能的ThreadPoolExecutor
4.3.1 FixedThreadPool
复用固定数量的线程处理一个共享的无边界队列
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
核心线程数和最大线程数相同,所以如果没空闲线程,就直接添加到阻塞队列中。
// 无参默认整数最大值
ExecutorService exec = Executors.newFixedThreadPool(3);
exec.execute(实现的Runnable);
4.3.2 SingleThreadExecutor
SingleThreadExecutor
只会使用单个工作线程来执行一个无边界的队列。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
4.3.3 CachedThreadPool
CachedThreadPool
会根据需要,在线程可用时,重用创建好的线程,否则创建新线程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
注意初始线程数为0,允许空闲时间为一分钟。
适用于短时间内执行多次任务,但是不能特别大,否则相当于创建无穷个线程,CPU扛不住。
4.3.4 ScheduledThreadPool
Timer
和TimerTask
可以实现线程的周期和延迟调度,但是存在问题,推荐采用ScheduledThreadPool
来解决。