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

大纲

JUC 介绍

JDK 5 中增加了并发大师 Doug Lea 的并发库,这一引进给 Java 线程的管理和使用提供了极大的便利性。在 Java 5 提供的 java.util.concurrent(简称 JUC)包中,增加了在并发编程中很常用的实用工具类,用于定义类似于线程的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。JUC 提供了可调的、灵活的线程池,还提供了设计用于多线程上下文中的 Collection(即线程安全的集合类)实现等。常用的 JUC 包有以下几个:

  • java.util.concurrent
    • java.util.concurrent.locks
    • java.util.concurrent.atomic

内存可见性

什么是内存可见性问题

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

内存可见性问题的产生

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 是 Java 虚拟机提供的轻量级的同步机制,用来确保将变量的更新操作通知到其他线程。通常可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:

  • 对于多个线程,不是一种互斥关系
  • 不能保证变量状态的 “原子性操作”

volatile 拥有三大特性:

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

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

CAS 算法