资讯专栏INFORMATION COLUMN

可见性问题实例

DevTTL / 2335人阅读

摘要:主线程启动这个线程后,将该变量置为,观察线程是否打印出那行,如果存在可见性问题,主线程修改值为,线程看的值应该还是。

说到并发安全时,我们常提及可见性的问题,通俗点讲就是线程1看不到线程2写入变量v的值(更专业的解释以及是什么导致可见性问题,又该如何解决,见扩展阅读),但一直偏于理论,实际中有没有因可见性而导致问题的例子呢?回答是肯定的,接下来我们一起来看几个例子。

这个例子很简单,新建的线程里有一个普通变量stop,用来表示是否结束循环里的自增操作。主线程启动这个线程后,将该变量置为true,观察线程是否打印出finish loop那行,如果存在可见性问题,主线程修改stop值为true,线程v看stop的值应该还是false。

class VisibilityThread extends Thread {
    private boolean stop;

    public void run() {
        int i = 0;
        System.out.println("start loop.");
        while(!getStop()) {
            i++;
        }
        System.out.println("finish loop,i=" + i);
    }

    public void stopIt() {
        stop = true;
    }

    public boolean getStop(){
        return stop;
    }
}

public class VisibilityTest {
    public static void main(String[] args) throws Exception {
        VisibilityThread v = new VisibilityThread();
        v.start();

        Thread.sleep(1000);//停顿1秒等待新启线程执行
        System.out.println("即将置stop值为true");
        v.stopIt();
        Thread.sleep(1000);
        System.out.println("finish main");
        System.out.println("main中通过getStop获取的stop值:" + v.getStop());
    }
}

我们先来执行一遍(操作系统:XP,下同。JDK:见图示):

执行结果如上图,有人该问了,线程v最终停下来了,这不是表示它看到stop值为true了吗?是的,确实如此。但让我们再看一个这个程序的执行结果。

这一次,我们发现程序一直未能结束,表示线程v看到stop的值是false,但是主线程打印出的值却是true。

对比两次的执行方式,我们发现后一次加上了-server选项。显示version的时候也由Client VM变成了Server VM。那么Client VM与Server VM有什么区别在哪里?简单地讲,Client VM启动时做了一般优化,耗时少,启动快,但程序执行的也相对也较慢;Server VM启动的时候做了更多优化,耗时多,启动慢,但程序执行快。如果在运行java命令的时候没有指定具体模式的时候,会有一个默认值,这个默认值随硬件和操作系统的不同而不同,这里有张JDK 1.6在各平台默认VM模式的图。

我们再来看个例子,这个例子源于hotspot VM的一个bug:

public class InterruptedVisibilityTest {
    public void think() {
        System.out.println("新线程正在执行");
        while (true) {
            if (checkInterruptedStatus()) break;
        }
        System.out.println("新线程退出循环");
    }

    private boolean checkInterruptedStatus() {
        return Thread.currentThread().isInterrupted();
    }

    public static void main(String[] args) throws Exception {
        final InterruptedVisibilityTest test = new InterruptedVisibilityTest();
        Thread thinkerThread = new Thread("Thinker") {
            public void run() {
                test.think();
            }
        };
        thinkerThread.start();
        Thread.sleep(1000);//等待新线程执行
        System.out.println("马上中断thinkerThread");
        thinkerThread.interrupt();
        System.out.println("已经中断thinkerThread");
        thinkerThread.join(3000);
        if (thinkerThread.isAlive()) {
            System.err.println("thinkerThread未能在中断后3s停止");
            System.err.println("JMV bug");
            System.err.println("主线程中检测thinkerThread的中断状态:" + thinkerThread.isInterrupted());
        }
    }
}

这个例子也很简单,thinkerThread一直检查中断状态,主线程在启动thinkerThread之后的某个时刻调用interrupt中断thinkerThread。在《The Java Language Specification Java SE 7 Edition》§17.4.4中我们能够看到,如果线程1调用线程2的interrupt方法,那么所有线程(包括线程2)都能通过Thread.isInterrupted方法来检测到这个中断状态。这里直接用hotspot VM的-server模式执行一下,结果如下图:

thinkerThread没能退出循环,没看到主线程所置的中断状态。

