首先,先了解一下JMM
什么是JMM:即java内存模型,是一种概念约定。
关于JMM的一些同步的约定:
- 线程解锁前,必须把共享变量立刻刷回主存。
- 线程加锁前,必须读取主存中的最新值到工作内存中。
- 加锁和解锁是同一把锁。
先来看看八种内存交互操作:
根据上图,有了个问题:线程A中拿到主存中的值,做了修改之后,B线程能否知道线程A干的这件事,并且将修改的值响应给B线程?
对于上面的这个问题,其实就是java并发编程中的可见性问题!
而可见性就是靠JMM内存模型来解实现的。
JMM对这八种指令的使用,制定了如下规则(百度来的):
1.不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须 write
2.不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
3.不允许一个线程将没有assign的数据从工作内存同步回主内存
4.一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变 量实施use、store操作之前,必须经过assign和load操作
5.一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能 解锁
6.如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量 前,必须重新load或assign操作初始化变量的值
7.如果一个变量没被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
8.对一个变量进行unlock操作之前,必须把此变量同步回主内存。
言归正传,现在来看看Volatile关键字
Volatile是java虚拟机提供轻量级的同步机制,他有如下特点: 保证可见性、不保证原子性、禁止指令重排。
1.保证可见性
保证可见性是与JMM相关的。
例子:
public class JMMdemo1 {
private volatile static int num=0;
public static void main(String[] args) {
new Thread(()->{
while (num==0){
System.out.println(num);
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println(num);
}
}
运行结果:
.
.
.
.
.
.
0
0
0
0
0
0
0
0
0
0
0
0
1
2.不保证原子性
原子性:不可分割,线程a执行的时候,不能被打扰,也不能被分割;要么同时成功,要么同时失败
public class JMMdemo2 {
private volatile static int num=0;
public static void add(){
num++;
}
public static void main(String[] args) {
//创20个线程,每个线程做1000次add()
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
while(Thread.activeCount()>1) { //保证前面的线程都执行完
Thread.yield();
}
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
运行结果: main 11848 每次都不一样,但都不是预想的20000
原因:因为num++这个操作不是原子性操作,分为了:
1.获取到这个值 2.修改这个值 3.写回这个值
而在多线程环境下,显然是不安全的。
如何能实现原子性呢?
1.synchronized
2.lock
3.使用原子类( AtomicInteger)
以上3种方式的具体例子就不一一举例了,可以自行尝试。
3.禁止指令重排
指令重排:你写的程序,计算机并不是按照你写的那样去执行的。而是按如下过程执行:
源代码-->编译器优化的重排-->指令并行也可能会重排-->内存系统也会重排-->执行
例如:
Int x=1 //1
Int y=2 //2
X=x+2 //3
Y=x*x //4
我们所期望的执行顺序的1234,但实际上执行的时候可能会变成2134 1324。
但不可能是4123因为处理器在进行指令重排的时候,也考虑:数据之间的依赖性。
在多线程环境下,指令重排的问题:
例子:令A B X Y 都默认为0
线程A | 线程B |
X=A | Y=B |
B=1 | A=2 |
正常结果: X=0,Y=0; 但可能由于指令重排
指令重排后:线程A执行B=1后,线程B抢到cpu时间片,执行完了B线程中的两个指令,最后执行X=A。
线程A | 线程B |
B=1 | A=2 |
X=A | Y=B |
指令重排导致的结果为:X=2;Y=1
而Volatile(通过内存屏障)可以避免指令重排:
内存屏障(CPU指令)的作用:
- 保证特定的操作的执行顺序
- 可以保证某些变量的内存可见性
以上就是我学习voliate关键字的简单总结!!!