1.概述
本文章讲解Java中如何使用 synchronized 关键字。
在多线程环境中,当两个或多个线程同时尝试更新可变的共享数据时会发生Race Conditions(竞争条件)。Java提供了一种通过同步线程控制对共享变量访问的机制来避免Reace Conditions。
一段标有 synchronized 的代码变为同步块,在任何时间上只允许被一个线程执行。
2.为什么需要同步?
让我们考虑一个典型的Race Conditions,用多个线程执行 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 来执行calculate()方法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)
...
这个结果是意料之中的。
避免Race Conditions的简单方法是通过使用 synchronized 关键字使得线程操作安全。
3. synchronized 关键词
synchronized关键字可以修饰不同层级的代码:
- 实例方法
- 静态方法
- 代码块
当我们使用 synchronized 时,java内部使用Monitor也称为监视锁或内在锁,来实现同步。每个对象都会绑定一个Monitor,因此同一对象的同步代码,同一时间只能被一个线程执行。
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 静态方法
静态方法同步和普通的实例方法一样:
public static synchronized void syncStaticCalculate() {
staticSum = staticSum + 1;
}
静态方法同步锁住的是类对象,并且由于每个jvm仅存在一个该类对象,所以同一时间只有一个线程可以执行该类的静态同步方法,无论有多少类的实例。
下面来测试:
@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 performSynchronisedTask() {
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()
。这个参数就是monitor对象,synchronized 块中的代码是在此monitor对象上同步。简单来说,每个monitor对象,同时只能有一个线程可以执行该代码块内的代码。
如果该方法是静态方法,我们则可以传递类对象,该类对象将成为用于同步块的monitor对象:
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());
}
3.4 可重入性
synchronized锁是支持重入的(reentrant)。也就是说,当前线程获得锁对象后,还能再次进入同步代码块获取相同的同步锁:
Object lock = new Object();
synchronized (lock) {
System.out.println("First time acquiring it");
synchronized (lock) {
System.out.println("Entering again");
synchronized (lock) {
System.out.println("And again");
}
}
}
如上所示,当我们在Synchronized代码块中时,我们还可以重复获取相同的Monitor锁。
4.结论
本文章中,我们学习了如何使用synchronized关键字实现线程同步。
更多使用Java锁实现线程安全性的内容,请参阅我们的java.util.concurrent.Locks文章。
本教程完整代码存放在GitHub