一网打尽MySQL各种锁
来源:一网打尽MySQL各种锁
简历上写的‘精通Java并发’是吧?行,那咱们聊聊。synchronized和ReentrantLock哪个性能好?为什么?synchronized是公平锁还是非公平锁?Java里怎么实现乐观锁和悲观锁?CAS原理是啥?ABA问题又是什么?怎么解决?CountDownLatch和CyclicBarrier有啥区别?最后,synchronized底层那个锁升级过程,偏向锁、轻量级锁、重量级锁,它们仨是为了解决啥问题?又是怎么一步步转换的?来,说说你的理解。
怎么样?是不是感觉灵魂都快被问出窍了,当场就想打开招聘软件更新一下简历?哈哈,别紧张!
这些玩意儿,说白了就是Java世界里的一帮‘保安’,专门负责维持多线程抢东西时候的秩序。今天,咱不搞那些虚头巴脑的定义,就用大白话,把这帮‘保安’的脾气、性格、还有他们内部的“晋升机制”,给你扒个底朝天!

记得点赞收藏,不然想用的时候就找不到了。跟上我的节奏,咱们一个一个盘!
首先,咱们得搞明白两种最基本的‘世界观’:悲观锁和乐观锁。

这一组锁是根据对并发冲突的假设不同进行区分的。要搞清楚这两个锁,你就记住一个场景,上厕所就够了。
悲观锁这哥们儿,纯纯一个‘被迫害妄想症’患者。它的座右铭是:‘总有刁民想害朕!’。所以它在干任何事之前,都必须先把门锁死。比如去上厕所,它不仅要锁门,还得在门外挂个‘正在使用,请勿打扰’的牌子。Java里咱们最熟悉的synchronized和ReentrantLock就是这种性格,只要一个线程抢到了,别的线程就只能在外面憋着。这种锁,用在像秒杀减库存这种‘兵家必争之地’就特别合适,虽然有点笨重,但胜在绝对安全。
那乐观锁呢?正好相反,是个‘傻白甜’,心特别大。它觉得世界如此美好,大家都会谦让。它去上厕所,门都不锁,直接就进去了。等办完事出来的时候,它会检查一下:‘哎,我进来时放这儿的纸巾,还是原来那个牌子吗?’。如果还是,说明没人来过,万事大吉。如果被人换了,说明中间有人来过,那它这次就算白上了,得重新再来一次。这个‘检查并替换’的动作,在Java里就叫CAS(Compare-And-Swap)。咱们常用的AtomicInteger这些原子类,就是靠这手绝活。它特别适合更新帖子阅读数这种竞争不激烈的场景,性能起飞。不过它有个坑,就是经典的ABA问题。啥意思呢?就是你女朋友跟你分手了,她又找了个新男友,然后又分手了,最后又回来找你复合。你一看,哎,还是那个她,但中间发生过啥,你还真不知道。想解决也简单,加个版本号就行,AtomicStampedReference就是干这个的。
好,搞懂了这两种哲学,咱们再来聊聊下一组:公平锁和非公平锁

这一组锁就是根据线程排队的规矩来区分的。公平锁就像银行叫排号,讲究一个先来后到。非公平锁就像路边招手打车,谁快谁上。
详细来说,公平锁,就跟去银行办业务一样,老老实实取号,谁先来谁先办,讲究一个‘武德’。好处是童叟无欺,每个线程都有出头之日。但是坏处也很明显,一老太太办理业务又慢又麻烦,其他人也只能在他之后乖乖等着。这队伍不长才怪。常用的ReentrantLock可以添加一个布尔型的参数,True就表示是采用公平锁。
非公平锁呢,就‘不讲武德’了。就像路边打车,才不管其他人排不排队,谁动作快,抢到了车,谁就上。这样做的好处非常明显,相比于公平锁,省去了叫号、线程唤醒的麻烦,整个系统的吞吐量一下子就上去了!而锁的最大敌人,就是吞吐量。所以ReentrantLock默认就是这种‘不讲武德’的非公平锁,而老大哥synchronized更是个彻头彻尾的‘非公平主义者’。所以记住,除非你的业务有严格的先后顺序要求,否则,就用默认的非公平锁,因为它更快!
接下来,就轮到下一组容易让人看不懂的锁:排他锁和共享锁。”

