11.一个诡异的可见性问题

我们说synchronized能解决的三个问题是:原子性、可见性和有序性。那是否所有场景都需要同时解决这三个问题呢?不一定!看个例子:

public class VolatileExample {
    public  static boolean stop = false;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            int i = 0;
            while (!stop) {
                i++;
            }
        });
        t1.start();
        System.out.println("begin start thread");
        Thread.sleep(1000);
        stop = true;
    }
}

这段代码的逻辑很简单,首先t1线程通过stop变量来判断是否结束循环,然后在main线程中通过修改stop变量的值来破坏t1线程的循环条件从而退出循环。但是实际情况是t1并没有按照期望输出。

注意如果你的JDK是client版本可能看不到效果。

我们while这里可以给i++加锁,也就是将上面t1的代码改成如下的样子,再运行就会让程序停下来

Thread t1 = new Thread(() -> {
    int i = 0;
    while (!stop) {
        synchronized (VolatileExample.class){
            i++;
        }

    }
});

或者我们添加一个打印和或者Thread.sleep(0),也就是将上面t1的代码改成如下的样子,再运行也会让程序停下来。

Thread t1 = new Thread(() -> {
    int i = 0;
    while (!stop) {
        System.out.println("thread t1");
        i++;
    }
});

还有一个更牛的方式,增加一个创建文件的操作,此时也会让程序停下来:

Thread t1 = new Thread(() -> {
    int i = 0;
    while (!stop) {
        new File("");
        i++;
    }
});

这是什么道理呢?很明显此时必然与new File()或者加锁等的底层机制有关系。

我们先看一下println的实现:

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

可以看到这里的println里加锁了,而且是类锁System。这里加了synchronized这个同步关键字,会防止循环期间对于stop值的缓存。因为println有加锁的操作,所以当其完成任务要释放锁的时候,会强制性的把工作内存中涉及到的写操作同步到主内存。 从IO角度来说,print本质上是一个IO的操作,我们知道磁盘IO的效率一定要比CPU的计算效率慢得多,所以IO可以使得CPU有时间去做内存刷新的事情,从而导致这个现象。比如我们可以在里面定义一个new File()。同样会达到效果。 Thread.sleep(0)生效的原因是导致线程切换,线程切换会导致缓存失效从而读取到了新的值。

在单线程的环境下,如果向一个变量先写入一个值,然后在没有写干涉的情况下读取这个变量的值,那这个时候读取到的这个变量的值应该是之前写入的那个值。这本来是一个很正常的事情。但是在多线程环境下,读和写发生在不同的线程中的时候,可能会出现:读线程不能及时的读取到其他线程写入的最新的值,这就是可见性。

为什么多线程环境下会存在可见性问题呢?

这主要是指令执行过程中存在重排序导致的,Server版本的编译器是面向服务器的,会做大量的优化,例如勿用代码消除,循环展开、消除公共子表达式等等。而本节最开始的代码之所以不能停止,就是因为代码被编译器优化了,我们直接在stop的定义前添加volatile关键字即可:

public volatile static boolean stop = false;

重排序问题具体咋回事?是不是只有重排序会带来可见性问题?不是的。我们后面继续讨论。

一个简单的例子是多线程中对共享变量的访问。假设有一个共享变量x,多个线程可能会同时对它进行读写操作。如果没有正确使用同步机制,就可能出现可见问题。 例如,一个线程可能将x的值修改了,但是另一个线程并没有看到这个修改。这是因为线程之间的缓存不一定是同步的,一个线程对变量的修改可能还没有被刷新到主存中,另一个线程就已经从自己的缓存中读取了这个变量,导致出现不一致的结果。 下面是一个简单的示例代码,展示了可见问题的出现: ``` class VisibilityDemo { private static boolean flag = true; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (flag) { System.out.println("Thread 1 is running"); } System.out.println("Thread 1 is done"); }).start(); Thread.sleep(1000); flag = false; System.out.println("Main thread sets flag to false"); } } ``` 该代码启动了两个线程,一个线程不断打印信息,另一个线程修改共享变量flag的值为false。由于没有使用同步机制来保证可见,因此第一个线程可能无法看到flag的修改,导致它无法退出循环。 可以通过使用volatile关键字来解决可见问题,它可以确保变量的修改被立即刷新到主存中,从而保证其他线程可以看到最新的值。修改后的代码如下: ``` class VisibilityDemo { private static volatile boolean flag = true; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (flag) { System.out.println("Thread 1 is running"); } System.out.println("Thread 1 is done"); }).start(); Thread.sleep(1000); flag = false; System.out.println("Main thread sets flag to false"); } } ``` 在这个版本中,flag变量的声明加上了volatile关键字,保证了可见。运行该代码,可以看到第一个线程可以正确退出循环。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

纵横千里,捭阖四方

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

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

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

打赏作者

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

抵扣说明:

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

余额充值