你认同JVM中先行发生原则是比较隐蔽和重要的一点吗

简介: 你认同JVM中先行发生原则是比较隐蔽和重要的一点吗

Java内存模型与线程

image.png

上面的图就是线程,工作内存,主内存的关系,也可以看到线程想要获取数据,需要先到工作内存找,工作内存从主内存中找,那为啥需要这个工作内存 ? 而不直接访问主内存,也可以避免数据不一致的情况了。

这就需要我们对物理计算机中如何并发访问有一点儿了解,我们知道CPU内含有寄存器,但寄存器能存放的内容太少,而大部分时间都要从内存中获取,如果等待从内存中取得数据,CPU又被浪费了,因为两者间的速度差太多,所以引入了高速缓存。

image.png

除了增加高速缓存外,为了处理器内部的运输单元能被充分利用,处理器会对输入代码进行乱序执行,在计算后将乱序执行的结果重组,保证该结果与顺序执行的结果一致。而潜在的风险就是一个计算任务依赖另一个计算任务的中间结果,其顺序性不能靠代码的先后顺序来保证,而Java内存模型也保留了这一点,我们也可以看到上面两幅图结构是非常相似的。所以Java内存模型的定义也是由硬件决定的。

内存间交互操作

也就是图1中,线程,工作内存,主内存数据是怎么交互的,Java内存模型定义了8个操作,每个操作都是原子性的,对double,long类型的变量来说,load,store,read,write有一定例外,分为两次32位操作,问题不大。

锁定(lock) : 作用于主内存中的变量,将他标记为一个线程独享变量。

解锁(unlock) : 作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。

read(读取) :作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。

load(载入) :把 read 操作从主内存中得到的变量值放入工作内存的变量的副本中。

use(使用) :把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令。

assign(赋值) :作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

store(存储) :作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。

write(写入) :作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。

其中,read,loadstore,write都要求顺序执行,但中间可以插入其他操作。

Java中还规定了这8种操作必须满足的规则(这里我通俗说)

  1. read,loadstore,write 不能单独出现,我要了必须用,我给了你必须收。
  2. 工作内存中修改了,必须同步到主内存
  3. 如果工作内存没修改,不允许同步主内存
  4. 一个新变量只能在主内存中诞生
  5. 一个变量同一时刻只能被一个线程lock,如果统一线程多次加锁后,必须多次释放锁。 排他,可重入
  6. 一个变量Lock后,会清空工作内存,之后使用需要重新获取加载。
  7. 不能先unlock 再lock,也不允许unlock 别的线程加的锁
  8. 执行unlock前,先将此变量的值同步回主内存

volatile

三大特性:

  1. 可见性(一个线程修改后,其他线程都能获取到新值)底层每次使用前都会刷新该变量值。
  2. 禁止指令重排优化,我用代码距离:
Map configOptions;
char[] configText;
volatile boolean initialized = false;
// 线程A中执行 读取配置文件进行初始化
configOptions = new HashMap();
configText = readConfigFile(fileName);
initialized = true;
// 线程B中执行 等待初始化好后,利用配置干活儿
while(!initialized) {
  sleep(500);
}
dosomethingWithConfig();

如果上面initialized 没用volatile修改,initialized = true;就可能排到上面,导致B线程以为初始化好后进行操作,导致问题。这些问题十分隐蔽,因为代码的执行顺序可能与代码编写的顺序不同。

这里我看有的资料说Volatile具有原子性,但我自己代码验证无法保证,因为Java运算操作符无原子性。Volatile是满足先行发生原则,具体看下面:

先行发生原则

负责判断数据是否存在竞争,线程是否安全的有效手段,如果两个操作之间关系不匹配这些规则,则可能被指令重拍

程序次序规则

在一个线程中,按照控制流顺序,书写在其那面的操作先行发生与书写在后面的操作。注意:控制流不是代码顺序

Java 提供了三种类型的控制流语句。

  1. 决策声明
  • if 语句
  • 切换语句
  1. 循环语句
  • 做while循环
  • while 循环
  • for 循环
  • for-each 循环
  1. 跳转语句
  • 中断语句
  • 继续声明

管程锁定规则

同一个锁,unlock 先行于 lock操作,这里先行指的时间上的先后

Volatile变量规则

对Volatile变量的写操作先行发生于后面对这个变量的读操作,这里的先后指时间上的先后

线程启动规则

Thread的start() 先行于此线程的每个动作

线程终止规则

Thread所有操作都先行发生于对此线程的终止检查

