java 集合类的线程安全
java valueof集合类的线程安全
为什么不是线程安全的
我们都知道在java中,经常会⽤到三⼤集合类Set,List,Map。但是像ArrayList, HashMap,HashSet这些常⽤的集合类是线程不安全的。在⾼并发的场景下使⽤这些集合类会导致很多的问题,⽐如丢失数据,数据的不⼀致性等等,甚⾄导致异常,给⽣产环境带来严重的损失。⾸先我们以List集合来举⼀个例⼦,来看看会导致的问题。
来看⼀下运⾏结果:
可能会出现空的情况,也可能丢失数据,也有可能正常,这个只是3个线程同时访问,我们加⼤线程的数量到30(代码省略,只需将for循环的3改成30即可!),就会报错:java.util.ConcurrentModificationException
当然,Set和Map集合的线程不安全也是⼀样的,下边看⼀下两者线程不安全的体现。
Set: List <String > list = new ArrayList <>(); // 3个线程向list 集合中添加数据 for (int i = 0; i < 3; i ++)
{ new Thread (() -> { list .add (UUID .randomUUID ().toString ().substring (0,4)); System .out .println (list ); }).start (); }
1
2
3
4
5
6
7
8第⼀种运⾏结果[null, e6bd ][null, e6bd ][null, e6bd ]第⼆种运⾏结果[ce2f, 0c63, 3ce0][ce2f, 0c63, 3ce0][ce2f, 0c63, 3ce0]第三种运⾏结果[55fe, e03f ][55fe, e03f ][55fe, e03f ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14Exception in thread "Thread-2" Exception in thread "Thread-3" java.util.ConcurrentModificationException at java.util.ArrayList $Itr .checkForComodification (ArrayList.java:911) at java.util.ArrayList $Itr .next (ArrayList.java:861) at java.String (AbstractCollection.java:461) at java.lang.String.valueOf (String.java:2994) at java.io.PrintStream.println (PrintStream.java:821) at com.chunqiu.learn.Test1.lambda $main$0(Test1.java:20) at java.lang.Thread.run (Thread.java:748)
1
2
3
4
5
6
7
8
9
Map:
当然,报错都是java.util.ConcurrentModificationException。
出错原因
这个异常翻译成中⽂就是并发修改异常。为什么会报这样的⼀个异常呢?
⾸先先举⼀个⽣活中的例⼦:假如我们进考场签到,进考场之前,我们需要签到,每个⼈在花名册上签上⾃⼰的名字,假如张三正在签到,但是忘了⾃⼰的名字怎么写了,签的很慢,后边的李四等不及了,很嫌弃张三,就从张三的⼿中抢签到的笔,此时张三正在写字,这样就容易导致在争抢的过程中在花名册上留下⼀道长长的笔迹。这个由于多个线程同时在争抢添加操作所导致的异常就是我们的
java.util.ConcurrentModificationException。
我们来看⼀下List中的添加⽅法。
ArrayList的add⽅法为了保证效率并没有加锁,⽆法保证线程安全。那么应该如何保证线程的安全性呢?
⼆、如何保证线程安全
List 的线程安全
List保证线程安全有三种⽅式:
1. 使⽤线程安全的⼦类:Vector。 Vector的⽅法使⽤synchronized修饰,进⾏了加锁,可以保证线程安全。
2. 使⽤集合的⼯具类:Collections。Collections可以创建线程安全的集合类。Collections.synchronizedList(new ArrayList<>())
3. 使⽤JUC包中的⼀个类:CopyOnWriteArrayList。
CopyOnWriteArrayList
原理介绍:CopyOnWriteArrayList采⽤的是⼀种写时复制的思想,也就是读写分离。 CopyOnWriteArrayList容器即写时复制的容器,往⼀个容器添加元素的时候,不直接往当前容器Object[]添加,⽽是先将当前容器Object[]进⾏copy,夫指出⼀个新的容器Object[]
newElements,然后新的容器newElements中添加元素,添加完元素之后,在将原容器的引⽤指向新的容器setArray(newElements);。这样做的好处是可以对CopyOnWriteArrayList容器进⾏并发的读,⽽不需要加锁,因为当前容器不会添加任何蒜素,所以
CopyOnWriteArrayList容器也是有⼀种读写分离的思想,读和写不同的容器。 Set <String > set = new HashSet <>(); for (int i = 0; i < 3; i ++) { new Thread (() -> { set .add (UUID .randomUUID ().toString ().substring (0,4)); System .out .println (set ); }).start (); }1
2
3
4
5
6
7
8 Map <String , String > map = new HashMap <>(); for (int i = 0; i < 3; i ++) { new Thread (() -> { String param = UUID .randomUUID ().toString ().substring (0,4); map .put (param , param ); System .out .println (map ); }).start (); }
1
2
3
4
5
6
7
8
9 public boolean add (E e ) { ensureCapacityInternal (size + 1); // Increments modCount!! elementData [size ++] = e ; return true ; }
1
2
3
4
5
查看CopyOnWriteArrayList的源码,我们发现有两个变量:ReentrantLock类型的lock和volatile关键字修饰的数组对象array。(volatile保证多线程对共享变量的可见性。)volatile的具体介绍可参见我之前的⽂章:
我们看⼀下CopyOnWriteArrayList的add⽅法。
Set
Set保证线程安全有两种⽅式:
1. 使⽤集合的⼯具类:Collections。Collections可以创建线程安全的集合类。Collections.synchronizedSet(new HashSet<>())
2. 使⽤JUC包中的⼀个类:CopyOnWriteArraySet。
查看CopyOnWriteArraySet的源码发现,其实底层还是CopyOnWriteArrayList。
在此呢多说⼀个⾯试题吧。
HashSet的底层是⼀个HashMap。 但是HashMap是K,V键值对的形式,HashSet只有⼀个,那HashSet的底层怎么是⼀个HashMap 呢? 那就看⼀下源码: /** The lock protecting all mutators */ final transient ReentrantLock lock = new ReentrantLock (); /** The array, accessed only via getArray/setArray. */ private transient volatile Object [] array ;
1
2
3
4
5public 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 (); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 private final CopyOnWriteArrayList <E > al ; /** * Creates an empty set. */ public CopyOnWriteArraySet () { al = new CopyOnWriteArrayList <E >(); }
1
2
3
4
5
6
7
8
Map
Map保证线程安全有两种⽅式:
1. 使⽤集合的⼯具类:Collections。Collections可以创建线程安全的集合类。Collections.synchronizedSet(new HashSet<>())
2. 使⽤JUC包中的⼀个类:ConcurrentHashMap。ConcurrentHashMap的内容较多,后续会专门出⼀⽚⽂章进⾏讲解。 private transient HashMap <E ,Object > map ; private static final Object PRE
SENT = new Object (); /** 构造⽅法: 构造⼀个新的空集; 后备HashMap 实例具有默认初始容量 (16) 和负载因⼦ (0.75)。 */ public HashSet () { map = new HashMap <>(); } /** 添加⽅法,发现添加的元素⾷作为map 的key,value 是⼀个固定的对象 */ public boolean add (E e ) { return map .put (e , PRESENT )==null ; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论