Java基础SE(一) 数据类型与关键字
Java基础SE(二) 集合
Java基础SE(三) 线程与并发
Java基础SE(三) 线程与并发-补充
Java基础SE(四) IO
集合
List
List是Java中最常用的集合类(容器),List本身是一个接口,其继承了Collection接口,并表示有序的队列。常用的List实现有ArrayList、LinkedList、Vector
List | null值 | 稳定性(order) | 有序性(sort) | 线程安全(safe) |
---|---|---|---|---|
ArrayList | yes | yes | no | no |
LinkedList | yes | yes | no | no |
Vector | yes | yes | no | yes |
ArrayList底层是用数组实现的,可以认为ArrayList是一个可改变大小的数组。随着越来越多的元素被添加到ArrayList中,其规模是动态增加的。ArrayList还实现了RandomAccess接口,获得了快速随机访问存储元素的功能
LinkedList底层是通过双向链表实现的。所以LinkedList和ArrayList之前的区别主要就是数组和链表的区别。数组中查询和赋值比较快,因为可以直接通过数组下标访问指定位置(寻址快);链表中删除和增加比较快(数组的动态扩容会比较慢,然而随着JDK发展,已经差不多了),因为可以直接通过修改链表的指针(引用)进行元素的增删。LinkedList还实现了Queue(Deque)接口,是一个双向队列,所以他还提供了offer()、peek()、poll()等方法
Vector和ArrayList一样,都是通过数组实现的,但是Vector是线程安全的,其中的很多方法都通过同步(synchronized)处理来保证线程安全,因此相对而言性能会差一点。它们的扩容大小也不同,默认ArrayList是增长原来的50%,Vector则增长原来的100%
扩容
ArrayList扩容:
// add时候传入最小扩容长度为size + 1,空列表时为10
private void ensureExplicitCapacity(int minCapacity) {
// 版本号++
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 内部用数组维护
transient Object[] elementData;
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 默认增加一半,即原值的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 拷贝到新列表,底层还是用的System.arraycopy本地方法
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
LinkedList由于是双向链表结构,因此不需要扩容,只需要分别设置前节点,后节点即可
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
对于Vector,基本上是个同步的ArrayList,不过扩容因子是增加1倍,即原值的2倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
其他
ArrayList删除:
public E remove(int index) {
rangeCheck(index);
// transient 修改版本号
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
// 拷贝后面的到前面
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 最后一位置为null,然后size-1
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
Set
Set是类似于List,又相较有点特殊的集合,里面存放的值是不重复的、大部分是无序的(按照哈希值来存的所以取数据也是按照哈希值获取)
Set | null值 | 稳定性(order) | 有序性(sort) | 线程安全(safe) |
---|---|---|---|---|
HashSet | yes | no | no | no |
LinkedHashSet | yes | yes | no | no |
TreeSet | no | no | yes | no |
HashSet内部维护了一个HashMap,将值传入其key,以传入值的hash值来保证唯一性。因此传入HashSet的对象需要实现hashcode和equals方法。putVal方法会使用对象的hashCode来判断对象加入的位置,即如果对象的hashCode值是不同的,那么就可以认为对象是不可能相等的。如果对象的hashCode相等,那么还会继续使用equals进行比较,如果为false那么依然认为新加入的对象没有重复,将以链状方式进行保存,否则即认为元素相同无法插入
public boolean add(E e) {
// HashMap# return putVal(hash(key), key, value, false, true);
return map.put(e, PRESENT)==null;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
LinkedHashSet继承自HashSet,不同点在于其创建调用父类HashSet的特殊构造方法时创建LinkedHashMap作为存储介质,因此它是稳定的,元素顺序是可以保证的,其他与HashSet一致。插入、删除操作相较HashSet会略慢,因为要维护链表,但相对有了链表的存在,遍历速度会更快
TreeSet其内部存储介质为NavigableMap,构造方法中创建TreeMap(继承自NavigableMap),是红黑树结构,每一个元素都是树中的一个节点,插入的元素都会进行排序,这保证了元素是有序的。它是SortedSet,其元素必须实现Comparable或继承Comparator,也具备了元素搜索功能。TreeSet判断元素重复并不是通过hashCode和equals,而是通过compare的结果来判断的。TreeSet支持两种排序方式:自然排序、定制排序。由于TreeSet需要额外的红黑树算法来维护集合元素的次序,因此性能不如HashSet
public boolean add(E e) {
// TreeMap#put
return m.put(e, PRESENT)==null;
}
以上Set都是线程不安全的,通常可以通过Collections工具类的synchronizedSet、synchronizedSortedSet、synchronizedNavigableSet方法来包装Set集合
由于Set是基于Map来实现的,因此详细在Map中讨论
Map
Map里存放key与value的映射信息,元素是成对存在的,就像一个字典一样,通过目录key查询内容value。它体现了一组关系或分组
Map | null值 | 稳定性(order) | 有序性(sort) | 线程安全(safe) |
---|---|---|---|---|
HashMap | All | no | no(hash) | no |
LinkedHashMap | All | yes | no | no |
Hashtable | None | no | no(hash) | yes |
TreeMap | Key only | no | yes | no |
HashMap内部存储的是Node对象,存储结构是数组,由key的hash值决定存储的槽位,同槽位value按照链或红黑树(8)存储
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
transient Node<K,V>[] table;
// 哈希算法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
当元素容量大于threshold(默认0.75)或第一次新增时,就会进行扩容resize方法,确认容量(范围内2倍增长)后创建新的Node数组,遍历老数组后放入新数组(重新hash)
h是hashcode,h
>>>
16是用来取出h的高16(>>>
是无符号右移) 由于和(length-1)运算,length 绝大多数情况小于2的16次方。所以始终是hashcode 的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列
为了让高16也参与运算,h = key.hashCode()) 与 (h>>>
16) 进行异或运算
如果使用 & 和 | 运算都会使得结果偏向0或者1,并不是均匀的概念,所以用 ^ 进行计算
java8还新增了compute、merge、forEach等方法,可以使用runnable方法对KV进行操作
HashMap高并发问题
HashMap在ReSize时,会进行两个步骤:1)扩容:创建一个新的Entry空数组,长度是原数组的2倍;2)ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。在高并发时,循环处理链条的next可能会形成循环链表,因此当get操作时会发生死循环
同样,在多线程下put操作时,执行addEntry(hash, key, value, i),如果有产生哈希碰撞,导致两个线程得到同样的bucketIndex去存储,就可能会出现覆盖的情况,导致元素丢失
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 原数组有元素,则是扩容,而非初始化
if (oldCap > 0) {
// 超过最大值不再扩充
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 旧阈值大于0
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
// 负载因子0.75 * 数组长度16 = 12,新阈值为12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新阈值为0,根据负载因子设置新阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 旧数组有数据,复制到新数组
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 有元素的节点
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 数组
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 红黑树
else if (e instanceof TreeNode)
// 将原本的二叉树结构拆分组成新的红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 链表
else { // preserve order
// jdk1.8中,旧链表迁移新链表,链表元素相对位置没有变化,实际是对对象的内存地址进行操作
// jdk1.7中,旧链表迁移新链表,如果在新表的数组索引位置相同,则链表元素会倒置
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
/**
hash值与旧的长度做与运算,判断元素在数组中的位置是否需要移动
数组的长度为 2^N,即高位为1,其余为0,计算 e.hash & oldCap 只需看oldCap最高位1所对应的hash位
因为 newCap 进行了双倍扩容,即将 oldCap 左移一位,那么 oldCap-1 相当于 newCap-1 右移一位,右移后高位补0,与运算只能得到0
如果 (e.hash & oldCap) == 0,hash值需要与运算的那一位为0,那么 oldCap - 1 与 newCap - 1 的高位都是0,其余位又是相同的
表明旧元素与新元素计算出的位置相同
同理,当其 == 1 时,oldCap-1 高位为0,newCap-1 高位为1,其余位相同,计算出的新元素的位置比旧元素位置多了 2^N
即得出【新元素的下标 = 旧下标 + oldCap】
**/
// 如果为0,元素位置在扩容后数组中的位置没有发生改变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
// 首位
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 不为0,元素位置在扩容后数组中的位置发生了改变,新的下标位置是 原下标位置 + 原数组长度
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 存入新数组
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
LinkedHashMap继承自HashMap,其Entry继承自HashMap.Node
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
显然Linked就说明了这些元素以链表形式存在,而非HashMap的数组。并且LinkedHashMap本身记录了head和tail。总体和HashMap类似,其中对数组的操作变为了对链表的操作(before、after)。并且链表的结构确定了存储的顺序与插入顺序一致,即有稳定性,相反也意味着不会和HashMap一样有元素的hash值的排序
Hashtable是HashMap的安全版本,即synchronized修饰方法,牺牲性能保证线程安全性
TreeMap会保证插入元素存储的顺序性,内部维护这root节点和Comparator比较器,确保构建的树是有序的。TreeMap维护的树是一颗红黑树(更高效的二叉搜索树),因此保证了当前节点的左子树都小于自己,而右子树都大于自己,这样就可以通过Comparator比较来确定向左还是向右查找,且是按key有序的。但是这也为新增/修改元素增加了复杂度,因为可能需要重新构建红黑树:
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
// ...
}
public V put(K key, V value) {
Entry<K,V> t = root;
// 第一次就直接构建根节点
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
// 找到插入元素的父节点
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
} else {
if (key == null)
throw new NullPointerException();
// 使用key自己的Comparable方法
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
// 修改红黑树,左旋或右旋保证弱平衡且符合红黑规则
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
Queue
java的Queue常用子接口有AbstractQueue(非阻塞队列),BlockingQueue(阻塞队列), Deque(双端队列)
Queue | null值 | 稳定性(order) | 有序性(sort) | 线程安全(safe) |
---|---|---|---|---|
LinkedList | yes | yes | no | no |
PriorityQueue | no | no | yes | no |
ArrayBlockingQueue | no | yes | no | yes |
LinkedBlockingQueue | no | yes | no | yes |
其中add(e)与offer(e)类似,区别是前者插入队尾失败会抛出异常。element()与peek()类似,会获取队首元素,区别也是前者失败会抛出异常。remove()与poll()也类似,会移除并获取队首元素,区别同上,失败时前者抛出异常,后者返回null
LinkedList在前面讲过,它其实除了做链表使用,还可以当做堆栈、队列、双端队列进行操作
PriorityQueue里有Comparator比较器,是一个基于优先堆的一个无界队列,不允许null和无法比较的对象存入。这个队列是非安全的,因此还有PriorityBlockingQueue解决多线程安全问题。PriorityQueue通过二叉小顶堆实现,可以用一棵完全二叉树表示,即任意一个非叶子节点的权值都不大于其左右子节点的权值,因此可以通过数组来作为PriorityQueue的底层实现,且位置满足以下规则:
leftNo = parentNo * 2 + 1
rightNo = parentNo * 2 + 2
parentNo = (nodeNo - 1) / 2
这也意味着如果改变元素,可能会破坏小顶堆的性质,因此需要进行必要的调整:
transient Object[] queue;
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
if (i >= queue.length)
grow(i + 1); // 扩容
size = i + 1;
if (i == 0) // 如果这是插入的第一个元素
queue[0] = e;
else
siftUp(i, e); // 调整
return true;
}
下面看下比较器的调整:
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
// 从指定位置k开始,将x逐层与当前点的parent进行比较并交换,直到满足 x >= queue[parent]为止
while (k > 0) {
// parentNo = (nodeNo-1)/2
int parent = (k - 1) >>> 1;
Object e = queue[parent];
if (key.compareTo((E) e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
删除操作也会改变队列的结构,因此也需要进行必要的调整:
public E poll() {
if (size == 0)
return null;
int s = --size;
modCount++;
E result = (E) queue[0]; // 0下表即最小值,即队首
E x = (E) queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x); // 调整
return result;
}
也是看比较器的调整:
private void siftDownUsingComparator(int k, E x) {
int half = size >>> 1;
// 从k开始,将x逐层向下与当前点孩子中较小的那个交换,直到x小于或等于孩子中的任何一个为止
while (k < half) {
// 首先找到左右孩子中较小的那个,记录到c里,并用child记录其下标
int child = (k << 1) + 1; // leftNo = parentNo * 2 + 1
Object c = queue[child];
int right = child + 1;
if (right < size &&
comparator.compare((E) c, (E) queue[right]) > 0)
c = queue[child = right];
if (comparator.compare(x, (E) c) <= 0)
break;
/ 然后用c取代原来的值
queue[k] = c;
k = child;
}
queue[k] = x;
}
除了基本的Queue操作,BlockingQueue有阻塞方法和阻塞超时方法,分别是插入put(e)、offer(e, time, unit),移除tak()、poll(time, unit),实现主要用于生产者消费者队列。都是线程安全的,使用ReentrantLock锁的方式保证
ArrayBlockingQueue是一个由数组结构组成的有界阻塞队列,内部为循环数组。是一个典型的有界缓存区,一旦创建就不能增加容量。试图向已满队列中放入元素会导致操作阻塞;试图从空队列中提取元素将同样阻塞。使用独占锁lock用来对出入队操作加锁,默认是非公平锁,支持公平策略,打开后构造的队列允许按照FIFO顺序访问线程。但会降低吞吐量。notEmpty,notFull条件变量用于入队出队时take和put的阻塞
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
看一下put方法,全局独占锁粒度很大,类似于方法上加synchronized
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length) // 循环队列
putIndex = 0;
count++;
notEmpty.signal();
}
LinkedBlockingQueue是基于Node链表的任意容量范围的队列。链表队列的吞吐量通常要高于基于数组的队列,但通常性能要低。如果指定容量,则可以防止队列过度扩展,未指定容量则等于Integer.MAX_VALUE。队列内部维护了head节点和last节点。与ArrayBlockingQueue不同的是,LinkedBlockingQueue具有两个非公平独占锁ReentrantLock,分别用来控制元素入队和出队加锁,可以由一个线程入队和一个线程出队,是一个生产者-消费者模型
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
Stack
Stack | null值 | 稳定性(order) | 有序性(sort) | 线程安全(safe) |
---|---|---|---|---|
Stack | yes | yes | no | yes |
Stack继承Vector,是一个基于数组的安全栈,很简单
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
// 数组倒着取即可
removeElementAt(len - 1);
return obj;
}
其他安全容器
对于List容器,除了Vector外,还提供了2类安全队列,分别是CopyOnWriteArrayList和Collections.synchronizedList,它们都是可存null、稳定、无序的线程安全队列。
CopyOnWriteArrayList内部基于数组存储,读写分离,最终一致,修改代价会越来越昂贵
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
在添加、删除元素时会加锁,并且拷贝一个新的数组作为自身的array,写数组的拷贝,读操作无锁的,线程安全,另外与ArrayList差不多。因此适合读多写少的场景,不适合写多和需要实时读的场景
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);
return true;
} finally {
lock.unlock();
}
}
Collections.synchronizedList使用的是装饰器模式,每个操作都加上了synchronized保证线程安全
CopyOnWriteArraySet与Collections.synchronizedSet是Set容器的安全版本,与上面的一样。其中CopyOnWriteArraySet内部使用CopyOnWriteArrayList作为存储容器
ConcurrentHashMap是一个基于Node数组的,不可存null,稳定无序的、安全的Map容器。采用了CAS +synchronized的方案保证线程安全。和HashMap一样,从1.7的Segment(继承ReentrantLock)数组 +HashEntry并采用分段锁的方案优化为1.8的这样,提升了查询遍历链表效率
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 获取hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果tab为空,初始化node数组
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// f为空,说明第一次在这个位置插入元素,用CAS插入Node节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 如果f的哈希为-1,说明当前f是ForwardingNode节点,表示有其它线程正在扩容,则一起进行扩容操作
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 把新的Node节点按链表或红黑树的方式插入到合适的位置
else {
V oldVal = null;
// 加锁
synchronized (f) {
// 插入前再次判断,防止被其他线程修改
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
// 遍历链表,找到对应的节点则修改值,否则在队尾加入节点
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//如果f是TreeBin,说明是红黑树节点,在树上操作
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
// 链条中节点数大于8,则将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 最后进行扩容判断
addCount(1L, binCount);
return null;
}
Collections.synchronizedMap和之前的一样,只是个装饰器模式包了一个Map
ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,不允许使用null,有序稳定。ConcurrentLinkedQueue中有两个volatile类型的Node节点分别用来存储列表的首尾节点,Node节点链接为一个单向无界链表。ConcurrentLinkedQueue使用CAS非阻塞算法解决线程安全问题,而没有使用锁,因此并发情况下size()并不准确。为了保证head、tail这2个Node操作的可见性和原子性是使用了volatile + CAS
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// p is last node
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q;
}
}
public int size() {
int count = 0;
for (Node<E> p = first(); p != null; p = succ(p))
if (p.item != null)
// Collection.size() spec says to max out
if (++count == Integer.MAX_VALUE)
break;
return count;
}
fail-fast与fail-safe
fail-fast会在以下两种情况抛出ConcurrentModifcationException:
1、单线程环境:集合被创建后,在遍历它时修改了解构(例外:remove()方法会让expectModCount与modCount相等而不会抛出异常)
2、多线程环境:但一个线程在遍历该集合,而另一个线程进行了修改
fail-safe任何对集合的修改都在一个复制的集合上进行修改,因此不会抛出异常,但有两点需要注意:
1、需要复制集合,产生大量对象,开销大
2、无法保证读取的是目前原始数据结构中的数组
样例:CopyOnWriteArrayList、ConcurrentHashMap
其他
Arrays.copyOf 与 System.arraycopy
// src:要复制的数组 srcPos:要复制的数组中的起始位置
// dest:副本数组 destPos:副本数组中的起始位置
// length:要复制的数组元素的数量
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
由上面Arrays.copyOf方法最终还是调用了System.arraycopy本地方法,方法返回的副本都是一个新数组,其长度选择参数值与原数组长度中的最小值