上面一节我们介绍了ArrayList、HashSet、HashMap这些容器都是非线程安全的。如果有多个线程并发访问这些容器时,就会触发线程安全问题。因此在编写程序的时候,必须要求开发人员手动的在任何访问到这些容器的地方进行同步处理,这样就导致使用起来非常不便。因此java提供了同步容器方便使用。
在java中同步容器主要包括两类:
- ArrayList -> Vector,Stack; HashMap -> HashTable(key,value不能为null)
- Collections.synchronizedXXX(List, Set, Map)
Vector
Vector实现了List接口,实际上就是一个数组。与ArrayList非常类似。但是Vector中的所有方法都是使用synchronized方法修饰的方法,进行了同步的措施。因此在多线程环境下使用ArrayList对象时,如果被多个线程共享使用可以换成同步的Vector,这样的话线程安全型会更好一些(
而不是完全线程安全的)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 1@Slf4j
2@ThreadSafe
3public class VectorExample1 {
4 // 请求总数
5 public static int clientTotal = 5000;
6
7 // 同时并发执行的线程数
8 public static int threadTotal = 200;
9
10 private static List<Integer> list = new Vector<>();
11
12 public static void main(String[] args) throws InterruptedException {
13 //线程池
14 ExecutorService executorService = Executors.newCachedThreadPool();
15 //定义信号量
16 final Semaphore semaphore = new Semaphore(threadTotal);
17 //定义计数器
18 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
19 for(int i = 0; i < clientTotal; i++) {
20 final int count = i;
21 executorService.execute(() ->{
22 try {
23 semaphore.acquire();
24 update(count);
25 semaphore.release();
26 } catch (InterruptedException e) {
27
28 log.error("exception", e);
29 }
30 countDownLatch.countDown();
31
32 });
33 }
34 countDownLatch.await();
35 executorService.shutdown();
36 log.info("size:{}",list.size()) ;
37 }
38
39 public static void update(int i) {
40 list.add(i);
41 }
42
43}
44
这样输出的结果就是预期的结果。
为什么说同步容器不是线程安全的?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 1@NotThreadSafe
2public class VectorExample2 {
3 private static Vector<Integer> vector = new Vector<>();
4
5 public static void main(String[] args) {
6 while (true){
7 for (int i = 0;i < 10;i++) {
8 vector.add(i);
9 }
10 Thread thread1 = new Thread(){
11 @Override
12 public void run() {
13 for (int i = 0;i < vector.size();i++) {
14 vector.remove(i);
15 }
16 }
17 };
18 Thread thread2 = new Thread(){
19 @Override
20 public void run() {
21 for (int i = 0;i < vector.size();i++) {
22 vector.get(i);
23 }
24 }
25 };
26 thread1.start();
27 thread2.start();
28 }
29
30 }
31}
32
运行,抛出异常:
1
2
3
4
5
6
7 1Exception in thread "Thread-611" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 5
2 at java.util.Vector.get(Vector.java:748)
3 at com.vincent.example.syncContainer.VectorExample2$2.run(VectorExample2.java:25)
4Exception in thread "Thread-1759" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 21
5 at java.util.Vector.get(Vector.java:748)
6 at com.vincent.example.syncContainer.VectorExample2$2.run(VectorExample2.java:25)
7
原因:get发生越界肯定是remove方法引起的,vector虽然能保证同一时刻只能有一个线程能访问他,但是不排除有这种可能:当某个线程某个时刻执行到int i = 0;i < vector.size()时,vector.size()返回10,i=9;而另外一个线程正好将i=9的vector移除掉了,这时get方法想调用i=9的元素就会出现数组越界的异常。
这个例子演示了两个同步容器的两个同步方法因为操作顺序的差异,在不同线程里面可能会触发线程不安全的问题。因此为了保证线程安全,必须在方法调用端做一些额外的同步措施才可以。在使用同步容器时并不是在所有场合下都是线程安全的。
Stack
Stack中的方法也使用了synchronized修饰了,实际上Stack类继承了Vector类。
HashTable
HashTable实现了Map接口,与HashMap很相似,但是HashTable进行了同步处理,方法也是使用了synchronized进行了修饰。但是在使用HashTable时一定要注意key和value是不能为null的。
Collections
将新建ArrayList、HashSet、HashMap对象由Collections产生:
1
2
3
4 1private static List<Integer> list = Collections.synchronizedList(new ArrayList<>());
2private static Set<Integer> set = Collections.synchronizedSet(new HashSet<>());
3private static Map<Integer, Integer> map = Collections.synchronizedMap(new HashMap<>());
4
在集合遍历过程中删除操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 1public class VectorExample3 {
2
3 //Exception in thread "main" java.util.ConcurrentModificationException
4 private static void test1(Vector<Integer> v1){ //foreach
5 for(Integer i: v1){
6 if(i.equals(3)){
7 v1.remove(i);
8 }
9 }
10 }
11 //Exception in thread "main" java.util.ConcurrentModificationException
12 private static void test2(Vector<Integer> v1){ //iterator
13 Iterator<Integer> integerIterator = v1.iterator();
14 while (integerIterator.hasNext()) {
15 Integer i = integerIterator.next();
16 if(i.equals(3)){
17 v1.remove(i);
18 }
19 }
20 }
21 // success
22 private static void test3(Vector<Integer> v1){
23 for(int i = 0; i < v1.size(); i++) {
24 if(v1.equals(3)){
25 v1.remove(i);
26 }
27 }
28 }
29
30 public static void main(String[] args) {
31 Vector<Integer> vector = new Vector<>();
32 vector.add(1);
33 vector.add(2);
34 vector.add(3);
35 test1(vector);
36 }
37}
38
如果使用了Foreach或者迭代器来循环我们的集合时,尽量不要在循环中做集合的删除操作,如果要做remove操作时,建议在遍历的过程中发现需要删除的值然后做一个标记,在遍历结束后在执行相应的remove操作。在多线程情况下出现异常的情况会更大。