前言
有段时间没发文了,学习群里有位同学经常问(鞭策)我,催进度,说实话,让我倍感欣慰。
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)。
当线程获取锁失败进入阻