1 内容概要
2 ArrayList集合线程不安全
2.1 ArrayList集合操作Demo
- 代码演示
/**
* list集合线程不安全
*/
public class ThreadDemo4 {
public static void main(String[] args) {
// 创建ArrayList集合
List<String> list = new ArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
// 向集合中添加内容
list.add(UUID.randomUUID().toString().substring(0,8));
// 从集合中获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
-
运行结果
-
异常内容
java.util.ConcurrentModificationException
-
问题:为什么会出现并发修改异常
查看ArrayList的add方法源码:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
那么我们如何去解决 List 类型的线程安全问题?
2.2 解决方案:Vector
Vector 是矢量队列,它是 JDK1.0 版本添加的类。继承于 AbstractList,实现了 List, RandomAccess, Cloneable 这些接口。 Vector 继承了 AbstractList,实现了 List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能。 Vector 实现了 RandmoAccess 接口,即提供了随机访问功能。
RandmoAccess 是 java 中用来被 List 实现,为 List 提供快速访问功能的。在Vector 中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。 Vector 实现了 Cloneable 接口,即实现 clone()函数。它能被克隆。
和 ArrayList 不同,Vector 中的操作是线程安全的。
- 代码修改1:Vector实现
/**
* Vector实现:list集合线程安全
*/
public class ThreadDemo4 {
public static void main(String[] args) {
// 创建ArrayList集合
List<String> list = new Vector<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
// 向集合中添加内容
list.add(UUID.randomUUID().toString().substring(0,8));
// 从集合中获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
- 现在没有运行出现并发异常,为什么?
查看 Vector 的 add 方法
/**
* Appends the specified element to the end of this Vector.
*
* @param e element to be appended to this Vector
* @return {@code true} (as specified by {@link Collection#add})
* @since 1.2
*/
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
add 方法被 synchronized 同步修辞,线程安全!因此没有并发异常
2.3 解决方案:Collections
Collections 提供了方法 synchronizedList 保证 list 是同步线程安全的
- 代码修改2:Collections实现
/**
* Collections实现:list集合线程安全
*/
public class ThreadDemo4 {
public static void main(String[] args) {
// 创建ArrayList集合
List<String> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 30; i++) {
new Thread(()->{
// 向集合中添加内容
list.add(UUID.randomUUID().toString().substring(0,8));
// 从集合中获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
- 没有并发修改异常
- 查看方法源码
/**
* Returns a synchronized (thread-safe) list backed by the specified
* list. In order to guarantee serial access, it is critical that
* <strong>all</strong> access to the backing list is accomplished
* through the returned list.<p>
*
* It is imperative that the user manually synchronize on the returned
* list when iterating over it:
* <pre>
* List list = Collections.synchronizedList(new ArrayList());
* ...
* synchronized (list) {
* Iterator i = list.iterator(); // Must be in synchronized block
* while (i.hasNext())
* foo(i.next());
* }
* </pre>
* Failure to follow this advice may result in non-deterministic behavior.
*
* <p>The returned list will be serializable if the specified list is
* serializable.
*
* @param <T> the class of the objects in the list
* @param list the list to be "wrapped" in a synchronized list.
* @return a synchronized view of the specified list.
*/
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
2.4 解决方案:CopyOnWriteArrayList
首先我们对 CopyOnWriteArrayList 进行学习,其特点如下:
它相当于线程安全的 ArrayList。和 ArrayList 一样,它是个可变数组;但是和ArrayList 不同的时,它具有以下特性:
- 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多
于可变操作,需要在遍历期间防止线程间的冲突。 - 它是线程安全的。
- 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove()
等等)的开销很大。 - 迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
- 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
- 思想和原理
- 独占锁效率低:采用读写分离思想解决
- 写线程获取到锁,其他写线程阻塞
- 复制思想:当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器
这就是 CopyOnWriteArrayList 的思想和原理。就是拷贝一份。
- 代码修改3:CopyOnWriteArrayList实现
/**
* CopyOnWriteArrayList实现:list集合线程安全
*/
public class ThreadDemo4 {
public static void main(String[] args) {
// 创建ArrayList集合
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
// 向集合中添加内容
list.add(UUID.randomUUID().toString().substring(0,8));
// 从集合中获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
- 没有线程安全问题
- 原因分析(重点):动态数组与线程安全
下面从“动态数组”和“线程安全”两个方面进一步对CopyOnWriteArrayList 的原理进行说明
- “动态数组”机制
- 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile 数组”, 这就是它叫做CopyOnWriteArrayList 的原因
- 由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList 效率很低;但是单单只是进行遍历查找的话,效率比较高。
- “线程安全”机制
- 通过 volatile 和互斥锁来实现的
- 通过“volatile 数组”来保存数据的。一个线程读取 volatile 数组时,总能看到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读取到的数据总是最新的”这个机制的保证
- 通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥锁”,就达到了保护数据的目的
3 HashSet集合线程不安全
3.1 HashSet集合操作Demo
- 代码演示
/**
* HashSet集合线程不安全
*/
public class ThreadDemo4 {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
// 想集合添加内容
set.add(UUID.randomUUID().toString().substring(0,8));
// 从集合获取内容
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
-
运行结果
-
异常内容
java.util.ConcurrentModificationException
-
问题:为什么会出现并发修改异常
查看HashSet的add方法源码:
/**
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element <tt>e</tt> to this set if
* this set contains no element <tt>e2</tt> such that
* <tt>(e==null ? e2==null : e.equals(e2))</tt>.
* If this set already contains the element, the call leaves the set
* unchanged and returns <tt>false</tt>.
*
* @param e element to be added to this set
* @return <tt>true</tt> if this set did not already contain the specified
* element
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
那么我们如何去解决Set类型的线程安全问题?
3.2 解决方案:Collections
Collections 提供了方法 synchronizedSet保证 list 是同步线程安全的
- 代码修改1:Collections实现
/**
* Collections实现:HashSet集合线程安全
*/
public class ThreadDemo4 {
public static void main(String[] args) {
Set<String> set = Collections.synchronizedSet(new HashSet<>());
for (int i = 0; i < 30; i++) {
new Thread(()->{
// 想集合添加内容
set.add(UUID.randomUUID().toString().substring(0,8));
// 从集合获取内容
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
- 没有并发修改异常
- 查看方法源码
/**
* Returns a synchronized (thread-safe) set backed by the specified
* set. In order to guarantee serial access, it is critical that
* <strong>all</strong> access to the backing set is accomplished
* through the returned set.<p>
*
* It is imperative that the user manually synchronize on the returned
* set when iterating over it:
* <pre>
* Set s = Collections.synchronizedSet(new HashSet());
* ...
* synchronized (s) {
* Iterator i = s.iterator(); // Must be in the synchronized block
* while (i.hasNext())
* foo(i.next());
* }
* </pre>
* Failure to follow this advice may result in non-deterministic behavior.
*
* <p>The returned set will be serializable if the specified set is
* serializable.
*
* @param <T> the class of the objects in the set
* @param s the set to be "wrapped" in a synchronized set.
* @return a synchronized view of the specified set.
*/
public static <T> Set<T> synchronizedSet(Set<T> s) {
return new SynchronizedSet<>(s);
}
3.3 解决方案:CopyOnWriteArraySet
- 代码修改2:CopyOnWriteArraySet实现
/**
* CopyOnWriteArraySet实现:HashSet集合线程安全
*/
public class ThreadDemo4 {
public static void main(String[] args) {
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
// 想集合添加内容
set.add(UUID.randomUUID().toString().substring(0,8));
// 从集合获取内容
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
- 没有线程安全问题
- 实现原理
- 读操作无锁:
- CopyOnWriteArraySet 的读操作(如 contains、iterator 等)不需要加锁,因为读操作总是基于当前的数组快照进行,不会受到写操作的影响。
- 写操作加锁并复制:
- 当进行写操作(如 add、remove 等)时,CopyOnWriteArraySet 会先获取锁,然后创建底层数组的新副本,在新副本上进行修改操作,最后将新副本替换原来的数组。
- 由于写操作是在新副本上进行的,因此不会影响正在进行读操作的线程,从而保证了线程安全。
- 迭代器安全:
- CopyOnWriteArraySet 的迭代器是“快照”迭代器,它基于创建迭代器时的数组快照进行遍历,因此不会抛出ConcurrentModificationException。
- 即使迭代器创建后集合发生了修改,迭代器仍然会基于创建时的快照进行遍历,不会受到后续修改的影响。
4 HashMap集合线程不安全
4.1 HasgMap集合操作Demo
- 代码演示
/**
* HashMap集合线程不安全
*/
public class ThreadDemo4 {
public static void main(String[] args) {
//演示HashMap
Map<String,String> set = new HashMap<>();
for (int i = 0; i < 30; i++) {
String key = String.valueOf(i);
new Thread(()->{
// 想集合添加内容
set.put(key,UUID.randomUUID().toString().substring(0,8));
// 从集合获取内容
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
-
运行结果
-
异常内容
java.util.ConcurrentModificationException
-
问题:为什么会出现并发修改异常
查看HashMap的put方法源码:
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
那么我们如何去解决 Map 类型的线程安全问题?
4.2 解决方案:HashTable
- 代码修改1:HashTable实现
/**
* HashTable实现:HashMap集合线程安全
*/
public class ThreadDemo4 {
public static void main(String[] args) {
//演示HashMap
Map<String,String> set = new HashTable<>();
for (int i = 0; i < 30; i++) {
String key = String.valueOf(i);
new Thread(()->{
// 想集合添加内容
set.put(key,UUID.randomUUID().toString().substring(0,8));
// 从集合获取内容
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
- 现在没有运行出现并发异常,为什么?
查看 HashTable 的 put方法
/**
* Maps the specified <code>key</code> to the specified
* <code>value</code> in this hashtable. Neither the key nor the
* value can be <code>null</code>. <p>
*
* The value can be retrieved by calling the <code>get</code> method
* with a key that is equal to the original key.
*
* @param key the hashtable key
* @param value the value
* @return the previous value of the specified key in this hashtable,
* or <code>null</code> if it did not have one
* @exception NullPointerException if the key or value is
* <code>null</code>
* @see Object#equals(Object)
* @see #get(Object)
*/
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
4.2 解决方案:Collections
- 代码修改2:Collections实现
/**
* Collections实现:HashMap集合线程安全
*/
public class ThreadDemo4 {
public static void main(String[] args) {
//演示HashMap
Map<String,String> set = Collections.synchronizedMap(new HashMap<>());
for (int i = 0; i < 30; i++) {
String key = String.valueOf(i);
new Thread(()->{
// 想集合添加内容
set.put(key,UUID.randomUUID().toString().substring(0,8));
// 从集合获取内容
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
- 没有并发修改异常
- 查看方法源码
/**
* Returns a synchronized (thread-safe) map backed by the specified
* map. In order to guarantee serial access, it is critical that
* <strong>all</strong> access to the backing map is accomplished
* through the returned map.<p>
*
* It is imperative that the user manually synchronize on the returned
* map when iterating over any of its collection views:
* <pre>
* Map m = Collections.synchronizedMap(new HashMap());
* ...
* Set s = m.keySet(); // Needn't be in synchronized block
* ...
* synchronized (m) { // Synchronizing on m, not s!
* Iterator i = s.iterator(); // Must be in synchronized block
* while (i.hasNext())
* foo(i.next());
* }
* </pre>
* Failure to follow this advice may result in non-deterministic behavior.
*
* <p>The returned map will be serializable if the specified map is
* serializable.
*
* @param <K> the class of the map keys
* @param <V> the class of the map values
* @param m the map to be "wrapped" in a synchronized map.
* @return a synchronized view of the specified map.
*/
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
4.3 解决方案:ConcurrentHashMap
- 代码修改3:CopyOnWriteHashMap实现
/**
*CopyOnWriteHashMap实现:HashMap集合线程安全
*/
public class ThreadDemo4 {
public static void main(String[] args) {
//演示HashMap
Map<String,String> set = new ConcurrentHashMap<>();
for (int i = 0; i < 30; i++) {
String key = String.valueOf(i);
new Thread(()->{
// 想集合添加内容
set.put(key,UUID.randomUUID().toString().substring(0,8));
// 从集合获取内容
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
- 没有线程安全问题
- 实现原理
- 读操作无锁:
- 读操作(如 get、containsKey 等)不需要加锁,因为读操作总是基于当前的 HashMap 快照进行,不会受到写操作的影响。
- 写操作加锁并复制:
- 当进行写操作(如 put、remove 等)时,CopyOnWriteHashMap 会先获取锁,然后创建底层 HashMap 的新副本,在新副本上进行修改操作,最后将新副本替换原来的 HashMap。
- 由于写操作是在新副本上进行的,因此不会影响正在进行读操作的线程,从而保证了线程安全。
- 迭代器安全:
- CopyOnWriteHashMap 的迭代器是“快照”迭代器,它基于创建迭代器时的 HashMap 快照进行遍历,因此不会抛出 ConcurrentModificationException。
- 即使迭代器创建后集合发生了修改,迭代器仍然会基于创建时的快照进行遍历,不会受到后续修改的影响。
5 总结
- 线程安全与线程不安全集合
集合类型中存在线程安全与线程不安全的两种,常见例如:
ArrayList ----- Vector
HashMap -----HashTable
但是以上都是通过 synchronized 关键字实现,效率较低 - Collections 构建的线程安全集合
- java.util.concurrent 并发包下
CopyOnWriteArrayList、CopyOnWriteArraySet 类型,通过动态数组与线程安全两个方面保证线程安全