目录
cas:乐观锁,假设没有冲突去尝试,直到成功
synchronized :悲观锁,非公平锁(随机唤醒线程,或者唤醒全部),有锁的升级
lock : api层面的锁
jvm 内存模型,实际上就是操作系统的一个内存池
读volatile修饰的变量 getstatic
每次读volatile修饰的共享变量都会从主内存中读取,然后再工作内存中生成一个副本,
变量存在一个栈空间中。
写volatile修饰的变量 使用putstatic
内存地址中加的锁是 cas 锁,
虚拟机栈中对变量进行修改,然后在主内存地址中对该变量的地址加锁,变成线程独占,
将修改后的值,写入到主内存中,解锁,实现了内存可见性。
内存可见性:内存模型volatile 是基于 Memory Barrier(内存屏障)实现的,jvm 在写入由volatile 修饰的变量后会插进一个Write-Barrier
指令,在读这个字段之前插入一个Read-Barrier
指令,就可以保证线程a写入变量后,其他线程访问该变量拿到的都是最新值,在写入a 之前的的写操作(前提是a已经写入成功),对其他线程也是可见的。因为 Memory Barrier 会刷出 cache 中所有先前的写入。
jvm 中虚拟机栈:一个线程一个虚拟机栈,随线程的创建而创建,随线程的消失而消失
每个虚拟栈中有多少个栈帧:每调用一次方法就会产生一个栈帧
jvm主内存:堆区+方法区
jvm工作内存:虚拟机栈
volatile是如何保证有序性的:禁止指令重排
禁止指令重排的过程:
需要使用一个插件才能更加清晰的看到不同,安装 jclasslib 插件,重启idea
使用 -p -v -l 参数,进行反编译,通过-v 参数输出了反编译的附加信息
有 volatile
没有volatile,
通过对class 文件进行反编译:生成 上面 Code 代码块的内容
可以看到不管是否是带有 volatile 生成的汇编内容是一样的。
之所以能识别是带有volatile,是由于jvm 会读取 Access flags 的内容
通过 cache->is_volatile() 进行判断是否含有volatile
指令重排:
1.编译期指令重排 jvm对代码进行编译优化,会发生指令重排
2.运行期指令重排 (cpu乱序执行带来的):JVM执行的时候,对有依赖性的语句,不会进行重排,对没有依赖性的指令都有可能会发生重排。
屏障:禁止指令重排
1.编译期,编译屏障
2.运行期,内存屏障
为什么有内存屏障:
因为CPU写内存机制有两种,
1. write through 异步写
①CPU将写请求写入 store buffer 中
②CPU空闲的时间将写的数据输入内存(等待空闲的时候出现延迟)
2. write back 同步写
① cpu将写请求写入 store buffer
② 将数据输入内存,直接输入
当有了内存屏障的,在使用异步的方式,写入store buffer中后,将cpu总线上锁,此时CPU 就空闲了,将数据写入内存,这样就达到了同步写的效果。保证了读写的有序性(只有cpu总线解锁了才能进行读操作)。
happens-before:
定义happends-before 规则,就是为了不让jvm对代码进行编译优化,保证并发编程的正确性
规则为:
1.程序的顺序性 ,手动对程序推演的结果一定==编译器最后执行的结果,但是编译器不一定按照推演的顺序编译
2.volatile,对 volatile 变量的写操作 在 读操作之前
3.传递性,a 在b 之前,b在c之前,a一定在c之前
4.锁规则,先解锁,再加锁
5.线程start()规则,线程a 调用线程b,则 b可以看到在调用b之前的操作
6.线程join()规则,线程b 加入到线程 a, 线程b执行完后,线程a 可以看到线程b 的所有操作。
as-if-serial 语义:
编译器、runtime和处理器都必须遵守as-if-serial语义,不会对存在数据依赖关系的操作做重排序