问题
(1)ReentrantLock有哪些优点?
(2)ReentrantLock有哪些缺点?
(3)ReentrantLock是否可以完全替代synchronized?
简介
synchronized是Java原生提供的用于在多线程环境中保证同步的关键字,底层是通过修改对象头中的MarkWord来实现的。
ReentrantLock是Java语言层面提供的用于在多线程环境中保证同步的类,底层是通过原子更新状态变量state来实现的。
既然有了synchronized的关键字来保证同步了,为什么还要实现一个ReentrantLock类呢?它们之间有什么异同呢?
ReentrantLock VS synchronized
直接上表格:(手机横屏查看更方便)
功能 | ReentrantLock | synchronized |
---|---|---|
可重入 | 支持 | 支持 |
非公平 | 支持(默认) | 支持 |
加锁/解锁方式 | 需要手动加锁、解锁,一般使用try..finally..保证锁能够释放 | 手动加锁,无需刻意解锁 |
按key锁 | 不支持,比如按用户id加锁 | 支持,synchronized加锁时需要传入一个对象 |
公平锁 | 支持,new ReentrantLock(true) | 不支持 |
中断 | 支持,lockInterruptibly() | 不支持 |
尝试加锁 | 支持,tryLock() | 不支持 |
超时锁 | 支持,tryLock(timeout, unit) | 不支持 |
获取当前线程获取锁的次数 | 支持,getHoldCount() | 不支持 |
获取等待的线程 | 支持,getWaitingThreads() | 不支持 |
检测是否被当前线程占有 | 支持,isHeldByCurrentThread() | 不支持 |
检测是否被任意线程占有 | 支持,isLocked() | 不支持 |
条件锁 | 可支持多个条件,condition.await(),condition.signal(),condition.signalAll() | 只支持一个,obj.wait(),obj.notify(),obj.notifyAll() |
对比测试
在测试之前,我们先预想一下结果,随着线程数的不断增加,ReentrantLock(fair)、ReentrantLock(unfair)、synchronized三者的效率怎样呢?
我猜测应该是ReentrantLock(unfair)> synchronized > ReentrantLock(fair)。
到底是不是这样呢?
直接上测试代码:(为了全面对比,彤哥这里把AtomicInteger和LongAdder也拿来一起对比了)
public class ReentrantLockVsSynchronizedTest {
public static AtomicInteger a = new AtomicInteger(0);
public static LongAdder b = new LongAdder();
public static int c = 0;
public static int d = 0;
public static int e = 0;
public static final ReentrantLock fairLock = new ReentrantLock(true);
public static final ReentrantLock unfairLock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
System.out.println("-------------------------------------");
testAll(1, 100000);
System.out.println("-------------------------------------");
testAll(2, 100000);
System.out.println("-------------------------------------");
testAll(4, 100000);
System.out.println("-------------------------------------");
testAll(6, 100000);
System.out.println("-------------------------------------");
testAll(8, 100000);
System.out.println("-------------------------------------");
testAll(10, 100000);
System.out.println("-------------------------------------");
testAll(50, 100000);
System.out.println("-------------------------------------");
testAll(100, 100000);
System.out.println("-------------------------------------");
testAll(200, 100000);
System.out.println("-------------------------------------");
testAll(500, 100000);
System.out.println("-------------------------------------");
// testAll(1000, 1000000);
System.out.println("-------------------------------------");
testAll(500, 10000);
System.out.println("-------------------------------------");
testAll(500, 1000);
System.out.println("-------------------------------------");
testAll(500, 100);
System.out.println("-------------------------------------");
testAll(500, 10);
System.out.println("-------------------------------------");
testAll(500, 1);
System.out.println("-------------------------------------");
}
public static void testAll(int threadCount, int loopCount) throws InterruptedException {
testAtomicInteger(threadCount, loopCount);
testLongAdder(threadCount, loopCount);
testSynchronized(threadCount, loopCount);
testReentrantLockUnfair(threadCount, loopCount);
// testReentrantLockFair(threadCount, loopCount);
}
public static void testAtomicInteger(int threadCount, int loopCount) throws InterruptedException {
long start = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
for (int j = 0; j < loopCount; j++) {
a.incrementAndGet();
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("testAtomicInteger: result=" + a.get() + ", threadCount=" + threadCount + ", loopCount=" + loopCount + ", elapse=" + (System.currentTimeMillis() - start));
}
public static void testLongAdder(int threadCount, int loopCount) throws InterruptedException {
long start = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
for (int j = 0; j < loopCount; j++) {
b.increment();
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("testLongAdder: result=" + b.sum() + ", threadCount=" + threadCount + ", loopCount=" + loopCount + ", elapse=" + (System.currentTimeMillis() - start));
}
public static void testReentrantLockFair(int threadCount, int loopCount) throws InterruptedException {
long start = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
for (int j = 0; j < loopCount; j++) {
fairLock.lock();
// 消除try的性能影响
// try {
c++;
// } finally {
fairLock.unlock();
// }
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("testReentrantLockFair: result=" + c + ", threadCount=" + threadCount + ", loopCount=" + loopCount + ", elapse=" + (System.currentTimeMillis() - start));
}
public static void testReentrantLockUnfair(int threadCount, int loopCount) throws InterruptedException {
long start = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
for (int j = 0; j < loopCount; j++) {
unfairLock.lock();
// 消除try的性能影响
// try {
d++;
// } finally {
unfairLock.unlock();
// }
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("testReentrantLockUnfair: result=" + d + ", threadCount=" + threadCount + ", loopCount=" + loopCount + ", elapse=" + (System.currentTimeMillis() - start));
}
public static void testSynchronized(int threadCount, int loopCount) throws InterruptedException {
long start = System.currentTimeMillis();
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
for (int j = 0; j < loopCount; j++) {
synchronized (ReentrantLockVsSynchronizedTest.class) {
e++;
}
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("testSynchronized: result=" + e + ", threadCount=" + threadCount + ", loopCount=" + loopCount + ", elapse=" + (System.currentTimeMillis() - start));
}
}
真的是不测不知道,一测吓一跳,运行后发现以下规律:
随着线程数的不断增加,synchronized的效率竟然比ReentrantLock非公平模式要高!
电脑上大概是高3倍左右,我的运行环境是4核8G,java版本是8,请大家一定要在自己电脑上运行一下,并且最好能给我反馈一下。
使用Java7及以下的版本运行了,发现在Java7及以下版本中synchronized的效率确实比ReentrantLock的效率低一些。
总结
(1)synchronized是Java原生关键字锁;
(2)ReentrantLock是Java语言层面提供的锁;
(3)ReentrantLock的功能非常丰富,解决了很多synchronized的局限性;
(4)至于在非公平模式下,ReentrantLock与synchronized的效率孰高孰低,彤哥给出的结论是随着Java版本的不断升级,synchronized的效率只会越来越高;
彩蛋
既然ReentrantLock的功能更丰富,而且效率也不低,我们是不是可以放弃使用synchronized了呢?
答:我认为不是。因为synchronized是Java原生支持的,随着Java版本的不断升级,Java团队也是在不断优化synchronized,所以我认为在功能相同的前提下,最好还是使用原生的synchronized关键字来加锁,这样我们就能获得Java版本升级带来的免费的性能提升的空间。
另外,在Java8的ConcurrentHashMap中已经把ReentrantLock换成了synchronized来分段加锁了,这也是Java版本不断升级带来的免费的synchronized的性能提升。
从构建分布式秒杀系统聊聊Lock锁使用中的坑
前言
在单体架构的秒杀活动中,为了减轻DB层的压力,这里我们采用了Lock锁来实现秒杀用户排队抢购。然而很不幸的是尽管使用了锁,但是测试过程中仍然会超卖,执行了N多次发现依然有问题。输出一下代码吧,可能大家看的比较真切:
@Service("seckillService")
public class SeckillServiceImpl implements ISeckillService {
/**
* 思考:为什么不用synchronized
* service 默认是单例的,并发下lock只有一个实例
*/
private Lock lock = new ReentrantLock(true);//互斥锁 参数默认false,不公平锁
@Autowired
private DynamicQuery dynamicQuery;
@Override
@Transactional
public Result startSeckilLock(long seckillId, long userId) {
try {
lock.lock();
//这里、不清楚为啥、总是会被超卖101、难道锁不起作用、lock是同一个对象
String nativeSql = "SELECT number FROM seckill WHERE seckill_id=?";
Object object = dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});
Long number = ((Number) object).longValue();
if(number>0){
nativeSql = "UPDATE seckill SET number=number-1 WHERE seckill_id=?";
dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});
SuccessKilled killed = new SuccessKilled();
killed.setSeckillId(seckillId);
killed.setUserId(userId);
killed.setState(Short.parseShort(number+""));
killed.setCreateTime(new Timestamp(new Date().getTime()));
dynamicQuery.save(killed);
}else{
return Result.error(SeckillStatEnum.END);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
return Result.ok(SeckillStatEnum.SUCCESS);
}
}
代码写在service层,bean默认是单例的,也就是说lock肯定是一个对象。感觉不放心,还是打印一下 lock.hashCode(),输出结果没问题。由于还有其他事情要做,最终还是带着疑问提交代码到码云。
追踪
如果想分享代码并使大家一起参与进来,一定要自荐,这样才会被更多的人发现。当然,如果有交流群一定要留下联系方式,这样讨论起来可能更方便。项目被推荐后,果然加群的小伙伴就多了。由于项目配置好相应参数就可以测试,并且每个点都有相应的文字注释,其中有心的小伙伴果然注意到了我写的注释<这里、不清楚为啥、总是会被超卖101、难道锁不起作用、lock是同一个对象>,然后提出了困扰自己好多天的问题。
码友zoain说,测试了好久终于发现了问题,原来lock锁是在事物单元中执行的。看到这里,小伙伴们有没有恍然大悟,反正我是悟了。这里,总结一下为什么会超卖101:秒杀开始后,某个事物在未提交之前,锁已经释放(事物提交是在整个方法执行完),导致下一个事物读取到了上个事物未提交的数据,也就是传说中的脏读。此处给出的建议是锁上移,也就是说要包住整个事物单元。
AOP+锁
为了包住事物单元,这里我们使用AOP切面编程,当然你也可以上移到Control层。
自定义注解Servicelock:
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Servicelock {
String description() default "";
}
自定义切面LockAspect:
@Component
@Scope
@Aspect
public class LockAspect {
/**
* 思考:为什么不用synchronized
* service 默认是单例的,并发下lock只有一个实例
*/
private static Lock lock = new ReentrantLock(true);//互斥锁 参数默认false,不公平锁
//Service层切点 用于记录错误日志
@Pointcut("@annotation(com.itstyle.seckill.common.aop.Servicelock)")
public void lockAspect() {
}
@Around("lockAspect()")
public Object around(ProceedingJoinPoint joinPoint) {
lock.lock();
Object obj = null;
try {
obj = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
} finally{
lock.unlock();
}
return obj;
}
}
切入秒杀方法:
@Service("seckillService")
public class SeckillServiceImpl implements ISeckillService {
/**
* 思考:为什么不用synchronized
* service 默认是单例的,并发下lock只有一个实例
*/
private Lock lock = new ReentrantLock(true);//互斥锁 参数默认false,不公平锁
@Autowired
private DynamicQuery dynamicQuery;
@Override
@Servicelock
@Transactional
public Result startSeckilAopLock(long seckillId, long userId) {
//来自码云码友<马丁的早晨>的建议 使用AOP + 锁实现
String nativeSql = "SELECT number FROM seckill WHERE seckill_id=?";
Object object = dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});
Long number = ((Number) object).longValue();
if(number>0){
nativeSql = "UPDATE seckill SET number=number-1 WHERE seckill_id=?";
dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});
SuccessKilled killed = new SuccessKilled();
killed.setSeckillId(seckillId);
killed.setUserId(userId);
killed.setState(Short.parseShort(number+""));
killed.setCreateTime(new Timestamp(new Date().getTime()));
dynamicQuery.save(killed);
}else{
return Result.error(SeckillStatEnum.END);
}
return Result.ok(SeckillStatEnum.SUCCESS);
}
}
所有的工作完成以后,我们来测试一下代码,意料之中,再也没有出现超卖的现象。然而,你以为就这么结束了么?细心的码友IM核米,又提出了以下问题:Spring 里的切片在未指定排序的时候,两个注解是随意执行的。如果事务在加锁前执行的话,是不是就会产生问题?
首先,由于自己实在没有时间去取证,最终还是码友IM核米完成了自问自答,这里引用下他的解释:
我说的没错,但 @Transactional 切片是特殊情况
1)多 AOP 之间的执行顺序在未指定时是 :undefined ,官方文档并没有说一定会按照注解的顺序进行执行,只会按照 @ Order 的顺序执行。
可参考官方文档: 可以在页面里搜索 Command+F「7.2.4.7 Advice ordering」https://docs.spring.io/spring/docs/3.0.x/spring-framework-reference/html/aop.html#aop-ataspectj-advice-ordering
2)事务切面的 default Order 被设置为了 Ordered.LOWEST_PRECEDENCE,所以默认情况下是属于最内层的环切。
可参考官方文档: 可以在页面里搜索 Command+F「Table 10.2. tx:annotation-driven/ settings」 https://docs.spring.io/spring/docs/3.0.x/reference/transaction.html#transaction-declarative-txadvice-settings
总结
经验真的很重要,踩的坑多了也变走成了路
不要吝啬自己的总结成果,分享交流才能够促使大家共同进步
最好不要怀疑久经考验的Lock锁同志,很有可能是你使用的方式不对
http://www.likecs.com/show-60479.html
https://www.jitwxs.cn/cbe421eb.html
https://www.jianshu.com/p/6f58346fe95e
https://blog.csdn.net/LoveJavaYDJ/article/details/53584396
https://blog.csdn.net/zengdeqing2012/article/details/54342331