全网最硬核的 synchronized 面试题深度解析

前言

有段时间没发文了,学习群里有位同学经常问(鞭策)我,催进度,说实话,让我倍感欣慰。

synchronized 说实话很早就想写了,因为在现在的面试中 synchronized 的地位基本和 HashMap 类似,本身集合和并发都是非常重要的知识体系,而 HashMap 和 synchronized 更是核心中的核心。

相比于 HashMap,synchronized 会更复杂一点,因为其主要原理都在 JVM 源码中,因此本次也花了不少时间去翻 JVM 源码,但是说实话,收获颇丰,因为有不少知识点跟当前的主流说法还是有些偏差。

正文

1、synchronized 的使用小例子?

public class SynchronizedTest {

    public static volatile int race = 0;

    private static CountDownLatch countDownLatch = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        // 循环开启2个线程来计数
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                // 每个线程累加1万次
                for (int j = 0; j < 10000; j++) {
                    race++;
                }
                countDownLatch.countDown();
            }).start();
        }
        // 等待,直到所有线程处理结束才放行
        countDownLatch.await();
        // 期望输出 2万(2*1万)
        System.out.println(race);
    }
}

熟悉的2个线程计数的例子,每个线程自增1万次,预期的结果是2万,但是实际运行结果总是一个小于等于2万的数字,为什么会这样了?

race++在我们看来可能只是1个操作,但是在底层其实是由多个操作组成的,所以在并发下会有如下的场景:

为了得到正确的结果,此时我们可以将 race++ 使用 synchronized 来修饰,如下:

synchronized (SynchronizedTest.class) {
    race++;
}

加了 synchronized 后,只有抢占到锁才能对 race 进行操作,此时的流程会变成如下:

2、synchronized 各种加锁场景?

1)作用于非静态方法,锁住的是对象实例(this),每一个对象实例有一个锁。

public synchronized void method() {}

2)作用于静态方法,锁住的是类的 Class 对象,Class 对象全局只有一份,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程。

public static synchronized void method() {}

3)作用于 Lock.class,锁住的是 Lock 的 Class 对象,也是全局只有一个。

synchronized (Lock.class) {}

4)作用于 this,锁住的是对象实例,每一个对象实例有一个锁。

synchronized (this) {}

5)作用于静态成员变量,锁住的是该静态成员变量对象,由于是静态变量,因此全局只有一个。

public static Object monitor = new Object(); 
synchronized (monitor) {}

有些同学可能会搞混,但是其实很容易记,记住以下两点:

1)必须有“对象”来充当“锁”的角色。

2)对于同一个类来说,通常只有两种对象来充当锁:实例对象、Class 对象(一个类全局只有一份)。

Class 对象:静态相关的都是属于 Class 对象,还有一种直接指定 Lock.class。

实例对象:非静态相关的都是属于实例对象。

3、为什么调用 Object 的 wait/notify/notifyAll 方法,需要加 synchronized 锁?

这个问题说难也难,说简单也简单。说简单是因为,大家应该都记得有道题目:“sleep 和 wait 的区别”,答案中非常重要的一项是:“wait会释放对象锁,sleep不会”,既然要释放锁,那必然要先获取锁。

说难是因为如果没有联想到这个题目并且没有了解的底层原理,可能就完全没头绪了。

究其原因,因为这3个方法都会操作锁对象,所以需要先获取锁对象,而加 synchronized 锁可以让我们获取到锁对象。

来看一个例子:

public class SynchronizedTest {

    private static final Object lock = new Object();

    public static void testWait() throws InterruptedException {
        lock.wait();
    }

    public static void testNotify() throws InterruptedException {
        lock.notify();
    }
}

在这个例子中,wait 会释放 lock 锁对象,notify/notifyAll 会唤醒其他正在等待获取 lock 锁对象的线程来抢占 lock 锁对象。

既然你想要操作 lock 锁对象,那必然你就得先获取 lock 锁对象。就像你想把苹果让给其他同学,那你必须先拿到苹果。

再来看一个反例:

public class SynchronizedTest {

    private static final Object lock = new Object();

    public static synchronized void getLock() throws InterruptedException {
        lock.wait();
    }
}

该方法运行后会抛出 IllegalMonitorStateException,为什么了,我们明明加了 synchronized 来获取锁对象了?

因为在 getLock 静态方法中加 synchronized 方法获取到的是 SynchronizedTest.class 的锁对象,而我们的 wait() 方法是要释放 lock 的锁对象。

这就相当于你想让给其他同学一个苹果(lock),但是你只有一个梨子(SynchronizedTest.class)。

4、synchronize 底层维护了几个列表存放被阻塞的线程?

这题是紧接着上一题的,很明显面试官想看看我是不是真的对 synchronize 底层原理有所了解。

synchronized 底层对应的 JVM 模型为 objectMonitor,使用了3个双向链表来存放被阻塞的线程:_cxq(Contention queue)、_EntryList(EntryList)、_WaitSet(WaitSet

  • 99
    点赞
  • 240
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 48
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 48
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员囧辉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值