JUC - 共享模型之管程安全
# 共享带来的问题
# 小故事
- 老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快
- 小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用
- 但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞)
- 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
- 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
- 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
- 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
- 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了 一个笔记本(主存),把一些中间结果先记在本上
- 计算流程是这样的
但是由于分时系统,有一天还是发生了事故
小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1...],不甘心地到一边待着去了(上下文切换)
老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本
这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写 入了笔记本
小南和小女都觉得自己没做错,但笔记本里的结果是 1 而不是 0
故事主要说明:A 获取初始值 0,修改为 1,但是没有及时放回去,B 就获取了初始值 0,修改为 -1,然后放回去,结果为 -1。最后 A 再将 1 放回去,覆盖了 -1。导致结果为 1 而不是 0。
# Java 的体现
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
public class Test {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 问题分析
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析。
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量 i 的值
iconst_1 // 准备常量 1
iadd // 自增
putstatic i // 将修改后的值存入静态变量 i
2
3
4
而对应 i-- 也是类似:
getstatic i // 获取静态变量 i 的值
iconst_1 // 准备常量 1
isub // 自减
putstatic i // 将修改后的值存入静态变量 i
2
3
4
所以极有可能 i 自增后,没有将修改后的值存入 i,就被 i 自减后存入了,导致最后自增的值覆盖掉自减的值。
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换(双向箭头):
如果是 单线程 以上 8 行代码是顺序执行(不会交错)没有问题:
但 多线程 下这 8 行代码可能交错运行:
i = 0,先 i++ 然后 i--,结果应该为 0,但是多线程有如下结果:
出现负数的情况:
- 线程 2 先获取 i = 0,然后进行 i - 1 = -1,来不及将 -1 放回去(覆盖 0),就被线程 1 获取 i = 0,然后进行 i + 1 = 1,将 1 放回去,最后线程 2 才将 -1 返回去,导致 1 被 -1 覆盖,结果为 -1
出现正数的情况:
- 线程 1 先获取 i = 0,然后进行 i + 1 = 1,来不及将 1 放回去(覆盖 0),就被线程 2 获取 i = 0,然后进行 i - 1 = -1,将 -1 放回去,最后线程 1 才将 1 返回去,导致 -1 被 1 覆盖,结果为 1
# 临界区 Critical Section
- 一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问 共享资源
- 多个线程读 共享资源 其实也没有问题
- 在多个线程对 共享资源 读写操作时发生指令交错,就会出现问题
- 一段代码块内如果存在对 共享资源 的多线程读写操作,称这段代码块为 临界区
例如,下面代码中的临界区:
public class Test {
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 竞态条件 Race Condition
多个线程在临界区内执行,由于代码的 执行序列不同 而导致结果无法预测,称之为发生了 竞态条件。
上面代码的 counter++
和 counter--
就是两个线程对 counter 进行竞争获取。
# Synchronized 解决方案
# 应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:Synchronized,Lock
- 非阻塞式的解决方案:原子变量
本内容使用阻塞式的解决方案:Synchronized,来解决上述问题,即俗称的「对象锁」,它采用互斥的方式让同一时刻至多只有一个线程能持有「对象锁」,其它线程再想获取这个「对象锁」时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
虽然 Java 中互斥和同步都可以采用 Synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
# 解决
语法:
synchronized(对象) // 线程 1 获取对象后, 线程 2来到这里就 blocked 阻塞
{
临界区
}
2
3
4
代码:
public class Test {
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);
}
}
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
如下图,当第一个人抢到钥匙进入 Owner 房子后,就锁住门,导致其他人无法进入,当第一个人执行完临界区(房子)代码,就开门唤醒其他两个人,并把钥匙放到一个地方,让两个人争抢。
你可以做这样的类比:
- Synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
- Synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
- 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
- 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
- 当 t1 执行完
synchronized{}
块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的count--
代码
用图来表示:
Synchronized 实际是用 对象锁 保证了临界区内代码的 原子性,原子性:临界区内的代码对外是不可分割的,不会被线程切换所打断。
如果把
synchronized(obj)
放在 for 循环的外面,如何理解?
原子性问题,这样锁住的临界区就包含在 for 循环,如果以 i++ 的四个字节码来说,那就是锁住了 2w 个字节码,结果是对的,性能差些。
如果 t1 执行
synchronized(obj1)
而 t2 执行synchronized(obj2)
会怎样运作?
锁对象问题,锁对象不一样,t2 进入 t1 不会被阻塞,同理 t1 进入 t2 不会被阻塞。
形象说就是每个锁都是一个钥匙,钥匙不一样,那么 t1 和 t2 就可以进入不同的房间,都可以对同一个 count 进行操作,导致结果不对。
如果 t1 执行
synchronized(obj)
而 t2 没有加会怎么样?如何理解?
锁对象问题,t2 进入临界区时,没有被 synchronized 锁限制,所以直接进去操作 count,导致结果不对。
# 面向对象改进
把需要保护的共享变量放入一个类
// 锁类
class Room {
int value = 0;
public void increment() {
synchronized (this) {
value++;
}
}
public void decrement() {
synchronized (this) {
value--;
}
}
// 获取时,也要锁住,防止正在 + 或者 - 时,返回值
public int get() {
synchronized (this) {
return value;
}
}
}
@Slf4j
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count: {}" , room.get());
}
}
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
# 方法上的 Synchronized
加在实例方法上的 Synchronized 等价于在方法里加 synchronized(this)
。
class Test{
public synchronized void test() {
}
}
// 等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
如上面的锁类 Room 可以写成:
// 锁类
class Room {
int value = 0;
public synchronized void increment() {
value++;
}
public synchronized void decrement() {
value--;
}
// 获取时,也要锁住,防止正在 + 或者 - 时,返回值
public synchronized int get() {
return value;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
加在静态方法上的 Synchronized 等价于在方法里加 synchronized(类.class)
。
class Test{
public synchronized static void test() {
}
}
// 等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
2
3
4
5
6
7
8
9
10
11
12
# 不加 synchronized 的方法
不加 Synchronzied 的方法就好比不遵守规则的人,不去老实排队,无法保证原子性,即无法保证线程安全。
# 线程八锁
其实就是考察 Synchronized 锁住的是哪个对象,下面给出八个例子:
例子 1
代码:
public class Test {
class Number{
public synchronized void a() {
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
等价于 synchronized(this)
,所以输出:
1 2
或 2 1
2
例子 2
代码:
public class Test {
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
等价于 synchronized(this)
,所以输出:
1s 后 12
或 2 1s 后 1
2
例子 3
代码:
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3");
}
}
public class Test {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
new Thread(()->{ n1.c(); }).start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c()
方法没有锁,因为 a()
睡眠 1s,所以 c()
必然先输出,所以输出:
3 1s 后 12
或 23 1s 后 1
或 32 1s 后 1
2
3
例子 4
代码:
class Number{
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public class Test {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
等价于 synchronized(this)
,this 是对象,而 n1 和 n2 是两个对象,所以锁不影响,输出:
2 1s 后 1
例子 5
代码:
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public class Test {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
输出:
2 1s 后 1
例子 6
代码:
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public class Test {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
等价于 synchronized(Number.class)
,所以输出:
1s 后 12
或 2 1s 后 1
2
例子 7
代码:
class Number{
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
}
public class Test {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
输出:
2 1s 后 1
例子 8
代码:
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public class Test {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
等价于 synchronized(Number.class)
,而 n1 和 n2 都属于 Number 类,所以谁拿到锁,就先输出谁,所以输出:
1s 后 12
或 2 1s 后 1
2
# 变量的线程安全分析
# 成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
# 局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必安全
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
# 局部变量线程安全分析
public static void test1() {
int i = 10;
i++;
}
2
3
4
每个线程调用 test1()
方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享。
如图:
局部变量的引用稍有不同。
先看一个成员变量的例子:
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);
}
}
public class Test {
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).start();
}
}
}
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
其中一种情况是,如果线程 2 还未 add,线程 1 remove 就会报错:
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:657)
at java.util.ArrayList.remove(ArrayList.java:496)
at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35)
at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26)
at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14)
at java.lang.Thread.run(Thread.java:748)
2
3
4
5
6
7
分析:
- 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
- method3 与 method2 分析相同
将 list 修改为局部变量,那么就不会有上述问题了
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");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
public class Test {
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).start();
}
}
}
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
分析:
- list 是局部变量,每个线程调用时会创建其不同实例,没有共享
- 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
- method3 的参数分析与 method2 相同
方法访问修饰符带来的思考,如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
情况 1:有其它线程调用 method2 和 method3。
不会引起线程安全问题,因为 参数 list 没有对外暴露,是局部的。
情况 2:在情况 1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法。
如下代码:
class ThreadSafe {
public 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{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
public class Test {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafeSubClass test = new ThreadSafeSubClass();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
}
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
test
是子类的对象,这样子类暴露出了 list,可能导致 list 为空时调用 list.remove(0)
报错。
从这个例子可以看出 private 或 final 提供「安全」的意义所在,请体会开闭原则中的「闭」,如下:
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");
}
private 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();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 常见线程安全类
JDK 提供的大部分线程安全类:
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
public void test() {
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
}
2
3
4
5
6
7
8
9
10
11
- 它们的每个方法是原子的
- 但注意它们多个方法的组合不是原子的,如下
# 线程安全类方法的组合
分析下面代码是否线程安全?
public void test() {
Hashtable table = new Hashtable();
// 线程 1,线程 2
if( table.get("key") == null) {
table.put("key", value);
}
}
2
3
4
5
6
7
上面代码是不安全的,可能线程 1 和 线程 2 都先进入 if 里,然后线程 2 先 put 值,最后线程 1 再 put 值,分析如下图:
# 不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的。
疑问:String 有 replace
,substring
等方法 可以 改变值,那么这些方法又是如何保证线程安全的呢?
看这些方法的源码就发现,这些方法的返回值都是 new 一个新的 String 对象,也就是返回新的改变值的 String 对象。
如下自定义不可变类:
public class Immutable{
private int value = 0;
public Immutable(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
// 增加一个修改值的方法,返回一个新的 Immutable 类
public Immutable setValue(int value){
return new Immutable(this.value);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 实例分析
例 1
引用对象不管是不是 final 修饰,只要对象的很多方法不是线程安全的(不加锁),那就是不安全的。
public class MyServlet extends HttpServlet {
// 是否安全?不安全
Map<String,Object> map = new HashMap<>();
// 是否安全?安全,因为 String 是不可变类
String S1 = "...";
// 是否安全?安全,因为 String 是不可变类
final String S2 = "...";
// 是否安全?不安全
Date D1 = new Date();
// 是否安全?不安全,Date 是引用对象,里面有很多方法不是线程安全的
final Date D2 = new Date();
}
2
3
4
5
6
7
8
9
10
11
12
13
例 2
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++;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
userService 是线程不安全的,因为可能有多个线程同时调用一个 MyServlet 对象,导致 count++ 临界区发送改变。
例 3
@Aspect
@Component
public class 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));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Spring 默认都是单例对象,导致 start 会被多个线程 同时 使用,所以不安全。
解决:使用环绕通知,将 start 改为 局部变量。
例 4
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 {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全?安全
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}
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
MyServlet、UserServiceImpl、UserDaoImpl 没有 公有 成员变量,所以无法被多线程进行共享。
例 5
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 {
// 是否安全?不安全
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
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
19 行 Connection 为成员变量,导致被多个线程共享,可能线程 1 执行到 22 行,线程 2 已经执行到 24 行,把 conn 关掉,导致线程 1 的 22 行报错。
例 6
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 conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
虽然 17 行 conn 依然是成员变量,但是 11 行的 userDao 是局部变量,所以每个线程都有一个 userDao 局部变量,导致都有一个 conn 互不影响。
例 7
public abstract class Test {
public void bar() {
// 是否安全?
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public void foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为 外星方法。
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();
}
}
2
3
4
5
6
7
8
9
10
11
12
线程不安全原因:sdf 对象在 main 线程创建,却在子类的另一个线程执行 parse 方法,所以线程不安全。
String 类为什么有 final 修饰,就是为了防止我们写子类继承 String,然后重写 String 的方法,导致线程不安全。
例 8
public class Test {
private 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);
}
}
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 ExerciseSell {
public static void main(String[] args) {
TicketWindow ticketWindow = new TicketWindow(2000);
List<Thread> list = new ArrayList<>();
// 用来存储买出去多少张票,Vector 是线程安全类
List<Integer> sellCount = new Vector<>();
for (int i = 0; i < 2000; i++) {
Thread t = new Thread(() -> {
// 分析这里的竞态条件
int count = ticketWindow.sell(randomAmount());
// 虽然多个线程都操作 sellCount,但 sellCount 是 Vector 线程安全类
sellCount.add(count);
});
list.add(t);
t.start();
}
list.forEach((t) -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 买出去的票求和
log.debug("selled count:{}",sellCount.stream().mapToInt(c -> c).sum());
// 剩余票数
log.debug("remainder count:{}", ticketWindow.getCount());
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~5
public static int randomAmount() {
return random.nextInt(5) + 1;
}
}
class TicketWindow {
private int count;
public TicketWindow(int count) {
this.count = count;
}
public int getCount() {
return count;
}
public int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
}
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
sell 方法是临界区,因为多个线程都能同时进入 sell 方法操作 count,所以锁住该方法即可。
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
} else {
return 0;
}
}
2
3
4
5
6
7
8
# 转账练习
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);
}
}
}
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
transfer 方法是临界区,所以我们需要针对 transfer 方法内部进行加锁,那么这样可以吗?为什么?
public synchronized void transfer(Account target, int amount) {
if (this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
2
3
4
5
6
直接在方法上加 synchronized,是不行的,因为有两个 Account 类,在方法上加 synchronized 等价于 synchronized(this)
,仅仅锁住自己的对象,也就是 a 锁住自己,b 锁住自己,互不影响。如何解决呢?
因为 a 和 b 都是 Account 类,所以锁住 Account 类即可,如下:
public void transfer(Account target, int amount) {
synchronized(Account.class) {
if (this.money > amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
2
3
4
5
6
7
8