Android进阶之路Android开发Android开发经验谈

一夜搞懂 | Java 内存模型与线程

2020-04-09  本文已影响0人  许朋友爱玩

前言

本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍:

我的 GIthub 博客

学习导图

学习导图

一.为什么要学习内存模型与线程?

并发处理的广泛应用是 Amdah1 定律代替摩尔定律成为计算机性能发展源动力的根本原因,也是人类压制计算机运算能力的最有力武器

线程通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递

线程同步是指程序用于控制不同线程之间操作发生相对顺序的机制。

Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果你想设计表现良好的并发程序,理解 Java 内存模型是非常重要的Java 内存模型规定了如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

二.核心知识点归纳

2.1 概述

Q1:多任务处理的必要性

通过指标 TPSTransactions Per Second)可衡量一个服务性能的高低好坏,它表示每秒服务端平均能响应的请求总数,进而体现出程序的并发能力

Q2:硬件的效率与一致性

为了更好的理解 Java 内存模型,先理解物理计算机中的并发问题,两者有很高的可比性

为了平衡内存交互速度与处理器的运算速度之间几个数量级的差距,引入一层高速缓存Cache)来作为内存与处理器之间的缓冲:

  • 出现问题:引入高速缓存虽解决了处理器与内存速度之间的矛盾,但是其引入了一个新的问题——缓存一致性
  • 解决办法:需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作
缓存与协议的结构图

内存模型可以理解为:在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象

2.2 Java 内存模型

之前笔者在 进阶之路 | 奇妙的Thread之旅中简要介绍过 Java 内存模型,相信看过的读者都有一些印象

2.2.1 设计目的

屏蔽掉各种硬件和操作系统的内存访问差异,实现 Java 程序在各种平台下都能达到一致的内存访问效果

2.2.2 设计方法

通过定义程序中各个变量访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节

注意:这里的变量与 Java 中说的变量不同,而指的是实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数(其存放于局部变量表中,而局部变量表在 JVM 栈中),因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。

2.2.3 模型结构

内存模型

注意:这里的主内存、工作内存与 一文洞悉JVM内存管理机制 说的 Java 内存区域中的 Java 堆、栈、方法区等并不是同一个层次的内存划分

注意:

  • 线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量
  • 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递必须通过主内存来完成
交互协议
  1. 作用于主内存变量:
  • 锁定 lock :把变量标识为一条线程独占的状态
  • 解锁 unlock :把处于锁定状态的变量释放出来
  • 写入 write :把 store 操作从工作内存中得到的变量的值放入主内存的变量中
  • 读取 read :把变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用

​ 2.用于工作内存变量:

  • 赋值 assign :把从执行引擎接收到的值赋给工作内存的变量
  • 使用 use :把工作内存中一个变量的值传递给执行引擎
  • 存储 store :把工作内存中变量的值传送到主内存中,以便随后的 write 操作使用
  • 写入 write :把 store 操作从工作内存中得到的变量的值放入主内存的变量中

结论:注意是顺序非连续

2.2.4 确保并发操作安全的原则

A1:执行八种基本操作的时候,必须满足如下规则:

可以简单理解为不能拒绝别人给的东西

下文的 volatile底层就是用到了 lock 来实现可见性

可见这么多规则非常繁琐,实践也麻烦,下面再介绍一个等效判断原则 -- 先行发生原则

A2:先行发生原则:

Java 内存模型中定义的两项操作之间的偏序关系。

下面例举一些 “天然的” 先行发生关系,无须任何同步器协助就已经存在,可以在编码中直接使用

  • 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作
  • 管程锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作
  • volatile 变量规则:对一个 volatile 变量的操作先行发生于后面对这个变量的操作
  • 线程启动规则Threadstart() 先行发生于此线程的每一个动作
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。可通过 Thread.join() 结束、Thread.isAlive() 的返回值等手段检测到线程已经终止执行
  • 线程中断规则:对线程 interrupt() 的调用先行发生于被中断线程的代码检测到中断事件的发生。可通过 Thread.isInterrupted() 检测到是否有中断发生
  • 对象终结规则:一个对象的初始化完成先行发生于它的 finalize() 的开始
  • 传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 一定先行发生于操作 C

