Java多线程”JUC”集合中的CopyOnWriteArraySet

本篇文章其实是网络编程的一个小作业,结合了CopyOnWriteArraySet的源码、API手册以及其它网友朋友的博客文章,算是一个类似于学习笔记的文章,如果其中有哪里理解不到位的希望读者不要介意。

在学习CopyOnWriteArraySet之前,我们先来了解一下什么是CopyOnWrite。CopyOnWrite简称“COW”,它是一种程序优化策略。其基本思想就是当大家一起共享某个资源的时候,只有当我想要修改这个资源中的内容时才会将其copy出去形成一个新的内容以后再修改,这是一种延时懒惰策略。从JDK1.5开始Java并发包提供了两个使用CopyOnWrite机制实现的并发容器,它们就是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器在很多并发场景中都显示出了自己的优越性。

那什么又是CopyOnWrite容器呢?根据字面翻译理解就是写时复制。简单一点的理解就是当我们想要向一个容器中修改其中的元素时,并不是直接在这个容器里直接操作,而是把这个容器里的元素复制到一个新的容器里以后再进行修改操作,待修改完毕以后再用新的容器去覆盖原有的容器。这样做的好处就是可以让多个程序并发的对CopyOnWrite容器进行读操作而且不需要加锁,它其实也是一种读写分离的操作。

在了解了CopyOnWrite容器以后,我们再来了解Java并发包中两个使用CopyOnWrite机制实现的并发容器的其中一个——CopyOnWriteArraySet。它是一个线程安全的无序集合,可以将他看成线程安全的HashSet。CopyOnWriteArraySet和HashSet都是继承同一个父类AbstractSet,不同的是CopyOnWriteArraySet是基于CopyOnWriteArrayList(动态数组)实现的,而HashSet是通过HashMap(散列表)实现的。

以下摘自Javadoc中对CopyOnWriteArraySet的描述:
A java.util.Set that uses an internal CopyOnWriteArrayList for all of its operations. Thus, it shares the same basic properties:
•It is best suited for applications in which set sizes generally stay small, read-only operations vastly outnumber mutative operations, and you need to prevent interference among threads during traversal.
•It is thread-safe.
•Mutative operations (add, set, remove, etc.) are expensive since they usually entail copying the entire underlying array.
•Iterators do not support the mutative remove operation.
•Traversal via iterators is fast and cannot encounter interference from other threads. Iterators rely on unchanging snapshots of the array at the time the iterators were constructed.

翻译一下,可以得出CopyOnWriteArraySet具有以下特性:
(1)它最适合于具有以下特征的应用程序:Set 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
(这里只读操作要远大于可变操作的原因是,在进行可变操作的时候要对容器里的元素进行加锁,加锁的时候其他程序并不能访问这个容器了,所以当频繁的进行可变操作,容器就会经常被加锁,大大降低了程序访问这个容器的效率)
(2)它是线程安全的。(对其进行可变操作的时候要对其加锁)
(3)因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
(4)迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等 操作。
(5)使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。

CopyOnWriteArraySet的数据结构:
《Java多线程”JUC”集合中的CopyOnWriteArraySet》

这里简单说明一下:
(1)Set 是对象的无序集聚,继承于Collection的接口。它是一个不允许有重复元素的集合。(Set是数学概念中的集合,所以不允许有重复的元素,而List是有序的队列因此可以有重复的元素)
(2)AbstractSet 是一个抽象类,它继承于AbstractCollection,AbstractCollection实现了Set中的绝大部分函数,为Set的实现类提供了便利。
(3)CopyOnWriteArraySet是继承于AbstractSet因此也是一个集合,其中也不允许有重复元素出现。
(4)CopyOnWriteArraySet包含了CopyOnWriteArrayList,它是通过CopyOnWriteArrayList来实现的,CopyOnWriteArrayList是一个动态数组队列,它和CopyOnWriteArraySet的区别就是CopyOnWriteArrayList中可以用重复的元素出现。

