JUC 高并发编程入门基础之一

volatile 与内存可见性

volatile 是 Java 虚拟机提供的轻量级的同步机制,它有三大特性:

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

什么是内存可见性问题?

内存可见性问题是,当多个线程操作共享数据时,彼此不可见。

内存可见性问题的产生

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
class ThreadDemo implements Runnable {

private boolean flag = false;

public boolean isFlag() {
return flag;
}

@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println(Thread.currentThread().getName() + " flag = " + flag);
}

}

public class VolatileTest {

public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();

new Thread(threadDemo, "T1").start();

while (true) {
if (threadDemo.isFlag()) {
System.out.println(Thread.currentThread().getName() + " flag = " + threadDemo.isFlag());
break;
}
}
}

}

上述代码输出 T1 flag = true 后,主线程会一直卡死在 while 循环的执行中,因为主线程感知不到 T1 线程对共享变量 flag 的更改。

内存可见性问题的解决

volatile 解决内存可见性问题

在多线程环境下,使用 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
39
40
class ThreadDemo implements Runnable {

// volatile 保证可见性
private volatile boolean flag = false;

public boolean isFlag() {
return flag;
}

@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println(Thread.currentThread().getName() + " flag = " + flag);
}

}

public class VolatileTest {

public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();

// 启动一个线程,更改共享数据
new Thread(threadDemo, "T1").start();

// 主线程读取共享数据
while (true) {
if (threadDemo.isFlag()) {
System.out.println(Thread.currentThread().getName() + " flag = " + threadDemo.isFlag());
break;
}
}
}

}

程序执行输出的结果:

1
2
T1 flag = true
main flag = true

synchonrized 解决内存可见性问题

synchronized 关键字的作用是让线程在进入方法或者同步代码块时自动获取锁,在退出时自动释放锁。当一个线程获取了锁之后,它会清空工作内存并从主内存中重新读取共享变量的值,这确保了线程读取到的变量值是最新的。当线程释放锁时,会将其在工作内存中对共享变量的修改刷写回主内存,以使其他线程可见。所以,使用 synchronized 也可以解决内存可见性问题,其底层是通过锁机制确保线程之间的同步和内存可见性。特别注意,由于 synchronized 是独占锁,在高并发场景下的性能较低,因此建议使用 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
39
40
41
42
class ThreadDemo implements Runnable {

private boolean flag = false;

public boolean isFlag() {
return flag;
}

@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println(Thread.currentThread().getName() + " flag = " + flag);
}

}

public class VolatileTest {

public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();

// 启动一个线程,更改共享数据
new Thread(threadDemo, "T1").start();

// 主线程读取共享数据
while (true) {
// 使用 synchronized 同步机制
synchronized (threadDemo) {
if (threadDemo.isFlag()) {
System.out.println(Thread.currentThread().getName() + " flag = " + threadDemo.isFlag());
break;
}
}
}
}

}

程序执行输出的结果:

1
2
T1 flag = true
main flag = true