04_并发容器类

1. 重现线程不安全:List

首先以List作为演示对象,创建多个线程对List接口的常用实现类ArrayList进行add操作。

public class NotSafeDemo {

    public static void main(String[] args) {

        List<String> list = new ArrayList<>();

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

测试结果:  

出现了线程不安全错误

ArrayList在多个线程同时对其进行修改的时候,就会抛出java.util.ConcurrentModificationException异常(并发修改异常),因为ArrayList的add及其他方法都是线程不安全的,有源码佐证:

解决方案:

​ List接口有很多实现类,除了常用的ArrayList之外,还有VectorSynchronizedList

他们都有synchronized关键字,说明都是线程安全的。

 

改用Vector或者synchronizedList试试:即可解决!

public static void main(String[] args) {

        //List<String> list = new Vector<>();
        List<String> list = Collections.synchronizedList(new ArrayList<>());

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

Vector和Synchronized的缺点:

vector:内存消耗比较大,适合一次增量比较大的情况

SynchronizedList:迭代器涉及的代码没有加上线程同步代码  

2. CopyOnWrite容器

什么是CopyOnWrite容器?

CopyOnWrite容器(简称COW容器)即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器

从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayListCopyOnWriteArraySet

先看看CopyOnWriteArrayList类:发现它的本质就是数组

再来看看CopyOnWriteArrayList的add方法:发现该方法是线程安全的

使用CopyOnWriteArrayList改造main方法:  

    public static void main(String[] args) {

        //List<String> list = new Vector<>();
        //List<String> list = Collections.synchronizedList(new ArrayList<>());
        List<String> list = new CopyOnWriteArrayList<>();

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

CopyOnWrite并发容器用于读多写少的并发场景。比如:白名单,黑名单。假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单一定周期才会更新一次。

缺点:

  1. 内存占用问题。写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存。通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

  2. 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

3. 扩展类比:Set和Map

HashSet和HashMap也都是线程不安全的,类似于ArrayList,也可以通过代码证明。

private static void notSafeMap() {
        Map<String, String> map = new HashMap<>();

        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                map.put(String.valueOf(Thread.currentThread().getName()), UUID.randomUUID().toString().substring(0, 8));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }

    private static void notSafeSet() {
        Set<String> set = new HashSet<>();

        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }

都会报:ConcurrentModificationException异常信息。

Collections提供了方法synchronizedList保证list是同步线程安全的,Set和Map呢?

HashMap<String, String> map = new HashMap<>(); //不安全
Hashtable<String, String> map = new Hashtable<>(); //安全
Map<String, String> map = Collections.synchronizedMap(new HashMap<>()); //安全
ConcurrentMap<String, String> map = new ConcurrentMap<>(); //安全

JUC提供的CopyOnWrite容器实现类有:CopyOnWriteArrayList和CopyOnWriteArraySet。

有没有Map的实现:

最终实现:

public class NotSafeDemo {

    public static void main(String[] args) {
        notSafeList();
        notSafeSet();
        notSafeMap();
    }

    private static void notSafeMap() {
        //Map<String, String> map = new HashMap<>();
        //Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
        Map<String, String> map = new ConcurrentHashMap<>();

        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                map.put(String.valueOf(Thread.currentThread().getName()), UUID.randomUUID().toString().substring(0, 8));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }

    private static void notSafeSet() {
        //Set<String> set = new HashSet<>();
        Set<String> set = new CopyOnWriteArraySet<>();

        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }

    private static void notSafeList() {
        //List<String> list = new Vector<>();
        //List<String> list = Collections.synchronizedList(new ArrayList<>());
        List<String> list = new CopyOnWriteArrayList<>();

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

扩展:HashSet底层数据结构是什么?HashMap ?

但HashSet的add是放一个值,而HashMap是放K、V键值对

4. 并发容器和同步容器

同步容器可以简单地理解为通过synchronized来实现同步的容器。同步容器会导致多个线程中对容器方法调用的串行执行,降低并发性,因为它们都是以容器自身对象为锁。在并发下进行迭代的读和写时并不是线程安全的。如:Vector、Stack、HashTable、Collections类的静态工厂方法创建的类(如Collections.synchronizedList)

并发容器是针对多个线程并发访问而设计的,在jdk5.0引入了concurrent包,其中提供了很多并发容器,如ConcurrentHashMap、CopyOnWriteArrayList等。

ConcurrentHashMap:内部采用Segment结构,进行两次Hash进行定位,写时只对Segment加锁
CopyOnWriteArrayList:CopyOnWrite写时复制一份新的,在新的上面修改,然后把引用指向新的。只能实现数据的最终一致性,非实时一致的;代替List,适用于读操作为主的情况

同步容器并发容器都为多线程并发访问提供了合适的线程安全,不过并发容器的可扩展性更高。

public static void main(String[]args){
    Vector v = new Vector();
    for (int i = 0; i < 10; i++) {
        int a = i;
        new Thread(()->{
            v.add(a);
        }).start();
    }
    for (Iterator iterator = v.iterator(); iterator.hasNext();) {
        int element = (int) iterator.next();
        v.remove(element);
    }
}

5. 性能测试(了解)

① 引入spring-core依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.3.18</version>
</dependency>

② 测试代码

StopWatch stopwatch = new StopWatch();

public static void main(String[] args) {
    StopWatch stopwatch = new StopWatch();
    List<Integer> list = new Vector<>();
    stopwatch.start("Vector:write数据");
    IntStream.rangeClosed(1,1000000).parallel().forEach( a ->{
        list.add(new Random().nextInt(1000000));
    });
    stopwatch.stop();
    stopwatch.start("Vector:read数据");
    IntStream.rangeClosed(1,1000000).parallel().forEach( a ->{
        list.get(new Random().nextInt(1000000));
    });
    stopwatch.stop();
    System.out.println(stopwatch.prettyPrint());
}

③ 结果

猜你喜欢

转载自blog.csdn.net/qq_45037155/article/details/130408459
04_