JUC系列之模拟抢票(N人同时抢票,票不足系统补仓,N-M人继续抢票)

下载 http://download.csdn.net/download/crazyzxljing0621/9969870

前言

10.1要去苏州玩。

9月初去携程买票,发现过几天才放票

现在可以预约抢票。

用户流程 : 选票 -> 选预约 -> 选抢票类型极速/快速/免费  -> 支付票价和抢票费用  -> 自动帮抢票

携程这个流程我猜测是(当然我没有做过和12306或什么票务接口对接的工作  )

  1.  记录用户选票类型和票信息,等放票的时候通过接口调用买票这个接口可能是多次提交或提交一次设定要购买的票信息数组?
  2.  得到票后将票分配给所有预付款用户,当然按照抢票类型优先分配,按照如果一个人一次买了3张票,优先分配邻座。
  3.  因为很少买火车票,百度说火车票是分批次放票,比如13点100张票,14点200张票这样。。
  4.  所以只要还有放票的可能携程还是继续给未得到票的人抢票,即便放票完毕。还有退票不是么

恰逢正在看juc,觉得这个可以做一个小demo来巩固一下

我的demo简单多了

  1. 设定N个用户
  2. N个用户全部预约就位
  3. 系统开始放票M张,总共会放Y张票
  4. 每放票M张,则用户开始同时去抢票
  5. 抢不到的等着继续放票
  6. 放票系统发现M张票强没了,会继续放票
  7. 用户也继续抢票
  8. 最后放票完毕,没有余票了。结束程序,记录抢票成功的用户

目录结构

《JUC系列之模拟抢票(N人同时抢票,票不足系统补仓,N-M人继续抢票)》

代码

 先看看最终结果 总共21张票,25个人同时并发去抢,每次放票5张,每隔几秒再次放票,直到票售罄

买票用户 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25] | 就位
sys : 开始放票
/**
**票仓[[票0, 票1, 票2, 票3, 票4]]
**增加了5张票
**/
用户[25]的抢票结果为 true[票0] 
用户[2]的抢票结果为 true[票1] 
用户[4]的抢票结果为 true[票2] 
用户[6]的抢票结果为 true[票3] 
用户[8]的抢票结果为 true[票4] 
/**
**票仓[[票5, 票6, 票7, 票8, 票9]]
**增加了5张票
**/
用户[10]的抢票结果为 true[票5] 
用户[12]的抢票结果为 true[票6] 
用户[14]的抢票结果为 true[票7] 
用户[16]的抢票结果为 true[票8] 
用户[18]的抢票结果为 true[票9] 
/**
**票仓[[票10, 票11, 票12, 票13, 票14]]
**增加了5张票
**/
用户[20]的抢票结果为 true[票10] 
用户[22]的抢票结果为 true[票11] 
用户[24]的抢票结果为 true[票12] 
用户[1]的抢票结果为 true[票13] 
用户[3]的抢票结果为 true[票14] 
/**
**票仓[[票15, 票16, 票17, 票18, 票19]]
**增加了5张票
**/
用户[5]的抢票结果为 true[票15] 
用户[9]的抢票结果为 true[票16] 
用户[7]的抢票结果为 true[票17] 
用户[13]的抢票结果为 true[票18] 
用户[15]的抢票结果为 true[票19] 
/**
**票仓[[票20]]
**增加了1张票
**/
用户[17]的抢票结果为 true[票20] 
放票完毕
买票用户 [0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 25] | 抢票成功
package juc.demo;
/**
 * 抢票的核心类
 * @author Allen 2017年9月7日
 *
 */

import java.util.Arrays;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import juc.demo.runnable.GrabThread;
import juc.demo.runnable.TicketSupplement;

/**
 * 抢票
 * 
 * @author Allen 2017年9月7日
 *
 */
public class GrabTicket {
 
	public int TICKET_INDEX = 0;// 票仓当前指针
	ReentrantLock reentrantlock = new ReentrantLock();// 独占锁
	Condition condition = reentrantlock.newCondition();// 在AQS中与独占锁配合使用,作为了队列的存在
	private CyclicBarrier cyclicBarrier;// 建立一个同步屏障

	public GrabTicket() {
		this.cyclicBarrier = new CyclicBarrier(Data.USER_NUM, new Runnable() {
			@Override
			public void run() {
				Temp.print("就位"); //当所有用户await后,打印已就位的用户列表
				System.out.println("sys : 开始放票");
				addTicket(); //向当前票仓填票
			}
		});
	}

