Java线程安全:CopyOnWriteArraySet应用详解,对比HashSet优势凸显
Java线程安全

—CopyOnWriteArraySet应用详解
龚建鹏150342208
概述:
CopyOnWriteArraySet就如同线程安全的 HashSet 一样,它是一个线程安全的集合,既无序又不可重复。并且,CopyOnWriteArraySet 和 HashSet 都继承了共同的父类 AbstractSet。
CopyOnWriteArraySet 的 Javadoc 里有这样一段话:
它最适合用于这样的应用场景:集合的大小通常保持较小,只读操作远远多于可变操作,并且在遍历过程中需要防止线程之间的干扰。
•It is thread-safe.
通过迭代器进行遍历速度很快,并且不会遇到其他线程的干扰。迭代器依赖于在创建迭代器时数组的不变快照。
翻译过来就是:
使用内部的 CopyOnWriteArrayList 的 java.util.Set 来进行所有操作。所以,它具备相同的基本属性:
CopyOnWriteArraySet的UML图:
说明:
CopyOnWriteArraySet继承自 AbstractSet,所以它是一个集合。
CopyOnWriteArraySet 是一种集合,其所有操作都使用内部的 CopyOnWriteArrayList。所以,CopyOnWriteArraySet 相当于由动态数组实现的“集合”,并且它里面的元素不能重复。
CopyOnWriteArraySet 的“线程安全”机制是依靠 volatile 以及互斥锁来达成的。
CopyOnWriteArraySet的方法摘要:
// 创建一个空 set。
CopyOnWriteArraySet()
创建一个集合,这个集合包含指定的 collection 里的所有元素。
CopyOnWriteArraySet 包含一个集合(Collection)。 extends E> c)
//将指定元素添加到此列表的尾部。
boolean add(E e)
按照指定 collection 的迭代器所返回元素的顺序,把指定 collection 中的所有元素添加到这个列表的尾部。
boolean addAll(Collection extends E> c)
//从此列表移除所有元素。
void clear()
//如果此列表包含指定的元素,则返回 true。
boolean contains(Object o)
如果此列表包含指定 collection 的所有元素,那么就返回 true。
判断是否包含所有元素。判断一个集合是否包含另一个集合中的所有元素。判断某个集合是否包含给定集合中的全部元素。判断某集合是否包含另一个集合里的所有元素。判断某一集合是否包含另一集合中的所有元素。> c)
//比较指定对象与此列表的相等性。
boolean equals(Object o)
对 Iterable 的元素逐个进行执行给定的操作,直至所有元素都被处理完或者操作抛出异常。
void forEach(Consumer super E> action)
//如果此 set 不包含任何元素,则返回 true。
boolean isEmpty()
返回一个迭代器,这个迭代器会按照元素在这个 set 中被添加的顺序来对包含的元素进行迭代。
Iterator iterator()
//如果指定元素存在于此 set 中,则将其移除。
boolean remove(Object o)
从这个 set 里移除所有在指定 collection 中的元素。
boolean removeAll(Collection> c)
//删除满足条件的所有这个集合的元素
boolean removeIf(Predicate super E> filter)
从这个 set 中挑选出那些包含在指定 collection 里的元素。
boolean retainAll(Collection> c)
//返回此 set 中的元素数目。
int size()
按照这些元素的添加顺序,返回此集合中元素的 Spliterator 。
Spliterator spliterator()
//返回一个包含此 set 所有元素的数组。
Object[] toArray()
返回一个数组,该数组包含此 set 的所有元素;返回的数组的运行时类型是指定数组的类型。
T[] toArray(T[] a)
CopyOnWriteArraySet源代码分析:
从 CopyOnWriterArraySet 的成员变量能看出它包含着 CopyOnWriterArrayList 对象,并且它是由 CopyOnWriterArrayList 来实现的。CopyOnWriterArrayList 本质上是一个动态数组队列。前面说过,CopyOnWriterArraySet 相当于通过动态数组实现的“集合”。在 CopyOnWriterArrayList 中允许有重复的元素,然而在 CopyOnWriterArraySet 中元素不能重复。
对 CopyOnWriterArraySet 的主要方法展开分析:
无参构造函数的作用是对它的成员变量 CopyOnWriterArrayList 进行实例化。
这个类的构造函数是无参的。它创建了一个新的 CopyOnWriteArraySet 对象。这个对象可以用于存储一组元素,并且在添加或删除元素时,会创建一个新的数组副本,而不是直接修改原数组。这样可以在多线程环境下提供高效的并发访问。
al = new CopyOnWriteArrayList();
}
/*
创建一个集合,这个集合包含指定 collection 的所有元素。
* @param c 最初包含元素的collection
如果指定的元素为空,那么就会抛出 NullPointerException 异常。
*/
这个集合是 CopyOnWriteArraySet 类型的。它接收一个 Collection 作为参数。 extends E> c) {
如果指定的 collection 中的元素是无序的,那么就创建一个 CopyOnWriteArraySet 对象。如果指定的 collection 中的元素是有序的,就不创建 CopyOnWriteArraySet 对象。
接着利用 CopyOnWriteArrayList 的有参构造函数来给成员变量赋予相应的值。
如果不满足判断条件,那就直接创建一个 CopyOnWriteArrayList 对象,接着通过 addAllAbsent() 方法。
在指定的 collection 中,找出那些不在此列表里的元素,然后把这些元素添加到 List 列表的尾部。
*/
如果 c 的类是 CopyOnWriteArraySet 类。
使用 @SuppressWarnings("unchecked") 这个注解。它用于抑制特定类型的编译器警告。在某些情况下,当我们进行泛型操作时,可能会出现类型擦除等问题导致编译器发出警告,但我们确定这些警告不会影响程序的正确性,就可以使用这个注解来消除这些警告。
CopyOnWriteArraySet cc = (CopyOnWriteArraySet) c;
al = new CopyOnWriteArrayList(cc.al);
} else {
al = new CopyOnWriteArrayList();
addAllAbsent()方法会依据指定 collection 的迭代器所返回元素的顺序,把指定的元素添加进来。 它会逐个检查指定 collection 中的元素,若当前元素在目标集合中不存在,就将其添加进去。 此方法的作用就是确保目标集合包含了指定 collection 中所有不在其中的元素。
将 collection 中那些不在此列表里的所有元素添加到列表的尾部。
al.addAllAbsent(c);
}
}
/*
按照添加这些元素的先后顺序,会返回此集合中元素的 Spliterator,即 Spliterator(splitable )
Iterator(可分割迭代器)接口是 Java 为了能够并行遍历数据源中的元素而专门设计的。Iterator 是一个特定的概念。
* 顺序遍历迭代器
Spliterator 接口通过递归的方式将并行任务拆分成更小的子任务,接着把每个子任务的结果进行合并。
* 生成整体结果
*/
public Spliterator spliterator() {
Spliterators 是一个用于对 Spliterator 进行操作或创建的静态类。
Spliterator()方法通过使用一组自定义的分割器特性来创建一个能够覆盖给定数组元素的 Spliterator。
使用 Spliterators 的 spliterator 方法,传入 al.getArray() 以及 Spliterator.IMMUTABLE 和 Spliterator.DISTINCT 这两个标志,然后返回该 spliterator。
}
CopyOnWriteArraySet 的大多数方法是借助它的成员变量 CopyOnWriterArrayList 对象来实现的。接下来,我将为你介绍其中一些主要的方法。
/*
返回一个数组,该数组包含这个列表中的所有元素,并且这些元素的顺序是正确的,是从第一个元素到最后一个元素。
如果数组的长度比列表的长度大,那么就把列表里的内容变成数组,同时把剩下的空地方设置为 null 。
如果给定的数组长度比列表长度小,那就直接利用 Arrays 类的 copyOf()方法把列表里的内容转变成指定数组。
*/
当代码可能存在类型不安全的情况时,可以使用它来避免编译器发出警告。
public T[] toArray(T a[]) {
获取数组,数组的元素为 elements。
int len = elements.length;
if (a.length < len)
return (T[]) Arrays.copyOf(elements, len, a.getClass());
else {
System.arraycopy(elements, 0, a, 0, len);
if (a.length > len)
a[len] = null;return a;
}
}
// 清除列表中的所有元素
public void clear() {
ReentrantLock 是一个可重入的互斥锁 Lock,它具备和 synchronized 一样的功能,然而它的功能更为强大。
这个类中的锁是 this.lock,而最终的重入锁 lock 就是它。
// 使该对象获得锁并执行后续操作lock.lock();
try {
setArray(new Object[0]);
}
finally {
lock.unlock();
}
}
对 Iterable 的每个元素都执行给定的操作,一直执行到处理完所有元素或者操作抛出异常为止。
public void forEach(Consumer super E> action) {
先判断参数,如果参数不存在,就不需要往下进行操作了,可以直接结束或者抛出异常。
在写自己的方法时应当具备这种思想,首先要对条件进行判别,接着再开始后续的操作。
如果 action 为 null ,那么就抛出一个空指针异常。
获取数组的操作,得到的数组元素存储在 Object[] 类型的变量 elements 中。
int len = elements.length;
for (int i = 0; i < len; ++i) {
@SuppressWarnings("unchecked")
E e = (E) elements[i];
action.accept(e);
}
}
//删除满足条件的所有这个集合的元素
public boolean removeIf(Predicate super E> filter) {
如果 filter 为 null ,那么就抛出一个 NullPointerException 。
this.lock 最终是一个 ReentrantLock 类型的锁,即 final ReentrantLock lock = this.lock;
lock.lock();
try {
获取数组的操作,得到了一个对象数组 elements 。
int len = elements.length;
if (len != 0) {
int newlen = 0;
创建了一个名为 temp 的 Object 数组,其长度为 len 。
for (int i = 0; i < len; ++i) {
@SuppressWarnings("unchecked")
//将测试不满足的元素存放在临时数组temp中
E e = (E) elements[i];
if (!filter.test(e))
temp[newlen++] = e;
}
//newlen是现在temp数组中元素的个数,len是原列表中元素的个数;
//如果两个值不一样,说明已经从该列表中删除了某个元素,这样将没删除的元素继续存放在列表
//中并返回true;
//如果以上的情况都不符合,说明要么列表中没有元素,无元素可删;要么就是没有符合删除条件
//的元素,所以返回false;
if (newlen != len) {
setArray(Arrays.copyOf(temp, newlen));
return true;
}
}
return false;
} finally {
lock.unlock();
}
}
我在此仅介绍一些新增的以及较为重要的方法。其他一些方法较为简单,你们能够通过 API 手册自行理解。在此我不再进行赘述。若有兴趣,可自行尝试去做。
CopyOnWriteArraySet的示例:
com.gong.copyonwritearrayset 这个包的相关内容。
import java.util.Iterator;
import java.util.Set;
引入了 Java 中的并发集合类 CopyOnWriteArraySet。
/**
CopyOnWriteArraySet 是一种集合,它具有“线程安全”的特性。而 HashSet 这种集合则是非线程安全的。
*
* 下面是“多个线程同时操作并且遍历集合set”的示例
当 set 为 CopyOnWriteArraySet 对象时,程序可以正常运行。
当 set 为 HashSet 对象时,程序会引发 ConcurrentModificationException 异常。
*
* @author jpgong
*
*/
类名为 CopyOnWriteArraySetTest 的类。
定义一个对象为 CopyOnWriteArraySet,其作用是存放线程内容。
一个名为 private static 的 CopyOnWriteArraySet set = new CopyOnWriteArraySet();
公共静态的 void 主方法(字符串数组参数)抛出中断异常。
// 同时启动两个线程对set进行操作!
创建了一个名为 test 的 CopyOnWriteArraySetTest 类的实例。
创建一个名为"ta"的新线程并启动它。具体来说,是通过"test.new WriteThread("ta")"创建线程对象,然后调用"start()"方法来启动该线程。
让一个进程进入睡眠模式。等待一个进程结束。接着执行下一个进程。
// Thread.sleep(600);
启动名为“tb”的写线程 test.new.WriteThread("tb")。
}
它会完成特定的任务并输出相关结果。
String value = null;
//返回按照元素添加顺序在此 set中包含的元素迭代器。
Iterator iter = set.iterator();
//将当前集合中存在的元素通过迭代器输出
while(iter.hasNext()) {
value = (String)iter.next();
输出 value 并在其后加上 ", " 。
}
System.out.println();
}
/**
定义一个写进程,其作用是往 CopyOnWriteArraySet 对象里写入内容。
* @author jpgong
*
*/
WriteThread 是一个继承自 Thread 的类。它具有特定的功能和行为。这个类用于执行写入相关的任务。它在多线程环境中发挥作用,通过继承 Thread 类来实现多线程的特性。
WriteThread(String name) {
super(name);
}
@Override
public void run() {
int i = 0;
while (i++ < 5) {
// “线程名” + "-" + "序号",将写好的元素添加到Set对象上去
String val = Thread.currentThread().getName() + "-" + i;
set.add(val);
// 通过“Iterator”遍历set。
printAll();
}
}
}
}
结果:
只运行一个进程:
ta-1,
ta-1, ta-2,
ta-1, ta-2, ta-3,
ta-1, ta-2, ta-3, ta-4,
ta-1, ta-2, ta-3, ta-4, ta-5,
两个进程一起运行:
ta-1, ta-1, tb-1, tb-1,
ta-1, ta-1, tb-1, tb-1, ta-2,
ta-2, ta-1, tb-2,
tb-2, ta-1, ta-3, tb-1, tb-3,
ta-2 以及 ta-1,还有 tb-2 和 tb-1,接着 ta-3 以及 ta-2,再有 tb-3 和 tb-2,最后 ta-4 。
ta-3 以及 ta-1,还有 tb-3 和 tb-1,接着 ta-2 以及 tb-2,之后 ta-3 与 tb-3,再之后 ta-4 和 tb-4,最后 ta-5
ta-4, tb-4,
当仅有一个进程在运行时,我们能够看到 set 集合中不存在重复元素。因为在这个进程开始运行的时候,每当存入一个元素,就会把该集合中的所有元素输出出来,不存在两个线程之间的同步问题。当我启动两个进程时,会出现上述情况。这是因为每个进程都是独立的,不存在哪个进程优先级更高的情况。所以,谁获得了 CPU 的资源,谁就会执行其操作,从而出现这种情况。不过,每个 set 集合中的元素并非重复的,这点一定要牢记。我再次尝试了让一个进程暂时进入休眠状态,等另一个进程运行完毕之后,这个进程再开始运行。这样做能够更清晰地观察到它们的执行过程。并且从图中可以看出,Set 集合中确实没有重复的元素。