一、高效读取:CopyOnWriteArrayList
个人总结:适用于读多写少场景。另外读写操作未加锁,可能会读取一些修改未完成的脏数据,只能保证最终一致性,不能满足实时性要求:线程并发读读不加锁,读写不加锁,写写时copy份原数组,在副本里修改,修改完后再将副本数据替换回原数据。
在很多应用场景中,读操作可能会远远大于写操作。比如,有些系统级别的信息,往往只需要加载或者修改很少的次数,但是会被系统内所有模块频繁的访问。对于这种场景,我们最希望看到的就是读操作可以尽可能地快,而写即使慢一些也没有太大关系。
由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问List的内部数据,毕竟读取操作是安全的。根据读写锁的思想,读锁和读锁之间确实也不冲突。但是,读操作会受到写操作的阻碍,当写发生时,读就必须等待,否则可能读到不一致的数据。同理,如果读操作正在进行,程序也不能进行写入。
为了将读取的性能发挥到极致,JDK中提供了CopyOnWriteArrayList类。对它来说,读取是完全不用加锁的,并且更好的消息是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。那它是怎么做的呢?
从这个类的名字我们可以看到,所谓CopyOnWrite就是在写入操作时,进行一次自我复制。换句话说,当这个List需要修改时,我并不修改原有的内容(这对于保证当前在读线程的数据一致
下面的代码展示了有关读取的实现:
private volatile transient Object[] array; public E get(int index) { return get(getArray(), index); } final Object[] getArray() { return array; } private E get(Object[] a, int index) { return (E) a[index]; }
和简单的读取相比,写入操作就有些麻烦了:
01 public boolean add(E e) { 02 final ReentrantLock lock = this.lock; 03 lock.lock(); 04 try { 05 Object[] elements = getArray(); 06 int len = elements.length; 07 Object[] newElements = Arrays.copyOf(elements, len + 1); 08 newElements[len] = e; 09 setArray(newElements); 10 return true; 11 } finally { 12 lock.unlock(); 13 } 14 }
首先,写入操作使用锁,当然这个锁仅限于控制写-写的情况。其重点在于第7行代码,进行了内部元素的完整复制。因此,会生成一个新的数组newElements。然后,将新的元素加入newElements。接着,在第9行,使用新的数组替换老的数组,修改就完成了。整个过程不会影响读取,并且修改完后,读取线程可以立即“察觉”到这个修改(因为array变量是volatile类型)