java并发编程基础
n++不是原子的操作
指令重排序,多线程并发时可能会出现问题
例如:
编译期出于性能的影响,指令重排,对于单线程没有问题
因为还未初始化,就直接返回,会报错
JMM
3.1并发编程的关键目标
并发编程需要处理两个关键问题,即线程之间如何通信和同步。
- 通信:指线程之间以何种机制来交换信息;
- 同步:指程序中用于控制不同线程之间的操作发生的相对顺序的机制。
3.2并发编程的内存模型
共有两种并发编程模型:共享内存模型、消息传递模型,Java采用的是前者。
- 在共享内存模型下,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信;
- 在共享内存模型下,同步是显示进行的,程序员必须显示指定某段代码需要在线程之间互斥执行。
3.3这个内存模型叫JMM
JMM是lava Memcry Model的缩写,Java线程 之间的通信由JMM控制,即JMM决定一个线程对共享变量的写入何时对另一个线程可见。JMM定义了线程和主内存之间的抽象关系,通过控制主内存与每个本地内存(抽象概念)之间的交互,JMM为Java程序员 提供了内存可见性的保证。
3.4源代码与指令间的重排序
为了提高性能,编译器和处理器常常会对指令做重排序。重排序有3种类型,其中后2种都是处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。
- 编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
3.5重排序对可见性的影响
有一种可能:对于内存来说A2先于A1执行,即还未赋值,即x,y的值就是初始值
3.6如何解决重排序带来的问题
对于不同的重排序有不一样的解决方案
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(volite,sychnoized)。
对于处理器重排序,JMM的处理器重排序规则会要求编译器在生成指令序列时,插入特定类型的内存屏障(MemoryBarries/Memory Fence)指令(特定的语法代码),通过内存屏障指令来禁止特定类型的处理器重排序。
由于常见的处理器内存模型比JMM要弱,Java编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。同时,由于各种处理器内存模型的强弱不同,为了在不同的处理器平台向程序员展示一个一 致的内存模型, JMM在不同的处理器中需要插入的内存屏障的数量和种类也不同。
cpu内存屏障有四种。java有个Unsafe类,里面都是本地的方法,有三个方法将cpu的四个内存屏障组合到一起
内存屏障:在底层编译的时候插特殊的代码,告诉操作系统避免重排序
3.7happens-before
内存可见性怎么保障的呢?
JMM使用happens- before规则来阐述操作之间的内存可见性,以及什么时候不能重排序。在JMM中,如果一个操作执行的结果需要对另一个操作可见 ,那么这两个操作之间必须要存在happens- before关系。
换个角度来说,如果A happens- before B.则意味着A的执行结果必须对B可见,也就是保证跨线程的内存可见性。其中,前4条规则与程序员密切相关。
1.程序顺序规则: 一个线程中的每个操作, happens- -before于该线程中的任意后续操作;
- volatile变 量规则:对一个volatile域的写,happens- before于任意后续对这个volatile域的读;
- synchronized规则: 对- - 个锁的解锁,happens-before于随后对这 个锁的加锁; B能感知到A解锁
- 传递性:若A happens- before B,且B happens- before C,则A happens- before C;
- start()规则: 若线程A执行Thread .start(),则线程A的star()操作happens-before 于线程B中的任意操作;
- join()规则: 若线程A执行ThreadB.join()并 成功返回,那么线程B中的任意操作happens- before于线程A从ThreadB.join()的成功返回。
内存屏障解决了CPU的重排序。JMM解决了可见性问题和编译时期的重排序(happens-before)
happens-before解决了重排序和可见性
JMM----》happens-before
volatile
4.1 volatile的基本特性
-
可见性:对一个volatile变 量的读,总是能看到对这个volatile变量最后的写入;
-
原子性:对任意单个volatile变量的读/写具有原子性,但类似volatile+ +这种复合操作不具有原子性。
4.2 volatile的内存语义
- 写内存语义:当写一个volatile变量时,JMM会把该线程本地内存中的共享变量的值刷新到主内存;
- 读内存语义: 当读一个voltile变量时,JMM会 把该线程本地内存置为无效,使其从主内存中读取共享变量。
4.3 volatile的实现机制
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。
-
在每个volatile写 操作的前面插入一个StoreStore屏障;
-
在每个volatile写操作的后面插入一个StoreLoad屏障;
-
在每个volatile读操作的后 面插入一个LoadLoad屏障;
-
在每个volatile读操作的后 面插入一个LoadStore屏障 。
4.4 volatile与锁的对比
volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上锁比volatile更强大,在可伸缩性和执行性能上volatile更 有优势。
锁
5.1锁的内存语义
-
synchronized: 采用"CAS + Mark Word"实现,存在锁升级的情况;
-
Lock: 采用“CAS + volatile" 实现,存在锁降级的情况,核心是AQS。
,JMM会把该线程对应的本地内存置为无效。
5.2锁的实现机制
-
synchronized: 采用"CAS + Mark Word"实现,存在锁升级的情况;
-
Lock: 采用“CAS + volatile" 实现,存在锁降级的情况,核心是AQS。