11.JMM
Volatile是java虚拟机提供的轻量级的同步机制,特点
1.保证可见性
2.不保证原子性
3.禁止指令重排
什么是JMM:
JAVA内存模型,不存在的东西,概念!约定!
JMM关于同步的规定
1.线程解锁前,必须把共享变量的值刷新会主内存
2.线程加锁前,必须读取主内存的最新值到自己的工作内存
3.加锁解锁是同一把锁
Java内存模型定义了以下八种操作来完成:
1.lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
2.unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
3.read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
4.load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
5.use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
6.assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
7.store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
8.write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
1.如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。
但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
2.不允许read和load、store和write操作之一单独出现
3.不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
4.不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
5.一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
6.一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
7.如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
8.如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
9.对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
针对图中的问题:B线程改了主内存中的内容,但是A线程还是用的自己工作内存中的东西,如何解决呢?
问题重现:
public class VolatitleDemo {
static int num=0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
重点:线程会一直卡死在这,即使main线程已经将num的数改为5,但是线程还是会卡死在这,不停的进行判断
while (num == 0) {
但是注意的是如果这里加入了代码:如输出代码等,会导致执行结束,推测原因是输出代码等有锁会导致该线程会重新拉取主存中的数据!
}
}, "线程A").start();
TimeUnit.SECONDS.sleep(5);
num=5;
System.out.println(Thread.currentThread().getName() + "内存模型中的内容:" + num);
}
}
volatile
1.保证可见性
1.可见性:(多个线程操作同一个变量时,当一个先执行的线程改动了这个变量的值时,应当通知其他线程变量的值已经被改动了,原来的值将不可用)
相同代码:只是加了volatile关键字
public class VolatitleDemo {
static volatile int num=0;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (num == 0) {
}
}, "线程A").start();
TimeUnit.SECONDS.sleep(5);
num=5;
System.out.println(Thread.currentThread().getName() + "内存模型中的内容:" + num);
}
}
2.不保证原子性(不可分割)
原子性:线程a在执行的时候,不能被打扰,也不能被分割,要么同时成功,要么同时失败!
样例如下:
public class VDemo02 {
//重点1:使用volatile 修饰,验证其的不保证原子性
private volatile static int num = 0;
public static void add() {
num++;
}
public static void main(String[] args) {
//重点2:创建20个线程,每个线程执行1000次num++操作,里面数值尽量设置大点,这样耗时较容易出现干扰!
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}, "线程" + i).start();
}
//重点3:判断上述线程是否执行完毕,因为java一般存在两个线程:Mian线程和gc线程
while (Thread.activeCount() > 2) {
//线程礼让
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + ":" + num);
}
}
结果:输出结果和预期的20000对不上
main:18629
结论:
num++在底层字节码文件如下,本身操作不是个原子性操作!所以会出现线程干扰的情况!
使用原子类解决上述问题:
上述问题在不加锁synchronized和lock的情况下,如何实现正确返回呢,使用原子类!
面试如果问到,如何不使用synchronzied和lock锁,而保证volatitle的原子性!可以回答使用原子类,原子类的底层原理CAS后面涉及!
代码改为:
public class VDemo02 {
//重点1:创建对应的原子类
private volatile static AtomicInteger num = new AtomicInteger();
public static void add() {
//重点2:调用原子类的+1操作!
num.getAndIncrement();
}
//重点3:其他未变
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
add();
}
}, "线程" + i).start();
}
while (Thread.activeCount() > 2) {
//线程礼让
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + ":" + num);
}
}
输出:和正确相同!
main:20000
原子类的 num.getAndIncrement();操作底层也并不是执行++操作,底层代码如下:
发现其调用的unsafe的方法,底层原理CAS后面笔记涉及!
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}