多线程—Java内存模型与线程

Java内存模型与线程

一、概述

由于计算机的 CPU 运算速度与它的存储和通信子系统的速度差距过大,大量的时间都花费在磁盘I/O、网络通信或者数据库访问上。如果不希望 CPU 大部分时间处于等待其他资源的闲置状态,想要充分利用 CPU 的高速运算性能,就会让计算机同时处理多项任务,“压榨” CPU的运算性能。

除了充分利用 CPU 的性能之外,一个服务端要同时对多个客户端提供服务,则是另一个更具体地并发场景。对于计算量相同地任务,程序线程并发协调地有条不紊,效率自然就会越高;反之,线程之间频繁争用数据,互相阻塞甚至死锁,将会大大的降低程序的并发能力

服务端的应用是 Java 语言最擅长的领域,不过如何写好并发程序却又是服务端程序开发的难点,幸好 Java 语言和 JVM 提供了许多工具,把并发编程的门槛降低了不少。各种中间件服务器、各类框架也都在尽可能地封装、隐藏线程并发细节,让我们程序员有更多地精力处理业务逻辑。

但是作为程序员,我们不能依靠它们处理所有的并发情况,否则一旦出现问题,我们将无法排查和进行性能优化,所以了解并发的内幕仍然是作为程序员不可缺少的过程

二、硬件效率与一致性

在讲解 JVM 并发相关知识之前,我们需要了解物理机中的并发问题处理

为了 “ 更充分的利用 CPU 的性能”,所以 “ 让计算机并发的执行若干个运算任务 ”,但是 CPU 并发执行时的安全性保证极其复杂

1、高速缓存

程序的运行并不能只依靠 CPU 计算,至少还要与内存交互,如读取要运算的数据、存储运算结果等,最起码这个I/O操作就是很难消除的。由于 CPU 的速度是内存的百倍、是硬盘的百万倍,为了加快 I/O 速度,所以现在都引入了高速缓存(Cache)来作为内存与处理器之间的缓冲——将运算需要的数据复制到缓存中,让运算能够快速运行,当运算结束后,再从缓存同步回内存中,这样 CPU 就不必等待缓慢的内存读写了

2、缓存一致性

高速缓存很好的解决了 CPU 与内存之间交互的速度矛盾,但是也带来了另外一个严重的问题——缓存一致性:在现代计算机的多核 CPU 系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存;当多个处理器的运算都涉及到同一内存区域时,将导致各自的缓存数据不一致,出现运算错误。为了解决缓存间一致性的问题,出现了缓存一致性协议,这类协议有很多,我们只要了解最常见的 Intel 使用的 MESI 协议即可

硬件间的交互架构如下图:

在这里插入图片描述

3、乱序执行优化

除了增加高速缓存之外,为了更充分的利用 CPU 的运算能力,处理器可能会对输入代码进行乱序执行的优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序和输入代码的顺序一致,因此如果一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能由代码顺序来保证

扫描二维码关注公众号,回复: 11868810 查看本文章

由内存屏障来保证乱序执行优化后的结果,仍然是正确的

CPU 的乱序执行优化,对应在 JVM 中就是指令重排序;在高并发情况下,指令重排序可能会产生数据不一致现象,为了防止指令重排序,在 Java 中使用Volatile 和 Synchronized(锁机制)来避免指令重排序

Volatile 和 Sync 底层使用内存屏障来避免指令重排序

三、Java内存模型

《Java虚拟机规范》曾试图定义一种 “ Java 内存模型”,来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java 程序在各种操作系统下都能达到一致的内存访问效果;经过了长时间的验证和修补,直到 JDK5 (JSR-133)发布后,Java内存模型才终于成熟、完善起来了

1、主内存与工作内存

Java 内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节

这里说的变量是指实例字段、静态字段和构成数组对象的元素(存储在Heap、方法区中的变量——所有线程共享),但是并不包含局部变量与方法参数(存储在 JVM Stack,每个线程独占),因为后者是线程私有的,不会被共享,自然就不会存在并发竞争问题

