Java 高并发编程 二(共享模型之管程) 线程安全问题、synchronized保证线程安全、private或final的重要性、线程八锁问题分析、变量的线程安全分析·
一. 共享带来的问题·
1、小故事·
- 老王(操作系统)有一个功能强大的算盘(CPU),现在想把它租出去,赚一点外快
- 小南、小女(线程)来使用这个算盘来进行一些计算,并按照时间给老王支付费用
- 但小南不能一天24小时使用算盘,他经常要小憩一会(sleep),又或是去吃饭上厕所(阻塞 io 操作),有时还需要一根烟,没烟时思路全无(wait)这些情况统称为(阻塞)
- 在这些时候,算盘没利用起来(不能收钱了),老王觉得有点不划算
- 另外,小女也想用用算盘,如果总是小南占着算盘,让小女觉得不公平
- 于是,老王灵机一动,想了个办法 [ 让他们每人用一会,轮流使用算盘 ]
- 这样,当小南阻塞的时候,算盘可以分给小女使用,不会浪费,反之亦然
- 最近执行的计算比较复杂,需要存储一些中间结果,而学生们的脑容量(工作内存)不够,所以老王申请了 一个笔记本(主存),把一些中间结果先记在本上 计算流程是这样的
- 但是由于分时系统,有一天还是发生了事故
- 小南刚读取了初始值 0 做了个 +1 运算,还没来得及写回结果
- 老王说 [ 小南,你的时间到了,该别人了,记住结果走吧 ],于是小南念叨着 [ 结果是1,结果是1…] 不甘心地 到一边待着去了(上下文切换)
- 老王说 [ 小女,该你了 ],小女看到了笔记本上还写着 0 做了一个 -1 运算,将结果 -1 写入笔记本
- 这时小女的时间也用完了,老王又叫醒了小南:[小南,把你上次的题目算完吧],小南将他脑海中的结果 1 写 入了笔记本
- 小南和小女都觉得自己没做错,但笔记本里的结果是 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 iconst_1 iadd putstatic i
|
而对应 i-- 也是类似:
1 2 3 4
| getstatic i iconst_1 isub putstatic i
|
- 可以看到count++ 和 count-- 操作实际都是需要这个4个指令完成的,那么这里问题就来了!Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
如果代码是正常按顺序运行的,那么count的值不会计算错
- 出现负数的情况:一个线程没有完成一次完整的自增/自减(多个指令) 的操作, 就被别的线程进行操作, 此时就会出现线程安全问题
下图解释:
首先线程2去静态变量中读取到值0, 准备常数1, 完成isub减法,变-1操作, 正常还剩下一个putstatic i写入-1的过程; 最后的指令没有执行, 就被线程1抢去了cpu的执行权; 此时线程1进行操作, 读取静态变量0, 准备常数1, iadd加法, i=1, 此时将putstatic i写入 1; 当线程2重新获取到cpu的执行权时, 它通过自身的程序计数器知道自己该执行putstatic 写入-1了; 此时它就直接将结果写为-1
出现正数的情况:同上类似; 主要就是因为线程的++/–操作不是一个原子操作, 在执行4条指令期间被其他线程抢夺cpu
临界区 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(对象) { 临界区 }
|
- 上面的实例程序使用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实际上利用对象锁保证了临界区代码的原子性,临界区内的代码在外界看来是不可分割的,不会被线程切换所打断
小故事·
synchronized(对象)
中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人 进行计算,线程 t1,t2 想象成两个人- 当线程 t1 执行到
synchronized(room)
时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行 count++ 代码 - 这时候如果 t2 也运行到了
synchronized(room)
时,它发现门被锁住了,只能在门外等待,发生了上下文切 换,阻塞住了 - 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦) , 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才 能开门进入
- 当 t1 执行完
synchronized{}
块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥 匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断。
- 如果把synchronized(obj)放在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 { 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 { 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 { public synchronized void a() { log.debug("1"); }
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 synchronized void a() {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); }
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 synchronized void a() {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); }
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(); }
public synchronized void a() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); }
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(); }
public static synchronized void a() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); }
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(); }
public static synchronized void a() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); }
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(); }
public static synchronized void a() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); }
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(); }
public static synchronized void a() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("1"); }
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
|
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。这就是“线程不安全”了。
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); } }
|
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(()->{ 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();
if( table.get("key") == null) { table.put("key", value); }
|
不可变类的线程安全·
- 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 { Map<String,Object> map = new HashMap<>(); String S1 = "..."; final String S2 = "..."; Date D1 = new 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 { 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 { 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) { } } }
|
示例五·
- 跟示例二大体相似,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 { 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(); } }
|
示例七·
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);
List<Thread> threadList = new ArrayList<>(); 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()); }
static Random random = new Random();
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);
List<Thread> threadList = new ArrayList<>(); 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()); }
static Random random = new Random();
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(); log.debug("total:{}", (a.getMoney() + b.getMoney())); }
static Random random = new Random();
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); } } }
|
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(); log.debug("total:{}", (a.getMoney() + b.getMoney())); }
static Random random = new Random();
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) { if (this.money >= amount) { this.setMoney(this.getMoney() - amount); target.setMoney(target.getMoney() + amount); } } } }
|