ArrayList用的太多了,几乎所有人都知道它是线程不安全的,但实际使用中,我们的多线程实现,普遍都是基于一些同步方法或者锁,很多场景其实并不需要关注ArrayList本身的线程安全。网上可以找到三种主流的实现ArrayList线程安全的手段,他们分别是什么样的思路,还是值得简单了解和记录的。

Vector

        Vector 是矢量队列,它是JDK1.0版本添加的类,历史比ArrayList(since 1.2)更为悠久。其具体历史已不太可考,个人只能简单猜测是在java初期从其他语言直接借鉴的名字及相关概念。其底层和ArrayList一样是数组,除线程安全外,大多数实现方法和ArrayList逻辑基本一致。值得一提的是,从jdk1.2以后,Java提供了系统的集合框架,就将Vector改为实现List接口,从而导致Vector里有一些重复的方法,例如:addElement(Object obj),实际上这个方法和add(Object obj)没什么区别。

        Vector 的实现,就是在方法上都加上synchronized(即使get也不例外)。康康部分源码。

public synchronized E get(int index) {
	if (index >= elementCount)
		throw new ArrayIndexOutOfBoundsException(index);
	return elementData(index);
}

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

关于Vector为什么被弃用

  1. 所有方法都有同步开销,非多线程下,效率不如ArrayList;
  2. 一些老代码,导致有重复的方法,以及风格和新的集合类格格不入;
  3. 线程安全的实现,可以通过新的Collections.synchronizedList之类的调用来替换。

Collections.synchronizedList

        基于集合类的实现,可以简单的使用函数来操作,例如,List<Integer> list2 = Collections.synchronizedList(new ArrayList<Integer>())。先看看函数的定义。

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
    	new Collections.SynchronizedRandomAccessList<>(list) :
    	new Collections.SynchronizedList<>(list));
}

        显然,里面还藏了一些list类,我们以SynchronizedRandomAccessList接着看看这个类是如何操作的。

//Collections中的静态类1号,主要实现在SynchronizedList中
static class SynchronizedRandomAccessList<E>
	extends Collections.SynchronizedList<E>
    implements RandomAccess {
        ... //绝大部分方法都没有单独的实现
}
//Collections中的静态类2号
static class SynchronizedList<E>
        extends Collections.SynchronizedCollection<E>
        implements List<E> {
    private static final long serialVersionUID = -7754090372962971524L;

    final List<E> list;

		...

    //可以看到,大部分方法的处理方式,是多了一个synchronized (mutex),
    //mutex是SynchronizedCollection中的一个Object变量
    public boolean equals(Object o) {
        if (this == o)
            return true;
        synchronized (mutex) {
            return list.equals(o);
        }
    }

    public int hashCode() {
        synchronized (mutex) {
            return list.hashCode();
        }
    }

    public E get(int index) {
        synchronized (mutex) {
            return list.get(index);
        }
    }

    public E set(int index, E element) {
        synchronized (mutex) {
            return list.set(index, element);
        }
    }

    public void add(int index, E element) {
        synchronized (mutex) {
            list.add(index, element);
        }
    }

    public E remove(int index) {
        synchronized (mutex) {
            return list.remove(index);
        }
    }

    public int indexOf(Object o) {
        synchronized (mutex) {
            return list.indexOf(o);
        }
    }

    public int lastIndexOf(Object o) {
        synchronized (mutex) {
            return list.lastIndexOf(o);
        }
    }

    public boolean addAll(int index, Collection<? extends E> c) {
        synchronized (mutex) {
            return list.addAll(index, c);
        }
    }

...

    //官方注释明确提出,对于使用 Iterator遍历列表时,Collections.synchronizedList可能发生错误
    //还需要手动去确保线程安全
    public ListIterator<E> listIterator() {
        return list.listIterator(); // Must be manually synched by user
    }

    public ListIterator<E> listIterator(int index) {
        return list.listIterator(index); // Must be manually synched by user
    }
}

CopyOnWriteArrayList

        CopyOnWriteArrayList是1.5后引入,属于JUC的一部分。他基本的原理还是和ArrayList一样,涉及线程安全的部分,是通过写时复制的方式来实现(从名字中就可以看出)。它内部有个volatile数组来保持数据。在“添加/修改/删除”数据时,会先获取互斥锁,再新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给volatile数组,然后再释放互斥锁。以get和set为例看看代码。

public class CopyOnWriteArrayList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    //互斥锁
    final transient ReentrantLock lock = new ReentrantLock();

    //存储数据的volatile数组,入口仅限于类中函数getArray/setArray。
    private transient volatile Object[] array;

    public E get(int index) {
        return get(getArray(), index);
    }

    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
                int len = elements.length;
				//直接开始复制一格数组,修改某个值,将型数组赋值给CopyOnWriteArrayList中的array
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                // 官方介绍为,并非无效的操作,是为了保证volatile的写语义。
                // 这里有点搞心态,翻译一下参考文档6,按照其中一个回答,这里的代码jdk 8中存在,jdk 11中已经移除。但我在jdk 14中又再次发现。
                // 参考热门回答,主要是为了确保一些逻辑中CopyOnWriteArrayList外的非 volatile变量,由于指令重排导致的执行顺序问题。
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }
}

因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。

性能对比

        我尝试的代码主要来自参考文档4,就不再贴出来了。

CopyOnWriteArrayList add method cost time is 17156
Collections.synchronizedList add method cost time is 23
Vector add method cost time is 5
CopyOnWriteArrayList get method cost time is 2
Collections.synchronizedList get method cost time is 4
Vector get method cost time is 5
---- 10万级数据

CopyOnWriteArrayList add method cost time is 133
Collections.synchronizedList add method cost time is 1
Vector add method cost time is 0
CopyOnWriteArrayList get method cost time is 0
Collections.synchronizedList get method cost time is 0
Vector get method cost time is 1
---- 万级数据

        以上代码又多跑了几次,大同小异。

        结论:

  1. 写时CopyOnWriteArrayList性能较差,且随着数据量的增大,几何级下跌。读操作各方式基本没有区别。
  2. CopyOnWriteArrayList,适用于以读为主,读操作远远大于写操作的场景中使用,比如缓存。
  3. Collections.synchronizedList则可以用在CopyOnWriteArrayList不适用,但是有需要同步的地方使用, 比如读写操作都比较均匀的地方。
  4. 不得不含泪承认,从简单的几次跑数中,Vector的读写都很优秀。但既然已经不建议使用,就忘了它吧。