编译器永远不会重新排序同步语句,因为它对最终发生的事情有很大影响。
同步块用于获得对放置在同步括号之间的特定对象的锁定。
private final Object LOCK_1 = new Object();
public void foo(){
synchronized(LOCK_1){
//code here...
}
}
获取对象 LOCK_1 的锁,并在同步块完成时释放它。由于同步块用于防止并发访问,因此有时可能需要使用多个锁,尤其是在写入/读取多个线程不安全对象时。
考虑以下使用嵌套同步块的代码:
private final Object LOCK_1 = new Object();
private final Object LOCK_2 = new Object();
public void bar(){
synchronized(LOCK_1){
//Point A
synchronized(LOCK_2){
//Point B
}
//Point C
}
//Point D
}
如果我们查看 A、B、C、D 点,我们可以了解为什么同步顺序很重要。
首先在 A 点,获得 LOCK_1 的锁,因此任何其他试图获得 LOCK_1 的线程都被放入队列中。
在 B 点,当前执行的线程拥有 两个 LOCK_1 和 LOCK_2 的锁。
在 C 点,当前正在执行的线程已经释放 LOCK_2 的锁
在 D 点,当前正在执行的线程已经释放了所有的锁。
如果我们翻转这个例子并决定将LOCK_2放在外部块上,你会意识到线程获取锁的顺序发生了变化,这对它最终会做什么有很大的影响。通常,当我使用同步块编写程序时,我对每个正在访问的线程不安全资源使用一个 MUTEX 对象(或每组一个 MUTEX)。假设我想使用 LOCK_1 从流中读取并使用 LOCK_2 写入流。认为交换锁定顺序意味着同样的事情是不合逻辑的。
考虑LOCK_2(写锁)被另一个线程持有。如果我们在外部块上有 LOCK_1,则当前执行的线程至少可以在被放入写入锁队列之前处理所有读取代码(本质上是在 A 点执行代码的能力)。如果我们颠倒锁的顺序,当前正在执行的线程最终将不得不等待写入完成,然后继续读取和写入同时保持写入锁(一直到也读)。
在切换锁的顺序时出现的另一个问题(并非始终如一,有些代码先有 LOCK_1,而另一些代码先有 LOCK_2)。考虑两个线程都急切地尝试执行具有不同锁定顺序的代码。线程 1 在外部块中获取 LOCK_1,线程 2 从外部块中获取 LOCK_2。现在当线程 1 尝试获取 LOCK_2 时,它不能因为线程 2 拥有它。而当线程 2 尝试获取 LOCK_1 时,它也不能,因为线程 1 拥有它。两个线程本质上是永远互相阻塞,形成死锁的情况。
要回答您的问题,如果您想立即锁定两个对象而不在锁定之间进行任何类型的处理,那么顺序是无关紧要的(在 A 点或 C 点基本上没有处理)。 但是必须在整个程序中保持顺序一致以避免死锁。