20Java多线程之线程安全关键字

java中与线程安全有关的关键字:volatile、synchronized

volatile

Java中Volatile关键字详解

可见性

volatile修饰的变量不允许线程内部缓存(保证可见性),即直接修改内存,所以对其他线程是可见的。

变量经过 volatile 修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,这个不需要过多了解,但是加了这个指令后,会引发两件事情:

  1. 将当前处理器缓存行的数据写回到系统内存。
  2. 这个写回内存的操作会使其他处理器缓存数据的内存地址无效。

  1. 对于写操作:对变量更改完之后,要立刻写回到主存中。
  2. 对于读操作:对变量读取的时候,要从主存中读,而不是缓存。

问题来了,既然它可以保证修改的值立即能更新到主存,其他线程也会捕捉到被修改后的值,那么为什么不能保证原子性呢?

比如 volatile int a = 0; 之后有一个操作 a++; 这个变量a具有可见性,但是 a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。

Java中只有对基本类型变量的赋值和读取是原子操作,如 i = 1; 的赋值操作,但是像 j = i; 或者 i++; 这样的操作都不是原子操作,存在线程安全问题。比如先读取 i 的值,再将 i 的值赋值给 j ,两个原子操作加起来就不是原子操作了。

所以,一个变量被volatile修饰,保证了每次读取的变量值是最新的。像变量自增这样的非原子操作,这个变量就不具有原子性了。

有序性

volatile 规定 禁止指令重排序,为了保证数据的一致性。

可见性示例

示例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("ThreadA开始执行...");
while (flag) { // 等同于 while (true)
// Thread.yield(); // 其实这里sleep或yield一下就不需要用volatile修饰了
if (!flag) {
System.out.println("跳出循环");
break;
}
}
System.out.println("ThreadA结束执行。。。");
}, "ThreadA").start();

TimeUnit.SECONDS.sleep(1);
flag = false;
System.out.println("标识已经变更");
}
}

输出:

1
2
ThreadA开始执行...
标识已经变更

示例2

示例1变形,改变 while 循环体内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("ThreadA开始执行...");
while (flag) {
System.out.println("跳出循环");
}
System.out.println("ThreadA结束执行。。。");
}, "ThreadA").start();

TimeUnit.SECONDS.sleep(1);
flag = false;
System.out.println("标识已经变更");
}
}

输出:

1
2
3
4
5
6
ThreadA开始执行...
跳出循环
...
跳出循环
标识已经变更
ThreadA结束执行。。。

示例3

继续改变 while 循环体内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("ThreadA开始执行...");
int i = 0;
while (flag) {
i++;
}
System.out.println("ThreadA结束执行。。。");
}, "ThreadA").start();

TimeUnit.SECONDS.sleep(1);
flag = false;
System.out.println("标识已经变更");
}
}

输出:

1
2
ThreadA开始执行...
标识已经变更

总结

有以上 3 个示例可得出,flag 的可见性会根据 while 循环体内容儿变化,不知道为什么,请指教。

所以为了保证 flag 可见,用 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
public class Test {
// volatile不保证原子性
// 原子性:保证数据一致性、完整性
private static volatile int num = 0;

public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 2; j++) {
new Thread(() -> {
for (int i = 0; i < 10000; i++) {
Test.num++;
}
}, String.valueOf(j)).start();
}

// 后台默认两个线程:一个是main线程,一个是gc线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "final num result = " + Test.num);
Thread.sleep(5000); // 由于不了解Thread.activeCount(),担心还有线程未执行完,所以休眠后再输出一次,事实证明Thread.activeCount()可靠
// 如果volatile保证原子性的话,最终的结果应该是20000。但是每次程序执行结果都不等于20000。
System.out.println(Thread.currentThread().getName() + "final num result = " + Test.num);
}
}

输出:

1
2
3
main final num result = 17114
main final num result = 20000
main final num result = 19317

三次执行,都是不同的结果。

为什么会出现这种呢?因为 num++ 被拆分成3个指令:

  • 执行 getfield 拿到主内存中的原始值 num。
  • 执行 iadd 进行加1操作。
  • 执行 putfield 把工作内存中的值写回主内存中。

