Day28 JMM内存模型

  • ForkJoinPool

    Fork/Join是一个并行计算框架,主要是用来支持分治模型的,Fork对应的是分治模型的任务分解,Join对应的是结果集的合并。核心思想是价格大任务拆成小任务求解结果集,然后将小结果集进行合并成最终结果,适用于可以采用分治策略的计算密集型任务。

    • ForkJoinPool使用

      • 构造器 有四个核心参数,分别用于控制线程池的并行数、工作线程的创建、异常处理和模式指定

        int parallelism: 指定并行级别,决定工作线程的数量,不设置默认使用Runtime.getRuntime().availableProcessors()

        ForkJoinWorkerThreadFactory factory:ForkJoinPool在创建线程时,会通过factory来创建,不指定默认使用DefaultForkJoinWorkerThreadFactory

        UncaughtExceptionHandler handler:指定异常处理器,当任务在运行中出错时,将由设定的处理器处理

        boolean asyncMode:设置队列的工作模式。当asyncMode为true时,将使用先进先出队列,而为false时则使用后进先出的模式。

      • 任务提交方式

        java
        复制代码
        // 异步执行 void execute(ForkJoinTask<?> task) void execute(Runnable task) // 等待获取结果 T invoke(ForkJoinTask<T> task) // 提交执行获取Future结果 ForkJoinTask<T> submit(ForkJoinTask<T> task) ForkJoinTask<T> submit(Callable<T> task) ForkJoinTask<T> submit(Runnable task) ForkJoinTask<T> submit(Runnable task, T result)

        ForkJoinTask是一个抽象类,定义了执行任务的基本接口,可以通过继承ForkJoinTask并重写compute方法自定义实现。通常情况下仅需继承它的子类 RecursiveAction:用于递归执行但不需要返回结果的任务; RecursiveTask :用于递归执行需要返回结果的任务。 CountedCompleter :在任务完成执行后会触发执行一个自定义的钩子函数

        compute(): 业务执行逻辑 fork(): 用于向当前任务所运行的线程池中提交任务 join(): 获取认为执行结果

        java
        复制代码
        public class Fibonacci extends RecursiveTask<Integer> { final int n; Fibonacci(int n) { this.n = n; } /** * 重写RecursiveTask的compute()方法 * @return */ protected Integer compute() { if (n <= 1) return n; Fibonacci f1 = new Fibonacci(n - 1); //提交任务 f1.fork(); Fibonacci f2 = new Fibonacci(n - 2); //合并结果 return f2.compute() + f1.join(); } public static void main(String[] args) { //构建forkjoin线程池 ForkJoinPool pool = new ForkJoinPool(); Fibonacci task = new Fibonacci(10); //提交任务并一直阻塞直到任务 执行完成返回合并结果。 int result = pool.invoke(task); System.out.println(result); } }
      • 处理阻塞任务

        使用ForkJoinPool处理阻塞型任务时需要注意:

        1. 防止线程饥饿。当一个线程正在执行一个阻塞型任务时,会一直等待任务完成,如果没有其他线程可以窃取任务,该线程将会一直阻塞
        2. 使用特定线程池。为了最大程度地利用ForkJoinPool的性能,可以使用专门的线程池来处理阻塞型任务,这些线程不会被ForkJoinPool的窃取机制所影响
        3. 不要阻塞工作线程。如果在ForkJoinPool中使用阻塞型任务,需要确保这些任务不会阻塞工作线程,否则会导致整个线程池的性能下降。可以将阻塞型任务提交到一个专门的线程池中,或者使用CompletableFuture等异步编程工具来处理阻塞型任务。
        java
        复制代码
        public class BlockingTaskDemo { public static void main(String[] args) { //构建一个forkjoin线程池 ForkJoinPool pool = new ForkJoinPool(); //创建一个异步任务,并将其提交到ForkJoinPool中执行 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { // 模拟一个耗时的任务 TimeUnit.SECONDS.sleep(5); return "Hello, world!"; } catch (InterruptedException e) { e.printStackTrace(); return null; } }, pool); try { // 等待任务完成,并获取结果 String result = future.get(); System.out.println(result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } finally { //关闭ForkJoinPool,释放资源 pool.shutdown(); } } }
    • 工作原理

      • ForkJoinPool的任务会被内部存储了一个WorkQueue数组,提交给ForkJoinPool的任务会被分配到指定的WorkQueue上执行
      • 每个WorkQueue内部维护了一个ForkJoinTask数组用来存储待执行的任务,以及一个独立的ForkJoinWorkerThread用来真正执行任务
      • 当有某个线程空闲时,会去窃取其他繁忙线程的任务拿过来执行

      image.png

      ForkJoinWorkerThread

      ForkJoinWorkerThread是ForkJoinPool中的一个专门用于执行任务的线程。当一个ForkJoinWorkerThread被创建时,它会自动注册一个WorkQueue到ForkJoinPool中。这个WorkQueue是该线程专门用于存储自己的任务的队列,只能出现在WorkQueues[]的奇数位。 ForkJoinWorkerThread工作线程启动后就会扫描偷取任务执行,另外当其在 ForkJoinTask#join() 等待返回结果时如果被 ForkJoinPool 线程池发现其任务队列为空或者已经将当前任务执行完毕,也会通过工作窃取算法从其他任务队列中获取任务分配到其任务队列中并执行。

      image.png

      WorkQueue

      WorkQueue是一个双端队列,用于存储工作线程自己的任务。每个工作线程都会维护一个本地的WorkQueue,并且优先执行本地队列中的任务。当本地队列中的任务执行完毕后,工作线程会尝试从其他线程的WorkQueue中窃取任务。

      WorkQueue 任务队列其实也分为了两种类型,一种是外部提交进来的任务所占用的队列,其在任务队列数组中的数组下标为偶数;另一种是属于工作线程私有的任务队列,保存大任务 fork 分解出来的任务,其在任务队列数组中的数组下标为奇数。

      image.png

      工作窃取

      就是允许空闲线程从繁忙线程的双端队列中窃取任务。默认情况下,工作线程从它自己的双端队列的头部获取任务。当自己的任务为空时,线程会从其他繁忙线程双端队列的尾部中获取任务。最大限度地减少了线程竞争任务的可能性,提高工作效率

      image.png

    • 执行流程

      image.png

JMM内存模型

是JVM定义的一套规范,规定了多线程程序中的变量如何在内存中存储和传递,约定了线程何时从主内存读取数据、何时把数据写回主内存。其核心目标是确保多线程环境下的可见性、有序性和原子性

  1. 可见性:一个线程对变量的修改能被其他线程看到,volatile关键字就是用来保证可见性的,强制线程每次读写都直接跟主内存交互
  2. 有序性:线程执行操作的顺序。JMM允许指令重排来提高性能,但通过happens-before关系保证跨线程的有序性
  3. 原子性:操作不可分割,执行过程不会被打断。synchronized关键字可以保证代码块的原子性
  • JMM的抽象内存模型组成

    • 主内存存放共享变量,所有线程都能访问

    • 每个线程都有自己的本地内存,存放共享变量的副本

    • 线程对变量的操作必须在本地内存中进行,不能直接操作主内存

    • 线程间变量传递必须通过主内存完成

    image.png

  • Happens-Before

    定义了某个操作的结果对另一个操作可见,用于确定两个操作之间的执行顺序,确保多线程程序的正确性和一致性,底层主要是利用内存屏障来实现的。

    Happens-Before规则包括:

    只是说呈现给开发者的规则是如此,但并不代表会严格按此执行

    1. 程序顺序规则:在一个线程内,按照代码顺序,前面的操作→Happens-Before→后面的操作

    2. 监视器锁规则:对一个锁的解锁操作Happens-Before后续对这个锁的加锁操作

    3. volatile变量规则:对一个volatile变量的写操作Happens-Before后续对这个变量的读操作

    4. 传递规则:如果A→Happens-Before→B,B→Happens-Before→C,那么A→Happens-Before→C

    5. 线程启动规则:对线程的Thread.start()调用Happens-Before该线程的每一个操作

    6. 线程终止规则:线程中的所有操作 Happens-Before 其他线程检测到线程已终止,通过Thread.join,Thread.isAlive等

    7. 线程中断规则:对线程的interrupt()调用Happens-Before检测到中断事件

    8. 对象终结规则:一个对象的初始化完成Happens-Before它的finalize()方法开始。

    image.png

  • 内存屏障

    是一种CPU指令,用于禁止特定类型的指令重排序,JVM在编译时会根据关键字(如volatile)插入相应的内存屏障

    • LoadLoad屏障:禁止读操作重排序,按序读
    • StoreStore:禁止写操作重排序,按序写
    • LoadStore:禁止读操作与后面的写操作重排序
    • StoreLoad:禁止写操作与后面的读操作重排序
  • Volatile的语义

    • 保证可见性,写操作立即刷新到主内存,读操作从主内存获取最新值
    • 禁止指令重排序,编译器和CPU不会把volatile变量的读写操作和其他操作乱序执行

    但不能保证原子性,i++这种复合操作(读i→加1→写回i),对于单次读或单次写是可以保证原子性的

    image.png

    不能保证原子性的场景:

    • i++;读i→加1→写回i
    • i = i + 1;读i→加1→写回i
    • long x;x = 1L;long类型64位,在32位系统会被拆成两部分,不保证原子性,除非在64位系统或加volatile关键字(JVM规定,对long/double之外的所有基本类型的单次操作都是原子的,加上volatile之后读写也是原子的)
    • list.add(item);即使list是volatile,add()是方法调用,非原子;只保证引用本身的原子性和可见性,不保证内部的线程安全
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP