Volatile是java虚拟机提供的<轻量级>的同步机制,synchronized太重了
保证了JMM的可见性
不保证JMM的原子性
禁止指令重排
JMM(java memory model),jmm本身是一个抽象的概念,并不真实存在 他描述的是一组规则或规范 (类似于12生肖中的龙并不真实存在 ) , 规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问形式
线程解锁前,必须把共享变量的值刷新回主内存
线程加锁前,必须读取主内存的最新值到自己的工作内存
加锁解锁是同一把锁
JVM运行程序的实体是线程,而每一个线程创建时JVM都会为其创建一个工作内存(栈),工作内存是每个线程的私有数据区域
java内存模型中规定所有的变量都存储在主内存,主内存是共享内存区域,所有的线程都可以访问,
但线程对变量的操作(读取赋值等)必须在工作内存中进行,
首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,
不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存完成,其简要访问过程如下图
举个例子:包饺子
背景:A,B,C 一家3口坐在一起包饺子,每个人都有自己的 饺子皮、饺子馅 包自己的饺子,3个人共用同一个砧板(主内存), 砧板上的饺子的数量就是共享变量
在第3步执行完成了以后,其他人现在并不知道饺子的总数是多少,此时 我就需要通知其他人,”来看一眼啊 饺子的数量现在变多了一个“ 如果来得及的话,其他人执行相加操作的时候, 就需要在我操作后的基础上 +1 而不是在原来的基础上 +1
简单的说:A线程修改了共享变量,其他线程要马上就能知道共享变量中的值,立刻收到最新消息这个机制叫可见性。
代码示例:
思路:开启2个线程,在使用volatile 和不适用volatile的情况下,A修改 方法中变量的值,并将此值设置为循环的条件,如果循环终结 则说明保证了可见性,如果循环不终结,则说明没有保证可见性
public class Test {
public static void main(String[] args) {
OkSee okSee = new OkSee();
new Thread(() -> {
System.out.println("thread start");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
okSee.setVal();
System.out.println("set num val");
}, "test").start();
//怎样得到当前线程数量
while (okSee.getNum() == 0L) {
}
}
}
@Data
class OkSee{
private volatile Long num = 0L;
public void setVal() {
num = 10L;
}
}
不使用volatile,可以看到程序设置值之后,不会终止,还是会继续执行
在上述例子的基础上: 执行第3步操作的时候,如果A、B, 2个人同时包好了个字的饺子,以同样的速度数出了饺子目前的总数,同时自己把共享变量的值 在原来的基础上 +1, 那么问题来了,
操作结果: 主内存相较于正常情况,是少了1个的,那么怎么解决呢,juc包下面的 atomic包下面的工具类了 这个后续会详细讲解
案例:使用20个线程,每隔线程跑1000次,那么最终得到的结果理论上应该是2w
//本案例中使用到了lombok包,需要自行导入
public class Test {
public static void main(String[] args) {
AtomicTest atomicTest = new AtomicTest();
for (int i = 1; i <= 20; i++) {
new Thread(() ->{
for (int j = 1; j <= 1000; j++) {
atomicTest.add();
}
}, String.valueOf(i)).start();
}
//判断程序是否执行完成,如果执行完成则终止,如果没有执行完成,则继续执行子线程
//默认是main线程, 然后后台有一个GC线程
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(atomicTest.getNum());
}
}
@Data
class AtomicTest{
private Integer num = 0;
public void add(){
num ++;
}
}
不管num字段是否添加volatile字段,每次执行的结果都不同,可能会达到2w
使用juc atomic包下的工具类,例如对上面案例进行修改
//本案例中使用到了lombok包,需要自行导入
public class Test {
public static void main(String[] args) {
AtomicTest atomicTest = new AtomicTest();
for (int i = 1; i <= 20; i++) {
new Thread(() ->{
for (int j = 1; j <= 1000; j++) {
atomicTest.add();
}
}, String.valueOf(i)).start();
}
//判断程序是否执行完成,如果执行完成则终止,如果没有执行完成,则继续执行子线程
//默认是main线程, 然后后台有一个GC线程
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(atomicTest.getNum());
}
}
@Data
class AtomicTest{
private AtomicInteger num = new AtomicInteger(0);
public void add(){
num.incrementAndGet();
}
}
计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排
即不管怎么重排序,程序的执行结果不能改变
数据依赖性
多线程中线程交替执行,由于编译器指令重排的存在,2个线程使用的变量能否保证一致性是无法确认的,结果无法预测
举个例子:
double pi = 3.14; // A
double r = 1; // B
double s= pi * r * r; // C
上面的语句,可以按照 A->B->C
执行,结果为 3.14
,但是也可以按照 B->A->C
的顺序执行, – 指令重排序
因为 A、B
是两句独立的语句,而 C
则依赖于 A
、 B
,所以 A
、 B
可以重排序,但是 C
却不能排到 A
、 B
的前面。 – 数据依赖性
JMM
保证了重排序不会影响到单线程的执行,但是在多线程中却容易出问题。
int a = 0;
boolean flag = false;
public void write() {
a = 2; //1
flag = true; //2
}
public void multiply() {
if (flag) { //3
int ret = a * a; //4
}
}
假如有两个线程执行上述代码段,线程 1
先执行 write
方法 线程 2
再执行 multiply
,最后 ret
的值一定是 4
吗?结果不一定:
如图所示, write
方法里的 1
和 2
做了重排序,线程 1
先对 flag
赋值为 true
,随后执行到线程 2
, ret
直接计算出结果,再到线程 1
,这时候 a
才赋值为 2
,很明显迟了一步。
这时候可以为 flag
加上 volatile
关键字,禁止重排序,可以确保程序的“有序性”,也可以上重量级的 synchronized
和 Lock
来保证有序性,它们能保证那一块区域里的代码都是一次性执行完毕的。