Java 并发-并发程序的特性
本文介绍 Java 并发编程中并发程序的特性,如并发程序和并发问题产生的背景,以及什么是并发的可见性、原子性、有序性问题

# Java 并发-并发程序的特性

# 并发程序的背景

  1. 冯诺依曼体系将计算机分为 CPU、运算器、存储器、输入设备、输出设备,奠定了经典计算机的发展

  2. 尽管 CPU、内存、IO 设备不断向更快的方向进步,但三者的速度差异一直是核心矛盾

    • CPU - 内存:天上一天,地上一年
    • 内存 - IO:天上一天,地上十年
  3. 为了缓解这一核心矛盾,计算机、操作系统、编译程序都做了相应的优化

    • CPU 增加缓存,均衡与内存的速度差异
    • 操作系统增加了进程、线程,分时复用 CPU,均衡 CPU 与 IO 的速度差异
    • 编译程序优化指令顺序,以提高缓存利用率

CPU 缓存技术、多线程技术、指令优化技术提高了程序的执行效率,但也使并发程序运行的问题暴露无遗

# 并发的可见性问题

  1. 程序变量存储在内存中,CPU 读写变量时,会操作缓存中的变量副本,以提高运行效率

    注意:对于何时把数据从缓存写到内存,没有固定的时间

  2. 在单核 CPU 机器上,线程操作同一个内核的缓存,多个线程看见的数据是统一的

  3. 在多核 CPU 机器上,每一个核心都有独立的缓存,而线程被调度到不同的核心执行

    image-20220114122935681

对同一变量的操作,不同缓存中的变量副本在线程之间不立即可见,导致多线程对共享变量的操作结果错误

# 并发的原子性问题

  1. 假若不考虑可见性问题,对于共享变量的非原子操作,也会引发错误的操作结果

  2. CPU 只能保证每一条指令是原子执行的,但每一条程序语句是不一定的

    例如i += 1,完成这条语句执行需要 3 条指令

    指令 1:将变量 i 加入 CPU 寄存器
    指令 2:寄存器执行 +1 操作
    指令 3:将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)
    
  3. 操作系统进行多线程调度时,可能在任意指令执行完毕时进行线程切换

    image-20220114132306418

非原子的程序语句,在语句没有执行完毕就切换线程,导致共享变量被其他线程覆盖

# 并发的有序性问题

  1. 编译器和 CPU 为了优化性能,会改变指令的执行顺序,在并发情况下可能会印象影响程序的运行结果

  2. 一条语句可能是多条指令,这些指令的执行顺序可能会被重排

    例如双检锁实现单例模式

    public class SafeLazyMan {
        private static SafeLazyMan man;    
        public static SafeLazyMan instance() {
            if (man == null) { // 第一次检查
                synchronized (SafeLazyMan.class) {
                    if (man == null) { // 第二次检查
                        man = new SafeLazyMan();
                    }
                }
            }
            return man;
        }
    }
    

    为了防止有多个线程完成第一次检查,多次创建对象,增加获取锁之后的第二次检查,保证对象不被重复的创建,但是 new 对象操作分为分配内存、初始化对象、引用赋值三个指令

    指令重排后先对引用赋值,再初始化对象,会导致其他线程第一次检查时虽然不为 null 了,但其实没有对象,引发空指针问题

    image-20220114150302753

编译器和 CPU 对汇编指令的执行顺序的进行优化,可以缩短执行消耗的时钟周期,但可能会导致不符合并发程序运行的预期结果

Comment here, be cool~

Copyright © 2020 CadeCode

Theme 2zh powered by VuePress

本页访问次数 0

Loading