首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Java并发编程(一)---原子性,可见性,有序性

Java并发编程(一)---原子性,可见性,有序性

作者头像
码农飞哥
发布2021-08-18 10:32:46
发布2021-08-18 10:32:46
3730
举报
文章被收录于专栏:好好学习好好学习

摘要

并发编程世界里,由于CPU缓存导致的可见性问题,线程切换导致的原子性问题,以及编译器重排序导致的有序性问题是并发编程Bug的根源。

正文

原子性

原子性是指一个操作是不可中断的,即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他的线程干扰。 一般认为CPU的指令都是原子操作,但是我们写的代码就不一定是原子操作了。一条高级语句在CPU中可能会分成若干条指令来执行,每条指令执行完之后就有可能会发生线程切换。故线程切换造成的原子性问题。

例如:count=count+1 共有三个指令

  • 指令一: 将count值从内存加载到到CPU的寄存器中
  • 指令二:在寄存器中+1操作
  • 指令三 :将新值写入到内存中(由于缓存机制,写入的可能是CPU的缓存而不是内存)

操作系统做任务切换可以发生在任何一条CPU指令执行完,是CPU指令执行完。

有序性

在并发时,由于编译器重排序导致不是按照程序的顺序执行。 例如:

代码语言:javascript
复制
public class SingletonDemo {
    private static  SingletonDemo instance = null;

    private SingletonDemo(){

    }

    static SingletonDemo getSingletonDemo() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();    //6
                }
            }
        }
        return instance;
    }
}

这里会有有序性问题: 问题主要出在了 new SingletonDemo() 这一步 因为instance = new SingletonDemo();主要有三个指令

  1. 分配内存空间M
  2. 在内存M上初始化对象
  3. 然后M的地址赋值给instance变量 正常顺序是1-2-3 但是CPU重排序之后执行顺序可能变成了 1-3-2 步骤如下:
  4. A首先进入synchronized,由于instance为null,所以它执行instance = new SingletonDemo();
  5. 然后线程 A执行1->JVM先画出了一些分配给SingletonDemo实例的空白内存,并赋值给instance
  6. 在还没有进行第三步(将instance引用指向内存空间)的时候,恰好发生了线程切换 ,切换到了线程B上,
  7. 如果此时线程B也执行getSingletonDemo()方法,那么线程B 在执行第一个判断是会发现instance!=null,所以直接返回了instance,而此时的instance是没有初始化的。 那么为什么会发生重排序呢?这个主要是为了提高执行效率。

可见性

一个线程对共享变量的修改。另外一个线程能够立刻看到,我们称之为可见性。 可见性问题可能在各个环节产生,比如:前面提到的指令重排序产生的可见性问题,另外在编译器的优化或者某些硬件的优化都会产生可见性问题。 比如:某个线程将一个共享值优化到了内存中,而另一个线程将这个共享值优化到了缓存汇总,当修改内存中的是值的时候,缓存中的值是不知道这个修改的。 比如有些硬件的优化,程序在怼同一个地址进行多次写是,它认为是没有必要的,最保留最后一次的写,那么之前写的数据对其他线程就不可见了。 如图所示:

在这里插入图片描述 共享变量V可以由线程A和线程B同时操作,并写入各自的CPU缓存,然后由CPU的寄存器写入内存中。

代码语言:javascript
复制
public class LongTest {
    private static long atest = 0L;
    public void countTest() {
        for (int i = 0; i < 10000; i++) {
            atest = atest + 1;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        final LongTest longTest = new LongTest();
        Thread threadA = new Thread(new Runnable() {
            public void run() {
                longTest.countTest();
            }
        });
        Thread threadB = new Thread(new Runnable() {
            public void run() {
                longTest.countTest();
            }
        });
        threadA.start();
        threadB.start();
        threadA.join();
        threadB.join();
        System.out.println("*******获得到的atest值为=" + atest);
    }
}

如上程序,运行之后我们会发现 atest 值永远都不会到达20000,而是在10000-20000之间的随机数。 原因分析:假设线程A和线程B同时执行 atest = atest + 1;, 线程A 读取到的原值是0,执行+1操作之后,得到新值1,同样的,线程B也是读取到的原值0,然后执行+1操作,得到新值1。这样就永远得不到结果2。 类推的话,循环执行10000次也是同理,线程A执行+1操作时不能及时获得线程B已经写入的值,故导致值永远不可能达到20000。

总结

并发编程中主要的问题就是可见性问题, 原子性问题,有序性问题。本文介绍了这三种问题的发生原因,以及发生的场景。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-10-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农飞哥 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 摘要
  • 正文
    • 原子性
    • 有序性
    • 可见性
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档