谁动了我的锁?
经过前面一顿分析,我们坐实了锁确实发生了变化,当你分析出这一点的时候勃然大怒,拍案而起,大喊一声:是哪个瓜娃子动了我的锁?这不是坑爹吗?
抢完票之后,执行了 ticket--
的操作,而这个 ticket 不就是你的锁对象吗?
这个时候你把大腿一拍,恍然大悟,对着围观群众说:问题不大,手抖而已。
于是大手一挥,把加锁的地方改成这样:
synchronized (TicketConsumer.class)
利用 class 对象来作为锁对象,保证了锁的唯一性。
经过验证也确实没毛病,非常完美,打完收工。
但是,真的就收工了吗?
其实关于锁对象为什么发生了变化,还隔了一点点东西没有说出来。
它就藏在字节码里面。
我们通过 javap 命令,反查字节码,可以看到这样的信息:
让人熟悉的 Integer 从 -128 到 127 的缓存。
也就是说我们的程序里面,会涉及到拆箱和装箱的过程,这个过程中会调用到 Integer.valueOf
方法。具体其实就是 ticket--
的这个操作。
对于 Integer,当值在缓存范围内的时候,会返回同一个对象。当超过缓存范围,每次都会 new 一个新对象出来。
这应该是一个必备的八股文知识点,我在这里给你强调这个是想表达什么意思呢?
很简单,改动一下代码就明白了。
我把初始化票数从 10 修改为 200,超过缓存范围,程序运行结果是这样的:
很明显,从第一次的日志输出来看,锁都不是同一把锁了。
这就是我前面说的:因为超过缓存范围,执行了两次 new Integer(200) 的操作,这是两个不同的对象,拿来作为锁,就是两把不一样的锁。
再修改回 10,运行一次,你感受一下:
从日志输出来看,这个时候只有一把锁,所以只有一个线程抢到了票。
因为 10 是在缓存范围内的数字,所以每次是从缓存中获取出来,是同一个对象。
我写这一小段的目的是为了体现 Integer 有缓存这个知识点,大家都知道。但是当它和其他东西揉在一起的时候因为这个缓存会带来什么问题,你得分析出来,这比直接记住干瘪的知识点有效一点。
但是...
我们的初始票是 10,ticket--
之后票变成了 9,也是在缓存范围内的呀,怎么锁就变了呢?
如果你有这个疑问的话,那么我劝你再好好想想。
10 是 10,9 是 9。
虽然它们都在缓存范围内,但是本来就是两个不同的对象,构建缓存的时候也是 new 出来的:
为什么我要补充这一段看起来很傻的说明呢?
因为我在网上看到其他写类似问题的时候,有的文章写的不清楚,会让读者误认为“缓存范围内的值都是同一个对象”,这样会误导初学者。
总之一句话:请别用 Integer 作为锁对象,你把握不住。
但是...
stackoverflow
但是,我写文章的时候在 stackoverflow 上也看到了一个类似的问题。
这个哥们的问题在于:他知道 Integer 不能做为锁对象,但是他的需求又似乎必须把 Integer 作为锁对象。
https://stackoverflow.com/questions/659915/synchronizing-on-an-integer-value
我给你描述一下他的问题。
首先看标号为 ① 的地方,他的程序其实就是先从缓存中获取,如果缓存中没有则从数据库获取,然后在放到缓存里面去。
非常简单清晰的逻辑。
但是他考虑到并发的场景下,如果有多个线程同一时刻都来获取同一个 id,但是这个 id 对应的数据并没有在缓存里面,那么这些线程都会去执行查询数据库并维护缓存的动作。
对应查询和存储的动作,他用的是 fairly expensive 来形容。
就是“相当昂贵”的意思,说白了就是这个动作非常的“重”,最好不要重复去做。
所以只需要让某一个线程来执行这个 fairly expensive 的操作就好了。
于是他想到了标号为 ② 的地方的代码。
用 synchronized 来把 id 锁一下,不幸的是,id 是 Integer 类型的。
在标号为 ③ 的地方他自己也说了:不同的 Integer 对象,它们并不会共享锁,那么 synchronized 也没啥卵用。
其实他这句话也不严谨,经过前面的分析,我们知道在缓存范围内的 Integer 对象,它们还是会共享同一把锁的,这里说的“共享”就是竞争的意思。
但是很明显,他的 id 范围肯定比 Integer 缓存范围大。
那么问题就来了:这玩意该咋搞啊?
我看到这个问题的时候想到的第一个问题是:上面这个需求我好像也经常做啊,我是怎么做的来着?
想了几秒恍然大悟,哦,现在都是分布式应用了,我特么直接用的是 Redis 做锁呀。
根本就没有考虑过这个问题。
如果现在不让用 Redis,就是单体应用,那么怎么解决呢?
在看高赞回答之前,我们先看看这个问题下面的一个评论:
开头三个字母:FYI。
看不懂没关系,因为这个不是重点。
但是你知道的,我的英语水平 very high,所以我也顺便教点英文。
FYI,是一个常用的英文缩写,全称是 for your information,供参考的意思。
所以你就知道,他后面肯定是给你附上一个资料了,翻译过来就是: Brian Goetz 在他的 Devoxx 2018 演讲中提到,我们不应该把 Integer 作为锁。
你可以通过这个链接直达这一部分的讲解,只有不到 30s秒的时间,随便练练听力:https://www.youtube.com/watch?v=4r2Wg-TY7gU&t=3289s
那么问题又来了?
Brian Goetz 是谁,凭什么他说的话看起来就很权威的样子?