标签:abs unlock 复杂 cep out png 根据 counter unit
这个专题我发现怎么慢慢演化为性能测试了,遇到任何东西我就忍不住去测一把。本文我们会大概看一下各种锁数据结构的简单用法,顺便也会来比拼一下性能。
首先,我们定一个抽象基类,用于各种锁测试的一些公共代码:
@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做一下最简单的操作:
@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中释放锁:
@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很复杂很容易用错,真的打算用的话务必研读官网的各种锁升级的例子(乐观读到读,乐观读到写,读到写)。
同样我们定义性能测试的类型:
@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后自己把玩,欢迎点赞。
欢迎关注我的微信公众号:随缘主人的园子
标签:abs unlock 复杂 cep out png 根据 counter unit
原文地址:https://www.cnblogs.com/lovecindywang/p/11216570.html