本人新书出版,对技术感兴趣的朋友请关注:
https://mp.weixin.qq.com/s/uq2cw2Lgf-s4nPHJ4WH4aw
上1篇讲述了Java并发编程的第1个基本思想–CAS/乐观锁,这1篇接着讲述并发编程的第2个基本思想:CopyOnWrite
- CopyOnWrite基本思想
- CopyOnWriteArrayList
- CopyOnWriteArraySet
- AtomicReference的一个应用
- 总结
CopyOnWrite基本思想
CopyOnWrite,
顾名思义:就是在Write的时候,不直接Write源数据,而是把数据Copy一份出来改,改完之后,再通过悲观锁或者乐观锁的方式写回去。
那为什么不直接改,而是要拷贝一份改呢? 其实为了”读“的时候,不加锁!
下面就通过几个案例来展现CopyOnWrite的应用。
#CopyOnArrayList
和ArrayList一样,CopyOnArrayList的核心数据结构,也是一个数组,如下:
private volatile transient Object[] array;
下面是CopyOnArrayList的几个”读“函数:
final Object[] getArray() {
return array;
}
public E get(int index) { //没加锁
return (E)(getArray()[index]);
}
public boolean isEmpty() { //没加锁
return size() == 0;
}
public boolean contains(Object o) { //没加锁
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length) >= 0;
}
public int indexOf(Object o) { //没加锁
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length);
}
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
这些”读“函数,都没有加锁,那如何保证”线程安全“的呢?
答案就在”写“函数里面:
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); //CopyOnWrite, 写的时候,先把之前的数组拷贝一份出来
newElements[len] = e;
setArray(newElements); //把新数组,赋值给老数组
return true;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}
其他“写”函数,比如remove, 和add类似,在此不在详述。
#CopyOnArraySet
就是用Array实现的一个Set,保证所有元素都不重复。其内部就是封装的一个CopyOnArrayList
public class CopyOnWriteArraySet<E> extends AbstractSet<E>
implements java.io.Serializable {
private static final long serialVersionUID = 5457747651344034263L;
private final CopyOnWriteArrayList<E> al; //封装的CopyOnWriteArrayList
/**
* Creates an empty set.
*/
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
public boolean add(E e) {
return al.addIfAbsent(e); //不重复的,加进去
}
...
}
#AtomicReference的一个应用
问题的提出:
一个NumberRange的对象,如下所示:
public class NumberRange
{
private int lower;
private int upper;public int getRange()
{
return upper - lower;
}
在多线程情况下,如何不加锁,实现该对象的线程安全?
错误的方法1:使用AtomicInteger
public class NumberRange {
// INVARIANT: lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
// Warning -- unsafe check-then-act
if (i > upper.get())
throw new IllegalArgumentException(
"can't set lower to " + i + " > upper");
lower.set(i); //原子操作
}
public void setUpper(int i) {
// Warning -- unsafe check-then-act
if (i < lower.get())
throw new IllegalArgumentException(
"can't set upper to " + i + " < lower");
upper.set(i); //原子操作
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
上述的setLower(), setUpper()各自都是原子的,但是2者合在一起,并不是原子的。
也就是说,多个线程分别调用这2个函数的时候,并不能实现互斥访问。这将导致lower的值,可能大于upper的值。
##正确的方法:final + CopyOnWrite + 乐观锁
public class CasNumberRange {
//Immutable
private static class IntPair {
final int lower; //1. final类型,只能copy on write,不能直接更改
final int upper;
...
}
private final AtomicReference<IntPair> values =
new AtomicReference<IntPair>(new IntPair(0, 0));
public int getLower() { return values.get().lower; }
public int getUpper() { return values.get().upper; }
public void setLower(int i) {
while (true) {
IntPair oldv = values.get();
if (i > oldv.upper)
throw new IllegalArgumentException(
"Can't set lower to " + i + " > upper");
IntPair newv = new IntPair(i, oldv.upper); //2. CopyOnWrite
if (values.compareAndSet(oldv, newv)) //3. CAS乐观锁
return;
}
}
// similarly for setUpper
}
备注:把此问题推而广之,当我们想实现”不加锁,对一个大的对象实现线程安全访问时“, 就可利用CopyOnWrite + CAS乐观锁来达到同样的效果。
#总结
CopyOnWrite的主要目的是实现“写”加锁,“读”不加锁,从而提高并发度。在上面例子中,CopyOnWriteArrayList使用了CopyOnWrite + 悲观锁; NumberRange使用了CopyOnWrite + 乐观锁。
同时,文章当中提到了volatile和final的使用,这也是高并发编程的另一个重要思想,将在下1篇当中详细阐述。