Java 多线程-理论基础

Java 多线程-理论基础

什么是多线程线程是任务调度的基本单位。怎么理解这句话? 任务从 cpu 角度来讲是一条条指令,比如“加”操作、“取”操作、”赋值“操作。 一条和多条指令组成了一句高级语言,也可以说是一行代码。比如 i++,该代码会执行三条指令:

将变量 i 从内存读取到 CPU寄存器;在CPU寄存器中执行 i + 1 操作;将最后的结果i写入内存,缓存机制导致可能写入的是 CPU 缓存而不是内存。多行代码可以被看作是一个任务,在 Java 中可以表示为 Runnable,这个任务的执行要依附于线程。线程调度,实际上是调度提交到线程的一个个任务。

为什么要多线程单线程执行任务可以类比为一个人干活。比如盖房子,先搬砖,再挖坑,再垒砖……一个人可以干,但是不管顺序怎样,要一件事一件事的完成。 如果是一群人来干,那么就可以有人搬砖,有人挖坑,有人垒砖……多件事情可以同时进行,大大提高了效率,这就是多线程。

在计算机系统中,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:

CPU 增加了缓存,以均衡与内存的速度差异;导致 可见性问题操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;导致 原子性问题编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。导致 有序性问题多线程要考虑什么问题(并发三要素)多线程要考虑三个问题: 可见性问题、原子性问题、有序性问题

1. 可见性: CPU缓存引起可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。举个简单的例子,看下面这段代码:

//线程1执行的代码

int i = 0;

i = 10;

//线程2执行的代码

j = i;假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i = 10 这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

2. 原子性: 分时复用引起原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。举个简单的例子,看下面这段代码:

int i = 1;

// 线程1执行

i += 1;

// 线程2执行

i += 1;这里需要注意的是:i += 1 需要三条 CPU 指令

将变量 i 从内存读取到 CPU寄存器;在CPU寄存器中执行 i + 1 操作;将最后的结果i写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。由于CPU分时复用(线程切换)的存在,线程1执行了第一条指令后,就切换到线程2执行,假如线程2执行了这三条指令后,再切换会线程1执行后续两条指令,将造成最后写到内存中的i值是2而不是3。

3. 有序性: 重排序引起有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:

int i = 0;

boolean flag = false;

i = 1; // 语句1

flag = true; // 语句2上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? 不一定,为什么呢? 这里可能会发生指令重排序(Instruction Reorder)。

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。

这些重排序都可能会导致多线程程序出现内存可见性问题。

对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。

对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

JAVA 如何保证现成安全: JMM(Java内存模型)一、理解的第一个维度:核心知识点JMM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。

具体来说,这些方法包括:

volatile、synchronized 和 final 三个关键字Happens-Before 规则1. 关键字: volatile、synchronized 和 final见后续文章

2. Happens-Before 规则上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。

二、理解的第二个维度:可见性,有序性,原子性1. 原子性在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。 请分析以下哪些操作是原子性操作:

x = 10; //语句1: 直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中

y = x; //语句2: 包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

x++; //语句3: x++包括3个操作:读取x的值,进行加1操作,写入新的值。

x = x + 1; //语句4: 同语句3上面4个语句只有语句1的操作具备原子性。

也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

相关信息

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

2. 可见性Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

相关信息

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

3. 有序性在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的。

相关推荐

[资料]小梅沙内部新消息
好多假365平台

[资料]小梅沙内部新消息

09-03 👁️ 5347
凯时® 前列地尔注射液
好多假365平台

凯时® 前列地尔注射液

06-28 👁️ 6641
抖音如何拍摄高质量游戏视频,轻松成为爆款创作者