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 代码块所在的方法里还可以有其他代码。