前言:
在生活中,每次出远门,避免不了的就是要坐火车或者高铁,那么抢票就是我们必须要经历的环节,但你是否想过,假如你和别人同时抢到一张票,会发生什么?
你肯定会疑惑,如果两个人都买到一张票,那么这张票到底算谁的,这显然是不符合常理的,那么怎样才能避免不会买到同买一张票?这就是今天我们要思考的问题;
其实这里面涉及到了Java中的多线程以及线程安全的问题,保证线程安全也是我们在实际开发中所需要重点关注的,那么用Java代码如何来实现和解决这个经典的抢火车票问题?
在用代码实现抢火车票问题前,你是否有疑惑,我前面提到的一个名词线程安全,那么,究竟什么是线程安全呢?
当多个线程访问一个资源对象时,如果进行了额外的同步控制,或者其他的协调操作,调用这个对象都可以获得正确的结果 (即多个线程去访问同一个对象,和单个线程去执行,其结果是一样的),我们就说这个对象是线程安全的;
就拿上面的火车抢票来举例,你可以把火车票看成是一个共享的资源对象,那么多个人去抢同一张票就是多个线程去竞争同一个资源对象,无论是此时只有我一个人在抢票,还是多个人去同时抢票,我们都应该能够买到我们要买的那张票,而不会出现两个人同时抢到一张票的情况
其实线程安全,准确来说应该是内存安全,因为堆是共享内存,所以它可以被所有的线程访问
上面提到了堆是用来共享内存的,那么堆又是什么呢?让我们一起来简单了解一下吧!
堆是进程和线程共有的空间,分为全局堆和局部堆
简单来说,全局堆就是没有分配的空间,局部堆就是用户系统的空间
上面提到了局部堆是用户系统的空间,那么就不得不提到操作系统,那么操作系统中的堆是什么呢?
在操作系统中,堆是在进程初始化时进行分配的,运行过程中可以向系统要额外的堆,但是用完了就要归还给操作系统,否则将会造成内存泄露
补充:
在Java中,堆是JVM (Java虚拟机) 所管理的内存中最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建;
堆所在的内存区域用来存放对象实例,几乎所有的对象实例以及数组都在这里分配内存
前面提到了堆是进程和线程的共有空间,说到堆,我们就会不由自主的想到栈,那么究竟什么是栈呢?
栈是每个线程独有的,保存其运行状态和局部变量的
栈在线程开始运行时初始化,每个线程的栈相互独立,因此,栈是线程安全的
前面简单解释了堆在操作系统中的理解,那么操作系统中的栈又是怎样的呢?
在操作系统中,切换线程时就会自动切换栈,栈空间不需要像在Java这样的高级语言中,显式的去分配和释放
目前主流的操作系统都是多任务的,即多个线程同时运行,为了保证线程安全,每个线程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统所保障的
而在每个线程的内存空间中,都会有一块特殊的公共区域,通常称为堆 (也就是内存),进程内的所有线程都可以访问到该区域,这就是造成线程安全问题的潜在因素
通过上面对线程安全的学习,你是否对线程安全有了简单的了解;接下来,就让我们一起用Java代码来实现简单的多线程火车抢票问题吧!
还记得在上一篇博客中,我们提到了Java中实现多线程的方式有哪些?你还记得吗?
答案是一共有三种,分别是:
继承Thread类 (重点)
实现Runnable接口 (重点)
实现Callable接口 (了解)
你答对了吗?今天我们主要使用前两种方式来实现火车抢票问题
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();
}
}
我们发现在抢票过程中发生了数据紊乱,出现同一张票被两个人都抢到的情况,并且还会出现抢到第0张票的结果,这显然不是我们期待的结果
我们再通过实现Runnable接口的方式来执行一下这个火车抢票,看是否会出现同样的问题
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();
}
}
这次虽然避免了第0张票的出现,但是还是会存在两个人同时抢到一张票的问题
那么我们不妨思考一下,为什么会出现两个人抢到同一张票的问题呢?
还记得前面提到的线程安全的概念吗,多个线程竞争同一个资源对象,如果额外的同步控制,那么就可以保证线程安全;因为这里我们并没有采取任何的同步控制,即给共享资源对象车票加一个同步锁,当其中一个人抢到票时就给这张票加锁,期间不允许其他人再抢,这样就很好的避免出现两个人会抢到同一张票!
为了解决两个人同时抢到一张票的问题,我们可以使用synchronized同步器实现同步控制!
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();
}
}
和我们预期的一样,每个人都成功抢到了各自的票,并且没有出现两个人抢到同一张票的问题!
到这里,我们就学习完了线程安全和多线程抢票问题,欢迎大家讨论和学习!
参考视频链接:
https://www.bilibili.com/video/BV1V4411p7EF (B站UP主遇见狂神说的多线程详解)
https://www.bilibili.com/video/BV1Eb4y1R7zd (B站UP主图灵学院程序员Mokey)