18Java多线程之初识线程安全

什么是线程安全

所谓线程安全指的是多个线程对同一资源进行访问时,有可能产生数据不一致问题,导致线程访问的资源并不是安全的。

判断线程安全

判断一个程序是否存在线程安全问题的依据

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

电影院卖票案例

某电影院目前正在上映贺岁大片,共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。

库存类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 为了让多个线程对象共享库存相关信息,所以用 static 修饰
*/
class Stock {
/**
* 原始库存
*/
public final static int originalStock = 100;
/**
* 剩余库存
*/
public static AtomicInteger remainStock = new AtomicInteger(originalStock);
/**
* 已售票数,线程不安全
*/
public static int sellTickets = 0;
/**
* 已售票数,线程安全,原子操作
*/
public static AtomicInteger sellTicketsAtomic = new AtomicInteger(0);
}

方式一:继承Thread类

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
public class SellTicketDemo {
public static void main(String[] args) {
// 创建三个线程对象
SellTicket st1 = new SellTicket();
SellTicket st2 = new SellTicket();
SellTicket st3 = new SellTicket();

// 给线程对象起名字
st1.setName("窗口1");
st2.setName("窗口2");
st3.setName("窗口3");

// 启动线程
st1.start();
st2.start();
st3.start();
}
}

/**
* 方式一:继承Thread类
*/
class SellTicket extends Thread {
@Override
public void run() {
// 已售票数小于库存,就进行售票
while (Stock.sellTickets < Stock.originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
System.out.println(getName() + "\t\t正在出售第" + (++Stock.sellTickets) + "张票\t\t" +
"原子操作出售第" + Stock.sellTicketsAtomic.incrementAndGet() + "张票\t\t" +
"原始库存" + Stock.originalStock + "张票\t\t" +
"库存剩余" + Stock.remainStock.decrementAndGet() + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

System.out.println("库存剩余" + Stock.remainStock.get());
}
}

方式二:实现Runnable接口

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
public class SellTicketDemo {
public static void main(String[] args) {
// 创建资源对象
SellTicket st = new SellTicket();

// 创建三个线程对象
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");

// 启动线程
t1.start();
t2.start();
t3.start();
}
}

/**
* 方式二:实现Runnable接口
*/
class SellTicket extends Stock implements Runnable {
@Override
public void run() {
// 已售票数小于库存,就进行售票
while (sellTickets < originalStock) {
try {
// 模拟售票员正在售票
Thread.sleep(100);
// 注意这里通过 Thread.currentThread().getName() 获取线程名
System.out.println(Thread.currentThread().getName() + "\t\t正在出售第" + (++sellTickets) + "张票\t\t" +
"原子操作出售第" + sellTicketsAtomic.incrementAndGet() + "张票\t\t" +
"原始库存" + originalStock + "张票\t\t" +
"库存剩余" + remainStock.decrementAndGet() + "张票\t\t");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

System.out.println("库存剩余" + remainStock.get());
}
}

发现问题(线程不安全)

为了更符合真实的场景,加入了休眠100毫秒,导致:

  • 已售票数超出库存
  • 同一张票出售多次