并发数据结构与多核编程 -- 列车售票系统

一.设计思路

认真分析题目要求后,我在TicketingDS类作为代码主体,调用其他个模块:SeatSection

、RouteSection、CoachSection,分别代表车次、车厢、座位。以及Test测试模块。

TicketingDS.java

1.定义私有属性

private final int routeNum;// 车次总数
private final int stationNum;// 车站总数
private ArrayList<RouteSection> routeArray;//车次列表

ArrayList是Java集合框架中的一个重要的类,它继承于AbstractList,实现了List接口,是一个长度可变的集合,提供了增删改查的功能。集合中允许null的存在。ArrayList类还是实现了RandomAccess接口,可以对元素进行快速访问。实现了Serializable接口,说明ArrayList可以被序列化,还有Cloneable接口,可以被复制。和Vector不同的是,ArrayList不是线程安全的。

2.依次遍历每个车次

for (int routeId = 1; routeId <= routeNum; routeId++)
     this.routeArray.add(new RouteSection(routeId, coachNum, seatNum));
	}

3.买票

使用buyTicket方法,先判断车次和车站是否在范围内,再调用RouteSection模块里的initSeal方法,尝试购票,并返回(route - 1)。

4.查询

先判断车次和车站是否在范围内,再调用RouteSection模块里的initInquiry方法尝试购票,并返回(route - 1)。

5.退票

先获取车票的车次,判断车票和车次是否在范围内,再调用RouteSection模块里的initRefund方法尝试购票,并返回(route - 1)。

RouteSection.java

具体实现TicketingDS类里的三个方法。

1.先定义属性:

	private final int routeId;//车次序号
	private final int coachNum;//车厢数目
	private ArrayList<CoachSection> coachList;//车厢列表
	private AtomicLong ticketId;//车票的票号,每个车票有唯一的票号
	private Queue<Long> queue_SoldTicket;//构造队列

2.使用构造方法,当java类实例化时,输入参数值,将属性初始化。

3.使用queue_SoldTicket构造队列。

this.queue_SoldTicket = new ConcurrentLinkedQueue<Long>();

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用了“wait-free”算法来实现

4.遍历车厢。

5.使用initSeal方法尝试购票。建立车厢的数据结构使用了:

int randCoach = ThreadLocalRandom.current().nextInt(this.coachNum);

建立车厢的数据结构,其中ThreadLocalRandom是线程相关的,调ThreadLocalRandom.current()会返回当前线程的ThreadLocalRandom对象。ThreadLocalRandom优点在于高并发条件下,不同线程产生的随机数能不一致。同样的,调用nextInt方法也会返回一个伪随机整数值。

使用getAndIncrement方法,以原子方式将当前值加 1,返回旧值(即加1前的原始值)。保证了操作的原子性。

6.使用initInquiry方法,查询余票情况,也使用getAndIncrement方法来保证原子性。

7.退票使用initRefund方法。建立哈希表,根据对象的地址或者字符串或者数字算出对应的int类型的数值。contains判断是否包含这张票,找到后然后删除票,再重新返回。也使用getAndIncrement方法来保证原子性。

CoachSection.java

同RouteSection.java。

先定义变量,建立座位列表,存储座位的情况。

尝试购票,以及查询,退票。

SeatSection.java

同RouteSection.java。

先定义变量,建立座位列表,存储座位的情况。

尝试购票,以及查询,退票。

这里为了验证座位是否空闲,使用了如下方法:

private AtomicLong availableSeat;

根据一个 AtomicLong 型 64 位的 availableSeat判断是否空闲。availableSeat 的每一位表示座位对应的每一站, 0 表示未售出, 1 表示售出。购票查询退票时均采用从 route->coach->seat 的方式调用方法,在 seatNode 操作时,用原语 compareAndSet 构造非阻塞式的自旋锁来保证并发操作的原子性。

Test.java

test方法参考了网上提供的思路和讲解。

先设置测试数值:

	private final static int ROUTE_NUM = 5;// 列车车次
	private final static int COACH_NUM = 8;// 车箱数
	private final static int SEAT_NUM = 100;// 每个车厢的座位数
	private final static int STATION_NUM = 10;// 总站数

	private final static int TEST_NUM = 10000;// 每个线程里调用的方法数是10000次
	private final static int refund = 10;// 退票数目
	private final static int buy = 40;// 买票数目
	private final static int query = 100;// 查询票数目
	private final static int thread = 64;// 线程数目

再获取乘客信息。

在main方法里,先设置不同的线程总数,再对不同threadNums数目的线程进行处理,并发完成threadNums数目的所有线程。在Thread构造函数中传入Runnable实现对象,在Thread源码中将Runnable对象传递给init方法。

运行线程,使用getAndIncrement方法将原值+1,并且返回+1前的原值。获取id。

根据退票/购票/查询余票=1/3/6的比例进行测试。

二.正确性与性能

运行老师提供的verify测试单线程,通过,并显示Verification Finished。

在这里插入图片描述

使用trace.java生成如下结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-muYWNOXY-1612868329754)(C:\Users\YUANMU\AppData\Roaming\Typora\typora-user-images\image-20201220143816851.png)]

使用自己准备的test.java生成如下结果,来测试性能:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mb1pDJPo-1612868329757)(C:\Users\YUANMU\AppData\Roaming\Typora\typora-user-images\image-20201220143751094.png)]

综上,测试合格。

三.是否满足相关要求