一会儿我们会对CopyOnWriteArraySet的源码来进行分析,到时可以发现CopyOnWriteArraySet是通过CopyOnWriteArrayList实现的,它的API基本上都是通过调用CopyOnWriteArrayList的API来实现的。既然CopyOnWriteArraySet是通过CopyOnWriteArrayList实现的,那么简单的了解一下CopyOnWriteArrayList的原理也就能够理解CopyOnWriteArraySet的原理了。

我们就从两个方面来简单的了解一下CopyOnWriteArrayList。
1. CopyOnWriteArrayList的“动态数组”机制 – 它内部有个“volatile数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”。这就是它叫做CopyOnWriteArrayList的原因!CopyOnWriteArrayList就是通过这种方式实现的动态数组;不过正由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很低;但是单单只是进行遍历查找的话,效率比较高。(这就和我们前面所说的CopyOnWrite容器的原理一致了,可以理解为CopyOnWriteArrayList和CopyOnWriteArraySet就是CopyOnWrite的具体实现方法了)
2.CopyOnWriteArrayList的“线程安全”机制 – 是通过volatile和互斥锁来实现的。
(1)CopyOnWriteArrayList是通过“volatile数组”来保存数据的。一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入;就这样,通过volatile提供了“读取到的数据总是最新的”这个机制的保证。
(关于volatile关键字,我们知道“volatile能让变量变得可见”,即对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。正在由于这种特性,每次更新了“volatile数组”之后,其它线程都能看到对它所做的更新。)
(2)CopyOnWriteArrayList通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile数组”中,然后再“释放互斥锁”;这样,就达到了保护数据的目的。
我们上面说到CopyOnWriteArraySet的API基本上都是通过调用CopyOnWriteArrayList的API来实现的。所以在简单了解了CopyOnWriteArrayList的原理以后相信读者对CopyOnWriteArraySet也有了更深层次的理解。

《Java多线程”JUC”集合中的CopyOnWriteArraySet》

《Java多线程”JUC”集合中的CopyOnWriteArraySet》

CopyOnWriteArraySet源码解析:

1.类的继承关系:

//继承AbstractSet<E>类
public class CopyOnWriteArraySet<E> extends AbstractSet<E> implements java.io.Serializable {}

2.类的属性:

private static final long serialVersionUID = 5457747651344034263L;//版本序列号
private final CopyOnWriteArrayList<E> al;
 //因为CopyOnWriteArraySet市通过CopyOnWriteArrayList实现的,所以先定义一个CopyOnWriteArrayList的动态数组

3.类的构造方法:

//创建一个空的Set
    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }
//创建一个包含指定 collection 所有元素的 set
    public CopyOnWriteArraySet(Collection<? extends E> c) {
        if (c.getClass() == CopyOnWriteArraySet.class) {
            // c集合为CopyOnWriteArraySet类型
            @SuppressWarnings("unchecked") CopyOnWriteArraySet<E> cc =
                (CopyOnWriteArraySet<E>)c;
            al = new CopyOnWriteArrayList<E>(cc.al);
            //调用CopyOnWriteArrayList的构造函数初始化cc的al
        }
        else {
            // c集合不为CopyOnWriteArraySet类型
            al = new CopyOnWriteArrayList<E>();
            //新生成CopyOnWriteArrayList并赋值给al
            al.addAllAbsent(c);
            // 添加c集合(c集合的元素在al中部存在时,才会添加)
        }
    }

4.类的方法
(1)

 public int size() {
        return al.size();
    }    //返回该集合中的元素个数

(2)

public boolean isEmpty() {
        return al.isEmpty();
    }  //如果集合中不包含元素,则返回true

(3)

public boolean contains(Object o) {
        return al.contains(o);
    }   //如果此 collection 包含指定的元素,则返回 true。

(4)

 public Object[] toArray() {
        return al.toArray();
    }     //返回包含此 collection 中所有元素的数组

说明:
从类 AbstractCollection 复制的描述
a.返回包含此 collection 中所有元素的数组。如果此 collection 保证其迭代器按顺序返回其元素,那么此方法也必须按相同的顺序返回这些元素。返回的数组将是“安全的”,因为此 collection 并不维护对返回数组的任何引用。(换句话说,即使 collection 受到数组的支持,此方法也必须分配一个新的数组)。因此,调用方可以随意修改返回的数组。
b.此实现会分配返回的数组,并迭代 collection 中的元素,将每个对象引用存储在数组的下一个连续元素中,并从元素 0 开始。
(5)

 public <T> T[] toArray(T[] a) {
        return al.toArray(a);
    }  //返回包含此 collection 中所有元素的数组