当多个线程并发执行 putfield 指令的时候,会出现写回主内存覆盖问题,所以才会导致最终结果不为 20000,所以 volatile 不能保证原子性。

总结

  • volatile可用于内存访问的可见性,保证每次读的都是最新写的数据
  • volatile不具备原子性,原子功能需要使用Atomic类
  • 原子操作和原子操作合并之后,不是原子操作。

synchronized

什么是同步锁

sychronized 采取的同步策略互斥同步

在每次获取资源之前,都需要检查是否有线程占用该资源。已经进入的线程尚未执行完,将会阻塞后面其他线程。

同步锁的本质是对象实例。多线程安全需保证每个线程使用的是同一个锁对象

同步锁保证多线程下的每个操作都是原子性的。

同步锁、同步监听对象、同步监视器、互斥锁 它们都是同步锁,说的是一个意思。

弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

1、对象是什么?

我们可以随便创建一个对象试试。

2、需要同步的代码是哪些?

多条语句操作共享数据的代码部分给包起来

注意:

  • 同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。

  • 这里的锁对象可以是任意对象。多个线程必须是同一把锁

使用方式

同步代码块(对象锁、类锁)

格式:

1
2
3
4
5
6
7
8
9
10
11
synchronized(对象) { // 对象锁
// 需要被同步的代码;
}

synchronized(this) { // 对象锁
// 需要被同步的代码;
}

synchronized(类.class) { // 类锁
// 需要被同步的代码;
}

优化 SellTicket :

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
43
44
45
46
47
48
49
50
51
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}

class SellTicket extends Stock implements Runnable {

/**
* 创建锁对象
*/
private Object objLock = new Object();

@Override
public void run() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 已售票数小于库存,就进行售票
while (sellTickets < originalStock) {
// synchronized (new Object()) 不同锁对象
// synchronized (this) 对象锁
// synchronized (Object.class) 类锁
synchronized (objLock) {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
stopWatch.stop();
System.out.println(Thread.currentThread().getName() + "总耗时: " + stopWatch.getTotalTimeNanos());
}
}

同步方法(对象锁)

把同步加在方法上。

这里的锁对象是this

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}

class SellTicket extends Stock implements Runnable {

@Override
public void run() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 已售票数小于库存,就进行售票
while (sellTickets < originalStock) {
sell();
}
stopWatch.stop();
System.out.println(Thread.currentThread().getName() + "总耗时: " + stopWatch.getTotalTimeNanos());
}

// 下面两个sell方法的锁对象相同
private synchronized void sell() {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void sell() {
synchronized (this) {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t" +
"\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

静态同步方法(类锁)

把同步加在方法上。只需要在同步方法上加一个 static

这里的锁对象是当前类的Class对象

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st1 = new SellTicket();
SellTicket st2 = new SellTicket();
SellTicket st3 = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st1, "窗口1");
Thread t2 = new Thread(st2, "窗口2");
Thread t3 = new Thread(st3, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}

class SellTicket extends Stock implements Runnable {

@Override
public void run() {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 已售票数小于库存,就进行售票
while (sellTickets < originalStock) {
sell();
}
stopWatch.stop();
System.out.println(Thread.currentThread().getName() + "总耗时: " + stopWatch.getTotalTimeNanos());
}

// 类锁
private static synchronized void sell() {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 类锁
private void sell() {
synchronized (this.getClass()) {
if (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t" +
"\t" +
"原始库存" + originalStock + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

Double-Check单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton(); // 1
}
}
}
return singleton; // 2
}
}
// Double-Check的目的是延迟初始化单例的一种方法
// 使用volatile关键字的主要目的是防止指令重排序。如果volatile不加入,jvm可以对指令进行重排,与类锁无关,过程1可能被拿到过程2进行延迟初始化,这样就违背了Double-Check目的与原则。
// 第一层if判断是为了减少多次synchronized,减少每次都同步,提升性能
// 第二层if为了防止同时锁后,进行多个实例创建