线程安全的集合 线程安全的集合

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

线程安全的集合

WangScaler: 一个用心创作的作者。

声明:才疏学浅,如有错误,恳请指正。

不安全的集合

日常coding,我们是不是经常用到ArrayList、HashSet、HashMap这样的集合?那你知不知道这些集合在多线程中是不安全的,举个例子。

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
java复制代码package com.wangscaler.securecollection;

import java.util.*;

/**
* @author WangScaler
* @date 2021/8/5 15:25
*/

public class NotSafeCollection {
   public static void main(String[] args) {

       List<Integer> list = new ArrayList<>();
       int number = 3;
       Random random = new Random();
       for (int i = 0; i < number; i++) {
           new Thread(() -> {
               int randomNumber = random.nextInt(10) % 11;
               list.add(randomNumber);
          }, String.valueOf(i)).start();
      }
       while (Thread.activeCount() > 2) {
           Thread.yield();
      }
       list.forEach(System.out::println);
  }
}

你的本意可能是起三个线程去填充这个ArrayList集合,然而结果却总是超出意料,上述的代码执行的结果可能是null;null;3也可能是null;2;3,当然也有可能达到你的预期效果1,2,3。

为什么会出现这种情况呢?我们翻开源码

1
2
3
4
5
java复制代码public boolean add(E e) {
   ensureCapacityInternal(size + 1);  // Increments modCount!!
   elementData[size++] = e;
   return true;
}

但是三行代码的执行的时候字节码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码 0 aload_0
1 aload_0
2 getfield #284 <java/util/ArrayList.size : I>
5 iconst_1
6 iadd
7 invokespecial #309 <java/util/ArrayList.ensureCapacityInternal : (I)V>
10 aload_0
11 getfield #287 <java/util/ArrayList.elementData : [Ljava/lang/Object;>
14 aload_0
15 dup
16 getfield #284 <java/util/ArrayList.size : I>
19 dup_x1
20 iconst_1
21 iadd
22 putfield #284 <java/util/ArrayList.size : I>
25 aload_1
26 aastore
27 iconst_1
28 ireturn

在程序执行的时候,线程是交替执行的。我们上面的例子有三个线程,分别是线程1、线程2、线程3。

看字节码 putfield #284 <java/util/ArrayList.size : I>是在aastore之前的,也就是说size写回主内存是在数组写回之前的。所以就有可能出现线程1数组还没写进去的时候,线程2就开始执行了,此时size已经是加一之后的了,所以此时线程2将新值保存到数组里,也就出现了null;2;3的情况。

HashMap也是线程不安全的,同样HashSet也是,因为HashSet的底层就是HashMap,话不多说上源码

1
2
3
java复制代码public HashSet() {
   map = new HashMap<>();
}

那HashMap和HashSet的区别是啥呢?我们知道HashMap是键值对的形式,而HashSet的value值是固定的,源码如下。

1
2
3
4
java复制代码private static final Object PRESENT = new Object();
public boolean add(E e) {
   return map.put(e, PRESENT)==null;
}

HashMap除了线程不安全,在jdk8之前HashMap扩容的时候还会产生死链的情况,

如何解决?

1、遗留的安全集合

  • Vector用于ArrayList
  • HashTable用于HashMap

举例如下:将上述的List<Integer> list = new ArrayList<>();修改为List<Integer> list = new Vector<>(); 即可。为什么这个就可以解决问题呢?打开源码

1
2
3
4
5
6
java复制代码public synchronized boolean add(E e) {
  modCount++;
  ensureCapacityHelper(elementCount + 1);
  elementData[elementCount++] = e;
  return true;
}

是个同步方法,通过互斥锁使问题得到解决,但是在多线程中极大的影响效率,已经被弃用了。HashTable同样因为同步的问题,被弃用了。

2、Collections

通过Collections的修饰将不安全的集合变成安全的集合。

  • synchronizedList用于ArrayList
  • synchronizedMap用于HashMap
  • synchronizedSet用于HashSet

在原有不安全集合上包装了一个线程安全的类,来达到预期的效果。举例如下:将上述的List<Integer> list = new ArrayList<>();修改为List<Integer> list = Collections.synchronizedList(new ArrayList<>());即可解决。原理是什么呢?还是看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码SynchronizedList(List<E> list) {
   super(list);
   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);}
      }

在所有的方法上加了synchronized修饰,从而达到同步的效果。

3、JUC

  • Bloacking
+ ArrayBlockingQueue
+ LinkedBlockingQueue
+ LinkedBlockingDeque
+ ...
  • CopyOnWrite
+ CopyOnWriteArrayList对应ArrayList
+ CopyOnWriteArraySet用于HashSet,底层还是CopyOnWriteArrayList
  • Concurrent(推荐使用,弱一致性。)
+ ConcurrentHashMap用于HashMap


只能保证一个操作是原子的,比如先检查key在不在(get),不在再添加(put)两个操作无法保证原子性,应该使用computeIfAbsent()
+ ConcurrentSkipListMap
+ ConcurrentSkipListSet
+ ...

举例如下:将上述的List<Integer> list = new ArrayList<>();修改为List<Integer> list = new CopyOnWriteArrayList<>();

适合读多写少的场景。看下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码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);
       newElements[len] = e;
       setArray(newElements);
       return true;
  } finally {
       lock.unlock();
  }
}

在写入的时候加锁,并复制一份,将新加入的写进新数组,最终把新数组写回原资源,从而保证数据的原子性。这个过程中只是给增加方法加锁,不影响读的操作。

结语

多线程中同步方法大大影响了工作效率,所以ConcurrentHashMap通过volatile结合自旋锁的方式,广受大家喜爱,同样也是面试官常问的题目之一,值得大家好好去读一下源码。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%