Java 高并发编程 二(共享模型之管程) 线程安全问题、synchronized保证线程安全、private或final的重要性、线程八锁问题分析、变量的线程安全分析·

image.png

一. 共享带来的问题·

1、小故事·

  • 老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快
  • image.png
  • 小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用
  • 但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞)
  • image.png
  • 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
  • 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
  • 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
  • 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
  • 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了 一个笔记本(主存),把一些中间结果先记在本上 计算流程是这样的
  • image.png
  • 但是由于分时系统,有一天还是发生了事故
  • 小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
  • 老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1…] 不甘心地 到一边待着去了(上下文切换)
  • 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本
  • 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写 入了笔记本
  • image.png
  • 小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0

2、线程出现问题的根本原因分析·

  • 线程出现问题的根本原因是因为线程上下文切换,导致线程里的指令没有执行完就切换执行其它线程了,下面举一个例子

两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int count = 0;

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count++;
}
}, "t1");

Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count--;
}
}, "t2");

t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", counter);
}

15:49:59.178 [main] DEBUG create.thread - -2401

分析·

我将从字节码的层面进行分析:

  • 因为在Java中对静态变量的 自增/自减 并不是原子操作

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

1
2
3
4
getstatic 	i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

1
2
3
4
getstatic	i // 获取静态变量i的值 
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
  • 可以看到count++ 和 count-- 操作实际都是需要这个4个指令完成的,那么这里问题就来了!Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

image.png 如果代码是正常按顺序运行的,那么count的值不会计算错 image.png

  • 出现负数的情况:一个线程没有完成一次完整的自增/自减(多个指令) 的操作, 就被别的线程进行操作, 此时就会出现线程安全问题

下图解释:

首先线程2去静态变量中读取到值0, 准备常数1, 完成isub减法,变-1操作, 正常还剩下一个putstatic i写入-1的过程; 最后的指令没有执行, 就被线程1抢去了cpu的执行权; 此时线程1进行操作, 读取静态变量0, 准备常数1, iadd加法, i=1, 此时将putstatic i写入 1; 当线程2重新获取到cpu的执行权时, 它通过自身的程序计数器知道自己该执行putstatic 写入-1了; 此时它就直接将结果写为-1

image.png 出现正数的情况:同上类似; 主要就是因为线程的++/–操作不是一个原子操作, 在执行4条指令期间被其他线程抢夺cpu image.png


临界区 Critical Section·

  • 一个程序运行多线程本身是没有问题的
  • 问题出现在多个线程共享资源(临界资源)的时候
    • 多个线程同时对共享资源进行读操作本身也没有问题 - **对读操作没问题 **
    • 问题出现在对对共享资源同时进行读写操作时就有问题了 - 同时读写操作有问题
  • 先定义一个叫做临界区的概念:一段代码内如果存在对共享资源的多线程读写操作,那么称这段代码为临界区; 共享资源也成为临界资源
1
2
3
4
5
6
7
8
9
10
11
12
13
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}

static void decrement()
// 临界区
{
counter--;
}

竞态条件·

  • 多个线程在临界区执行,那么由于代码指令的执行不确定而导致的结果问题,称为竞态条件

3、 synchronized 解决方案·

为了避免临界区中的竞态条件发生,由多种手段可以达到

  • 阻塞式解决方案: synchronized , Lock (ReentrantLock)
  • 非阻塞式解决方案: 原子变量 (CAS)

现在讨论使用synchronized来进行解决,即俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程持有对象锁,其他线程如果想获取这个锁就会阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

注意: 虽然Java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区的代码
  • 同步是由于线程执行的先后,顺序不同但是需要一个线程等待其它线程运行到某个点

3.1、synchronized语法·

1
2
3
synchronized(对象) { // 线程1获得锁, 那么线程2的状态是(blocked)
临界区
}
  • 上面的实例程序使用synchronized后如下,计算出的结果是正确!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
// 对临界资源(共享资源的操作) 进行 加锁
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}

16:15:34.628 [main] DEBUG create.thread - 0

3.2、synchronized原理·

  • synchronized实际上利用对象锁保证了临界区代码的原子性,临界区内的代码在外界看来是不可分割的,不会被线程切换所打断
小故事·
  • image.png
  • synchronized(对象)中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人 进行计算,线程 t1,t2 想象成两个人
  • 当线程 t1 执行到synchronized(room)时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码
  • 这时候如果 t2 也运行到了 synchronized(room)时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了
  • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦) , 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才 能开门进入
  • 当 t1 执行完 synchronized{}块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥 匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