Java内存模型规定了所有的变量都存储在主内存中(类比于物理硬件的主内存),每条线程还有各自的工作内存(类比于物理硬件的高速缓存)。线程的工作内存中保存了该线程使用的变量的内存副本,所有线程对于变量的操作(读取、赋值等)都要在各自的工作内存中进行,而不能直接读取主内存中的数据

不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要主内存来完成,线程、主内存、工作内存间的关系如下图:

在这里插入图片描述

这里所讲的主内存、工作内存与在 JVM 中所讲的运行时数据区中的堆、栈、方法区等并不是同一个层次的对内存的划分,这两者并没有强关联性。如果一定要勉强对应起来,则主内存主要对应于 堆(Heap) 中的对象实例数据部分,工作内存则对应虚拟机栈(JVM Stack)

2、内存间交互操作

关于主内存与工作内存之间具体的交互协议(相当于缓存一致性协议——MESI),即一个变量如何从主内存拷贝到工作内存运算、运算完成后修改过的变量如何从工作内存同步回主内存的实现细节,Java内存模型定义了以下 8 种操作来完成。JVM 保证以下 8 种操作都是原子性的、不可再分的(即线程安全):

  • lock(锁定):作用于主内存的变量,它会把一个变量标识为一条线程独占的状态
  • unlocck(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load操作使用
  • write(写入):作用于主内存的变量,它把store操作从工作内存中获取到的变量的值,放到主内存变量中
  • load(载入):作用于工作内存的变量,它把read动作从主内存中获取到的变量值放到工作内存的变量副本中
  • store(存储):作用于工作内存的变量,它把工作内存中的值传递给主内存中,以便随后的write操作使用
  • use(使用):作用于工作内存的变量,把工作内存中的值传递给执行引擎
  • assign(赋值):作用于工作内存的变量,把从执行引擎接收到的值赋值给工作内存的变量

以上 8 中原子性指令是保证在多线程并发情况下数据一致性的基础,保证线程安全的机制、锁机制等,最底层的实现都要依托于这 8 种原子性指令

如果要把一个变量从主内存拷贝到工作内存,那就要顺序执行 read 和 load操作,如果要把修改过的变量值从工作内存同步回主内存,就要顺序执行 store 和 write 操作;除此之外,执行以上 8 种基本操作必须满足一定规则,重点规则概述如下:

  • 一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁

    锁的可重入特性的底层实现机制;lock 次数记录在对象头部(mark word)

  • 如果一个变量事先没有被 lock 锁定,那就不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量

    锁机制的互斥性的根本保证——不允许去 unlock 一个被其他线程锁定的变量

  • 如果对一个变量执行 lock 操作,那么将会情况工作内存中此变量的值,执行引擎使用这个变量前,需要重新 load;如果对一个变量执行 unlock 操作之前,必须要把此变量同步回主内存中(store、write操作)

    多线程下数据可见性、一致性的保证

以上 8 种操作保证了 Java 程序中哪些内存访问操作在并发下是安全的,但是 8 种操作很繁琐,所以官方又将 Java 内存模型的操作简化为 lock、unlock、read 和 write 四种,但是我们仍然不会使用指令来进行并发程序的编写,只要理解 Java 内存模型中,线程安全(原子性)的保证是通过以上指令的配合使用来完成的即可

在实际中我们一般通过与以上定义的一个等效判断原则——先行发生原则(happends-Before),用来确定一个操作在并发环境下是否安全

3、原子性、可见性与有序性

通过上面的学习我们知道,Java 内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征建立的,我们来看一下哪些操作实现了这三个特征

1)原子性

由 Java 内存模型直接保证的原子性变量操作包括read、write、load、store、use 和 assign 这六个,我们可以大致认为——基本数据类型的访问、读写都是具备原子性的

