這個專題我發現怎麼慢慢演化爲性能測試了,遇到任何東西我就忍不住去測一把。本文咱們會大概看一下各類鎖數據結構的簡單用法,順便也會來比拼一下性能。java
首先,咱們定一個抽象基類,用於各類鎖測試的一些公共代碼:git
@Slf4j
abstract class LockTask implements Runnable {
protected volatile static long counter;
protected boolean write;
protected static HashMap<Long, String> hashMap = new HashMap<>();
int loopCount;
CountDownLatch start;
CountDownLatch finish;
public LockTask(Boolean write) {
this.write = write;
}
@Override
public void run() {
try {
start.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < loopCount; i++) {
doTask();
}
finish.countDown();
}
abstract protected void doTask();
}
複製代碼
下面咱們實現最簡單的使用synchronized來實現的鎖,拿到鎖後咱們針對hashMap和counter作一下最簡單的操做:github
@Slf4j
class SyncTask extends LockTask {
private static Object locker = new Object();
public SyncTask(Boolean write) {
super(write);
}
@Override
protected void doTask() {
synchronized (locker) {
if (write) {
counter++;
hashMap.put(counter, "Data" + counter);
} else {
hashMap.get(counter);
//log.debug("{}, {}", this.getClass().getSimpleName(), value);
}
}
}
}
複製代碼
而後是ReentrantLock,使用也是很簡單,須要在finally中釋放鎖:bash
@Slf4j
class ReentrantLockTask extends LockTask {
private static ReentrantLock locker = new ReentrantLock();
public ReentrantLockTask(Boolean write) {
super(write);
}
@Override
protected void doTask() {
locker.lock();
try {
if (write) {
counter++;
hashMap.put(counter, "Data" + counter);
} else {
hashMap.get(counter);
}
} finally {
locker.unlock();
}
}
}
複製代碼
而後是ReentrantReadWriteLock,可重入的讀寫鎖,這屋裏咱們須要區分讀操做仍是寫操做來得到不一樣類型的鎖:微信
@Slf4j
class ReentrantReadWriteLockTask extends LockTask {
private static ReentrantReadWriteLock locker = new ReentrantReadWriteLock();
public ReentrantReadWriteLockTask(Boolean write) {
super(write);
}
@Override
protected void doTask() {
if (write) {
locker.writeLock().lock();
try {
counter++;
hashMap.put(counter, "Data" + counter);
} finally {
locker.writeLock().unlock();
}
} else {
locker.readLock().lock();
try {
hashMap.get(counter);
} finally {
locker.readLock().unlock();
}
}
}
}
複製代碼
而後是可重入鎖和可重入讀寫鎖的公平版本:數據結構
@Slf4j
class FairReentrantLockTask extends LockTask {
private static ReentrantLock locker = new ReentrantLock(true);
public FairReentrantLockTask(Boolean write) {
super(write);
}
@Override
protected void doTask() {
locker.lock();
try {
if (write) {
counter++;
hashMap.put(counter, "Data" + counter);
} else {
hashMap.get(counter);
}
} finally {
locker.unlock();
}
}
}
@Slf4j
class FairReentrantReadWriteLockTask extends LockTask {
private static ReentrantReadWriteLock locker = new ReentrantReadWriteLock(true);
public FairReentrantReadWriteLockTask(Boolean write) {
super(write);
}
@Override
protected void doTask() {
if (write) {
locker.writeLock().lock();
try {
counter++;
hashMap.put(counter, "Data" + counter);
} finally {
locker.writeLock().unlock();
}
} else {
locker.readLock().lock();
try {
hashMap.get(counter);
} finally {
locker.readLock().unlock();
}
}
}
}
複製代碼
最後是1.8推出的StampedLock:併發
@Slf4j
class StampedLockTask extends LockTask {
private static StampedLock locker = new StampedLock();
public StampedLockTask(Boolean write) {
super(write);
}
@Override
protected void doTask() {
if (write) {
long stamp = locker.writeLock();
try {
counter++;
hashMap.put(counter, "Data" + counter);
} finally {
locker.unlockWrite(stamp);
}
} else {
long stamp = locker.tryOptimisticRead();
long value = counter;
if (!locker.validate(stamp)) {
stamp = locker.readLock();
try {
value = counter;
} finally {
locker.unlockRead(stamp);
}
}
hashMap.get(value);
}
}
}
複製代碼
這裏一樣區分讀寫鎖,只是讀鎖咱們先嚐試進行樂觀讀,拿到一個戳後讀取咱們須要保護的數據,隨後校驗一下這個戳若是沒問題的話說明數據沒有改變,樂觀鎖生效,若是有問題升級爲悲觀鎖再讀取一次。由於StampedLock很複雜很容易用錯,真的打算用的話務必研讀官網的各類鎖升級的例子(樂觀讀到讀,樂觀讀到寫,讀到寫)。app
一樣咱們定義性能測試的類型:ide
@ToString
@RequiredArgsConstructor
class TestCase {
final Class lockTaskClass;
final int writerThreadCount;
final int readerThreadCount;
long duration;
}
複製代碼
每一種測試能夠靈活選擇:高併發
下面是性能測試的場景定義:
@Test
public void test() throws Exception {
List<TestCase> testCases = new ArrayList<>();
Arrays.asList(SyncTask.class,
ReentrantLockTask.class,
FairReentrantLockTask.class,
ReentrantReadWriteLockTask.class,
FairReentrantReadWriteLockTask.class,
StampedLockTask.class
).forEach(syncTaskClass -> {
testCases.add(new TestCase(syncTaskClass, 1, 0));
testCases.add(new TestCase(syncTaskClass, 10, 0));
testCases.add(new TestCase(syncTaskClass, 0, 1));
testCases.add(new TestCase(syncTaskClass, 0, 10));
testCases.add(new TestCase(syncTaskClass, 1, 1));
testCases.add(new TestCase(syncTaskClass, 10, 10));
testCases.add(new TestCase(syncTaskClass, 50, 50));
testCases.add(new TestCase(syncTaskClass, 100, 100));
testCases.add(new TestCase(syncTaskClass, 500, 500));
testCases.add(new TestCase(syncTaskClass, 1000, 1000));
testCases.add(new TestCase(syncTaskClass, 1, 10));
testCases.add(new TestCase(syncTaskClass, 10, 100));
testCases.add(new TestCase(syncTaskClass, 10, 200));
testCases.add(new TestCase(syncTaskClass, 10, 500));
testCases.add(new TestCase(syncTaskClass, 10, 1000));
testCases.add(new TestCase(syncTaskClass, 10, 1));
testCases.add(new TestCase(syncTaskClass, 100, 10));
testCases.add(new TestCase(syncTaskClass, 200, 10));
testCases.add(new TestCase(syncTaskClass, 500, 10));
testCases.add(new TestCase(syncTaskClass, 1000, 10));
});
testCases.forEach(testCase -> {
System.gc();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
benchmark(testCase);
} catch (Exception e) {
e.printStackTrace();
}
});
StringBuilder stringBuilder = new StringBuilder();
int index = 0;
for (TestCase testCase : testCases) {
if (index % 20 == 0)
stringBuilder.append("\r\n");
stringBuilder.append(testCase.duration);
stringBuilder.append(",");
index++;
}
System.out.println(stringBuilder.toString());
}
複製代碼
在這裏能夠看到,咱們爲這6個鎖定義了20種測試場景,覆蓋幾大類:
每一次測試之間強制觸發gc後休眠1秒,每20次結果換行一次輸出。 測試類以下:
private void benchmark(TestCase testCase) throws Exception {
LockTask.counter = 0;
log.info("Start benchmark:{}", testCase);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch finish = new CountDownLatch(testCase.readerThreadCount + testCase.writerThreadCount);
if (testCase.readerThreadCount > 0) {
LockTask readerTask = (LockTask) testCase.lockTaskClass.getDeclaredConstructor(Boolean.class).newInstance(false);
readerTask.start = start;
readerTask.finish = finish;
readerTask.loopCount = LOOP_COUNT / testCase.readerThreadCount;
if (testCase.lockTaskClass.getSimpleName().startsWith("Fair")) readerTask.loopCount /= 100;
IntStream.rangeClosed(1, testCase.readerThreadCount)
.mapToObj(__ -> new Thread(readerTask))
.forEach(Thread::start);
}
if (testCase.writerThreadCount > 0) {
LockTask writerTask = (LockTask) testCase.lockTaskClass.getDeclaredConstructor(Boolean.class).newInstance(true);
writerTask.start = start;
writerTask.finish = finish;
writerTask.loopCount = LOOP_COUNT / testCase.writerThreadCount;
if (testCase.lockTaskClass.getSimpleName().startsWith("Fair")) writerTask.loopCount /= 100;
IntStream.rangeClosed(1, testCase.writerThreadCount)
.mapToObj(__ -> new Thread(writerTask))
.forEach(Thread::start);
}
start.countDown();
long begin = System.currentTimeMillis();
finish.await();
if (testCase.writerThreadCount > 0) {
if (testCase.lockTaskClass.getSimpleName().startsWith("Fair")) {
Assert.assertEquals(LOOP_COUNT / 100, LockTask.counter);
} else {
Assert.assertEquals(LOOP_COUNT, LockTask.counter);
}
}
testCase.duration = System.currentTimeMillis() - begin;
log.info("Finish benchmark:{}", testCase);
}
複製代碼
代碼主要乾了幾件事情:
在這裏,咱們把循環次數設置爲1000萬次,在阿里雲12核12G機器JDK8環境下運行獲得的結果以下:
這裏,咱們進行兩次測試,其實一開始個人測試代碼裏沒有HashMap的讀寫操做,只有counter的讀寫操做(這個時候循環次數是1億次),全部第一次測試是僅僅只有counter的讀寫操做的,後一次測試是這裏貼的代碼的版本。
因此這個表格中的數據不能直接來對比由於混雜了三種循環次數,上面那個表是1億從循環的時間,下面那個是1000萬次,黃色的兩條分別是100萬次和10萬次循環。
這個測試信息量很大,這裏說一下我看到的幾個結論,或者你還能夠從這個測試中品味出其它結論:
因此說對於這些鎖的選擇也很明確:
以前也提到了可重入鎖相對synchronized有一些高級特性,咱們寫一些測試代碼:
@Test
public void test() throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock(true);
IntStream.rangeClosed(1, 10).forEach(i -> reentrantLock.lock());
log.info("getHoldCount:{},isHeldByCurrentThread:{},isLocked:{}",
reentrantLock.getHoldCount(),
reentrantLock.isHeldByCurrentThread(),
reentrantLock.isLocked());
List<Thread> threads = IntStream.rangeClosed(1, 10).mapToObj(i -> new Thread(() -> {
try {
if (reentrantLock.tryLock(i, TimeUnit.SECONDS)) {
try {
log.debug("Got lock");
} finally {
reentrantLock.unlock();
}
} else {
log.debug("Cannot get lock");
}
} catch (InterruptedException e) {
log.debug("InterruptedException Cannot get lock");
e.printStackTrace();
}
})).collect(Collectors.toList());
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> log.info("getHoldCount:{}, getQueueLength:{}, hasQueuedThreads:{}, waitThreads:{}",
reentrantLock.getHoldCount(),
reentrantLock.getQueueLength(),
reentrantLock.hasQueuedThreads(),
threads.stream().filter(reentrantLock::hasQueuedThread).count()), 0, 1, TimeUnit.SECONDS);
threads.forEach(Thread::start);
TimeUnit.SECONDS.sleep(5);
IntStream.rangeClosed(1, 10).forEach(i -> reentrantLock.unlock());
TimeUnit.SECONDS.sleep(1);
}
複製代碼
輸出以下:
08:14:50.834 [main] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:10,isHeldByCurrentThread:true,isLocked:true
08:14:50.849 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:10, hasQueuedThreads:true, waitThreads:10
08:14:51.849 [Thread-0] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:51.848 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:9, hasQueuedThreads:true, waitThreads:9
08:14:52.849 [Thread-1] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:52.849 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:8, hasQueuedThreads:true, waitThreads:8
08:14:53.846 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:8, hasQueuedThreads:true, waitThreads:8
08:14:53.847 [Thread-2] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:54.847 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:7, hasQueuedThreads:true, waitThreads:7
08:14:54.849 [Thread-3] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:55.847 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:6, hasQueuedThreads:true, waitThreads:6
08:14:55.850 [Thread-4] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Cannot get lock
08:14:55.850 [Thread-5] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:55.851 [Thread-6] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:55.852 [Thread-7] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:55.852 [Thread-8] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:55.852 [Thread-9] DEBUG me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - Got lock
08:14:56.849 [pool-1-thread-1] INFO me.josephzhu.javaconcurrenttest.lock.ReentrantLockTest - getHoldCount:0, getQueueLength:0, hasQueuedThreads:false, waitThreads:0
複製代碼
從這個輸出能夠看到:
這也能夠看到可重入鎖相比synchronized功能更強大點:
提到了可重入,咱們進行一個無聊的實驗看看能夠重入多少次:
@Test
public void test2() {
ReentrantLock reentrantLock = new ReentrantLock(true);
int i = 0;
try {
while (true) {
reentrantLock.lock();
i++;
}
} catch (Error error) {
log.error("count:{}", i, error);
}
}
複製代碼
結果以下:
最後再提下最簡單的鎖誤用的例子,雖然沒有那麼高大上,可是這種由於鎖範圍和鎖保護對象的範圍不一致致使誤用的問題在業務代碼中處處都是,好比:
@Slf4j
public class LockMisuse {
@Test
public void test1() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
IntStream.rangeClosed(1, 100000).forEach(i -> executorService.submit(new Container()::test));
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.HOURS);
log.info("{}", Container.counter);
}
}
class Container {
static int counter = 0;
Object locker = new Object();
void test() {
synchronized (locker) {
counter++;
}
}
}
複製代碼
在代碼裏咱們要保護的資源是靜態的,可是鎖倒是對象級別的,不一樣的實例持有不一樣的鎖,徹底起不到保護做用:
本文咱們簡單測試了一下各類鎖的性能,我感受這個測試可能還沒法100%模擬真實的場景,真實狀況下不只僅是讀寫線程數量的不一致,更可能是操做頻次的不一致,不過這個測試基本看到了咱們猜想的結果。在平常代碼開發過程當中,你們能夠根據實際功能和場景須要來選擇合適的鎖類型。
有的時候高大上的一些鎖由於使用複雜容易致使誤用、錯用、死鎖、活鎖等問題,我反而建議在沒有明顯問題的狀況下先從簡單的『悲觀』鎖開始使用。還有就是像最後的例子,使用鎖的話務必須要認證檢查代碼,思考鎖和保護對象的關係,避免鎖不產產生效果致使隱藏的Bug。
一樣,代碼見個人Github,歡迎clone後本身把玩,歡迎點贊。
歡迎關注個人微信公衆號:隨緣主人的園子