线程安全的定义与理解

什么是线程安全?什么是非线程安全,该如何定义和理解?什么场景会出现非线程安全?这几个问题在脑子里有个概念和轮廓,但要描述清楚始终不得,不全,非一两个例子代码。

前提

描述线程安全或非线程安全的前提条件必须放在 多线程并发 的场景下,变量是否被多个线程访问。在单线程或多线程非并发而是轮替执行的场景下,线程安全问题几乎是不存在的。

要理解线程安全问题还需要先了解 Java内存模型 和线程的工作原理。Java内存模型中规定了所有变量(非局部变量)都存储在 主内存 中,每条线程会都会有自己的 工作内存,线程开始工作时首先将变量从 主内存 中拷贝一个副本到 工作内存,在 工作内存 对这个副本进行操作(读取,赋值等),操作完后在适当时间再将变量写回到 主内存。不同线程之间无法直接访问对方 工作内存 中的变量,线程间的变量值的传递需要通过 主内存 来实现。

后续将另外写两篇对Java内存模型JVM主内存与工作内存理解描述。

定义

基于以上前提,当多个线程同时 读写 某个内存(共享)数据时,若没做线程同步处理,就会因多线程并发引发的安全问题。

共享数据指的是 实例变量静态变量,若每条线程对 实例变量静态变量 只有只读操作,没有写操作,可以认为该变量是线程安全的。

针对并发编程下的安全问题定义了三大特性:原子性、有序性、可见性;Java 为实现这三个特性提供了一系列的方法和工具,如:synchronized、ThreadLoacl、volatile、lock、并发工具包 等。

当并发情况下满足这三个特性可以确定线程是安全的,当这三个特性中任意一个不满足时就会存在线程并发引起的安全问题。后续单独写篇并发编程三大特性的描述。

采用多线程的目的是为了充分利用(压榨)CPU多核多线程的性能,将一个功能拆分成多个子任务,创建多线程同时执行这些子任务,提高CPU的使用率,提高程序执行效率(如:多线程分段下载)。

一个任务拆分成多个子任务通过多线程执行得到的结果,与该任务在单线程执行的结果必须是一致的,结果是确定的。使用多线程是在此确定的结果基础上来提高程序的运行效率,这才是我们真正想要的结果。  

线程安全

线程安全:多线程并发情况下的结果与单线程执行的结果一致,结果是确定的,和预期也是一样的,不会产生不确定的结果(没有二义性)。

线程安全 的前提是非线程安全的存在,Java为解决非线程安全问题而提供的一系列方法和工具,。

非线程安全

非线程安全 是多线程并发编程下自然存在的一个特性(问题),只是这个特性并不是我们想要的。
非线程安全:多线程并发情况下得到的结果不确定,与单线程执行的结果存在不一致的情况,存在二义性认为是非线程安全的。。

多线程运行同一段代码情况下,CPU会来回切换不同的线程执行,线程存在被中断、睡眠、等待等操作,CPU对线程的切换也不是人为可控的,这样就无法维护 原子性有序性,若线程运行结果不作处理让一组线程里的其它线程可见,就无法维护 可见性,这些就会造成线程不安全。

Java 线程安全类

以 Java 常用的类来举例线程安全和非线程安全。

  1. ArrayList 是非线程安全的,Vector 是线程安全的。
  2. HashMap 是非线程安全的,HashTable 是线程程安全的。
  3. StringBuilder 是非线程安全的,StringBuffer 是非线程安全的。
  4. SimpleDateFormat 是非线程安全的。

线程安全示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ThreadSafe {

public static void main(String[] args) throws InterruptedException {
final CountTest ct = new CountTest();
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
// ct.generalNumPlus();
// ct.volatileNumPlus();
// ct.syncMethodNumPlus();
// ct.syncForNumPlus();
ct.syncBlockNumPlus();
}
}).start();
}
// 存在main线程已执行完,创建的线程还没执行完的情况
Thread.sleep(100);
System.out.println(ct.getNum());
}

}
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.java.jdk.thread;

public class CountTest {
private int num = 0;
private static volatile int count = 0;

public int getNum() {
return num;
}

public void setNum(int num) {
this.num = num;
}

public static int getCount() {
return count;
}

public static void setCount(int count) {
CountTest.count = count;
}

/**
* 多线程操作成员变量
* 100个线程执行加1操作,加到10
* 会存在线程安全问题,得到结果可能不是 1000
* Thread-86 : 940
* Thread-80 : 992
* 992
*/
public void generalNumPlus() {
for (int i = 0; i < 10; i++) {
num++;
}
System.out.println(Thread.currentThread().getName() + " : " + num);
}

/**
* 成员变量添加 volatile,使变量修改线程可见
* 仍会存在线程安全问题,得到结果可能不是 1000
* Thread-96 : 982
* Thread-97 : 992
* 992
* volatile 无法解决非原子操作的线程同步问题
*/
public void volatileNumPlus() {
for (int i = 0; i < 10; i++) {
num++;
}
System.out.println(Thread.currentThread().getName() + " : " + num);
}

/**
* 同步方法是线程安全的
* 同一时刻只有一个线程执行,其它线程等待
*/
public synchronized void syncMethodNumPlus() {
for (int i = 0; i < 10; i++) {
num++;
}
System.out.println(Thread.currentThread().getName() + " : " + num);
}

/**
* 同步循环是线程安全的
*/
public void syncForNumPlus() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
num = num + 1;
}
System.out.println(Thread.currentThread().getName() + " : " + num);
}
}

/**
* 同步代码块,线程是安全的
* 但线程执行顺序是随机的
*/
public void syncBlockNumPlus() {
for (int i = 0; i < 10; i++) {
synchronized (this) {
num = num + 1;
}
}
System.out.println(Thread.currentThread().getName() + " : " + num);
}
}
作者

光星

发布于

2018-01-07

更新于

2022-06-17

许可协议

评论