lock和synchronized的区别

目录

使用方式

公平性

锁状态的表示

线程可重入实现

线程同步(条件变量)

锁优化

响应中断


关于这两者的区别,网上随便一搜,会有各路大神的帖子,这里我仅仅是简单记根据自己不同阶段对并发的认识以及对synchronized和lock接口的了解程度简单记录一下。

不管是synchronized,还是Lock接口,本质上都是利用管程的思想来解决多线程的问题:互斥和线程通信两个问题。他们两者都是实现了MESA管程模型。

解决互斥的方式都是一样的,就是通过锁,而解决线程通信就是通过引入条件变量来实现。

使用方式

synchronized我们可以认为是通过代理的方式来实现的,编译器(编译成.class文件的静态编译)会在synchronized防护的临界区域的入口加上entermonitor指令,在临界区出口处加上exitmonitor,从而实现加锁和解锁的过程,这个过程对程序员是透明的。所以程序员使用synchronized的语法上非常简单。

Lock接口实现锁完全是基于java代码来实现的,它的子类实现锁是基于抽象父类AQS来实现。加锁和锁释放都需要程序员手动调用lock()和unlock()接口。

另外

Lock支持才有超时参数的获取锁tryLock(timeout),也支持非阻塞获得锁tryLock():如果锁空闲,获得锁,如果锁被占用,返回错误标识,不会阻塞线程。

公平性

synchronized只能实现公平锁竞争

Lock接口,可以是公平锁竞争,也可以是非公平锁竞争

锁状态的表示

synchronized对锁状态的表示是依赖于对象的内存布局中,对象头中的mark word部分。mark work有两bit的标志位来表示锁状态。

Lock接口是AQS中有个state变量,用来表示锁的状态。

线程可重入实现

synchronized关键字实现的可重入,同样是将当前获得锁的线程id放在了对象头的mark word中,在获得锁的时候会先检查对象头。

Lock接口是AQS中有个thread变量,获得锁的时候会先检查这个变量是否是Thread.currentThread()

线程同步(条件变量)

synchronized只有一个条件变量,所以也就只有一个对应的条件阻塞队列,即Object#wait()对应的阻塞队列。这对于实现一个锁多个条件的阻塞通信来说,比较麻烦。

而Lock接口中,AQS内部实现了Condition接口,可以创建多个Condition,每个Condition都有一个条件阻塞队列。Condition#await()会将对应的线程放入到该条件的阻塞队列。这对于实现一个锁多个条件的阻塞通信来说,就灵活得多。比如生产者-消费者模型中,有的线程生产馒头,有的生产香蕉,对应有的线程消费馒头,有的消费香蕉,这使用Lock接口相对就更容易。

另外,synchronized竞争锁,如果锁一值被别的线程占用,竞争锁的线程也将一直等待。但是Lock接口除了提供lock()方法外,还有tryLock(timeout),等待指定时间后,将推出等待。而有 一些编程规范中,总是建议使用tryLock(timeout)接口,因为如果这样即使产生了死锁,那么超时后也能自己挣脱出来。synchronized就不具备这样的能力。

ps:曾经看到有人说两者的区别还体现在jstack工具上,jstack工具只能检测synchronized导致的死锁,而不能检测出Lock接口导致的死锁,原因Lock是一个java原因层面的,synchronized是jvm层面的。 这种说法至少在jdk8中是不靠谱的,亲测jstack是可以检测出这两种方式的死锁。另外,死锁的检测一般都是基于图论的方式,如mysql的wait-for-graph,应该跟锁的实现方式没有关系,jstack检测死锁,应该也是类似的资源等待图的方式来检测的。

另外,不管是Lock还是synchronized,都是采用了两个队列:一个是放因为等待锁而阻塞的线程,一个是放因为获得锁后条件不满足而主动进入等待状态的线程(Object.wait,Condition.await)

锁优化

在jdk1.5之前,synchronized都是基于操作系统mutex(重量级锁)实现,性能比较差(原因:java的线程是基于KTL实现,和操作系统内核线程是1:1的,所以线程切换就意味着系统调用,会发生进程的上线文切换)。所以到了jdk1.8都还有人谈synchronized而色变,不愿意使用synchronized。但实际上synchronized已经经过很多优化了,在竞争不激烈的场景下,基本会不会使用到操作系统的mutex。

synchronized的锁优化:引入偏向锁--轻量级锁--重量级锁。以及采用自旋锁,锁粗化,基于逃逸分析的锁消除等优化手段。但是有个问题:1. 取消偏向锁,需要暂停偏向的线程,导致停顿,所以qps较高的时候,应该禁用偏向锁。2. synchronized会有锁升级的过程,比如因为竞争使得同步使用了重量级锁,那么以后的锁竞争都是重量级锁,不能再回到轻量级锁。这些都是和synchronized的实现机制决定的。

lock接口是基于AQS实现,在获得锁的时候,会先使用CAS去改变state的状态(获得锁),而执行CAS也有自旋的过程,只有CAS失败后才会调用park()接口挂起线程。但是下次获得锁的时候,还是会先用CAS,不存在锁升级的问题。

