ArrayList
这里记录一些线程与集合的内容,首先讲一下ArrayList
ArrayList底层是Object类型的数组,初始容量是10(jdk7之前,jdk8之后是空引用,到add之后会变成10,类似于懒加载的机制),其扩容的方式是每次扩容为之前的一半,比如10会扩容成15,15扩容成22,扩容用到的方法时Arrays的copyof方法,OK,接下来后进入主题。
- 先编写一段代码
List<String> list=new ArrayList<>();
public class _List不安全 {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
//3个线程
for (int i = 0; i < 30; i++) {
new Thread(()->{
//一边写入,一边读取对象
list.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
运行之后会报一个异常java.util.ConcurrentModificationException 并发修改异常,原因是多线程并发争抢统一资源,且没加锁
解决办法在下面
Vector
List<String> list=new Vector<>();
内部加锁,有 synchronized 方法,同一时间段只有一个个线程,效率低性能差
Collections
这是Connections的静态方法,传入一个ArrayList。
List<String> list= Collections.synchronizedList(new ArrayList<>());
CopyOnWriteArrayList
这个JUC包的类,实现了List接口,写时复制。底层是olatile Object[] array,其add方法里面加了锁ReentrantLock:
List<String> list= new CopyOnWriteArrayList<>();
写时复制
CopyOnWrite容器,即写时复制的容器。往一个容器添加元素时,不直接往当前容器object[]添加,而是先将当前容器object[]进行copy复制出一个新的容器object[] newElements,然后往新容器添加元素,添加完成之后,再将原容器的引用指向新容器setArray(newElements)
;
这样做的好处就是可以对CopyOnWrite容器进行并发的读而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的思想,读和写不同容器。
HashSet与HashMap
-
HashSet 扩展点知识点,HashSet底层数据结构是HashMap(源码构造器里面 new HashMap()),其add方法实际上return map.put(e,PRESENT)==null; PRESENT实际上就是一个object常亮,所以实际上就是HashMap的keys。
也就是说,hashset底层是hashmap的key,value默认添加object常量,固定写死,hashmap底层是数组链表红黑树,hashset的add()调用的就是hashmap的put()。同样,可以通过CopyOnWriteArraySet
或Collections.synchronizedSet(new HashSet<>())
解决,其构造器其实new CopyOnWriteArrayList();
-
HashMap
HashMap(无序无重复)底层是数组+链表(单向)+红黑树,HashMap存的是node,node里面存的是Key-Value,HashMap初始容量为16,负载因子为0.75(到16*0.75的时候扩容),初始容量和负载因子可以通过构造器来改,每次扩容为原值得一倍。
解决办法:
Map<String, String> map = new ConcurrentHashMap();
另外,在new HashMap的时候,可以指定初始值大小,即HashMap容器数组大小,这样做的好处是
- 如果确定map需要存放多少数据(例如1000),就直接指定1000的大小(通常是2幂)
- 这样就减少了使用初始大小(16)的map时,要不断扩容导致的性能下降问题