Java—对锁的理解

本文通过日常生活中的超市储物柜和健身房更衣室为例,深入浅出地解释Java中的锁机制,包括锁的概念、类型(如公平锁、非公平锁、乐观锁等)、升级以及死锁和活锁现象。强调理解锁的原理和目的,而非死记硬背技术细节。
摘要由CSDN通过智能技术生成

在Java面试中,对锁的理解是一个重要的话题。

网上关于锁的技术层面的博文有一大堆,这里不再赘述。本文尝试从生活的角度去对Java中的锁机制进行解释说明,以便你能更好的理解和消化。

之前看过一个大神对锁的一个总结:锁的实现原理都是为了达到一个目的——让所有的线程都能看到某种标记。(这里暂停一会儿会儿,你可以细品一下这句话)

注意这句话的关键字:标记!

类比生活中的例子,你想想,不管是一般房门上的锁,还是保险箱的锁,亦或公共厕所中的锁,是不是都是一种标记?看到这种标记之后,你的第一反应是什么?~~~噢,这个坑位已经有人了,我用不了了。

联想一下生活,我们生活中为什么要使用锁?锁都有哪些类型?锁的使用方式是怎样的?......等等,这些关于锁的问题,你完全可以把Java中的锁带入到日常生活中进行类比,这样它就没那么难理解了。

1.锁的概念

在现实生活中,锁的概念我们可以类比为超市的储物柜。

假设一个超市有多个储物柜,每个储物柜就像Java中的共享资源。当一个人(线程)进入超市时,他首先会选择一个空的储物柜,然后把物品放入并锁上柜门(获取锁)。这时,其他人无法打开这个储物柜(其他线程无法访问已锁定的资源),从而保证了个人物品的安全性。

如果另一个人试图在同一时间打开同一个储物柜(另一个线程尝试访问已被锁定的资源),由于柜门已经被锁住,他将无法立即打开,只能等待第一个使用者使用完毕,打开柜门取出物品并解锁柜子(释放锁)后,第二个使用者才能获取到锁,进而使用这个储物柜。

在Java编程中,这种互斥机制同样存在,例如通过synchronized关键字或者Lock接口实现的锁,可以防止多个线程同时修改某个共享变量或执行特定代码块,从而避免因并发导致的数据不一致等问题。

你可以再找生活中的例子,对锁的概念进行自己的理解。

2.锁的类型

Java中的锁有好几个分类角度,可以是读写锁、可以是公平锁和非公平锁、也可以是悲观锁和乐观锁......等等。以下对常见分类进行说明,上边我们的实例场景在超市,接下来我们进入公共浴室。

  1. 公平锁与非公平锁
    • 公平锁就像是排队等候储物柜的规则,每个人都按照到达浴室的顺序依次使用储物柜。即使后来的人动作更快,也不能插队优先使用。
    • 非公平锁则是没有严格排队的规则,刚空出来的储物柜可能会被任何一个等待的人迅速占有,即使他们并非下一个到达的人。在高并发情况下,非公平锁可能带来更高的效率,但也可能导致某些线程长时间等待,形成“线程饥饿”。
  2. 可重入锁
    • 类似于一个人可以在拥有某个储物柜钥匙的同时,再次打开并关闭这个储物柜(递归调用加锁的方法)。在Java中,synchronized关键字修饰的方法默认是可重入的,意味着持有锁的线程可以再次进入同步代码块,而不必担心自己把自己锁在外面。
  3. 悲观锁与乐观锁
    • 悲观锁对应现实生活中的保守做法,就像人们总是假设储物柜随时可能被人占用,所以每次都要确认柜门是否已经上锁(即每次访问资源前都进行加锁操作)。
    • 乐观锁则相对乐观,如同认为短时间内储物柜不太可能被占用,于是先直接打开柜门查看(不立即加锁),若发现已有物品(即数据已被修改),则重新开始尝试(回滚并重试)。在Java中,乐观锁常用CAS(Compare And Swap)原子指令实现,通过不断地比较并交换内存值来实现无锁并发控制。
  4. 条件变量与信号量
    • 在Java的java.util.concurrent.locks.Condition接口中,条件变量可以看作是储物柜旁边的公告板。假设每个人在等待储物柜清空时,并不一直傻傻地站在那里,而是去公告板那里登记一下,留下联系方式,然后去做其他事情。一旦储物柜空出来了,会有人通知(signal)登记过的下一个人。这就好比条件变量提供的await()方法让线程等待,并通过signal()或signalAll()唤醒等待的线程。
  5. StampedLock
    • 假设储物柜升级为了智能储物柜,不仅有锁,还有印章(stamp)。你可以快速查看印章判断储物柜状态(读模式),如果想存取物品,则需要换领新的印章(写模式或乐观写模式)。StampedLock就是这样一种更精细的锁机制,它可以提供读模式、写模式和乐观读写模式,极大地提高了多线程环境下的读操作并发性能。
  6. 死锁与活锁
    • 死锁就像浴室里每个人都拿着别人的储物柜钥匙,互相等待对方释放储物柜,结果谁也无法离开。在Java并发编程中,如果不合理地管理和释放锁,就可能出现死锁现象,导致程序无法正常执行。
    • 活锁则像是一群人为了避免碰撞而不断调整自己的行进方向,结果却导致彼此都无法前进。在并发编程中,活锁表现为线程因为某种条件反复重试,虽然都在运行并未阻塞,但实际上却没有进展。

