Java学习笔记——集合III
线程安全的集合类
集合的线程安全问题
fast-fail 机制 —— 多个线程(通常情况下),在结构上对集合进行改变时。就有可能触发fast-fail机制,抛出java.util.ConcurrentModificationException异常
代码实例
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
//多线程对list操作
//线程A对list进行遍历
new Thread(() ->{
for (Integer i : list) {
System.out.println(i); //Exception in thread "A" java.util.ConcurrentModificationException
}
} , "A").start();
//线程B对list进行添加操作
new Thread(() ->{
for (int i = 0; i < 10; i++) {
list.add(i);
}
},"B").start();
}
以ArrayList为例对集合进行遍历时,当调用内部迭代器的next方法时,会先调用checkForComodification方法
int expectedModCount = modCount; //开始遍历时,将modCount复制给expectedModCount
//而当调用add、remove等方法时 modCount会执行++ 操作
final void checkForComodification() {
if (modCount != expectedModCount) //当遍历时 ,如果其他线程对modCount进行了更改,则会使次条件为真
throw new ConcurrentModificationException();
}
常见的集合类,HashMap、LinkedList、ArrayList均为线程不安全的线程实现类
解决线程安全的三种方式
1)采用线程安全的集合类如Vector、HashTable等、此类集合中增删改查等方法均为同步方法
//Vector类中的get方法,使用synchronized同步
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
但也因此,效率较低,现在较少采用
2)应用Collections.synchronizedList( List list ) / synchronizedMap(Map map) 方法
public E get(int index) {
synchronized (mutex) {return list.get(index);} //本质也是将集合中的方法进行同步
}
3)采用JUC包下提供的集合类CopyOnWriteArrayList 、 ConcurrentLinkedQueue、ConcurrentHashMap
CopyOnWriteArrayList
java.util.concurrent包下的线程安全的ArrayList
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 底层依然是数组,只不过是volatile
private transient volatile Object[] array;
// 实例维护了一个可重入锁
final transient ReentrantLock lock = new ReentrantLock();
//获取size()、get()等方法并没有进行同步
public int size() {
return getArray().length;
}
}
add 、 remove等方法,进行了同步
add方法
//默认的add方法(在队尾添加)
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); //复制了一份数组,长度为原数组长度加一
newElements[len] = e; //将欲添加的值赋值给数组的末尾
setArray(newElements); //将新的数组重新赋值给array
return true;
} finally {
lock.unlock();
}
}
remove方法
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
//新的数组长度为原数组长度减一
Object[] newElements = new Object[len - 1];
//将除欲删除元素之外的元素拷贝到新数组中
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
//将新数组赋值给array
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
set等方法类似
总结:
CopyOnWriteArrayList中,采用了读写分离的方式,对于读/获取长度等方法并未同步,而对于增删改等方法,采取了同步的方式,复制一份数组,对新的数组进行操作,操作完之后再将新的数组赋值给array,此过程中,并不影响其他线程的读操作。对于大多数场景下,此种方法即兼顾了效率,又解决了线程安全的问题。
ConcurrentHashMap
ConcurrentHashMap 同是juc包下的集合类,可以看作是线程安全的ConcurrentHashMap
该类在JDK1.7 与 JDK1.8 中的实现有所不同。
jdk1.7中的实现
![](https://i-blog.csdnimg.cn/blog_migrate/1b84d0d63ce39c63264319e238532c41.png)
jdk1.7中该类的实现原理就是将数据一段一段的存储,然后把每一段数据配一把锁。当以个线程占用锁访问其中一段数据的时候,其他段的数据也能被锁访问
初始化
1)初始化segment数组 长度通过ConcurrencyLevel得出(一定为2^n)
2)初始化segmentShift与segmentMask —— 用于定位segment
3)初始化每个segment —— HashEntry数组(默认大小为 容量 / segments的长度(取>= 其的2^n))
相关操作
get操作 —— 不加锁(变量定义为volatile) —— 直接读取
put操作 —— 对segment加锁(同样、扩容对segment扩容)
获取size —— 两次不加锁的获取size()结果相同 ->返回 不同 所有segment加锁,再计算一次
jdk1.8中的实现
jdk1.8中ConcurrentHashMap的实现和之前有所不同,取消了Segment的概念,采用CAS与Synchronized的方式进行加锁,对集合的线程安全进行保障
一些重要的成员参数
简单列举部分ConcurrentHashMap中重要的变量
/*
sizeCtl:
0 未初始化,且数组的初始容量为16
正数 初始容量 / 扩容阈值 (容量 * 加载因子)
-1 正在进行初始化
其他负数 (-1 + n)正在扩容的线程个数 = n
*/
private transient volatile int sizeCtl;
// 静态内部类Node —— 替换HashEntry
static class Node<K,V> implements Map.Entry<K,V>
// table Node数组
transient volatile Node<K,V>[] table;
// 另一个Node数组 , 仅在扩容的时候非空
private transient volatile Node<K,V>[] nextTable;
// BaseCount 用于统计Size
private transient volatile long baseCount;
// CounterCell 底层维护一个volatile的long值,用于统计Size
static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
// 内部类ForwardingNode 代表数组正在扩容
static final class ForwardingNode<K,V> extends Node<K,V>
初始化
该类的初始化数组长度,计算方法与之前版本以及HashMap的计算方法有所不同
相同的是ConcurrentHashMap也在第一次调用put方法之后,才对数组进行初始化
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
//tableSizeFor方法返回大于输入参数且最接近的2的整数次幂的数
//不同于HashMap,该类的容量为1.5倍参数 + 1的大于等于其的2的整数次幂
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
相关操作
put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
putVal方法相对较为复杂,这里采用图示的方式对于其进行简单的解释
![](https://i-blog.csdnimg.cn/blog_migrate/9b74d80283efbf9c163e94411e75d573.png)
这里对addCount方法进行一个简单的解释
private final void addCount(long x, int check) {}
//首先会尝试对BaseCount进行修改
U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x))
//如果失败则尝试随机对于CounterCells 计数盒子中的数进行修改
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)))
// 如若再失败,调用fullAddCount方法,循环修改CounterCells 知道成功位置
统计大小 size()
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
// 将baseCount值与CounterCell的值相加返回,方法并非同步方法,相关变量为volatile变量
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
get方法与之前版本类似,并没有加锁
扩容
1.8中的扩容,仍然采用分治的方式,每个线程负责一部分(扩容线程最多为CPU核数),每个线程最少负责的桶数为16,以下,对于扩容具体的实现进行了简单的解释
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
//stride —— 每个线程需要负责的桶的个数
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // 最小值为16
if (nextTab == null) {...} // 如果nextTab为空,就先创建一个数组,长度为原数组的2倍(n << 1)
// ForwardingNode用于标识桶是否已被扩容
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 具体扩容的过程,bound,i 用于标识索引与边界
for (int i = 0, bound = 0;;) {
while (advance) {} // 内部用于定位本轮需要处理的桶区间
}
if (i < 0 || i >= n || i + n >= nextn) {} //最后一个扩容线程的一些收尾工作
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd); // 桶位为空,直接方式forwardingNode
else if ((fh = f.hash) == MOVED)
advance = true; // 遇到fwd,同样跳过
else {
synchronized (f) {} //否则,对于头节点加锁,进行扩容(与HashMap类似两个链表loTail、hiTail,分别放置在对应的位置)
}
}
以上就是对于集合类的线程安全问题,以及相关解决方法的一些简单整理,欢迎大家批评指正。