我们不希望处理器在大部分时间都处于等待其他资源的空闲状态,为了节省,节约资源。我们选择让计算机同时处理多件任务
《深入理解Java虚拟机》这本书中给我们讲述了一个概念:
Java内存模型的主要目的:是定义程序中各种变量的访问规则。即关注在虚拟机中把变量值存储到内存和从内存中取出变量值的底层细节
然后它讲述,这里面的变量和Java中的变量还不一样,
前者包括了
但是不包括局部变量和方法参数
但是后者
它的线程是私有的,不会被共享,自然不会发生竞争关系
这段话有点拗口
我理解的大致意思是:
java虚拟机中的变量是所有线程共有,比如你定义100张车票,然后弄2个线程,让他们去竞争这100张车票
会出现谁去领取第一张车票,谁去领取第2张车票这种问题。这个车票就是我们刚才java虚拟机中说到的变量
然后java中的变量就理解为多线程中你某个线程定义的private变量,其他线程没办法访问读取它
java虚拟机中的规定所有变量都存储到主内存中,每条线程都有属于自己的工作内存
工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取,赋值)都在工作内存中完成,不能直接读取主内存的数据
如果你使用volitale这个关键字的话,会少掉两个步骤
volatile
变量时,会直接从主内存中读取最新的值,而不是从工作内存中读取。这确保了对该变量的读取操作能够获取最新的值。volatile
变量进行写入操作时,会立即将写入的值刷新到主内存中,而不是仅仅更新自己的工作内存。这确保了对该变量的写入操作对其他线程是可见的。就是有了这个关键字,保证了多线程的可见性和有序性
Java 程序中所有线程共享的内存区域,它存储了对象实例、静态变量、类信息等数据。主内存可以被多个线程同时访问。
工作内存是线程私有的内存区域,每个线程都有自己的工作内存。工作内存包含了线程运行时所需的数据,例如线程栈、本地变量等。
Java内部定义了8个方法
lock | 加锁,把一个变量标识为一条线程独占的状态 | 主内存 |
---|---|---|
unlock | 把刚才加的那个锁给去掉 | 主内存 |
read | 把一个变量的值从主内存传送到工作内存中 | 主内存 |
load | 把read操作从主内存得到的变量值放入到工作内存的变量副本中 | 工作内存 |
use | 把工作内存的变量值传递给执行引擎 | 工作内存 |
assign | 把从执行引擎获得的值传递给工作内存 | 工作内存 |
store | 把工作内存的变量传送给主内存 | 工作内存 |
write | 把工作内存的变量值放入主内存的变量中 | 主内存 |
lock和unlock肯定不用想了,这个很好理解
read是把主内存的变量 传送到 工作内存中
而load是把主内存的变量 读到 工作内存中的副本中
read和load的不同的是,read只是把主内存的变量 传送到 工作内存,然后就没了,比如你要送某个人回家,你把她送到她家门口就没了,你也不能指望你把她送到家里面
而load在read把她送到家门口的基础上,然后又把她送到家里面
就是这样的道理
下面的store和write也是这个样子的,不过read是把主内存的变量传给工作内存;而store是把工作内存的变量传给主内存
剩下就剩use和assign了,其实use就可以理解为使用一个变量;assign相当于给变量赋值
这两个都在工作内存中执行
如果要把一个变量从主内存复制到工作内存,那么你得先执行read然后执行load
如果你要把工作内存的值同步到主内存,那么你得先执行store再执行write
java内存模型要求上面的两个操作按顺序执行,但不要求是连续的
比如说我现在先:
read a
然后不用直接load a,你可以再read b
可以出现read a read b load b load a
我们在主内存和工作内存中讲过,如果不用volatile的话,直接写的话,它是先把变量放到自己的工作内存,然后再把工作内存的东西放到主内存
但是我们用了volatile的话就会少掉把工作内存的东西放入主内存的这个操作
说到这个之前我们先简单聊聊多线程并发状态下的3个特性
多线程并发状态下的三大特性分别是:
1.原子性
2.有序性
3.可见性
一般说到原子性就说原子性和原子操作,在《Android进阶之光》中我对它的理解就是,一个过程,要么你一次性执行完,要么不执行
经典的原子操作就是赋值操作,还有提供的一些原子性操作的方法
在IPC方式中,讲到过一个叫作:
private AtomicBoolean mIsServiceDestroyed = new AtomicBoolean(false);
这里mIsServiceDestroyed就是一个具有原子性的一个boolean
前面举得都是有原子性,现在举一些没有原子性的,最常见的就是自增操作
为什么自增操作不是一个原子性操作呢?
你可以把自增操作看成4个步骤
《Java虚拟机》中用字节码的方式给我们写了一下,但我看不懂。反正就和我上面是一个意思
可以明白自增操作不是一个原子操作,也不具有原子性
Java内存模型直接保证的原子性变量操作包括:read,load,assign,use,store,write
那么为什么保证原子性呢,比如这样
public class rwo {
public static int i = 0 ;
public static void add(){
for(int w = 0;w<=100;w++) {
i++;
}
}
}
public class one {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
add();
}
});
thread.start();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
add();
}
});
thread1.start();
System.out.println(i);
}
}
我们原本是想让结果输出202,但是运行之后输出来的结果各不一样,有时候是202,有时候又是100多,这就是为什么我们要保证原子性,如果不保证原子性,它可能自增操作还没执行完,就执行别的操作了
可见性就更容易了,我们刚才扯了那么久的主内存,工作内存就是和这个有关的
一个线程的工作内存的变量,别的线程没办法获得变量
我们为什么要保证多线程的可见性呢?试着想一下,如果一个线程改变了一个值,但是另一个线程也在调用它,不保证可见性的话,可能原本的是4,进行+1操作是5,但是你另一个线程调用过来那个变量还是4,就会导致错误
有序性可能会颠覆以前我们的思考逻辑
在java中存在一种叫做:指令重排序的现象
比如:
a = 1;
b = 2;
a++;
b++;
这段代码我们的理解就是给a赋值为1,b赋值为2,然后执行**a++操作后面执行b++**操作
但是实际上可能会出现,给a,b赋完值后,先执行b++然后执行a++
在单线程中指令重排序不会影响正确性,但是会影响多线程并发的正确性
比如说
public class ReorderingExample {
private static int x = 0;
private static int y = 0;
private static int a = 0;
private static int b = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
a = 1;
x = b;
});
Thread thread2 = new Thread(() -> {
b = 1;
y = a;
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
// 输出的结果可能为 (0, 0),表示发生了指令重排序
System.out.println("(" + x + ", " + y + ")");
}
}
我们本想输出(1,1)但是最后输出(0,0)这就是因为出现了指令重排序
先执行了x = b这个操作,然后执行y = a这个操作,然后是a = 1,b = 1最后输出(0,0)
volatile可以说是Java虚拟机提供的最轻量级的同步机制
volatile可以保证多线程的可见性和有序性,那么原子性呢?
继续跑我们刚才的这段代码
public class rwo {
public volatile static int i = 0 ;
public static void add(){
for(int w = 0;w<=100;w++) {
i++;
}
}
}
public class one {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
add();
}
});
thread.start();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
add();
}
});
thread1.start();
System.out.println(i);
}
}
我们发现打印出来的和刚才一样,要么202,要么100多,要么不到100
但是我们在add方法前面加上synchronized
public synchronized static void add(){
for(int w = 0;w<=100;w++) {
i++;
}
}
会发现,无论你跑了多少次,最后结果都是202
这一点就说明了volatile无法保证原子性,也说明了自增操作不是一个原子操作
我们说volatile可以保证操作的可见性,它可以少掉将变量放入工作内存再放到主内存
而是直接放到主内存
但是为什么无法保证原子性呢?
我们还是举刚才的例子
假设现在的i的值为0
第一个线程执行i++,它先读取i的原始值,然后第一个线程被阻塞了。然后执行第二个线程,这时候第二个线程将所有操作执行完,i的值已经变成了1。第一个线程再去执行后面的操作的时候因为之前已经读取过i的值了,就不会继续去读取,进行**+1操作**,结果第1个线程最后执行完后的结果还是1。
本来我们想的是2,结果因为没保证原子性,出来的结果还是1
1.已经可以保证操作的原子性
比如这段代码
public class Example {
private volatile boolean flag = false;
public void setFlag() {
// 更新状态标志位
flag = true;
}
public void doSomething() {
// 检查状态标志位
if (flag) {
// 执行相应操作
}
}
}
flag的赋值操作是原子操作,只要满足了原子操作,我们就可以用volatile关键字了
2.DCL双重锁检验
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
// 私有构造函数
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
一般它们会问volatile的作用与两个if的作用
volatile的作用就是保证了可见性与有序性,可是下面不是已经有synchronized 了嘛?为什么还要加volatile,很简单,因为instance在这个锁的外面,要是在里面的话,就不需要加volatile了
现在说一下两个if的作用
因为它带有一个lock前缀,它的作用就是将本处理器的缓存写入内存,该写入操作会引起别的处理器或者别的内核无效化其缓存,相当于store和write操作,让volatile变量的修改对其他处理器立即可见
《深入理解java虚拟机》中说到volatile修饰的变量,赋值后会多执行一个lock addl $0x0,(%esp)操作,这个操作相当于一个内存屏障,指令排序时不能把后面的指令排序到内存屏障之前的位置,只有一个处理器访问的时候,并不需要内存屏障;但如果有2个或者以上的多处理器访问同一内存,其中有一个在观察另一个,这个时候就需要内存屏障了
虽然可能发生指令的重排序,但是处理器必须正确处理指令依赖情况保障程序能够得出正确执行结果
比如:指令1把地址A的值+10,指令2把地址A的值×2
指令3把地址B的值减去3
1,2这两个指令有依赖关系。但是1,3;2,3没有这种关系
1与2不能乱排,但是2,3;1,3可以乱排
只要保证了处理器执行后面依赖到A,B的值的操作时能获得正确的A与B即可
因此用volatile修饰后把修改同步到内存,意味着前面的操作都已经执行过了,因此出现指令重排序无法越过内存屏障的效果
内存屏障之前的指令都会在内存屏障之前执行,而在内存屏障之后的指令都会在内存屏障之后执行,保证了操作的有序性。内存屏障之前的指令都会在内存屏障之前执行,而在内存屏障之后的指令都会在内存屏障之后执行,保证了操作的有序性。