-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathConcurrentLinkedQueueDemo.java
More file actions
153 lines (135 loc) · 5.98 KB
/
Copy pathConcurrentLinkedQueueDemo.java
File metadata and controls
153 lines (135 loc) · 5.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package Phase5_CollectionsLambdasStreams.Collections;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
* java.util.concurrent.ConcurrentLinkedQueue<E>
* ---------------------------------------------
* A LOCK-FREE, UNBOUNDED FIFO queue suitable for many producers and many
* consumers. Internally it uses a singly-linked list with atomic CAS
* (compare-and-swap) operations - no locks, no blocking.
* <p>
*
* head -> [A] -> [B] -> [C] -> tail
* <p>
*
* Why It Exists
* -------------
* - LinkedList / ArrayDeque are NOT thread-safe.
* - Collections.synchronizedQueue / Vector lock the entire collection.
* - BlockingQueue blocks; sometimes you do not want to block.
* <p>
*
* ConcurrentLinkedQueue is the right pick when:
* - Many threads call offer() and poll() concurrently.
* - You do not need backpressure (no max capacity).
* - You do not want callers to wait when the queue is empty (poll
* returns null - no blocking).
* <p>
*
* Differences From BlockingQueue
* ------------------------------
* ConcurrentLinkedQueue BlockingQueue
* --------------------- ----------------
* Lock-free CAS Usually locks (BlockingQueue impls)
* Never blocks put / take block by design
* Unbounded Often bounded for backpressure
* poll() returns null take() blocks until an element exists
* No "fair" option Some have a fairness mode
* <p>
*
* Methods That Matter
* -------------------
* offer(e) - always succeeds (unbounded)
* add(e) - same as offer, here
* poll() - remove head, or null if empty
* peek() - look at head, or null
* size() - O(n)! Walks the list, not a counter.
* isEmpty() - O(1)
* iterator() - weakly consistent, never throws CME
* <p>
*
* Why size() Is O(n)
* ------------------
* The lock-free design lets many threads insert and remove without
* coordinating. There is no maintained counter because keeping one in sync
* would require its own CAS loop. If you need to check whether the queue
* is empty, use isEmpty() - that one is O(1).
* <p>
*
* Big-O
* -----
* offer / poll / peek / isEmpty O(1) amortised
* size O(n)
* contains / remove(Object) O(n)
*/
public class ConcurrentLinkedQueueDemo {
public static void main(String[] args) throws InterruptedException {
section("1) Single-threaded basics - just like any other Queue");
ConcurrentLinkedQueue<String> q = new ConcurrentLinkedQueue<>();
q.offer("a"); q.offer("b"); q.offer("c");
System.out.println("queue = " + q);
System.out.println("peek = " + q.peek());
System.out.println("poll = " + q.poll() + " -> " + q);
System.out.println("isEmpty = " + q.isEmpty());
section("2) Many producers, many consumers - lock-free in action");
final int PRODUCERS = 4;
final int CONSUMERS = 4;
final int PER_PRODUCER = 5_000;
ConcurrentLinkedQueue<Integer> shared = new ConcurrentLinkedQueue<>();
AtomicInteger consumedCount = new AtomicInteger(0);
Thread[] producers = new Thread[PRODUCERS];
Thread[] consumers = new Thread[CONSUMERS];
long start = System.nanoTime();
for (int p = 0; p < PRODUCERS; p++) {
final int base = p * PER_PRODUCER;
producers[p] = new Thread(() -> {
for (int i = 0; i < PER_PRODUCER; i++) shared.offer(base + i);
});
producers[p].start();
}
for (int c = 0; c < CONSUMERS; c++) {
consumers[c] = new Thread(() -> {
// Each consumer pulls until it has seen its share.
// Using PRODUCERS * PER_PRODUCER / CONSUMERS as a target.
int target = (PRODUCERS * PER_PRODUCER) / CONSUMERS;
int taken = 0;
while (taken < target) {
Integer v = shared.poll();
if (v != null) { taken++; consumedCount.incrementAndGet(); }
// else: spin briefly, the producers will catch up
}
});
consumers[c].start();
}
for (Thread t : producers) t.join();
for (Thread t : consumers) t.join();
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.println("produced : " + (PRODUCERS * PER_PRODUCER));
System.out.println("consumed : " + consumedCount.get());
System.out.println("leftover : " + shared.size() + " (size() is O(n))");
System.out.println("elapsed : " + ms + " ms");
section("3) Weakly consistent iterator - never throws CME");
ConcurrentLinkedQueue<Integer> live = new ConcurrentLinkedQueue<>(
java.util.List.of(1, 2, 3, 4, 5));
java.util.Iterator<Integer> it = live.iterator();
live.offer(99); // mutate WHILE iterating - no CME
live.poll();
int seen = 0;
while (it.hasNext()) { it.next(); seen++; }
System.out.println("iterated " + seen + " element(s), no CME thrown");
section("4) poll on empty queue returns null - no blocking");
ConcurrentLinkedQueue<Integer> empty = new ConcurrentLinkedQueue<>();
long t = System.nanoTime();
Integer v = empty.poll();
long us = (System.nanoTime() - t);
System.out.println("poll() returned " + v + " in " + us + " ns (instant)");
section("5) Memory ordering note - lock-free does not mean lock-bad");
// Offer/poll publish/observe through volatile-and-CAS, so consumers
// see fully-constructed objects. Just don't ASSUME a particular
// interleaving between producer events.
// OUTPUT (timings vary)
}
private static void section(String title) {
System.out.println("\n====== " + title + " ======");
}
}