集合类不安全之ArrayList
1. 不安全的ArrayList
大家都知道ArrayList线程不安全,怎么个不安全法呢?上代码:
public class ContainerNotSafeDemo {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
for (int i = 0;i<5;i++){
new Thread(()->{
list.add(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName()+"\t"+list);
}).start();
}
}
}
//运行结果如下: 多个线程同时修改列表的元素,产生了并发修改异常
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
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 juc.ContainerNotSafeDemo.lambda$main$0(ContainerNotSafeDemo.java:26)
at java.lang.Thread.run(Thread.java:748)
为啥呢?看一下add()方法的源码:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
可以看到仅仅是在扩容和添加操作,并没有任何的线程安全控制。所以在实际的高并发场景下,ArrayList的应用很有局限。
2. 安全的解决方式
2.1 使用Vector解决
注意到,ArrayList的add方法并没有任何保证线程安全的机制 ~ 所以不安全了。怎么解决呢?首先想到的是加锁,凑巧的是Vector已经为我们提供了安全的add方法:
public class ContainerNotSafeDemo {
public static void main(String[] args) throws InterruptedException {
List<String> list = new Vector<>();//修改为Vector对象
for (int i = 0;i<3;i++){
new Thread(()->{
list.add(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName()+"\t"+list);
}).start();
}
}
}
//结果如下:
Thread-0 [Thread-0]
Thread-2 [Thread-0, Thread-2]
Thread-1 [Thread-0, Thread-2, Thread-1]
这么看来,在这种情况下多线程的添加操作是没有任何问题的?那么,这么做真的可取嘛?尝试查看Vector的add()方法源码:
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
可以看到:Vector使用了synchronized关键字来保证线程安全。可是为了添加一个的操作,加了个重锁,这样做在多线程环境下会造成严重的资源浪费与性能损耗!!高并发情况下是万万不可取的。矛盾来了:ArrayList可以提升并发性,但是牺牲了线程安全性,而Vector恰恰与之相反。所以,我们在不同的场合下可以根据业务需求有所取舍。
### 2.4 使用Collections解决
一个神奇的工具类——Collections,看一下它的结构:

