前言:
上一篇博客我们发现并发情况下使用ArrayList线程不安全,那么Set集合在多线程环境下,是否线程安全呢?后面还会对HashSet的源码进行浅析,这就是今天我们所要学习和讨论的问题!
1.1 测试Set集合是否线程安全
1. 首轮Set集合安全测试
1-1 测试代码
package com.kuang.unsafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @ClassName SetTest
* @Description Set安全性测试
* @Author 狂奔の蜗牛rz
* @Date 2021/7/30
*/
public class SetTest {
public static void main(String[] args) {
//获取一个HashSet集合
Set<String> set = new HashSet<>();
//模拟多线程环境(首轮测试中我们现将循环次数设置为10)
for (int i = 1; i <= 10; i++) {
//使用Lambda表达式创建线程
new Thread(()->{
//向set集合中添加随机字符串元素(截取下标0到5的元素)
set.add(UUID.randomUUID().toString().substring(0,5));
//打印set集合的输出结果
System.out.println(set);
//获取下标为i的字符串,并启动线程
},String.valueOf(i)).start();
}
}
}
1-2 测试结果
结果:没有出现异常,并不符合预期结果!
1-3 结果分析
似乎是我们的循环次数太少,导致没有出现异常,所以我们加大循环次数重新测试,看一看会不会抛出异常!
2. 加大循环次数重新测试
2-1 测试代码
package com.kuang.unsafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @ClassName SetTest
* @Description Set安全性测试
* @Author 狂奔の蜗牛rz
* @Date 2021/7/30
*/
public class SetTest {
public static void main(String[] args) {
//获取一个HashSet集合
Set<String> set = new HashSet<>();
//模拟多线程环境(加大循环次数为20)
for (int i = 1; i <= 20; i++) {
//使用Lambda表达式创建线程
new Thread(()->{
//向set集合中添加随机字符串元素(截取下标0到5的元素)
set.add(UUID.randomUUID().toString().substring(0,5));
//打印set集合的输出结果
System.out.println(set);
//获取下标为i的字符串,并启动线程
},String.valueOf(i)).start();
}
}
}
2-2 测试结果
结果:抛出ConcurrentModificationException(并发修改异常)!
2-3 测试结论
使用Set集合时也抛出ConcurrentModificationException(并发修改异常),
因此我们可以得出结论: Set集合线程不安全!
3.解决方案
- 使用Collections集合工具类的 synchronizedSet()方法
- 使用 CopyOnWriteArraySet(写时复制数组Set集合)
1.2 解决Set集合线程不安全问题
1.使用Collections工具类的synchronizedList方法
1-1 源码分析
- 查看Collections工具类中 synchronizedSet()方法的相关源码
package java.util;
/**
*
* @author Josh Bloch
* @author Neal Gafter
* @see Collection
* @see Set
* @see List
* @see Map
* @since 1.2
*/
//Collections工具类
public class Collections {
// 抑制默认的构造函数, 确保其非实例化
private Collections() {
}
// ...(省略前面部分代码)...
//静态的SynchronizedCollection(同步集合类)
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
//连续版本UID
private static final long serialVersionUID = 3053995032091335093L;
//返回的Collection集合
final Collection<E> c;
//同步互斥量
final Object mutex;
/**
* 同步Collection集合构造方法(只有一个参数)
* @param c Collection集合
*/
SynchronizedCollection(Collection<E> c) {
//参数c(Collection集合)不能为空
this.c = Objects.requireNonNull(c);
mutex = this;
}
/**
* 同步Collection集合构造方法(包含两个参数)
* @param c Collection集合
* @param mutex 同步互斥量
*/
SynchronizedCollection(Collection<E> c, Object mutex) {
//参数c(Collection集合)不能为空
this.c = Objects.requireNonNull(c);
//参数mutex(同步互斥量)不能为空
this.mutex = Objects.requireNonNull(mutex);
}
//...(省略后面部分代码)...
}
// ...(省略中间部分代码)...
/**
* synchronizedSet构造方法(只包含一个参数)
* 如果指定的Set集合是可序列化的,则返回的Set集合将会被序列化
* @param <T> set集合中对象的类型
* @param s 被包装的在同步Set中的Set集合
* @return a 指定set集合的同步视图
*/
public static <T> Set<T> synchronizedSet(Set<T> s) {
return new SynchronizedSet<>(s);
}
/**
* synchronizedSet构造方法(包含两个参数)
* @param <T> set集合中对象的类型
* @param s 被包装的在同步set中的set集合
* @param mutex 同步互斥量
* @return a 指定set集合的同步视图
*/
static <T> Set<T> synchronizedSet(Set<T> s, Object mutex) {
return new SynchronizedSet<>(s, mutex);
}
/**
* @serial include
*/
//SynchronizedSet(同步Set集合类)
static class SynchronizedSet<E>
//继承SynchronizedCollection(同步Collection集合)
extends SynchronizedCollection<E>
//实现了Set集合接口
implements Set<E> {
//私有静态最终的长整型的serialVersionUID(连续版本UID)
private static final long serialVersionUID = 487447009682186044L;
/**
* 同步Set集合有参构造(一个参数)
* @param s Set集合
*/
SynchronizedSet(Set<E> s) {
super(s);
}
/**
* 同步Set集合有参构造(两个参数)
* @param s Set集合
* @param mutex 互斥量/锁
*/
SynchronizedSet(Set<E> s, Object mutex) {
super(s, mutex);
}
//equals方法
public boolean equals(Object o) {
//判断对象值是否相等
if (this == o)
return true;
//同步代码块
synchronized (mutex) {
//返回集合和对象值是否相等
return c.equals(o);
}
}
//hashCode方法
public int hashCode() {
synchronized (mutex) {return c.hashCode();}
}
}
// ...(省略后面部分代码)...
}
1-2 测试代码
package com.kuang.unsafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @ClassName SetTest
* @Description Set安全性测试
* @Author 狂奔の蜗牛rz
* @Date 2021/7/30
*/
public class SetTest {
public static void main(String[] args) {
//获取一个HashSet集合
// Set<String> set = new HashSet<>();
//方案1: 使用Collections集合工具类的synchronizedSet方法
Set<String> set = Collections.synchronizedSet(new HashSet<>());
//模拟多线程环境
for (int i = 1; i <= 20; i++) {
//使用Lambda表达式创建线程
new Thread(()->{
//向set集合中添加随机字符串元素(截取下标0到5的元素)
set.add(UUID.randomUUID().toString().substring(0,5));
//打印set集合的输出结果
System.out.println(set);
//获取下标为i的字符串,并启动线程
},String.valueOf(i)).start();
}
}
}
1-3 测试结果
结果:执行成功,没有出现异常!
2. 使用CopyOnWriteArrayList集合
2-1 CopyOnWriteArraySet简单了解
- JDK 8 API文档中CopyOnWriteArraySet类的位置
- API文档中对CopyOnWriteArraySet类的简单介绍
2-2 CopyOnWriteArraySet源码分析
- 查看CopyOnWriteArraySet集合类源码
package java.util.concurrent;
import java.util.Collection;
import java.util.Set;
import java.util.AbstractSet;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Predicate;
import java.util.function.Consumer;
/**
* 此类属于Java集合框架的一员
* @see CopyOnWriteArrayList
* @since 1.5
* @author Doug Lea
* @param <E> 容纳这个集合中所有元素类型
*/
//CopyOnWriteArraySet(写时复制数组Set集合)
public class CopyOnWriteArraySet<E> extends AbstractSet<E> implements java.io.Serializable {
//连续版本UID
private static final long serialVersionUID = 5457747651344034263L;
//写时复制数组List集合
private final CopyOnWriteArrayList<E> al;
/**
* 创建一个空的Set集合.
*/
public CopyOnWriteArraySet() {
//本质上还是创建了一个CopyOnWriteArrayList(写时复制数组List集合)
al = new CopyOnWriteArrayList<E>();
}
//...(省略中间部分代码)...
/**
* 创建一个包含所有指定集合的元素的Set集合
* @param c 初始化包含的元素集合
* @throws 如果指定集合为空,抛出NullPointerException空指针异常
*/
public CopyOnWriteArraySet(Collection<? extends E> c) {
//判断c(包含初始化元素的集合)的类是否和CopyOnWriteArraySet(写时发复制数组Set集合)相同
if (c.getClass() == CopyOnWriteArraySet.class) {
//抑制不检查警告
@SuppressWarnings("unchecked")
//如果相同,将c进行强制类型转换为CopyOnWriteArraySet<E>
CopyOnWriteArraySet<E> cc = (CopyOnWriteArraySet<E>)c;
al = new CopyOnWriteArrayList<E>(cc.al);
}
else {
//如果不相同,创建一个新的CopyOnWriteArrayList<E>集合
al = new CopyOnWriteArrayList<E>();
//添加所有的缺席(不包含在当前list集合中)元素到al集合中去
al.addAllAbsent(c);
}
}
/**
* 在指定的集合中,追加所有的不包含在当前list集合中的元素,到list集合的尾部,
* 通过指定集合的迭代器,按照顺序将它们进行返回
* @param c 包含被添加到list集合中元素的集合
* @return 添加元素的数量
* @throws 如果指定集合为空,抛出NullPointerException空指针异常
* @see #addIfAbsent(Object)
*/
public int addAllAbsent(Collection<? extends E> c) {
//转换成cs数组
Object[] cs = c.toArray();
//判断长度是否为0
if (cs.length == 0)
//若为0,返回0
return 0;
//获取一个ReentrantLock重入锁
final ReentrantLock lock = this.lock;
//首先上锁
lock.lock();
try {
//获取元素数组
Object[] elements = getArray();
//元素长度
int len = elements.length;
//添加的元素数量
int added = 0;
//在cs数组中实例唯一化且紧凑的元素
for (int i = 0; i < cs.length; ++i) {
//获取数组中下标为i的元素
Object e = cs[i];
//判断
if (indexOf(e, elements, 0, len) < 0 &&
indexOf(e, cs, 0, added) < 0)
//如果条件满足,将e(元素)添加到cs
cs[added++] = e;
}
//判断添加元素数量是否大于0
if (added > 0) {
//复制一份原数组中元素(包括原数量+添加数量)到新数组(newElements)中
Object[] newElements = Arrays.copyOf(elements, len + added);
//
System.arraycopy(cs, 0, newElements, len, added);
//设置数组
setArray(newElements);
}
//返回添加的元素数量
return added;
} finally {
//最终解锁
lock.unlock();
}
}
// ...(省略中间部分代码)...
/**
* indexOf的静态版本, 允许重复调用,不需要每次重新获取数组
* @param o 搜索的元素
* @param elements 数组
* @param index 搜索的第一个元素的索引
* @param fence 过去的最后一个元素的索引
* @return 元素的索引下标, 如果元素不存在返回-1
*/
private static int indexOf(Object o, Object[] elements, int index, int fence) {
//判断搜索的元素o是否为空
if (o == null) {
//若为空,遍历之前的元素(范围为index(第一个元素索引)到fence(过去的最后一个元素索引))
for (int i = index; i < fence; i++)
//判断下标为i的元素是否为空
if (elements[i] == null)
//返回索引下标
return i;
} else {
//如果搜素的元素o不为空
//遍历之前的元素
for (int i = index; i < fence; i++)
//判断搜素的元素o是否等于数组中下标为i的元素
if (o.equals(elements[i]))
//若相等,将下标i进行返回
return i;
}
//如果搜索的元素不存在,则返回-1
return -1;
}
//...(省略后面部分代码)...
}
Set集合使用CopyOnWriteArrayList(写时数组List集合)所有的内部操作,因此它们有相同的基本属性
使用案例:
//Handler处理器类
class Handler { void handle(); ... }
//测试类X
class X {
//创建一个泛型为Handler的CopyOnWriteArraySet集合
private final CopyOnWriteArraySet<Handler> handlers = new CopyOnWriteArraySet<Handler>();
/**
* 添加处理器方法
* @param h Handler处理器
*/
public void addHandler(Handler h) { handlers.add(h); }
//长整型的internalState(内部状态)
private long internalState;
//使用了synchronized同步锁修饰的changeState(改变状态)方法
private synchronized void changeState() {
//"..."表示可变长参数,可以传入任意个该类型参数,简单来说就是个数组
internalState = ...;
}
//更新
public void update() {
//改变状态
changeState();
//遍历handlers集合
for (Handler handler : handlers) {
//调用集合子元素handler的handle方法
handler.handle();
}
}
}
2-3 CopyOnWriteArraySet集合的使用
- 测试代码:
package com.kuang.unsafe;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @ClassName SetTest
* @Description Set安全性测试
* @Author 狂奔の蜗牛rz
* @Date 2021/7/30
*/
public class SetTest {
public static void main(String[] args) {
//获取一个HashSet集合
// Set<String> set = new HashSet<>();
//方案2: 使用CopyOnWriteArraySet(写时复制数组Set集合)
Set<String> set = new CopyOnWriteArraySet<>();
//模拟多线程环境
for (int i = 1; i <= 20; i++) {
//使用Lambda表达式创建线程
new Thread(()->{
//向set集合中添加随机字符串元素(截取下标0到5的元素)
set.add(UUID.randomUUID().toString().substring(0,5));
//打印set集合的输出结果
System.out.println(set);
//获取下标为i的字符串,并启动线程
},String.valueOf(i)).start();
}
}
}
- 测试结果:
结果:执行成功,没有出现异常!
最后,我们来浅析一下HashSet的源码,HashSet底层也是面试会经常问到的,所以不看过源码,你怎么能说你很懂HashSet?
3. HashSet源码分析
3-1 查看HashSet集合源码
package java.util;
/**
* <p>该类是Java集合框架中的一员</p>
* @param <E> 该Set集合维护的元素类型
* @author Josh Bloch
* @author Neal Gafter
* @see Collection
* @see Set
* @see TreeSet
* @see HashMap
* @since 1.2
*/
//HashSet集合类
public class HashSet<E>
extends AbstractSet<E> //继承AbstractSet<E>(抽象Set集合)类
implements Set<E>, Cloneable, java.io.Serializable //实现Set集合接口和序列化接口
{
//静态最终的持续版本UID
static final long serialVersionUID = -5024744406713321676L;
//获取一个HashMap对象
/**
* 与常用的HashMap中Key-Value(键值对)不同的是,
* 这里的HashMap的key是E(即Element,表示集合中存放的元素)
* 这里的Value是Object类(它是所有类的父类,数组也是Object类的子类)
*/
//使用transient修饰HashMap(表示序列化对象的时候,这个属性就不会被序列化)
private transient HashMap<E,Object> map;
//在支持的Map中,和一个object对象相关联的虚拟值(PRESENT, 即已存在对象)
private static final Object PRESENT = new Object();
/**
* 构造一个新的、空的set集合,背后的HashMap实例有默认的初始容量(16)和装载因子(0.75)
*/
public HashSet() {
map = new HashMap<>();
}
/**
* 构造有个新的set集合包含指定集合中的元素
* HashMap被创建时默认装载因子为(0.75),并且初始容量足够包含指定集中合的元素
* @param c 数组元素被放置在当前Set集合中的Collection集合
* @throws 如果指定集合为空,则抛出NullPointerException(空指针异常)
*/
public HashSet(Collection<? extends E> c) {
//创建一个HashMap(最大值是集合大小/0.75,初始数组大小为16)
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
//添加所有的元素
addAll(c);
}
/**
* 构造一个新的、空的Set集合,后面的HashMap实例有指定初始容量和指定装载因子
* @param initialCapacity HashMap的初始容量
* @param loadFactor HashMap的装载因子
* @throws IllegalArgumentException 如果初始容量小于0,或者装载因子为负数, 抛出IllegalArgumentException(不合法的参数异常)
*/
public HashSet(int initialCapacity, float loadFactor) {
//创建HashMap对象,并设置其初始容量和装载因子大小
map = new HashMap<>(initialCapacity, loadFactor);
}
/**
* 构造一个新的、空的Set集合,后面的HashMap实例有指定初始容量和默认装载因子为0.75
* @param initialCapacity 哈希表的初始容量
* @throws 如果初始容量小于0,抛出IllegalArgumentException(非法参数异常)
*/
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
/**
* 构建一个新的、空的LinkedHashSet(这个包私有的构造器只被LinkedHashSet使用) The backing
* 后面的HashMap实例是一个有初始容量和转载因子的LinkedHashMap
* @param initialCapacity HashMap的初始容量
* @param loadFactor HashMap的装载因子
* @param dummy 忽略(将此构造器与其他的int和float型构造器区分开)
* @throws 如果初始容量小于0, 或者装载因子为负数, 抛出IllegalArgumentException(不合法异常)
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
//创建HashMap对象,并设置其初始容量和装载因子大小
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
/**
* 返回该Set集合中迭代的元素, 元素未按指定顺序返回
* @return 该Set集合中迭代的元素
* @see ConcurrentModificationException(并发修改异常)
*/
public Iterator<E> iterator() {
return map.keySet().iterator();
}
/**
* 返回set集合中元素的数量(它的基数)
* @return set集合中的元素数量(它的基数)
*/
public int size() {
//map的大小
return map.size();
}
/**
* 如果set集不包含元素, 则返回值为true
* @return 布尔型值true或false, 如果set集合不包含元素,则返回true
*/
public boolean isEmpty() {
//map为空
return map.isEmpty();
}
/**
* 如果Set集合中包含指定元素,则返回布尔型值true.
* 更正式的讲, 如果该Set集合包含一个元素e,
* 例如(o==null ? e==null : o.equals(e)), 则返回true
* 使用三元运算符: 判断o(要测试的元素)是否为空, 若为空, 则e元素不存在, 否则测试元素o的值等于元素e
* More formally, returns <tt>true</tt> if and only if this set
* contains an element <tt>e</tt> such that
* <tt>(o==null ? e==null : o.equals(e))</tt>.
*
* @param o 要测试在该集合中存在的元素
* @return 布尔型值true或者false 如果包含指定元素, 则返回true
*/
public boolean contains(Object o) {
//map中是否包含测试元素的key
return map.containsKey(o);
}
/**
* 如果指定元素尚未出现, 则将其添加到该Set集合中
* 更正式的讲, 如果该Set集合不包含元素e2, 添加指定元素e到该Set集合中
* 例如:(e==null ? e2==null : e.equals(e2)) 使用三元运算符表达式:
* 判断e(添加到该集合中元素)是否为空, 若为空, 则e2(集合中未包含元素)为空, 否则e元素值等于e2
* 如果Set集合已经包含该元素, 该调用使集合保持不变并返回false
* @param e 被添加到该集合中的元素
* @return 布尔型值true或者false 如果该Set集合中不包含指定的元素, 则返回true
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
/**
* 如果指定元素已存在, 将其从该Set集合中移除
* Removes the specified element from this set if it is present.
* 更正式的讲, 移除一个元素e, 例如: (o==null ? e==null : o.equals(e))
* 使用三元运算符: 如果测试元素o为空, 则元素e为空, 否则测试元素o的值等于元素e
* 如果该Set集合包含元素(或为等效值, 如果该Set集合因调用结果发生改变)
* (一旦返回调用, 该Set集合将不能包含元素.)
* @param o 如果存在, object对象将从该Set集合中移除
* @return 布尔型值true或false 如果Set集合包含指定元素, 则返回true
*/
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
/**
* 移除该Set集合中的所有元素
* 返回调用后该Set集合将清空
*/
public void clear() {
map.clear();
}
/**
* 返回该HashSet实例的浅拷贝: 元素本身没有被克隆
* @return 该Set集合的浅拷贝
*/
//抑制未检查警告
@SuppressWarnings("unchecked")
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (HashMap<E, Object>) map.clone();
return newSet;
//捕获CloneNotSupportedException(克隆不被支持异常)
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
...(省略后面部分代码,如果想了解更多,请去查阅相关源码)...
}
3-2 HashSet的底层是什么?
HashSet的底层实际上是一个HashMap
与常用的HashMap中Key-Value(键值对)不同的是:它的key是 E,即Element, 表示集合中存放的元素 ; 而它的Value是 Object类,它是所有类的父类,数组也是Object类的子类
3-3 HashSet的主要特点是什么?
- HashSet是 Set集合接口的实现类,由哈希表支持 (实际上是一个HashMap实例);
- 它不保证set集合的迭代顺序,特别是,它不能保证顺序会随时间保持不变;
- HashSet允许元素为空,对基础操作 (例如add, remove, contains和size方法) 提供持续时间表现;
3-4 为什么说不能设置初始容量太高(或者装载因子太低)?
假设哈希函数正确的在桶中分散元素, 那么遍历该Set集合所需的时间和
HashSet实例大小(元素的数量)加上支持的HashMap实例的“容量”(桶的数量)之和成正比例;
因此,如果重视迭代性能,不设置初始容量太高(或者装载因子太低)是至关重要的
3-5 HashSet是同步的吗?
HashSet实现类是不同步的
如果多个线程并发访问一个HashSet,并且至少有一个线程修改了该Set集合,它必须从外部同步; 这通常在一些对象上同步来实现,自然的封装了Set集合.
3-6 对象不存在怎么实现同步?
如果object对象不存在, Set集合应该使用Collections集合工具类的synchronizedSet方法进行包装,最好在创建时完成,以防意外的非同步访问Set集合:
Set s = Collections.synchronizedSet(new HashSet(...));
3-7 迭代器的快速失效行为是指什么?
通过HashSet的迭代器方法返回的迭代器们是fail-fast(快速失效)的; 在迭代器创建后,如果Set集合在任何时间被修改, 使用任何方法(除通过迭代器自己的remove方法外),迭代器将抛出一个ConcurrentModificationException(并发修改异常);
因此,在面对并发修改时,迭代器更加的快速、干净的失效, 而不是在将来的不确定时间,冒着任意的, 不确定行为的风险.
fail-fast(快速失效)行为会力所能及的抛出ConcurrentModificationException(并发修改异常), 因此为了编写程序的正确性而依赖这个异常是错误的;迭代器的fail-fast(快速失效)行为应该只用于去检测错误
到这里,今天的有关Set集合线程安全的学习就结束了,欢迎大家学习和讨论!
参考视频链接:https://www.bilibili.com/video/BV1B7411L7tE (B站UP主遇见狂神说的JUC并发编程基础)