后面这个例子是hotpost VM的一个bug导致的,在最新的hotspot中应该已经被修复了(笔者未测试最新版)。其它VM如IBM J9,JRockit,harmony等并没有发现这样的bug。说这是bug,是因为JLS中规定了main发出的中断必须对thinkerThread可见。但是,如第一个例子,则不是bug,因为JLS是允许这种行为的。当在第一个例子的循环中的i++后面加上一句Thread.yield()调用(该调用在规范中并没有特殊内存语义),这我使用的这个版本的VM上,就看不到可见性问题了。这也说明,JVM的优化是无法预知的,允许可见性的地方不一定就真会出现或一直出现。

JLS允许未充分同步的代码出现可见性问题,但是某个实际的JVM完全可以实现的比JLS上规定的更强,比如不允许可见性问题出现,那么,在这样的JVM上就展现不出这样的问题了。第一个例子这里只是运行在hotpost下,也许在其它JVM下同样采用最优化的方式执行,可能并不会出现这里的问题。

在我们编码的时候,也许并不知道代码会跑在什么样的系统上,不知道会采用什么样的JVM,为了使得写出的代码更健壮,我们只能按照规范所规定的最低保证去编码,要避免这类问题,只有保证代码充分同步,避免数据争用,而不应该依赖于某个具体JVM实现。即使是具体的某款JVM,不同的版本间也可能存在着差异。

最后,这样的例子启发我们,测试代码的时候应尽可能启用各JVM的最佳优化模式。

扩展阅读:

至此,我们已经了解到实际中多线程运行真的会出现这样的场景。为什么会出现可见性问题?有什么解决方案?下面链接中的内容为我们提供了专业的解答。

http://ifeve.com/volatile/

http://ifeve.com/java-memory-model-0/

http://ifeve.com/java-concurrency-constructs/

CSDN上有人发的一个真实案例

via ifeve

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/64061.html

相关文章

  • 猫头鹰的深夜翻译:Volatile的原子, 可见和有序

    摘要:有可能一个线程中的动作相对于另一个线程出现乱序。当实际输出取决于线程交错的结果时,这种情况被称为竞争条件。这里的问题在于代码块不是原子性的,而且实例的变化对别的线程不可见。这种不能同时在多个线程上执行的部分被称为关键部分。 为什么要额外写一篇文章来研究volatile呢?是因为这可能是并发中最令人困惑以及最被误解的结构。我看过不少解释volatile的博客,但是大多数要么不完整,要么难...

    Lionad-Morotar 评论0 收藏0
  • java 关键字总结

    摘要:关键字总结有个关键字,它们是接下来对其中常用的几个关键字进行概括。而通过关键字,并不能解决非原子操作的线程安全性。为了在一个特定对象的一个域上关闭,可以在这个域前加上关键字。是语言的关键字,用来表示一个域不是该对象串行化的一部分。 java 关键字总结 Java有50个关键字,它们是: abstract do implements private ...

    honmaple 评论0 收藏0
  • 多线程学习笔记(1):volatile和synchronized

    摘要:今天开始整理学习多线程的知识,谈谈最重要的两个关键字和。但是这样一个过程比较慢,在使用多线程的时候就会出现问题。有序性有序性是指多线程执行结果的正确性。这种机制在多线程中会出现问题,因此可以通过来禁止重排。 今天开始整理学习多线程的知识,谈谈最重要的两个关键字:volatile和synchronized。 一、三个特性 1、原子性 所谓原子性操作就是指这些操作是不可中断的,要么执行过程...

    jk_v1 评论0 收藏0
  • 高并发 - 基础

    摘要:异步非阻塞方式,任务的完成的通知由其他线程发出。并发并行死锁饥饿活锁死锁线程持有,线程持有。如等,在多线程情况下,该操作不是原子级别的而是原子的,所以一般用于状态标记。 同步/异步、阻塞/非阻塞 同步/异步是 API 被调用者的通知方式。阻塞/非阻塞则是 API 调用者的等待方式(线程挂机/不挂起)。 同步非阻塞 Future方式,任务的完成要主线程自己判断。如NIO,后台有多个任务在...

    phpmatt 评论0 收藏0
  • Java进阶3 —— 类和接口设计原则

    摘要:首当其冲的便是接口中的每个声明必须是即便不指定也是,并且不能设置为非,详细规则可参考可见性部分介绍。函数式接口有着不同的场景,并被认为是对编程语言的一种强大的扩展。抽象类与中的接口有些类似,与中支持默认方法的接口更为相像。 原文链接:http://www.javacodegeeks.com/2015/09/how-to-design-classes-and-interfaces.htm...

    lauren_liuling 评论0 收藏0

发表评论

0条评论

DevTTL

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<