一、并发工具类
JDK1.5 引入常用并发工具类:CountDownLatch/Semaphore/CyclicBarrier/Exchanger
1. CountDownLatch
CountDownLatch允许一个或多个线程等待其他线程完成操作,效果跟join()类似
应用场景:常用于等待多线程运行结果
原理:内部采用共享锁实现
public class CountDownLatchJob extends Thread{
private CountDownLatch startSignal;
private CountDownLatch doneSignal;
CountDownLatchJob(CountDownLatch startSignal, CountDownLatch doneSignal) {
this.startSignal = startSignal;
this.doneSignal = doneSignal;
}
@Override
public void run() {
try {
this.startSignal.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": execute ");
this.doneSignal.countDown();
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new CountDownLatchJob(startSignal,doneSignal).start();
}
Thread.sleep(2000);
System.out.println("thread start");
startSignal.countDown();
doneSignal.await();
System.out.println("main exit");
}
}
2. Semaphore
信号量,用于控制访问共享资源的计数器。
应用场景:常用于限制可以访问共享资源的线程数
原理:内部采用共享锁实现
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);//一次只能两个线程执行
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.submit(()->{
try {
semaphore.acquire();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+": excute something");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
3. CyclicBarrier
简单来说,让一组线程到达一个屏障时阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有线程才继续运行。
应用场景:多线程结果合并的操作,比如多线程计算数据,最后合并计算结果
原理:底层采用ReentrantLock + Condition实现
public class CyclicBarrierJob implements Runnable {
int [][] array;
CyclicBarrier cyclicBarrier;
ConcurrentHashMap val = new ConcurrentHashMap<>();
public CyclicBarrierJob(int [][] array){
this.array = array;
cyclicBarrier = new CyclicBarrier(array.length,this);
}
public void execute(){
for (int i = 0; i < array.length; i++) {
int index = i;
new Thread(()->{
try {
double result = Arrays.stream(array[index]).average().getAsDouble();
System.out.println(Thread.currentThread().getName() + " avg:" + result);
val.put(index, result);
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName()+" end");
} catch (BrokenBarrierException | InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
@Override
public void run() {
System.out.println(val.values().stream().mapToDouble((s) -> s).summaryStatistics().getAverage());
}
public static void main(String[] args) throws InterruptedException {
int[][] array = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12, 13, 14}};
CyclicBarrierJob cyclicBarrierJob = new CyclicBarrierJob(array);
cyclicBarrierJob.execute();
}
}
1.CountDownLatch 的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier是允许N个线程互相等待
2.CountDownLatch的计算器无法被重置,CyclicBarrier可以使用reset()方法重置
3.CyclicBarrier提供高级的构造函数CyclicBarrier(int parties, Runnable barrierAction),都到达屏障优先执行barrierAction
4. Exchanger
允许两个线程之间定义同步点,进行数据交换
应用场景:一个任务需要两个人做,然后互相进行校对
原理:内部CAS
public class ExchangerJob extends Thread{
static Exchanger exchanger = new Exchanger<>();
private String content;
public ExchangerJob(String content){
super(content);
this.content = "content " + content;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + ":" + exchanger.exchange(content));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new ExchangerJob("A").start();
new ExchangerJob("B").start();
}
}
二、 并发集合&阻塞队列
JDK1.5 之前,对于集合并发访问可以使用一下三种方式:(1)Synchronized 关键字加锁,(2)利用集合框架中的线程安全类:Vector和HashTable,(3)Collections工具类 通过调用其中的静态方法,来得到线程安全的集合,比如Collections.synchronizedList(List
这三种方式实现原理都是采用synchronized锁,性能开销比较大
JDK1.5 开始,JDK包java.util.concurrent提供了丰富的安全并发集合类:一种是非阻塞式,另一种阻塞式;阻塞式都是队列结构
非阻塞式
- ConcurrentHashMap
主要是通过CAS和降低锁的力度来提升并发性能(JDK1.7使用分段锁)
平时最常用的HashMap是线程不安全,并发执行put操作会引起死循环,是因为HashMap的Entry链表形成环形数据结构,Entry的next节点永不为空。
- CopyOnWriteArrayList
Copy-On-Write原理:是先copy出一个容器(简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给之前旧的容器地址
类名 | 数据结构 | 说明 |
---|---|---|
CopyOnWriteArrayList | 基于写时复制(Copy-On-Write) 的List结构; 读不会阻塞,写会阻塞 | 并发读性能高,并发写空间消耗大;适合读多写少的场景 |
CopyOnWriteArraySet | 基于写时复制(Copy-On-Write) 的Set结构; 读不会阻塞,写会阻塞 | 并发读性能高,并发写空间消耗大;适合读多写少的场景 |
ConcurrentSkipListSet | 基于跳表(SkipList)实现的Set;跳表以空间换取时间, | 实际直接使用ConcurrentSkipListMap实现的,类似HashSet也是使用HashMap实现的 |
ConcurrentSkipListMap | 基于跳表(SkipList)实现的Map;跳表以空间换取时间 | |
ConcurrentHashMap | 采用分段锁技术实现 | |
ConcurrentLinkedDeque | 基于CAS实现双向无界队列(FIFO、FILO) | |
ConcurrentLinkedQueue | 基于CAS实现单向无界队列(FIFO) |
阻塞式队列
[图片上传失败...(image-8cca20-1668762493898)]
阻塞队列提供的常用方法如下:
方法/处理方式 | 抛出异常 | 返回值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | boolean add(E e) | boolean offer(E e) | void put(E e) | boolean offer(E e, long timeout, TimeUnit unit) |
移除方法 | E remove() | E poll() | E take() | E poll(long timeout, TimeUnit unit) |
检查方法 | E element() | E peak() | 不可用 | 不可用 |
类名 | 数据结构 | 说明 |
---|---|---|
ArrayBlockingQueue | 基于独占锁ReentrantLock+数组的阻塞单向有界队列 | 支持公平与非公平两种模式,公平队列:ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true); |
LinkedBlockingQueue | 基于两个独占锁+链表的阻塞单向无界队列 | 其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,提升整个队列的 并发性能 |
PriorityBlockingQueue | 基于独立锁+数组(堆)支持优先级的无界队列 | 内部提供自然顺序升序Comparator,也可通过构造函数参数指定自定义Comparator |
DelayQueue | 基于独立锁+优先级队列支持延迟获取元素的无界队列 | 常用于(1)缓存系统设计:一旦能从DelayQueue 中获取元素时,表示缓存有效期到了(2)定时任务调度:一旦从DelayQueue中获取到任务就开始执行,TimerQueue就是基于DelayQueue实现 |
SynchronousQueue | 一个不存储元素的阻塞队列 | 每一个put 操作必须等待一个take 操作,队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用 |
LinkedTransferQueue | 一个由链表结构组成的无界阻塞TransferQueue 队列 | 它多了tryTransfer 和transfer 方法,可以算是 LinkedBolckingQueue 和 SynchronousQueue 和合体。transfer 方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer 方法会将元素存放在队列的tail 节点,并等到该元素被消费者消费了才返回,适合线程间传递数据 |
LinkedBlockingDeque | 基于独占锁ReentrantLock+双向链表的阻塞双向无界队列 | 因为是双向队列,所以 多了addFirst , addLast , offerFirst , offerLast ,peekFirst,peekLast 等方法 |
三、ThreadLocal
也叫本地线程变量,是为了方便每个线程处理自己的状态而引入一个机制。其思路是为每一个线程创建一个单独的变量副本,互不影响。
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
final int index = i;
new Thread(()->{
ThreadLocal startTime = new ThreadLocal<>();
startTime.set(System.currentTimeMillis());
long time = 500 * index;
try {
Thread.sleep(time);
System.out.println(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("time:" + (System.currentTimeMillis() - startTime.get()));
}).start();
}
}
ThreadLocal内存泄漏根源:由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除(remove()方法)对应 key 就会导致内存泄漏