也许上边的解释中,有的你能理解,有的理解起来有点困难,没关系,挑自己能理解的多想想就行。

3.锁升级

我们继续,接下来想象一个健身房的更衣柜管理系统:

  1. 偏向锁(Biased Locking)

    • 生活场景:每天你都固定使用同一个更衣柜。一开始,管理员并没有给任何柜子上锁,但当你首次使用时,管理员会在你的柜子上标注“专属”标签,并记录你的会员卡号(线程ID)。之后你每次来健身,只要出示相同的会员卡,管理员就知道这个柜子一直是你在用,无需每次都开锁、关锁,大大提升了效率。
    • Java中的偏向锁也是这样,当一个线程首次获取对象锁时,JVM会在对象头存储该线程ID,后续该线程再次进入同步代码块时,只需检查对象头的线程ID是否一致即可,几乎无额外开销。
  2. 轻量级锁(Lightweight Locking)

    • 生活场景:如果有一天,健身房突然来了很多新会员,其中有些人也需要使用你平时习惯的柜子。管理员这时候不能假设柜子一直是你专用,但他仍然尽量减少物理锁的使用。当别人尝试使用你的柜子时,管理员首先会让你继续使用,但会密切关注这个柜子的状态,如果发现有其他人在等待,他会要求你尽快完成并离开,以便其他人使用(自旋等待)。
    • 在Java中,当有第二个线程尝试获取已被偏向的锁时,偏向锁会升级为轻量级锁。轻量级锁不会立即阻塞线程,而是通过CAS操作尝试获取锁,若获取失败则通过自旋的方式等待锁释放,这种方式对于线程竞争不激烈的情况能有效降低上下文切换的开销。
  3. 重量级锁(Heavyweight Locking)

    • 生活场景:随着健身房越来越热门,更衣柜的竞争变得异常激烈,连续几次都有不同的人需要使用同一个柜子。管理员不得不采取严格的措施,当柜子被占用时,后面的会员必须排队等待,而且占用者结束使用时需明确通知管理员解锁。
    • 在Java中,当自旋多次尝试获取轻量级锁依然失败时,意味着竞争过于激烈,此时JVM会把锁升级为重量级锁,通过操作系统互斥量(mutex)来确保线程间的互斥,这会导致线程阻塞和唤醒,产生较大的性能开销,但能确保在高并发场景下数据的正确性。

参照上述生活中的理解,你能找出普通锁和分布式锁的实例来吗?评论区欢迎留下你的看法!

再回到开头的总结,锁的实现原理都是为了达到一个目的——让所有的线程都能看到某种标记。这个时候,你是否对这句话有了进一步的理解?

授人以鱼,不如授人以渔。

当编程时间久了之后,你会发现,编程中的世界和我们生活中的世界简直一模一样。编程中的一些设计思想和原理跟生活中是相通的,只要你愿意思考,你总能通过编程想到生活中的例子。

编程中的概念我们不能靠死记硬背,一定要理解它,从目的入手,从原理入手,一通百通,多多练习,你会发现自己进步的更快了!

希望本篇文章能对你有所帮助和启发。

  • 55
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值