今日份鸡汤:那些你暂时不能战胜的,不能克服的,不能容忍的,不能宽容的,就告诉自己,凡是不能杀死你的,最终都会让你更强~
选择哪个其实就看什么场景,说白了,你不需要考虑线程安全问题,那就直接用ArrayList呗,不加锁,效率高,但是遇到多线程时,你就要考虑线程安全的问题了,来看看下面的code:
public class ListDemo {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for(int i = 0; i < 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 10));
System.out.println(Thread.currentThread().getName() + list);
}).start();
}
}
}
很简单的一段代码,但是再来看看结果:
Exception in thread "28" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at java.util.AbstractCollection.toString(AbstractCollection.java:461)
at java.lang.String.valueOf(String.java:2994)
at java.lang.StringBuilder.append(StringBuilder.java:131)
at com.wyn.demo.ListDemo.lambda$main$0(ListDemo.java:14)
at java.lang.Thread.run(Thread.java:748)
那么如何解决呢?
我们比较熟悉的是使用 Vector
先来看看二者源码对比
ArrayList源码:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
Vector源码:
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
非常明显的区别, Vector add方法加了synchronized锁。而List没有。
Vector 加了方法级别的锁,性能比较差,要是不用他还可以用哪个呢?
使用集合工具类 Collections.synchronizedList(new ArrayList<>()) 就可以创建一个安全的list,看看源码是怎么实现的
Collections.synchronizedList源码:
public boolean add(T e) {
synchronized(mutex) {
return backingList.add(e);
}
}
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
从这个实现上来看不能看出,他的性能也并不是那么高,而我们工作场景中有很多读多写少的场景,有没有适合这种场景的,并且能保证线程安全呢?
答案一定是肯定的,介绍两个并发包里面的并发集合类:
java.util.concurrent.CopyOnWriteArrayList
java.util.concurrent.CopyOnWriteArraySet
CopyOnWrite集合类也就这两个,Java 1.5 开始加入 ,你值得拥有,采用了读写分离思想,写时复制出来一份副本,读不加锁,这时读的还是原版本的数据,而写则是往copy出来的副本写,当写完后将指针指向这个副本,读多写少的场景建议使用他,毕竟效率高嘛,但是如果写多读少,使用这个就没意义了,因为每次写操作都要进行集合内存复制,性能开销很大,如果集合较大,很容易造成内存溢出。
CopyOnWriteArrayList源码:
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);
newElements[len] = e;
// 替换原始集合为新集合
setArray(newElements);
return true;
} finally {
// 释放锁
lock.unlock();
}
}
public E get(int index) {
return get(getArray(), index);
}
CopyOnWriteArraySet
CopyOnWriteArraySet逻辑就更简单了,就是使用 CopyOnWriteArrayList 的 addIfAbsent 方法来去重的,添加元素的时候判断对象是否已经存在,不存在才添加进集合。
CopyOnWriteArraySet源码:
/**
* Appends the element, if not present.
*
* @param e element to be added to this list, if absent
* @return {@code true} if the element was added
*/
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
同理map 和 set 也存在不安全问题呢,解决方式也类似哦,好了,这篇就到这了。
网友评论