多线程安全学习之售票案例

在说多线程之前,先说两个概念:并行和并发。

并行:指两个或多个事件在同一时刻发生(同时发生)。
并发:指两个或多个事件在同一个时间段内发生。

从上面的概念可知,现在经常提起的高并发,并不是指瞬时同时发生,而是一种CPU的瞬时大量切换处理线程的情形。CPU在多个线程之间来回切换,来处理相应的任务。

现在我们在购买电脑时,导购员经常会跟我们说是四核八线程等等。指的就是CPU的核数,这样在处理多线程任务时,就可以更高效地并行执行任务了。

再来说一个概念:多个CPU和多核CPU的区别?

多个CPU,那就是多个物理CPU,各个CPU之间是通过总线进行通信的,效率比较低。
多核CPU:不同的核通过L2 Cache进行通信,存储和外设通过总线与CPU通信。
这二者的效率肯定是:多核CPU>多个CPU,但是多核的它贵啊。


现在让我们一个简单售票的小例子来看一下线程安全的问题及解决方案吧。

售票的简单例子

模拟一下售票的例子,使用三个线程去售票(总共票数设置为100张):

/**
 * @author 030
 * @date 14:45 2021/11/4
 * @description 实现卖票案例
 */
public class SaleTicketImpl implements Runnable {
    /*定义一个多线程共享的资源*/
    private int ticket = 100;

    /*实现多线程任务:卖票*/
    @Override
    public void run() {
        // 使用死循环,重复卖票
        while (true) {
            if (ticket > 0) {
                try {
                    // 为了提高安全问题出现的概率, 让程序在这里睡眠一会
                    Thread.sleep(10);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                // 票存在,卖票,ticket--
                System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票");
                ticket--;
            }
        }
    }
}

测试类如下:

/**
 * @author 030
 * @date 14:58 2021/11/4
 * @description 模拟卖票案例:创建3个线程,同时开启,对共享票进行出售
 */
public class SaleTicketTest {
    public static void main(String[] args) {	// main线程,即主线程
        // 创建Runnable接口的实现类对象
        /*线程不安全的售票类*/
        SaleTicketImpl saleImpl = new SaleTicketImpl();
        try {
            // 因为 Runnable 接口中没有提供 start开启线程方法,因此还需要通过 Thread 类实例对象来实现。
            // 创建Thread类对象,构造方法中传递Runnable接口的实现类。
            Thread t1 = new Thread(saleImpl);
            t1.setName("一号窗口"); // 设置线程的名字,默认的线程名叫Thread-0
            Thread t2 = new Thread(saleImpl);
            t2.setName("二号窗口");
            Thread t3 = new Thread(saleImpl);
            t3.setName("三号窗口");

            // 调用 start 方法开启多线程
            t1.start();	// 开启了一个新的线程
            t2.start();
            t3.start();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

经过测试发现,这种代码会出现重复售票以及售出负数票的情况。

说明上面的售票线程实现类存在多线程下安全问题,多线程之所以会出现安全问题,究其本质是因为多个线程操作了共享数据资源导致的。

那么我们该怎么解决这个多线程访问共享资源的安全问题呢?

来看第一种解决方式,使用同步代码块

解决多线程安全问题

同步代码块

/**
 * @author 030
 * @date 14:57 2021/11/4
 * @description 解决线程安全问题方式一:使用 synchronized 同步代码块
 */
public class SaleTicketImpl implements Runnable{
    /*定义一个多线程共享的资源*/
    private int ticket = 100;

    // 解决线程安全问题的第一种方案:使用同步代码块
    /**
     * 格式:
     *  synchronized(锁对象){
     *      // 这里写可能会出现线程安全问题的代码(访问了共享数据的代码)
     *  }
     *
     *  注意:
     *      1. 同步代码块中的锁对象,可以使用任意的对象。
     *      2. 但是必须保证多个线程使用的锁对象是同一个
     *      3. 锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。
     */
    // 构造 Object 锁对象
    Object obj = new Object();

    /*实现多线程任务:卖票*/
    @Override
    public void run() {
        // 使用死循环,重复卖票
        while (true) {
            synchronized (obj) {
                if (ticket > 0) {
                    try {
                        // 为了提高安全问题出现的概率, 让程序在这里睡眠一会
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    // 票存在,卖票,ticket--
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票");
                    ticket--;
                }
            }
        }
    }
}

同步方法

/**
 * @author 030
 * @date 15:03 2021/11/4
 * @description 解决线程安全问题方式二:使用 同步方法
 *  使用步骤:
 *      1. 把访问了共享数据的代码抽取出来,放到一个方法中
 *      2. 在方法上添加 synchronized 修饰符
 */
public class SaleTicketImpl implements Runnable{
    /*定义一个多线程共享的资源*/
    private int ticket = 100;

    /*实现多线程任务:卖票*/
    @Override
    public void run() {
        // 使用死循环,重复卖票
        while (true) {
            saleTicket();
        }
    }

    /**
     * 定义一个同步方法
     * 同步方法也会把方法内部的代码锁住,只让一个线程执行
     * 同步方法的锁对象是谁?
     *  就是实现类对象 new SaleTicketImpl(),也就是 This
     */
    public synchronized void saleTicket(){
        if (ticket > 0) {
            try {
                // 为了提高安全问题出现的概率, 让程序在这里睡眠一会
                Thread.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            }
            // 票存在,卖票,ticket--
            System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票");
            ticket--;
        }
    }
}

使用Lock锁的子类实现

/**
 * @author 030
 * @date 15:21 2021/11/4
 * @description 解决线程安全问题方式三:使用 Lock锁(jdk1.5之后才有的接口,常见的该接口实现类 ReentrantLock)
 * 使用步骤:
 *  1. 在成员位置创建一个 ReentrantLock 对象
 *  2. 在可能会出现线程安全问题的代码前调用 Lock 接口中的 lock() 方法获取锁
 *  3. 在可能会出现线程安全问题的代买后调用 Lock 接口中的 unLock() 方法释放锁
 */
public class SaleTicketImpl implements Runnable{
    /*定义一个多线程共享的资源*/
    private int ticket = 100;

    /*1. 在成员位置创建一个 ReentrantLock 对象*/
    Lock lock = new ReentrantLock();

    /*实现多线程任务:卖票*/
    @Override
    public void run() {
        // 使用死循环,重复卖票
        while (true) {
            /*2. 在可能会出现线程安全问题的代码前调用 Lock 接口中的 lock() 方法获取锁*/
            lock.lock();
            if (ticket > 0) {
                try {
                    // 为了提高安全问题出现的概率, 让程序在这里睡眠一会
                    Thread.sleep(10);
                    // 票存在,卖票,ticket--
                    System.out.println(Thread.currentThread().getName() + "正在出售第" + ticket + "张票");
                    ticket--;
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    /*3. 在可能会出现线程安全问题的代买后调用 Lock 接口中的 unLock() 方法释放锁*/
                    lock.unlock();
                }

            }
        }
    }
}

以上三种方式都可以解决多线程对共享资源操作带来的安全问题。

posted @ 2022-06-22 00:07  LoremMoon  阅读(81)  评论(0编辑  收藏  举报