微信公众号:MyClass社区
如有问题或建议,请公众号留言
1.ArrayList并发测试
写在前边,越是简单的数据结构,我们有时候就是容易忽略。并发下的ArrayList会出现什么情况,大家可能会说因为并发扩容可能抛数据越界异常,真的是这样吗?我们进行简单测试,分析一下:
@Test
public void conTestArrayListSizeAdd() throws Exception {
List<Integer> list = new ArrayList<>();
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int threadNum = 0; threadNum < 10; threadNum++) {
executorService.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
list.add(size++);
}
}
});
}
Thread.sleep(1000);
System.out.println("size"+list.size()+",list:"+list);
executorService.shutdown();
}
预期结果:
实际情况:发现很多都是空,这是啥情况?这里添加了一个size++,进行比较衬托效果。
分析:
//添加一个元素
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//扩容1.5倍 (oldCapacity + oldCapacity >> 1)
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
其中很明显的线程不安全的非原子操作:size++,扩容过程中,数组会扩充很多null数据槽,elementData[size++]操作会填充数据,所以不难分析出这个结果:扩容之后,size++并发下赋值的位置就会错乱。数组越界其实分析并发下也是会出现的,但是跑了很多我这没有出现,大家可以自己测试一下。
2.再看看LinkedList
LinkedList是基于双向链表实现的,所以它的特征基本上是链表相关的特性:
//1.双链表结构
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
//2.添加元素
public boolean add(E var1) {
this.linkLast(var1);
return true;
}
//last节点赋值
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
//3.get获取节点node(index)
Node<E> node(int index) {
//判断是从前遍历还是从尾部遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
LinkedList添加元素时,会在尾节点进行替换,或者add(index,e) ,set(index,e)都是在相应的节点进行替换,所以并发情况下,节点赋值可能被覆盖,同样会出现错乱的现象。
//4.往链表中间插入元素
public void add(int var1, E var2) {
this.checkPositionIndex(var1);
if (var1 == this.size) {
this.linkLast(var2);
} else {
//如果不是尾节点,需要获取遍历链表获取index的节点,
this.linkBefore(var2, this.node(var1));
}
}
//插入元素,替换old节点的前节点为newNode,old的前节点的next为newNode,完成插入
void linkBefore(E var1, LinkedList.Node<E> var2) {
LinkedList.Node var3 = var2.prev;
LinkedList.Node var4 = new LinkedList.Node(var3, var1, var2);
var2.prev = var4;
if (var3 == null) {
this.first = var4;
} else {
var3.next = var4;
}
++this.size;
++this.modCount;
}
LinkedList 添加元素:add(index,e),则会遍历链表获取index节点的位置,所以插入的时候也是需要遍历index节点位置,再进行节点前后替换,完成插入操作,所以LinkedList的插入和索引效率不能一概而论,以偏概全。
3.简单总结
1.ArrayList底层是一个动态Object数组,数组默认大小,初始值为10,如果需要扩容则会扩充1.5倍。1.6是(原size×3)/2。,后1.7之后优化为(原size+原size>>1);
2.ArrayList线程不安全,会有并发扩容问题,多线程赋值也会出现不一致,数组越界异常,还有会出现null现象;
3.LinkedList基于双链表的数据结构,线程不安全。
ArrayList&LinkedList索引和插入效率分析:
索引:对于随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要遍历链表来获取节点时间复杂度是O(n),而arrayList直接是数组下表定位,时间复杂O(1)。所以LinkedList的get就相对非常消耗资源,除非位置离头部尾很近,遍历时间很快。
插入:对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList往中间add操作或者需要扩容操作是调用Array.copy复制数组,Array.copy是操作系统的内存操作,很耗性能;LinedList只需要在最后一个节点挂上节点,或者中间添加或者删除,都只是前后节点的引用赋值的改变,除了遍历没耗性能的操作。
4.并发怎么用
以上jdk自带arrayList和linkedList都是线程不安全的,如果非要在并发情况下使用这两种数据结构:
1.建议加锁来保证线程安全;
2.或者使用Vector,方法都加了synchronized synchronized保证了线程安全,但是1.6以后基本都不再使用了;
3.还有一种方式可以 new SynchronizedCollection(List),或者使用Collections.synchronizedList(new LinkedList()),本质都是加锁;
4.以上都是利用synchronized进行加锁保证的,性能不是最好的,一般的建议使用下面并发阻塞队列操作,例如ConcurrentLinkedQueue阻塞队列,底层实现CAS算法操作,保证了线程安全。
欢迎关注微信公众号:MyClass社区,查看精彩历史。
本文分享自微信公众号 - MyClass社区(MyClass_ZZ)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
来源:oschina
链接:https://my.oschina.net/u/4591203/blog/4410935