你好呀,我是歪歪。
前几天在某技术平台上看到别人提的关于 Synchronized 的一个用法问题,我觉得挺有意思的,这个问题其实也是我三年前面试某公司的时候遇到的一个真题,当时不知道面试官想要考什么,没有回答的特别好,后来研究了一下就记住了。
所以看到这个问题的时候觉得特别亲切,准备分享给你一起看看:
首先为了方便你看文章的时候复现问题,我给你一份直接拿出来就能跑的代码,希望你有时间的话也把代码拿出来跑一下:
public class SynchronizedTest { public static void main(String[] args) { Thread why = new Thread(new TicketConsumer(10), "why"); Thread mx = new Thread(new TicketConsumer(10), "mx"); why.start(); mx.start(); } } class TicketConsumer implements Runnable { private volatile static Integer ticket; public TicketConsumer(int ticket) { this.ticket = ticket; } @Override public void run() { 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 (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket-- + "张票,票数减一"); } else { return; } } } } }
程序逻辑也很简单,是一个模拟抢票的过程,一共 10 张票,开启两个线程去抢票。
票是共享资源,且有两个线程来消费,所以为了保证线程安全,TicketConsumer 的逻辑里面用了 synchronized 关键字。
这是应该是大家在初学 synchronized 的时候都会写到的例子,期望的结果是 10 张票,两个人抢,每张票只有一个人能抢到。
但是实际运行结果是这样的,我只截取开始部分的日志:
截图里面有三个框起来的部分。
最上面的部分,就是两个人都在抢第 10 张票,从日志输出上看也完全没有任何毛病,最终只有一个人抢到了票,然后进入到第 9 张票的争夺过程。
但是下面被框起来的第 9 张票的争夺部分就有点让人懵逼了:
why抢到第9张票,成功锁到的对象:288246497 mx抢到第9张票,成功锁到的对象:288246497
为什么两个人都抢到了第 9 张票,且成功锁到的对象都一样的?
这玩意,超出认知了啊。
这两个线程怎么可能拿到同一把锁,然后去执行业务逻辑呢?
所以,提问者的问题就浮现出来了。
- 1.为什么 synchronized 没有生效?
- 2.为什么锁对象 System.identityHashCode 的输出是一样的?
为什么没有生效?
我们先来看一个问题。
首先,我们从日志的输出上已经非常明确的知道,synchronized 在第二轮抢第 9 张票的时候失效了。
经过理论知识支撑,我们知道 synchronized 失效,肯定是锁出问题了。
如果只有一把锁,多个线程来竞争同一把锁,synchronized 绝对是不会有任何毛病的。
但是这里两个线程并没有达成互斥的条件,也就是说这里绝对存在的不止一把锁。
这是我们可以通过理论知识推导出来的结论。
先得出结论了,那么我怎么去证明“锁不止一把”呢?
能进入 synchronized 说明肯定获得了锁,所以我只要看各个线程持有的锁是什么就知道了。
那么怎么去看线程持有什么锁呢?
jstack 命令,打印线程堆栈功能,了解一下?
这些信息都藏在线程堆栈里面,我们拿出来一看便知。
在 idea 里面怎么拿到线程堆栈呢?
这就是一个在 idea 里面调试的小技巧了,我之前的文章里面应该也出现过多次。
首先为了方便获取线程堆栈信息,我把这里的睡眠时间调整到 10s:
复制下来的信息很多,但是我们只需要关心 why 和 mx 这两个线程即可。
这是第一次 Dump 中的相关信息
mx 线程是 BLOCKED 状态,它在等待地址为 0x000000076c07b058 的锁。
why 线程是 TIMED_WAITING 状态,它在 sleeping,说明它抢到了锁,在执行业务逻辑。而它抢到的锁,你说巧不巧,正是 mx 线程等待的 0x000000076c07b058。
从输出日志上来看,第一次抢票确实是 why 线程抢到了:
从 Dump 信息看,两个线程竞争的是同一把锁,所以第一次没毛病。
好,我们接着看第二次的 Dump 信息:
这一次,两个线程都在 TIMED_WAITING,都在 sleeping,说明都拿到了锁,进入了业务逻辑。
但是仔细一看,两个线程拿的锁是不相同的锁。
mx 锁的是 0x000000076c07b058。
why 锁的是 0x000000076c07b048。
由于不是同一把锁,所以并不存在竞争关系,因此都可以进入 synchronized 执行业务逻辑,所以两个线程都在 sleeping,也没毛病。
然后,我再把两次 Dump 的信息放在一起给你看一下,这样就更直观了:
如果我用“锁一”来代替 0x000000076c07b058,“锁二”来代替 0x000000076c07b048。
那么流程是这样的:
why 加锁一成功,执行业务逻辑,mx 进入锁一等待状态。
why 释放锁一,等待锁一的 mx 被唤醒,持有锁一,继续执行业务。
同时 why 加锁二成功,执行业务逻辑。
从线程堆栈中,我们确实证明了 synchronized 没有生效的原因是锁发生了变化。
同时,从线程堆栈中我们也能看出来为什么锁对象 System.identityHashCode 的输出是一样的。
第一次 Dump 的时候,ticket 都是 10,其中 mx 没有抢到锁,被 synchronized 锁住。
why 线程执行了 ticket--
操作,ticket 变成了 9,但是此时 mx 线程被锁住的 monitor 还是 ticket=10 这个对象,它还在 monitor 的 _EntryList 里面等着的,并不会因为 ticket 的变化而变化。
所以,当 why 线程释放锁之后,mx 线程拿到锁继续执行,发现 ticket=9。
而 why 也搞到一把新锁,也可以进入 synchronized 的逻辑,也发现 ticket=9。
好家伙,ticket 都是 9, System.identityHashCode 能不一样吗?
按理来说,why 释放锁一后应该继续和 mx 竞争锁一,但是却不知道它在哪搞到一把新锁。
那么问题就来了:锁为什么发生了变化呢?