Java中同步关键字指南
1.概述
这篇快速文章将介绍如何在Java中使用synchronized块。
简单地说,在多线程环境中,当两个或多个线程同时尝试更新可变共享数据时,就会发生竞争条件。Java提供了一种通过同步对共享数据的线程访问来避免竞争条件的机制。
标记为synchronized的逻辑变为同步块,在任何给定时间只允许一个线程执行。
2.为什么同步?
让我们考虑一个典型的竞争条件,我们计算总和,多个线程执行calculate()方法:
public class BaeldungSynchronizedMethods {
private int sum = 0;
public void calculate() {
setSum(getSum() + 1);
}
// standard setters and getters
}
让我们写一个简单的测试:
@Test
public void givenMultiThread_whenNonSyncMethod() {
ExecutorService service = Executors.newFixedThreadPool(3);
BaeldungSynchronizedMethods summation = new BaeldungSynchronizedMethods();
IntStream.range(0, 1000)
.forEach(count -> service.submit(summation::calculate));
service.awaitTermination(1000, TimeUnit.MILLISECONDS);
assertEquals(1000, summation.getSum());
}
我们只是使用带有3线程池的ExecutorService来执行1000次计算()。
如果我们按顺序执行,预期的输出将是1000,但我们的多线程执行几乎每次都会失败,实际输出不一致,例如:
java.lang.AssertionError: expected:<1000> but was:<965>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
...
这个结果当然不是意料之外的。
避免竞争条件的一种简单方法是使用synchronized关键字使操作成为线程安全的。
3. 同步关键字
该同步关键字可以以不同级别被使用:
- 实例方法
- 静态方法
- 代码块
当我们使用synchronized块时,内部Java使用监视器(也称为监视器锁或内部锁)来提供同步。这些监视器绑定到一个对象,因此同一对象的所有同步块只能有一个线程同时执行它们。
3.1 Synchronized实例方法
只需在方法声明中添加synchronized关键字即可使方法同步:
public synchronized void synchronisedCalculate() {
setSum(getSum() + 1);
}
请注意,一旦我们同步该方法,测试用例就会通过,实际输出为1000:
@Test
public void givenMultiThread_whenMethodSync() {
ExecutorService service = Executors.newFixedThreadPool(3);
SynchronizedMethods method = new SynchronizedMethods();
IntStream.range(0, 1000)
.forEach(count -> service.submit(method::synchronisedCalculate));
service.awaitTermination(1000, TimeUnit.MILLISECONDS);
assertEquals(1000, method.getSum());
}
实例方法在拥有该方法的类的实例上同步。这意味着每个类的实例只能有一个线程可以执行此方法。
3.2 Synchronized static方法
静态方法与实例方法一样是同步的:
public static synchronized void syncStaticCalculate() {
staticSum = staticSum + 1;
}
这些方法在与该类关联的Class对象上同步,并且由于每个JVM每个类只存在一个Class对象,因此每个类只能在一个静态同步方法内执行一个线程,而不管它具有多少个实例。
我们来试试吧:
@Test
public void givenMultiThread_whenStaticSyncMethod() {
ExecutorService service = Executors.newCachedThreadPool();
IntStream.range(0, 1000)
.forEach(count ->
service.submit(BaeldungSynchronizedMethods::syncStaticCalculate));
service.awaitTermination(100, TimeUnit.MILLISECONDS);
assertEquals(1000, BaeldungSynchronizedMethods.staticSum);
}
3.3 方法中的同步块
有时我们不想同步整个方法,只需要同步其中的一些指令。这可以通过将 synchronized应用于块来实现:
public void performSynchrinisedTask() {
synchronized (this) {
setCount(getCount()+1);
}
}
让我们测试一下这个变化:
@Test
public void givenMultiThread_whenBlockSync() {
ExecutorService service = Executors.newFixedThreadPool(3);
BaeldungSynchronizedBlocks synchronizedBlocks = new BaeldungSynchronizedBlocks();
IntStream.range(0, 1000)
.forEach(count ->
service.submit(synchronizedBlocks::performSynchronisedTask));
service.awaitTermination(100, TimeUnit.MILLISECONDS);
assertEquals(1000, synchronizedBlocks.getCount());
}
请注意,我们将参数this传递给synchronized块。这是监视器对象,块内的代码在监视器对象上获得同步。简而言之,每个监视器对象只能在该代码块内执行一个线程。
如果方法是静态的,我们将传递类名来代替对象引用。该类将成为块同步的监视器:
public static void performStaticSyncTask(){
synchronized (SynchronisedBlocks.class) {
setStaticCount(getStaticCount() + 1);
}
}
让我们在静态方法中测试块:
@Test
public void givenMultiThread_whenStaticSyncBlock() {
ExecutorService service = Executors.newCachedThreadPool();
IntStream.range(0, 1000)
.forEach(count ->
service.submit(BaeldungSynchronizedBlocks::performStaticSyncTask));
service.awaitTermination(100, TimeUnit.MILLISECONDS);
assertEquals(1000, BaeldungSynchronizedBlocks.getStaticCount());
}
5.结论
在这篇快速文章中,我们已经看到了使用synchronized关键字实现线程同步的不同方法。
我们还探讨了竞争条件如何影响我们的应用程序,以及同步如何帮助我们避免这种情况。有关使用Java中的锁的线程安全性的更多信息,请参阅我们的java.util.concurrent.Locks 文章。
关注微信公众号:Java知己,
回复关键词 synchronized 获取本教程的完整代码。