你好呀,我是歪歪。

前几天在某技术平台上看到别人提的关于 Synchronized 的一个用法问题,我觉得挺有意思的,这个问题其实也是我三年前面试某公司的时候遇到的一个真题,当时不知道面试官想要考什么,没有回答的特别好,后来研究了一下就记住了。

所以看到这个问题的时候觉得特别亲切,准备分享给你一起看看:

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

首先为安全中心了方便你看文章的时候复现问题,我给你一份直接拿出来就java培训能跑的代码,希望你有时间的话也把代码拿出来跑一字节码和机器码的区别下:

publicclassSynchronizedTest{
publicstaticvoidmain(String[]args){
Threadwhy=newThread(newTicketConsumer(10),"why");
Threadmx=newThread(newTicketConsumer(10),"mx");
why.start();
mx.start();
}
}
classTicketConsumerimplementsRunnable{
privatevolatilestaticIntegerticket;
publicTicketConsumer(intticket){
this.ticket=ticket;
}
@Override
publicvoidrun(){
while(true){
System.out.println(Thread.currentThread().getName()+"开始抢第"+ticket+"张票,对象加锁之前:"+System.identityHashCode(ticket));
synchronized(ticket){
System.out.println(Thread.currentThread().getName()+"抢到第"+ticket+"张票,成功锁到的对象:"+System.identityHashCode(ticket));
if(ticket>0){
try{
//模拟抢票延迟
TimeUnit.SECONDS.sleep(1);
}catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了第"+ticket--+"张票,票数减一");
}else{
return;
}
}
}
}
}

程序逻辑也很简单,是一个模拟抢线程票的过程,一共 10 张票,开启两个线程去抢票。

票是共享资源,且有两个线程来消费,所以为了安全保证线程安全,TicketConsumer 的逻辑里面用字节码文件了 synchronized 关键字。

这是应该是大家在初学 synchronized 的时候都会写到的例子,期望的结果是 10 张票,线程安全两个人安全抢,每张票只有一个人能抢到。

但是实际运行结果是这样的,我只截取开始部分的日志:

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

截图里面有三个框起来的部分。

最上面的线程的概念部分,就是两个人都在抢第 10 张票,从日志输出上看也完全没有任何毛病,最安全终只有一个人抢到了票,然后进入到第 9 张票安全教育平台的争夺过程。

但是下面被框起来的第 9 张票的争夺部分就有点让人懵逼了:

why抢到第9张票,成功锁到的对象:288246497
mx抢到第9张票,成功锁到的对象:288246497

为什么两个人都抢到了第 9 张票,且成功锁到的对象都一样的?

这玩意,超出认知了啊。

这两个线程怎么可能安全拿到同一把锁,然后去执行业务逻辑呢?

所以,提问者的问题就浮现出来了。java面试题

  • 1.为什么 synchronized 没有生效?
  • 2.为什么锁对象 System.identityHashCode 的输出javascript是一样的?

为什么没有生效?

我们先来看一个问题。

首先,我们从日志的输出上已经非常明确的知道,synchronized 在第二轮抢第 9 张票的时候失效了。

经过理论知识支撑,我们知道 synchronized 失效,肯redis数据结构定是锁出问题了。

如果只有一把锁,多个线程来竞争同一把锁,synchronized 绝对是不会有任何毛病的。

但是这里两个线程并没有达成互斥安全工程师的条件,也就是说这里绝对存在的不止一把锁。

这是我们可以通过理论知识推导出来的结论。

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

先得出结论了,那么我怎么去证明“锁不止一把”呢?

能进入 synchronized 说明肯定获得了锁,所以我只要看线程和进程的区别是什么各个线程持有的锁是什么就知道了。

那么怎么去看线程持有什么锁呢?

jstack 命安全工程师令,打印线程堆栈功能,了解一下?

这些信息都藏在线程堆栈里面,我们拿出来一看便知。

在 idea 里面怎么拿到线程堆栈呢?

这就是一个在 idea 里面调安全生产法试的小技巧了,我之前的文章里面应该也出现过多次。

首先为了方便获取线程堆栈信息字节码文件,我把这里的睡线程是什么意思眠时间调整到 10s:

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

跑起来之后点击这里的“照相机”图标:

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

点击几次就会有对应点击时间点的几个 Dump 信息

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

由于我需要观察前两线程安全次锁的情况,安全生产法而每次线程进入锁之后都会等待 10s 时间,所以我就在项目启动的第一个 10s 和第二个 10s 之间各点击一次就行。

为了更rediscover直观的观察数据,我选择点击下面这个图标,安全把 Dump 信息复制下来:

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

复制下来的信息很多,但是我们只需要关心 why 和 mx 这两个线程即可java模拟器

这是第一次 Dumjava环境变量配置p 中的相关信息:

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

mx 线程java培训是 BLOCKED 状态,它在等待地址为 0x000000076c07b058 的锁。

