一、用图说话
在这里插入图片描述- 问题思考:
为什么需要volatile这个关键字。通过上图我们可以看出,cpu为了获得更快的速度,是允
许线程对于共享内存中的共享变量进行私有拷贝的,也就是说java使用主内存来保存变量
的值,而每个线程有自己独立的工作内存,这样就造成了,线程对自己拷贝的那一份值进
行一些操作的时候,会造成工作内存变量拷贝的值和主内存中不一致的情况。
二、情景在线
- 当执行i=10++操作时,干了什么事呢?
底层第一步需要先load i到线程的工作内存中,在对i进行10++操作,最后进行赋值,最后
刷入主内存中。
1、设想我们准备两根线程执行这个操作,我们希望,最后得到的值为12,因为想当然第
一次,对10进行了加一操作,然后对产生的结果11在次加一操作,不就是12吗?
2、但是会存在这种情况,线程一和线程二同时将i load到线程的工作内存中,线程一执行
了对i的加一操作,然后将结果11写入主存中,但是线程二现在的工作内存中i的值还是为
10,继续执行加一操作,得到的值还是为11,继续同步到主存中,最后尴尬的是结果还是
11,这是因为每个线程的工作内存对其余的为不可见。
三、多线程的三大特性。
- 可见性
对于volatile关键字,今天主要讨论可见性和有序性这个特点,正因为java内存模型的设计,对于不同线程的 工作内存,是属于线程间的私有内存,是不可见的,那么我们如何确保,一个线程修改了共享变量 中的值以后,其他线程能立刻看见。 1、java是如何解决这个问题的? java提供了volatile这个关键字,来保证可见性问题,当一个变量被这个关键字修饰时,它会保证 修改的值会立马刷入主存中,其他线程去读取这个值的时候,会拿到这个值的最新值,其实我个 人猜想,java是在主存中加了一块内存屏障,当线程对这个操作未完成的时候,其他线程访问不 了这个块内存,在外等待。
- 程序例子
public static int i=20;
public static void main(String[] args) {
Thread t1=new Thread(()->{
i++;
});
t1.start();
Thread t2=new Thread(()->{
i++;
System.out.println(Thread.currentThread().getId()+":"+i);
});
t2.start();
System.out.println("---------"+i);
}
}
多执行几次会发现输出的结果和预想的不一样。但是对i加上了volatile关键字后,在看看结果。
- 有序性:
在代码执行的过程中,代码执行的顺序是否是按照代码编写的先后顺序执行的呢?
答案是不一定的,cpu为了提高运行的效率,会对指令进行重排序,他最终会保证数据
最后的结果为正确。
直接上代码:
int i=0;
boolean flag=false;
i=10; //语句1
flag=true; //语句2
//对于语句1和语句2执行的顺序会1->2吗?其实不一定,对于语句1和语句2谁先之执行对程序其实没
多大影响。
** 但是注意的是:cpu不是🐷,不会瞎搞指令重排,对于有关联的操作,比如操作A,需要操作B的结果,
那么操作A就不会在操作B的前面。
- 思考指令重排会对多线程操作造成什么影响呢?
很容易理解,对于单线程来说,其实指令的重排并没有什么影响。
package com.pjw.Thread.vo;
public class Reorder {
static boolean flag=false;
public static void init(){
System.out.println("初始化操作完成了");
}
public static void main(String[] args) {
Thread t1=new Thread(()->{
init(); //1⃣️
flag=true; //2⃣️
});
Thread t2=new Thread(()->{
while (flag){
try {
System.out.println("进入了方法");
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("没进入while循环");
});
t1.start();
t2.start();
//运行以上的代码,在多次测试后会发生两种情况,对于1,2两句指令,没有特殊的关联,所以谁先运行
你我也不知道,只有cpu知道,那么对于正常的流程,语句1先运行进行初始化,然后对flag进行赋值,是
正确的,但是如果语句2先运行,那么结果是不是就错误了呢?
我们加上volatile关键字以后在看一看效果。
//所以得出结论,有序性对于多线程程序也是需要的,没法保证有序性会造成程序的错误。
}
}
- java中的有序性:
在java中,允许出现指令重排的,对于多线程来说指令重排会导致一些不可预见的错误。
java中可以通过volatile关键字来禁止指令重排或使用一些锁机制来保证有序性,另外最近
在翻阅jsr规范的时候了解到java内存模型保证了一些有序性,通常叫做happens-before原
则
介绍一下:
Two actions can be ordered by a happens-before relationship.
If one actionhappens-before another, then the first is visible to and ordered before the second.
上述为概论:简单的翻译一下,就是两个动作如果符合happens-before原则,第一动作必先发生在第二个
动作以前,并且对其可见。
1、If x and y are actions of the same thread and x comes before y in program order,then hb(x, y);
(对于同一个线程中两个动作,写在前面的必先发生在后面的以前。)
2、If hb(x, y) and hb(y, z), then hb(x, z)
(传递规则,如果x先于y,y先于z,则x先于z)
3、An unlock on a monitor happens-before every subsequent lock on that monitor
(一个锁释放操作先于后面对同一把锁的获得操作)
4、A write to a volatile field (§8.3.1.4) happens-before every subsequent read ofthat field.
(一个volatile的写操作先于读操作。)
5、A call to start() on a thread happens-before any actions in the started thread.
(一个线程的start方法,先于这个线程的任何方法)
6、All actions in a thread happen-before any other thread successfully returns froma join() on that thread.
(线程中的所有操作先于这个线程的中止检测,比如说join()方法或islive()方法)
7、There is a happens-before edge from the end of a constructor of an object to thestart of a finalizer
for that object.
(线程的初始化操作先于他的回收操作,想想如果我还没初始化就被干掉,我得郁闷死啊)
解释一下第一条:我们刚才的代码不就是同一个线程的中两个动作吗?还是指令重排了啊,因为第一个只保证
单线程的情况下,对于多线程是不保证的。
四、作用
- volatile关键字到底有什么作用?
1、保证了可见性,一旦一个共享变量被其修饰,保证一个线程操作以后,会立马刷新到内存
中去,保证其他线程可以看见。
2、禁止了指令的重排;(两层意思) - 在程序读到这个关键字的时候,在其前面的操作更改肯定全部完成了,且结果对后面必须可见。
- 在进行指令优化时,不能将volatile操作的语句放在后面执行,也不能把volatile的后面语句放在前面执行。一个内存屏障的作用吧我感觉。
3、思考对于线程的三大特性原子性,volatile可以保证吗?先看下面的代码?
package com.pjw.Thread.vo;
public class AtomicDemo {
public volatile int inc=0;
public void increase(){
inc++;
}
public static void main(String[] args) {
final AtomicDemo demo=new AtomicDemo();
for(int i=0;i<10;i++) {
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
demo.increase();
}
};
}.start();
}
System.out.println(demo.inc);
//实验发现,每次输出的结果都不到1000,是因为volatile只保证了可见性,没有保证原子性。
inc++不是原子操作,如果线程一只是load完inc以后,并没有++操作,就被等待,其实对于其他线程
拿到的数据还是线程一的原始数据,没有达到我们需要的效果。其实这样可以使用atomic包或锁,保证原子操作下一节介绍CAS等操作。
}
}
五、解析以前单例模式的困惑。
- 为什么单例模式会使用volatile修饰instance,原因是防止指令重排;
instance=new Instance()不是一个原子操作。
1、new关键字给instance在heap中分配内存;
2、调用构造函数进行初始化;
3、将引用指向对象。
但是二三操作的顺序我们是不能保证的,一旦3先执行于2,这时候有线程过来取,得到的
instance不是null,但是还没有初始化,从而导致调用报错。
六、总结
细细研究还是很多东西的,路漫漫其修远兮,吾将上下而求索。
网友评论