[JAVAEE] 面试题(四) - 多线程下使用ArrayList涉及到的线程安全问题及解决
目录
一. 多线程下使用ArrayList
1.1. 自行判断加锁
1.2 使用Collections.synchronizedList()套壳加锁
1.3 CopyOnWriteArrayList类
二. 总结
一. 多线程下使用ArrayList
多线程下使用ArrayList会涉及到线程安全问题, 例如:
public static void main(String[] args) throws InterruptedException{ List<Integer> list = new ArrayList<>(); Runnable runnable = () -> { for (int i = 0; i < 50000; i++) { list.add(i); } }; // 创建两个线程 for (int i = 0; i < 2; i++) { new Thread(runnable).start(); } Thread.sleep(5000); System.out.println(list.size()); }
可以发现, 最后的结果并不是期待的100000, 这是因为ArrayList中的add方法并没有处理线程安全问题.
那么, 如何解决在多线程场景下使用ArrayList产生的线程安全问题呢? 如下, 有三种解决方案.
1.1. 自行判断加锁
就是根据需要来进行加锁, 将一些可能会产生线程安全问题的操作通过加锁打包成为原子操作.
Runnable runnable = () -> {
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
list.add(i);
}
}
};
结果正如我们所期待的那样.
1.2 使用Collections.synchronizedList()套壳加锁
对读操作和写操作全部都加锁, 保证线程安全. (此时返回的List对象中的重要操作都加锁了)
但是会降低程序运行效率, 因为读操作本身不会产生线程安全问题, 又加锁就是多此一举了!!!
我们来看看synchronizedList类中add方法是怎么实现的,
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
此时, 结果也是正确的.
1.3 CopyOnWriteArrayList类
CopyOnWrite 写时拷贝, 实现了读与读不互斥, 读与写不互斥, 写与写互斥. 保证了线程安全.
那么, 是怎么实现读与写不互斥的呢?
在面临写操作的时候, 会依照旧数组复制一个新数组, 修改操作就在新数组上进行, 最后再将旧数组覆盖.如果在写操作的时候发生了线程切换, 并且切换到了读操作, 此时还是会读取旧数组. 保证了线程安全.
CopyOnWriteArrayList类中add方法的实现:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
结果也是正确的.
二. 总结
1. 多线程下使用ArrayList类, 涉及到了线程安全问题, 以及解决线程安全问题的方法.
2. 自行判断加锁. 效率高
3. Collections.synchronizedList()套壳封装, 效率低下. 因为对不涉及线程安全问题的操作进行加锁.(即对List接口中的所有方法进行加锁)
4. CopyOnWriteArrayList类, 写时拷贝. 实现了读与读, 读与写操作不互斥, 写与写操作互斥. 保证了线程安全, 并且效率相对高效.