说明:
从类AbstractCollection复制的描述
a.返回包含此 collection 中所有元素的数组;返回数组的运行时类型是指定数组的类型。如果指定的数组能容纳该collection,则在此数组中返回 collection 的元素。否则,将根据指定数组的运行时类型和此 collection 的大小分配一个新数组。
b.如果指定的数组能容纳collection,并且有剩余的空间(即数组的元素比 collection 多),那么会将紧挨着collection尾部的元素设置为 null(这对确定collection的长度很有用,但只有 在调用方知道 collection 不包含任何null元素时才可行)。
e.此实现会检查该数组是否足够大,以包含该collection中的元素;如果不能包含,则将分配一个具有适当大小和类型的新数组(使用反射)。然后,在该collection上进行迭代,将每个对象引用存储在数组的下一个连续元素中,并从元素 0 开始。如果该数组比该collection大,则在该 collection尾部后面的第一个位置存储 null。
(6)

 public void clear() {
        al.clear();
    }   //从此 collection 中移除所有元素(可选操作)。

(7)

 public boolean remove(Object o) {
        return al.remove(o);
    }    //从此 collection 中移除指定元素的单个实例(如果存在)
        //如果该 collection 包含指定的元素,则返回 true。

(8)

public boolean add(E e) {
        return al.addIfAbsent(e);
    }   //确保此 collection 包含指定的元素(可选操作)。
        //如果此 collection 随调用的结果而发生改变,则返回 true。
        //如果此 collection 不允许有重复元素,并且已经包含了指定的元素,则返回 false。

(9)

public boolean containsAll(Collection<?> c) {
        return al.containsAll(c);
    }//如果此 collection 包含指定 collection 中的所有元素,则返回 true。
    //此实现在指定的 collection 上进行迭代,依次检查该迭代器返回的每个元素查看其是否包含在此 collection 中。如果包含所有元素,则返回 true;否则将返回 false。

(10)

 public boolean addAll(Collection<? extends E> c) {
        return al.addAllAbsent(c) > 0;
    } // 将指定 collection 中的所有元素添加到此 collection 中(可选操作)。
    //此实现在指定的 collection 上进行迭代,并依次将迭代器返回的每个对象添加到此 collection 中。

(11)

public boolean removeAll(Collection<?> c) {
        return al.removeAll(c);
    }//从此 set 中移除包含在指定 collection 中的所有元素(可选操作)。

说明:
通过在此set 指定collection上调用size方法,此实现可以确定哪一个更小。如果此 set 中的元素更少,则该实现将在此set上进行迭代,依次检查迭代器返回的每个元素,查看它是否包含在指定的collection中。如果包含它,则使用迭代器的remove方法从此set中将其移除。如果指定collection中的元素更少,则该实现将在指定的collection上进行迭代,并使用此set的remove方法,从此set中移除迭代器返回的每个元素。
(12)

 public boolean retainAll(Collection<?> c) {
        return al.retainAll(c);
    }//仅在此 collection 中保留指定 collection 中所包含的元素(可选操作)。

说明:
换句话说,移除此collection中未包含在指定collection中的所有元素。
此实现在此collection上进行迭代,依次检查该迭代器返回的每个元素,以查看其是否包含在指定的collection中。如果不是,则使用迭代器的remove方法将其从此collection中移除。
注意,如果iterator方法返回的迭代器无法实现remove方法,并且此 collection包含一个或多个在指定collection中不存在的元素,那么此实现将抛出UnsupportedOperationException。
(13)

 public Iterator<E> iterator() {
        return al.iterator();
    }//返回在此 collection 中的元素上进行迭代的迭代器。

分析完源码以后不难看出,CopyOnWriteArraySet确实是通过CopyOnWriteArrayList实现的,它的API基本上都是通过调用CopyOnWriteArrayList的API来实现的。

