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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 当Synchronized遇到这玩意儿,有个大坑,要注意! (上)

你好呀,我是歪歪。

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

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

微信图片_20220429074428.png


首先为了方便你看文章的时候复现问题,我给你一份直接拿出来就能跑的代码,希望你有时间的话也把代码拿出来跑一下:

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 张票,两个人抢,每张票只有一个人能抢到。

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

image.png

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

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

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

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

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

这玩意,超出认知了啊。

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

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

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


为什么没有生效?


我们先来看一个问题。

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

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

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

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

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

image.png

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

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

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

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

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

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

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

首先为了方便获取线程堆栈信息,我把这里的睡眠时间调整到 10s:

image.png

image.png

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

这是第一次 Dump 中的相关信息

image.png

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

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

从输出日志上来看,第一次抢票确实是 why 线程抢到了:


image.png

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

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

image.png

这一次,两个线程都在 TIMED_WAITING,都在 sleeping,说明都拿到了锁,进入了业务逻辑。

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

mx 锁的是 0x000000076c07b058。

why 锁的是 0x000000076c07b048。

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

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

image.png

如果我用“锁一”来代替 0x000000076c07b058,“锁二”来代替 0x000000076c07b048。

那么流程是这样的:

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

why 释放锁一,等待锁一的 mx 被唤醒,持有锁一,继续执行业务。

同时 why 加锁二成功,执行业务逻辑。

从线程堆栈中,我们确实证明了 synchronized 没有生效的原因是锁发生了变化。

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

image.png

第一次 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 竞争锁一,但是却不知道它在哪搞到一把新锁。

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

image.png

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
5月前
|
监控 安全 IDE
别再瞎用了!synchronized的正确使用姿势在这里!
别再瞎用了!synchronized的正确使用姿势在这里!
213 4
|
7月前
|
安全 Java 程序员
惊呆了!Java多线程里的“synchronized”竟然这么神奇!
【6月更文挑战第20天】Java的`synchronized`关键字是解决线程安全的关键,它确保同一时间只有一个线程访问同步代码。在案例中,`Counter`类的`increment`方法如果不加同步,可能会导致竞态条件。通过使用`synchronized`方法或语句块,可以防止这种情况,确保线程安全。虽然同步会带来性能影响,但它是构建并发应用的重要工具,平衡同步与性能是使用时需考虑的。了解并恰当使用`synchronized`,能有效应对多线程挑战。
23 1
|
8月前
|
Java
面试官:说一说CyclicBarrier的妙用!我:这个没用过...
【5月更文挑战第5天】面试官:说一说CyclicBarrier的妙用!我:这个没用过...
60 2
|
8月前
|
消息中间件 安全 算法
通透!从头到脚讲明白线程锁
线程锁在分布式应用中是重中之重,当谈论线程锁时,通常指的是在多线程编程中使用的同步机制,它可以确保在同一时刻只有一个线程能够访问共享资源,从而避免竞争条件和数据不一致性问题。
330 0
详解JDK锁02:万字文!结合实战案例,手撕AQS源码!
详解JDK锁02:万字文!结合实战案例,手撕AQS源码!
115 0
面试又被问懵了吗?不如把ThreadLocal拆开了揉碎看看
1.为什么用 ThreadLocal? 所谓并发,就是有限资源需要应对远超资源的访问。解决问题的方法,要么增加资源应对访问;要么增加资源的利用率。 所以,相信这年头做开发的多多少少,都会那么几个“线程二三招”、“用锁五六式”。 那所带来的就是多线程访问下的并发安全问题。 共享变量的访问域跨越了原始的单线程,进入了千家万户的线程眼里。谁都可以用,谁都可以改,那不就打起来了吗? 因此,防止并发问题的最好办法,就是不要多线程访问(这科技水平倒退二十年~)。ThreadLocal 顾名思义,将一个变量限制为“线程封闭”:对象只被一个线程持有、访问、修改。
|
设计模式 算法 Java
|
算法 安全 Java
threadlocal再温习
早时总结过《ThreadLocal解析》、《FastThreadLocal解析》 最近看一些资料的时候,又重重发现了这类,不希望再温下,许多知识点,之前已经总结了,这篇文章主要有两个问题: 1、弱引用的意义 2、如何防键冲突
251 0
threadlocal再温习
|
存储 缓存 NoSQL
当Synchronized遇到这玩意儿,有个大坑,要注意! (中)
当Synchronized遇到这玩意儿,有个大坑,要注意! (中)
172 0
当Synchronized遇到这玩意儿,有个大坑,要注意! (中)
|
缓存 Oracle Java
当Synchronized遇到这玩意儿,有个大坑,要注意! (下)
当Synchronized遇到这玩意儿,有个大坑,要注意! (下)
146 0
当Synchronized遇到这玩意儿,有个大坑,要注意! (下)