juc--并发编程的核心问题总结③

接上篇 juc–并发编程的核心问题总结②

一、异步回调

1. 什么是异步回调

我们平时最常见的是同步回调,同步回调是会阻塞的,单个的线程需要等待结果的返回才能继续执行。
在这里插入图片描述

假设有两个任务A和B,A任务中需要使用B任务计算成果,有两种方法实现:

  1. A和B在同一个线程中顺序执行。即先执行B,得到返回结果之后再执行A。

  2. A和B并行执行。当A需要B的计算结果时如果B还没有执行完,A可以先做其他的工作,避免阻塞,过一段时间后再询问一次B。

我们可以直接在A中写一个方法对B处理完的结果进行处理,然后B处理完之后调用A这个方法。

在这里插入图片描述

2. java实现异步回调

通过Future接口实现

Future类存在于JDK的concurrent包中,主要用途是接收Java的异步线程计算返回的结果。
java.util.concurrent.Future

  • 异步执行
  • 成功回调
  • 失败回调
public class FutureDemo {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        // 发送请求 Void:没有返回结果
        CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
    
    
            try {
    
    
                // 模仿耗时的操作
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"runAsync-》Void");
        });

        System.out.println(Thread.currentThread().getName()+"线程");
    }
}

只有main线程执行了
在这里插入图片描述

public class FutureDemo {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        // 发送请求 Void:没有返回结果
        CompletableFuture<Void> future = CompletableFuture.runAsync(()->{
    
    
            try {
    
    
                // 模仿耗时的操作
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"runAsync-》Void");
        });

        System.out.println(Thread.currentThread().getName()+"线程");

        //获取阻塞执行结果
        Void result = future.get();
    }
}

在这里插入图片描述

// 有返回值的异步回调 supplyAsync
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(()->{
    
    
     System.out.println(Thread.currentThread().getName()+"runAsync-》Integer");
     //成功则返回1024
     return 1024;
 });

System.out.println(future.whenComplete((t, u) -> {
    
    
    System.out.println("t:" + t + "  u:" + u);
}).exceptionally((e) -> {
    
    
    System.out.println(e.getMessage());
    //失败则返回233
    return 233;
}).get());

在这里插入图片描述
添加一行错误代码:

// 有返回值的异步回调 supplyAsync
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(()->{
    
    
    System.out.println(Thread.currentThread().getName()+"runAsync-》Integer");
    int i = 2/0;
    //成功则返回1024
    return 1024;
});

System.out.println(future.whenComplete((t, u) -> {
    
    
    System.out.println("t:" + t + "  u:" + u);
}).exceptionally((e) -> {
    
    
    System.out.println(e.getMessage());
    //失败则返回233
    return 233;
}).get());
}

在这里插入图片描述

二、JMM

JMM-----Java内存模型

在java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。
局部变量、方法定义参数、异常处理器参数不会在线程间共享,它们不会有内存可见性的问题,也不收内存模型的影响。

JMM定义了线程和主内存间的抽象关系:线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存,本地内存存储了该线程读/写共享变量的副本。本地内存是JMM的一个抽象概念并不实际存在。

JMM的约定:

  1. 线程解锁前,必须把共享变量立刻刷新回主存。
    在这里插入图片描述
  2. 线程加锁前,必须读取主存中的最新值到工作内存中。
  3. 加锁和解锁是同一把锁。

在这里插入图片描述
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

  • lock(锁定)︰作用于主内存的变量,把一个变量标识为线程独占状态。
  • unlock(解锁)︰作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入)︰作用于工作内存的变量,它把read操作从主存中变量放入工作内存中。
  • use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令。
  • assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中。
  • store (存储)︰作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
  • write(写入)∶作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

JMM对这八种指令的使用,制定了如下规则:

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write。
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存。
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存。
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作。
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁。
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存。

若线程A修改了共享变量的值,但线程B的本地内存中该变量仍然是旧值,即线程B不能及时可见。怎样让B知道主内存中值发生了变化呢?使用Volatile。

三*、Volatile

Volatile是java虚拟机提供的轻量级的同步机制。

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

详见 深入理解java中volatile的特性

四、CAS详解

什么是CAS?
CAS是Compare and Swap的缩写,就是比较并替换。

CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

假设存在多个线程执行CAS操作并且CAS的步骤很多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?
答案是否定的,因为CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

public class CASDemo {
    
    
    public static void main(String[] args) {
    
    
        AtomicInteger atomicInteger = new AtomicInteger(999);
        //compareAndSet(int expect, int update)
        System.out.println(atomicInteger.compareAndSet(999, 666));//true

        System.out.println(atomicInteger.get());//666
    }
}

ABA问题:
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那就能说明它的值没有被改变过吗?不能,因为在这段时间里它的值可能被修改成了一个值B,后来又被修改回来了A,那CAS就会误以为它从来就没有变过。

JUC包为了解决这个问题,提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。不过目前这个类比较鸡肋。

CAS的缺点:

  1. ABA问题。
  2. CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
  3. CAS底层是自旋锁,在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

猜你喜欢

转载自blog.csdn.net/myjess/article/details/121392658