多线程基础学习之线程安全和抢火车票问题

前言

在生活中,每次出远门,避免不了的就是要坐火车或者高铁,那么抢票就是我们必须要经历的环节,但你是否想过,假如你和别人同时抢到一张票,会发生什么?

你肯定会疑惑,如果两个人都买到一张票,那么这张票到底算谁的,这显然是不符合常理的,那么怎样才能避免不会买到同买一张票?这就是今天我们要思考的问题;

其实这里面涉及到了Java中的多线程以及线程安全的问题,保证线程安全也是我们在实际开发中所需要重点关注的,那么用Java代码如何来实现和解决这个经典的抢火车票问题?

在用代码实现抢火车票问题前,你是否有疑惑,我前面提到的一个名词线程安全,那么,究竟什么是线程安全呢?

一、线程安全

1.1 什么是线程安全?

当多个线程访问一个资源对象时,如果进行了额外的同步控制,或者其他的协调操作,调用这个对象都可以获得正确的结果 (即多个线程去访问同一个对象,和单个线程去执行,其结果是一样的),我们就说这个对象是线程安全的;

1.2 简单案例解释

就拿上面的火车抢票来举例,你可以把火车票看成是一个共享的资源对象,那么多个人去抢同一张票就是多个线程去竞争同一个资源对象,无论是此时只有我一个人在抢票,还是多个人去同时抢票,我们都应该能够买到我们要买的那张票,而不会出现两个人同时抢到一张票的情况

1.3 线程安全的知识扩展

其实线程安全,准确来说应该是内存安全,因为堆是共享内存,所以它可以被所有的线程访问

上面提到了堆是用来共享内存的那么堆又是什么呢?让我们一起来简单了解一下吧!

1.3.1 堆的相关概念
1.什么是堆?

是进程和线程共有的空间,分为全局堆局部堆

2. 全局堆和局部堆的区别?

简单来说,全局堆就是没有分配的空间局部堆就是用户系统的空间

上面提到了局部堆是用户系统的空间,那么就不得不提到操作系统,那么操作系统中的堆是什么呢?

3.操作系统中堆的理解

在操作系统中,是在进程初始化时进行分配的,运行过程中可以向系统要额外的堆,但是用完了就要归还给操作系统,否则将会造成内存泄露

补充

在Java中,JVM (Java虚拟机) 所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建

堆所在的内存区域用来存放对象实例,几乎所有的对象实例以及数组都在这里分配内存

前面提到了堆是进程和线程的共有空间,说到堆,我们就会不由自主的想到,那么究竟什么是栈呢?

1.3.2 栈的相关概念
1.什么是栈?

是每个线程独有的,保存其运行状态局部变量

2.栈是线程安全的吗?

在线程开始运行时初始化,每个线程的栈相互独立因此,栈是线程安全的

前面简单解释了堆在操作系统中的理解,那么操作系统中的栈又是怎样的呢

3.操作系统中栈的理解

在操作系统中,切换线程时就会自动切换栈,栈空间不需要像在Java这样的高级语言中,显式的去分配和释放

1.4 造成线程/内存不安全的主要原因是什么?

  • 目前主流的操作系统都是多任务的,即多个线程同时运行,为了保证线程安全,每个线程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统所保障的

  • 而在每个线程的内存空间中,都会有一块特殊的公共区域,通常称为 (也就是内存),进程内的所有线程都可以访问到该区域,这就是造成线程安全问题的潜在因素

通过上面对线程安全的学习,你是否对线程安全有了简单的了解;接下来,就让我们一起用Java代码来实现简单的多线程火车抢票问题吧!

二、多线程抢火车票问题

还记得在上一篇博客中,我们提到了Java中实现多线程的方式有哪些?你还记得吗?

答案是一共有三种,分别是

  • 继承Thread类 (重点)

  • 实现Runnable接口 (重点)

  • 实现Callable接口 (了解)

你答对了吗?今天我们主要使用前两种方式来实现火车抢票问题

2.1 通过继承Thread类实现
2.1.1 代码实现
package com.kuang.thread;
//多线程案例:抢火车票问题
//方式一:自定义MyThread类,并继承Thread类
public class MyThread extends Thread {
    //共享变量火车票,初始值为10张
    private int ticketNums = 10;
    //重写run方法
    @Override
    public void run() {
        //判断值是否为真
        while (true) {
            //判断票数是否小于0
            if(ticketNums < 0) {
                //如果票数小于0,就跳出循环
                break;
            }
            try {
                //设置线程休眠时间为1秒(防止票被一个人全拿完了)
                Thread.sleep(1000);
            //捕获中断异常    
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //打印当前线程名称以及获取到第几张票的信息 (票数每次减1)
            System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNums--+"张票");
        }
    }
    //主方法测试
    public static void main(String[] args) {
        //获取自定义线程类对象
        MyThread ticket = new MyThread();
        //设置线程对象的名字(这里模拟三个人同时抢票),并调用start方法启动线程
        new Thread(ticket,"张三").start();
        new Thread(ticket,"罗翔").start();
        new Thread(ticket,"黄牛").start();
    }
}
2.2.2 测试结果