可以看到这个工具类提供了SynchronizedList、SynchronizedMap、SynchronizedSet等看名字就很安全的类,怎么实现的嗫?看Collections.synchronizedList(List list) 方法源码:
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
我们在传值的时候传的是ArrayList的对象,ArrayList它又实现了RandomAccess,所以返回 new SynchronizedRandomAccessList<>(list, mutex)对象,但是:
static class SynchronizedRandomAccessList<E>
extends SynchronizedList<E>
implements RandomAccess {
...
}
//它又继承了SynchronizedList,所以返回的还是SynchronizedList对象
看SynchronizedList它的源码怎么写:
static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E> {
...
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
...
...
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);}
}
...
}
可以看到,SynchronizedList 的实现里,get, set, add 等操作都加了 mutex 对象锁,再将操作委托给最初传入的 list。mutex来自哪里?
SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
mutex = this; //mutex就是这个list本身咯~
}
### 2.4 使用CopyOnWriteArrayList解决
还是下边这段代码,进行了一下简单的修改:
public class ContainerNotSafeDemo {
public static void main(String[] args) throws InterruptedException {
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0;i<3;i++){
new Thread(()->{
list.add(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName()+"\t"+list);
}).start();
}
}
}
使用到了new CopyOnWriteArrayList<>();,字面意思看来时:写时复制集合。
先来了解一下Copy-On-Write(写时复制技术):通俗的讲,写时复制技术就是不同进程访问同一资源的时候,只有在写操作,才会去复制一份新的数据,否则都是访问同一个资源。
CopyOnWriteArrayList,是一个写入时复制的容器,它是如何工作的呢?简单来说,就是平时查询的时候,都不需要加锁,随便访问,只有在写入/删除的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。这点要跟读写锁区分一下。
那么java里面是如何实现的,看源码:
/**
* Appends the specified element to the end of this list.【添加元素至列表末尾】
* @param e element to be appended to this list 【e:新增的元素】
* @return {@code true} (as specified by {@link Collection#add})
*/
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); //拷贝一份旧容器,并且扩容+1
newElements[len] = e; //填充元素
setArray(newElements); //新的集合列表替换掉旧的
return true;
} finally {
lock.unlock();
}
}
我们看到Java中JDK的源码实现其实也是非常的简单,往一个容器里添加元素的时候,不直接往当前容器object[]添加,而是先将当前容器object[]进行cpoy,复制出来一个新的容器object[] newElements,然后往新的容器newElements里面添加元素。添加完成之后再将原容器的引用指向新的容器setArray(newElements);。
优点:对于一些读多写少的数据,这种做法的确很不错,对容器并发的读不需要加锁,因为此时容器内不会添加任何新的元素。所以CopyOnWriteArrayList也是一种读写分离的思想,读和写操作的是不同的容器。
缺点:这种实现只保证数据的最终一致性,在副本未替换掉旧数据时,读到的仍然是旧数据。如果对象比较大,频繁地进行替换会消耗内存,从而引发频繁的GC,此时,应考虑其他的容器,例如ConcurrentHashMap。
集合类不安全之ArrayList的更多相关文章
- java常用集合类:Deque,ArrayList,HashMap,HashSet
图一:java collection 类图 Queue家族 无论是queue还是stack,现在常用的是Deque的实现类:如单线程的ArrayQueue,多线程的ArrayBlockingQueue ...
- JDK8集合类源码解析 - ArrayList
ArrayList主要要注意以下几点: 1构造方法 2添加add(E e) 3 获取 get(int index) 4 删除 remove(int index) , remove(Objec ...
- java集合类学习笔记之ArrayList
1.简述 ArrayList底层的实现是使用了数组保存所有的数据,所有的操作本质上是对数组的操作,每一个ArrayList实例都有一个默认的容量(数组的大小,默认是10),随着 对ArrayList不 ...
- java集合类,HashMap,ArrayList
集合类 Collection LinkedList.ArrayList.HashSet是非线程安全的, Vector是线程安全的; ArrayXxx:底层数据结构是数组,连续存放,所以查询快,增删慢. ...
- C# - 集合类
C#的集合类命名空间介绍: // 程序集 mscorlib.dll System.dll System.Core.dll // 命名空间 using System.Collections:集合的接口和 ...
- java基础 集合类
java集合类主要有以下集中: List结构的集合类: ArrayList类, LinkedList类, Vector类, Stack类 Map结构的集合类: HashMap类,Hashtable类 ...
- java集合类的学习(一)
为何要用集合类:可以储存不同类型的数据,可以进行动态的删除和修改,不用考虑数组越界的问题. 软件开发常用的集合类:Vector,ArrayList,Stack,HashMap,Hashtable. 3 ...
- Set,List,Map,Vector,ArrayList的区别(转)
JAVA的容器---List,Map,Set Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set Map ├Hashtab ...
- Java集合类源码分析
常用类及源码分析 集合类 原理分析 Collection List Vector 扩充容量的方法 ensureCapacityHelper很多方法都加入了synchronized同步语句,来保 ...
随机推荐
- SQL学习记录:函数(二)
字符串函数 1.获取字符的ASCII码 语法结构: ASCII(espression) 这里的expression是一个返回char或varchar数据类型的表达式,ASCII函数仅对表达式最左 ...
- Eclipse对web项目设置请求路径(与项目名称不同)
可以在下图位置,进行修改 也可以修改项目的路径下的.settings文件夹下的org.eclipse.wst.common.component的value属性 <property name=&q ...
- Fatal Error -26000: Not enough memory (12320 bytes) for “new buffer in LrwSrvNetTaskIt 问题解决及lr脚本心得
Fatal Error -26000: Not enough memory (12320 bytes) for “new buffer in LrwSrvNetTaskIt 问题解决及lr脚本心得 2 ...
- ubuntu下MySQL的安装及远程连接配置(转)
1.命令窗口中输入sudo apt-get install mysql-server mysql-client 即可(配置文件位置:/etc/mysql/my.cnf 启动文件位置:/etc/init ...
- Robot Framework +钉钉通知(Dingding[钉钉] Plugin)构建通知
1.点击钉钉个人头像进入[机器人管理] 2.添加自定义机器人 3.创建机器人,选择通知群 4.完善机器人信息 5.复制机器人token(只需要连接access_token后面token) 6.进入je ...
- Linux GNU GAS introduction
Linux汇编语法简介(GNU GAS) 声明:本教程不是介绍汇编语言和CPU指令的书籍,只是对gas的用法做一简单介绍.市面上所有讲汇编的书都是在微软的环境下,使用的是Intel的语法格式, ...
- 关于she'll脚本中"echo -e"使用sh命令执会显示参数"-e"-的问题
今天尝试写了个简单的菜单shell脚本文件: clear echo echo -e "\t\t\tSys Admin Menu\n" echo -e "\t1. Disp ...
- Java技术中的三大特性
1.虚拟机 Java虚拟机JVM(Java Virtual Machine)在Java编程里面具有非常重要的地位,约相当于前面学到的Jav ...
- Linux Shell脚本在service模式下的环境变量
普通Shell脚本在终端下运行时,环境变量来自于init/profile/.bash_profile等,就是在当前终端下可以得到的环境,比如PATH.在service模式下,脚本运行在受限的环境中:比 ...
- mysql安装与修改密码
数据库基本概念:数据的仓库 数据库服务器-->数据库-->表-->记录-->属性(列,字段) unix下数据库服务安装: apt-get install -y mysql-se ...