当Synchronized遇到这玩意儿,有个大坑,要注意! (中)

简介: 当Synchronized遇到这玩意儿,有个大坑,要注意! (中)

谁动了我的锁?


经过前面一顿分析,我们坐实了锁确实发生了变化,当你分析出这一点的时候勃然大怒,拍案而起,大喊一声:是哪个瓜娃子动了我的锁?这不是坑爹吗?

image.png


image.png

抢完票之后,执行了 ticket-- 的操作,而这个 ticket 不就是你的锁对象吗?

这个时候你把大腿一拍,恍然大悟,对着围观群众说:问题不大,手抖而已。

于是大手一挥,把加锁的地方改成这样:

synchronized (TicketConsumer.class)

利用 class 对象来作为锁对象,保证了锁的唯一性。

经过验证也确实没毛病,非常完美,打完收工。

但是,真的就收工了吗?

image.png

其实关于锁对象为什么发生了变化,还隔了一点点东西没有说出来。

它就藏在字节码里面。

我们通过 javap 命令,反查字节码,可以看到这样的信息:

image.png

让人熟悉的 Integer 从 -128 到 127 的缓存。

也就是说我们的程序里面,会涉及到拆箱和装箱的过程,这个过程中会调用到 Integer.valueOf 方法。具体其实就是 ticket-- 的这个操作。

对于 Integer,当值在缓存范围内的时候,会返回同一个对象。当超过缓存范围,每次都会 new 一个新对象出来。

这应该是一个必备的八股文知识点,我在这里给你强调这个是想表达什么意思呢?

很简单,改动一下代码就明白了。

我把初始化票数从 10 修改为 200,超过缓存范围,程序运行结果是这样的:

image.png

很明显,从第一次的日志输出来看,锁都不是同一把锁了。

这就是我前面说的:因为超过缓存范围,执行了两次 new Integer(200) 的操作,这是两个不同的对象,拿来作为锁,就是两把不一样的锁。

再修改回 10,运行一次,你感受一下:

image.png

从日志输出来看,这个时候只有一把锁,所以只有一个线程抢到了票。

因为 10 是在缓存范围内的数字,所以每次是从缓存中获取出来,是同一个对象。

我写这一小段的目的是为了体现 Integer 有缓存这个知识点,大家都知道。但是当它和其他东西揉在一起的时候因为这个缓存会带来什么问题,你得分析出来,这比直接记住干瘪的知识点有效一点。

但是...

我们的初始票是 10,ticket-- 之后票变成了 9,也是在缓存范围内的呀,怎么锁就变了呢?

如果你有这个疑问的话,那么我劝你再好好想想。

10 是 10,9 是 9。

虽然它们都在缓存范围内,但是本来就是两个不同的对象,构建缓存的时候也是 new 出来的:

image.png

为什么我要补充这一段看起来很傻的说明呢?

因为我在网上看到其他写类似问题的时候,有的文章写的不清楚,会让读者误认为“缓存范围内的值都是同一个对象”,这样会误导初学者。

总之一句话:请别用 Integer 作为锁对象,你把握不住。

但是...

image.png



stackoverflow


但是,我写文章的时候在 stackoverflow 上也看到了一个类似的问题。

这个哥们的问题在于:他知道 Integer 不能做为锁对象,但是他的需求又似乎必须把 Integer 作为锁对象。

https://stackoverflow.com/questions/659915/synchronizing-on-an-integer-value


微信图片_20220429074955.png


我给你描述一下他的问题。

首先看标号为 ① 的地方,他的程序其实就是先从缓存中获取,如果缓存中没有则从数据库获取,然后在放到缓存里面去。

非常简单清晰的逻辑。

但是他考虑到并发的场景下,如果有多个线程同一时刻都来获取同一个 id,但是这个 id 对应的数据并没有在缓存里面,那么这些线程都会去执行查询数据库并维护缓存的动作。

对应查询和存储的动作,他用的是 fairly expensive 来形容。

就是“相当昂贵”的意思,说白了就是这个动作非常的“重”,最好不要重复去做。

所以只需要让某一个线程来执行这个 fairly expensive 的操作就好了。

于是他想到了标号为 ② 的地方的代码。

用 synchronized 来把 id 锁一下,不幸的是,id 是 Integer 类型的。

在标号为 ③ 的地方他自己也说了:不同的 Integer 对象,它们并不会共享锁,那么 synchronized 也没啥卵用。

其实他这句话也不严谨,经过前面的分析,我们知道在缓存范围内的 Integer 对象,它们还是会共享同一把锁的,这里说的“共享”就是竞争的意思。

但是很明显,他的 id 范围肯定比 Integer 缓存范围大。

那么问题就来了:这玩意该咋搞啊?

我看到这个问题的时候想到的第一个问题是:上面这个需求我好像也经常做啊,我是怎么做的来着?

想了几秒恍然大悟,哦,现在都是分布式应用了,我特么直接用的是 Redis 做锁呀。

根本就没有考虑过这个问题。

如果现在不让用 Redis,就是单体应用,那么怎么解决呢?

在看高赞回答之前,我们先看看这个问题下面的一个评论:

image.png

开头三个字母: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 是谁,凭什么他说的话看起来就很权威的样子?


目录
相关文章
|
6月前
|
存储 安全 Java
《吊打面试官》从根上剖析ReentrantLock的来龙去脉
《吊打面试官》从根上剖析ReentrantLock的来龙去脉
|
2月前
|
Java
死磕-java并发编程技术(二)
死磕-java并发编程技术(二)
|
2月前
|
存储 Java 调度
死磕-java并发编程技术(一)
死磕-java并发编程技术(一)
|
3月前
|
监控 安全 IDE
别再瞎用了!synchronized的正确使用姿势在这里!
别再瞎用了!synchronized的正确使用姿势在这里!
91 4
|
3月前
|
Java 开发者
解锁Java并发编程的秘密武器!揭秘AQS,让你的代码从此告别‘锁’事烦恼,多线程同步不再是梦!
【8月更文挑战第25天】AbstractQueuedSynchronizer(AQS)是Java并发包中的核心组件,作为多种同步工具类(如ReentrantLock和CountDownLatch等)的基础。AQS通过维护一个表示同步状态的`state`变量和一个FIFO线程等待队列,提供了一种高效灵活的同步机制。它支持独占式和共享式两种资源访问模式。内部使用CLH锁队列管理等待线程,当线程尝试获取已持有的锁时,会被放入队列并阻塞,直至锁被释放。AQS的巧妙设计极大地丰富了Java并发编程的能力。
44 0
|
6月前
|
消息中间件 安全 算法
通透!从头到脚讲明白线程锁
线程锁在分布式应用中是重中之重,当谈论线程锁时,通常指的是在多线程编程中使用的同步机制,它可以确保在同一时刻只有一个线程能够访问共享资源,从而避免竞争条件和数据不一致性问题。
300 0
|
存储 安全 Python
python多线程------>这个玩意很哇塞,你不来看看吗
python多线程------>这个玩意很哇塞,你不来看看吗
|
设计模式 算法 Java
|
缓存 Oracle Java
当Synchronized遇到这玩意儿,有个大坑,要注意! (下)
当Synchronized遇到这玩意儿,有个大坑,要注意! (下)
133 0
当Synchronized遇到这玩意儿,有个大坑,要注意! (下)
|
安全
当Synchronized遇到这玩意儿,有个大坑,要注意! (上)
当Synchronized遇到这玩意儿,有个大坑,要注意! (上)
200 0
当Synchronized遇到这玩意儿,有个大坑,要注意! (上)