如果应用场景需要一个更大范围的保证(其实经常会遇到),就需要使用 lock 和 unlock 这两个原子性操作,JVM并未把这两个操作开放给我们,而是提供了更高层次的字节码指令 monitorentermonitorexit 来隐式使用这两个操作。这两个字节码指令反映在 Java 代码中就是同步块——Synchronized 关键字,因此在 Synchronized 块之间的操作都具备原子性

2)可见性

可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改

Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存帅新变量值这种依赖主内存的方式来实现可见性的,无论是普通变量还是 volatile 变量都是如此

普通变量和 Volatile 变量的区别在于: volatile 的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新,保证每次使用数据一定都是最新的

因此我们可以说 volatile 保证了多线程操作时变量的可见性,而普通变量并不能做到这一点

除了 volatile 之外,Java 还有两个关键字能实现可见性,它们是 Synchronized 和 final。同步块的可见性是由 “ 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(store、write操作)” 这条规则获得的

3)有序性

Java 程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的

前半句是指 “ 线程内似表现为串行的语义 ”,后半句是指 “ 指令重排序 ” 现象和 “ 工作内存与主内存同步延迟 ” 现象

Java 语言提供了 volatile 和 synchronized 两个操作来保证线程之间操作的有序性:

  • volatile:volatile 关键字本身就包含了禁止指令重排序的语义
  • synchronized :synchronized 关键字是由 “ 一个变量在同一时刻只允许一条线程对其进行 lock 操作 ” 这条规则决定了持有同一个锁的两个同步块只能串行的进入

我们可以发现 **synchronized 关键字能保证上面三种特性的一个解决方案,比较万能,这也造成了synchronized 关键字的滥用,为了提高synchronized **的性能,JVM 进行了锁优化,后面会讲

4、先行发生原则

如果 Java 语言中所有的有序性都靠 volatile 和 synchronized 来保证,则代码的编写将十分繁琐,但是我们在编写并发代码的时候,并没有察觉这一点,这是因为 Java 语言中有一个 “ 先行发生 ” 原则——它是判断数据是否存在竞争,线程是否安全的非常有用的手段

“ 先行发生 ” 是指在 Java 内存模型中定义的两项操作之间的偏序关系,比如说操作 A 先行发生于操作 B ,则在操作 B 执行之前,操作 A 产生的全部影响都能被操作 B 观察到,“ 影响 ” 包括修改了主内存中共享变量的值、发送了消息、调用了方法等

先行发生原则示例参考:《深入理解 Java 虚拟机》 P453

时间先后顺序与先行发生原则之间基本没有因果关系,所以我们衡量并发问题的时候不要受时间顺序的干扰,一切必须以先行发生原则为准

四、Java与线程

1、线程的实现

线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度

主流的操作系统都提供了线程实现,Java 语言则提供了在不同硬件和操作系统平台下对线程操作的统一处理——每个已经调用过 start() 方法且还未结束的 java.lang.Thread 类的实例就代表着一个线程

在本小节我们先把 Java 的技术背景放下,以一个通用的应用程序的角度来看看线程是如何实现的

1)内核线程实现

使用内核线程实现的方式也被称为 1:1 实现

内核线程(Kernel-Level Thread KLT)就是直接由操作系统内核(Kernel)支持的线程,这种内核线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上

操作系统一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process LWP),轻量级进程就是我们通常意义上所讲的线程,每个轻量级进程都由一个内核线程支持

这种轻量级进程与内核线程的 1:1 的线程实现方式被称作一对一的内核线程实现,如下图
在这里插入图片描述

这种一对一的内核线程实现优缺点如下:

  • 优点:由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使其中某一个轻量级进程在系统调用中被阻塞了,也不会影响整个进程继续工作
  • 缺点:由于是基于内核线程实现,所以各自线程操作都需要进行系统调用,系统调用需要在用户态和内核态中来回切换,线程调度代价很高

2)用户线程实现

使用用户线程实现的方式又被称为 1:N 实现