	public void execute() {
		ExecutorService threadPool = Executors.newFixedThreadPool(Data.USER_NUM); //启动一个固定长度的线程池来维护
		for (int i = 0; i < Data.USER_NUM; i++) {
			threadPool.execute(new GrabThread(cyclicBarrier, this, i + 1)); //每个抢票的用户
		}
		threadPool.execute(new TicketSupplement(this));//启动补票线程
                threadPool.shutdown();
	}

	/**
	 * 放票
	 */
	public void addTicket() {
		TICKET_INDEX = 0; //每次放票把放票指针归零 
		Data.refresh(); //刷新新票到仓库
		System.out.println("/**");
		System.out.printf("**票仓[%s]\n", Arrays.toString(Data.TICKETS_NOW));
		System.out.printf("**增加了%s张票\n", Data.TICKETS_NOW.length);
		System.out.println("**/");
	}

	public void signalAll() { 
		try {
			reentrantlock.lock(); //
			condition.signalAll();//独占锁确保可以把reentrantlock中condition队列阻塞的线程全部解阻塞,类似 wait noityAll
		} finally {
			reentrantlock.unlock();
		}
	}
       //独占锁确保了同步性,并使用condition来使未得到票的线程,来等待放票后唤醒         
	public void Grab(int i) throws InterruptedException {
		String ticketName = null;
		reentrantlock.lock();
		try {
			if (TICKET_INDEX >= Data.TICKETS_NOW.length) { //当前指针超过了容器大小则进入阻塞等待下一次放票
				condition.await();  //Condition将独占锁中满足条件的放入阻塞队列进行等待,直到signal唤醒
				Grab(i);//递归来进入Grab抢票
			} else {
				ticketName = Data.TICKETS_NOW[TICKET_INDEX++];//通过指针获得当前票号
				Data.TICKETS_NOW[TICKET_INDEX - 1] = null;//将得到的票设为null标识已经被买走 
				System.out.printf("用户[%s]的抢票结果为 %s[%s] \n", i, ticketName != null, ticketName);
				Temp.add(i);//将抢票成功的用户添加到数组中
                                Data.USER_SUM++;//记录抢完票的用户,主要用于当用户数小于票数,放票机进行拦截
			}
		} finally {
			reentrantlock.unlock();//释放锁
		}

	}
}
//此类提供启动入口,用户初始化,票仓初始化,以及购票等主要函数 

package juc.demo.runnable;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

import juc.demo.GrabTicket;
import juc.demo.Temp;

/**
 * 抢票线程
 * 
 * @author Allen 2017年9月7日
 *
 */
public class GrabThread implements Runnable {
	CyclicBarrier cb;
	GrabTicket grabTicket;
	int i;

	public GrabThread(CyclicBarrier cb, GrabTicket grabTicket, int i) {
		this.cb = cb;
		this.grabTicket = grabTicket;
		this.i = i;
	}
	@Override
	public void run() {
		Temp.add(i);//添加用户到临时容器
		try {   
			cb.await(); //进入await等待,当等待人数满足CyclicBarrier构造传入的num时,condition会唤醒所有await
			grabTicket.Grab(i);//与其他线程同时购票
		} catch (InterruptedException | BrokenBarrierException e) {
			e.printStackTrace();
		}
	}
}
package juc.demo.runnable;

import java.util.Arrays;

import juc.demo.Data;
import juc.demo.GrabTicket;
import juc.demo.Temp;

/**
 * 放票线程
 * 
 * @author Allen 2017年9月7日
 *
 */
public class TicketSupplement implements Runnable {

	GrabTicket grabTicket;

	public TicketSupplement(GrabTicket grabTicket) {
		this.grabTicket = grabTicket;
	}

