使用synchronized关键字

本文通过一个实例展示了在多线程环境下,未同步的共享变量可能导致结果不一致的问题,并详细介绍了`synchronized`关键字的三种用法:修饰普通方法、静态方法和代码块,以及它们在确保线程安全方面的效果。通过测试用例验证了同步方法和同步代码块的有效性。
摘要由CSDN通过智能技术生成

01、为什么需要保护

为什么多线程环境下,可变共享变量修改后的结果会超出预期。为了解释清楚这一点,来看一个例子。

public class synchronizedMethod {
    private int sum;

    public int getSum() {
        return sum;
    }

    public void setSum(int sum) {
        this.sum = sum;
    }

    public void calculate () {
        setSum(getSum() + 1);
    }
}

上面的SynchronizedMethod 是一个非常简单的类,有一个私有的成员变量 sum,对应的 getter/setter,以及给 sum 加 1 的 calculate() 方法。

ps.快速创建测试用例

第一步,把鼠标移动到类名上,会弹出一个提示框。

图片

第二步,点击「More actions」按钮,会弹出以下提示框。

图片

第三步,选择「Create Test」,弹出创建测试用例的对话框。

图片

选择最新的 JUnit5,如果项目之前没有引入 JUnit5 依赖的话,IDEA 会提醒,点击 Fix,IDEA 会自动添加,非常智能化。在对话框中勾选要创建测试用例的方法——calculate()。

点击 OK 按钮后,IDEA 会在 src 的同级目录 test 下创建一个名为 SynchronizedMethodTest 的测试类:

class SynchronizedMethodTest {
    @Test
    void calculate() {
    }
}

calculate() 方法上会有一个 @Test 的注解,表示这是一个测试方法。

测试方法代码如下:

import org.junit.Test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import static org.junit.Assert.assertEquals;

public class synchronizedMethodTest {

    @Test
    public void calculate() throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(3);
        synchronizedMethod summation = new synchronizedMethod();

        IntStream.range(0, 1000)
                .forEach(count -> service.submit(summation::calculate));
        service.awaitTermination(1000, TimeUnit.MILLISECONDS);

        assertEquals(1000, summation.getSum());
    }
}

1)Executors.newFixedThreadPool() 方法可以创建一个指定大小的线程池服务 ExecutorService。

2)通过 IntStream.range(0, 1000).forEach() 来执行 calculate() 方法 1000 次。

3)通过 assertEquals() 方法进行判断。

运行该测试用例的结果如下所示:
在这里插入图片描述
预期的值为 1000,但实际的值是 996。这是因为多线程环境下,可变的共享数据没有得到保护。

02、synchronized 的用法

synchronized 最常见的三种用法。

1)直接用在方法上(普通方法)

public synchronized void synchronizedCalculate() {
    setSum(getSum() + 1);
}

修改一下测试用例:

@Test
public void synchronizedCalculate() throws InterruptedException {
    ExecutorService service = Executors.newFixedThreadPool(3);
    SynchronizedMethod summation = new SynchronizedMethod();

    IntStream.range(0, 1000)
            .forEach(count -> service.submit(summation::synchronizedCalculate));
    service.awaitTermination(1000, TimeUnit.MILLISECONDS);

    assertEquals(1000, summation.getSum());
}

这时候,再运行测试用例就通过。因为 synchronized 关键字会对 SynchronizedMethod 对象进行加锁,同一时间内只允许一个线程对 sum 进行修改。

2)用在 static 方法上(静态方法)

public class SynchronizedStaticMethod {

    private static int sum;

    public synchronized static void synchronizedCalculate () {
        sum = sum + 1;
    }
}

sum 是一个静态变量,要修改静态变量的时候,就需要把方法也变成 static 的。(即需要在静态方法里面修改静态变量)

新建一个测试用例:

import org.junit.Test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import static org.junit.jupiter.api.Assertions.*;

public class SynchronizedStaticMethodTest {
    @Test
    void synchronizedCalculate() throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(3);

        IntStream.range(0, 1000)
                .forEach(count -> service.submit(SynchronizedStaticMethod::synchronizedCalculate));
        service.awaitTermination(1000, TimeUnit.MILLISECONDS);

        assertEquals(1000, SynchronizedStaticMethod.sum);
    }
}

静态方法上添加 synchronized 的时候就不需要实例化对象了,直接使用类名就可以引用方法和使用变量了。测试用例也是可以通过的。

synchronized static 和 synchronized 不同的是,
前者锁的是,同一时间只能有一个线程访问这个类;
后者锁的是对象实例,同一时间只能有一个线程访问方法。

3)用在代码块

public void synchronisedThis() {
    synchronized (this) {
        setSum(getSum() + 1);
    }
}

这时候,将 this 传递给了 synchronized 代码块,当在某个线程中执行这段代码块时,该线程会获取 this 对象的锁,从而使得其他线程无法同时访问该代码块。
如果方法是静态方法,我们将传递类名代替对象引用,示例如下所示:

public static void synchronisedThis() {
    synchronized (SynchronizedStaticMethod.class) {
        sum = sum + 1;
    }
}

新建一个测试用例:

@Test

public void synchronisedThis() throws InterruptedException {
    ExecutorService service = Executors.newFixedThreadPool(3);
    SynchronizedMethod summation = new SynchronizedMethod();

    IntStream.range(0, 1000)
            .forEach(count -> service.submit(summation::synchronisedThis));
    service.awaitTermination(1000, TimeUnit.MILLISECONDS);

    assertEquals(1000, summation.getSum());
}

运行后也是可以通过的。
synchronized 代码块的锁粒度要比 synchronized 方法小一些,因为 synchronized 代码块所在的方法里还可以有其他代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

℡folk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值