synchronized 保证可见性、原子性、有序性

发布时间 2023-04-27 20:37:22作者: 变体精灵

一、概述

并发三大特性即 可见性、原子性、有序性

可见性: 一个线程修改了共享变量的值,另外一个线程应该立即得到共享变量的最新值

原子性: 一个或多个操作要么全部执行,并且在执行的过程中不会被其它因素打断,要么全部不执行

有序性: 为了提高程序运行效率,Java 在编译和运行时会对指令进行重排序,重排序后的指令可以保证单线程环境下程序的最终结果一致,但是多线程情况下可能会出现不符合预期的结果

 

二、测试

2.1、可见性

@Slf4j
public class Visibility {
    // 定义共享变量
    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {

            }
        }, "t1").start();

        // main 线程休眠 2s
        TimeUnit.SECONDS.sleep(2);

        new Thread(() -> {
            flag = false;
            log.info(Thread.currentThread().getName() + " 修改共享变量 flag 的值为 " + false);
        }, "t2").start();
    }
}

从上面程序的执行结果可以看出,t2 线程修改了 flag 的值为 false 之后,t1 线程并没有停止,一直在执行 while(true) 循环,也就是说线程 t2 修改了 flag 的值后,t1 线程并没有看到修改后的 flag 的新值

添加 synchronized 对上面的代码进行改造

@Slf4j
public class Visibility {
    // 定义共享变量
    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {
                synchronized (new Object()) {
                    log.info("flag 的值为: {}", flag);
                }
            }
        }, "t1").start();

        // main 线程休眠 2s
        TimeUnit.SECONDS.sleep(2);

        new Thread(() -> {
            flag = false;
            log.info(Thread.currentThread().getName() + " 修改共享变量 flag 的值为 " + false);
        }, "t2").start();
    }
}

上述代码添加 synchronized 之后,整个程序就会停下来,不会一直循环下去了

 

2.2、原子性

@Slf4j
public class Atomicity {
private static int count = 0;
private static final int THREAD_NUMBER = 50;
private static final int CIRCLE_NUMBER = 1000;

private static void increase() {
count++;
}

public static void main(String[] args) throws InterruptedException {
// 定义线程数组
Thread[] threadGroup = new Thread[THREAD_NUMBER];

for (int i = 0; i < threadGroup.length; i++) {
threadGroup[i] = new Thread(() -> {
for (int j = 0; j < CIRCLE_NUMBER; j++) {
increase();
}
});
threadGroup[i].start();
}
// main 线程等待线程数组中的所有线程执行完
for (int i = 0; i < threadGroup.length; i++) {
threadGroup[i].join();
}

log.info("count 的值为: {}", count);
}
}

50 个线程,每个线程循环执行 1000 次的 count++ 操作,我们预期值是 50000,但是多次执行程序得到的结果都是一个小于 50000 的值(极少数的情况下会出现 50000),上面的程序为什么不能得到我们的预期值 50000 呢

究其原因是 count++ 并不是一个原子性操作,其底层对应着 4 条 JVM 指令(甚至是对应机器指令层面的更多的微指令)

0 getstatic    // 从主内存中获取 count 的值
3 iconst_1     // 准备常量 1
4 iadd		   // 将 count 与常量进行相加
5 putstatic    // 将 count 的值写回主内存
8 return

例如在并发场景下两个线程进行 count++ 操作,可能会发生如下情况

时间节点 线程 1 线程 2
t1 从主内存中获取 count 的值,此时 count = 0  
t2 准备常量 1  
t3   从主内存中获取 count 的值,此时 count = 0
t4   准备常量 1
t5   将 count 的值与常量进行相加,此时 count = 1
t6   将 count 的值写回主内存
t7 将 count 的值与常量进行相加,此时 count = 1  
t8 将 count 的值写回主内存  

两个线程都执行了一次 count++ 操作,期望结果是 2,但是最终得到的结果是 1,这也同时解释了上面的例子为什么得不到预期值 50000 的原因了

使用 synchronized 对代码进行改造(在 increase 方法上使用 synchronized)

@Slf4j
public class Atomicity {
    private static int count = 0;
    private static final int THREAD_NUMBER = 50;
    private static final int CIRCLE_NUMBER = 1000;
    // 使用 synchronized 修饰方法
    private synchronized static void increase() {
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        // 定义线程数组
        Thread[] threadGroup = new Thread[THREAD_NUMBER];

        for (int i = 0; i < threadGroup.length; i++) {
            threadGroup[i] = new Thread(() -> {
                for (int j = 0; j < CIRCLE_NUMBER; j++) {
                    increase();
                }
            });
            threadGroup[i].start();
        }
        // main 线程等待线程数组中的所有线程执行完
        for (int i = 0; i < threadGroup.length; i++) {
            threadGroup[i].join();
        }

        log.info("count 的值为: {}", count);
    }
}

不论执行多少次,最终的结果都是预期值 50000

时间节点 线程 1 线程 2
t1 获取锁成功  
t2 从主内存中获取 count 的值,此时 count = 0  
t3 准备常量 1  
t4 将 count 的值与常量进行相加,此时 count = 1  
t5 CPU 时间片到,上下文切换  
t6   尝试获取锁,获取锁失败,进入阻塞状态,让出 CPU 执行权
t7 获得 CPU 时间片,将 count 的值写回主内存,唤醒线程 2  
t8   竞争锁,获取锁成功
t9   从主内存中获取 count 的值,此时 count = 1
t10   准备常量 1
t11   将 count 的值与常量进行相加,此时 count = 2
t12   将 count 的值写回主内存

加了 synchronized 关键字进行修饰后,两个线程执行两次 count++ 操作,最终的结果为 2,符合预期

 

2.3、有序性

测试参考 https://www.cnblogs.com/dalianpai/p/14175292.html

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Orderly {
    int num = 0;
    boolean ready = false;

    // 线程 1 执行的代码
    @Actor
    public void actor1(I_Result r) {
        if (ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    // 线程 2 执行的代码
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
}

上面代码出现 1、4 这两个值是可以理解的,但是为什么会出现 0 这个值呢,唯一的解释就是线程 2 执行代码的顺序是这样的

// 线程 2 执行的代码
@Actor
public void actor2(I_Result r) {
	ready = true;
	num = 2;
}

也就是说,在程序执行的过程中是会出现指令重排现象的,注意: 指令重排不会影响单线程的执行结果,但是多线程环境下就有可能出现我们不想要的结果,这种情况要特别注意

为了保证线程安全,我们需要使用 synchronized 来解决有序性问题

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class Orderly {
    int num = 0;
    boolean ready = false;

    // 线程 1 执行的代码
    @Actor
    public void actor1(I_Result r) {
        synchronized (this) {
            if (ready) {
                r.r1 = num + num;
            } else {
                r.r1 = 1;
            }
        }
    }

    // 线程 2 执行的代码
    @Actor
    public void actor2(I_Result r) {
        synchronized (this) {
            num = 2;
            ready = true;
        }
    }
}

使用 synchronized 之后就没有再出现 0 这个值了

synchronized 保证有序性的原理,我们加 synchronized 之后指令依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码中的代码,保证有序性