下面我们就先通过一个小的例子来了解一下CopyOnWriteArraySet的使用。

import java.util.concurrent.CopyOnWriteArraySet;

public class CopyOnWriteArraySetText {
    public static void main(String[] args) {
        CopyOnWriteArraySet<Integer> al = new CopyOnWriteArraySet<Integer >();
        int i , j =0 ;
        for(i=0;i<10;i++){
            j = j + 2;
            al.add(j);  //向al中添加元素
        }
        //同时启动两个线程,验证它的线程安全性。
        PutThread p1 = new PutThread(al);
        PutThread p2 = new PutThread(al);
        p1.start();
        p2.start();
    }
}
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArraySet;

public class PutThread extends Thread{
    private CopyOnWriteArraySet<Integer> al;
    //声明一个Integer类型的CopyOnWriteArraySet变量al

    public PutThread(CopyOnWriteArraySet<Integer> al) {
        super();
        this.al = al;
    }

    public void run() {
        int i ;
        int j = 0;
        for(i=0;i<10;i++){  //向al中添加元素
            j = j + 2;
            al.add(j);
        }

         //利用迭代器将其打印出来
        Iterator<Integer> iterator = al.iterator();
        while (iterator.hasNext()) {
            System.out.print(terator.next() + " ");
        }
  }    

结果:
《Java多线程”JUC”集合中的CopyOnWriteArraySet》

上面的程序中我们给al赋了两次值,且都是相同值,但是一个线程的结果中并没有出现重复的元素,这就是我们一开始说的CopyOnWriteArraySet中不允许有重复的元素出现,在我们调用add方法的时候就自动会检查是否有重复的元素存在,当没有该元素没有重复时才进行添加操作。
接下来我们在进行一下验证,我们把run()中的赋值改一下,看一下这次可不可以将两次的赋值结果都放到数组里。

public void run() {
        int i ;
        int j = 0;
        for(i=0;i<10;i++){  //向al中添加元素
            j = j + 3;
            al.add(j);
        }

结果:
《Java多线程”JUC”集合中的CopyOnWriteArraySet》
可见当两次赋值操作赋不同值的时候,两次的赋值结果都可以被添加进数组中。

CopyOnWriteArraySet可以理解为线程安全的HashSet,那么我们就用一个例子来对比一下两者,深入了解一下CopyOnWriteArr线程安全机制。
我们先将变量al定义为HashSet型,启动两个线程看看会出现什么结果。

import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

public class CopyOnWriteArraySetText {

    private static Set<String> al = new HashSet<String>();
    //private static Set<String> al = new CopyOnWriteArraySet<String>();

    public static void main(String[] args) {
        MyThread myThread1 = new MyThread("线程a");
        MyThread myThread2 = new MyThread("线程b");
        myThread1.start();

        myThread2.start();
    }
}
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

public class MyThread extends Thread{

    private static Set<String> al = new HashSet<String>();
    //private static Set<String> al = new CopyOnWriteArraySet<String>();

    public MyThread(String name) {
        super(name);
    }

    private static void printAll() {
        String value = null;
        Iterator iter = al.iterator();
        while(iter.hasNext()) {
            value = (String)iter.next();
            System.out.print(value+", ");
        }
        System.out.println();
    }
    @Override
    public void run() {
        int i = 0;
        while (i++ < 10) {
            // “线程名” + "-" + "序号"
            String val = Thread.currentThread().getName() + "-" + (i);
            al.add(val);
            // 通过“Iterator”遍历al。
            printAll();
        }
    }       
}

结果:
《Java多线程”JUC”集合中的CopyOnWriteArraySet》
程序无法正常执行,抛出了ConcurrentModificationException异常。
然后我们把变量al定义为CopyOnWriteArraySet型的,再看一下结果是什么。

//private static Set<String> al = new HashSet<String>();
    private static Set<String> al = new CopyOnWriteArraySet<String>();

《Java多线程”JUC”集合中的CopyOnWriteArraySet》
结果可以正常执行,两个程序可以并发执行。

    原文作者:JUC
    原文地址: https://blog.csdn.net/xue_xiao/article/details/78508534
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