Copy-On-Write(COW)
Copy-On-Write简称COW(不会产奶的奶牛),是一种用于程序设计中的优化策略。Copy On Write技术在Linux和文件系统中均有应用,Linux通过Copy On Write技术极大地减少了Fork的开销,文件系统通过Copy On Write技术一定程度上保证数据的完整性。(todo: 专门写一篇文章去阐述这个事情)
而在Java的concurrent包中也有他的身影,本节就是阐述concurrent包中的Copy On Write技术。
Copy-On-Write的基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。
concurrent包中含有依据COW的组件:CopyOnWriteArrayList和CopyOnWriteArraySet
什么是CopyOnWrite容器
CopyOnWrite容器即写时复制的容器。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。
-
添加元素的过程:
通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素(添加过程需要加锁),添加完元素之后,再将原容器的引用指向新的容器。
-
优点:
可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
-
缺点:
-
内存占用问题
在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象。如果这些对象占用的内存比较大,则会占用比较多的资源,造成长时间GC或频繁的GC。
压缩容器中的元素或使用其他并发容器,如ConcurrentHashMap
-
数据一致性问题
只能保证数据的最终一致性,不能保证数据的实时一致性
-
-
应用场景:
读多写少的并发场景。例子比如:
- 白名单,黑名单
- 商品类目的访问和更新场景
-
类库组件:CopyOnWriteArrayList和CopyOnWriteArraySet
CopyOnWriteArrayList和CopyOnWriteArraySet分别实现了List接口和AbstractSet接口,其中CopyOnWriteArraySet含有一个CopyOnWriteArrayList实例域,进而实现COW应用。因此它们两个行为的类似
实现原理(读/写)
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
......
Object[] newElements = Arrays.copyOf(current, len + 1);// 复制出新数组
newElements[len] = e; // 把新元素添加到新数组里
setArray(newElements);// 把原数组引用指向新数组
return true;
} finally {
lock.unlock();
}
}
...
final void setArray(Object[] a) {
array = a;//直接修改引用
}
以上代码是写元素,可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。
读元素的时候不需要加锁,如果读的时候有多个线程正在向容器添加数据,读还是会读到旧数据,因为写的时候不会锁住旧的容器
ConcurrentMap
todo: ConcurrentHashMap
todo: ConcurrentSkipListMap ==> ConcurrentSkipListSet
todo: 聊聊并发(四)深入分析ConcurrentHashMap
多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap
效率低下的HashTable容器, HashTable容器使用synchronized来保证线程安全,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态
ConcurrentHashMap的锁分段技术
ConcurrentHashMap的初始化
定位Segment
ConcurrentHashMap的get操作
ConcurrentHashMap的size操作
ConcurrentLinkedQueue
todo: ConcurrentLinkedQueue
todo: ConcurrentLinkedDeque
实现一个线程安全的队列有两种实现方式:一种是使用阻塞算法,另一种是使用非阻塞算法。
使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS的方式来实现,ConcurrentLinkedQueue是使用非阻塞的方式来实现线程安全队列的
BlockingQueue
todo: BlockingQueue
todo: 聊聊并发(七)——Java中的阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。(即生产和消费)
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
提供了7个阻塞队列。分别是
- ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- 队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口
- 可以用于:
- 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
- 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
阻塞队列的实现原理
使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。即使用await、signal实现
todo: 聊聊并发(七)——Java中的阻塞队列