用户线程(User Thread,UT)指的是完全完全建立在用户空间的线程库上,系统内核不能感知到线程的存在以及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中实现,不需要内核的帮助

如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快且低消耗的,也能够支持更大规模的线程数量,部分高性能数据库的多线程就是由用户线程来实现的

这种进程与用户线程之间 1:N 的关系称为一对多的线程模型,如下图所示:

在这里插入图片描述

这种一对多的用户线程实现优缺点如下:

  • 优点:不需要系统内核支援,省去了用户态到内核态切换,节省系统资源

  • 缺点:由于没有系统内核支援,所有的操作都由用户程序自己处理。线程的创建、销毁、切换和调度都是用户必须考虑的问题,而且操作系统只把资源分配到进程,则 “ 线程阻塞处理 ”、“ 如何将线程映射到处理器上 ” 这类问题解决起来相当麻烦

    由于用户线程的实现相当麻烦,所以都不倾向于使用用户线程,Java、Ruby曾经采用过用户线程,后来又放弃使用。而最近的 Golang 却是使用的用户线程

3)混合实现

这种将内核线程与用户线程一起使用的线程实现方式,被称为 N:M 方式

在这种混合实现下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换和销毁等操作依然代价较低,并且可以支持大规模的用户线程并发;而操作系统支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度及处理器映射功能,并且用户线程的系统调用要通过轻量级进程来完成,大大降低了整个进程被完全阻塞的风险

这种多对多的线程模型,如下图所示:

在这里插入图片描述

2、Java 线程的实现

主流JVM 的线程模型普遍使用基于操作系统的原生线程模型来实现,即采用 1:1 的线程模型

以 HotSpot 虚拟机为例,它的每一个线程都是直接映射到一个操作系统原生线程来实现的,而且中间没有额外的间接结构,所以 JVM 自己是不会去干涉线程调度的(可以通过优先级提供建议),全权交由底层的操作系统去处理,所以何时冻结与唤醒线程、应该给线程分配多少处理器执行时间、应该把线程映射到哪个处理器核心去执行等等,都是由操作系统完成的,也都是由操作系统来全权决定的

线程模型只对线程的并发规模和操作成本产生影响,对 Java 程序的编码和运行过程来说,这些差异都是完成透明的

3、线程调度算法

线程调度是指系统为线程分配处理器使用权的过程,调度的方式主要有两种:

  • 协同式线程调度
  • 抢占式线程调度(Java使用)

1)协同式线程调度

使用了协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知操作系统切换到另一个线程上去

优点:实现简单;而且由于线程要把自己的事情干完之后才会进行线程切换,切换操作对于线程本身而言是透明的,不会有并发线程同步的问题

缺点:线程的执行时间不受操作系统的控制,如果一个线程的代码编写有问题,一直不告知操作系统进行线程切换,那么程序就会一直阻塞在那里

协同式线程调度实现多线程多任务相当不稳定,只要有一个进行坚持不让出 CPU 执行时间,就会导致整个系统的崩溃

2)抢占式线程调度

使用了抢占式调度的多线程系统,每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定,线程本身不能主动的获取执行时间。

Java就是使用的线程调度就是抢占式调度

优点:线程的执行时间是系统可控的,也不会有一个线程导致整个进程甚至系统阻塞的问题;当一个进程出现问题,我们还可以通过任务管理器把这个进程杀掉,而不至于导致整个系统崩溃

我们可以给线程设置优先级,可以 “建议” 操作系统给哪些线程多分配一些时间,另外的线程可以少分配一些时间

但是线程优先级并不是一个稳定的调节手段,因为JVM的Java线程是被映射到系统的原生线程上来实现的,所以线程调度最终还是由操作系统说了算

4、线程的创建

创建线程一般来说有两种方法:

  1. 从Thread类继承,重写run()方法
  2. 重写Runnable接口,重写run()方法

