├── .vscode
└── settings.json
├── src
├── test
│ └── java
│ │ ├── verify
│ │ ├── verify.jar
│ │ ├── Readme
│ │ └── Trace.java.copy
│ │ ├── linerChecker
│ │ ├── checker.jar
│ │ ├── check.sh
│ │ ├── README
│ │ └── Trace.java.copy
│ │ └── ticketingsystem
│ │ ├── .travis.yml
│ │ ├── MultiTraceVerifyTest.java
│ │ ├── TraceVerifyTest.java
│ │ ├── RandomTest.java
│ │ ├── MultiThreadTest.java
│ │ └── UnitTest.java
└── main
│ └── java
│ └── ticketingsystem
│ ├── TicketingSystem.java
│ ├── Backoffer.java
│ ├── jmh
│ └── benchmark
│ │ └── PerformanceBenchmarkRunner.java
│ ├── TicketingDS.java
│ ├── RemainSeatsTable.java
│ ├── Train.java
│ ├── PerformanceBenchmark.java
│ └── Trace.java
├── trace.sh
├── .travis.yml
├── .gitignore
├── .factorypath
├── .github
└── workflows
│ ├── maven.yml
│ └── codeql-analysis.yml
├── pom.xml
├── README.md
├── result.json
└── LICENSE
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "java.configuration.updateBuildConfiguration": "automatic"
3 | }
--------------------------------------------------------------------------------
/src/test/java/verify/verify.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/specialpointcentral/TrainTicketingSystem/HEAD/src/test/java/verify/verify.jar
--------------------------------------------------------------------------------
/trace.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd src/main/java
3 | javac -encoding UTF-8 -cp . ticketingsystem/Trace.java
4 | java -cp . ticketingsystem/Trace
--------------------------------------------------------------------------------
/src/test/java/linerChecker/checker.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/specialpointcentral/TrainTicketingSystem/HEAD/src/test/java/linerChecker/checker.jar
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 |
3 | jdk:
4 | - openjdk11
5 |
6 | install:
7 | - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
8 | script:
9 | - mvn test -B
--------------------------------------------------------------------------------
/src/test/java/ticketingsystem/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 |
3 | install:
4 | - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V
5 |
6 | script:
7 | - mvn test -B
--------------------------------------------------------------------------------
/src/test/java/verify/Readme:
--------------------------------------------------------------------------------
1 | 1. copy Trace.java to your dir
2 | 2. generate trace
3 | 3. enter verify dir
4 | 4. execute "java -jar verify.jar trace"
5 |
6 | If the first error is found, the result is as follows:
7 |
8 | Error: RemainTicket 57982522 57982654 0 3 3 2 3
9 | Real RemainTicket is 3 , Expect RemainTicket is 4, 3 2 3
10 | Verification Finished
11 |
12 | Else only "Verification Finished" is printed.
13 |
14 |
--------------------------------------------------------------------------------
/src/main/java/ticketingsystem/TicketingSystem.java:
--------------------------------------------------------------------------------
1 | package ticketingsystem;
2 |
3 | class Ticket {
4 | long tid;
5 | String passenger;
6 | int route;
7 | int coach;
8 | int seat;
9 | int departure;
10 | int arrival;
11 | }
12 |
13 | public interface TicketingSystem {
14 | Ticket buyTicket(String passenger, int route, int departure, int arrival);
15 |
16 | int inquiry(int route, int departure, int arrival);
17 |
18 | boolean refundTicket(Ticket ticket);
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled class file
2 | *.class
3 |
4 | # Log file
5 | *.log
6 |
7 | # BlueJ files
8 | *.ctxt
9 |
10 | # Mobile Tools for Java (J2ME)
11 | .mtj.tmp/
12 |
13 | # Package Files #
14 | *.jar
15 | !verify.jar
16 | !checker.jar
17 | *.war
18 | *.nar
19 | *.ear
20 | *.zip
21 | *.tar.gz
22 | *.rar
23 |
24 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
25 | hs_err_pid*
26 |
27 | # maven ignore
28 | target/
29 |
30 | # eclipse ignore
31 | .settings/
32 | .project
33 | .classpath
34 |
35 |
36 |
37 | # idea ignore
38 | .idea/
39 | *.ipr
40 | *.iml
41 | *.iws
42 |
43 | **/trace
--------------------------------------------------------------------------------
/.factorypath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/test/java/linerChecker/check.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ## compile Trace.java and put .class into bin
4 | javac ../../../main/java/ticketingsystem/Trace.java -d ./bin
5 |
6 | result=1
7 |
8 | ## begin test
9 | for i in $(seq 1 50); do ## you can change the number of test, default is 50
10 | java -cp bin ticketingsystem/Trace > trace
11 | java -jar checker.jar --no-path-info --coach 10 --seat 100 --station 10 < trace
12 | if [ $? != 0 ]; then
13 | echo "Test failed!!! see trace file to debug"
14 | result=0
15 | break
16 | fi
17 | done
18 |
19 | if [ $result == 1 ]; then
20 | echo "Test passed!!!"
21 | fi
22 |
--------------------------------------------------------------------------------
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Java project with Maven
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
3 |
4 | name: Java CI with Maven
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 | pull_request:
10 | branches: [ main ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Set up JDK 11
20 | uses: actions/setup-java@v1
21 | with:
22 | java-version: 11
23 | - name: Build with Maven
24 | run: mvn -B package --file pom.xml
25 |
--------------------------------------------------------------------------------
/src/main/java/ticketingsystem/Backoffer.java:
--------------------------------------------------------------------------------
1 | package ticketingsystem;
2 |
3 | import java.util.concurrent.ThreadLocalRandom;
4 |
5 | public class Backoffer {
6 | ThreadLocal retryTime = ThreadLocal.withInitial(()->1);
7 | private ThreadLocalRandom rand = ThreadLocalRandom.current();
8 |
9 | public void setBackoffTime(int time) {
10 | if(time > 1)
11 | retryTime.set(time);
12 | else
13 | retryTime.set(1);
14 | }
15 |
16 | public void backoff() {
17 | int bound = retryTime.get();
18 | int j = rand.nextInt(bound);
19 | for(int i = 0; i < j; ++i);
20 | if(bound < 0xfff)
21 | retryTime.set(bound << 1);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/ticketingsystem/jmh/benchmark/PerformanceBenchmarkRunner.java:
--------------------------------------------------------------------------------
1 | package ticketingsystem.jmh.benchmark;
2 |
3 | import org.openjdk.jmh.results.format.ResultFormatType;
4 | import org.openjdk.jmh.runner.Runner;
5 | import org.openjdk.jmh.runner.RunnerException;
6 | import org.openjdk.jmh.runner.options.Options;
7 | import org.openjdk.jmh.runner.options.OptionsBuilder;
8 |
9 | import ticketingsystem.*;
10 |
11 | public class PerformanceBenchmarkRunner {
12 | public static void main(String[] args) throws RunnerException {
13 | Options opt = new OptionsBuilder().include(PerformanceBenchmark.class.getSimpleName())
14 | .result("result.json")
15 | .resultFormat(ResultFormatType.JSON)
16 | .build();
17 | new Runner(opt).run();
18 | }
19 | }
--------------------------------------------------------------------------------
/src/test/java/linerChecker/README:
--------------------------------------------------------------------------------
1 | 1. 说明
2 | 本测试包含有一个测试工具checker.jar以及一个方便测试的脚本check.sh
3 | 测试工具的使用格式如下:
4 | java -jar checker.jar --coach xx --seat xx --station xx < trace
5 | 其他使用说明见参数--help
6 |
7 | 2. 使用步骤
8 | step 1: 将本包中的文件加压到ticketingsystem的同级目录
9 | step 2: 修改ticketingsystem/Trace.java中的参数,routenum请务必设置成1
10 | step 3: 得到一个trace
11 | step 4: 运行命令checker.jar
12 |
13 | 3. 结果说明
14 | 如果trace可线性化,会打印出线性化的执行路径。否则输出Not Linearizable.
15 |
16 | 4. 注意事项
17 | check.sh是一个便于测试的脚本,可以直接运行。使用前请修改脚本中的参数。
18 | Trace.java中route数请设置为1,火车间是独立的,所以checker我是按照一辆车检验的。
19 | 当前版本支持的trace记录数可以达到500条左右,运行时间随着交叠区间个数增加呈指数级上升。所以建议不要生成过大的trace文件。
20 |
21 | 推荐的设置值:
22 | final static int threadnum = 5;
23 | final static int routenum = 1; // route is designed from 1 to 3
24 | final static int coachnum = 3; // coach is arranged from 1 to 5
25 | final static int seatnum = 5; // seat is allocated from 1 to 20
26 | final static int stationnum = 5; // station is designed from 1 to 5
27 | final static int testnum = 20;
28 |
29 | 可以常识的设置值: (一般500条一下运行时间可以接受)
30 | final static int threadnum = 5;
31 | final static int routenum = 1; // route is designed from 1 to 3
32 | final static int coachnum = 10; // coach is arranged from 1 to 5
33 | final static int seatnum = 100; // seat is allocated from 1 to 20
34 | final static int stationnum = 10; // station is designed from 1 to 5
35 | final static int testnum = 100;
36 |
37 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '17 13 * * 2'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 |
28 | strategy:
29 | fail-fast: false
30 | matrix:
31 | language: [ 'java' ]
32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
33 | # Learn more:
34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
35 |
36 | steps:
37 | - name: Checkout repository
38 | uses: actions/checkout@v2
39 |
40 | - name: Set up JDK 11
41 | uses: actions/setup-java@v1
42 | with:
43 | java-version: 11
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/src/main/java/ticketingsystem/TicketingDS.java:
--------------------------------------------------------------------------------
1 | package ticketingsystem;
2 |
3 | import java.util.concurrent.atomic.AtomicLong;
4 |
5 | public class TicketingDS implements TicketingSystem {
6 | private int threadNum;
7 | private Train[] trains;
8 | private int coachNum;
9 | private int seatNum;
10 | private int routeNum;
11 |
12 | private AtomicLong buyTicketQueryID;
13 | private long hashMask;
14 |
15 | private ThreadLocal ticketBeginID = ThreadLocal.withInitial(()->0L);
16 | private ThreadLocal ticketEndID = ThreadLocal.withInitial(()->-1L);
17 |
18 | public TicketingDS(int routenum, int coachnum, int seatnum, int stationnum, int threadnum) {
19 | this.threadNum = threadnum;
20 | this.coachNum = coachnum;
21 | this.seatNum = seatnum;
22 | this.routeNum = routenum;
23 | trains = new Train[routenum];
24 | buyTicketQueryID = new AtomicLong(0);
25 | for (int i = 0; i < routenum; i++) {
26 | trains[i] = new Train(coachnum, seatnum, stationnum);
27 | }
28 |
29 | int coachBitNum = 32 - Integer.numberOfLeadingZeros(Math.max(this.coachNum - 1, 1));
30 | int threadNumBitNum = 32 - Integer.numberOfLeadingZeros(Math.max(this.threadNum - 1, 1));
31 | // hash mask for query
32 | if (this.threadNum > this.coachNum) {
33 | // all coach need used as hash
34 | this.hashMask = (0x1 << coachBitNum) - 1;
35 | } else {
36 | // will send to some coach
37 | this.hashMask = ((0x1 << threadNumBitNum) - 1) << (coachBitNum - threadNumBitNum);
38 | }
39 | }
40 |
41 | @Override
42 | public Ticket buyTicket(String passenger, int route, int departure, int arrival) {
43 | Ticket ticket = new Ticket();
44 | long threadID = Thread.currentThread().getId();
45 | // deliver to every train and coach
46 | int queryCoachID = (int)(threadID & hashMask);
47 | Train currTrian = trains[route - 1];
48 | int beginSeats = currTrian.getFindRefund(departure - 1, arrival - 1);
49 | if(beginSeats == -1) beginSeats = queryCoachID * seatNum;
50 | int seat = currTrian.getAndLockSeat(departure - 1, arrival - 1, beginSeats);
51 | if (seat < 0)
52 | return null;
53 | // find a tid
54 | long queryID = ticketBeginID.get();
55 | if(queryID > ticketEndID.get()) {
56 | queryID = buyTicketQueryID.getAndAdd(512);
57 | ticketEndID.set(queryID + 511);
58 | }
59 | ticketBeginID.set(queryID + 1);
60 | // build a ticket
61 | ticket.tid = queryID;
62 | ticket.passenger = passenger;
63 | ticket.route = route;
64 | ticket.departure = departure;
65 | ticket.arrival = arrival;
66 | ticket.coach = (seat / seatNum) + 1;
67 | ticket.seat = (seat % seatNum) + 1;
68 |
69 | currTrian.addSoldTicket(ticket);
70 | return ticket;
71 | }
72 |
73 | @Override
74 | public int inquiry(int route, int departure, int arrival) {
75 | Train currTrian = trains[route - 1];
76 | return currTrian.getRemainSeats(departure - 1, arrival - 1);
77 | }
78 |
79 | @Override
80 | public boolean refundTicket(Ticket ticket) {
81 | Train currTrian = trains[ticket.route - 1];
82 | if(!currTrian.containAndRemove(ticket)) {
83 | return false;
84 | }
85 | int seat = (ticket.coach - 1) * seatNum + (ticket.seat - 1);
86 | currTrian.insertRefundList(seat, ticket.departure - 1, ticket.arrival - 1);
87 | return currTrian.unlockSeat(seat, ticket.departure - 1, ticket.arrival - 1);
88 | }
89 |
90 | public void clear() {
91 | ticketBeginID.remove();
92 | ticketEndID.remove();
93 | for (int i = 0; i < routeNum; i++) {
94 | trains[i].clear();
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | ucas.huqi
7 | trainTicketingSystem
8 | 1.0-SNAPSHOT
9 | jar
10 |
11 | trainTicketingSystem
12 | https://github.com/specialpointcentral/TrainTicketingSystem
13 |
14 |
15 | UTF-8
16 | 11
17 | ${java.version}
18 | ${java.version}
19 |
20 | 5.6.2
21 |
22 |
23 | 3.2.2
24 | 3.1.0
25 | 3.1.0
26 | 3.8.1
27 | 3.0.0-M5
28 | 3.2.0
29 | 3.0.0-M1
30 |
31 |
32 |
33 |
34 |
35 | org.openjdk.jmh
36 | jmh-core
37 | 1.23
38 |
39 |
40 | org.openjdk.jmh
41 | jmh-generator-annprocess
42 | 1.23
43 |
44 |
45 |
46 |
47 | org.junit.jupiter
48 | junit-jupiter-api
49 | ${junit}
50 | test
51 |
52 |
53 | org.junit.jupiter
54 | junit-jupiter-engine
55 | ${junit}
56 | test
57 |
58 |
59 | org.junit.jupiter
60 | junit-jupiter-params
61 | ${junit}
62 | test
63 |
64 |
65 |
66 |
67 |
68 |
69 | maven-clean-plugin
70 | 3.1.0
71 |
72 |
73 | maven-resources-plugin
74 | 3.1.0
75 |
76 |
77 | maven-compiler-plugin
78 | 3.8.1
79 |
80 |
81 | maven-surefire-plugin
82 | 3.0.0-M4
83 |
84 |
85 | maven-jar-plugin
86 | 3.2.0
87 |
88 |
89 | maven-install-plugin
90 | 3.0.0-M1
91 |
92 |
93 | org.apache.maven.plugins
94 | maven-shade-plugin
95 | ${maven.shade}
96 |
97 |
98 | package
99 |
100 | shade
101 |
102 |
103 |
104 |
105 | ticketingsystem.App
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/src/test/java/ticketingsystem/MultiTraceVerifyTest.java:
--------------------------------------------------------------------------------
1 | package ticketingsystem;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 |
5 | import java.io.*;
6 |
7 | import org.junit.jupiter.api.BeforeAll;
8 | import org.junit.jupiter.api.BeforeEach;
9 | import org.junit.jupiter.api.DisplayName;
10 | import org.junit.jupiter.api.RepeatedTest;
11 | import org.junit.jupiter.api.RepetitionInfo;
12 | import org.junit.jupiter.api.TestInfo;
13 |
14 | @DisplayName("MultiTraceVerifyTest")
15 | public class MultiTraceVerifyTest {
16 | private static File workDir = new File("src/main/java/ticketingsystem/");
17 | private static File verifyDir = new File("src/test/java/linerChecker/");
18 | private static boolean isWindows;
19 |
20 | protected int currentRepetition, totalRepetitions;
21 |
22 | @BeforeAll
23 | static void prepareFile() throws IOException, InterruptedException {
24 | String oldfile = verifyDir.getAbsolutePath() + "/Trace.java.copy";
25 | String newfile = workDir.getAbsolutePath() + "/Trace.java";
26 | copyFile(oldfile, newfile);
27 | isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows");
28 | // compiler
29 | ProcessBuilder builder = new ProcessBuilder();
30 | if (isWindows) {
31 | builder.command("cmd.exe", "/c", "javac -encoding UTF-8 -cp . ticketingsystem/Trace.java");
32 | } else {
33 | builder.command("sh", "-c", "javac -encoding UTF-8 -cp . ticketingsystem/Trace.java");
34 | }
35 | builder.directory(workDir.getParentFile());
36 | Process process = builder.start();
37 | int exitCode = process.waitFor();
38 | assertEquals(0, exitCode);
39 | }
40 |
41 | @BeforeEach
42 | void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) throws IOException, InterruptedException {
43 | currentRepetition = repetitionInfo.getCurrentRepetition();
44 | totalRepetitions = repetitionInfo.getTotalRepetitions();
45 | String methodName = testInfo.getTestMethod().get().getName();
46 | System.out.println(
47 | String.format("Execute repetition %d of %d for %s", currentRepetition, totalRepetitions, methodName));
48 | System.out.flush();
49 |
50 | generateTraceFile();
51 | System.out.println(
52 | String.format("[%d/%d] Trace has been generated", currentRepetition, totalRepetitions));
53 | System.out.flush();
54 | }
55 |
56 | void generateTraceFile() throws IOException, InterruptedException {
57 | File f = new File(verifyDir.getPath() + "/trace");
58 | if (f.exists()) {
59 | f.delete();
60 | }
61 | ProcessBuilder builder = new ProcessBuilder();
62 | if (isWindows) {
63 | builder.command("cmd.exe", "/c", "java -cp . ticketingsystem/Trace > " + f.getAbsolutePath());
64 | } else {
65 | builder.command("sh", "-c", "java -cp . ticketingsystem/Trace > " + f.getAbsolutePath());
66 | }
67 | builder.directory(workDir.getParentFile());
68 | Process process = builder.start();
69 | int exitCode = process.waitFor();
70 | assertEquals(0, exitCode);
71 | }
72 |
73 | @RepeatedTest(value = 10, name = "{displayName} {currentRepetition}/{totalRepetitions}")
74 | void verifyMultiTrace() throws IOException, InterruptedException {
75 | ProcessBuilder builder = new ProcessBuilder();
76 | if (isWindows) {
77 | builder.command("cmd.exe", "/c", "java -jar checker.jar --coach 10 --seat 10 --station 7 --no-path-info < trace");
78 | } else {
79 | builder.command("sh", "-c", "java -jar checker.jar --coach 10 --seat 10 --station 7 --no-path-info < trace");
80 | }
81 | builder.directory(verifyDir);
82 | Process process = builder.start();
83 |
84 | int exitCode = process.waitFor();
85 | assertEquals(0, exitCode);
86 | }
87 |
88 | private static void copyFile(String oldPath, String newPath) {
89 | try {
90 | int bytesum = 0;
91 | int byteread = 0;
92 | File oldfile = new File(oldPath);
93 | if (oldfile.exists()) {
94 | InputStream inStream = new FileInputStream(oldPath);
95 | FileOutputStream fs = new FileOutputStream(newPath);
96 | byte[] buffer = new byte[1024];
97 | while ((byteread = inStream.read(buffer)) != -1) {
98 | bytesum += byteread;
99 | fs.write(buffer, 0, byteread);
100 | }
101 | inStream.close();
102 | }
103 | } catch (Exception e) {
104 | System.out.println("copy error!");
105 | e.printStackTrace();
106 | }
107 | }
108 | }
--------------------------------------------------------------------------------
/src/test/java/ticketingsystem/TraceVerifyTest.java:
--------------------------------------------------------------------------------
1 | package ticketingsystem;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 |
5 | import java.io.*;
6 |
7 | import org.junit.jupiter.api.BeforeAll;
8 | import org.junit.jupiter.api.BeforeEach;
9 | import org.junit.jupiter.api.DisplayName;
10 | import org.junit.jupiter.api.RepeatedTest;
11 | import org.junit.jupiter.api.RepetitionInfo;
12 | import org.junit.jupiter.api.Test;
13 | import org.junit.jupiter.api.TestInfo;
14 |
15 | @DisplayName("TraceVerifyTest")
16 | public class TraceVerifyTest {
17 | private static File workDir = new File("src/main/java/ticketingsystem/");
18 | private static File verifyDir = new File("src/test/java/verify/");
19 | private static boolean isWindows;
20 |
21 | protected int currentRepetition, totalRepetitions;
22 |
23 | @BeforeAll
24 | static void prepareFile() throws IOException, InterruptedException {
25 | String oldfile = verifyDir.getAbsolutePath() + "/Trace.java.copy";
26 | String newfile = workDir.getAbsolutePath() + "/Trace.java";
27 | copyFile(oldfile, newfile);
28 | isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows");
29 | // compiler
30 | ProcessBuilder builder = new ProcessBuilder();
31 | if (isWindows) {
32 | builder.command("cmd.exe", "/c", "javac -encoding UTF-8 -cp . ticketingsystem/Trace.java");
33 | } else {
34 | builder.command("sh", "-c", "javac -encoding UTF-8 -cp . ticketingsystem/Trace.java");
35 | }
36 | builder.directory(workDir.getParentFile());
37 | Process process = builder.start();
38 | int exitCode = process.waitFor();
39 | assertEquals(0, exitCode);
40 | }
41 |
42 | @BeforeEach
43 | void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) throws IOException, InterruptedException {
44 | currentRepetition = repetitionInfo.getCurrentRepetition();
45 | totalRepetitions = repetitionInfo.getTotalRepetitions();
46 | String methodName = testInfo.getTestMethod().get().getName();
47 | System.out.println(
48 | String.format("Execute repetition %d of %d for %s", currentRepetition, totalRepetitions, methodName));
49 | System.out.flush();
50 |
51 | generateTraceFile();
52 | System.out.println(
53 | String.format("[%d/%d] Trace has been generated", currentRepetition, totalRepetitions));
54 | System.out.flush();
55 | }
56 |
57 | void generateTraceFile() throws IOException, InterruptedException {
58 | File f = new File(verifyDir.getPath() + "/trace");
59 | if (f.exists()) {
60 | f.delete();
61 | }
62 | ProcessBuilder builder = new ProcessBuilder();
63 | if (isWindows) {
64 | builder.command("cmd.exe", "/c", "java -cp . ticketingsystem/Trace > " + f.getAbsolutePath());
65 | } else {
66 | builder.command("sh", "-c", "java -cp . ticketingsystem/Trace > " + f.getAbsolutePath());
67 | }
68 | builder.directory(workDir.getParentFile());
69 | Process process = builder.start();
70 | int exitCode = process.waitFor();
71 | assertEquals(0, exitCode);
72 | }
73 |
74 | @RepeatedTest(value = 10, name = "{displayName} {currentRepetition}/{totalRepetitions}")
75 | void verifyTrace() throws IOException, InterruptedException {
76 | ProcessBuilder builder = new ProcessBuilder();
77 | if (isWindows) {
78 | builder.command("cmd.exe", "/c", "java -jar verify.jar trace");
79 | } else {
80 | builder.command("sh", "-c", "java -jar verify.jar trace");
81 | }
82 | builder.directory(verifyDir);
83 | Process process = builder.start();
84 | InputStream in = process.getInputStream();
85 | BufferedReader read = new BufferedReader(new InputStreamReader(in));
86 | assertEquals("Verification Finished", read.readLine());
87 | int exitCode = process.waitFor();
88 | assertEquals(0, exitCode);
89 | }
90 |
91 | private static void copyFile(String oldPath, String newPath) {
92 | try {
93 | int bytesum = 0;
94 | int byteread = 0;
95 | File oldfile = new File(oldPath);
96 | if (oldfile.exists()) {
97 | InputStream inStream = new FileInputStream(oldPath);
98 | FileOutputStream fs = new FileOutputStream(newPath);
99 | byte[] buffer = new byte[1024];
100 | while ((byteread = inStream.read(buffer)) != -1) {
101 | bytesum += byteread;
102 | fs.write(buffer, 0, byteread);
103 | }
104 | inStream.close();
105 | }
106 | } catch (Exception e) {
107 | System.out.println("copy error!");
108 | e.printStackTrace();
109 | }
110 | }
111 | }
--------------------------------------------------------------------------------
/src/main/java/ticketingsystem/RemainSeatsTable.java:
--------------------------------------------------------------------------------
1 | package ticketingsystem;
2 |
3 | import java.util.Arrays;
4 | import java.util.concurrent.atomic.AtomicStampedReference;
5 |
6 | public class RemainSeatsTable {
7 | private int stationNum;
8 | private int seatNum;
9 | private AtomicStampedReference remainSeats;
10 |
11 | private ThreadLocal localTable;
12 | private ThreadLocal localTableSwither;
13 |
14 | Backoffer backoff = new Backoffer();
15 |
16 | public RemainSeatsTable(final int seatnum, final int stationnum) {
17 | this.stationNum = stationnum;
18 | this.seatNum = seatnum;
19 |
20 | this.localTable = ThreadLocal.withInitial(() -> {
21 | int[][] remainSeat = new int[stationNum][];
22 | for (int i = 0; i < stationNum; ++i) {
23 | // remainSeat[from][to]
24 | // NOTE: 'from' is real, but 'to' is (to+from)
25 | // if 'to' is 0, result is always 0
26 | // Example: remainSeat[1][2] = (1)->(3)
27 | remainSeat[i] = new int[stationNum - i];
28 | remainSeat[i][0] = 0;
29 | for (int j = 1; j < stationNum - i; ++j) {
30 | remainSeat[i][j] = seatNum;
31 | }
32 | }
33 | return remainSeat;
34 | });
35 |
36 | this.localTableSwither = ThreadLocal.withInitial(() -> {
37 | int[][] remainSeat = new int[stationNum][];
38 | for (int i = 0; i < stationNum; ++i) {
39 | // remainSeat[from][to]
40 | // NOTE: 'from' is real, but 'to' is (to+from)
41 | // if 'to' is 0, result is always 0
42 | // Example: remainSeat[1][2] = (1)->(3)
43 | remainSeat[i] = new int[stationNum - i];
44 | remainSeat[i][0] = 0;
45 | for (int j = 1; j < stationNum - i; ++j) {
46 | remainSeat[i][j] = seatNum;
47 | }
48 | }
49 | return remainSeat;
50 | });
51 |
52 | this.remainSeats = new AtomicStampedReference<>(localTable.get(), 0);
53 | }
54 |
55 | public final int getRemainSeats(final int departure, final int arrival) {
56 | int currTimestap = remainSeats.getStamp();
57 | int[][] currTable = remainSeats.getReference();
58 | int remain = currTable[departure][arrival - departure];
59 | int twiceTimestap = remainSeats.getStamp();
60 | while (currTimestap != twiceTimestap) {
61 | currTimestap = remainSeats.getStamp();
62 | currTable = remainSeats.getReference();
63 | remain = currTable[departure][arrival - departure];
64 | twiceTimestap = remainSeats.getStamp();
65 | }
66 | return remain;
67 | }
68 |
69 | public final void setRemainSeats(final int departure, final int arrival, final long origin, final int num) {
70 | while (true) {
71 | int[][] oldTable = remainSeats.getReference();
72 | int[][] newTable = localTable.get();
73 | if (Arrays.equals(oldTable, newTable)) {
74 | // using my local table as global table,
75 | // so we need create a new one to modify
76 | newTable = localTableSwither.get();
77 | }
78 | int stamp = remainSeats.getStamp();
79 | // decrease/increase the table
80 |
81 | // if origin is 0, mark we need do all task
82 | boolean currIsNotClean = (origin != 0);
83 | // 0 1 2 3 4 5
84 | // Example: 2->4
85 | // 0-3 -> 3-5
86 | // departure station
87 | for (int i = 0; i < arrival; ++i) {
88 | // copy
89 | for (int j = i + 1; j < stationNum; ++j) {
90 | newTable[i][j - i] = oldTable[i][j - i];
91 | }
92 | // arrival station
93 | for (int j = Math.max(departure, i) + 1; j < stationNum; ++j) {
94 | if (currIsNotClean && isOverlapping(i, j, origin)) {
95 | newTable[i][j - i] = oldTable[i][j - i];
96 | } else {
97 | newTable[i][j - i] = oldTable[i][j - i] + num;
98 | }
99 | }
100 | }
101 | // copy
102 | for (int i = arrival; i < stationNum; ++i) {
103 | for (int j = i + 1; j < stationNum; ++j) {
104 | newTable[i][j - i] = oldTable[i][j - i];
105 | }
106 | }
107 |
108 | if (remainSeats.compareAndSet(oldTable, newTable, stamp, stamp + 1)) {
109 | return;
110 | }
111 | backoff.backoff();
112 | }
113 | }
114 |
115 | public void decrementRemainSeats(final int departure, final int arrival, final long origin) {
116 | setRemainSeats(departure, arrival, origin, -1);
117 | }
118 |
119 | public void incrementRemainSeats(final int departure, final int arrival, final long origin) {
120 | setRemainSeats(departure, arrival, origin, 1);
121 | }
122 |
123 | private final boolean isOverlapping(final int departure, final int arrival, final long origin) {
124 | // departure and arrival not overlapping the origin data
125 | int mask = ((0x01 << (arrival - departure)) - 1) << departure;
126 | return ((mask & origin) > 0);
127 | }
128 |
129 | public void clear() {
130 | localTable.remove();
131 | localTableSwither.remove();
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TrainTicketingSystem
2 |
3 | 多核与并发数据结构-用于列车售票的可线性化并发数据结构
4 |
5 | 
6 | 
7 | 
8 | 
9 | 
10 |
11 | ## 数据结构说明
12 |
13 | 给定`Ticket`类:
14 |
15 | ```java
16 | class Ticket{
17 | long tid;
18 | String passenger;
19 | int route;
20 | int coach;
21 | int seat;
22 | int departure;
23 | int arrival;
24 | }
25 | ```
26 |
27 | 其中,`tid`是车票编号,`passenger`是乘客名字,`route`是列车车次,`coach`是车厢号,`seat`是座位号,`departure`是出发站编号,`arrival`是到达站编号。
28 |
29 | 给定`TicketingSystem`接口:
30 |
31 | ```java
32 | public interface TicketingSystem {
33 | Ticket buyTicket(String passenger, int route, int departure, int arrival);
34 | int inquiry(int route, int departure, int arrival);
35 | boolean refundTicket(Ticket ticket);
36 | }
37 | ```
38 |
39 | 其中:
40 |
41 | - `buyTicket`是购票方法,即乘客`passenger`购买`route`车次从`departure`站到`arrival`站的车票1张。若购票成功,返回有效的`Ticket`对象;若失败(即无余票),返回无效的`Ticket`对象(即`return null`)。
42 | - `refundTicket`是退票方法,对有效的`Ticket`对象返回`true`,对错误或无效的`Ticket`对象返回`false`。
43 | - `inquriy`是查询余票方法,即查询`route`车次从`departure`站到`arrival`站的余票数。
44 |
45 | ## 完成`TicketingDS`类
46 |
47 | 完成一个用于列车售票的可线性化并发数据结构:`TicketingDS`类:
48 |
49 | 1. 实现`TicketingSystem`接口,
50 | 2. 提供`TicketingDS(routenum, coachnum, seatnum, stationnum, threadnum);`构造函数。
51 |
52 | 其中:
53 |
54 | - `routenum`是车次总数(缺省为5个),
55 | - `coachnum`是列车的车厢数目(缺省为8个),
56 | - `seatnum`是每节车厢的座位数(缺省为100个),
57 | - `stationnum`是每个车次经停站的数量(缺省为10个,含始发站和终点站),
58 | - `threadnum`是并发购票的线程数(缺省为16个)。
59 |
60 | 为简单起见,假设每个车次的`coachnum`、`seatnum`和`stationnum`都相同。
61 | 车票涉及的各项参数均从1开始计数,例如车厢从1到8号,车站从1到10编号等。
62 |
63 | ## 完成多线程测试程序
64 |
65 | 需编写多线程测试程序,在`main`方法中用下述语句创建`TicketingDS`类的一个实例。
66 |
67 | ```java
68 | final TicketingDS tds = new TicketingDS(routenum, coachnum, seatnum, stationnum, threadnum);
69 | ```
70 |
71 | 系统中同时存在`threadnum`个线程(缺省为16个),每个线程是一个票务代理,需要:
72 |
73 | 1. 按照60%查询余票,30%购票和10%退票的比率反复调用`TicketingDS`类的三种方法若干次(缺省为总共10000次);
74 | 2. 按照线程数为4,8,16,32,64个的情况分别调用。
75 |
76 | 需要最后给出:
77 |
78 | 1. 给出每种方法调用的平均执行时间;
79 | 2. 同时计算系统的总吞吐率(单位时间内完成的方法调用总数)。
80 |
81 | ## 正确性要求
82 |
83 | 需要保证以下正确性:
84 |
85 | - 每张车票都有一个唯一的编号`tid`,不能重复。
86 | - 每一个`tid`的车票只能出售一次。退票后,原车票的`tid`作废。
87 | - 每个区段有余票时,系统必须满足该区段的购票请求。
88 | - 车票不能超卖,系统不能卖无座车票。
89 | - 买票、退票和查询余票方法均需满足可线性化要求。
90 |
91 | ## 文件清单
92 |
93 | 所有Java程序放在`ticketingsystem`目录中,`trace.sh`文件放在`ticketingsystem`目录的上层目录中。
94 | 如果程序有多重目录,那么将主Java程序放在`ticketingsystem`目录中。
95 |
96 | 文件清单如下:
97 |
98 | - `trace.sh`是trace生成脚本,用于正确性验证,不能更改。
99 | - `pom.xml`是依赖配置文件,使用`mvn`。
100 | - `.travis.yml`是CI配置文件,用于自动化测试。
101 | - 文件夹`.github`是github自动化测试配置文件。
102 | - 文件夹`src/main/java`为代码文件夹。
103 | 1. `TicketingSystem.java`是规范文件,不能更改。
104 | 2. `Trace.java`是trace生成程序,用于正确性验证,不能更改。
105 | 3. `TicketingDS.java`是并发数据结构的实现。
106 | 4. ... 其他的自建类。
107 | 5. `PerformanceBenchmark.java`是JMH基准测试程序。
108 | 6. `jmh.benchmark.PerformanceBenchmarkRunner.java`是JMH基准测试启动文件。
109 |
110 | - 文件夹`src/test/java`为测试文件夹。
111 | 1. `ticketingsystem`存放基本测试单元。
112 | - `UnitTest.java`为系统的单元测试,为单线程运行。
113 | - `RandomTest.java`为系统的随机测试,通过多线程,随机购、退、查票。
114 | - `MultiThreadTest.java`为多线程买、退票测试程序,通过多线程随机购、退票。
115 | - `TraceVerifyTest.java`为trace单线程可线性化比对测试。
116 | 2. `verify`文件夹存放trace单线程可线性化比对测试资源文件
117 | - `Trace.java.copy`为Trace调用文件,会自动替换原先的Trace.java。
118 | - `verify.jar`为单线程线性化测试包。
119 | 3. `linerChecker`文件夹存放trace多线程可线性化比对测试。
120 | - `check.sh`为启动脚本。
121 | - `checker.jar`为多线程线性化测试包。
122 |
123 | ## 使用说明
124 |
125 | ### 文件目录
126 |
127 | 项目文件主体在`src/main/java`下,你需要将你的文件放在`src/main/java/ticketingsystem`文件夹内,`PerformanceBenchmark.java`以及`jmh`文件夹用于基准测试不能删除。
128 |
129 | 1. 保证整个项目的结构。
130 | 2. 在`src/main/java/ticketingsystem`替换自己的实现。
131 | 3. 如果使用非`java-11`版本,请调整`pom.xml`。更改`your java version`为自己版本。
132 |
133 | ```xml
134 |
135 | UTF-8
136 | your java version
137 | ${java.version}
138 | ${java.version}
139 | ...
140 |
141 | ```
142 |
143 | ### 使用`maven`
144 |
145 | 项目使用`maven`构建,运行前请安装`maven`。没有改变基本操作,常用的命令如下:
146 |
147 | - 通过`mvn clean`清理生成文件
148 | - 通过`mvn package`打成jar包
149 | - 通过`mvn test`执行测试
150 | - ...
151 |
152 | ### 使用`Junit`进行正确性测试
153 |
154 | > 注意:你需要安装`maven`才能执行,并且在执行过程中会自动安装相应依赖。
155 |
156 | 项目使用`Junit`进行正确性测试,你可以使用:
157 |
158 | - `mvn test`命令完成测试
159 | - 查看运行结果,会报告测试数量以及通过测试点数量。
160 |
161 | ### 使用`JMH`进行性能测试
162 |
163 | > 注意:你需要安装`maven`才能执行,并且在执行过程中会自动安装相应依赖。
164 |
165 | 项目使用`JMH`进行性能测试,你可以使用:
166 |
167 | - `mvn package`将项目打包
168 | - 在项目根目录下,运行:
169 | - `java -cp .\target\trainTicketingSystem-1.0-SNAPSHOT.jar ticketingsystem.jmh.benchmark.PerformanceBenchmarkRunner`。
170 | - 查看运行结果,结果单位为`ops/s`,即每秒操作数。这里的操作数与真实数量有差距,需要对数据乘上每次操作执行的买、退、查票动作数,即需要乘上64000。
171 |
172 | ### 使用CI自动化测试
173 |
174 | 项目支持`github workflow`以及`travis-ci`自动化测试,开箱即用。
175 | 每次`push`都会自动触发测试。
176 |
177 | ## 联系方式
178 |
179 | 
180 | 
181 |
182 | 任何问题欢迎提交issue。
183 |
--------------------------------------------------------------------------------
/src/main/java/ticketingsystem/Train.java:
--------------------------------------------------------------------------------
1 | package ticketingsystem;
2 |
3 | import java.util.concurrent.ConcurrentHashMap;
4 | import java.util.concurrent.atomic.AtomicInteger;
5 |
6 | class RefundTicket {
7 | int seat;
8 | int departure;
9 | int arrival;
10 | public RefundTicket(final int seat, final int departure, final int arrival) {
11 | this.seat = seat;
12 | this.departure = departure;
13 | this.arrival = arrival;
14 | }
15 | }
16 |
17 | public class Train {
18 | private AtomicInteger[] seats;
19 | private final int seatNum;
20 | private final int coachNum;
21 | private final int stationNum;
22 | private final int allSeatNum;
23 | private ConcurrentHashMap soldTickets;
24 |
25 | private static final int BUFSIZE = 20;
26 | private ThreadLocal refundList = ThreadLocal.withInitial(()-> new RefundTicket[BUFSIZE]);
27 | private ThreadLocal pointer = ThreadLocal.withInitial(()-> 0);
28 | // map for remain seats
29 | RemainSeatsTable remainSeats;
30 |
31 | public Train(final int coachnum, final int seatnum, final int stationnum) {
32 | this.seatNum = seatnum;
33 | this.allSeatNum = coachnum * seatnum;
34 | this.coachNum = coachnum;
35 | this.stationNum = stationnum;
36 | this.seats = new AtomicInteger[this.allSeatNum];
37 | for (int i = 0; i < this.allSeatNum; ++i) {
38 | this.seats[i] = new AtomicInteger(0);
39 | }
40 | remainSeats = new RemainSeatsTable(this.allSeatNum, this.stationNum);
41 | int initialCapacity = 128;
42 | float loadFactor = 0.5f;
43 | int concurrencyLevel = 2;
44 | soldTickets = new ConcurrentHashMap<>(initialCapacity, loadFactor, concurrencyLevel);
45 | }
46 |
47 | public final int getAndLockSeat(final int departure, final int arrival) {
48 | return getAndLockSeat(departure, arrival, 0);
49 | }
50 |
51 | public int getAndLockSeat(final int departure, final int arrival, final int beginSeats) {
52 | // check if has seats
53 | if (haveRemainSeats(departure, arrival)) {
54 | int beginSeat = beginSeats % allSeatNum;
55 | // find the seat
56 | for (int i = 0; i < allSeatNum; ++i) {
57 | int pos = (beginSeat + i) % allSeatNum;
58 | int tmp = seats[pos].get();
59 | // if seat is not occupied
60 | while (!isSeatOccupied(tmp, departure, arrival)) {
61 | // CAS!
62 | if (seats[pos].compareAndSet(tmp, setOccupied(tmp, departure, arrival))) {
63 | remainSeats.decrementRemainSeats(departure, arrival, tmp);
64 | return pos;
65 | }
66 | tmp = seats[pos].get();
67 | }
68 | }
69 | }
70 | return -1;
71 | }
72 |
73 | public final int getRemainSeats(final int departure, final int arrival) {
74 | return remainSeats.getRemainSeats(departure, arrival);
75 | }
76 |
77 | public boolean unlockSeat(final int seat, final int departure, final int arrival) {
78 | while (true) {
79 | int tmp = seats[seat].get();
80 | int cleanTmp = cleanOccupied(tmp, departure, arrival);
81 | if (seats[seat].compareAndSet(tmp, cleanTmp)) {
82 | remainSeats.incrementRemainSeats(departure, arrival, cleanTmp);
83 | return true;
84 | }
85 | }
86 | }
87 |
88 | private final boolean isSeatOccupied(final int block, final int departure, final int arrival) {
89 | int occupied = ((0x01 << (arrival - departure)) - 1) << departure;
90 | // 00000|0000|000000 block
91 | // 00000|1111|000000 occupied
92 | return (occupied & block) != 0;
93 | }
94 |
95 | private final int setOccupied(final int block, final int departure, final int arrival) {
96 | int occupied = ((0x01 << (arrival - departure)) - 1) << departure;
97 | return block | occupied;
98 | }
99 |
100 | private final int cleanOccupied(final int block, final int departure, final int arrival) {
101 | int occupied = ((0x01 << (arrival - departure)) - 1) << departure;
102 | return block & ~occupied;
103 | }
104 |
105 | private final boolean haveRemainSeats(final int departure, final int arrival) {
106 | return remainSeats.getRemainSeats(departure, arrival) > 0;
107 | }
108 |
109 | public final void insertRefundList(final int seat, final int departure, final int arrival) {
110 | int p = pointer.get();
111 | refundList.get()[p] = new RefundTicket(seat, departure, arrival);
112 | pointer.set((p + 1) % BUFSIZE);
113 | }
114 |
115 | public final int getFindRefund(final int departure, final int arrival) {
116 | int usefulSeat = -1;
117 | for(int i = 0; i < BUFSIZE; ++i) {
118 | RefundTicket tick = refundList.get()[i];
119 | if(tick != null && tick.departure <= departure && tick.arrival >= arrival) {
120 | usefulSeat = tick.seat;
121 | refundList.get()[i] = null;
122 | break;
123 | }
124 | }
125 | return usefulSeat;
126 | }
127 |
128 | public void clear() {
129 | refundList.remove();
130 | pointer.remove();
131 | remainSeats.clear();
132 | }
133 |
134 | public final boolean containAndRemove(Ticket ticket) {
135 | Ticket containTicket = soldTickets.get(ticket.tid);
136 | if (containTicket == null || !ticketEquals(ticket, containTicket)) {
137 | return false;
138 | }
139 | return soldTickets.remove(ticket.tid, containTicket);
140 | }
141 |
142 | public final void addSoldTicket(Ticket ticket) {
143 | soldTickets.put(ticket.tid, ticket);
144 | }
145 |
146 | private final boolean ticketEquals(Ticket x, Ticket y) {
147 | if(x == y) return true;
148 | if(x == null || y == null) return false;
149 |
150 | return(
151 | (x.tid == y.tid) &&
152 | (x.passenger.equals(y.passenger)) &&
153 | (x.route == y.route) &&
154 | (x.coach == y.coach) &&
155 | (x.seat == y.seat) &&
156 | (x.departure == y.departure) &&
157 | (x.arrival == y.arrival)
158 | );
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/test/java/verify/Trace.java.copy:
--------------------------------------------------------------------------------
1 | package ticketingsystem;
2 |
3 | import java.util.*;
4 |
5 | import java.util.concurrent.atomic.AtomicInteger;
6 |
7 | class ThreadId {
8 | // Atomic integer containing the next thread ID to be assigned
9 | private static final AtomicInteger nextId = new AtomicInteger(0);
10 |
11 | // Thread local variable containing each thread's ID
12 | private static final ThreadLocal threadId =
13 | new ThreadLocal() {
14 | @Override protected Integer initialValue() {
15 | return nextId.getAndIncrement();
16 | }
17 | };
18 |
19 | // Returns the current thread's unique ID, assigning it if necessary
20 | public static int get() {
21 | return threadId.get();
22 | }
23 | }
24 |
25 | public class Trace {
26 | final static int threadnum = 1;
27 | final static int routenum = 3; // route is designed from 1 to 3
28 | final static int coachnum = 3; // coach is arranged from 1 to 5
29 | final static int seatnum = 3; // seat is allocated from 1 to 20
30 | final static int stationnum = 3; // station is designed from 1 to 5
31 |
32 | final static int testnum = 3000;
33 | final static int retpc = 30; // return ticket operation is 10% percent
34 | final static int buypc = 60; // buy ticket operation is 30% percent
35 | final static int inqpc = 100; //inquiry ticket operation is 60% percent
36 |
37 | static String passengerName() {
38 | Random rand = new Random();
39 | long uid = rand.nextInt(testnum);
40 | return "passenger" + uid;
41 | }
42 |
43 | public static void main(String[] args) throws InterruptedException {
44 |
45 |
46 | Thread[] threads = new Thread[threadnum];
47 |
48 | final TicketingDS tds = new TicketingDS(routenum, coachnum, seatnum, stationnum, threadnum);
49 |
50 | final long startTime = System.nanoTime();
51 | //long preTime = startTime;
52 |
53 | for (int i = 0; i< threadnum; i++) {
54 | threads[i] = new Thread(new Runnable() {
55 | public void run() {
56 | Random rand = new Random();
57 | Ticket ticket = new Ticket();
58 | ArrayList soldTicket = new ArrayList();
59 |
60 | //System.out.println(ThreadId.get());
61 | for (int i = 0; i < testnum; i++) {
62 | int sel = rand.nextInt(inqpc);
63 | if (0 <= sel && sel < retpc && soldTicket.size() > 0) { // return ticket
64 | int select = rand.nextInt(soldTicket.size());
65 | long preTime = System.nanoTime() - startTime;
66 | if ((ticket = soldTicket.remove(select)) != null) {
67 | preTime = System.nanoTime() - startTime;
68 | if (tds.refundTicket(ticket)) {
69 | long postTime = System.nanoTime() - startTime;
70 | System.out.println(preTime + " " + postTime + " " + ThreadId.get() + " " + "TicketRefund" + " " + ticket.tid + " " + ticket.passenger + " " + ticket.route + " " + ticket.coach + " " + ticket.departure + " " + ticket.arrival + " " + ticket.seat);
71 | //System.out.println(preTime + " " + ThreadId.get() + " " + "TicketRefund" + " " + ticket.tid + " " + ticket.passenger + " " + ticket.route + " " + ticket.coach + " " + ticket.departure + " " + ticket.arrival + " " + ticket.seat);
72 | //System.out.flush();
73 | } else {
74 | long postTime = System.nanoTime() - startTime;
75 | System.out.println(preTime + " " + postTime + " " + ThreadId.get() + " " + "ErrOfRefund");
76 | //System.out.flush();
77 | }
78 | } else {
79 | long postTime = System.nanoTime() - startTime;
80 | System.out.println(preTime + " " + postTime + " " + ThreadId.get() + " " + "ErrOfRefund");
81 | //System.out.flush();
82 | }
83 | } else if (retpc <= sel && sel < buypc) { // buy ticket
84 | String passenger = passengerName();
85 | int route = rand.nextInt(routenum) + 1;
86 | int departure = rand.nextInt(stationnum - 1) + 1;
87 | int arrival = departure + rand.nextInt(stationnum - departure) + 1; // arrival is always greater than departure
88 | long preTime = System.nanoTime() - startTime;
89 | if ((ticket = tds.buyTicket(passenger, route, departure, arrival)) != null) {
90 | long postTime = System.nanoTime() - startTime;
91 | System.out.println(preTime + " " + postTime + " " + ThreadId.get() + " " + "TicketBought" + " " + ticket.tid + " " + ticket.passenger + " " + ticket.route + " " + ticket.coach + " " + ticket.departure + " " + ticket.arrival + " " + ticket.seat);
92 | //System.out.println(preTime + " " + ThreadId.get() + " " + "TicketBought" + " " + ticket.tid + " " + ticket.passenger + " " + ticket.route + " " + ticket.coach + " " + ticket.departure + " " + ticket.arrival + " " + ticket.seat);
93 | soldTicket.add(ticket);
94 | //System.out.flush();
95 | } else {
96 | long postTime = System.nanoTime() - startTime;
97 | System.out.println(preTime + " " + postTime + " " + ThreadId.get() + " " + "TicketSoldOut" + " " + route + " " + departure+ " " + arrival);
98 | //System.out.println(preTime + " " + ThreadId.get() + " " + "TicketSoldOut" + " " + route + " " + departure+ " " + arrival);
99 | //System.out.flush();
100 | }
101 | } else if (buypc <= sel && sel < inqpc) { // inquiry ticket
102 |
103 | int route = rand.nextInt(routenum) + 1;
104 | int departure = rand.nextInt(stationnum - 1) + 1;
105 | int arrival = departure + rand.nextInt(stationnum - departure) + 1; // arrival is always greater than departure
106 | long preTime = System.nanoTime() - startTime;
107 | int leftTicket = tds.inquiry(route, departure, arrival);
108 | long postTime = System.nanoTime() - startTime;
109 | System.out.println(preTime + " " + postTime + " " + ThreadId.get() + " " + "RemainTicket" + " " + leftTicket + " " + route+ " " + departure+ " " + arrival);
110 | //System.out.println(preTime + " " + ThreadId.get() + " " + "RemainTicket" + " " + leftTicket + " " + route+ " " + departure+ " " + arrival);
111 | //System.out.flush();
112 |
113 | }
114 | }
115 |
116 | }
117 | });
118 | threads[i].start();
119 | }
120 |
121 | for (int i = 0; i< threadnum; i++) {
122 | threads[i].join();
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/test/java/ticketingsystem/RandomTest.java:
--------------------------------------------------------------------------------
1 | package ticketingsystem;
2 |
3 | import java.util.*;
4 |
5 | import static org.junit.jupiter.api.Assertions.*;
6 | import org.junit.jupiter.api.Test;
7 | import org.junit.jupiter.api.DisplayName;
8 |
9 | @DisplayName("RandomTest")
10 | public class RandomTest {
11 | int threadnum = 8;
12 | int routenum = 20; // route is designed from 1 to 3
13 | int coachnum = 10; // coach is arranged from 1 to 5
14 | int seatnum = 100; // seat is allocated from 1 to 20
15 | int stationnum = 16; // station is designed from 1 to 5
16 |
17 | int testnum = 640000;
18 | final static int RETPC = 10; // return ticket operation is 10% percent
19 | final static int BUYPC = 30; // buy ticket operation is 30% percent
20 | final static int INQPC = 100; // inquiry ticket operation is 60% percent
21 |
22 | private String passengerName() {
23 | Random rand = new Random(System.currentTimeMillis());
24 | long uid = rand.nextInt(testnum);
25 | return "passenger" + uid;
26 | }
27 |
28 | @Test
29 | @DisplayName("RandomTest - beginTest")
30 | void beginTest() throws InterruptedException {
31 |
32 | final TicketingDS tds = new TicketingDS(routenum, coachnum, seatnum, stationnum, threadnum);
33 |
34 | Thread[] threads = new Thread[threadnum];
35 | final long startTime = System.nanoTime();
36 |
37 | for (int i = 0; i < threadnum; i++) {
38 | threads[i] = new Thread(new Runnable() {
39 | public void run() {
40 | Random rand = new Random(System.currentTimeMillis());
41 | Ticket ticket = new Ticket();
42 | ArrayList soldTicket = new ArrayList();
43 |
44 | for (int i = 0; i < testnum; i++) {
45 | int sel = rand.nextInt(INQPC);
46 | // refund ticket
47 | if (0 <= sel && sel < RETPC && !soldTicket.isEmpty()) {
48 | int select = rand.nextInt(soldTicket.size());
49 | if ((ticket = soldTicket.remove(select)) != null) {
50 | long preTime = System.nanoTime() - startTime;
51 | if (tds.refundTicket(ticket)) {
52 | // long postTime = System.nanoTime() - startTime;
53 | // System.out.println(preTime + " " + postTime + " " + ThreadId.get() + " "
54 | // + "TicketRefund" + " " + ticket.tid + " " + ticket.passenger + " "
55 | // + ticket.route + " " + ticket.coach + " " + ticket.departure + " "
56 | // + ticket.arrival + " " + ticket.seat);
57 | // System.out.flush();
58 | } else {
59 | System.err.println(preTime + " " + String.valueOf(System.nanoTime() - startTime)
60 | + " " + ThreadId.get() + " " + "ErrOfRefund");
61 | fail("Err: cannot refund ticket");
62 | assert false : "Err: cannot refund ticket";
63 | }
64 | } else {
65 | long preTime = System.nanoTime() - startTime;
66 | System.err.println(preTime + " " + String.valueOf(System.nanoTime() - startTime) + " "
67 | + ThreadId.get() + " " + "ErrOfRefund");
68 | assertNotNull(ticket, "Err: soldTicket out of bounds");
69 | assert false : "Err: soldTicket out of bounds";
70 | }
71 | } else
72 | // buy ticket
73 | if (RETPC <= sel && sel < BUYPC) {
74 | String passenger = passengerName();
75 | int route = rand.nextInt(routenum) + 1;
76 | int departure = rand.nextInt(stationnum - 1) + 1;
77 | int arrival = departure + rand.nextInt(stationnum - departure) + 1; // arrival is always
78 | // greater than
79 | // departure
80 | // long preTime = System.nanoTime() - startTime;
81 | if ((ticket = tds.buyTicket(passenger, route, departure, arrival)) != null) {
82 | // long postTime = System.nanoTime() - startTime;
83 | // System.out.println(preTime + " " + postTime + " " + ThreadId.get() + " "
84 | // + "TicketBought" + " " + ticket.tid + " " + ticket.passenger + " "
85 | // + ticket.route + " " + ticket.coach + " " + ticket.departure + " "
86 | // + ticket.arrival + " " + ticket.seat);
87 | soldTicket.add(ticket);
88 | // System.out.flush();
89 | } else {
90 | // System.out.println(preTime + " " + String.valueOf(System.nanoTime() - startTime) + " "
91 | // + ThreadId.get() + " " + "TicketSoldOut" + " " + route + " " + departure + " "
92 | // + arrival);
93 | // System.out.flush();
94 | }
95 | } else
96 | // inquiry ticket
97 | if (BUYPC <= sel && sel < INQPC) {
98 |
99 | int route = rand.nextInt(routenum) + 1;
100 | int departure = rand.nextInt(stationnum - 1) + 1;
101 | int arrival = departure + rand.nextInt(stationnum - departure) + 1; // arrival is always
102 | // greater than
103 | // departure
104 | // long preTime = System.nanoTime() - startTime;
105 | int leftTicket = tds.inquiry(route, departure, arrival);
106 | // long postTime = System.nanoTime() - startTime;
107 | // System.out.println(preTime + " " + postTime + " " + ThreadId.get() + " " + "RemainTicket"
108 | // + " " + leftTicket + " " + route + " " + departure + " " + arrival);
109 | // System.out.flush();
110 | }
111 | }
112 | }
113 | });
114 | threads[i].start();
115 | }
116 |
117 | for (int i = 0; i < threadnum; i++) {
118 | threads[i].join();
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/java/ticketingsystem/PerformanceBenchmark.java:
--------------------------------------------------------------------------------
1 | package ticketingsystem;
2 |
3 | import org.openjdk.jmh.annotations.*;
4 |
5 | import java.text.DecimalFormat;
6 | import java.util.*;
7 |
8 | import java.util.concurrent.Callable;
9 | import java.util.concurrent.ExecutorService;
10 | import java.util.concurrent.Executors;
11 | import java.util.concurrent.ThreadLocalRandom;
12 | import java.util.concurrent.TimeUnit;
13 | import java.util.logging.Logger;
14 |
15 | @BenchmarkMode(Mode.Throughput)
16 | @Warmup(iterations = 5, time = 1)
17 | @Measurement(iterations = 5, time = 5)
18 | @Fork(2)
19 | @State(value = Scope.Benchmark)
20 | @OutputTimeUnit(TimeUnit.SECONDS)
21 | public class PerformanceBenchmark {
22 | @Param({ "1", "2", "4", "8", "16", "32", "64", "128" })
23 | static int nThreads;
24 |
25 | private ExecutorService pool;
26 |
27 | final static int routenum = 20; // route is designed from 1 to 3
28 | final static int coachnum = 10; // coach is arranged from 1 to 5
29 | final static int seatnum = 100; // seat is allocated from 1 to 20
30 | final static int stationnum = 16; // station is designed from 1 to 5
31 |
32 | final static int testnum = 64000;
33 | final static int retpc = 10; // return ticket operation is 10% percent
34 | final static int buypc = 30; // buy ticket operation is 30% percent
35 | final static int inqpc = 100; // inquiry ticket operation is 60% percent
36 |
37 | TicketingDS tds;
38 | ArrayList> list;
39 |
40 | static ThreadLocalRandom rand = ThreadLocalRandom.current();
41 |
42 | // perform record
43 | class PerformRecord {
44 | int buySuccessTimes;
45 | int buyFailTimes;
46 | int refundTimes;
47 | int inqueryTimes;
48 | }
49 |
50 | ThreadLocal perform = ThreadLocal.withInitial(() -> new PerformRecord());
51 | List singlePerform = Collections.synchronizedList(new ArrayList());
52 | ArrayList performList = new ArrayList<>();
53 |
54 | Logger logger = Logger.getLogger("PerformLog");
55 |
56 | static String passengerName() {
57 | long uid = rand.nextLong();
58 | return "passenger" + uid;
59 | }
60 |
61 | @Setup(Level.Trial)
62 | public void initPerform() {
63 | performList.clear();
64 | }
65 |
66 | @Setup(Level.Iteration)
67 | public void init() {
68 | this.pool = Executors.newFixedThreadPool(nThreads);
69 | tds = new TicketingDS(routenum, coachnum, seatnum, stationnum, nThreads);
70 | list = new ArrayList<>();
71 | singlePerform.clear();
72 | for (int i = 0; i < nThreads; i++) {
73 | list.add(new Callable