Java线程同步-模拟买票

文章首发于 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

 

 

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

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

落雨湿红尘

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

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

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

打赏作者

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

抵扣说明:

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

余额充值