image.png

思考·

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断。

  • 如果把synchronized(obj)放在for循环的外面, 如何理解?
    • for循环也是一个原子操作, 表现出原子性
  • 如果t1 synchronized(obj1) 而 t2 synchronized(obj2)会怎么运行?
    • 因为t1, t2拿到不是同一把对象锁, 所以他们仍然会发现安全问题 – 必须要是同一把对象锁
  • 如果t1 synchronized(obj) 而 t2 没有加会怎么样 ?
    • 因为t2没有加锁,所以t2, 不需要获取t1的锁, 直接就可以执行下面的代码, 仍然会出现安全问题
小总结·
  • 当多个线程对临界资源进行写操作的时候, 此时会造成线程安全问题, 如果使用synchronized关键字, 对象锁一定要是多个线程共有的, 才能避免竞态条件的发生。

3.3 面向对象改进·

把需要保护的共享变量放入一个类

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
@Slf4j(topic = "create.thread")
public class CreateThread {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");

Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");

t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.getCounter());
}
}

class Room {
private int counter = 0;

public void increment() {
synchronized (this) {
counter++;
}
}

public synchronized void decrement() {
synchronized (this) {
counter--;
}
}

public synchronized int getCounter() {
synchronized (this) {
return counter;
}
}
}

16:37:56.546 [main] DEBUG create.thread - 0

3.4、synchronized 加在方法上·

  • 加在实例方法上, 锁对象就是对象实例
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo {
//在方法上加上synchronized关键字
public synchronized void test() {

}
//等价于
public void test() {
synchronized(this) {

}
}
}

  • 加在静态方法上, 锁对象就是当前类的Class实例
1
2
3
4
5
6
7
8
9
10
11
12
public class Demo {
//在静态方法上加上synchronized关键字
public synchronized static void test() {

}
//等价于
public void test() {
synchronized(Demo.class) {

}
}
}

二、线程八锁案例分析·

  • 其实就是考察synchronized 锁住的是哪个对象, 如果锁住的是同一对象, 就不会出现线程安全问题

1、锁住同一个对象都是this(e1对象),结果为:1,2或者2,1·

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j(topic = "create.thread")
public class CreateThread {
// 锁对象就是this, 也就是e1
public synchronized void a() {
log.debug("1");
}
// public void a () {
// synchronized (this) {
// log.debug("1");
// }
// }

// 锁对象也是this, e1
public synchronized void b() {
log.debug("2");
}

public static void main(String[] args) {
CreateThread e1 = new CreateThread();
new Thread(() -> e1.a()).start();
new Thread(() -> e1.b()).start();
}
}

16:50:38.294 [Thread-1] DEBUG create.thread - 1 16:50:38.295 [Thread-2] DEBUG create.thread - 2

2、锁住同一个对象都是this(e1对象),结果为:1s后1,2 || 2,1s后1·

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
@Slf4j(topic = "create.thread")
public class CreateThread {
public static void main(String[] args) {
CreateThread e1 = new CreateThread();
new Thread(() -> {
log.debug("a-begin");
e1.a();
}).start();
new Thread(() -> {
log.debug("b-begin");
e1.b();
}).start();
}
// public void a () {
// synchronized (this) {
// log.debug("1");
// }
// }

// 锁对象就是this, 也就是e1
public synchronized void a() {

try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}

// 锁对象也是this, e1
public synchronized void b() {
log.debug("2");
}
}

16:55:45.284 [Thread-1] DEBUG create.thread - a-begin 16:55:45.284 [Thread-2] DEBUG create.thread - b-begin 16:55:46.286 [Thread-1] DEBUG create.thread - 1 16:55:46.286 [Thread-2] DEBUG create.thread - 2

3、a,b锁住同一个对象都是this(e1对象),c没有上锁。结果为:3,1s后1,2 || 2,3,1s后1 || 3,2,1s后1·

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
@Slf4j(topic = "create.thread")
public class CreateThread {
public static void main(String[] args) {
CreateThread e1 = new CreateThread();
new Thread(() -> {
log.debug("a-begin");
e1.a();
}).start();
new Thread(() -> {
log.debug("b-begin");
e1.b();
}).start();
new Thread(() -> {
log.debug("c-begin");
e1.c();
}).start();
}
// public void a () {
// synchronized (this) {
// log.debug("1");
// }
// }

// 锁对象就是this, 也就是e1
public synchronized void a() {

try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}

// 锁对象也是this, e1
public synchronized void b() {
log.debug("2");
}

public void c() {
log.debug("3");
}
}

