【collection】6.collection源碼剖析

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;
}

參考

https://blog.csdn.net/MonkeyITBoy/article/details/79020636

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

  1. 這裏需要注意下的是,size方法返回的時候是直接循環遍歷鏈表進行計算的
  2. 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);
        }
    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章