目录
上下文切换
并行和并发的区别
并发三大特性(一) 可见性
并发三大特性(二) 原子性
并发三大特性(三) 有序性
volatile介绍
jvm层面内存屏障
上下文切换
概念:当因为某些原因造成cpu不再执行当前线程,转去执行另一个线程,就会发生上下文切换。当发生上下文切换时,操作系统会保存当前线程的状态,并且恢复另一个线程的状态。保存当前线程状态时就会涉及到JVM的一个内存结构:程序计数器(Program Counter Register),它是线程私有的,用于记录当前线程要执行的下一条命令的地址。
发生上下文切换的时机:
- 线程的时间片用完了
- 垃圾回收
- 线程中调用 sleep、yield、wait、join、park、synchronized、lock等方法
当发生上下文切换的时候,被恢复的线程读取的共享数据是从堆中读取的,而不是从本地变量副本读取
并行和并发的区别
并行:指在同一时刻,多条指令在多个cpu上同时执行,不论时微观还是宏观上看,都是同时执行
并发:指多条指令在一个cpu上交替执行,同一时刻只能有一条指令在执行。但是由于交替的时间间隔很短,看不出来,所有宏观上来看是同时执行的,而微观上来看是分开交替执行的
并发三大特性(一) 可见性
不可见性概念:当A线程修改了共享数据时,B线程没有及时获取到最新的值,如果还在使用原先的值,就会某些不可预期的问题
不可见原理:
如图所示,共享数变量储在堆内存当中,而堆内存是唯一的,每个线程又有自己的线程栈,当线程要读取共享变量的时候,会先从堆内存中读取一份到线程栈中的变量副本中,之后读取该变量的时候会优先读取变量副本。而线程A在修改变量副本之后,会将堆中的共享变量的值进行更替,此时A的变量副本与堆内存中的共享变量都是最新的值,但是,线程B仍然读取的是之前的变量副本,两个线程读到的值就有所不同,这个的条件也相对苛刻,因为如果时间间隔较长不使用副本变量,就会对副本变量进行回收,然后下次读取还是会读取堆中的值。
可见性概念:当一个线程修改了共享变量的值,其他线程能够看到修改的值。
保证可见性的方式:
- 通过 volatile 关键字保证可见性。
- 通过 内存屏障保证可见性(Unsafe.getUnsafe().storeFence())
- 通过 synchronized 关键字保证可见性。
- 通过 Lock保证可见性。
以上几种都是通过内存屏障实现
- 上下文切换
- 通过 final 关键字保证可见性
猜测也是使用内存屏障实现的,顺便提一下Integer也可以实现可见性,因为内部也是使用final修饰。
并发三大特性(二) 原子性
原子性问题
例如i++等操作执行过程是这样的,首先要先拿到内存中的数据,然后再保存到变量副本中,这时候在修改变量副本的值,然后再把变量副本的值替换掉内存中的值,在这个运算过程中走了三个步骤,在多线程环境中,可能修改好变量副本了,准备要往内存中复制这个过程中,就被其他线程修改了值,这时候再去复制,就导致结果最终不一致
解决方法:
- 通过 synchronized 关键字保证原子性。
- 通过 Lock保证原子性。
- 通过 CAS保证原子性。
并发三大特性(三) 有序性
有序性问题:
JVM 会在不影响正确性的前提下,可以调整语句的执行顺序(指令重排),思考下面问题
图中左侧是 3 行 Java 代码,右侧是这 3 行代码可能被转化成的指令。可以看出 a = 100 对应的是 Load a、Set to 100、Store a,意味着从主存中读取 a 的值,然后把值设置为 100,并存储回去,同理, b = 5 对应的是下面三行 Load b、Set to 5、Store b,最后的 a = a + 10,对应的是 Load a、Set to 110、Store a。如果你仔细观察,会发现这里有两次“Load a”和两次“Store a”,说明存在一定的重排序的优化空间。
重排序后, a 的两次操作被放到一起,指令执行情况变为 Load a、Set to 100、Set to 110、 Store a。下面和 b 相关的指令不变,仍对应 Load b、 Set to 5、Store b。
可以看出,重排序后 a 的相关指令发生了变化,节省了一次 Load a 和一次 Store a。重排序通过减少执行指令,从而提高整体的运行速度,这就是重排序带来的优化和好处
但是在多线程情况下,就会发生一些不可预期的事情!
解决方法:
通过 volatile 关键字保证有序性。 通过 内存屏障保证有序性。 通过 synchronized关键字保证有序性。 通过 Lock保证有序性。volatile介绍
valotile的作用:
可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
有序性:对volatile修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性。
volatile写-读的内存语义- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量
- 第二个操作是volatile写,不管第一个操作是什么都不会重排序
- 第一个操作是volatile读,不管第二个操作是什么都不会重排序
- 第一个操作是volatile写,第二个操作是volatile读,也不会发生重排序
valotile的实现方式:
volatile在jvm层面上的实现是通过一个storeload内存屏障
在linux的x86系统中可以看到volatile的实现它实际上是调用汇编语言的lock前缀指令实现可见性的,它会使副本变量失效,强制性的要求下一次访问要从堆中获取最新的值。
lock前缀指令的作用:
1. LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排 序。 2. LOCK前缀指令会 等待它之前所有的指令完成、并且所有缓冲的写操作写回内存 (也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新store buffer的操作会导致其他cache中的副本失效


