• 【并发编程】1.并发问题的由来


    并发编程中问题的由来:

    CPU、内存、I/O设备的速度存在巨大差异,程序的整体性能取决于最慢的操作——读取I/O设备,为了合理利用CPU性能,平衡三者的速度差异,计算机体系结构、操作系统、编译程序做出了以下改进。

    1. CPU增加了缓存
    2. 操作系统增加进程、线程分时复用CPU,进而均衡CPU与I/O设备的速度差异
    3. 编译程序优化指令执行次序,使得缓存能够更加合理地利用

    由此引发出了以下问题:

    1.可见性——CPU缓存导致

    早期单核CPU时,CPU缓存的数据与内存的数据是保持一致的,
    线程A,线程B在同一个核心上切换运行,A对缓存中的操作对于B是立即可见的,所以不存在可见性问题

    到了多核CPU时代,每个核心都有自己的CPU缓存(L1 cache,L2 cache).
    线程A,线程B在执行不同的核心上执行时,先将数据从内存读取到各自的CPU缓存,此时线程A对于数据的操作,对于线程B是不可见的。

    写操作对于读操作 是立即可见的

    2.原子性——线程切换导致

    高级程序语言中的一条语句往往对应这操作系统中的多条指令,
    线程在CPU的执行又是时间片轮转的方式执行,和可能在线程A将内存中的变量值V = 0 读取到寄存器中时,切换到线程B执行将V的值进行了更新 V=V+1
    此时V = 0 更新到内存,切换到A线程,对寄存器中 V=0 进行V++,在更新到内存 V=1。
    就会出现两个线程进行 V = V+1操作,结果却还是2的情况。

    一个或者多个操作在 CPU 执行的过程中不被中断的特性,称 为“原子性”

    3.有序性——编译器优化导致

    编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:“a=6;b=7;”
    编译器优化后可能变成“b=7;a=6;”,在这个例子中,编译器调整了语句的顺序,
    但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的 Bug。

    例如:双重校验单例模式

    public class Singleton {
        static Singleton instance;
        static Singleton getInstance(){
            if (instance == null) {
                synchronized(Singleton.class) {
                    if (instance == null)
                        instance = new Singleton();
                }
            }
            return instance;
        }
    }
    

    理想的执行过程:
    假设线程A、线程B同时调用getInstance()方法,

    • 1.假设A先得到执行权对Singleton.class进行加锁
    • 2.此时instance = null 创建Singleton对象
    • 3.获取到单例对象,释放锁
    • 4.线程B调用getInstance(),instance不为null,获取到线程A初始化的instance

    经过编译优化后对象的创建过程

    • 1.分配一块内存 M;
    • 2.将 M 的地址赋值给 instance 变量;
    • 3.最后在内存 M 上初始化 Singleton 对象。

    当执行2后发生线程切换,线程B得到的instance应用地址并未初始化对象就会产生空指针。

    Java内存模型如何解决并发问题

    Java内存模型针对于
    可见性问题 ——CPU缓存导致
    有序性问题 ——编译优化导致
    原子性问题 ——线程切换导致
    提供的方案是按需禁用CPU缓存及编译优化
    https://www.bilibili.com/video/av81008349

    具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及Happens-Before 规则

    volatile关键字:
    被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
    虽然禁用了缓存,所有线程都要从内存中读取,但是不具备互斥性(即同一时间只有一个线程可以执行)
    适用于一个线程写 另一个线程读 可以读到写入的最新值

    synchronized关键字:
    同步关键字,进行修饰的代码块或者方法,进行加锁,保证同时只有一个线程能够获取到锁。

    Happens-Before 约束 了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 HappensBefore 规则。
    核心原则:前面一个操作的结果对后续操作是可见的。
    六大规则:

    • 1.程序的顺序性规则
      指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意 操作。
      即程序前面对某个变量的修改一定是对后续操作可见的。
    • 2.volatile 变量规则
      指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。
    • 3.传递性
      指如果 A Happens-Before B,且 B Happens-Before C,那么 A HappensBefore C。
    • 4.管程中锁的规则
      管程是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。
      上一个加锁的线程中的操作释放锁后对下一个加锁线程可见。
    • 5.线程 start() 规则
      指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在 启动子线程 B 前的操作。
    • 6.线程 join() 规则
      指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能 够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。
    /**
     * 可见性问题
     * 1.主线程中启动子线程 获取a的值 判断进行循环
     * 2.因为循环中是空方法,a一直会读取CPU缓存中的值 一直为true
     * 3.所以执行结果是 打印false后 子线程还在死循环执行
     */
    public class Test {
        static boolean a = true;
    
        public static void main(String[] args) throws InterruptedException{
            new Thread(()->{
                while(a){
    
                }
            }).start();
            Thread.sleep(1000);
            a = false;
            System.out.println(a);
        }
    }
    
    /**
     * 解决可见性问题
     * volatile禁用CPU缓存
     * 并且使得线程的写操作对 读操作可见
     * 执行结果: 主线程打印false后 子线程读取到a的变更停止死循环
     */
    public class Test {
        static volatile boolean a = true;
    
        public static void main(String[] args) throws InterruptedException{
            new Thread(()->{
                while(a){
    
                }
            }).start();
            Thread.sleep(1000);
            a = false;
            System.out.println(a);
        }
    }
    
    /**
     * 原子性问题
     * a++操作对应
     * 1.读取a的值
     * 2.a+1
     * 3.赋值
     * 下面两个线程可能出现 线程1 读取a = 0
     * 切换到 线程2 读取到 a = 0
     * 线程2 执行 a++  更新 a的值 a=1
     * 再次回到线程1 寄存器中 a = 0,执行a+1,更新a=1
     * 两个线程的操作 却没有 a = 2
     *
     * 执行结果 <=20000
     **/
    public class Test {
        static  int a = 0;//此时使用volatile关键字修饰a 并没有保证原子性
    
        public static void main(String[] args) throws InterruptedException{
            for(int i=0;i<10000;i++){
                new Thread(()->{
                    a++;
                }).start();
                new Thread(()->{
                    a++;
                }).start();
            }
            Thread.sleep(2000);
            System.out.println(a);
        }
    }
    
    /**
     * 原子性问题
     * a++操作对应
     * 1.读取a的值
     * 2.a+1
     * 3.赋值
     * 
     * 解决原子性问题之使用原子类 
     * AtomicInteger 的 getAndAdd方法 在执行过程中 的读写是原子性的,不允许线程切换的
     * AtomicInteger内部基于volatile关键字
     * 
     * 执行结果:20000
     **/
    public class Test {
        static AtomicInteger a = new AtomicInteger(0);
        
        public static void main(String[] args) throws InterruptedException{
            for(int i=0;i<10000;i++){
                new Thread(()->{
                   a.getAndAdd(1);
                }).start();
                new Thread(()->{
                    a.getAndAdd(1);
                }).start();
            }
            Thread.sleep(2000);
            System.out.println(a);
        }
    }
    
    /**
     * 原子性问题
     * a++操作对应
     * 1.读取a的值
     * 2.a+1
     * 3.赋值
     *
     * 解决原子性问题之使用synchronized关键字
     * 线程1、线程2 同一个时间只有一个线程能执行同步代码块,通过互斥性来保证原子性
     * 最为重量级的实现
     *
     * 执行结果:20000
     **/
    public class Test {
        static int  a = 0;
    
        public static void main(String[] args) throws InterruptedException{
            for(int i=0;i<10000;i++){
                new Thread(()->{
                    synchronized (Test.class){
                        a++;
                    }
                }).start();
                new Thread(()->{
                    synchronized (Test.class){
                        a++;
                    }
                }).start();
            }
            Thread.sleep(2000);
            System.out.println(a);
        }
    
    
    }
    

    总结:

    • volatile 保证可见性,一个线程的写对另一个线程的读可见 无锁
    • AtomicInteger(原子类) 保证原子性,同时进行读写时的操作是原子性的. (基于CPU提供的CAS) 无锁
    • Synchrnoized 保证互斥性 最为重量级 同步代码块是原子性的,同时只有一个线程可以执行 加锁
  • 相关阅读:
    ASP.NET 4.0 与 Entity Framework 4第三篇使用Entity Framework调用存储过程
    雕虫无小技 JavaScript初学者的10个迷你技巧
    IE6下button随着文字的增多两边的内容边框也会增加的bug
    bigint ,int ,smallint ,tinyint 数据类型
    分享7个不错的jQuery游戏( 转)
    IE CSS Bug及解决方案参考手册
    利用CSS样式打印
    SQL 2005 弹出不允许对系统目录进行即席更新解决方法
    VS2010快捷键
    SQL2K,DTC错误:"该伙伴事务管理器已经禁止了它对远程/网络事务的支持"的解决办法
  • 原文地址:https://www.cnblogs.com/shinyrou/p/13264240.html
Copyright © 2020-2023  润新知