Java中的Synchronized关键字
1.介绍
简单的讲,在多线程环境中,当一个或多个线程试图更新一个可变共享数据时会出现竞争情况,你无法确定共享数据将如何变化。Java通过同步线程获取共享资源方式可以做到避免此竞争。
使用 synchronized 关键字的代码变成一个 synchronized 块,同一时间只允许一个线程执行。
2.为什么要同步机制?
让我们考虑以下代码在多线程情况下的执行结果:
public class SynchronizedMethods {
private int sum = 0;
public void calculate() {
setSum(getSum() + 1);
}
// 标准 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) …
很明显这不是我们想要的结果。
3.Synchronized关键字
synchronized关键字可用于不同级别 :
- 实例方法
- 静态方法
- 代码块
当使用synchronized代码块时,Java内部使用一个monitor或叫monitor锁来提供同步。 这些监视器绑定到一个对象,因此同一对象的所有同步块只能有一个线程同时执行它们 。
3.1 同步实例方法
只需要在方法声明处添加synchronized关键字使方法变成同步的。
public synchronized void synchronisedCalculate() {
setSum(getSum() + 1);
}
注意,一旦我们同步了方法,测试用例就通过了,实际输出为1000 :
@Test
public void givenMultiThread_whenMethodSync() throws InterruptedException {
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());
}
Java实例方法同步是同步在拥有该方法的对象上。这样,每个实例其方法同步都同步在不同的对象上,即该方法所属的实例。只有一个线程能够在实例方法同步块中运行。如果有多个实例存在,那么一个线程一次可以在一个实例同步块中执行操作。一个实例一个线程。
3.2 同步静态方法
也是添加synchronized关键字:
public static synchronized void syncStaticCalculate() {
staticSum = staticSum + 1;
}
那静态方法的同步对比实例方法同步区别在于:
静态方法同步相对于JVM的类级别同步,针对某个类在同一时刻只允许一个线程可以执行。无关类的实例数量。
来测试一下:
@Test
public void givenMultiThread_whenStaticSyncMethod() throws InterruptedException {
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 同步代码块
有时我们只希望同步方法中的某些指令而不是整个方法。例如:
public class SynchronizedBlocks {
private int count = 0;
public void performSynchronisedTask() {
synchronized (this) {
setCount(getCount()+1);
}
}
// getter and setter
}
测试一下有何变化:
@Test
public void givenMultiThread_whenBlockSync() throws InterruptedException {
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());
}
这里注意我们将参数this传递给了synchronized块。这就是监视器对象,块中的代码在此监视器对象上实现同步。简单的说,每一个监视器对象只有一个线程可以执行代码块。
假如方法是静态的,我们将传递类名以代替对象引用。并且对于该代码块的同步来说该类将是一个监视器。
public static void performStaticSyncTask(){
synchronized (SynchronisedBlocks.class) {
setStaticCount(getStaticCount() + 1);
}
}
测试一下:
@Test
public void givenMultiThread_whenStaticSyncBlock() throws InterruptedException {
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 Reentrancy
同步方法和块后面的锁是可重入的。也就是说,当前线程可以在持有同一个同步锁的同时一次又一次地获取它。
例如下面代码:
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");
}
}
}
输出:
First time acquiring it
Entering again
And again
4.总结
在本文中,我们看到了使用synchronized关键字实现线程同步的不同方法。 我们还探讨了竞争条件如何影响我们的应用程序,以及同步如何帮助我们避免这种情况。