17:00:15.991 [Thread-3] DEBUG create.thread - c-begin 17:00:15.991 [Thread-2] DEBUG create.thread - b-begin 17:00:15.991 [Thread-1] DEBUG create.thread - a-begin 17:00:15.993 [Thread-3] DEBUG create.thread - 3 17:00:16.993 [Thread-1] DEBUG create.thread - 1 17:00:16.993 [Thread-2] DEBUG create.thread - 2

4、a锁住对象this(n1对象),b锁住对象this(n2对象),不互斥。结果为:2,1s后1·

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
@Slf4j(topic = "create.thread")
public class CreateThread {
public static void main(String[] args) {
CreateThread e1 = new CreateThread();
CreateThread e2 = new CreateThread();
new Thread(() -> {
log.debug("a-begin");
e1.a();
}).start();
new Thread(() -> {
log.debug("b-begin");
e2.b();
}).start();
}

// 锁对象是e1
public synchronized void a() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}

// 锁对象是e2
public synchronized void b() {
log.debug("2");
}
}

17:04:18.653 [Thread-2] DEBUG create.thread - b-begin 17:04:18.653 [Thread-1] DEBUG create.thread - a-begin 17:04:18.655 [Thread-2] DEBUG create.thread - 2 17:04:19.655 [Thread-1] DEBUG create.thread - 1

5、a锁住的是CreateThread.class对象, b锁住的是this(e1),不会互斥; 结果: 2,1s后1·

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
@Slf4j(topic = "create.thread")
public class CreateThread {
public static void main(String[] args) {
CreateThread e1 = new CreateThread();
new Thread(() -> {
log.debug("a-begin");
e1.a();
}).start();
new Thread(() -> {
log.debug("b-begin");
e1.b();
}).start();
}

// 锁对象是e1
public static synchronized void a() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}

// 锁对象是e2
public synchronized void b() {
log.debug("2");
}
}

6、a,b锁住的是CreateThread.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后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
@Slf4j(topic = "create.thread")
public class CreateThread {
public static void main(String[] args) {
CreateThread e1 = new CreateThread();
new Thread(() -> {
log.debug("a-begin");
e1.a();
}).start();
new Thread(() -> {
log.debug("b-begin");
e1.b();
}).start();
}

// 锁对象是e1
public static synchronized void a() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}

// 锁对象是e2
public static synchronized void b() {
log.debug("2");
}
}

17:09:28.243 [Thread-1] DEBUG create.thread - a-begin 17:09:28.243 [Thread-2] DEBUG create.thread - b-begin 17:09:29.245 [Thread-1] DEBUG create.thread - 1 17:09:29.245 [Thread-2] DEBUG create.thread - 2

7、a锁住的是CreateThread.class对象, b锁住的是this(e1),不会互斥; 结果: 2,1s后1·

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
@Slf4j(topic = "create.thread")
public class CreateThread {
public static void main(String[] args) {
CreateThread e1 = new CreateThread();
CreateThread e2 = new CreateThread();
new Thread(() -> {
log.debug("a-begin");
e1.a();
}).start();
new Thread(() -> {
log.debug("b-begin");
e2.b();
}).start();
}

// 锁对象是e1
public static synchronized void a() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}

// 锁对象是e2
public synchronized void b() {
log.debug("2");
}
}

17:11:20.221 [Thread-2] DEBUG create.thread - b-begin 17:11:20.221 [Thread-1] DEBUG create.thread - a-begin 17:11:20.222 [Thread-2] DEBUG create.thread - 2 17:11:21.223 [Thread-1] DEBUG create.thread - 1

8、a,b锁住的是CreateThread.class对象, 会发生互斥; 结果为:2,1s后1 || 1s后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
30
@Slf4j(topic = "create.thread")
public class CreateThread {
public static void main(String[] args) {
CreateThread e1 = new CreateThread();
CreateThread e2 = new CreateThread();
new Thread(() -> {
log.debug("a-begin");
e1.a();
}).start();
new Thread(() -> {
log.debug("b-begin");
e2.b();
}).start();
}

// 锁对象是e1
public static synchronized void a() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("1");
}

// 锁对象是e2
public static synchronized void b() {
log.debug("2");
}
}

