线程安全——卖票问题
描述:在使用多线程进行操作时可能会出现线程冲突的问题, 使得最终的结果存在一定的隐患, 那么是由于什么导致的线程不安全, 又有什么办法可以解决这个问题就是该部分的所讲述内容
问题描述:
模拟卖票,三个窗口同时进行买票,一共有100张票,100张票卖完程序结束
问题代码①:
实现内容:创建三个线程卖票
public class ThreadSafe {
public static void main(String[] args) {
// 通过实现 Runnable 接口创建线程
SellTicket sellTicket = new SellTicket();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
// 存在重复卖票或超卖票到负值, 没有来得及判断循环是否结束就又一次进入代码中进行 --, 出现超卖情况
}
}
class SellTicket implements Runnable {
private static int ticketNumber = 100;// 设置初始票数
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "卖出去一张票, 剩余票数:" + --ticketNumber);
// 可能会出现多个线程同时执行到这条判断语句的情况, 当所剩票数为2时, 就会超卖一张票
if (ticketNumber <= 0) {
System.out.println(Thread.currentThread().getName() + "票卖完了");
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
引出问题①:
由运行结果可以看出, 出现了重复卖票的情况, 在卖票结束的时候,还出现了票卖到负数的情况, 但是我们起初的总票数一共100张, 根据实际情况不可能出现票数为负数的情况,也不可能出现同一张票卖两次的情况
问题代码②:
public class ThreadSafe {
public static void main(String[] args) {
// 通过继承 类Thread 创建线程
SellTicket sellTicket1 = new SellTicket();
SellTicket sellTicket2 = new SellTicket();
SellTicket sellTicket3 = new SellTicket();
sellTicket1.start();
sellTicket2.start();
sellTicket3.start();
// 存在重复卖票或超卖票到负值, 没有来得及判断循环是否结束就又一次进入代码中进行 --, 出现超卖情况
}
}
class SellTicket extends Thread {
private static int ticketNum = 100;// 设置初始票数
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "卖出去一张票, 剩余票数:" + --ticketNum);
// 可能会出现多个线程同时执行到这条判断语句的情况, 当所剩票数为2时, 就会超卖一张票
if (ticketNum <= 0) {
System.out.println(Thread.currentThread().getName() + "票卖完了");
break;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
引出问题②:
可以看出, 无论是使用实现Runnable接口的方式实现卖票还是通过继承类Thread的方式实现卖票均会出现重复卖票和超卖票的问题. 由此可以得出结论这与创建线程的方式无关
解决问题
首先认识一个关键字:synchronized——同步
我们将使用这个关键字来解决线程安全的问题, synchronized会给指定范围内的代码加上一个互斥锁, 当某一线程运行这段代码时其他线程无法进入, 直到这个线程把这段代码运行结束(释放锁), 其他线程才能进去并且再次给这段代码加上一个互斥锁.
新的问题:互斥锁到底是什么, 它是以什么为锁的使得只能有一个线程进入, 互斥锁可以是一个对象, 这个对象可以是自己定义的也可是系统默认加, 对于要进行安全控制的线程必须使他们被同一个锁控制, 即同一个对象
现在分别演示这两种加锁方式
①(代码)系统默认加:
public class ThreadSafe {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
}
}
// 实现接口方式, 使用 synchronized实现线程同步
class SellTicket implements Runnable {
private static int ticketNumber = 100;// 设置初始票数
private boolean flag = true;
// public synchronized void sell() {} 就是一个同步方法
// 此时锁在 this对象, 由系统默认加
public synchronized void sell() { // 线程同步, 同一时刻, 只允许一个线程调用 sell方法
if (ticketNumber <= 0) {
System.out.println(Thread.currentThread().getName() + "票卖完了");
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + "卖出去一张票, 剩余票数:" + --ticketNumber);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
// 同步方法, 在同一时刻, 只能有一个线程来执行 run方法, 也相当于同一时刻只能一个线程调用 sell方法
// 切记以后不要这样写, 不要将synchronized修饰符修饰到 run方法上, 否则就只有这一个线程在卖了, 剩余线程出来后就发现已经卖光了
while (flag) {
sell();// 将卖票的内容全都写在一个方法中, 给这个方法用 synchronized 修饰, 这样才能让多线程启动并且同一时刻只能有一个操作(卖票)
}
}
}
运行结果:
②(代码)自定义锁, 即在指定代码块上加锁:
public class ThreadSafe {
public static void main(String[] args) {
// 方式: 实现Runnable接口完成
SellTicket sellTicket = new SellTicket();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
}
}
// 实现接口方式, 使用 synchronized实现线程同步
class SellTicket implements Runnable {
private static int ticketNumber = 100;// 设置初始票数
private boolean flag = true;
Object object = new Object();
// 在代码块上加锁 写synchronized, 互斥锁仍然在当前对象上, 注意: 必须是当前对象, 相同对象
public void sell() {
synchronized (object) { // 括号中要求得是一个对象, 不同对象会有多把锁, 这个地方只是填入一把锁, 然后线程去使用它, 所以只要是一个对象即可
if (ticketNumber <= 0) {
System.out.println(Thread.currentThread().getName() + "票卖完了");
flag = false;
return;
}
System.out.println(Thread.currentThread().getName() + "卖出去一张票, 剩余票数:" + --ticketNumber);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
while (flag) {
sell();
}
}
}
运行结果:
现在我们知道了怎么给线程上锁, 解决了线程的安全问题. 那么再来了解一下 synchronized 会带来什么问题, 在日常编写代码的时候一定要避免此类问题的发生
线程死锁
代码演示:
public class ThreadDeadLock {
public static void main(String[] args) {
DeadLock A = new DeadLock(true);
DeadLock B = new DeadLock(false);
A.setName("A线程");// 设置线程名称
B.setName("B线程");// 设置线程名称
A.start();
B.start();
// 一定要避免线程死锁, 很危险的!!!!!
}
}
class DeadLock extends Thread {
static Object o1 = new Object();
static Object o2 = new Object();
boolean flag;
public DeadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (o1) { // 对象互斥锁, 下面就是同步代码
System.out.println(Thread.currentThread().getName() + " 进入1");
synchronized (o2) { // 这里获得o1对象的监视权
System.out.println(Thread.currentThread().getName() + " 进入2");
}
}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + " 进入3");
synchronized (o1) { // 这里获得o1对象的监视权
System.out.println(Thread.currentThread().getName() + " 进入4");
}
}
}
}
}
运行结果:
原因分析:
运行结果中发现程序并不能执行完毕, 原因分析:
1.如果 flag 为true, 线程 A 会先得到 o1对象锁, 然后尝试去获取 o2 对象锁
如果线程 A 得不到 o2 对象锁, 就会Blocked
2.如果 flag 为false, 线程 B 就会先得到 o2 对象锁, 然后尝试获取 o1 对象锁
如果线程 B 得不到 o1 对象锁, 就会Blocked