专栏首页JVMGCJava并发容器J.U.C
原创

Java并发容器J.U.C

J.U.Cjava.util.concurrent的简写,里面提供了很多线程安全的集合。

CopyOnWriteArrayList介绍

CopyOnWriteArrayList相比于ArrayList是线程安全的,字面意思是写操作时复制CopyOnWriteArrayList使用写操作时复制技术,当有新元素需要加入时,先从原数组拷贝一份出来。然后在新数组里面加锁添加,添加之后,将原来数组的引用指向新数组。

 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(); //释放锁
        }
    }

从上面的源码中得到CopyOnWriteArrayListadd操作是在加锁的保护下完成的。加锁是为了多线程对CopyOnWriteArrayList并发add时,复制多个副本,把数据搞乱。

public E get(int index) {
        return get(getArray(), index);
}

以上代码显示get是没有加锁的

如果出现并发get,会有以下3种情况。

  • 如果写操作未完成,那么直接读取原数组的数据;
  • 如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
  • 如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

CopyOnWriteArrayList多线程代码演示。

package com.rumenz.task;

import java.util.List;
import java.util.concurrent.*;


//线程安全
public class CopyOnWrireArrayListExample {

    public static Integer clientTotal=5000;
    public static Integer threadTotal=200;

   private static List<Integer> list=new CopyOnWriteArrayList();

    public static void main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore=new Semaphore(threadTotal);
        final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final Integer j=i;
            executorService.execute(()->{
                try{
                    semaphore.acquire();
                    update(j);
                    semaphore.release();
                }catch (Exception e){
                    e.printStackTrace();
                }
                countDownLatch.countDown();

            });


        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("size:"+list.size());
    }

    private static void update(Integer j) {
        list.add(j);
    }
}

//size:5000

CopyOnWriteArrayList使用场景

  • 由于在add的时候需要拷贝原数组,如果原数组内容比较多,比较大,可能会导致young gcfull gc
  • 不能用于实时读的场景,像拷贝数组,新增元素都需要时间,所以调用get操作后,有可能得到的数据是旧数据,虽然CopyOnWriteArrayList能做到最终一致性,但是没有办法满足实时性要求。
  • CopyOnWriteArrayList适合读多写少的场景,比如白名单,黑名单等场景
  • CopyOnWriteArrayList由于add时需要复制数组,所以不适用高性能的互联网的应用。

CopyOnWriteArraySet介绍

public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
}

CopyOnWriteArraySet底层是用CopyOnWriteArraySet来实现的。可变操作(add,set,remove等)都需要拷贝原数组进行操作,一般开销很大。迭代器支持hasNext(),netx()等不可变操作,不支持可变的remove操作,使用迭代器速度很快,并且不会与其它线程冲突,在构造迭代器时,依赖不变的数组快照。

CopyOnWriteArraySet多线代码演示

package com.rumenz.task;

import java.util.List;
import java.util.Set;
import java.util.concurrent.*;


//线程安全
public class CopyOnWrireArraySetExample {

    public static Integer clientTotal=5000;
    public static Integer threadTotal=200;

   private static Set<Integer> set=new CopyOnWriteArraySet();

    public static void main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore=new Semaphore(threadTotal);
        final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final Integer j=i;
            executorService.execute(()->{
                try{
                    semaphore.acquire();
                    update(j);
                    semaphore.release();
                }catch (Exception e){
                    e.printStackTrace();
                }
                countDownLatch.countDown();

            });


        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("size:"+set.size());
    }

    private static void update(Integer j) {
        set.add(j);
    }
}
//size:5000

CopyOnWriteArraySet使用场景

  • 适用于set大小一般很小,读操作远远多于写操作的场景

ConcurrentSkipListSet

public ConcurrentSkipListSet() {
     m = new ConcurrentSkipListMap<E,Object>();
}

ConcurrentSkipListSet<E>jdk6新增的类,支持自然排序,位于java.util.concurrentConcurrentSkipListSet<E>都是基于Map集合的,底层由ConcurrentSkipListMap实现。

在多线程环境下,ConcurrentSkipListSet<E>add,remove,contains是线程安全的。但是对于批量操作addAll,removeAll,containsAll并不能保证原子操作,所以是线程不安全的,原因是addAll,removeAll,containsAll底层调用的还是add,remove,contains方法,在批量操作时,只能保证每一次的add,remove,contains是原子性的(即在进行add,remove,contains,不会被其它线程打断),而不能保证每一次批量操作都不会被其它线程打断,因此在addAllremoveAllretainAllcontainsAll操作时,需要添加额外的同步操作。

