JUC 集合的线程安全问题

场景

因为在工作的时候,不会去过度的考虑高并发的问题。大多数集合的使用都是一般的list,set,map。也不会有太大的问题。但是如果放在多线程的场景下,多个线程同时去操纵一个集合,问题甚多。

集合安全问题代码展示

List

这里是一段代码

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

这是部分执行结果

java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at com.anxin.juc.listThread.ListThread.lambda$main$0(ListThread.java:20)
	at java.lang.Thread.run(Thread.java:748)
java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at com.anxin.juc.listThread.ListThread.lambda$main$0(ListThread.java:20)
	at java.lang.Thread.run(Thread.java:748)
java.util.ConcurrentModificationException

对于ConcurrentModificationException异常,意思是并发修改异常,我们在打印这个集合的时候就会爆出这个异常,包括有时候我们一边遍历一边删除的时候也会出现这个异常,这个就要用迭代器解决了。扯远了。
那么在JUC中如何解决这个问题。解决方案无外乎有三种。

1.用Vector集合

 List<String> list = new Vector<>();
        for (int i = 0; i < 300; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(10));
                System.out.println(list);
            }).start();
        }

这个Vector是线程安全的单线程集合。可以解决这个问题。他的方法几乎都用同步锁修饰。但是自我从业以来,没用过这个,这个也是被废弃的,因为这个性能太低了。而且同步锁并不能真正的用于一些并发量极大的场景。

2.用 Collections.synchronizedList

List<String> list = Collections.synchronizedList(new ArrayList<>());
        for (int i = 0; i < 300; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(10));
                System.out.println(list);
            }).start();
        }

源码是这样的:

 public static <T> List<T> synchronizedList(List<T> list) {
	return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<T>(list) :
                new SynchronizedList<T>(list));
    }

不难解释,为什么他是线程安全的。

3.用 CopyOnWriteArrayList

  List<String> list =new CopyOnWriteArrayList();
        for (int i = 0; i < 300; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(10));
                System.out.println(list);
            }).start();
        }

这个主要是写时复制技术实现的。
我们看一个代码,猜猜它会不会报错?

  List<String> list =new CopyOnWriteArrayList();
        list.add("111");
        list.add("222");
        list.add("333");
        list.add("444");
        list.add("555");
        for (String s : list) {
            list.remove(s);
            System.out.println(list);
        }

不会报错!为什么?
因为
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来得及写会内存,其他的线程就会读到了脏数据。==这就是 CopyOnWriteArrayList 的思想和原理。就是拷贝一份。

  1. 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多
    于可变操作,需要在遍历期间防止线程间的冲突。
  2. 它是线程安全的。
  3. 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove()
    等等)的开销很大。
  4. 迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
  5. 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代
    器时,迭代器依赖于不变的数组快照。
  6. 独占锁效率低:采用读写分离思想解决
  7. 写线程获取到锁,其他写线程阻塞
  8. 复制思想
Set

Set set = new CopyOnWriteArraySet<>();
解决问题

Map

HashTable

   Map<String,String> map = new Hashtable<>();
        for (int i = 0; i < 300; i++) {
            new Thread(() -> {
                map.put(UUID.randomUUID().toString().substring(8),UUID.randomUUID().toString().substring(16));
                System.out.println(map);
            }).start();
        }

有必要说一下hashtable,底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率不是很高,不推荐使用。

ConcurrentHashMap

 Map<String,String> map = new ConcurrentHashMap<>();
        for (int i = 0; i < 300; i++) {
            new Thread(() -> {
                map.put(UUID.randomUUID().toString().substring(8),UUID.randomUUID().toString().substring(16));
                System.out.println(map);
            }).start();
        }

推荐使用这个,那为什么推荐?必须先明白一个问题,为什么该集合线程安全?
ConcurrentHashMap是由一个Segment数组和多个HashEntry数组组成
Segment是一种可重入锁ReentrantLock),ConcurrentHashMap里面扮演锁的角色;HashEntry则用于存储键值对数据。
那么为什么他的效率高呢?
这就是我之前说为为何同步锁根本扛不住很大并发的原因。因为HashTable之流,是很多线程去争夺一把锁。而ConcurrentHashMap的锁的粒度很小,每个分段去分一把锁。所以效率高。

总结

1.线程安全与线程不安全集合
集合类型中存在线程安全与线程不安全的两种,常见例如:
ArrayList ----- Vector
HashMap -----HashTable
但是以上都是通过 synchronized 关键字实现,效率较低
2.Collections 构建的线程安全集合
3.java.util.concurrent 并发包下
CopyOnWriteArrayList CopyOnWriteArraySet 类型,通过动态数组与线程安全个方面保证线程安全。

预告

下一节是volatile关键字。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值