17:12:09.671 [Thread-1] DEBUG create.thread - a-begin 17:12:09.671 [Thread-2] DEBUG create.thread - b-begin 17:12:09.672 [Thread-2] DEBUG create.thread - 2 17:12:10.672 [Thread-1] DEBUG create.thread - 1

三、 变量的线程安全分析·

1、 成员变量和静态变量的线程安全分析 (重要)·

  • 如果变量没有在线程间共享,那么变量是安全的
  • 如果变量在线程间共享
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

2、 局部变量线程安全分析 (重要)·

  • 局部变量【局部变量被初始化为基本数据类型】是安全的
  • 但局部变量引用的对象则未必 (要看该对象是否被共享且被执行了读写操作)
    • 如果该对象没有逃离方法的作用范围,它是线程安全的
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

3、线程安全的情况 (重要)·

  • 局部变量表是存在于栈帧中, 而虚拟机栈中又包括很多栈帧, 虚拟机栈是线程私有的;
  • 局部变量【局部变量被初始化为基本数据类型】是安全的,示例如下
1
2
3
4
public static void test1() {
int i = 10;
i++;
}
  • 每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10
2: istore_0
3: iinc 0, 1
6: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
3 4 0 i I

image.png

4、线程不安全的情况·

  • 如果局部变量引用的对象逃离方法的范围,那么要考虑线程安全问题的,代码示例如下

循环创建了100个线程, 在线程体里面都调用了method1方法, 在method1方法中又循环调用了100次method2,method3方法。方法2,3都使用到了成员变量arrayList, 此时的问题就是: 1个线程它会循环调用100次方法2和3, 一共有100个线程, 此时100个线程操作的共享资源就是arrayList成员变量 , 而且还进行了读写操作. 必然会造成线程不安全的问题

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
public class Test15 {
public static void main(String[] args) {
UnsafeTest unsafeTest = new UnsafeTest();
for (int i =0;i<100;i++){
new Thread(()->{
unsafeTest.method1();
},"线程"+i).start();
}
}
}
class UnsafeTest{
ArrayList<String> arrayList = new ArrayList<>();
public void method1(){
for (int i = 0; i < 100; i++) {
method2();
method3();
}
}
private void method2() {
arrayList.add("1");
}
private void method3() {
arrayList.remove(0);
}
}

Exception in thread “线程1” Exception in thread “线程2” java.lang.ArrayIndexOutOfBoundsException:-1

4.1、不安全原因分析·

  • 无论哪个线程中的 method2 和 method3 引用的都是同一个对象中的 list 成员变量
  • 一个 ArrayList ,在添加一个元素的时候,它可能会有两步来完成:
    • 第一步: 在 arrayList[size]的位置存放此元素
    • 第二步: size++
  • 在单线程运行的情况下,如果 size = 0,添加一个元素后,此元素在位置 0,而且 size=1;(没问题)
  • 在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 进行上下文切换 (线程A还没来得及size++),线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍等于0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 size 的值。
  • 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 size 却等于 2。这就是“线程不安全”了。

image.png image.png

4.2、解决方法·

  • 可以将list修改成局部变量,局部变量存放在栈帧中, 栈帧又存放在虚拟机栈中, 虚拟机栈是作为线程私有的;
  • 因为method1方法, 将arrayList传给method2,method3方法, 此时他们三个方法共享这同一个arrayList, 此时不会被其他线程访问到, 所以不会出现线程安全问题, 因为这三个方法使用的同一个线程。
  • 在外部, 创建了100个线程, 每个线程都会调用method1方法, 然后都会再从新创建一个新的arrayList对象, 这个新对象再传递给method2,method3方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class UnsafeTest {
public void method1() {
ArrayList<String> arrayList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
method2(arrayList);
method3(arrayList);
}
}

private void method2(List<String> arrayList) {
arrayList.add("1");
}

private void method3(List<String> arrayList) {
arrayList.remove(0);
}
}

image.png

4.3、思考 private 或 final的重要性 (重要)·

提高线程的安全性

  • 方法访问修饰符带来的思考: 如果把method2和method3 的方法修改为public 会不会导致线程安全问题; 分情况:
  • 只修改为public修饰,此时不会出现线程安全的问题, 即使线程2调用method2/3方法, 给2/3方法传过来的list对象也是线程2调用method1方法时,传递给method2/3的list对象, 不可能是线程1调用method1方法传的对象。 具体原因看上面: 4.2解决方法。
  • 情况2:在情况1 的基础上,为ThreadSafe 类添加子类,子类覆盖method2 或 method3方法,即如下所示: 从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】
  • 如果改为public, 此时子类可以重写父类的方法, 在子类中开线程来操作list对象, 此时就会出现线程安全问题: 子类和父类共享了list对象
  • 如果改为private, 子类就不能重写父类的私有方法, 也就不会出现线程安全问题; 所以所private修饰符是可以避免线程安全问题.
  • 所以如果不想子类, 重写父类的方法的时候, 我们可以将父类中的方法设置为private, final修饰的方法, 此时子类就无法影响父类中的方法了!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}

