Baeldung翻译系列之Java并发基础:
Java中的concurrent包
Java中的Synchronized关键字
Future介绍
ThreadLocal介绍
Java线程的生命周期
如何杀掉一个Java线程
Java中的线程池介绍
实现Runnable接口还是继承Thread类
Java中的wait和notify方法
Runnable vs Callable
wait和sleep的区别
Thread.join方法介绍
Java中使用锁对象
ThreadPoolTaskExecutor中的corePoolSize和maxPoolSize
Java中的异步编程
1.概览
在本文中,我们将学习在 Java 中使用 synchronized 块
。
简而言之,在多线程环境中,当两个或多个线程尝试同时更新可变共享数据时,就会发生竞态条件。Java 提供了一种机制,通过同步线程对共享数据的访问来避免竞态条件。
使用 synchronized
标记的代码逻辑成为 synchronized 块
,它只允许一个线程在任何给定时间执行。
2.为何要同步
让我们考虑一个典型的竞态条件,我们计算数字的和,多个线程执行calculate()
方法:
public class SynchronizedMethods {
private int sum = 0;
public void calculate() {
setSum(getSum() + 1);
}
// standard setters and getters
}
然后我们写一个简单的测试:
@Test
public void givenMultiThread_whenNonSyncMethod() {
ExecutorService service = Executors.newFixedThreadPool(3);
SynchronizedMethods summation = new SynchronizedMethods();
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)
...
当然,这个结果并不出乎我们的预料。
避免竞态条件的一个简单方法是使用synchronized
关键字使操作线程安全。
3. Synchronized关键字
我们可以在不同的层次上使用synchronized
关键字:
- 实例方法
- 静态方法
- 代码块
当我们使用一个synchronized
块时,Java内部使用一个监视器,也被称为监视器锁或内在锁,来提供同步。这些监视器绑定到一个对象上;因此,同一个对象的所有synchronized
块在同一时间内只能有一个线程执行它们。
3.1. 同步实例方法
我们可以在方法前面添加 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. 同步静态方法
静态方法的同步方式与实例方法相类似:
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(SynchronizedMethods::syncStaticCalculate));
service.awaitTermination(100, TimeUnit.MILLISECONDS);
assertEquals(1000, SynchronizedMethods.staticSum);
}
3.3. 方法内部的同步代码块
有时我们不希望同步整个方法,只同步其中的一些指令。我们可以通过将synchronized应用到一个块来实现这一点:
public void performSynchronisedTask() {
// 这里可以有其他非同步代码
synchronized (this) {
setCount(getCount()+1);
}
// 这里也可以有其他非同步代码
}
然后我们可以测试一下这个改变:
@Test
public void givenMultiThread_whenBlockSync() {
ExecutorService service = Executors.newFixedThreadPool(3);
SynchronizedBlocks synchronizedBlocks = new SynchronizedBlocks();
IntStream.range(0, 1000)
.forEach(count ->
service.submit(synchronizedBlocks::performSynchronisedTask));
service.awaitTermination(100, TimeUnit.MILLISECONDS);
assertEquals(1000, synchronizedBlocks.getCount());
}
注意我们向synchronized
块传递了一个参数this
,这就是监视器对象。块内的代码在监视器对象上进行同步。简单来说,每个监视器对象只能有一个线程在该代码块内部执行。
如果方法是静态的,我们会将类名传递给对象引用,类将成为块同步的监视器:
(译者:监视器就是这把锁的主体,多个线程同时执行,谁拿到了这把锁,谁才能执行,其他的线程必须等待)
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(SynchronizedBlocks::performStaticSyncTask));
service.awaitTermination(100, TimeUnit.MILLISECONDS);
assertEquals(1000, SynchronizedBlocks.getStaticCount());
}
3.4. 可重入性
synchronized方法和块背后的锁是可重入的。这意味着当前线程可以在持有它的同时,反复获取同一个synchronized锁:
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块中时,我们可以反复获取同一个监视器锁。
4. 结论
在这篇简短的文章中,我们探讨了使用synchronized
关键字实现线程同步的不同方式。
我们还了解了竞态条件如何影响我们的应用程序,以及同步如何帮助我们避免这种情况。
评论区