他们的核心区别,就是是否允许有线程并发。
排他锁,顾名思义,就是‘排斥他人’,非常霸道。它就像KTV里的豪华包间,你一个人进去了,不管你是想在里面唱歌、跳舞还是睡觉,反正门一锁,谁也别想再进来。在系统当中这就是独占锁,一个线程把资源占住了,其他读、写操作全都被排斥,都只能乖乖等着。synchronized和ReentrantLock都是这种锁。
但有时候我们不需要这么霸道。比如大家一起看报纸,这就是一种不影响报纸内容的读操作,那来多少人一起看都无所谓。这就是共享锁,也叫读锁。一个线程占住了读锁,其他线程也可以同样拿到读锁,这就是读读并发。但如果这时候,有个人想在报纸上涂鸦,这就成了改变报纸内容的写操作,那对不起,他必须等所有看书的人都走了才行,而且他涂鸦的时候,谁都不能看。
Java里的要实现共享锁很容易,可以用ReentrantReadWriteLock这么一个强大的工具,它里面就同时维护着一个读锁(共享)和一个写锁(排他)。在读多写少的场景下,更多使用读锁能让并发性能原地起飞,这对于性能调优非常重要!
接下来还有一个面试中经常会问的锁:可重入锁。表示同一个线程是否可以重复持有自己的锁。

这个非常重要,因为Java中天天用的Reentrant,其实就是可重入的意思。啥叫可重入?就是你拿着你家钥匙打开大门进屋了,然后发现你的卧室门也需要同一把钥匙开。这时候你不需要再跑出去开门,可以直接用手里的钥匙去开卧室门。一个线程拿到了锁之后,可以反复地进入这个锁保护的代码块,而不会被自己锁住。synchronized和ReentrantLock都是可重入的,它们内部有个计数器,你每进一层就+1,出层就-1,直到计数器变成0,才算真正把锁还回去了。要是锁是不可重入的,那你进了大门,想再开卧室门的时候,发现钥匙已经被‘大门锁’占用了,结果自己把自己锁死在客厅,这就搞笑了。
最后,就是面试的王炸,synchronized内部那套骚气的锁升级机制。

JVM为了让synchronized这个‘亲儿子’跑得更快,给它设计了一套从低到高的‘保安’等级,根据资源的竞争激烈程度不同,实现了强大的自动升级功能。还是以上厕所的故事为例。
- “偏向锁:一开始,这个厕所就你一个人用,你就是VIP。于是你在门上贴了个条‘老王专用’。以后你每次来,一看是你的专属坑位,连门都不用锁,直接进,开销几乎为零。这就是‘偏向’你。”
- “轻量级锁:好景不长,有一天你正在里面,外面来了个老李。老李一看门关着,但他觉得你可能马上就出来,于是他没有去排队叫号,而是在门口来回踱步、玩手机,嘴里还念叨着‘快点啊快点啊’。这个‘踱步玩手机’的动作,就叫‘自旋’。如果他踱步这会儿你正好出来了,他就能立刻进去,避免了去服务台排大队的麻烦。这就是轻量级锁,用自旋代替阻塞,提升效率。”
- “重量级锁:结果你今天吃坏了肚子,半天没出来。门口除了老李,又来了老张、老赵、老孙……一堆人都在门口‘自旋’,场面一度非常混乱。这时候,厕所管理员看不下去了,拿着大喇叭和花名册就来了:‘都别挤了!都到我这儿登记!我叫到谁的名字谁再进去!’。于是,所有人都被安排得明明白白,进入了正式的等待队列。这就是重量级锁。这里的管理员就是操作系统。为什么叫重量呢?因为这种方式虽然能管住所有人,但需要管理员频繁的亲自出动整理队伍,对于操作系统就增加了线程上下文切换的性能消耗,很重,很消耗性能。”
所以,synchronized就是这么个能屈能伸的家伙,从偏向锁到轻量级锁再到重量级锁,它会根据竞争的激烈程度,自动选择最合适的‘安保级别’。”而且Java为了提升synchronized的性能,其实自己也在不断优化。从JDK17开始,就默认取消了偏向锁,锁升级更顺滑。
(结尾 - 升华)
“好了,聊到这儿,咱们收个尾。”

“其实,搞懂Java这些锁,真正拉开差距的,不是让你去背诵“锁的八股文”,那只是初级程序员的“肌肉记忆”。”
“真正的高手,是能在错综复杂的场景里,懂得如何去比较、衡量、与取舍。他们能告诉你,在这个场景下,为什么synchronized比ReentrantLock更合适;在下一个并发场景,又为什么果断要用ReadWriteLock。”
“他们知道,工程的世界里没有最好的锁,只有最合适的场景。”
这其实跟人生一样,从来不存在最好的工作,只有最合适的职业。
“现在,你悟了吗?”