Skip to content

Commit fbc8105

Browse files
committedAug 7, 2024
[util] add RWSpinLock and upgrade ObjectPool
1 parent 5517931 commit fbc8105

File tree

7 files changed

+319
-64
lines changed

7 files changed

+319
-64
lines changed
 

‎base/src/main/java/io/vproxy/base/util/Utils.java

+11
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,17 @@ public static boolean allZerosAfter(ByteArray bytes, int index) {
470470
return true;
471471
}
472472

473+
public static int minPow2GreaterThan(int n) {
474+
n -= 1;
475+
n |= n >>> 1;
476+
n |= n >>> 2;
477+
n |= n >>> 4;
478+
n |= n >>> 8;
479+
n |= n >>> 16;
480+
n += 1;
481+
return n;
482+
}
483+
473484
public static boolean assertOn() {
474485
return assertOn;
475486
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package io.vproxy.base.util.lock;
2+
3+
import java.util.concurrent.atomic.AtomicInteger;
4+
5+
public class ReadWriteSpinLock {
6+
private static final int WRITE_LOCKED = 0x80_00_00_00;
7+
// 32 31 ------ 0
8+
// W RRRR...RRRR
9+
private final AtomicInteger lock = new AtomicInteger(0);
10+
private final AtomicInteger wLockPending = new AtomicInteger(0);
11+
private final int spinTimes;
12+
13+
public ReadWriteSpinLock() {
14+
this(20);
15+
}
16+
17+
public ReadWriteSpinLock(int spinTimes) {
18+
this.spinTimes = spinTimes;
19+
}
20+
21+
public void readLock() {
22+
while (true) {
23+
if (wLockPending.get() != 0) {
24+
spinWait();
25+
continue;
26+
}
27+
if (lock.incrementAndGet() < 0) {
28+
continue;
29+
}
30+
break;
31+
}
32+
}
33+
34+
public void readUnlock() {
35+
lock.decrementAndGet();
36+
}
37+
38+
public void writeLock() {
39+
wLockPending.incrementAndGet();
40+
while (!lock.compareAndSet(0, WRITE_LOCKED)) {
41+
spinWait();
42+
}
43+
}
44+
45+
public void writeUnlock() {
46+
lock.set(0);
47+
wLockPending.decrementAndGet();
48+
}
49+
50+
private void spinWait() {
51+
for (int i = 0; i < spinTimes; ++i) {
52+
Thread.onSpinWait();
53+
}
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,77 @@
11
package io.vproxy.base.util.objectpool;
22

3+
import io.vproxy.base.util.Utils;
4+
import io.vproxy.base.util.lock.ReadWriteSpinLock;
5+
import io.vproxy.base.util.thread.VProxyThread;
6+
37
import java.util.concurrent.atomic.AtomicInteger;
4-
import java.util.concurrent.atomic.AtomicReference;
58
import java.util.concurrent.atomic.AtomicReferenceArray;
69

710
/**
811
* The pool is split into a few partitions, each partition has a read array and a write array.
912
* When adding, elements will be added into the write array.
1013
* When polling, elements will be polled from the read array.
11-
* If read array is empty and write array is full, and when running polling, the two arrays will be swapped
12-
* (they will not be swapped when adding).
14+
* If read array is empty and write array is full, and when running polling or adding, the two arrays will be swapped
1315
* The arrays will not be operated when they are being swapped.
1416
* When concurrency occurs, the operations will retry for maximum 10 times.
1517
*
1618
* @param <E> element type
1719
*/
1820
public class ConcurrentObjectPool<E> {
1921
private final int partitionCount;
22+
private final int partitionCountMinusOne;
2023
private final Partition<E>[] partitions;
24+
private final int maxTraversal;
2125

2226
public ConcurrentObjectPool(int capacityHint) {
23-
this(capacityHint, 16, 4);
27+
this(capacityHint, 16, 0);
2428
}
2529

26-
public ConcurrentObjectPool(int capacityHint, int partitionCountHint, int minPartitionCapHint) {
27-
capacityHint -= 1;
28-
capacityHint |= capacityHint >>> 1;
29-
capacityHint |= capacityHint >>> 2;
30-
capacityHint |= capacityHint >>> 4;
31-
capacityHint |= capacityHint >>> 8;
32-
capacityHint |= capacityHint >>> 16;
33-
capacityHint += 1;
30+
public ConcurrentObjectPool(int capacityHint, int partitionCountHint, int maxTraversalHint) {
31+
capacityHint = Utils.minPow2GreaterThan(capacityHint) / 2;
32+
partitionCountHint = Utils.minPow2GreaterThan(partitionCountHint);
3433

35-
if (capacityHint / minPartitionCapHint == 0) {
34+
if (capacityHint / partitionCountHint == 0) {
3635
partitionCount = 1;
3736
} else {
38-
partitionCount = Math.min(capacityHint / minPartitionCapHint, partitionCountHint);
37+
partitionCount = partitionCountHint;
3938
}
39+
partitionCountMinusOne = partitionCount - 1;
4040

4141
//noinspection unchecked
4242
this.partitions = new Partition[partitionCount];
4343
for (int i = 0; i < partitionCount; ++i) {
4444
partitions[i] = new Partition<>(capacityHint / partitionCount);
4545
}
46+
47+
if (maxTraversalHint <= 0 || maxTraversalHint >= partitionCount) {
48+
maxTraversal = partitionCount;
49+
} else {
50+
maxTraversal = maxTraversalHint;
51+
}
52+
}
53+
54+
private int hashForPartition() {
55+
var tid = VProxyThread.current().threadId;
56+
return (int) (tid & partitionCountMinusOne);
4657
}
4758

4859
public boolean add(E e) {
49-
for (int i = 0; i < partitionCount; ++i) {
50-
if (partitions[i].add(e)) {
60+
int m = maxTraversal;
61+
int hash = hashForPartition();
62+
for (int i = hash; m > 0; ++i, --m) {
63+
if (partitions[i & partitionCountMinusOne].add(e)) {
5164
return true;
5265
}
5366
}
5467
return false;
5568
}
5669

5770
public E poll() {
58-
for (int i = 0; i < partitionCount; ++i) {
59-
E e = partitions[i].poll();
71+
int m = maxTraversal;
72+
int hash = hashForPartition();
73+
for (int i = hash; m > 0; ++i, --m) {
74+
E e = partitions[i & partitionCountMinusOne].poll();
6075
if (e != null) {
6176
return e;
6277
}
@@ -73,27 +88,37 @@ public int size() {
7388
}
7489

7590
private static class Partition<E> {
76-
private final AtomicReference<StorageArray<E>> read;
77-
private volatile StorageArray<E> write;
78-
private final StorageArray<E> _1;
79-
private final StorageArray<E> _2;
91+
private final ReadWriteSpinLock lock = new ReadWriteSpinLock();
92+
private volatile ArrayQueue<E> read;
93+
private volatile ArrayQueue<E> write;
94+
private final ArrayQueue<E> _1;
95+
private final ArrayQueue<E> _2;
8096

8197
public Partition(int capacity) {
82-
_1 = new StorageArray<>(capacity);
83-
_2 = new StorageArray<>(capacity);
84-
read = new AtomicReference<>(_1);
98+
_1 = new ArrayQueue<>(capacity, lock);
99+
_2 = new ArrayQueue<>(capacity, lock);
100+
read = _1;
85101
write = _2;
86102
}
87103

88104
public boolean add(E e) {
89-
StorageArray<E> write = this.write;
105+
return add(1, e);
106+
}
107+
108+
private boolean add(int retry, E e) {
109+
if (retry > 10) { // max retry for 10 times
110+
return false; // too many retries
111+
}
90112

91-
// adding is always safe
92-
//noinspection RedundantIfStatement
113+
var write = this.write;
93114
if (write.add(e)) {
94115
return true;
95116
}
96-
// $write is full, storing fails
117+
118+
// the $write is full now
119+
if (swap(read, write, false)) {
120+
return add(retry + 1, e);
121+
}
97122
return false;
98123
}
99124

@@ -106,103 +131,134 @@ private E poll(int retry) {
106131
return null; // too many retries
107132
}
108133

109-
StorageArray<E> read = this.read.get();
110-
StorageArray<E> write = this.write;
134+
var read = this.read;
135+
var write = this.write;
111136

112-
// polling is always safe
113-
E ret = read.poll();
137+
var ret = read.poll();
114138
if (ret != null) {
115139
return ret;
116140
}
117141

118142
// no elements in the $read now
119-
// check whether we can swap (whether $write is full)
143+
if (swap(read, write, true)) {
144+
return poll(retry + 1);
145+
}
146+
return null;
147+
}
120148

121-
int writeEnd = write.end.get();
122-
if (writeEnd < write.capacity) {
123-
return null; // capacity not reached, do not swap and return nothing
124-
// no retry here because the write array will not change until something written into it
149+
// return true -> need retry
150+
// return false -> failed and should not retry
151+
private boolean swap(ArrayQueue<E> read, ArrayQueue<E> write, boolean isPolling) {
152+
// check whether we can swap
153+
if (read == write) {
154+
// is being swapped by another thread
155+
return true;
125156
}
126-
// also we should check whether there are no elements being stored
127-
if (write.storing.get() != 0) { // element is being stored into the array
128-
return poll(retry + 1); // try again
157+
158+
if (isPolling) { // $read is empty
159+
int writeEnd = write.end.get();
160+
if (writeEnd < write.capacity) {
161+
return false; // capacity not reached, do not swap and return nothing
162+
// no retry here because the write array will not change until something written into it
163+
}
164+
} else { // $write is full
165+
int readStart = read.start.get();
166+
if (readStart < read.end.get()) {
167+
return false; // still have objects to fetch, do not swap
168+
// no retry here because the read array will not change until something polling from it
169+
}
129170
}
130-
// now we can know that writing operations will not happen in this partition
131171

132-
// we can safely swap the two arrays now
133-
if (!this.read.compareAndSet(read, write)) {
134-
return poll(retry + 1); // concurrency detected: another thread is swapping
172+
lock.writeLock();
173+
if (this.read != read) {
174+
// already swapped by another thread
175+
lock.writeUnlock();
176+
return true;
135177
}
178+
// we can safely swap the two arrays now
179+
this.read = write;
136180
// the $read is expected to be empty
137181
assert read.size() == 0;
138182
read.reset(); // reset the cursors, so further operations can store data into this array
139183
this.write = read; // swapping is done
140-
return poll(retry + 1); // poll again
184+
lock.writeUnlock();
185+
186+
return true;
141187
}
142188

143189
public int size() {
144190
return _1.size() + _2.size();
145191
}
146192
}
147193

148-
private static class StorageArray<E> {
194+
private static class ArrayQueue<E> {
149195
private final int capacity;
196+
private final ReadWriteSpinLock lock;
150197
private final AtomicReferenceArray<E> array;
151-
private final AtomicInteger start = new AtomicInteger(-1);
152-
private final AtomicInteger end = new AtomicInteger(-1);
153-
private final AtomicInteger storing = new AtomicInteger(0);
198+
private final AtomicInteger start = new AtomicInteger(0);
199+
private final AtomicInteger end = new AtomicInteger(0);
154200

155-
private StorageArray(int capacity) {
201+
private ArrayQueue(int capacity, ReadWriteSpinLock lock) {
156202
this.capacity = capacity;
203+
this.lock = lock;
157204
this.array = new AtomicReferenceArray<>(capacity);
158205
}
159206

160207
boolean add(E e) {
161-
storing.incrementAndGet();
208+
lock.readLock();
162209

163210
if (end.get() >= capacity) {
164-
storing.decrementAndGet();
211+
lock.readUnlock();
165212
return false; // exceeds capacity
166213
}
167-
int index = end.incrementAndGet();
214+
int index = end.getAndIncrement();
168215
if (index < capacity) {
169216
// storing should succeed
170217
array.set(index, e);
171-
storing.decrementAndGet();
218+
lock.readUnlock();
172219
return true;
173220
} else {
174221
// storing failed
175-
storing.decrementAndGet();
222+
lock.readUnlock();
176223
return false;
177224
}
178225
}
179226

180227
E poll() {
181-
if (start.get() + 1 >= end.get() || start.get() + 1 >= capacity) {
228+
lock.readLock();
229+
230+
if (start.get() >= end.get() || start.get() >= capacity) {
231+
lock.readUnlock();
182232
return null;
183233
}
184-
int idx = start.incrementAndGet();
234+
int idx = start.getAndIncrement();
185235
if (idx >= end.get() || idx >= capacity) {
236+
lock.readUnlock();
186237
return null; // concurrent polling
187238
}
188-
return array.get(idx);
239+
var e = array.get(idx);
240+
lock.readUnlock();
241+
return e;
189242
}
190243

191244
int size() {
192-
int start = this.start.get() + 1;
245+
int start = this.start.get();
193246
if (start >= capacity) {
194247
return 0;
195248
}
196-
int cap = end.get() + 1;
249+
int cap = end.get();
197250
if (cap > capacity) {
198251
cap = capacity;
199252
}
253+
if (start > cap) {
254+
return 0;
255+
}
200256
return cap - start;
201257
}
202258

203259
void reset() {
204-
end.set(-1);
205-
start.set(-1);
260+
end.set(0);
261+
start.set(0);
206262
}
207263
}
208264
}

0 commit comments

Comments
 (0)
Failed to load comments.