why 线程是 TIMED_WAITING 状java语言态,它在 sleeping,说明它抢到了锁,在执行业务逻辑。而它抢到的锁,你说巧不巧,正是 mx 线程等待的 0x000000076c07b058。

从输安全中心出日java培训志上来看,第一次抢票确实是 why 线程抢到了:

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

从 Dump 信息看,两个线程竞争的是同一把锁,所以第一次没毛病。

好,我们接着看第二次的 Dump 信息:

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

这一次,两个线程都在 TI安全教育MED_WAITING,都在 sleeping,说明都拿到了锁,进入了业务逻辑。

但是仔细一看,两个线程拿的锁是不相同的锁。

mx 锁的是 0x000000076c07b058。

why 锁的是 0x000000076c07b048。

由于不是同一把锁,所以并不存在竞争关系,因此都可以进入 synchredis面试题ronized 执行业务逻辑,所redis命令以两个线程都在 sleeping,也没毛病。

然后,线程数我再把两次 Dump 的信息放java模拟器在一起给你看一下,这样就更直观了:

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

如果我用“锁一”来代替 0x000000076c07b058,“锁二”来代替 0x线程的几种状态000000076c07b048。

那么流程是这样的:

why 加锁一成功,执行业务逻辑,mx 进入锁一等待状态。

why 释放锁一,等安全教育平台待锁一的 mx 被唤醒,持有锁一,继续执行业务。

同时线程的概念 why 加锁二成功,执行业务逻辑。

从线程堆栈中,我们确实证明了 synchroredis的五种数据类型nized 没有生效的原redistribute因是锁发生了变化。

同时,从线程redis分布式锁堆栈中我们也能看出来为什么锁对象 System.identityHashCode 的输出是一样的。

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

第一次 Dump 的时候,ticket 都是 10,其中 mx 没有抢安全到锁,被 synchronize线程的概念d 锁住。

why 线程执rediscover行了 ticket-- 操作,ticket 变成了 9,但是此时 mx 线程被锁住的 monitor 还是 ticket=10 这个对象,它还在 monitor 的 _EntryList 里面等着的,并不会因为 ticket 的变化而变化。

所以,当 why 线程释放锁之后,mx 线程拿到锁继续执行,发现 ticketredis面试题=9。

而 why 也搞到一把新锁,也可以进入 synchronized 的逻辑,也发现安全生产法 ticket=9。

好家伙,ticket 都是 9, System.identityHashC安全生产法ode 能不一样吗?

按理来说,why 释放锁一后应该继续和 mx 竞争锁一,但是却不知道它在哪搞到一把新锁。

那么问题就来了:锁为什么发生了变化呢?

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

谁动了我的锁?

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

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

按照我的经验,这个时候不要急着甩锅,继续往下看,你会发现小丑竟是自己:

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

抢完票之后,执行了 ticket-- 的操作线程是什么意思,而这个 ticket 不就是你的锁对象字节码文件吗?

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

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

synchronized(TicketConsumer.class)

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

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

但是,真的就收工了吗?

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

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

它就藏在字节码里面。

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

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

Integer.valueOf 这是什么玩意?

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

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

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

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

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

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

我把初始化票数从 10 修改为 2redis分布式锁00,超过缓存范围,程序运行结果是这样的:

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

很明显,从第一次的日志输出来看,锁都字节码文件的扩展名是什么不是同一把锁了。

这就是我前面说的:因为超过缓存范围,执行了两次 new Integer(200) 的操作,这java语言是两个不同的对象,拿来作为锁,就字节码文件的扩展名是什么是两把不一样的锁java模拟器

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

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

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

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

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

但是…

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

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

10 是 10,9 是 9。

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

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

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

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

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

但是…

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

stackoverflow

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

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

stackoverflow.com/questions/6…

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

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

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

非常简单清晰javaee的逻辑。

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

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

就是“相当昂贵”的意思,说白了就是安全教育这个动作非常的“重”,最好不要重复去做字节码

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

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

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

在标号为 ③ 的地方线程和进程的区别是什么他自己也说了:不同java怎么读的 Integer 对象,它们并不会共享锁,那么redistribute synchronize线程撕裂者d 也没啥卵用。

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

但是很明显,他的 id 范围肯定redis数据结构比 Integer 缓存范围大。

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

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

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

根本就没安全教育平台有考虑过这个问题。

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

在看高赞回答之前,我们先看看这安全教育平台登录个问题下面的一个评论:

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

开头三个字母:安全FYI。

看不懂没关系,因为这个不是重点。

但是你知道的,我的英语水平 very high,所以我也顺便教点英文。javascript

FYI,是一个常用的英文缩字节码文件是什么意思写,全称是 for your information,供参考的字节码文件意思。

所以你就知道,他后面肯定是给你附上一个资料了,翻译过来就是: Brian Goetz 在他的 Devoxx 2018 演讲中提到,我们不应该把 Integer 作为锁。

