线程安全问题

Posted Devil丶俊锅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程安全问题相关的知识,希望对你有一定的参考价值。

本文楼主主要以用户在售票厅购买车票为背景进行多线程的实现。假设A市到B市的车票共50张,共有3个售票窗口在进行售票,使用多线程来模拟理想情况下的用户购票:

实现Runnable的Ticket类:

 1 package com.jon.thread;
 2 
 3 public class TicketSell implements Runnable {
 4     private int tickets = 50;//设置车票数量
 5     @Override
 6     public void run() {
 7         while(true){
 8             if(tickets>0){        
 9                 //输出当前是哪个线程在出售第几张车票
10                 System.out.println(Thread.currentThread().getName() + "正在售第" + (tickets--) + "张车票");
11             }
12         }
13     }
14 
15 }

简单的售票业务构建好后,我们用三个线程模拟售票窗口来进行测试:

 1 package com.jon.thread;
 2 
 3 public class TicketSellTest {
 4     public static void main(String[] args) {
 5         TicketSell ts = new TicketSell();
 6         Thread td1 = new Thread(ts, "售票窗口1");//设置线程名称以区分哪个售票窗口
 7         Thread td2 = new Thread(ts, "售票窗口2");
 8         Thread td3 = new Thread(ts, "售票窗口3");
 9         td1.start();
10         td2.start();
11         td3.start();
12     }
13 }

输出结果可以看到,三个线程抢占式地将50张车票完全售出:

 1 售票窗口2正在售第50张车票
 2 售票窗口3正在售第49张车票
 3 售票窗口1正在售第48张车票
 4 售票窗口3正在售第46张车票
 5 售票窗口2正在售第47张车票
 6 售票窗口3正在售第44张车票
 7 售票窗口1正在售第45张车票
 8 售票窗口3正在售第42张车票
 9 售票窗口2正在售第43张车票
10 售票窗口3正在售第40张车票
11 售票窗口1正在售第41张车票
12 售票窗口3正在售第38张车票
13 售票窗口2正在售第39张车票
14 售票窗口2正在售第35张车票
15 售票窗口3正在售第36张车票
16 售票窗口3正在售第33张车票
17 售票窗口3正在售第32张车票
18 售票窗口3正在售第31张车票
19 售票窗口3正在售第30张车票
20 售票窗口3正在售第29张车票
21 售票窗口3正在售第28张车票
22 售票窗口1正在售第37张车票
23 售票窗口3正在售第27张车票
24 售票窗口2正在售第34张车票
25 售票窗口3正在售第25张车票
26 售票窗口1正在售第26张车票
27 售票窗口1正在售第22张车票
28 售票窗口1正在售第21张车票
29 售票窗口1正在售第20张车票
30 售票窗口1正在售第19张车票
31 售票窗口1正在售第18张车票
32 售票窗口1正在售第17张车票
33 售票窗口1正在售第16张车票
34 售票窗口1正在售第15张车票
35 售票窗口1正在售第14张车票
36 售票窗口1正在售第13张车票
37 售票窗口1正在售第12张车票
38 售票窗口1正在售第11张车票
39 售票窗口1正在售第10张车票
40 售票窗口1正在售第9张车票
41 售票窗口1正在售第8张车票
42 售票窗口1正在售第7张车票
43 售票窗口1正在售第6张车票
44 售票窗口1正在售第5张车票
45 售票窗口1正在售第4张车票
46 售票窗口1正在售第3张车票
47 售票窗口1正在售第2张车票
48 售票窗口1正在售第1张车票
49 售票窗口3正在售第23张车票
50 售票窗口2正在售第24张车票
View Code

但是在实际应用场景中,我们通常要考虑到因为网络延迟等其他因素造成的购票延迟,这里我们将Ticket稍微进行了改造:

 1 package com.jon.thread;
 2 
 3 public class TicketSell implements Runnable {
 4     private int tickets = 50;//设置车票数量
 5     @Override
 6     public void run() {
 7         while(true){
 8             try {
 9                 Thread.sleep(100);//将线程睡眠100毫秒用来模拟延迟
10             } catch (InterruptedException e) {                
11                 e.printStackTrace();
12             }
13             if(tickets>0){        
14                 //输出当前是哪个线程在出售第几张车票
15                 System.out.println(Thread.currentThread().getName() + "正在售第" + (tickets--) + "张车票");
16             }
17         }
18     }
19 
20 }

再次运行,可以看到有些售票窗口售出了相同的票,甚至还出现了-1、0 ,很明显出现了线程安全问题:

 1 售票窗口1正在售第49张车票
 2 售票窗口2正在售第49张车票
 3 售票窗口3正在售第50张车票
 4 售票窗口2正在售第48张车票
 5 售票窗口1正在售第46张车票
 6 售票窗口3正在售第47张车票
 7 售票窗口2正在售第45张车票
 8 售票窗口1正在售第44张车票//窗口1,3出售了相同的44号车票
 9 售票窗口3正在售第44张车票
