当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 是谁,凭什么他说的话看起来就很权威的样子?


目录
相关文章
|
5月前
|
存储 安全 Java
《吊打面试官》从根上剖析ReentrantLock的来龙去脉
《吊打面试官》从根上剖析ReentrantLock的来龙去脉
|
11天前
|
消息中间件 安全 算法
通透!从头到脚讲明白线程锁
线程锁在分布式应用中是重中之重,当谈论线程锁时,通常指的是在多线程编程中使用的同步机制,它可以确保在同一时刻只有一个线程能够访问共享资源,从而避免竞争条件和数据不一致性问题。
|
2月前
|
存储 缓存 Oracle
Java线程池,白话文vs八股文,原来是这么回事!
一、线程池原理 1、白话文篇 1.1、正式员工(corePoolSize) 正式员工:这些是公司最稳定和最可靠的长期员工,他们一直在工作,不会被解雇或者辞职。他们负责处理公司的核心业务,比如生产、销售、财务等。在Java线程池中,正式员工对应于核心线程(corePoolSize),这些线程会一直存在于线程池中。他们负责执行线程池中的任务,如果没有任务,他们会等待新的任务到来。 1.2、所有员工(maximumPoolSize) 所有员工:这些是公司所有的员工,包括正式员工和外包员工。他们共同组成了公司的团队,协作完成公司的各种业务。在Java线程池中,所有员工对应于所有线程(maxim
|
10月前
|
存储 安全 Python
python多线程------>这个玩意很哇塞,你不来看看吗
python多线程------>这个玩意很哇塞,你不来看看吗
|
消息中间件 JavaScript 小程序
麻了,代码改成多线程,竟有9大问题 上
麻了,代码改成多线程,竟有9大问题 上
|
安全 Java 数据库连接
麻了,代码改成多线程,竟有9大问题 下
麻了,代码改成多线程,竟有9大问题 下
|
Java 编译器 调度
重生之我在人间敲代码_Java并发基础_原子性问题之互斥锁
原子性问题的源头是线程切换,如果能够禁用线程切换那就能解决这个问题。而操作系统做线程切换是依赖 CPU 中断的,所以禁止 CPU 发生中断就能够禁止线程切换。
|
缓存 Java 编译器
重生之我在人间敲代码_Java并发基础_Java内存模型
导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性、有序性最直接的办法就是禁用缓存和编译优化,但是这样问题虽然解决了,我们程序的性能可就堪忧了。
|
设计模式 安全 Java
重生之我在人间敲代码_Java并发基础_浅析并发编程
并发编程可以抽象为三个核心问题:分工、同步、互斥。 所谓分工指的是如何高效地拆解任务并分配给线程,而同步指的是线程之间如何协作,互斥则是保证同一时刻只允许一个线程访问共享资源。
|
缓存 Oracle Java
当Synchronized遇到这玩意儿,有个大坑,要注意! (下)
当Synchronized遇到这玩意儿,有个大坑,要注意! (下)
103 0
当Synchronized遇到这玩意儿,有个大坑,要注意! (下)