你可以通过这个链接直达这一部分的讲解,只有不到 30s秒的时间,随便练练听力:www.yjavascript百炼成仙outube.com/watch?v=4r2…

那么安全问题Redis又来了?

Brian Goetz 是谁,凭什么他说的话看起来就很权威的样子?

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

Java Language Architect at Oracle,开发 Java 语言的,就问java环境变量配置你怕不怕。

同时,他还是我多次推荐过的《Java并发编程redis持久化实践》这本书的作者。

好了,现在也找到大佬背书了,接下来带你看看高赞回答是怎么说的。

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

前部分就不详说了,其实就是线程是什么意思我们前面提到的那一些点,不能用 Integer ,涉及到缓存内、缓存外巴拉巴拉字节码文件的…

关注划线的部分,我安全加上自己的理解给你翻译一线程撕裂者下:

如果你真的必须用 Integer 作为锁,那么你需要搞一个 Map 或安全教育 Integer 的 Set,通过集合类做映射,你就可以保证映射出安全教育平台登录来的是你想要的明确的字节码和机器码的区别一个实例。而这个实例,就那可以拿来做锁。

然后他给出了这样的代码片段:

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

就是用 ConcurrentHashMap 然后用 putIfAbsent 方法来做一个映射。

比如多次调用 locks.putIfAbsent安全(200, 200),在 map 里面也只有一个值为 200 的 Integer 对象,这是 map 的特redis数据库性保证的,无线程的几种状态需过多解释。

但是这个哥们很好,为了防止有人转不java语言过这java怎么读个弯,他又给大家解释了一下。

首先,他说你也可以这样的写:

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

但这样一来,你就会多产生一个很小成本,就是每次访问的时候,如果这个值redis集群三种方式没有被安全模式怎么解除映射,你都会创建一个 Object 对象。

为了避免这一点,他只是把整数本身保存在 Map 中。这样做的目的是什么?这与直接使用整数本身有什么不同呢?

他是这样解释的线程的概念,其实就是我前面说的“这是 map 的redis面试题特性保证的安全中心”:

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

当你从 Map 中执行 get() 时,会用到 equals() 方法比较安全中心键值。

两个相同值的不同 In线程teger 实例,调用 equals() 方法是会判定为相同的 。

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

因此,你可以传递任何数量的 “new Integer(5)” 的不同 Integer 实例作为 getCacheSyncObject 的参数,但是你将永远只能得到传递进来的包含该值的第一个实例。

就是这个意思:

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

汇总一句话:就是通redis分布式锁过 Map 做了映射,不管你 new 多少个 Integer 出来,这多个 Injavascript百炼成仙teger 都会被映射为同一个 Integer,从而保证即使超出 Integer 缓存范围时,也只有一把锁。

除了高赞回答之外,还安全有两个回答我也想说一下。

安全工程师一个是这个:

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

不用关心他说的内容是什么,只是我看到这句话翻译的时候虎躯一震:

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

skin this cat ???

太残忍了吧线程安全

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

我当时就觉得这个翻译肯定不太对,这肯定是一个小俚语。于是考证了一下,原来是这个意思:

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

免费送你一个英语小java编译器知识,不用客气。

第二个应该关注的回答排在最后:

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

这个哥们叫你看看《Java并发编程实战》的第 5.6 节的内容,里面有你要寻找的答案。

巧了,我线程的概念手边就有这本书,于是我翻开看了一眼。

第 5.线程的概念6 节的名称叫做“构建高效且可伸缩的结果缓存”:

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

好家伙,我仔细一看这一java环境变量配置节,发现这是宝贝呀。

你看书里面的示例redis集群三种方式代码:

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

不就和提问题的这个哥们的代码如出一辙吗?

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

都是从缓存中获取,拿不到再去构建。

不同的地方在于书上把 synchronize 加在了方法上。但是书上也说了,这是最差的解决方案安全教育平台登录,只是为了引出问题。

随后他借助了 ConcurrentHashMap、putIfAbsent 和 FutureTask 给出了一个相对较好的解决方案。

你可以看到完全是从另外一个角度去解决问题的,根本就没有在 synchro安全中心nize 上纠缠,直接第二个方法就拿掉了 synchronize。

看完书上的方案后我才恍然大悟:好家伙,虽然前面给出的方案可以解决这个问题,但是总感觉怪怪的,又说不出来哪里怪。原来是死盯java模拟器着 synchronize 不放,思路一开始就没打开啊。

书里面一共给出了四段代码,解决方案层层递进,具体是怎么写的,由于书上已经写的很清楚了,我就不赘述了,大家去翻翻书就行了。线程数

没有书的直接在网上搜“构建java怎么读高效且线程的概念可伸缩线程是什么意思的结果缓存”也能搜出原文。

我就java怎么读指个安全期计算器路,看去吧。