	@Override
	public void run() {
		for (;;) {
			try {
				Thread.sleep(1000);
				if (Arrays.asList(Data.TICKETS_NOW).stream().allMatch(this::isNull)) { 
        //确保没票了才放票,网上说火车票是每隔一个时间段就放票。这里增加了没票才放票的逻辑是因为我这里是个demo
					if (Data.TICKET_INDEX * Data.TICKET_MAX > Data.TICKET_COUNT) {//判断放票是否越界
						System.out.println("放票完毕");
						Temp.print("抢票成功");
						System.exit(-1);
					} else {
						grabTicket.addTicket();//放票
						grabTicket.signalAll();// 补票完毕把condition队列中的等待全部唤醒
					}
				} else if(Data.USER_SUM  == Data.USER_NUM){//如果得到票的人和总人数相匹配,则证明没票了。那么这个模拟放票进程就可以关闭了
					System.out.printf("都抢完票了 %s : [%s张]\n", "余票",Arrays.asList(Data.TICKETS_NOW).stream().filter(this::isNotNull).count());
					System.exit(-1);
				}
				} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	private boolean isNull(String str) { //方法引用的2个函数
		return str == null;
	}

	private boolean isNotNull(String str) {
		return !isNull(str);
	}

}

主要就是上面这2个线程与抢票的核心类来提供整个demo服务 1.模拟开始抢票

2.模拟初始化票仓 3.模拟同时去让用户抢票 4.票仓一瞬间空了 5.模拟下一个时间段继续放票 6.模拟用户继续抢票
如果票比人多则输出

买票用户 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] | 就位
sys : 开始放票
/**
**票仓[[票0, 票1, 票2, 票3, 票4]]
**增加了5张票
**/
用户[19]的抢票结果为 true[票0] 
用户[2]的抢票结果为 true[票1] 
用户[1]的抢票结果为 true[票2] 
用户[18]的抢票结果为 true[票3] 
用户[4]的抢票结果为 true[票4] 
/**
**票仓[[票5, 票6, 票7, 票8, 票9]]
**增加了5张票
**/
用户[20]的抢票结果为 true[票5] 
用户[6]的抢票结果为 true[票6] 
用户[8]的抢票结果为 true[票7] 
用户[10]的抢票结果为 true[票8] 
用户[12]的抢票结果为 true[票9] 
/**
**票仓[[票10, 票11, 票12, 票13, 票14]]
**增加了5张票
**/
用户[14]的抢票结果为 true[票10] 
用户[16]的抢票结果为 true[票11] 
用户[5]的抢票结果为 true[票12] 
用户[3]的抢票结果为 true[票13] 
用户[7]的抢票结果为 true[票14] 
/**
**票仓[[票15, 票16, 票17, 票18, 票19]]
**增加了5张票
**/
用户[9]的抢票结果为 true[票15] 
用户[11]的抢票结果为 true[票16] 
用户[13]的抢票结果为 true[票17] 
用户[15]的抢票结果为 true[票18] 
用户[17]的抢票结果为 true[票19] 
/**
**票仓[[票20]]
**增加了1张票
**/
都抢完票了 余票 : [1张]

其他类

package juc.demo;

import java.util.Arrays;

/**
 * 模拟用户组
 * 
 * @author Allen 2017年9月7日
 *
 */
public class Temp {
        //为了输出用户组做了一个temp来打印到console
	private static int[] temp = new int[Data.USER_NUM];
	private static int tempIndex = 0;

	public static void add(int val) {
		temp[tempIndex++] = val;
	}

	public static void print(String str) {
                //这里用快排做了个升序 当然因为new int[xx]肯定会有0位(如果票比人少),这里没做去0处理。太麻烦了demo而已
                //快速排序用的是我上上篇《六种常见代码》中的类
                //http://blog.csdn.net/crazyzxljing0621/article/details/77867788
		System.out.printf("买票用户 %s | %s\n", Arrays.toString(QuickSort.execute(temp)),str);
		temp = new int[Data.USER_NUM]; 
		tempIndex=0;
	}
}
package juc.demo;

/**
 * 配置
 * 
 * @author Allen 2017年9月7日
 *
 */
public class Data {
	/** class Constant **/
        //一些数据的初始化
	public final static int USER_NUM = 20;// 抢票用户总数
	public static int USER_SUM = 0;// 抢票完毕的用户
	public final static int TICKET_COUNT = 21;// 总票数
	public final static int TICKET_MAX = 5;// 票仓容量
	public static String[] TICKETS_NOW;// 当前可买票仓
	public static int TICKET_INDEX = 0;// 当前仓页索引

	private final static String[] TICKETS = new String[TICKET_COUNT];// 总票仓
	static {
		for (int i = 0; i < TICKET_COUNT; i++)
			TICKETS[i] = new StringBuffer("票").append(i).toString(); //把票都初始化到总票仓
	}

	/**
	 * 刷新可买票仓
	 */
	public static void refresh() { //这里和分页一样了
		int s = TICKET_INDEX * TICKET_MAX;  
		int e = s + TICKET_MAX > TICKET_COUNT ? TICKET_COUNT : s + TICKET_MAX;
		TICKETS_NOW = new String[e - s];
		for (int i = s, j = 0; i < e; i++, j++) {//每次只拿到当前“页”的票
			TICKETS_NOW[j] = TICKETS[i];
		}
		TICKET_INDEX++;
	} 
}

总结

写这个demo的意义是什么呢? 主要是借助携程带来的一点业务灵感。用CyclicBarrier,reentrantlock,condition来完成同时购买,同步购买的功能

    原文作者:JUC
    原文地址: https://blog.csdn.net/crazyzxljing0621/article/details/77891620
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