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关键字实现线程同步的不同方式。
我们还了解了竞态条件如何影响我们的应用程序,以及同步如何帮助我们避免这种情况。
评论区