public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
}

public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        boolean modified = false;
        Iterator<?> it = iterator();
        while (it.hasNext()) {
            if (c.contains(it.next())) {
                it.remove();
                modified = true;
            }
        }
        return modified;
}

public boolean containsAll(Collection<?> c) {
        for (Object e : c)
            if (!contains(e))
                return false;
        return true;
}

ConcurrentSkipListSet代码演示

package com.rumenz.task;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.*;


//线程安全
public class CopyOnWrireArrayListExample {

    public static Integer clientTotal=5000;
    public static Integer threadTotal=200;

    private static Set<Integer> set= new ConcurrentSkipListSet();

    public static void main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore=new Semaphore(threadTotal);
        final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final Integer j=i;
            executorService.execute(()->{
                try{
                    semaphore.acquire();
                    update(j);
                    semaphore.release();
                }catch (Exception e){
                    e.printStackTrace();
                }
                countDownLatch.countDown();

            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("size:"+set.size());
    }


    private static void update(Integer r) {
        set.add(r);
    }
}

//size:5000

ConcurrentHashMap

ConcurrentHashMapkeyvalue都不允许为null,ConcurrentHashMap针对读操作做了大量的优化。在高并发场景很有优势。

在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率到100%,所以在多线程环境不能随意使用HashMap。原因分析:HashMap在进行put的时候,插入的元素超过了容量就会发生rehash扩容,这个操作会把原来的元素hash到新的扩容新的数组,在多线程情况下,如果此时有其它线程在进行put操作,如果Hash值相同,可能出现在同一数组下用链表表示,造成闭环,导致get的时候出现死循环,所以是线程不安全的。

HashTable它是线程安全的,它涉及到多线程的操作都synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争同一把锁,在多线程环境下是安全的,但是效率很低。

HashTable有很多的优化空间,锁住整个table这么粗暴的方法可以变相的柔和点,比如在多线程的环境下,对不同的数据集进行操作时其实根本就不需要去竞争一个锁,因为他们不同hash值,不会因为rehash造成线程不安全,所以互不影响,这就是锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table,多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMapJDK1.7版本的核心思想。

ConcurrentHashMap代码演示案例

package com.rumenz.task;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;


//线程安全
public class ConcurrentHashMapExample {

    public static Integer clientTotal=5000;
    public static Integer threadTotal=200;

   private static Map<Integer,Integer> map=new ConcurrentHashMap<Integer,Integer>();

    public static void main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore=new Semaphore(threadTotal);
        final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final Integer j=i;
            executorService.execute(()->{
                try{
                    semaphore.acquire();
                    update(j);
                    semaphore.release();
                }catch (Exception e){
                    e.printStackTrace();
                }
                countDownLatch.countDown();

            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("size:"+map.size());
    }

    private static void update(Integer j) {
        map.put(j, j);
    }
}
//size:5000

ConcurrentSkipListMap

ConcurrentSkipListMap内部使用SkipList结构实现。跳表是一个链表,但是通过跳跃式的查找方式使得插入,读取数据时的时间复杂度变成O(log n)

跳表(SkipList):使用空间换时间的算法,令链表的每个结点不仅记录next结点位置,还可以按照level层级分别记录后继第level个结点。

image-20210110165426146

ConcurrentSkipListMap代码案例

package com.rumenz.task;

import java.util.Map;
import java.util.concurrent.*;


//线程安全
public class ConcurrentSkipListMapExample {

    public static Integer clientTotal=5000;
    public static Integer threadTotal=200;

   private static Map<Integer,Integer> map=new ConcurrentSkipListMap<>();

    public static void main(String[] args) throws Exception{
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore=new Semaphore(threadTotal);
        final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final Integer j=i;
            executorService.execute(()->{
                try{
                    semaphore.acquire();
                    update(j);
                    semaphore.release();
                }catch (Exception e){
                    e.printStackTrace();
                }
                countDownLatch.countDown();

            });


        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("size:"+map.size());
    }

    private static void update(Integer j) {
        map.put(j, j);
    }
}

//size:5000

ConcurrentHashMapConcurrentSkipListMap的对比