面试官如果告诉你有三种,问你有哪三种?第三种应该是通过线程池创建——Executors.newCachedThrad,这种说法严格来说其实仍然是上面的方式

线程创建的示例代码:

public class T02_HowToCreateThread {
    
    
    //继承子Thread类
    static class MyThread extends Thread {
    
    
        @Override
        public void run() {
    
    
            System.out.println("Hello MyThread!");
        }
    }

    //重写Runnable接口
    static class MyRun implements Runnable {
    
    
        @Override
        public void run() {
    
    
            System.out.println("Hello MyRun!");
        }
    }

    public static void main(String[] args) {
    
    
        new MyThread().start();
        new Thread(new MyRun()).start();
    }

}

5、线程的启动

线程的启动方式也可以分为两种:

  1. run() 方法启动线程:程序只有一条执行路径,并不能实现并发操作
  2. start() 方法启动线程:程序真真正正另外新起了一条执行路径,实现了并发操作

线程启动的示例代码:

public class T01_WhatIsThread {
    
    
    private static class T1 extends Thread {
    
    
        @Override
        public void run() {
    
    
           for(int i=0; i<10; i++) {
    
    
               try {
    
    
                   TimeUnit.MICROSECONDS.sleep(1);
               } catch (InterruptedException e) {
    
    
                   e.printStackTrace();
               }
               System.out.println("T1");
           }
        }
    }

    public static void main(String[] args) {
    
    
        //顺序输出 10次t1之后,才会输出 main 
        new T1().run();
        
        //交替输出 t1 和 main
        new T1().start();
        
        for(int i=0; i<10; i++) {
    
    
            try {
    
    
                TimeUnit.MICROSECONDS.sleep(1);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("main");
        }

    }
}

对上面程序的分析:

new T1().run(),在程序里面只有1条执行路径,在t1 输出完之后才会继续输出 main

在这里插入图片描述

new T1().start(),在程序里面新建一条执行路径,t1 和 main 交替输出

在这里插入图片描述

总结:只有使用 start() 方法启动的线程,才是真真正正的实现了并发操作

6、线程状态转换

Java语言定义了六种线程任意一个时间点中,一个线程只能有且只有其中的一种状态,并且可以通过特定的方法在不同状态之间切换

在这里插入图片描述

对应上图 6 中状态分别是:

  • 新建(New):创建后尚未启动的线程

  • 运行(Runnable):包括操作系统线程状态中的 Running(分配到时间片,正在运行) 和 Ready(等待操作系统分配时间片)

  • 无限期等待(Waiting):处于这种状态的线程不会被分配 CPU 执行时间,它们必须被其他线程显式唤醒才会被 CPU 调度。以下方法可能会让线程进入无限期的等待状态:

    • 没有设置 Timeout 参数(超时时间)的 Object::wait() 方法
    • 没有设置 Timeout 参数(超时时间)的 Thread::join() 方法
    • LockSupport::park() 方法
  • 限期等待(Timed Waiting):处于这种状态的线程也不会被分配 CPU 执行时间,但是无需由其他线程显式唤醒,而是在一定时间后由系统自动唤醒。以下方法会让线程进入限期等待状态:

    • Thread::sleep() 方法
    • 设置了 Timeout 参数(超时时间)的 Object::wait() 方法
    • 设置了 Timeout 参数(超时时间)的 Thread::join() 方法
    • LockSupport::parkNanos() 方法
    • LockSupport::parkUntil() 方法
  • 阻塞(Blocked):线程被阻塞了,但是处于此阶段的线程仍然会被 CPU 调度,当处于此阶段的线程数量很多,就会导致 CPU 把大量时间浪费在线程间的切换上,在程序等待进入同步区域的时候,线程就处于阻塞状态

    线程调度切换的开销——Java线程是被映射掉内核线程,内核线程的调度成本主要来自于用户态和内核态之间的切换,而这两种状态切换的开销主要来自于响应中断、保护和恢复现场的成本

