Java基础-0x04:集合类线程不安全

ss

JUC

集合类线程不安全

在多线程并发环境下,对集合类执行像add( ),put( )这样的操作
会触发java.util.ConcurrentModificationException这样的异常

上一篇已经举例了ArrayList的解决方法,这次看看其他集合类的底层到底是怎么解决的。

HashSet

1
2
3
4
5
6
7
8
9
10
11
public class SetNotSafeDemo {
public static void main(String[] args) {
Set<String> stringSet = Collections.synchronizedSet(new HashSet<>());
for (int i = 0; i < 3; i++) {
new Thread(()->{
stringSet.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(stringSet);
},String.valueOf(i)).start();
}
}
}
  • 使用synchronizedSetnew一个HashSet,就可以解决报错的问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class SetNotSafeDemo {
    public static void main(String[] args) {
    Set<String> stringSet = Collections.synchronizedSet(new HashSet<>());
    for (int i = 0; i < 3; i++) {
    new Thread(()->{
    stringSet.add(UUID.randomUUID().toString().substring(0,8));
    System.out.println(stringSet);
    },String.valueOf(i)).start();
    }
    }
    }
  • 还可以使用CopyOnWriteArraySet<E>这个类来初始化,也能解决这个问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class SetNotSafeDemo {
    public static void main(String[] args) {
    Set<String> stringSet = new CopyOnWriteArraySet<>();
    for (int i = 0; i < 30; i++) {
    new Thread(()->{
    stringSet.add(UUID.randomUUID().toString().substring(0,8));
    System.out.println(stringSet);
    },String.valueOf(i)).start();
    }
    }
    }
    • 可以看一下CopyOnWriteArraySet的源码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      public class CopyOnWriteArraySet<E> extends AbstractSet<E>
      implements java.io.Serializable {

      ······

      private final CopyOnWriteArrayList<E> al;

      /**
      * Creates an empty set.
      */
      public CopyOnWriteArraySet() {
      al = new CopyOnWriteArrayList<E>();
      }
      }

      可以发现CopyOnWriteArraySet在初始化的时候,还是new了一个CopyOnWriteArrayList<E>

HashSet底层数据结构

就是HashMap!

下面是HashSet的构造函数。

注释上说了:构造一个新的、空的集合,后面的HashMap的实例有默认为16的容量和0.75的负载因子。

1
2
3
4
5
6
7
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}

但是对比HashSetHashMap的添加方法会发现传的值是不一样的。

1
2
3
4
5
Set<String> set = new HashSet<>();
Map<String,String> map = new HashMap<>();

set.add("a");
map.put("a","a");

既然底层都是HashMap,那为什么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
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

······

/**
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element <tt>e</tt> to this set if
* this set contains no element <tt>e2</tt> such that
* <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
* If this set already contains the element, the call leaves the set
* unchanged and returns <tt>false</tt>.
*
* @param e element to be added to this set
* @return <tt>true</tt> if this set did not already contain the specified
* element
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

······
}

HashSetadd方法,其实调用的是Mapput方法,也是两个参数。只不过HashSetadd的就是HashMap中的Key,然后Value是一个叫PRESENT的常量

HashMap

1
2
3
4
5
6
7
8
9
10
11
12
public class SetNotSafeDemo {
public static void main(String[] args) {
Map<String,String> map = new HashMap<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),
UUID.randomUUID().toString().substring(0,8));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}

跟其他的集合类一样,也会爆出ConcurrentModificationException

解决方法:

  • 使用Collections.synchronizedMap(new HashMap());

  • 不要去JUC包下面找CopyOnWriteHashMap

    Java提供的解决方法是一个叫ConcurrentHashMap<K,V>