  • ConcurrentHashMapConcurrentSkipListMap性能要好一些。
  • ConcurrentSkipListMapkey是有序的,ConcurrentHashMap做不到。
  • ConcurrentSkipListMap支持高并发,它的时间复杂度是log(N),和线程数无关,也就是说任务一定的情况下,并发的线程越多,ConcurrentSkipListMap的优势就越能体现出来。
wx.jpg

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【死磕 Java 并发】—– J.U.C 之 Java并发容器:ConcurrentHashMap

    HashMap是我们用得非常频繁的一个集合,但是由于它是非线程安全的,在多线程环境下,put操作是有可能产生死循环的,导致CPU利用率接近100%。为了解决该问...

    芋道源码
  • 【死磕Java并发】-----J.U.C之Java并发容器:ConcurrentHashMap

    此篇博客所有源码均来自JDK 1.8 HashMap是我们用得非常频繁的一个集合,但是由于它是非线程安全的,在多线程环境下,put操作是有可能产生死循环的,导致...

    用户1655470
  • 【死磕Java并发】-----J.U.C之Java并发容器:ConcurrentSkipListMap

    到目前为止,我们在Java世界里看到了两种实现key-value的数据结构:Hash、TreeMap,这两种数据结构各自都有着优缺点。 Hash表:插入、查找最...

    用户1655470
  • 【死磕Java并发】—–J.U.C之Java并发容器:ConcurrentLinkedQueue

    要实现一个线程安全的队列有两种方式:阻塞和非阻塞。阻塞队列无非就是锁的应用,而非阻塞则是CAS算法的应用。下面我们就开始一个非阻塞算法的研究:Coucurren...

    用户1655470
  • 【死磕 Java 并发】—– J.U.C 之 Java 并发容器:ConcurrentLinkedQueue

    SkipListSkipList的特性SkipList的查找SkipList的插入SkipList的删除ConcurrentSkipListMapput操作ge...

    芋道源码
  • 008. J.U.C 之并发容器类 Map

    山海散人
  • Java并发J.U.C 之 AQS

    入门小站
  • 【死磕Java并发】----- 死磕 Java 并发精品合集

    【死磕 Java 并发】系列是 LZ 在 2017 年写的第一个死磕系列,一直没有做一个合集,这篇博客则是将整个系列做一个概览。

    用户1655470
  • 死磕Java并发:J.U.C之Condition

    在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()、notify()系列方法可以实现等待/通知模式。在Java SE...

    程序猿DD
  • 【死磕Java并发】-----J.U.C之Condition

    此篇博客所有源码均来自JDK 1.8 在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()、notify()系列方法可...

    芋道源码
  • 【死磕Java并发】—–J.U.C之Condition

    在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()、notify()系列方法可以实现等待/通知模式。在Java SE...

    用户1655470
  • Java并发容器--ConcurrentLinkedQueue

      ConcurrentLinkedQueue是一种基于链表实现的无界非阻塞线程安全队列,遵循先入先出规则。

    在周末
  • Java并发容器--ConcurrentHashMap

      1、不安全:大家都知道HashMap不是线程安全的,在多线程环境下,对HashMap进行put操作会导致死循环。是因为多线程会导致Entry链表形成环形数据...

    在周末
  • Java并发容器篇

    因为并发容器类都位于java.util.concurrent下,所以我们也习惯把并发容器简称为JUC容器;

    汤圆学Java
  • 【死磕Java并发】—–J.U.C之并发工具类:Semaphore

    信号量Semaphore是一个控制访问多个共享资源的计数器,和CountDownLatch一样,其本质上是一个“共享锁”。

    用户1655470
  • 【死磕Java并发】—– J.U.C之并发工具类:Semaphore

    芋道源码
  • 死磕Java并发:J.U.C之并发工具类:Semaphore

    信号量Semaphore是一个控制访问多个共享资源的计数器,和CountDownLatch一样,其本质上是一个“共享锁”。

    程序猿DD
  • 【死磕Java并发】—–J.U.C之并发工具类:Exchanger

    前面三篇博客分别介绍了CyclicBarrier、CountDownLatch、Semaphore,现在介绍并发工具类中的最后一个Exchange。Exchan...

    用户1655470
  • 【死磕Java并发】—–J.U.C之并发工具类:CountDownLatch

    在上篇博客中介绍了Java四大并发工具一直的CyclicBarrier,今天要介绍的CountDownLatch与CyclicBarrier有点儿相似。 Cyc...

    用户1655470

扫码关注云+社区

领取腾讯云代金券