juc学习四(集合不安全问题)

并发环境下,我们经常使用的集合类(List、Map、Set)其实都是不安全的。

集合不安全问题之List

List在单线程的情况下是安全的,但是多线程的情况下是不安全的,我们来看两段代码:

单线程

public class UnsafeList1 {

    public static void main(String[] args) {
        List<String> list= Arrays.asList("a","b","c");
        list.forEach(System.out::println);
    }
}

 

 多线程

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

        List<String> list=new ArrayList<>();
        for (int i = 1; i <=50; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

 通过以上两段代码,我们可以看到,在多线程情况下会报java.util.ConcurrentModificationException并发修改异常。

而ArrayList是jdk1.2出现的,是一个不加锁的集合类,并发性上升,但是牺牲多线程的安全性为代价再看ArrayList源码发现add方法没有加锁,所有多线程情况下出现线程异常问题。

 解决方案

1 使用List接口下的实现集合Vector 类,但是该类是JDK1.0出现的,其add()是syncronized修饰的。数据一致性可以得到保证,但是并发性会下降。

.

 2 使用工具类 Collections中的synchronizedList将其变为安全的集合类,其中还有构建set,map集合安全类的方法。

public class UnsafeList4 {
    public static void main(String[] args) {
        //构建一个同步的synchronizedList
        List<String> list= Collections.synchronizedList(new ArrayList<>());
        for (int i = 1; i <=50; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

 3 JUC包中的CopyOnWriteArrayList,推荐使用

public class UnsafeList5 {
    public static void main(String[] args) {
     //写时复制容器
        List<String> list= new CopyOnWriteArrayList<>();
        for (int i = 1; i <=50; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

CopyOnWriteArrayList写时复制容器。 往一个容器添加元素的时候,不直接往当前容器 Object[] 添加, 而是先将当前容器 Object[] 进行copy, 复制出一个新的容器 Object[] newElements, 然后往新的容器Object[] newElements里添加元素, 添加完元素之后, 再将原容器的引用指向新的容器 setArray(newElements)。这样做的好处是可以对 CopyOnWrite容器进行并发的读, 而不需要加锁, 因为当前容器不会添加任何元素. 所以 CopyOnWrite容器 也是一种读写分离的思想, 读和写不同的容器。

CopyOnWriteArrayList的add源码,底层volatile Object[] array,add方法里面加了锁ReentrantLock

 写入时复制(COW)思想原理:指针,复制指向的问题

集合的线程不安全问题之Set 

HashSet底层数据结构是HashMap(源码构造器里面 new HashMap()),其add方法实际上return map.put(e,PRESENT)==null; PRESENT实际上就是一个object常量,所以实际上就是HashMap的keys。

 

 

解决方案

1 使用工具类 Collections => Set<Object> set = Collections.synchronizedSet(new HashSet<>());
2 JUC包 中的Set<Object> set = new CopyOnWriteArraySet<>();

CopyOnWriteArraySet和CopyOnWriteArrayList类似,其实CopyOnWriteSet底层包含一个CopyOnWriteList,几乎所有操作都是借助CopyOnWriteList实现的,就像HashSet包含HashMap。而CopyOnWriteArrayList本质是个动态数组队列,所以CopyOnWriteArraySet相当于通过通过动态数组实现的“集合”! CopyOnWriteArrayList中允许有重复的元素;但是,CopyOnWriteArraySet是一个集合,所以它不能有重复集合。因此,CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,通过这些API来添加元素时,只有当元素不存在时才执行添加操作!至于CopyOnWriteArraySet的“线程安全”机制,和CopyOnWriteArrayList一样,是通过volatile和互斥锁来实现的。

 

 

 集合的线程不安全问题之Map

Map集合仍然会出现上述的java.util.ConcurrentModificationException异常。

解决方案:

1.使用Collections工具类的synchronizedMap()方法
2.使用HashTable
但HashTable也是一个古老的技术,它的所有方法也是都加了锁,并发性不好,不推荐使用。独占锁。
3.使用ConcurrentHashMap(J.U.C提供)
java没有为map提供写时复制的类,所以我们使用ConcurrentHashMap来解决map类线程安全的问题。ConcureentHashMap同步容器类是java5增加的一个线程安全的哈希表。对于多线程的操作,介于HashMap与Hashtable之间。内部采用“锁分段”机制替代Hashtable的独占锁。进而提高性能。

示例代码:

public class UnsafeMap {

    public static void main(String[] args) {
        //hashMap();
       // collections();
        concurrentHashMap();
    }

    //多线程下HashMap线程不安全,出现并发修改异常
    public static void hashMap(){
        Map<String,Object> map= new HashMap<>();
        for (int i = 1; i <=50; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }

    //使用Collections工具类中的synchronizedMap构建安全map
    public static void collections(){
        Map<String,Object> map= Collections.synchronizedMap(new HashMap<>());
        for (int i = 1; i <=50; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }

    //使用ConcurrentHashMap
    public static void concurrentHashMap(){
        Map<String,Object> map= new ConcurrentHashMap<>();
        for (int i = 1; i <=50; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }
}

ConcurrentHashMap底层实现原理可以仔细阅读下面这篇博客,博主写的特详细

https://www.jianshu.com/p/865c813f2726

原文地址:https://www.cnblogs.com/mabaoying/p/13149939.html