线程安全, 线程死锁

文章探讨了在多线程环境下卖票过程中出现的线程安全问题,通过实例展示了未加锁的代码可能导致重复卖票和超卖。文章介绍了如何使用`synchronized`关键字和自定义锁来确保线程安全,并提到了线程死锁的概念及其避免策略。
摘要由CSDN通过智能技术生成

线程安全——卖票问题

描述:在使用多线程进行操作时可能会出现线程冲突的问题, 使得最终的结果存在一定的隐患, 那么是由于什么导致的线程不安全, 又有什么办法可以解决这个问题就是该部分的所讲述内容

问题描述:

模拟卖票,三个窗口同时进行买票,一共有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

  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Black—slience

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值