Java学习笔记——集合III

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中的实现

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方法相对较为复杂,这里采用图示的方式对于其进行简单的解释

这里对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,分别放置在对应的位置)
     }
 }
    

以上就是对于集合类的线程安全问题,以及相关解决方法的一些简单整理,欢迎大家批评指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值