深入理解volatile

一.java内存模型

在说volatile之前,需要了解java的内存模型。java采用的是共享内存模型,线程之间的共享变量存储在主内存中,每个线程有一个私有的本地本地内存,本地内存存储了该线程读写共享变量的副本,如图

举个栗子:a = 100

一个线程需要修改a的值,必须先将a读到自己的本地内存,对a执行更新操作,再将a的值写回到主存中

二.并发编程中的三个概念

  1. 原子性:一个操作或多个操作,要么全执行,要么全不执行。最典型的例子就是银行转账问题,

假设A向B转账1000块钱,包含两个操作,A减去1000块钱,B加上1000块钱,假设这个操作不包含原子性,会发生一件很恐怖的事情,在A减去1000块钱后,操作突然中断,这导致A少了1000块钱,但是B没有增加,这1000块钱凭空消失,所有这两个操作必须具备原子性。

在java中,对变量的简单读取和赋值(将数值变量)才具备原子性

1
2
3
int a = 10 ;//原子性
int b = a;//非原子性
int count=a+1;//原子性
  1. 可见性: 当多个线程共享一个变量时,如果一个线程修改了该变量的值,其它线程会立刻知道

java中通过volatile实现可见性,被volatile修饰的变量,当它被修改时,它会保证修改的值会立即被写回主存,当有其它线程需要的时候,拿到的是最新值

  1. 有序性:程序执行的顺序会按照代码的顺序执行。在java中,编译器和处理器会对我们编写的代码优化,进行指令重排,指令重排不会影响单线程执行的结果,但是会影响多线程执行的结果。java中使用volatile可以禁止指令重排序。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //线程一执行的代码
    int a = 10;
    boolean start = true;

    //线程二执行的代码
    while(!start){

    }
    对a的操作

    由于线程一的两行代码没有依赖关系,所以可能打算指令重排,线程一先执行boolean start = true;当还没有执行int a = 10;,线程二开始执行,执行对a的操作,但是由于a的值不是10,不是a的操作需要的结果,程序会出错,使用volatile可以避免这个问题

三.volatile

案例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class TestVolatile {

private /* volatile */ List<String> list = new ArrayList<>();
public void t() {
while(true) {
if(list.size()==2) {
break;
}
}
}
public void t2() {
for(int i=0;i<3;i++) {
System.out.println("添加了第"+i+"个元素");
list.add("a");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
TestVolatile t = new TestVolatile();
new Thread(new Runnable() {
@Override
public void run() {
t.t2();
}
}).start();//线程一
new Thread(new Runnable() {
@Override
public void run() {
t.t();
}
}).start();//线程二
}
}

如果不使用volatile关键字修饰,程序会进入到死循环,因为当线程一执行list.add()方法时,线程二不能马上获得线程一添加元素后list.size()的大小,当线程一将程序执行完,list.size()=3,那线程二永远也不可能中断。

使用volatile,当线程一执行list.add(),方法时,会改变list.size()的大小,线程二会立刻获取到修改后的新值,当list.size()=2时,线程二就会结束。

案例二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class TestVolatile2 {

boolean flag = true;
public void t1() {
while(true) {
// System.out.println("111"); // 使用这句化程序会正常结束 1
// synchronized(this) { }//使用这句化程序会正常结束 2
int a = 10; //使用这句化程序不会正常结束 3
if(!flag) {
throw new IllegalArgumentException(Thread.currentThread().getName()+"停止");
}
}
}
public static void main(String[] args) {
TestVolatile2 t = new TestVolatile2();
new Thread(new Runnable() {
@Override
public void run() {
t.t1();
}
}).start();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.flag = false;
System.out.println(Thread.currentThread().getName()+"执行完毕");
}
}

使用上面的1,2语句都可以让程序正常结束,但是使用3不行。为什么呢,因为synchronized也保证了可见性,但是为什么1可以呢?看下面println源码,是不是恍然大悟

1
2
3
4
5
6
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}

四.volatile有没有保证原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class SynchronizedTest1 {
private volatile int count = 0;

public void synchronizedTest1() {

for(int i=0;i<1000;i++) {
count++;
}
}

public static void main(String[] args) {
SynchronizedTest1 t1 = new SynchronizedTest1();
for(int i=0;i<10;i++) {
new Thread(new Runnable() {
@Override
public void run() {
t1.synchronizedTest1();
}
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
System.out.println(t1.count);

}
}

10个线程每个线程累加1000,如果volatile可以保证原子性的话,那么最后的结果就是10000,但是最后的运行结果是小于10000,也就是说volatile没有保证原子性

分析:假设现在count=10;线程一将count=10读到自己的本地内存,线程一阻塞,线程二运行,线程二将count=10读到自己的本地内存,对count进行加一操作(count=11),但是还没写回到主存,线程二就被阻塞了,线程一开始执行,对count进行加一操作(count=11),将count=11写回到主存中,线程一执行完毕,线程二开始执行,但是由于线程二只剩写操作,不需要重新从主存读数据,直接将count=11写回到主存,线程二将线程一的值覆盖了。这就是最后的结果小于10000的原因。

总结:volatile保证了共享变量的可见性和有序性,但是没有保证原子性

文章目录
  1. 1. 一.java内存模型
  2. 2. 二.并发编程中的三个概念
  3. 3. 三.volatile
  4. 4. 四.volatile有没有保证原子性
|
载入天数...载入时分秒...