    • 保护和记录现场:以程序员的角度来说就是记录方法调用过程中的各种局部变量和资源;以线程角度来看,是JVM Stack存储的各类信息;以操作系统的角度来看,则是存储在内存、缓存和寄存器种的数值
    • 恢复现场:恢复到线程被挂起时的程序运行到哪一步、各个变量的数值等等——相当于游戏保存了一个快照,然后下次直接从快照处开始运行

    “阻塞状态” 和 “等待状态” 的区别是:

    • “阻塞状态” 在等待获取一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生
    • “等待状态” 则是等待一段时间,或者有唤醒动作的发生
  • 结束(Terminated):已终止运行的的线程状态,线程已经结束执行

五、展望未来

1、内核线程的局限

在 Java 时代的早期,Java 语言抽象出来隐藏了各种操作系统线程差异性的统一线程接口,语言和框架已经屏蔽了相当多同步和并发的复杂性,这曾经是它领先于其他编程语言的一大优势。时至今日,这种便捷的并发编程方式和同步的机制依然在有效的运行着,但是在某些场景下,已经逐渐展露疲态

由于 Web 服务的请求数量和复杂度都远超往日,所以现在 B/S 架构不断进行服务细分,这种服务细分的架构在减少单个服务复杂度、增加复用性的同时,不可避免的增加了服务的数量,缩短了服务的响应时间。这要求每个服务都必须在极短的时间内完成计算;也要求每一个服务的提供者能够同时处理更大规模的请求,这样才不会出现请求由于某个服务阻塞而出现等待

Java 目前的并发机制就逐渐无法满足上述架构,1:1 的线程模型仍然是如今Java虚拟机线程实现的主流选择,但是这种映射到操作系统上的线程的天然缺陷是切换、调度成本高昂,系统能够容纳的线程数量也有限。以前的单体应用允许处理一个请求花费很长时间,具有这种线程切换的成本是无伤大雅的,但是在目前每个请求本身的执行时间变得很短、请求数量很多的情况下,用户线程切换的开销甚至要超过用于计算本身的开销,造成严重的性能浪费

2、协程的复苏

由于内核线程调度切换起来成本很高(响应中断、保护和恢复现场),这一部分是操作系统的黑箱操作,我们程序员无法操作。但是如果把保护、恢复现场及调度的工作由操作系统交道程序员手上,那么我们就可以玩出很多花样来缩短这些开销,由此诞生了协程

协程的主要优势在于轻量,比传统的内核线程要轻量的多。在 Hotspot 的线程栈默认容量是 1M,而一个协程的栈通常在几百个字节到几 KB 之间,所以 JVM 中线程池容量达到两百就不算小了,而很多支持协程的应用中,同时并存的协程数量可达数十万

3、纤程

在最新的 JDK 版本中,Java 语言引入了与现在线程模型平行的新并发编程机制——纤程

这个新功能并不是为了取代当前的基于操作系统的线程实现,而是会有两个并发编程模型在 JVM 中并存,可以在程序中同时使用,纤程在高 QPS 测试下的性能极优

在纤程并发模型下,一段使用纤程并发的代码会被分为两个部分——执行过程和调度器

  • 执行过程主要用于维护执行现场,保护、恢复上下文状态
  • 调度器主要负责编排所有要执行的代码的顺序

六、总结

在本篇文章中,我们了解了 Java 内存模型的结构及操作,了解原子性、可见性和有序性在 Java 内存模型中的体现,介绍了先行发生原则的规则和使用,了解了线程在 Java 语言中是如何实现的,以及未来并发机制的发展趋势

这一篇文章介绍了 Java 的 “ 高效并发 ” 中的如何实现 “ 并发 ”,后面我们将重点关注 JVM 如何实现 “ 高效 ”,JVM 对于我们编写的并发代码提供了怎样的优化手段

参考:《深入理解 Java 虚拟机》第 12 章

猜你喜欢

转载自blog.csdn.net/qq_42583242/article/details/108045617