您的位置:首页 > 编程语言 > Java开发

Java线程安全之CopyOnWriteArraySet 应用详解

2017-11-12 20:17 495 查看
                             Java线程安全

                      —CopyOnWriteArraySet应用详解
                  
                                                                                             龚建鹏  150342208
概述:
             


             CopyOnWriteArraySet相当于线程安全的HashSet,它是一个线程安全的无序、不可重复集合。CopyOnWriteArraySet和HashSet都继承共同的父类AbstractSet。
            在CopyOnWriteArraySet的Javadoc中有这么一段话:
   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.

 
翻译过来就是:
    对所有操作使用内部CopyOnWriteArrayList的java.util.Set。 因此,它具有相同的基本属性:

•它最适合应用程序的集合大小通常很小,只读操作远远超过可变操作,并且需要防止在遍历期间线程之间的干扰。

•线程安全。

•突变操作(添加,设置,删除等)是昂贵的,因为它们通常需要复制整个底层阵列。

迭代器不支持可变删除操作。

•迭代器遍历速度快,不会受到来自其他线程的干扰。 迭代器构建时迭代器依赖于数组的不变快照。

CopyOnWriteArraySet的UML图:         

                                           


       说明:
    1、CopyOnWriteArraySet继承于AbstractSet,这就意味着它是一个集合。
    2、因为CopyOnWriteArraySet是所有操作都使用内部CopyOnWriteArrayList的Set集合,所以CopyOnWriteArraySet相当于动态数组实现的“集合”,它里面的元素不能重复。
    3、CopyOnWriteArraySet的“线程安全”机制是通过volatile和互斥锁来实现的。

CopyOnWriteArraySet的方法摘要:

// 创建一个空 set。
CopyOnWriteArraySet()
// 创建一个包含指定 collection 所有元素的 set。
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。
boolean containsAll(Collection<?> c)
//比较指定对象与此列表的相等性。
boolean equals(Object o)
//对Iterable的每个元素执行给定的操作,直到处理完所有元素或操作抛出异常。
void forEach(Consumer<? super E> action)
//如果此 set 不包含任何元素,则返回 true。
boolean isEmpty()
//返回按照元素添加顺序在此 set 中包含的元素上进行迭代的迭代器。
Iterator<E> 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<E> spliterator()
//返回一个包含此 set 所有元素的数组。
Object[] toArray()
// 返回一个包含此 set 所有元素的数组;返回数组的运行时类型是指定数组的类型。
<T> T[] toArray(T[] a)


CopyOnWriteArraySet源代码分析:
    (1)从CopyOnWriterArraySet的成员变量可以看出它包含CopyOnWriterArrayList对象,它是通过CopyOnWriterArrayList实现的。而CopyOnWriterArrayList本质是一个动态数组队列,所以正如前面说过的CopyOnWriterArraySet相当于通过动态数组实现的“集合”。只是CopyOnWriterArrayList中允许有重复的元素;但CopyOnWriterArraySet中元素不能重复。

 


   (2)对CopyOnWriterArraySet的主要方法进行分析:
//无参构造函数,就是对它的成员变量CopyOnWriterArrayList实例化
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}

/*
* 创建一个包含指定 collection 所有元素的 set
* @param c 最初包含元素的collection
* @throws NullPointerException 如果指定的元素为空则抛出异常
*/
public CopyOnWriteArraySet(Collection<? extends E> c) {
/** 如果指定collection中的元素是无序的,就创建一个CopyOnWriteArraySet对象,
* 然后通过CopyOnWriteArrayList的有参构造函数对成员变量进行赋值
* 如果不符合判断条件,则直接创建一个CopyOnWriteArrayList对象,然后通过addAllAbsent()
* 将指定collection中尚未包含在此列表中的元素添加到List列表尾部
*/
if (c.getClass() == CopyOnWriteArraySet.class) {
@SuppressWarnings("unchecked")
CopyOnWriteArraySet<E> cc = (CopyOnWriteArraySet<E>) c;
al = new CopyOnWriteArrayList<E>(cc.al);
} else {
al = new CopyOnWriteArrayList<E>();
//addAllAbsent()方法是按照指定 collection 的迭代器返回元素的顺序,将指定
//collection 中尚未包含在此列表中的所有元素添加列表的尾部
al.addAllAbsent(c);
}
}