4.4、 常见线程安全类·

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent 包下的类 JUC

重点:

  • 这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的 , 也可以理解为 它们的每个方法是原子的
1
2
3
4
5
6
7
8
9
10
11
Hashtable table = new Hashtable();

new Thread(()->{
// put方法增加了synchronized
table.put("key", "value1");
}).start();

new Thread(()->{
table.put("key", "value2");
}).start();

  • 它们的每个方法是原子的(方法都被加上了synchronized)
  • 但注意它们多个方法的组合不是原子的,所以可能会出现线程安全问题

线程安全类方法的组合·

  • 但注意它们多个方法的组合不是原子的,见下面分析
    • 这里只能是get方法内部是线程安全的, put方法内部是线程安全的. 组合起来使用还是会受到上下文切换的影响
1
2
3
4
5
6
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}

image.png

不可变类的线程安全·

  • String和Integer类都是不可变的类,因为其类内部状态是不可改变的,因此它们的方法都是线程安全的, 都被final修饰, 不能被继承.
  • 肯定有些人他们知道String 有 replace,substring 等方法【可以】改变值啊,其实调用这些方法返回的已经是一个新创建的对象了! (在字符串常量池中当修改了String的值,它不会再原有的基础上修改, 而是会重新开辟一个空间来存储)

4.5、 示例分析-是否线程安全·

示例一·

  • Servlet运行在Tomcat环境下并只有一个实例,因此会被Tomcat的多个线程共享使用,因此存在成员变量的共享问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyServlet extends HttpServlet {
// 是否安全? 否:HashMap不是线程安全的,HashTable是
Map<String,Object> map = new HashMap<>();
// 是否安全? 是:String 为不可变类,线程安全
String S1 = "...";
// 是否安全? 是
final String S2 = "...";
// 是否安全? 否:不是常见的线程安全类
Date D1 = new Date();
// 是否安全? 否:引用值D2不可变,但是日期里面的其它属性比如年月日可变。与字符串的最大区别是Date里面的属性可变。
final Date D2 = new Date();

public void doGet(HttpServletRequest request,HttpServletResponse response) {
// 使用上述变量
}
}

实例二·

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyServlet extends HttpServlet {

// 是否安全? 不是
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++;
}
}

示例三·

  • 分析线程是否安全,先对类的成员变量,类变量,局部变量进行考虑,如果变量会在各个线程之间共享,那么就得考虑线程安全问题了,如果变量A引用的是线程安全类的实例,并且只调用该线程安全类的一个方法,那么该变量A是线程安全的的。下面对实例一进行分析:此类不是线程安全的。MyAspect切面类只有一个实例,成员变量start 会被多个线程同时进行读写操作
  • Spring中的Bean都是单例的, 除非使用@Scope修改为多例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Aspect
@Component
public class MyAspect {
// 是否安全?不安全, 因为MyAspect是单例的
private long start = 0L;

@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}

@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}

示例四·

  • 此例是典型的三层模型调用,MyServlet UserServiceImpl UserDaoImpl类都只有一个实例,UserDaoImpl类中没有成员变量,update方法里的变量引用的对象不是线程共享的,所以是线程安全的;UserServiceImpl类中只有一个线程安全的UserDaoImpl类的实例,那么UserServiceImpl类也是线程安全的,同理 MyServlet也是线程安全的
  • Servlet调用Service, Service调用Dao这三个方法使用的是同一个线程。
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
public class MyServlet extends HttpServlet {
// 是否安全 是:UserService不可变,虽然有一个成员变量,
// 但是是私有的, 没有地方修改它
private UserService userService = new UserServiceImpl();

public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}

public class UserServiceImpl implements UserService {
// 是否安全 是:Dao不可变, 其没有成员变量
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) {
// ...
}
}
}