synchronized锁优化

响应中断

使用synchronized,因等待锁而被阻塞的线程不会响应中断。

但是使用Lock接口,提供了会影响中断的获取锁方式。

这里关于中断补充一下:

了解一点单片机,如c51的同学应该都知道,在51单片机的寄存器中,专门有个中断位,收到中断信号51芯片会将这个中断标志位置位,这样写程序的时候,就可以通过这个中断标志位来决定如何处理中断信号(大学的时候学过一点51,都忘差不多了,这里说出来不是讨论51的,只是方便理解java线程的中断,所以这里关于51的表述可能有不准确的地方)

在java线程中,Thread提供了一个interrupt()方法,很多地方都管这个方法叫中断线程,但是我觉得更准确的描述是向指定线程发送一个中断信号,而操作系统底层收到这个中断信号的时候,会类似于51一样,有个标记:该线程收到中断信号了,至于会怎么响应中断信号,还是程序员的事情。

比如:jdk大牛程序员,在实现Thread.sleep(),Object().wait()的时候,如果收到中断信号,它就抛出了InterruptedException异常,并且清楚中断标志位。再比如,处于RUNNING状态的线程,收到中断信号,其实啥也不会做,如果想要通过中断来终止线程执行run()方法,需要程序员自己在run方法中利用Thread.isInterrupt()方法来检测中断信号。

关于interrupt()方法,在学习java的时候比较困惑,因为大部分资料的介绍都是中断线程,且建议使用该方法终止线程的运行。但是自己试验了一下,run方法中用死循环,然后对该线程调用Interrupt()方法,run方法并没有结束,线程并没有退出。所以他们所说的"中断线程"和杀死线程并不是一回事,更准确的说就是发送中断信号。

总结:

从使用灵活性程度上,Lock更胜一筹

1. 最明显的就是Lock支持多个条件变量

2. lock的加锁和释放锁是显示的调用接口,那么程序员就可以有更灵活的控制加锁和释放锁的过程。

3. 加锁等待上,Lock只是指定超时时间的tryLock()。

4. 正式因为synchronized的使用方式不够灵活:不支持锁等待超时,不支响应中断,不支持非阻塞获得锁,所以synchronized无法通过破坏“不可抢夺”条件来避免死锁。

从性能上

其实看一些测试结果,如《深入理解jvm虚拟机》中引用的测试结果,两者差距不大。但是从synchronized存在锁升级,以及取消偏向锁会导致STW来看理论分析来看,Lock好像更胜一筹(这一点纯粹是个人理解而已,synchronized是jvm层面的,不同的jvm实现可能会对齐做更深层次的优化,而这些优化是jdk接口的Lock无法做到的也说不定)。但是偏向锁synchronize是可以有参数取消到的(就冲这一点,大概发明偏向锁的那个哥们也没有那么有底气,那就搞个开关,不行还可以观点)。所以下面就从锁升级的角度来看下synchronize和lock在性能上真的有那么大的区别么?

synchronize存在所谓的锁升级问题,一旦锁升级后,就不能降级了。但是Lock是不存在的这种升级的。

synchronize的锁升级就是:一旦锁变成了重量级锁,那么后续的线程进入这个锁防护的临界区的时候,都将实时用重量级锁,就不会再有轻量级锁自旋的过程了。

lock是没有这个升级的,不管是时候,获得锁的过程都是一样的。

表面上看,synchronize好像在这方面更逊色一些,但是我的观点是设计理念不一样而已,面对不同的场景就各有优劣,但区别并没有那么明显。

  • 对于synchronize有锁升级的。只要出现了相对严重的冲突,那么后续所有的进入临界区都将使用重量级锁,享受不到轻量级锁自旋的优化了。那怕是后面的冲突没那么严重了,也是一样的,还是只能使用重量级锁。
  • 对于lock,因为锁的获取都是一样的,不敢啥时候,都会先自旋、自旋超时后才会启用重量级锁。那么Lock对于应对突然冲突后、又缓和了的情况是更加从容的,效率其实也会更高。

但如果冲突一直很严重,那Lock就会有大量的无用的自旋,这个CAS自旋占用的cpu是白白浪费的,因为有冲突所以CAS最终还是会失败,从而启用重量级锁。

所以,从这个角度讲,一般来说不会有那么大的锁冲突,即使某个时间段流量尖峰导致锁冲突加剧,那么也不太可能会一直持续。所以时间拉长平均来看,好像lock在性能上还是更有优势一旦。但别忘了,synchronize不是遇到冲突就升级,而是自旋不成功才会导致锁升级,也就是说锁里的逻辑比较复杂,占用了锁太长时间导致锁长时间不能释放,加剧了冲突概率,从而导致锁升级。这中在锁里写中逻辑,本身就不是什么好的实践,锁防护的代码应该只是访问需要互斥保护的资源,其他所有操作都应该挪到锁外面去,所以遵循这种规范,其实synchronize锁升级不应该是个频繁发生才对。所以这么一看,两者的区别也就没那么大了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值