1.买票

使用buyTicket方法,先判断车次和车站是否在范围内,再调用RouteSection模块里的initSeal方法,尝试购票,并返回(route - 1)。

	//买票
	public Ticket buyTicket(String passenger, int route, int departure,int arrival) {
		//先判断车次和车站是否在范围内
		if (route <= 0 || route > this.routeNum || arrival > this.stationNum
				|| departure >= arrival)
			return null;
		//尝试购票,并返回(route - 1)
		return this.routeArray.get(route - 1).initSeal(passenger, departure,
				arrival);
	}

从RouteSection到CoachSection,再到SeatSection,进一步的具体购票:(SeatSection)

	public int initSeal(final int departure, final int arrival) {
		long oldAvailSeat = 0;
		long newAvailSeat = 0;
		long temp = 0;

		int i = departure - 1;
		while(i < arrival - 1){
			long pow = 1;
			pow = pow << i;
			temp |= pow;
			i++;
		}

		do {
			oldAvailSeat = this.availableSeat.get();
			long result = temp & oldAvailSeat;
			if (result != 0) {
				return -1;
			}
			else {
				newAvailSeat = temp | oldAvailSeat;
			}
		} while (!this.availableSeat.compareAndSet(oldAvailSeat, newAvailSeat));
		return this.seatId;
	}

这里使用了compareAndSet,实现了区别于synchronouse同步锁的一种乐观锁,使用这些类在多核CPU的机器上会有比较好的性能。它的作用是将指定内存地址的内容与所给的某个值相比,如果相等,则将其内容替换为指令中提供的新值,如果不相等,则更新失败。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

2.查询

先判断车次和车站是否在范围内,再调用RouteSection模块里的initInquiry方法尝试购票,并返回(route - 1)。

	//查询
	public int inquiry(int route, int departure, int arrival) {

		//先判断车次和车站是否在范围内
		if (route <= 0 || route > this.routeNum || arrival > this.stationNum
				|| departure >= arrival)

			return -1;

		//尝试查询,并返回(route - 1)
		return this.routeArray.get(route - 1).initInquiry(departure, arrival);

	}

从RouteSection到CoachSection,再到SeatSection,进一步的具体购查询:(SeatSection)

	public int initInquiry(final int departure, final int arrival) {
		long oldAvailSeat = this.availableSeat.get();
		long temp = 0;
		long pow;
		int i = departure - 1; 
		while(i < arrival - 1){
			pow = 1;
			pow = pow << i;
			temp |= pow;
			i++;
		}

		long result = temp & oldAvailSeat;
		return (result == 0) ? 1 : 0;
	}

3.退票

先获取车票的车次,判断车票和车次是否在范围内,再调用RouteSection模块里的initRefund方法尝试购票,并返回(route - 1)。

//退票
	public boolean refundTicket(Ticket ticket) {

		//获取车票的车次
		final int routeId = ticket.route;
		//先判断车票和车次是否在范围内
		if (ticket == null || routeId <= 0 || routeId > this.routeNum)
			return false;
		//尝试退票,并返回(route - 1)
		return this.routeArray.get(routeId - 1).initRefund(ticket);

	}

从RouteSection到CoachSection,再到SeatSection,进一步的具体退票:(SeatSection)

public boolean initRefund(final int departure, final int arrival) {

		long oldAvailSeat = 0;
		long newAvailSeat = 0;
		long temp = 0;

		int i = departure - 1;
		while(i < arrival - 1){
			long pow = 1;
			pow = pow << i;
			temp |= pow;
			i++;
		}

		temp = ~temp;

		do {
			oldAvailSeat = this.availableSeat.get();
			newAvailSeat = temp & oldAvailSeat;
		} while (!this.availableSeat.compareAndSet(oldAvailSeat, newAvailSeat));

		return true;
	}

}

这里再次使用了compareAndSet,来实现退票的方法。

对于可线性化。可以实现并行操作时时序重叠,实际按可线性化点的顺序来执行。我使用的是cas,它的作用是将指定内存地址的内容与所给的某个值相比,如果相等,则将其内容替换为指令中提供的新值,如果不相等,则更新失败。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。买票、查询、退票都满足可线性化。

对于无死锁。申请锁的线程最终都会获得锁,所以满足无死锁。

对于无饥饿。我使用的是cas,每个申请锁的线程都会获得锁,并且所有的线程都能在有限步当中完成,因此必然不会有线程永久地呆在临界区内出不去,所以它一定是无饥饿的。所以无饥饿。

对于无锁。我的某一个方法调用线程,最终都会返回,保证至少有一个线程能够在有限步当中完成它的操作,所有的线程在不停地竞争直到有一个胜出为止。所以是无锁的。

对于无等待。相比于无锁更进一步,它首先要求是无锁的,保证所有线程能进并且至少有一个线程能出来,它要求所有进入临界区的线程都能够在有限步当中完成其操作。这个要求很高,因为任何线程都能够无障碍进入临界区,并且任何线程都能够在有限步当中完成操作后离开临界区。调用一个方法的线程最终都会返回,所以无等待。

综上,均满足要求。

附:项目要求

用于列车售票的可线性化并发数据结构
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

码字不易,都看到这里了不如点个赞哦~
在这里插入图片描述
我还写了很多文章,欢迎关注我哦~ 一起加油~

猜你喜欢

转载自blog.csdn.net/weixin_42784535/article/details/113774423