/*
* 按照添加这些元素的顺序,返回此集合中的元素的Spliterator Spliterator(splitable
* iterator可分割迭代器)接口是Java为了并行遍历数据源中的元素而设计的迭代器 Interator是一个
* 顺序遍历迭代器
* Spliterator接口是用递归的方式把并行的任务拆分成更小的子任务,然后把每个子任务的结果合并起来
* 生成整体结果
*/
public Spliterator<E> spliterator() {
// Spliterators是用于操作或创建Spliterator的一个静态类
// spliterator()方法使用一组自定义的分割器特性创建一个覆盖给定数组元素的Spliterator。
return Spliterators.spliterator(al.getArray(), Spliterator.IMMUTABLE | Spliterator.DISTINCT);
}

// ---CopyOnWriteArraySet的大多数方法都是通过他的成员变量CopyOnWriterArrayList对象实现的,下面我将把其中一些主要的方法介绍一下-----//

/*
* 返回一个包含这个列表中的所有元素的数组正确的顺序(从第一个到最后一个元素)
* 如果给定的数组长度大于该列表长度,则将列表中的内容转换为数组,并将剩余的空余空间设置为null
* 如果给定的数组长度小于列表长度,则直接通过Arrays类的copyOf()方法把列表中的内容转换为指定数组
*/
@SuppressWarnings("unchecked")
public <T> T[] toArray(T a[]) {
Object[] elements = getArray();
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相同的功能,但它的功能更强大
final ReentrantLock lock = this.lock;
// 使该对象获得锁并执行后续操作lock.lock();
try {
setArray(new Object[0]);
}
finally {
lock.unlock();
}
}

//对Iterable的每个元素执行给定的操作,直到处理完所有元素或操作抛出异常
public void forEach(Consumer<? super E> action) {
//先对参数进行判断,如果不存在则不需要往下做了,可以直接结束或者抛出异常
//在自己写方法时也应该有这种思想,首先对条件进行判别然后再开始接下来的操作
if (action == null) throw new NullPointerException();
Object[] elements = getArray();
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) {
if (filter == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (len != 0) {
int newlen = 0;
Object[] temp = new 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的示例:
package com.gong.copyonwritearrayset;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

/**
* CopyOnWriteArraySet是“线程安全”的集合,而HashSet是非线程安全的。
*
* 下面是“多个线程同时操作并且遍历集合set”的示例
* (01) 当set是CopyOnWriteArraySet对象时,程序能正常运行。
* (02) 当set是HashSet对象时,程序会产生ConcurrentModificationException异常。
*
* @author jpgong
*
*/
public class CopyOnWriteArraySetTest {

//定义一个CopyOnWriteArraySet对象,用来存放线程内容。
private static CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<String>();

public static void main(String[] args) throws InterruptedException {
// 同时启动两个线程对set进行操作!
CopyOnWriteArraySetTest test = new CopyOnWriteArraySetTest();
test.new WriteThread("ta").start();
//让一个进程先进入睡眠模式,等一个进程结束后再执行下一个进程
// Thread.sleep(600);
test.new WriteThread("tb").start();
}

private static void printAll() {
String value = null;
//返回按照元素添加顺序在此 set中包含的元素迭代器。
Iterator<String> iter = set.iterator();
//将当前集合中存在的元素通过迭代器输出
while(iter.hasNext()) {
value = (String)iter.next();
System.out.print(value + ", ");
}
System.out.println();
}

/**
* 定义一个写进程,用于往CopyOnWriteArraySet对象中写内容
* @author jpgong
*
*/
class WriteThread extends 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-1, ta-1, ta-2, tb-1, tb-2, ta-2, ta-3,
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,
ta-1, tb-1, ta-2, tb-2, ta-3, tb-3, ta-4, tb-4, ta-5, tb-5,


             当只有一个进程运行时我们可以看到set集合中没有重复元素,因为该进程开始运行时,每存入一个元素就会将该集合中的所有元素输出,不存在两个线程的同步问题。但当我启动两个进程时,就会出现上面的情况,这是因为每个进程都是独立的,没有哪个进程的优先级更高,所以当谁获得了CPU的资源谁就会执行它的操作,因此才会出现上面这种情况,但每个set集合中的元素并不是重复的,这个一定要牢记。我又尝试了将一个进程暂时休眠,等一个进程运行完毕之后它再运行,这样就可以更清楚的观察到他们的执行过程。从图中也可以看到Set集合中的确没有重复的元素。

         
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息