示例五·

  • 跟示例二大体相似,UserDaoImpl类中有成员变量,那么多个线程可以对成员变量conn 同时进行操作,故是不安全的
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
public 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, 一个线程拿到conn,刚创建一个连接赋值给conn, 此时另一个线程进来了, 直接将conn.close
//另一个线程恢复了, 拿到conn干事情, 此时conn都被关闭了, 出现了问题
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}

示例六·

  • 跟示例三大体相似,UserServiceImpl类的update方法中UserDao是作为局部变量存在的,所以每个线程访问的时候都会新建有一个UserDao对象,新建的对象是线程独有的,所以是线程安全的
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 MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
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();
}
}

示例七·

  • 私有变量sdf被暴露出去了, 发生了逃逸
1
2
3
4
5
6
7
8
9
10
11
12
public 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 的行为是不确定的,可能导致不安全的发生,被称之为外星方法因为foo方法可以被重写,导致线程不安全。 在String类中就考虑到了这一点,String类是final的,子类不能重写它的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
public 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();
}
}

4.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
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
package cn.itcast.n4.exercise;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
public static void main(String[] args) throws InterruptedException {
// 模拟多人买票
TicketWindow window = new TicketWindow(1000);

// 所有线程的集合(由于threadList在主线程中,不被共享,因此使用ArrayList不会出现线程安全问题)
List<Thread> threadList = new ArrayList<>();
// 卖出的票数统计(Vector为线程安全类)
List<Integer> amountList = new Vector<>();
for (int i = 0; i < 2000; i++) {
Thread thread = new Thread(() -> {
// 买票
int amount = window.sell(random(5));
// 统计买票数
amountList.add(amount);
});
threadList.add(thread);
thread.start();
}

for (Thread thread : threadList) {
thread.join();
}

// 统计卖出的票数和剩余票数
log.debug("余票:{}",window.getCount());
log.debug("卖出的票数:{}", amountList.stream().mapToInt(i -> i).sum());
}

// Random 为线程安全
static Random random = new Random();

// 随机 1~5
public static int random(int amount) {
return random.nextInt(amount) + 1;
}
}

// 售票窗口
class TicketWindow {
// 票总数
private int count;

public TicketWindow(int count) {
this.count = count;
}

// 获取余票数量
public int getCount() {
return count;
}

// 售票
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}

把sell加个synchronized

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
package cn.itcast.n4.exercise;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
public static void main(String[] args) throws InterruptedException {
// 模拟多人买票
TicketWindow window = new TicketWindow(1000);

// 所有线程的集合(由于threadList在主线程中,不被共享,因此使用ArrayList不会出现线程安全问题)
List<Thread> threadList = new ArrayList<>();
// 卖出的票数统计(Vector为线程安全类)
List<Integer> amountList = new Vector<>();
for (int i = 0; i < 2000; i++) {
Thread thread = new Thread(() -> {
// 买票
int amount = window.sell(random(5));
// 统计买票数
amountList.add(amount);
});
threadList.add(thread);
thread.start();
}

for (Thread thread : threadList) {
thread.join();
}

// 统计卖出的票数和剩余票数
log.debug("余票:{}",window.getCount());
log.debug("卖出的票数:{}", amountList.stream().mapToInt(i -> i).sum());
}

// Random 为线程安全
static Random random = new Random();

// 随机 1~5
public static int random(int amount) {
return random.nextInt(amount) + 1;
}
}

// 售票窗口
class TicketWindow {
// 票总数
private int count;

public TicketWindow(int count) {
this.count = count;
}

// 获取余票数量
public int getCount() {
return count;
}

// 售票
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}

  • 转账练习 测试下面代码是否存在线程安全问题,并尝试改正
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
package cn.itcast.n4.exercise;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;

@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看转账2000次后的总金额
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}

// Random 为线程安全
static Random random = new Random();

// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}

// 账户
class Account {
private int money;

public Account(int money) {
this.money = money;
}

public int getMoney() {
return money;
}

public void setMoney(int money) {
this.money = money;
}

// 转账
public void transfer(Account target, int amount) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}

// 没问题, 最终的结果仍然是 2000元
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
package cn.itcast.n4.exercise;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;

@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看转账2000次后的总金额
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}

// Random 为线程安全
static Random random = new Random();

// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}

// 账户
class Account {
private int money;

public Account(int money) {
this.money = money;
}

public int getMoney() {
return money;
}

public void setMoney(int money) {
this.money = money;
}

// 转账
public void transfer(Account target, int amount) {
synchronized(Account.class) { //锁住Account类,因为涉及到A.money和B.money。
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
}

// 没问题, 最终的结果仍然是 2000元