并发容器-写时复制的list和Set
CopyOnWriteArrayList
基于synchronized的同步容器有问题,其中一个就是复合操作饿时候(比如先检查再更新),也需要调用方加锁。
CopyOnWriteArrayList的内部也是一个数组,每次修改操作,都会新建一个数组,复制原数组的内容到新数组,在新数组上进行需要的修改,然后以原子方式设置内部的数组引用,这就是写时复制。
一句话:数组内容是只读的,写操作都是通过新建数组,然后原子性的修改数组引用来实现的。
读不需要锁,可以并行。读写也可以并行,但是只允许一个写线程。
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
来看 add方法
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
以优化读的操作为目标,在优化读的同时牺牲了写的性能。
保证线程安全的两种思路,一种是锁,使用synchronized或者ReentrantLock,另外一种就是循环CAS。写时复制提现了保证线程安全的另外一种思路。
**锁和循环CAS都是控制对同一个资源的访问冲突,而写时复制通过复制资源减少冲突。**适合读多,写少的场合,是一个很好的解决方案。
CopyOnWriteArraySet
看源码可知,CopyOnWriteArraySet是基于CopyOnWriteArrayList实现了,和HashSet和TreeSet相比,性能低下。
简单总结:写时复制,是计算机程序中一种重要的思维和技术。
ConcurrentHashMap
特点1-并发安全,直接支持一些原子复合操作,2,-支持高并发,读操作并行,写操作支持一定程度的并行。3-与同步容器synchronizedMap相比,迭代不用加锁,不会抛出ConcurrentModificationException,4,-弱一致性
Java7 hashMap 多线程下,会出现死循环问题(链表头插法的问题)
java8 修改为尾插法了,但是会出现并发修改数据被覆盖的情况
接口:java.util.concurrent.ConcurrentMap
ConcurrentHashMap实现高并发的基本机制
- 分段锁
- 读不需要锁
ConcurrentHashMap采用分段锁技术,将数据分为多个段,而每个段有一个独立的锁。
ConcurrentHashMap弱一致性
ConcurrentHashMap的迭代器创建后,就会按照哈希表结构遍历每个元素,但是在遍历过程中,内部元素可能会发生变化,如果变化发生在已经遍历过的部分,迭代器就不会反映出来,否则,将反映出来。
基于跳表的map和set
java并发包中与TreeMap和TreeSet对应的并发版本是:ConcurrentSkipListMap和ConcurrentSkipListSet。
特点
1-没有使用锁,所有操作都是无阻塞的。所有操作都是可以并行,包括写。
2-弱一致性。一些方法不是原子的,比如 putAll 以及clear
跳表是基于链表的,在链表的基础上加了多层索引结构。
ConcurrentSkipListMap 为了实现并发安全,高效,无锁非阻塞,实现非常复杂。
并发队列
无锁非阻塞:主要通过循环CAS实现并发安全。
阻塞队列:这些队列都使用了锁和条件。很多操作都是需要先获取锁或者满足特定条件,获取不到锁,会等待。
ConcurrentLinkedQueue的算法基于一篇论文,感兴趣的话,看下面链接,参考别人的博客
普通阻塞队列
注意:所有普通阻塞队列都是基于:ReentrantLock和显式条件Condition实现的。
例子:ArrayBlockingQueue的实现:一个数组存储元素,有不满和不空两个条件用于协作,和用显式条件实现生产者消费者模式类似。
异步任务执行服务
目的:将执行任务和提交任务分开来。,需要理解这种思维。
Runable和Callable --表示要执行的异步任务
Executor和ExecutorService – 表示执行服务
Future :表示异步任务的结果
Runable没有返回结果,且不会抛出异常。Callable则都会。
线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
使用过程注意点:
1-如果是有界队列,maxPoolSize 是无限的,则会创建过多的线程,占满CPU和内存
2-如果是无界队列,服务不了的task,总是在排队,导致内存占用过多。
注意:在任务量非常大的场景下,让拒绝策略有机会执行,是保证系统稳定的一个方面。
线程池的死锁:线程池任务之间有依赖,可能会出现死锁。
例子:任务A,在执行过程中,给同样的任务执行服务提交了一个任务B,但需要等待任务B结束。
解决:使用SynchronousQueue
多线程情况下,优先使用线程池。
定时任务
应用场景:
1-闹钟程序或者任务提醒
2-监控系统。比如每一段时间采集一下系统数据,对异常事件报警
3-统计系统。凌晨一定时间统计一下昨日的各种数据指标
Java实现方式
Timer和TimerTask,以及JUC包下的 ScheduledExecutorService
/**TimerTask 延时任务
* @author root
*/
public class TimerFixedDelay {
static class LongRunningTask extends TimerTask{
@Override
public void run() {
try {
Thread.sleep(5_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("long running finished");
}
}
static class FixedDelayTask extends TimerTask{
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
}
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new LongRunningTask(),10);
timer.schedule(new FixedDelayTask(),1000);
}
}
Timer内部主要由任务队列和Timer线程两部分组成,任务队列是一个基于堆实现的优先级队列,按照下次执行的时间排优先级。需要强调的是:一个Timer对象只有一个Timer线程。这也意味着,定时任务不能耗时太长,更不能无限循环。
/**
* Timer死循环
* */
public class EndlessLoopTimer {
static class LoopTask extends TimerTask{
@Override
public void run() {
while (true){
try {
// 模拟执行任务
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 永远没有机会执行
static class ExampleTask extends TimerTask{
@Override
public void run() {
System.out.println("hello");
}
}
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new LoopTask(),10);
timer.schedule(new ExampleTask(), 100);
}
}
第一个定时任务是一个无限循环,其后的定时任务,exampleTask将没有执行的机会。
非常重要的一点:当执行任何一个任务的run方法时,一旦run抛出异常,Timer线程会退出,从而所有定时任务都会取消。
总结:Timer以及TimerTask不建议使用。