collection源碼剖析
List
ArrayList
ArrayList底層是數組
add
新增元素的時候其實就是在數組下一個位置進行元素賦值,重點是在擴容上
擴容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 新空間擴容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果擴容1.5倍,比設置的值還小,你那麼使用入參,否則使用1.5倍
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
// 判斷是否超出容量
if (newCapacity - MAX_ARRAY_SIZE > 0) {
// MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 判斷是否超出這個範圍,如果超了,就最大隻能用Integer.MAX_VALUE,否則就Integer.MAX_VALUE - 8
newCapacity = hugeCapacity(minCapacity);
}
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
get
因爲是數組,所以可以直接索引
LinkedList
節點數據結構
/**
* 泛型結構
* @param <E> node
*/
private static class Node<E> {
E item;
// 雙向鏈表,向前和向後
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
add
結論:新節點是插入到原來index的前面,原來index以及以後的節點,整體後移一位
/**
* Returns the (non-null) Node at the specified element index.
* 這裏索引用了二分的思想,但是不是二分的算法
* 首先區分index是否小於一般,如果是,那麼從前往後找
* 如果大於一般,那麼從後往前找
*/
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
// 從first往後
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
// 從last往前
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
/**
* Inserts element e before non-null Node succ.
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
// 斷開原來的前置連接線,並修改爲新的
succ.prev = newNode;
if (pred == null) {
first = newNode;
} else {
// 斷開原來的後置,並更新
pred.next = newNode;
}
size++;
modCount++;
}
reomove,removeFirst,remove(index)
remove默認移除首節點,於removefirst作用相同
/**
* Unlinks non-null first node f.
*/
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else {
// 更新完first之後,這裏只需要把next.prev對象設置爲null即可
next.prev = null;
}
size--;
modCount++;
return element;
}
/**
* Unlinks non-null node x.
*/
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
// 前置節點爲空,那麼直接把first移動到next
if (prev == null) {
first = next;
} else {
// 把前面節點的後置設置爲下一個,跳過當前節點
prev.next = next;
x.prev = null;
}
// 如果next本來是空的,那麼把last指針前移
if (next == null) {
last = prev;
} else {
// 不爲空,那麼把後面節點的前置指針跳過當前,設置前面一個節點
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
remove(index)和 remove(Object)類似
但是remove(object)的意思是判斷空和非空,因爲空的無法進行equals比較,循環查找
另外remove(index)也是先根據node方法定位關聯節點
get,indexof查找
get方法也是用node(index)定位,indexof方法:判斷空和非空,因爲空的無法進行equals比較,循環查找
參考
https://pdai.tech/md/java/collection/java-collection-LinkedList.html#queue-方法
Stack
棧的實現主要依賴的是vector
這裏主要是push和pop操作
擴容參考arraylist
push就是類似add,但是這裏的操作都加了synchronized關鍵字,所以stack是線程安全的
CopyOnWriteArrayList
add操作
public boolean add(E e) {
// 加鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 先直接複製一個新的數組出來,並且直接把長度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 然後設置最後一個位置的節點
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
擴容
這個結構的擴容方式很簡單暴力
直接複製出來一份滿足要求大小的數組
newElements = Arrays.copyOf(elements, len + 1);
get
沒有加鎖
private E get(Object[] a, int index) {
return (E) a[index];
}
總結:
數據一致性問題:CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。
這句話的意思是在循環操作的過程中,這個get結果是不可知的,只能保證在set的時候沒問題,然後所有數據add完畢之後的結果符合預期
內存佔用問題:因爲CopyOnWrite的寫時複製機制,所以在進行寫操作的時候,內存裏會同時駐紮兩個對象的內存,舊的對象和新寫入的對象(注意:在複製的時候只是複製容器裏的引用,只是在寫的時候會創建新對象添加到新容器裏,而舊容器的對象還在使用,所以有兩份對象內存)
Set
HashSet
hashset本質其實就是hashmap
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
LinkedHashSet
與hashset相比就是構造函數不同
TreeSet
實現的是treemap
Queue
PriorityQueue
構造函數
/**
* 通過數組來存放堆的數據信息
*/
transient Object[] queue; // non-private to simplify nested class access
public PriorityQueue(int initialCapacity,
Comparator<? super E> comparator) {
// Note: This restriction of at least one is not actually needed,
// but continues for 1.5 compatibility
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.queue = new Object[initialCapacity];
// 設置比較器,這個決定數據出入順序
this.comparator = comparator;
}
add和offer
add和offer基本沒差別,add也是調用的offer方法
我們重點看一下offer方法
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;
// 如果隊列是空的,那麼直接賦值到0位置
if (i == 0) {
queue[0] = e;
} else {
// 如果不爲空,那麼就需要計算一下位置
siftUp(i, e);
}
return true;
}
/**
* Increases the capacity of the array.
* 數據擴容
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// 獲取舊數組容量大小
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
// 判斷old是否小於64,如果是,那麼擴大原來(長度+2),否則擴大原來的50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1));
// overflow-conscious code overflow檢測
if (newCapacity - MAX_ARRAY_SIZE > 0) {
newCapacity = hugeCapacity(minCapacity);
}
// 拷貝數組,並創建新的數組長度
queue = Arrays.copyOf(queue, newCapacity);
}
private void siftUpComparable(int k, E x) {
// 要求元素本身具備comparable接口能力
Comparable<? super E> key = (Comparable<? super E>) x;
// 噹噹前k數量大於0
while (k > 0) {
// 尋找parent。 k是數據實際個數,而下標是從0開始的,那麼就需要在原來的基礎上-1
int parent = (k - 1) >>> 1;
// 獲取父節點位置的值
Object e = queue[parent];
// 比較,如果key大於父節點,那麼就放後面,java默認小的再前
if (key.compareTo((E) e) >= 0) {
break;
}
// 如果比parent值要小,那麼就取代,吧父節點的值往後移
// k是當前位置,parent是父節點位置,e是父節點元素,key纔是當前元素
queue[k] = e;
// 指針偏移到父節點從新遍歷
k = parent;
}
queue[k] = key;
}
這是一個完全二叉樹,可以採用數組的方式存儲,那麼每個父節點對應的子節點都是 (node * 2)和(node * 2)+1
element和peek
和add還有offer類似,element和peek也是相互調用的關係,區別是element會拋出異常,peek不會,會直接返回null
peek不會對原來的數組元素做出改變,只會取出頭元素,也就是說peek只會一直取索引位置爲0的元素
remove和poll
remove也是調用的poll函數
public E poll() {
if (size == 0) {
return null;
}
// 數組長度--
int s = --size;
modCount++;
// 獲取0位置的節點
E result = (E) queue[0];
// 獲取末尾節點數據
E x = (E) queue[s];
// 設置爲空,然後重新調整數組
queue[s] = null;
if (s != 0) {
siftDown(0, x);
}
return result;
}
參考
Deque
這是一個雙端隊列, 具體實現可以參考LinkedList
ConcurrentLinkedQueue
offer操作
使用idea進行debug測試驗證的時候,發現,queue對象的第一次的入隊之後tail節點指向了自己
tail = tail.next 然後就是死循環遍歷
解決辦法是關閉idea的debug的時候的toString方法,因爲這個toString方法會對集合進行遍歷
遍歷的時候會調用這個方法
java.util.concurrent.ConcurrentLinkedQueue#first
而這個方法會更新head,並且會調用節點的lazySetNext
java.util.concurrent.ConcurrentLinkedQueue#updateHead
在這個lazySetNext會吧head指向自己,這裏的head是第一個頭節點,那就會出現循環情況,至於putOrderedObject方法是關於指令重排序的,不在本次討論範圍內
debug異常解決辦法
關掉這2個選項即可
offer操作
/**
* Inserts the specified element at the tail of this queue.
* As the queue is unbounded, this method will never return {@code false}.
*
* 注意下這裏更新tail位置的時候是延遲了一次的,也就是說tail指向的節點是在下次入隊的時候更新
*
* @return {@code true} (as specified by {@link Queue#offer})
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
// 無限循環,cas操作
// t 指向tail位置
// p 賦值爲t
// q 爲p的next
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".
// 判定cas成功之後,第一次並不會更新tail對象因爲這個時候p==t恆成立
// 只有在第二次進來的時候,發現q!=null的情況下,進入第三個判斷
// 這個時候p指向的是t的next的時候,也就是進入了第三個判斷了,那麼這個時候我們再更新tail
if (p != t) { // hop two nodes at a time
// 更新tail位置,這樣做的好處是對tail的更新次數變少了,對tail的讀取
casTail(t, newNode); // Failure is OK.
}
return true;
}
// Lost CAS race to another thread; re-read next
} else if (p == q) {
// 從新設置隊列尾部,p節點是null的head節點剛好被出隊,更新head節點時h.lazySetNext(h)把舊的head節點指向自己
// 也就是出現循環的場景,比如debug的時候執行一個toString也會有這個問題,迭代循環的時候會調用first方法,也會調用lazySetNext
// 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對象,因爲這個時候q不爲空,說明tail指向的不是真正的末尾節點
// 如果p!= t,那麼就重新設置t=tail對象。如果發生t!=(t=tail)那麼可能是多線程情況下,差距tail有點多,那麼直接返回tail
// q爲p的next,相當於是往下移動一位
// 然後把新的p作爲條件重新循環
p = (p != t && t != (t = tail)) ? t : q;
}
}
}
poll
public E poll() {
restartFromHead:
for (;;) {
// p初始化爲頭節點
// q 爲p的下一個節點
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
// 從頭節點獲取數據,cas操作設置爲空
if (item != null && p.casItem(item, null)) {
// Successful CAS is the linearization point
// for item to be removed from this queue.
// 這個地方和之前offer一樣,也是在第二次的時候才進行更新head
if (p != h) {
// hop two nodes at a time3
// 把head設置爲p,並lazySetNext
updateHead(h, ((q = p.next) != null) ? q : p);
}
return item;
} else if ((q = p.next) == null) {
// 如果後續節點爲空,那麼重新更新頭節點,這裏可能會發生自循環
// 把head設置爲p,並lazySetNext
updateHead(h, p);
return null;
} else if (p == q) {
// 發生自循環,這種情況就重新獲取數據
continue restartFromHead;
} else {
// 如果匹配不成功,往後續節點遍歷
p = q;
}
}
}
}
remove&size
- 這裏需要注意下的是,size方法返回的時候是直接循環遍歷鏈表進行計算的
- remove也是循環鏈表比較,然後再cas刪除的
HOPS(延遲更新的策略)的設計
如果讓tail永遠作爲隊列的隊尾節點,實現的代碼量會更少,而且邏輯更易懂。但是,這樣做有一個缺點,如果大量的入隊操作,每次都要執行CAS進行tail的更新,彙總起來對性能也會是大大的損耗。如果能減少CAS更新的操作,無疑可以大大提升入隊的操作效率,所以doug lea大師每間隔1次(tail和隊尾節點的距離爲1)進行才利用CAS更新tail。對head的更新也是同樣的道理,雖然,這樣設計會多出在循環中定位隊尾節點,但總體來說讀的操作效率要遠遠高於寫的性能,因此,多出來的在循環中定位尾節點的操作的性能損耗相對而言是很小的
著作權歸@pdai所有 原文鏈接:https://pdai.tech/md/java/thread/java-thread-x-juc-collection-ConcurrentLinkedQueue.html
參考
https://www.cnblogs.com/zaizhoumo/p/7726218.html
https://www.cnblogs.com/sunshine-2015/p/6067709.html
https://blog.csdn.net/AUBREY_CR7/article/details/106331490
https://tech.meituan.com/2014/09/23/java-memory-reordering.html
BlockingQueue
這個類是最常用來做生產消費隊列的類,可實現offer或take的阻塞操作
這裏我們用ArrayBlockingQueue做例子來看看這個類
對象的阻塞通過2個condition鎖進行控制
private final Condition notEmpty;
private final Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
put/offer
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 無限超時等待
while (count == items.length) {
// notFull 被阻塞,也就是現在隊列是滿的,等待被喚醒,那麼就需要
// notFull.signal()!來進行喚醒,那麼我們吧這個放到隊列數據出庫之後
notFull.await();
}
enqueue(e);
} finally {
lock.unlock();
}
}
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 隊列滿了
while (count == items.length) {
if (nanos <= 0) {
return false;
}
// 在這裏進行阻塞等待,返回剩餘等待時間
nanos = notFull.awaitNanos(nanos);
}
// 數據入庫
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
take/poll
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 無限超時等待
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();
} finally {
lock.unlock();
}
}
實戰樣例
import lombok.Data;
import lombok.ToString;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
/**
* 功能描述
*
* @since 2022-11-25
*/
public class Code008ConsumerAndProduct {
@Data
@ToString
static class ThreadEleObject {
private String serviceName;
private String methodName;
private Map args;
}
static class ProviderService implements Runnable {
private BlockingQueue<ThreadEleObject> blockingQueue;
private static boolean stopFlag = false;
public ProviderService(BlockingQueue blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
// 定時生產數據
while (!stopFlag) {
try {
ThreadEleObject threadEleObject = new ThreadEleObject();
threadEleObject.setServiceName("threadDemoService");
threadEleObject.setMethodName("test1");
Random random = new Random();
threadEleObject.setArgs(new HashMap() {
{
put(random.nextInt(), random.nextLong());
put(random.nextInt(), random.nextLong());
put(random.nextInt(), random.nextLong());
put(random.nextInt(), random.nextLong());
}
});
blockingQueue.put(threadEleObject);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class ConsumeService implements Runnable {
private BlockingQueue<ThreadEleObject> blockingQueue;
private static boolean stopFlag = false;
public ConsumeService(BlockingQueue blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
while (!stopFlag) {
try {
// 獲取元素bean對象
ThreadEleObject ele = blockingQueue.poll();
if (ele != null) {
System.out.println(ele.toString());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
private static void testConcrrent() {
BlockingQueue threadServicequeue = new ArrayBlockingQueue(1024);
BlockingQueue<Object> executorServiceQueue = new ArrayBlockingQueue(1024);
// 啓動生產/消費
ProviderService providerService = new ProviderService(threadServicequeue);
ConsumeService consumeService = new ConsumeService(threadServicequeue);
ExecutorService executorService = Executors.newCachedThreadPool();
IntStream.range(0, 1).forEach(i -> {
executorService.submit(providerService);
});
IntStream.range(0, 30).forEach(i -> {
executorService.submit(consumeService);
});
}
public static void main(String[] args) {
try {
testConcrrent();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}