变量的线程安全分析
1、成员变量和局部变量
- 成员变量和静态变量的线程安全性
- 如果它们没有共享,则线程安全。
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况。
- 如果只有读操作,则线程安全。
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全。
- 局部变量的线程安全性
- 局部变量是线程安全的。
- 但局部变量引用的对象则未必。
- 如果该对象没有逃离方法的作用访问,它是线程安全的。
- 如果该对象逃离方法的作用范围,需要考虑线程安全。
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
30public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
method2();
method3();
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}由于不同线程调用method2()和method3()时是对同一个成员变量list进行操作,这样就有可能存在当集合元素未添加时就先进行remove操作从而报错如下:
1
2
3
4
5
6
7Exception in thread "Thread2" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:659)
at java.util.ArrayList.remove(ArrayList.java:498)
at org.example.ThreadUnsafe.method3(TestThreadSafe.java:37)
at org.example.ThreadUnsafe.method1(TestThreadSafe.java:28)
at org.example.TestThreadSafe.lambda$main$0(TestThreadSafe.java:17)
at java.lang.Thread.run(Thread.java:748)而当把list集合调至局部变量位置时,线程就安全了,因为此时不同线程调用的是各自栈帧中的list对象,不存在共享,代码如下:
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
30public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafe test = new ThreadSafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
如果在上面的基础上,为ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即:
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
39public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafe test = new ThreadSafeSubClass();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}由于重写后的method3方法又开启了一个线程来访问局部变量list,导致局部变量list能被不同线程共享,因此也是线程不安全的。
3、常见的线程安全类
常见的线程安全类有:
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为:
1
2
3
4
5
6
7Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();它们的每个方法是原子的,但注意它们多个方法的组合不是原子的,会带来线程安全问题,如下例子:
1
2
3
4
5Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}分析下面实例的线程安全性:
例子1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//Servlet是运行在tomcat中的且只有一份实例,此实例会被多个线程共享,所以会带来线程安全问题
public class MyServlet extends HttpServlet {
//线程不安全
Map<String,Object> map = new HashMap<>();
//线程安全
String S1 = "...";
//线程安全
final String S2 = "...";
//线程不安全
Date D1 = new Date();
//线程不安全,使用final的修饰的引用数据类型只表明引用地址不可变,但是该对象的属性值可以被改变
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}例子2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class MyServlet extends HttpServlet {
// Servlet只有一份实例导致userService也只有一份实例,且会被不同线程共享,所以线程不安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数,线程不安全
private int count = 0;
public void update() {
count++;
}
}例子3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyAspect {
// Spring容器中的对象默认是单例的,意味着该成员变量会被共享,所以会出现线程安全问题,改进方法是做成环绕通知,该变量变成局部变量来使用
private long start = 0L;
public void before() {
start = System.nanoTime();
}
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}例子4
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
27public class MyServlet extends HttpServlet {
// 线程安全的,因为即使userService里面有成员变量,但由于此变量是私有的,其它地方无法对其进行修改
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 线程安全的,因为虽然userDao作为成员变量,但里面没有可以更改的成员属性
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// conn是线程安全的
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}例子5
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
26public class MyServlet extends HttpServlet {
// 线程安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 线程安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 线程不安全,因为conn只会有一份且可以同时被不同线程更改
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}例子6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class MyServlet extends HttpServlet {
// 线程安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
//线程安全,每创建一个线程调用此方法时都会创建一个不同的userDao对象
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 线程安全
private Connection = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}例子7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public abstract class Test {
public void bar() {
// 线程不安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}- 其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法。
1
2
3
4
5
6
7
8
9
10
11
12public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}例子8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23private static Integer i = 0;
public static void main(String[] args) throws InterruptedException {
List<Thread> list = new ArrayList<>();
for (int j = 0; j < 2; j++) {
Thread thread = new Thread(() -> {
for (int k = 0; k < 5000; k++) {
synchronized (i) {
i++;
}
}
}, "" + j);
list.add(thread);
}
list.stream().forEach(t -> t.start());
list.stream().forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
log.debug("{}", i);
}