为什么Vector和Collections.SynchronizedList的get方法要加锁呢?
1. 线程不安全的ArrayList
为什么说ArrayList是线程不安全的:
- add()操作抛出数组越界异常;
- add()操作会丢失元素;
- set()操作去修改元素,get()操作去获取元素时,可以读到新值也可能读到旧值,无法保证一致性。
源码分析:
//存放list集合元素的数组,默认容量10
transient Object[] elementData;
//list大小
private int size;
add()的源码:
public boolean add(E e) {
//确定添加元素之后,集合的大小是否足够,若不够则会进行扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//插入元素
elementData[size++] = e;
return true;
}
场景1:多个线程都没进行扩容,但是执行了elementData[size++] = e;
时,便会出现“数组越界异常”;
场景2:因为size++本身就是非原子性的,多个线程之间访问冲突,这时候两个线程可能对同一个位置赋值,就会出现“size小于期望值的结果”;
2. Vector和Collections.SynchronizedList的get方法要加锁呢?
get()操作时集合中的元素不能并发的被修改,否则就易出现数据问题。
- Vector和Collections.SynchronizedList的get方法加了synchronized后可以保证顺序性与实时一致性,当一个线程在读取数据时,一定可以看到其他线程解锁前写入的全部数据。
- 并且Vector和Collections.SynchronizedList的数组并没有用volatile修饰,如果不加锁,也无法保证可见性。
3. 线程安全的3种List集合
//方法上使用sync关键字(读写均加锁)
Vector vector = new Vector();
//写操作每一次均copy一个数组,读操作不加锁(写加锁性能低,读不加锁性能极高)
CopyOnWriteArrayList<Integer> r2 = new CopyOnWriteArrayList<>();
//使用sync代码块装饰传入List的读写操作(读写均加锁)
List<String> r3 = Collections.synchronizedList(new ArrayList<>());
- Vector/Collections.synchronizedList:读写均加锁,来实现线程安全;
- CopyOnWriteArrayList基于写时复制技术实现的,读操作无锁(读取快照),写操作有锁,体现了读写分离的思想,但是无法提供实时一致性。
4. 并发安全的案例
下面给出一个案例,即容易出现并发问题的场景:
public class TestList {
private static final ExecutorService VIEW_EXECUTOR = new ThreadPoolExecutor(2,4,1000,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(2));
/**
* 目前比较常用的构建线程安全的List有三种方法:
* <p>
* 使用Vector容器
* 使用Collections的静态方法synchronizedList(List< T> list)
* 采用CopyOnWriteArrayList容器
*/
public static void main(String[] args) {
//常用方式:使用线程池并发处理,填充结果
ArrayList<Object> res = new ArrayList<>();
CompletableFuture.runAsync(() -> {
//todo 逻辑处理
//线程不安全,需要使用一个线程安全的List,这里推荐Collections.synchronizedList
res.add("success");
}, VIEW_EXECUTOR);
}
}