文章首发于 2020-11-29
知乎文章:Java线程同步-模拟买票
作者:落雨湿红尘(也是我o)
01 导语
本文使用JAVA代码模拟买票场景下的业务交互,通过示例讲解线程的初始化、线程同步等java线程基础知识。
提出这样一个问题:三个人排队买电影票(每张5元)。三个人按前后顺序分别为张某(拿着20元钞票)、李某(拿着10元钞票)和赵某(拿着5元钞票)。而售票员的钱箱中现在只有3张5元钞票,试对这样一个场景进行模拟

02 线程同步问题
当多个线程对共享资源进行操作时,势必会出现进程间相互争夺资源的情况,而线程同步问题则是规划各线程间操作顺序,让每个线程不受其他线程打扰的情况下独自安全的操作共享资源。
而在本场景中,那个售票员就是这样一个共享资源。在对每个人受理业务(即每个线程)的过程中,这个售票员钱箱里的钱会发生变化,但是每个人受理业务时,不能被其他线程打扰(即别人不能插队)
下面构造售票员这样一个共享资源,其有一个找零功能change()。
找零算法的思想是:对于需要找零的金额moneyChange,若其等于票价,就不需要找零。否则,用售票元手上的每个币种,从大到小依次找零。每找零一次,找零金额moneyChange就会减小。最后当最小币种找零时,若其不足以找零,说明售票员手上的钱是不足以找零的,返回一个false。若其可以找零,说明售票员手上的钱足以找零,把受理对象钱收下,给他票,返回true。
//售票员
class Seller {
public static final int PRICE = 5;//票单价5元
private int five ;//5元纸币数量
private int ten ;//10元纸币数量
private int twenty ;//20元纸币数量
//getter and setter
public int getFive() {
return five;
}
public void setFive(int five) {
this.five = five;
}
public int getTen() {
return ten;
}
public void setTen(int ten) {
this.ten = ten;
}
public int getTwenty() {
return twenty;
}
public void setTwenty(int twenty) {
this.twenty = twenty;
}
public Seller(int five, int ten, int twenty) {
this.five = five;
this.ten = ten;
this.twenty = twenty;
}
/**
*找零
* @param target受理对象
* @param five 收到的5元纸币数
* @param ten 收到的10元纸币数
* @param twenty 收到的20元纸币数
*/
public boolean change(String target,int five,int ten ,int twenty){
int pay = five*5+ten*10 +twenty*20;//支付的钱
int moneyChange = pay - PRICE;//找零
if(moneyChange == 0){
//不需要找0
System.out.println("售票员收到"+pay+"元钱,递给"+target+"1张入场券\n");
this.setFive(this.five + five);
return true;
}
//使用不同币种依次找零
//使用20元纸币找零
int twentyChange = moneyChange/20;
if (this.twenty >= twentyChange) {
moneyChange = moneyChange -twentyChange*20;
this.setTwenty(this.twenty - twentyChange);
}else {
moneyChange = moneyChange-this.twenty*20;
twentyChange = this.twenty;//钱箱中只能找这么多20块的
this.setTwenty(0);//全部拿去找零
}
//使用10元纸币找零
int tenChange = moneyChange/10;
if (this.ten >= tenChange) {
moneyChange = moneyChange -tenChange*10;
this.setTen(this.ten - tenChange);
}else {
moneyChange = moneyChange-this.ten*10;
tenChange = this.ten;//只能找这么多10块的
this.setTen(0);//全部拿去找零
}
//使用5元纸币找零
int fiveChange = moneyChange/5;
if (this.five >= fiveChange) {
moneyChange = moneyChange -fiveChange*5;
this.setFive(this.five - fiveChange);
//此时可以说明钱箱中的钱足以找零
System.out.println("售票员收到"+pay+"元钱,递给"+target+"1张入场券,并找零20*"+twentyChange+"+10*"+tenChange+"+5*"+fiveChange+"元\n");
//收钱
this.setFive(this.five + five);
this.setTen(this.ten +ten);
this.setTwenty(this.twenty +twenty);
return true;
}else {
//此时,钱箱中的所有币种加起来都不足以找零
return false;
}
}
}
03 线程初始化
Java中,线程初始化一般有两种方式:
1.继承Thread类,重写run方法
运行线程时,直接使用实例化对象的start方法即可运行线程,原因是Thread类及其子类带有start方法。以下为伪代码
//买票过程
class BuyAndSell extends Thread {
//对线程各参数的设定
//线程构造方法
@Override
public void run() {
//你要让线程执行的功能,在这里书写
}
}
}
public class Main {
public static void main(String args[ ]) {
//线程构造,这里使用了向上转型
Thread zhang = new BuyAndSell();
//运行线程
zhang.start();
}
}
2.实现Runable接口,重写run方法
此时BuyAndSell是一个实现了Runnable接口的实现类,需要将该实现类的对象如下面的zhang,传给Thread类,让Thread类构造线程并调用start方法启动线程。以下为伪代码
class BuyAndSell implements Runnable {
//设定线程各参数
//线程构造方法等
@Override
public void run() {
//你要让线程执行的功能,在这里书写
}
}
}
public class Main {
public static void main(String args[ ]) {
//线程构造,使用接口向上转型
Runnable zhang = new BuyAndSell();
//运行线程
new Thread(zhang).start();
}
}
两种方法都可以构造线程,但是实现接口的方式比继承类的方式更灵活,也能减少程序之间的耦合度,是最优之选。因此,我也使用此方式对线程进行构造
//买票过程
class BuyAndSell implements Runnable {
private Seller mb;
private String name;//买票人名字
private int five;
private int ten ;
private int twenty;
public BuyAndSell(Seller mb, String name, int five, int ten, int twenty) {
this.mb = mb;
this.name = name;
this.five = five;
this.ten = ten;
this.twenty = twenty;
}
@Override
public void run() {
// 对公共资源售票员加锁,每当有一个线程办理业务时,不能被其他线程打扰
synchronized (mb) {
int pay = five*5+ten*10 +twenty*20;//支付的钱
System.out.println(name+"拿出"+pay+"元钱买票");
// try {
// Thread.sleep(100);
// } catch (InterruptedException e1) {
// // TODO Auto-generated catch block
// e1.printStackTrace();
// }
boolean flag = mb.change(name,five, ten, twenty);
while(!flag){
System.out.println("钱箱里的钱不足以找零,"+name+"请稍等..\n");
try {
mb.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(name+"重新拿出"+pay+"元钱买票");
flag = mb.change(name,five, ten, twenty);
}
// 此时说明办理成功,有零钱了,唤醒其他线程来争夺资源。即叫没有买到票的人来继续买票
mb.notifyAll();
}
}
}
在run中,使用静态代码块对公共资源售票员加锁,每当有一个受理人办理业务时,不能被其他线程打扰(即该线程拿到了Seller对象mb的锁)。如果钱不够找零(即flag=false)就让这个受理人稍等,即让这个线程wait()(此时,调用wait的同时,也把他原来拿到的锁给释放了)。
有朋友想要继承Thread实现的,我也放这里啦
class BuyAndSell1 extends Thread {
private Seller mb;
private String name;//买票人名字
private int five;
private int ten ;
private int twenty;
public BuyAndSell1(Seller mb, String name, int five, int ten, int twenty) {
this.mb = mb;
this.name = name;
this.five = five;
this.ten = ten;
this.twenty = twenty;
}
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (mb) {
int pay = five*5+ten*10 +twenty*20;//支付的钱
System.out.println(name+"拿出"+pay+"元钱买票");
// try {
// Thread.sleep(100);
// } catch (InterruptedException e1) {
// // TODO Auto-generated catch block
// e1.printStackTrace();
// }
boolean flag = mb.change(name,five, ten, twenty);
while(!flag){
System.out.println("钱箱里的钱不足以找零,"+name+"请稍等..\n");
try {
mb.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(name+"重新拿出"+pay+"元钱买票");
flag = mb.change(name,five, ten, twenty);
}
// System.out.println("售票员钱箱里有20元纸币"+mb.getTwenty()+"张,10元纸币"+mb.getTen()+"张,5元纸币"+mb.getFive()+"张\n");
mb.notifyAll();
}
}
}
04 业务模拟
主函数中,开始整个业务流程
public class Main {
public static void main(String args[ ]) {
Seller seller = new Seller(3,0,0);//初始只有3张5元纸币
System.out.println("售票员钱箱里有20元纸币"+seller.getTwenty()+"张,10元纸币"+seller.getTen()+"张,5元纸币"+seller.getFive()+"张\n");
//构造3个买票过程
//Runable 接口实现
//张某(拿着20元钞票
Runnable zhang = new BuyAndSell(seller,"张某", 0, 0, 1);
//李某(拿着10元钞票
Runnable li = new BuyAndSell(seller,"李某", 0, 1, 0);
//赵某(拿着5元钞票
Runnable zhao = new BuyAndSell(seller,"赵某", 1, 0, 0);
new Thread(zhang).start();
new Thread(li).start();
new Thread(zhao).start();
/*
*Thread继承实现
//张某(拿着20元钞票
Thread zhang = new BuyAndSell1(seller,"张某", 0, 0, 1);
//李某(拿着10元钞票
Thread li = new BuyAndSell1(seller,"李某", 0, 1, 0);
//赵某(拿着5元钞票
Thread zhao = new BuyAndSell1(seller,"赵某", 1, 0, 0);
zhang.start();
li.start();
zhao.start();
*/
}
}
运行结果会随机不同,大概是因为线程会随着cpu时间片的分配随机执行

运行结果1

运行结果2
以上代码,经过本人调试,没有看出问题来。如有问题,多半是编辑错误,欢迎指正!

3323

被折叠的 条评论
为什么被折叠?