2.2.5 保证原子性、可见性和有序性的措施

  • 可直接保证的原子性变量操作有:readloadassignusestorewrite,因此可认为基本数据类型的访问读写是具备原子性的

  • 若需要保证更大范围的原子性,可通过更高层次的字节码指令 monitorentermonitorexit 来隐式地使用 lockunlock 这两个操作,反映到 Java 代码中就是同步代码块 synchronized 关键字

  • 通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现
  • 提供三个关键字保证可见性:
    1. volatile 能保证新值能立即同步到主内存,且每次使用前立即从主内存刷新
    2. synchronized 对一个变量执行 unlock 操作之前可以先把此变量同步回主内存中
    3. final 修饰的字段在构造器中一旦初始化完成且构造器没有把 this 的引用传递出去,就可以在其他线程中就能看见 final 字段的值
  • 如果在本线程内观察,所有的操作都是有序的,指 “线程内表现为串行的语义”;
  • 如果在一个线程中观察另一个线程,所有的操作都是无序的,指 “指令重排序” 现象和 “工作内存与主内存同步延迟” 现象
  • 提供两个关键字保证有序性:
    1. volatile 本身就包含了禁止指令重排序的语义
    2. synchronized 保证一个变量在同一个时刻只允许一条线程对其进行 lock 操作,使得持有同一个锁的两个同步块只能串行地进入

想详细了解 volatile 的读者,可以看下笔者之前写的文章:进阶之路 | 奇妙的 Thread 之旅

2.3 Java 与线程

2.3.1 线程实现的三种方式

1.使用内核线程

英文: Kernel-Level Thread ,简称:KLT

  • 优点:每个轻量级进程都由一个内核线程支持,因此每个都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞,也不会影响整个进程继续工作
  • 缺点:
    1. 由于基于内核线程实现,所以各种线程操作(创建、析构及同步)都需要进行系统调用,代价相对较高,需要在用户态内核态中来回切换
    2. 一个系统支持轻量级进程的数量是有限的
  • 一对一线程模型:轻量级进程与内核线程之间 1:1 的关系,如图所示
内核线程模型
2.使用用户线程

英文: User Thread ,简称:UT

  • 广义上认为一个线程不是内核线程就是用户线程
  • 狭义上认为用户线程指的是完全建立在用户空间的线程库上,而系统内核不能感知线程存在的实现
用户线程模型
3.混合
  • 用户线程完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发
  • 操作系统提供支持的轻量级进程作为用户线程和内核线程之间的桥梁,可以使用内核提供的线程调度功能及处理器映射,且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险
混合模型

Q: Java 线程的实现是选择哪一种呢?

A:答案是不确定的。操作系统支持怎样的线程模型,在很大程度上决定了 JVM 的线程是怎样映射的。线程模型只对线程的并发规模和操作成本产生影响,而对 Java 程序的编码和运行过程来说,这些差异都是透明的。

2.3.2 线程调度的两种方式

线程调度:指系统为线程分配处理器使用权的过程

1.协同式线程调度
  • 实现简单
  • 切换操作自己可知,不存在线程同步的问题
2.抢占式线程调度

但是线程优先级并不是太靠谱,一方面因为 Java 的线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统,在一些平台上不同的优先级实际会变得相同;另一方面优先级可能会被系统自行改变。

2.3.3 线程的六种状态

线程状态

在任意一个时间点,一个线程只能有且只有其中的一种状态:

  • Thread.sleep()
  • 设置了 Timeout 参数的 Object.wait()
  • 设置了 Timeout 参数的 Thread.join()
  • LockSupport.parkNanos()
  • LockSupport.parkUntil()

注意区别阻塞和等待:

  • 阻塞状态:在等待获取到一个排他锁,在另外一个线程放弃这个锁的时候发生;
  • 等待状态:在等待一段时间或者唤醒动作的发生,在程序等待进入同步区域的时候发生。

三.碎碎念

恭喜你!已经看完了前面的文章,相信你对 Java 内存模型与线程已经有一定深度的了解!你可以稍微放松奖励自己一下,可以睡一个美美的觉,明天起来继续冲冲冲!!!

PS:原本《深入理解Java虚拟机》第3版中还提及了协程,但是我还没学过协程的基本用法,这时候给大家讲解感觉有点打肿脸充胖子的感觉 hhh,明天《第一行代码-第三版》也要到了,待我看完《第一行代码》再补充协程的内容吧 hhhh


如果文章对您有一点帮助的话,希望您能点一下赞,您的点赞,是我前进的动力

本文参考链接:

上一篇 下一篇

猜你喜欢

热点阅读