在这里插入图片描述

2.2.3 结果分析

我们发现在抢票过程中发生了数据紊乱,出现同一张票被两个人都抢到的情况,并且还会出现抢到第0张票的结果,这显然不是我们期待的结果

2.2 通过实现Runnable接口方式

我们再通过实现Runnable接口的方式来执行一下这个火车抢票,看是否会出现同样的问题

2.2.1 测试代码
package com.kuang.thread;
//方式二:创建自定义实现Runnable接口的MyThread2类
public class MyThread2 implements Runnable{
    //共享变量火车票,初始值为10张
    private int ticketNums = 10;
    //重写run方法
    @Override
    public void run() {
        //判断值是否为真
        while (true) {
            //判断票数是否小于等于0(为了避免出现第0张票的问题,我们将条件修改为<=)
            if(ticketNums <= 0) {
                //如果票数小于0,就跳出循环
                break;
            }
            try {
                //设置线程休眠时间为1秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //打印当前线程名称以及获取到第几张票的信息 (票数每次减1)
            System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNums--+"张票");
        }
    }
    public static void main(String[] args) {
        //获取自定义线程类对象
        MyThread2 ticket = new MyThread2();
        //设置线程对象的名字(模拟三个人同时抢票),并调用start方法启动线程
        new Thread(ticket,"张三").start();
        new Thread(ticket,"罗翔").start();
        new Thread(ticket,"黄牛").start();
    }
}
2.2.2 测试结果

在这里插入图片描述

2.2.3 结果分析

这次虽然避免了第0张票的出现,但是还是会存在两个人同时抢到一张票的问题

那么我们不妨思考一下,为什么会出现两个人抢到同一张票的问题呢?

还记得前面提到的线程安全的概念吗,多个线程竞争同一个资源对象,如果额外的同步控制,那么就可以保证线程安全;因为这里我们并没有采取任何的同步控制,即给共享资源对象车票加一个同步锁,当其中一个人抢到票时就给这张票加锁,期间不允许其他人再抢,这样就很好的避免出现两个人会抢到同一张票!

为了解决两个人同时抢到一张票的问题,我们可以使用synchronized同步器实现同步控制!

2.3 使用synchronized同步器解决抢票问题
2.3.1 代码实现
package com.kuang.thread;
//解决会出现同一张票被两个人都抢到问题
public class MyThread3 implements Runnable{
    //共享变量火车票,初始值为10张
    private int ticketNums = 10;
    //设置标志位,初始值为true
    boolean flag = true;
    //重写run方法
    @Override
    public void run() {
        //判断标志位值是否为真
        while (flag) {
            try {
                //设置线程休眠时长为1秒 (防止票被一个人全拿完了)
                Thread.sleep(1000);
                //执行买票的方法
                buy();
            //捕获中断异常    
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //使用synchronized同步器来修饰买票的方法(防止出现一张票被两人同时抢到)
    private synchronized void buy() throws InterruptedException {
        //判断票数是否小于等于0
        if(ticketNums <= 0) {
            //如果票数小于等于0,就将标志位设置为false并返回
           flag = false;
           return;
        }
        //设置线程休眠时长为1秒
        Thread.sleep(1000);
        //打印当前线程名称以及获取到第几张票的信息 (票数每次减1)
        System.out.println(Thread.currentThread().getName()+"抢到了第"+ticketNums--+"张票");
    }
    public static void main(String[] args) {
        //获取自定义线程类对象ticket
        MyThread3 ticket = new MyThread3();
        //设置线程对象的名字,并调用start方法启动线程
        new Thread(ticket,"张三").start();
        new Thread(ticket,"罗翔").start();
        new Thread(ticket,"黄牛").start();
    }
}
2.3.2 测试结果

在这里插入图片描述

2.3.3 结果分析

和我们预期的一样,每个人都成功抢到了各自的票,并且没有出现两个人抢到同一张票的问题!

到这里,我们就学习完了线程安全和多线程抢票问题,欢迎大家讨论和学习!

参考视频链接
https://www.bilibili.com/video/BV1V4411p7EF (B站UP主遇见狂神说的多线程详解)
https://www.bilibili.com/video/BV1Eb4y1R7zd (B站UP主图灵学院程序员Mokey)

  • 1
    点赞
  • 1
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术工厂 设计师:CSDN官方博客 返回首页
评论

打赏作者

狂奔の蜗牛rz

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值