JAVA中的copy-on-write容器

实际开发中,对许多数据结构的使用,很多情况下都是读多写少的。在多线程中,读操作并不会改变数据,所以并发读并不会影响线程安全,需要着重考虑的是并发写,所以读操作线程和写操作线程分离会提高多线程效率。

JDK1.5之后出现了copy-on-write原则,即写时复制。其核心思想是,有线程使用容器中的数据时,如果是写入,则复制出一个新容器,修改新容器中的数据后,再将引用指向新容器。如果是读操作则正常读引用地址中的容器数据。这里需要注意的是,复制新容器时底层通过copyof()方法来实现的,这是一个深层复制方法,即新容器是一个新的对象,在堆中有自己的内存区域,操作新容器不会影响旧容器,当然这也会增加内存开销。所以在复制的时候也加了锁,否则多线程会复制出多个对象占用内存,很容易导致OOM。

JAVA的concurrent包中提供了两种copy-of-write原则容器,即copyOnWriteArrayList和copyOnWriteArraySet。copyOnWriteArraySet其实是通过copyOnWriteArrayList来实现的,其底层有copyOnWriteArrayList对象,增加了不允许有重复元素的逻辑,所以额外提供了addIfAbsent()和addAllAbsent()两个添加元素的方法,我想着重总结一下copyOnWriteArrayList的底层实现。

其add()方法,即写入操作通过copyof()方法深层复制一个新数组写入,再把原引用指向新数组。同时加了lock,防止多线程并发创建多个对象占用内存。

JAVA中的copy-on-write容器

其get()方法,即读取操作没有加锁,多线程下不能保证数据实时一致性。

JAVA中的copy-on-write容器

 copy-on-write原则容易应用场景很多,比如黑名单,白名单这类读多写少的数据结构应用,而且可以批量写入,效率更高。

但是copy-on-writer容器缺点也很明显。前文提到过这里总结下,一是深层复制会增加内存开销,二是只能保证数据最终一致性,不能保证数据实施一致性,即写入的数据不一定会立马被读到,因为引用地址可能还没有来得及改变。

针对第二个缺点,可以用并发包中的读写锁来改进,如ReadWriteLock,ReentrantReadWriteLock以及StampedLock等。ReentrantReadWriteLock是对ReadWriteLock的优化,即有再入锁机制。他们的读与写两个操作是通过两个不同的锁来控制的,而且如果写锁被锁定是无法获得读锁的,即写入的时候不让读,这样既实现了读写分离又实现了数据实时一致性。StampedLock又对读操作进行了优化,在锁定读锁前有一个尝试读的操作,然后判断数据是否被修改,如果数据被修改了,则获得读锁再读一次,如果没有被修改,就直接返货尝试读的结果,可以不用获取读锁,大大提高了效率。

JAVA中的copy-on-write容器

总结一下, ReentrantReadWriteLock读写操作都加了锁,虽然读操作比copy-on-write容器要慢,但是可以保证数据实时一致。由于写操作比读操作要慢,所以比传统的读写不分离一起加锁效率要高。StampedLock的读操作已经很接近copy-on-write容器的无锁操作了,又快又可以保证数据实时一致。