线程中断规则

Thread::interrupt()先行发生与对此线程的中断检查 Thread::interrupted()

对象终止规则

一个对象的初始化完成先于finalized()方法的开始

传递性

操作A先行于操作B,操作B先与操作C,则操作A先于操作B

这里也举个重要的例子:简单的变量get,set方法

private int value = 0;
void setValue(int value) {
  this.value = value;
}
int getValue() {
  return value;
}

假设现在有线程A,B,线程A先执行(时间上)了setValue(1),线程B后执行了getValue(),问线程B获取到的返回值是多少?

这道题很坑,常理来想,时间上先发生的操作肯定比后发生的操作先执行。但实际上:

时间先后顺序与先行发生原则之间基本没有因果关系,我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。 时间上的先后,代码上的先后 都不能决定是否并发安全。


Java与线程

实现线程主要有三种方式,使用内核线程实现(1:1),使用用户线程实现(1:N),使用用户线程+轻量级进程混合实现(N:M)

内核线程实现

直接由操作系统内核支持的线程,线程的切换也由内核管理,程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程,也就是传统意义线程,每个轻量级进程都由一个内核线程支持,一比一的关系。

优点:每个轻量级进程都是独立的调度单元,一个阻塞了,不影响整个进程继续工作

缺点:基于内核线程实现,各类操作需要频繁在 用户态 与 内核态间切换。且一个系统支持的轻量级进程是有限的

用户线程实现

狭义的用户线程指:完全建立在用户空间的线程库上,系统内核无法感知,线程的建立,同步,销毁和调度完全在用户态中完成,可以不切换到内核态,速度很快。 部分高性能的数据库多线程就是由用户线程实现的。

缺点:也是由于没有系统内核的支持,所有系统操作都要由用户程序自己处理,Java,Ruby曾使用,后面也不用了,不过Go语言支持用户线程,作为高并发。

混合使用

既存在用户线程,也存在轻量级进程。用户线程还是在用户态,轻量级进程作为内核线程与用户线程之间的桥梁。

Java线程的实现

HotSpot为例,它的每个Java线程都是直接映射到一个操作系统原生线程实现的,也就是上面的轻量级进程。线程的调度全权交给操作系统。

Java线程调度

线程调度分为:协同式 和 抢占式

  1. 协同式:线程执行时间由线程本身来控制。线程把自己工作干完了,主动通知系统切换到另一个线程。Lua使用
  2. 抢占式:每个线程由操作系统来分配执行时间。Java使用

协同式的好处是实现简单,但如果一个线程卡死会导致程序一直阻塞。如果使用抢占式,可以保证线程的执行时间片到时间了,就切换到其他线程了,比方说我们用IDEA写代码,如果卡死了,我们可以启动任务管理器来Kill这个卡死线程。

状态转化

线程状态。线程可以处于以下状态之一:

  • NEW 尚未启动的线程处于此状态。
  • RUNNABLE 在 Java 虚拟机中执行的线程处于这种状态。
  • BLOCKED 阻塞等待监视器锁的线程处于此状态。
  • WAITING 无限期等待另一个线程执行特定操作的线程处于此状态。
  • TIMED_WAITING 等待另一个线程执行某个操作达指定等待时间的线程处于此状态。
  • TERMINATED 已退出的线程处于此状态。


目录
相关文章
|
14天前
|
存储 人工智能 安全
数字化转型的10大陷阱及如何避免
数字化转型的10大陷阱及如何避免
|
存储 编译器 Linux
C生万物 | 窥探数组设计的种种陷阱
数组在设计的时候为何会出现那么多纰漏?数组越界是如何导致的?,我们来一探究竟🔍
59 0
C生万物 | 窥探数组设计的种种陷阱
|
前端开发 程序员 开发者
「知识盲区系列」 带你了解 KISS 原则,此 KISS 非彼 KISS 💋啦~
「知识盲区系列」 带你了解 KISS 原则,此 KISS 非彼 KISS 💋啦~
280 0
|
安全 Java 容器
Happens-beofre 先行发生原则(JVM 规范)
Happens-beofre 先行发生原则(JVM 规范)
95 0
《战争论》第七篇《进攻》的主要原则
《战争论》第七篇《进攻》的主要原则   《进攻》是《战争论》的第七篇,主要论述了六部分内容,包括进攻概论、消灭敌军的四种看法、破坏敌人作战力量、进攻的顶点、战略机动和各种进攻(如图1所示)。
1419 0