1. 线程有三大特性:
1.1可见性: Visibility:
可见性就是当一个线程修改了共享变量时, 其他线程能够立即得知这个修改, 但是由于CPU缓存的存在, 可见性往往存在一些问题.
如何保证可见性? :
使用volatile关键字, volatile保证了变量的可见性和有序性
假设 T 表示一个线程, V 和 W分别表示两个 volatile 型变量,那么在进行 read 、 load、 use 、assign 、store 、write 操作时需要满足一下规则:
● 只有当线程T对变量V执行的前一个动作时 load 时,线程T才能对变量V执行 use 动作;并且,只有当线程T对变量V执行的最后一个动作时 use 时,线程T才能对变量V执行 load 动作。线程T对变量V的 use 动作和 load 、read 动作是相关联的,必须连续且一起出现。这条规则保证了T线程对其他线程修改的变量的可见性
● 只有当线程T对变量V执行的前一个动作是 assign 时,线程T才能对变量V执行 store 动作;并且,只有当线程T对变量V执行最后一个动作是 store 时,线程T才能对变量V执行 assign 动作。 线程T对变量V的 assign 动作和 store 、 write 动作是相关联的,必须连续且一起出现。这条规则确保了其他线程对T线程修改的变量的可见性
● 如果把线程T对变量V实施的 use 或 assign 动作比作 A ,对变量W实施的 use 或 assign 比作B。对变量V实施的 load 或 store 动作比作 F,对变量W实施的 load 或 store 比作G。对变量V实施的 read 或 write 动作比作 P,对变量W实施的 read 或 write 动作比作Q。那么如果A先于B,那么P先于Q。这条规则保证了volatile修饰的变量不会被指令重排序优化。
使用了内存屏障等方面的知识~
有序性: Ordering
有序性, 顾名思义就是程序按照代码的先后顺序来执行. 但其实内部并不完全是, 为了使处理器内部运算单元能够尽量被充分利用, 处理器可能对输入的代码进行乱序执行优化, 也就是说处理器可能会次序颠倒的执行命令.
为什么这种乱序执行在平常的开发中无感呢?
对指令重排序需要满足以下两个条件:
As-if_serial语义: 不管怎么重排序, 代码在多线程环境下执行和单线程环境下执行的结果不能被改变, 编译器, runtime和处理器都必须遵守as-if-serial语义. 但as-if-serial语义只对单线程代码行的执行顺序进行了约束, 却无法约束多线程和单条指令的有序性.
Happens-Before:
int a = 1; //A
int b = 2; //B
int c = a * b * b;//C
按照Happens-Before的程序顺序规则,上述代码存在3种Happens-Before关系:
- A happens-before B
- B happens-before C
- A happens-before C
但事实上 B 可以排在 A 之前执行,因为 A 和 B 并不存在数据依赖关系,并且 B 在 A 之前执行并不影响最后的执行结果。 所以 JMM 并不要求 A 一定要在 B 之前执行。这种不影响结果的乱序是 JMM 所允许的。
原子性: Atomicity
无论是单核还是多核Cpu, 从宏观上我们都可以并发执行多个进程, 从微观角度看, 实际上是操作系统给每个进程分配一个时间片, 多个进程分时复用CPU, 它带来诸多好处, 让某个进程不会因为等待IO而浪费CPU资源, 然而不同的进程是不共享内存空间的, 所以在做任务切换的时候, 就要切换内存映射地址, 这种切换是一种重量级切换, 而现在的操作系统普遍都是基于更轻量级的线程来调度, 进程内的所有线程共享一个内存空间, 所以线程的切换成本更低.
这样的切换被称为上下文切换, 上下文切换也为我们带来了原子性的问题, 我们拿Java代码来说i+=1;
这段代码在低层至少需要三条CPU指令, 1. 把变量i的值load到CPU寄存器中 2.在CPU中执行+1 3.将结果Store到内存中, 当然有可能只存到缓存, 更严谨的说应该是写缓存区, 并没有刷新到主存中.
虽然每条指令具有原子性, 但在进行上下文切换时, 可能发生任意一条CPU指令执行完之后, 这对高级编程语言来说就会在多线程并发时, 造成原子性问题, 如下图所示:
如何保证原子性? :
当我们的代码需要原子性保证时, 可以使用lock和unlock操作来满足要求, 尽管虚拟机并未把这两个指令直接开放给用户使用, 但却提供了更高层次的字节码指令monitorenter和monitorexit来隐式的使用这两个操作, 而这两个字节码指令反映到Java代码中就是同步代码块—synchronized关键字
我们来看一段代码:
public void test01(){
synchronized (this){
int i = 1;
}
}
它的字节码如下:
0 aload_0
1 dup
2 astore_1
3 monitorenter
4 iconst_1
5 istore_2
6 aload_1
7 monitorexit
8 goto 16 (+8)
11 astore_3
12 aload_1
13 monitorexit
14 aload_3
15 athrow
16 return
其中3, 7, 13便对应的是synchronized代码块了
注: 这里为什么会出现两次monitorexit? 实际上monitorexit并没有执行两次, 这里是异常表对安全释放锁的一种保证, 类似我们平时开发中的try-finally
JMM(Java Memory Model)
定义 Java 内存模型并非一件容易的事,这个模型必须定义的足够严谨,才能让 Java 的并发内存访问操作不会产生歧义。我们不能单纯的禁用CPU缓存和编译优化,这样会严重影响程序性能。所以定义必须足够宽松,使得虚拟机的实现能有足够的自由空间去利用硬件的各种特性(寄存器、高速缓存和指令集中某些特有的指令)来取得更好的执行速度。
经过长时间的验证和修补,直至JDK5 (JSR-133)发布后,Java 内存模型才终于成熟、完善起来
JSR-133 :Java Memory Model and Thread Specification Revision (Java 内存模型和线程规范修订)
JMM的主要目的就是定义程序中的各种变量的访问规则, 既关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的低层细节, 也是决定一个线程共享变量的写入何时对另一个线程可见.
JMM中的主内存与本地内存(工作内存)
注:本地内存是一个抽象概念,并不真实存在。它包含了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
每个线程都有自己的执行空间(既工作内存), 线程执行的时候, 用到的某变量, 首先要从主内存中拷贝到自己的工作内存中, 然后对对象进行操作.
注: 拷贝到工作内存中, 并不是拷贝对象, 而是拷贝对象的引用. 线程对变量的所有操作(读和写)都必须在工作内存中进行, 而不能直接读写主内存中的数据.
从主内存中拷贝对象到工作内存中, 共涉及到8类原子操作, 分别为:
a. read(读取): 作用于主内存, 它把变量从主内存拷贝到工作内存中, 以便随后的load动作使用
b. load(载入): 作用于工作内存, 它把read操作的值放入工作内存的变量副本中
c. use(使用): 作用于工作内存, 它把工作内存中的值传递给执行引擎, 每当虚拟机遇到一个需要使用这个变量的指令时候, 将会执行这个动作
d. assign(赋值): 作用于工作内存, 它把执行引擎获取的值赋值给工作内存中, 每当虚拟机遇到一个给变量赋值的指令的时候, 执行该操作
e. store(存储): 作用于工作内存, 它把工作内存的一个变量传递给主内存, 以便随后的write操作使用
f. write(写入): 作用于工作内存, 它把store传送值放到主内存的变量中
g. lock(锁定): 作用于主内存, 它把一个变量标记为一条线程独占状态
h. unlock(解锁): 作用于主内存, 它把一个处于锁定状态的变量释放出来, 释放后的变量能够被其它线程锁定
注意: 对于上图的理解: 当线程2修改了静态变量中的值, 并write到主内存中, 线程1是无法获取到主内存中的该值, 要想两个线程都对彼此的修改可见, 需要对这个静态变量加上 volatile修饰, 加上这个关键字后, jvm会启动MESI缓存一致性. 此时: 当线程2改变了静态变量的值时, jvm内部会使线程1引用该变量的地址失效, 当线程1再次使用该共享变量时, 就会重新从主内存中读取, 因此线程1就可以及时读取到线程2修改的变量值.
JMM 还规定了上述 8 种基本操作,需要满足以下规则:
a. 不允许 read 和 load 、store 和 write 操作单一出现。
b. 不允许一个线程丢弃它最近的 assign 操作,及工作内存值被改变之后必须同步回主内存。
c. 不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存
d. 一个新变量只能从主存中”诞生“,也就是说 user 、store 操作之前,必须先执行 assign 和 load 操作。
e. 一个变量在同时只能被一个线程 lock ,但可以被同一个线程多次 lock ,之后只有执行相同次数的 unlock 才能被解锁。
f. 如果一个变量执行了 lock 操作,将会清空工作内存中的此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量值。
g. 如果一个变量没有被 lock ,那就不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程 lock 的变量。
h. 对第一个变量执行 unlock 之前,必须把此变量同不回主内存中(执行 store 、 write 操作)。