不可变对象的同步(在java中)
代码段 – 1
class RequestObject implements Runnable { private static Integer nRequests = 0; @Override public void run() { synchronized (nRequests) { nRequests++; } } }
代码段 – 2
public class Racer implements Runnable { public static Boolean won = false; @Override public void run() { synchronized (won) { if (!won) won = true; } } }
我在第一个代码片段中遇到了竞争条件。 我明白这是因为我在一个不可变对象(Integer类型)上获得了一个锁。
我已经编写了第二个代码片段,它再次不受“布尔”不可变的影响。 但是这样可行(输出运行中不显示竞争条件)。 如果我已正确理解上一个问题的解决方案,则下面是一种可能出错的方法
- 线程1获取对
won
的对象(比如A)的锁定 - 线程2现在试图锁定
won
指向的对象并进入A的等待队列 - 线程1进入同步块,validationA是否为假并通过说
won = true
(A认为它赢得了比赛)创建了一个新对象(比如说B)。 - ‘won’现在指向B.线程1释放对象A上的锁(不再指向
won
) - 现在,在对象A的等待队列中的线程2被唤醒并获得对象A的锁定,该对象仍然是
false
(不可变的)。 它现在进入同步块并假设它也赢了,这是不正确的。
为什么第二个代码段始终正常工作?
synchronized (won) { if (!won) won = true; }
在这里你有一个瞬态竞争条件,你没有注意到它,因为它在第一次执行run
方法后就消失了。 之后, won
变量不断指向Boolean
表示true
的相同实例,因此适合用作互斥锁。
这并不是说您应该在实际项目中编写此类代码。 应将所有锁定对象分配给final
变量,以确保它们永远不会更改。
对象是否不可变与它是否适合作为synchronized
语句中的锁定对象无关。 然而,重要的是,进入同一组关键区域的所有线程都使用相同的对象(因此最好使对象引用final
),但是可以修改对象本身而不影响它的“锁定”。 另外,两个(或更多)不同的synchronized
语句可以使用不同的引用变量,并且仍然是互斥的,只要不同的引用变量都引用相同的对象即可。
在上面的例子中,关键区域中的代码将一个对象替换为另一个对象,这是一个问题。 锁定在对象上 ,而不是引用 ,因此更改对象是禁止的。
我在第一个代码片段中遇到了竞争条件。 我明白这是因为我在一个不可变对象(Integer类型)上获得了一个锁。
实际上,这根本不是原因。 获取不可变对象的锁定将“正常”工作。 问题是它可能不会做任何有用的事情……
第一个例子打破的真正原因是你正在锁定错误的东西。 执行此操作时 – nRequests++
– 您实际执行的操作等同于此非primefaces序列:
int temp = nRequests.integerValue(); temp = temp + 1; nRequests = Integer.valueOf(temp);
换句话说,您正在为static
变量nRequests
分配不同的对象引用。
问题是,在您的代码段中,每次对变量进行更新时,线程将在不同的对象上进行同步。 那是因为每个线程都会更改对要锁定的对象的引用 。
为了正确同步,所有线程都需要锁定同一个对象 ; 例如
class RequestObject implements Runnable { private static Integer nRequests = 0; private static final Object lock = new Object(); @Override public void run() { synchronized (lock) { nRequests++; } } }
事实上,第二个例子遇到的问题与第一个问题相同。 您在测试中没有注意到的原因是,从won == false
到won == true
的转换仅发生一次……因此潜在竞争条件实际发生的概率要小得多。
实际上,您的第二个代码也不是线程安全的。 请使用下面的代码自行检查(你会发现第一个print语句有时会是2,这意味着synchronized块中有两个线程!)。 底线:代码片段 – 1和代码片段 – 2基本相同,因此不是线程安全的…
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; public class Racer implements Runnable { public static AtomicInteger counter = new AtomicInteger(0); public static Boolean won = false; @Override public void run() { synchronized (won) { System.out.println(counter.incrementAndGet()); //should be always 1; otherwise race condition if (!won) { won = true; try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(counter.decrementAndGet()); //should be always 0; otherwise race condition } } public static void main(String[] args) { int numberOfThreads = 20; ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads); for(int i = 0; i < numberOfThreads; i++) { executor.execute(new Racer()); } executor.shutdown(); } }