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

posted @ 2021-06-06 23:09  一锤子技术员  阅读(5)  评论(0编辑  收藏  举报  来源