目录
java内存模型
JSR-133 Java Memory Model and Thread Specification
JMM主要目的是定义程序中各种变量的访问控制,关注在虚拟机中把变量值存储到内存和从内存取出变量值得底层细节
此处得变量(Variables)与java代码中的变量略有区别.它包含了实例字段,静态字段和构成数组的对象的元素,不包含局部变量,方法参数.因为后者是线程私有.
工作内存
JMM规定的所有变量存储在主内存(Main memory,类比物理机内存),每条线程还有自己的工作内存(working memory,可类比cpu高速缓存),线程的工作内存保存了被该线程使用的变量的主内存副本(并非完全复制,只有使用到的字段副本).线程对所有变量的所有操作(读取,赋值)都必须是在工作内存中(volatile关键字也不例外,只是由于它有特殊的操作顺序规定,看起来像是直接操作主内存).
主内存、工作内存与java内存区的堆栈方法去并不是一个层次的对内存的划分,二者没有关系.
java堆中的数据(HotSpot虚拟机):
- 对象实例数据
- Mark Word数据(存储对象的hash码,gc标记,gc年龄,同步锁)
- Klass Point(指向存储类型元数据的指针)
- 用于字节对齐补白的填充数据.
内存间的交互
关于主内存与工作内存的具体的交互协议,即一个对象如何从主内存拷贝到工作内存,又如何从工作内存同步回主内存的实现细节,JMM规定了8中操作:
- lock (锁):作用于主内存的变量,它把一个变量标识为一条线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放,释放之后,才能被其他线程锁定
- read (读取):作用于主线程的变量,把一个变量的值从主内存传输到线程的工作内存,以便随后的load动作使用.
- load (载入):作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的副本中
- use (使用):作用于工作内存的变量,把工作内存的变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量值的字节码指令时会执行这个操作.
- assign (赋值):作用于工作内存的变量,它把一个从执行引擎接受的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令都会执行这个操作
- store (存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后write操作使用
- write (写入):作用于主内存的变量,把sotre操作从工作内存得到的变量的值放入主内存的变量中
在上述操作中,JMM做出如下规定:
- read-load,store-write 必须按顺序执行(不要求连续)
- read-load,store-write 必须成对出现.
- 不允许一个线程丢弃它最近的assign操作.即变量在工作内存改了之后必须同步回主内存
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据同步回主内存
- 一个新的变量必须在主内存中”诞生”,不允许工作内存中直接使用一个未被初始化(load或assign)的变量,即对一个变量实施use,store操作之前,必须先执行assign,load操作
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但是lock操作可以被一条线程执行多次,多次lock之后,执行相同次数的unlock变量才会被解锁
- 如果对一个变量执行lock,那么将会清空工作内存中此变量的值,执行引擎使用这个变量前,需要重新执行load或assign操作以初始化该变量
- 如果一个变量没有被lock,那么不允许对它进行unlock操作,也不允许去unlock一个被其他线程锁定的变量.
- 对一个变量执行unlock之前,必须将其stroe,write回主内存.
volatile的语义
volatile关键字是java提供的最轻量级的同步机制.
volatile的可见性
volatile可见性: 当一条线程修改变量的值,新值对于其他线程来说可以立即得知.
普通变量不能做到这一点,
volatile在并发下并不是安全的,这是由于java里的运算操作符并非线程安全.
volatile线程安全的条件
- 运算结果并不依赖变量的当前值,或者能够确保只有单一现场修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
禁止指令重排
线程内表现为串行(Within-Thread As-If-Serial Semantics)
指令重排:指令重排是JVM为了优化指令,提高程序运行效率,在不影响程序运行结果的前提下,尽可能提高并行度。编译器、处理器也遵循这样一个目标。
不管如何重排(编译器与处理器为了提高并行度),(单线程)程序的结果不能被改变.编译器,runtime,处理器必须遵守的予以.
指令重盘包含编译器重排序和运行时重排序.
指令重排与单例
下面的单例代码中,instance = new Singleton()
不是一个原子操作.
jvm指令可抽象为3步:
- 分配内存空间
- 初始化
- 设置instance指向分配的内存空间
3和2依赖于1,但是3不依赖于2,因此可能进行重排为1,3,2.如果此时另外一个线程判断instance引用不为null,就将其返回使用,导致出错.
使用volatile关键字禁止指令重排,其通过提供”内存屏障”的方式来防止指令重新排序的.
1 | public class Singleton { |
内存屏障
内存屏障(Memory Barrier或 Memory Fence):重排序时不能把后面的指令排序到内存屏障之前的位置.
volatile使用场景
jvm对锁有各种优化,很难确切比较volatile比synchronized快多少.
volatile变量读操作的性能与普通变量差不多,写操作会慢一点,需要在本地代码中插入许多内存屏障指令保证处理器不发生乱序执行.大多数场景下,volatile比锁开销低.
从内存可见性来讲,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。
volatile与锁选择判断的唯一依据仅是依赖volatile的语义能否满足场景.
happens-before
happens-before规则
Happens-Before:前面一个操作的结果对后续操作可见。
Happens-Before约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守Happens-Before规则。
程序的顺序性规则
在同一线程内,按照控制流顺序,书写在前边的操作线性于书写在后面的操作。变量规则
对一个volatile
变量的写操作,Happens-Before于后续对这个volatile变量的读操作. 时间先后传递性
如果 A Happens-Before B,且B Happens-Before C,则A happens-before c管程中锁的规则
这条规则是指对一个锁的解锁Happens-Before于后续对这个锁的加锁.同一个锁,且为时间的先后线程 start() 规则
主线程A启动子线程B后,子线程B能看到主线擦在启动子线程B前的操作.
如果线程A调用B的start()方法(在线程A中启动B线程).start()操作Happens-Before线程B的任何操作.
线程终止原则
线程中的所有操作都happens-before对于此线程的终止检测,可以通过Thread::join()方法是否结束,Thread::isAlive()的返回值等手段检测线程是否已经终止执行.线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。对象终结规则
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
线程安全与锁
线程安全
并发首先要保证程序得正确性
安全的定义:《java并发编程实战》中坐着定义:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不用进行额外的同步,或者在调用方进行任何其他协调操作,调用这个对象的执行行为都能获得正确得结果,那么就称这个对象是线程安全的.
java语言中的线程安全
以多个线程访问共享数据为前提.将安全感程度依次分为:不可变,绝对安全,相对线程安全,线程兼容,线程对立
- 不可变:用final修饰的基本数据类型变量(如果为对象,则要求对象的属性都为final)
- 绝对线程安全:满足定义的,不过要求太高,java库中标注为线程安全的类也基本达不到此要求
- 相对线程安全:通常意义的线程安全,只要求单次调用为线程安全,但是对于一些特定顺序的调用,需要额外代码来保证线程安全
- 县城兼容:指的是本身非线程安全,但是可以在调用端正确地使用同步手段来调用,可以在并发环境中使用
- 线程对立:即使调用端采取了同步措施,还是无法在多线程时使用的代码。入Thread类的suspend(),resume().
线程安全的实现办法
阻塞同步(互斥同步)
blocking synchronization
进行线程阻塞和唤醒会带来性能开销。
悲观并发策略:总认为只要不去做正确地同步措施(加锁)则会带来并发问题。
synchronzied与lock,详情参见《java并发编程实战》
非阻塞同步
乐观并发策略:非阻塞同步(non-blocking synchronization)也被称为无锁(lock-free)编程。
硬件指令支持:
- 测试并设置(Test-and-Set)
- 获取并增加(Fetch-and-Increment)
- 交换(Swap)
- 比较并交换(Compare-and-swap)
- 加载链接/条件存储(Load-Linked/Store-Conditional,LL/SC)
无同步
可重入代码(reentrant code,纯代码 pure code):可以在代码执行的任何时刻中断它,转去执行另外一段代码.包括递归调用它本身.而在控制权返回后,原来的程序也不会出错.也不会对结果有任何影响.
可重入代码的特征:不依赖全局变量,存储在堆上的数据和工用的系统资源,用到的状态量都是由参数传入,不调用非可重入方法灯.
线程本地应用.
锁优化
jdk6之后的锁优化技术:适应性自选(adaptive spinning),锁消除(lock elimination),锁膨胀(lock coarsening),轻量级锁(lightweight locking),偏向锁(biased locking)
自旋与自适应自旋
jdk6之后已经默认开启自旋(-XX:UseSpinging),默认为10次,可使用-XX:PreBlockSpin修改
锁消除
jvm即时编译器运行期间,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁消除.判定依据主要来源于逃逸分析的数据支持.如果判断一段代码,在堆上的所有数据都不会逃逸出去被其他线程访问到,就把它当作栈上的数据对待,认为它们是线程私有,同步加锁自然也就无需再进行.
比如sringbuilder的append方法,其中就有同步块
锁粗化
如果一系列的连续操作都是对一个对象加锁解锁,即使没有线程竞争,频繁进出互斥体也会带来消耗,会导致不必要的性能损失
轻量级锁
jvm对象头
虚拟机对象的内存布局(主要为对象头部分):
hotspot虚拟机对象头(object header)主要分为两部分:
- 用于存储对象自身的运行时数据,哈希值(hashcode),GC粉黛年龄(Generational GC Age).在32/64位虚拟机分别占32/64bit.官方称为”mark word“.这部分是实现轻量级锁和偏向锁的关键.
- 存储指向方法区对象类型数据的指针.如果是数组对象,还会有个数组长度.
Mark word 被设计成一个非固定的动态数据结构,以便在极小的空间内存存储尽量多的信息.会根据对象的状态复用自己的存储空间.
加锁过程
- 代码即将进入同步块时:
如果此同步对象没有被锁定(锁标志位01),jvm先将在当前线程的栈帧中建立一个名为所记录(lock record)空间,用于存储锁对象的Mark Word拷贝.(官方为这个拷贝加了一个Displaced前缀,即displaced mark word) - 使用CAS操作尝试把对象的Mark word更新为只想Lock Record的指针.如果成功,则代表成功了,即代表线程拥有了这个对象的锁,并且对象的mark word中锁标志位转为00.如果更新十遍了.虚拟机首先会检测对象的mark word是否指向当前线程的栈帧.如果是,则说明当前线程已经拥有了这个对象的锁.否则锁门该锁对象已经被其他线程抢占了.如果出现两条以上的线程争抢一个锁,那么轻量级锁就膨胀为重量级锁,锁标志位就变为10,此使Markword 中存储的就是只想重量级锁的指针.后面等待锁的线程也将进入阻塞状态.
解锁过程
通用用cas操作来进行,如果Mark word 仍然指向线程的锁记录,使用Cas操把对象当前的Mark word和线程复制的Displaced Makr Word替换.替换成功,则整个同步过程完成,失败,说明其他线程尝试获取过该对象的锁.则会在释放锁的同时唤醒被挂起的线程.
偏向锁
偏向第一个获取锁的线程.(jdk6之前用-XX:+UseBiasedLocking)
锁对象第一次被线程获取的时候,锁标志位设置为01,偏向模式设置为1.同时使用CAS操作把获取导得这个所得线程的ID记录在对象的Mark Word中.如果CAS操作成功,持有偏向锁的县城以后每次进入这个锁的相关同步块时,虚拟机都可以不再进行任何同步操作(加锁解锁,对mark word的更新)
一旦出现另外一个线程去尝试获取这个锁的情况偏向模式则马上结束.根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式被设置为0),撤销后标志位恢复到未锁定(标志位为01)或轻量级锁状态(为00).后续按照轻量级锁方式.
hashcode与偏向锁
当对象进入偏向锁状态,对象头中Mark word的大部分空间(23个bit)都用于存储线程Id了,这部分空间占用了原有存储对象Hash码的位置.
java中,如果一个对象计算过hash码,就应该一直保持该值不变(强烈推荐但不强制),作为绝大多数hash值来源的Object::hashCode()方法,返回时对象的一致性hash(Identity Hash Code),这个值是能强制保证不变的.它通过在对象头中存储计算结果来保证计算过一次后,再次调用该方法取到的hash值永远不会再变化.因此当一个对象计算过一致性hash后,就再也无法进入偏向锁状态了.而当一个对象处于偏向锁状态,又收到需要计算其一致性hashcode的请求(指的是 Object::hashCode(),或者System::identityHashCode(Object)方法调用,而非重写的hashCode()方法),他的偏向状态立即会被撤销,并且锁会膨胀为重量级锁.