10 售票窗口2正在售第43张车票
11 售票窗口1正在售第41张车票
12 售票窗口3正在售第42张车票
13 售票窗口2正在售第40张车票
14 售票窗口3正在售第39张车票
15 售票窗口1正在售第39张车票
16 售票窗口1正在售第38张车票
17 售票窗口2正在售第37张车票
18 售票窗口3正在售第36张车票
19 售票窗口1正在售第35张车票
20 售票窗口3正在售第33张车票
21 售票窗口2正在售第34张车票
22 售票窗口1正在售第32张车票
23 售票窗口3正在售第31张车票
24 售票窗口2正在售第30张车票
25 售票窗口3正在售第29张车票
26 售票窗口1正在售第29张车票
27 售票窗口2正在售第28张车票
28 售票窗口3正在售第27张车票
29 售票窗口1正在售第27张车票
30 售票窗口2正在售第26张车票
31 售票窗口1正在售第25张车票
32 售票窗口3正在售第24张车票
33 售票窗口2正在售第23张车票
34 售票窗口1正在售第22张车票
35 售票窗口3正在售第21张车票
36 售票窗口2正在售第20张车票
37 售票窗口1正在售第19张车票
38 售票窗口3正在售第18张车票
39 售票窗口2正在售第17张车票
40 售票窗口3正在售第16张车票
41 售票窗口1正在售第15张车票
42 售票窗口2正在售第14张车票
43 售票窗口3正在售第13张车票
44 售票窗口1正在售第12张车票
45 售票窗口2正在售第11张车票
46 售票窗口1正在售第10张车票
47 售票窗口3正在售第9张车票
48 售票窗口2正在售第8张车票
49 售票窗口1正在售第7张车票
50 售票窗口3正在售第6张车票
51 售票窗口2正在售第5张车票
52 售票窗口1正在售第4张车票
53 售票窗口2正在售第2张车票
54 售票窗口3正在售第3张车票
55 售票窗口1正在售第0张车票
56 售票窗口3正在售第1张车票
57 售票窗口2正在售第-1张车票//甚至出现了-1号、0号

产生这种结果的原因:

  假设系统在出售“第44张车票”的时候,线程“售票窗口1”获取到了CPU的执行权,流程用下图表示:

判断应用程序是否有线程安全的问题不外乎以下几点:  

*是否是多线程环境
*是否有共享数据
*是否有多条语句操作共享数据

很明显上面的程序都满足这三点,解决思路:把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。楼主这里使用同步代码块改造Ticket类如下:

 1 package com.jon.thread;
 2 
 3 public class TicketSell implements Runnable {
 4     private int tickets = 50;
 5     private Object obj = new Object();
 6     @Override
 7     public void run() {
 8         while(true){
 9             synchronized (obj) {
10                 try {
11                     Thread.sleep(100);
12                 } catch (InterruptedException e) {                
13                     e.printStackTrace();
14                 }
15                 if(tickets>0){                
16                     System.out.println(Thread.currentThread().getName() + "正在售第" + (tickets--) + "张车票");
17                 }
18             }            
19         }
20     }
21 
22 }

再来运行,结果如下:

 1 售票窗口3正在售第50张车票
 2 售票窗口3正在售第49张车票
 3 售票窗口3正在售第48张车票
 4 售票窗口1正在售第47张车票
 5 售票窗口1正在售第46张车票
 6 售票窗口1正在售第45张车票
 7 售票窗口2正在售第44张车票
 8 售票窗口2正在售第43张车票
 9 售票窗口2正在售第42张车票
10 售票窗口2正在售第41张车票
11 售票窗口1正在售第40张车票
12 售票窗口1正在售第39张车票
13 售票窗口3正在售第38张车票
14 售票窗口1正在售第37张车票
15 售票窗口2正在售第36张车票
16 售票窗口2正在售第35张车票
17 售票窗口2正在售第34张车票
18 售票窗口2正在售第33张车票
19 售票窗口2正在售第32张车票
20 售票窗口2正在售第31张车票
21 售票窗口1正在售第30张车票
22 售票窗口3正在售第29张车票
23 售票窗口3正在售第28张车票
24 售票窗口3正在售第27张车票
25 售票窗口3正在售第26张车票
26 售票窗口3正在售第25张车票
27 售票窗口3正在售第24张车票
28 售票窗口3正在售第23张车票
29 售票窗口3正在售第22张车票
30 售票窗口3正在售第21张车票
31 售票窗口1正在售第20张车票
32 售票窗口1正在售第19张车票
33 售票窗口1正在售第18张车票
34 售票窗口2正在售第17张车票
35 售票窗口2正在售第16张车票
36 售票窗口2正在售第15张车票
37 售票窗口1正在售第14张车票
38 售票窗口3正在售第13张车票
39 售票窗口3正在售第12张车票
40 售票窗口3正在售第11张车票
41 售票窗口1正在售第10张车票
42 售票窗口1正在售第9张车票
43 售票窗口2正在售第8张车票
44 售票窗口2正在售第7张车票
45 售票窗口2正在售第6张车票
46 售票窗口1正在售第5张车票
47 售票窗口1正在售第4张车票
48 售票窗口1正在售第3张车票
49 售票窗口3正在售第2张车票
50 售票窗口3正在售第1张车票
View Code

可以看到,不再有重复的票出现。当然同步代码块也有它的弊端,当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

 有兴趣的小伙伴可以到这里下载文章中用到的代码:https://github.com/LJunChina/JavaResource

以上是关于线程安全问题的主要内容,如果未能解决你的问题,请参考以下文章

HashMap 和 ConcurrentHashMap 的区别

线程同步-使用ReaderWriterLockSlim类

newCacheThreadPool()newFixedThreadPool()newScheduledThreadPool()newSingleThreadExecutor()自定义线程池(代码片段

线程安全问题的概述和线程安全的代码实现与问题产生的原理

多线程 Thread 线程同步 synchronized

活动到片段方法调用带有进度条的线程