文章目錄
原理探究:Java併發包中併發隊列
ConcurrentLinkedQueue
- 無界隊列
- 無鎖算法,入隊和出隊使用CAS算法進行設置隊首和隊尾元素
- 由於是無鎖算法,所以在獲取size的時候是進行遍歷操作的,在遍歷過程中,已經遍歷過的節點可能有增刪,所以size在高併發場景下存在一定誤差,而且size性能較差,所以如果只是判斷隊列是否有元素建議使用isEmpty(),該方法只會獲取first節點是否有元素。
- 初始化隊列時頭尾節點均指向哨兵節點head = tail = new Node(null);
public int size() {
int count = 0;
// 循環遍歷,效率低,由於是無鎖算法,所以size在高併發情況下不準確
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;
}
(題外話:在判斷字符串是否爲空的時候慣用寫法是(null == str || “”.equals(str))其實String的equals方法也是遍歷字符串,建議使用(cs == null || cs.length() == 0),還有習慣用StringUtils的,org.apache.commons.lang3.StringUtils和org.springframework.util.StringUtils是不一樣的,spring使用的是equals,lang3使用的是length,建議自己封裝StringUtils去繼承lang3,後期可以根據自己需要去修改繼承或者覆蓋方法)
LinkedBlockingQueue
- 生產消費模型
- 有界隊列Integer.MAX_VALUE,也可以在new的時候自行制定capacity
- 加鎖,take鎖和put鎖,notFull Condition條件隊列控制生產者,notEmpty Condition條件隊列控制消費者
- put鎖:在每次執行offer(無阻塞尾部入隊enqueue(node),已滿則丟棄)、put(while阻塞入隊,若隊列滿則等待,可被中斷,中斷後拋出InterruptedException)後若隊列沒有滿則調用notFull.signal()喚醒生產者條件隊列進行first節點進行生產,unlock之後判斷操作前如果隊列中沒有元素(count.getAndIncrement() == 0)則會調用signalNotEmpty喚醒消費者條件隊列的first節點進行消費;
- take鎖:在每次執行poll(無阻塞頭部出隊dequeue(node))、take(while阻塞頭部出隊,若隊列空則等待,可被中斷,中斷後拋出InterruptedException)後若隊列還有數據則調用notEmpty.signal()喚醒消費者條件隊列的first節點進行消費,unlock之後判斷操作前如果隊列是否滿(count.getAndDecrement() == capacity)則會調用signalNotFull喚醒生產者條件隊列的first節點進行消費;
- take鎖:peek操作與poll操作相似,但只是窺視,不出隊。
- 雙重加鎖(put、take鎖):remove遍歷查找,找到則調用unlink刪除,並鏈接前置節點和後置節點,操作完之後如果隊列是否滿(count.getAndDecrement() == capacity)則會調用signalNotFull喚醒生產者條件隊列的first節點進行消費;
- size使用AtomicInteger存儲,結果準確,獲取size複雜度O(1)。
public boolean offer(E e) {
// 爲空直接拋出空指針
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
// 如果隊列已滿直接返回false
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
// 獲取put鎖
putLock.lock();
try {
// 如果隊列未滿直接調用enqueue入隊,count+1
if (count.get() < capacity) {
// put(E e)與offer區別在於put在獲取可中斷鎖(putLock.lockInterruptibly();)
// count.get() == capacity時while阻塞,offer無阻塞直接返回
// enqueue方法: last = last.next = node;
enqueue(node);
c = count.getAndIncrement();
// 入隊後如果隊列未滿則喚醒生產者
if (c + 1 < capacity)
notFull.signal();
}
} finally {
// 釋放鎖
putLock.unlock();
}
if (c == 0)
// 如果添加前隊列爲空則喚醒消費者消費當前入隊元素
signalNotEmpty();
return c >= 0;
}
ArrayBlockingQueue
- 由數組實現的有界隊列。
- 獨佔鎖ReentrantLock,同時只能有一個線程進行入隊/出隊操作,由入隊指針(putIndex)和出隊指針(takeIndex)記錄入隊/出隊元素位置。
- offer(無阻塞尾部入隊enqueue(node),已滿則丟棄)、put(while阻塞入隊,若隊列滿則調用notFull.await()放入notFull條件隊列等待,可被中斷,中斷後拋出InterruptedException)入隊成功之後計算隊尾指針++putIndex,count++,並調用notEmpty.signal()喚醒消費者。
- poll(無阻塞頭部出隊dequeue(node)),take(while阻塞頭部出隊,若隊列空則等待,可被中斷,中斷後拋出InterruptedException)出隊成功之後計算隊尾指針++takeIndex,count–,並調用notFull.signal()喚醒生產者。
- peek(無阻塞窺視,獲取頭部元素不移除)。
- size(加鎖獲取count值,count沒有被volatile修飾,因爲操作count的時候都是在獲取鎖之後,所以沒有加volatile,獲取size的時候同理,使用鎖來保證內存可見性)。
PriorityBlockingQueue
- 由數組實現的無界優先級隊列,因爲無界所以只有notEmpty條件隊列控制消費者。
- allocationSpinLock是個自旋鎖,使用CAS操作保證同時只有一個線程進行擴容,狀態0/1,0表示當前沒有擴容,1表示當前正在擴容。擴容前會unlock,其實也可以不unlock,因爲擴容需要時間,爲了得到更好的性能,在擴容完成後,再獲取鎖,將當前queue裏面的元素複製到新數組。
- siftUpComparable、siftDownComparable,二叉樹堆維護優先級。
private static <T> void siftUpComparable(int k, T x, Object[] array) {
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = array[parent];
if (key.compareTo((T) e) >= 0)
break;
array[k] = e;
k = parent;
}
array[k] = key;
}
private static <T> void siftDownComparable(int k, T x, Object[] array, int n) {
if (n > 0) {
Comparable<? super T> key = (Comparable<? super T>)x;
int half = n >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = array[child];
int right = child + 1;
if (right < n &&
((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
c = array[child = right];
if (key.compareTo((T) c) <= 0)
break;
array[k] = c;
k = child;
}
array[k] = key;
}
}
- put內部調用offer由於是無界隊列,所以不需要阻塞。
- poll:獲取對頭元素。
- take:阻塞出隊,直到有元素位置,可被中斷,中斷後拋出InterruptedException。
- peek:獲取鎖之後,return (size == 0) ? null : (E) queue[0];
- size:獲取所之後返回size,複雜度O(1)。
- 示例,優先級隊列創建類實現Comparable接口,並重寫compareTo,自定義元素比較規則,取出順序和先後順序無關,只與優先級有關。
/**
* PriorityBlockingQueue 示例
**/
public class PriorityBlockingQueueTest {
/**
* 實現Comparable接口,getDelay和compareTo方法
**/
@Data // lombok
static class Task implements Comparable<Task> {
private int priority = 0;
private String taskName;
/**
* 實現compareTo方法
**/
@Override
public int compareTo( Task o ) {
return this.priority >= o.priority ? 1 : -1;
}
}
public static void main( String[] args ) {
PriorityBlockingQueue<Task> tasks = new PriorityBlockingQueue<>();
Random random = new Random();
for (int i = 0; i < 10; i++) {
Task task = new Task();
task.setPriority(random.nextInt(10));
task.setTaskName("taskName" + i);
tasks.offer(task);
}
while (!tasks.isEmpty()) {
Task poll = tasks.poll();
System.out.println(poll);
}
}
}
DelayQueue
- 併發無界阻塞延時隊列,頭部爲最快要過期的元素。
- private final PriorityQueue q = new PriorityQueue();用於存放元素。leader變量基於Leader-Follower模式的變體,用於儘量減少不必要的線程等待。
- offer:獲取鎖然後q.offer入隊,插入元素要實現Delayed接口,入隊完成後peek獲取元素,若peek() == e,則將leader置空並喚醒Condition available = lock.newCondition()的一個follwer線程,告訴它隊列裏面有元素了。
- take:獲取並移除隊列裏面延遲時間過期的元素,如果隊列沒有過期元素則等待。leader不爲null這說明有其它線程在執行take。如果隊首元素未到過期時間,並且leader線程是當前線程,則會調用awaitNanos(delay),休眠delay時間(這期間會釋放鎖,所以其他線程可以執行offer操作,也可以take阻塞自己),剩餘時間過期後當前線程會重新競爭到鎖,然後重置leader線程爲null,重新進入for循環這時候會發現隊首元素已經過期了,則會直接返回隊首元素。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 獲取可中斷鎖
lock.lockInterruptibly();
try {
// 循環操作
for (;;) {
// 獲取隊首元素,不出隊
E first = q.peek();
if (first == null)
// 隊首元素爲空則等待,並釋放鎖
available.await();
else {
// 隊首元素不爲空,則獲取過期時間
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
// 若已過期則直接取出並返回
return q.poll();
// 若未到期,則釋放引用
first = null; // don't retain ref while waiting
if (leader != null)
// 如果leader不爲null,則說明有其他縣城在執行take操作,此時休眠自己
available.await();
else {
// 如果未過期切leader爲null,則將leader設置爲當前線程
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 休眠等待delay時間,這期間會釋放鎖,所以其他線程可以執行offer操作,也可以take阻塞自己,
// 這期間會釋放鎖,所以其他線程可以執行offer操作,也可以take阻塞自己,剩餘時間過期後
// 當前線程會重新競爭到鎖,然後重置leader線程爲null
// 重新進入for循環這時候會發現隊首元素已經過期了,則會直接返回隊首元素
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 操作完成後將leader設置爲null,如果隊列還有元素,則喚醒條件等待隊列中的隊首線程進行操作
if (leader == null && q.peek() != null)
available.signal();
// 釋放鎖
lock.unlock();
}
}
- poll操作比較簡單,獲取非中斷鎖後,無阻塞peek隊首元素,若爲null或者沒有過期直接返回。
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return q.poll();
} finally {
lock.unlock();
}
}
- size操作,獲取隊列元素個數,包含過期和未過期的,複雜度O(1)。
/**
* DelayQueueTest 示例
**/
public class DelayQueueTest {
/**
* 實現Delayed接口,getDelay和compareTo方法
**/
@Data // lombok
static class DelayQueueEle implements Delayed {
// 延時時間
private final int delayTime;
// 到期時間
private final long expire;
// 任務名稱
private String taskName;
public DelayQueueEle( int delayTime, String taskName ) {
this.delayTime = delayTime;
this.taskName = taskName;
this.expire = System.currentTimeMillis() + delayTime;
}
/**
* 實現getDelay方法
**/
@Override
public long getDelay( TimeUnit unit ) {
return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
/**
* 實現compareTo方法
**/
@Override
public int compareTo( Delayed o ) {
return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
}
public static void main( String[] args ) {
// 創建delay隊列
DelayQueue<DelayQueueEle> delayeds = new DelayQueue<>();
// 創建延時任務
Random random = new Random();
for (int i = 0; i < 10; i++) {
delayeds.offer(new DelayQueueEle(random.nextInt(500), "taskName:" + i));
}
// 依次取出並打印
DelayQueueEle ele;
// 循環,防止虛假喚醒,則不能打印全部元素
try {
for (; ; ) {
while ((ele = delayeds.take()) != null) {
System.out.println(ele);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 或者
// for (; ; ) {
// while ((ele = delayeds.poll()) != null) {
// System.out.println(ele);
// }
// }
}
}