文章目录
一、多线程基础
1.进程与线程
线程是进程内一个相对独立的、可调度的执行单元,又称为轻量级进程。和进程类似,线程也有运行、就绪、阻塞等状态。线程必须拥有父进程。系统没有为线程分配资源,它与进程中的其他线程共享该进程的共享资源。同一进程的不同线程共享相同的地址空间,它们可以访问相同的变量和对象,实现线程间的信息共享。线程间的通信简单而有效,上下文切换非常快。
进程相当于一个应用程序(以QQ为例),进程间不能通信(因为内存不共享),QQ中的每一个聊天窗口便是线程。
2.多线程的好处
多线程开启时,start()让jvm重新开辟了一个新的栈,并run()方法压栈,此时线程处于就绪状态,cpu随机挑选一个栈分配执行时间,执行完毕后再次到就绪状态,所以,多线程的本质仍然是顺序栈执行
多线程程序是指一个程序中包含多个执行流,它是实现并发机制的有效手段。从逻辑的观点看,多线程意味着一个程序的多个语句快同时执行,但不等于多次启动一个程序
根本的好处是:“线程操作资源类”
引入多线程设计原因
- 某些应用具有内在的多控制流结构,这些控制流具有合作性质,需要共享内容。采用多线程易于对问题建模
- 在需要多控制流的应用中,多线程比多进程在速度上具有绝对优势。统计表明,线程的建立速度约比进程的建立速度快100倍
- 采用多线程可以提高处理机与设备之间的并行性
- 在多处理机的硬件环境中,多线程可并行执行,从而可提高资源利用率和进程推进速度。
3.run()和start()方法
- run()方法是新线程启动的前提,start()方法是新线程启动的标志
- 当调用start()时,会自动告诉jvm机,此时我们需要开辟新的栈空间
- 当开辟完新的栈空间后,start()方法会自动弹栈,在新栈中,run()方法压栈
- 如果我们直接调用新对象的run()方法,此时不会造成多线程并发,因为此时相当于代码由上至下的顺序执行
- run()方法在分支栈中等价于原栈空间的main
4.多线程的状态
多线程状态流程图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5HKbKXYc-1687074974651)(…/blog示意图/IMG_20230616_180836.jpg)]
1.新生状态
在创建Thread实例后,此Thread对象就处于新生状态,此时线程已被创建但还未开始执行。是一个空线程对象,还没有分配到系统资源
2.就绪状态(*)
为了得到实际的线程,为线程创建属于它的内存资源,需要使用start()方法启动线程,此时线程属于就绪状态,等待cpu分配执行时间(cpu时间片),自动调用自己的run()方法,开启运行状态
主线程会继续执行start()方法下面的语句,这时run()方法可能还在执行,从而实现了多任务操作
3.运行状态
如果线程不能在一个时间片中执行完毕,会被中断,由执行状态回到就绪状态,等待下一轮cpu分配执行时间
运行状态可以调用yield()方法主动放弃执行,转到就绪状态。仍然可能被再次调用
4.阻塞状态(*)
阻塞状态其实是睡眠、资源阻塞、等待三种状态的结合体,此时线程仍是活的,只是缺乏运行的条件。
引起阻塞状态的因素:
- 睡眠:线程调用sleep()方法睡眠一段时间
- 资源阻塞:线程在等待一种资源
- 等待:调用了wait()方法使线程挂起,直到线程得到notify()或notifyAll()消息
阻塞状态时,会让出cpu,让其他保持就绪状态的线程获得可能的执行时间(cpu时间片)
5.死亡状态
如果线程的run()方法执行完毕,线程正常结束;或者线程执行过程中抛出一个未捕获的异常或错误,线程异常结束。结束后,线程处于死亡状态
一旦线程死亡,就不能复生。如果死亡状态任然调用start()方法,会出现运行时异常IllegalThreadException
5.多线程数据安全问题
数据安全问题的出现因为存在着原子性。
本质不是多线程导致了资源争抢,而是打破了方法执行的完整性,导致最后的“读脏数据、重复读数据”等的问题。因此使用到synchronized同步关键字解决数据安全问题
二、多线程引例
1.多线程步骤
构建Thread子类对象引发多线程的步骤如下:
- 构建Thread子类,重写其中的run()方法
- 创建线程对象
- 线程对象调用start()方法启动该线程
2.代码实现
public class ThreadDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 此处如果直接调用myThread.run()方法,会直接进行程序的顺序执行,不 会构成多线程
for (int i = 0; i < 10; i++) {
System.out.println("main-->" + i);
}
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("myThread-->" + i);
}
}
}
/**
*上面的代码包含三个线程,
1.main线程
2.thread线程
3.GC线程(守护线程)
**/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8sPxJywV-1687074974652)(…/blog示意图/微信图片_20230609180331.png)]
3.代码分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pyIp1LAO-1687074974653)(…/blog示意图/多线程jvm示意图.png)]
三、多线程应用
多线程最为关键的好处即利用不同的线程操作同一个内容
下面是一个售票的小程序,两个售票员一起售卖100张门票,利用多线程实现
package GoodsTrade.administrator; public class ThreadDemo { public static void main(String[] args) { MyThread myThread = new MyThread(100); myThread.start(); int num = myThread.getNum(); while(num > 0) { myThread.sale(); } } } class MyThread extends Thread { private int num; public MyThread() {} public MyThread(int num) { //构造函数,方便在定义对象时传入售票的票数 this.num = num; } public int getNum() { return num; } public void setNum(int num) { this.num = num; } @Override public void run() { sale(); } public void sale() { while(num > 0) { try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(currentThread().getName() + "正在出售第 " + this.num + " 张票,剩余票数:" + --num); setNum(num); } } } //运行结果 /* Thread-0正在出售第 100 张票,剩余票数:99 main正在出售第 100 张票,剩余票数:98 main正在出售第 98 张票,剩余票数:97 Thread-0正在出售第 97 张票,剩余票数:96 Thread-0正在出售第 96 张票,剩余票数:95 main正在出售第 96 张票,剩余票数:94 main正在出售第 94 张票,剩余票数:93 Thread-0正在出售第 93 张票,剩余票数:92 main正在出售第 92 张票,剩余票数:91 Thread-0正在出售第 91 张票,剩余票数:90 main正在出售第 90 张票,剩余票数:89 Thread-0正在出售第 90 张票,剩余票数:88 main正在出售第 88 张票,剩余票数:87 Thread-0正在出售第 88 张票,剩余票数:86 main正在出售第 86 张票,剩余票数:85 Thread-0正在出售第 85 张票,剩余票数:84 Thread-0正在出售第 84 张票,剩余票数:83 main正在出售第 84 张票,剩余票数:83 main正在出售第 83 张票,剩余票数:82 Thread-0正在出售第 82 张票,剩余票数:81 main正在出售第 81 张票,剩余票数:80 Thread-0正在出售第 80 张票,剩余票数:79 Thread-0正在出售第 79 张票,剩余票数:78 main正在出售第 78 张票,剩余票数:77 main正在出售第 77 张票,剩余票数:76 Thread-0正在出售第 77 张票,剩余票数:75 Thread-0正在出售第 75 张票,剩余票数:74 main正在出售第 74 张票,剩余票数:73 Thread-0正在出售第 73 张票,剩余票数:72 main正在出售第 73 张票,剩余票数:72 Thread-0正在出售第 72 张票,剩余票数:70 main正在出售第 72 张票,剩余票数:71 main正在出售第 70 张票,剩余票数:69 Thread-0正在出售第 70 张票,剩余票数:69 Thread-0正在出售第 69 张票,剩余票数:68 main正在出售第 69 张票,剩余票数:68 main正在出售第 68 张票,剩余票数:66 Thread-0正在出售第 68 张票,剩余票数:67 Thread-0正在出售第 66 张票,剩余票数:65 main正在出售第 65 张票,剩余票数:64 main正在出售第 64 张票,剩余票数:63 Thread-0正在出售第 63 张票,剩余票数:62 main正在出售第 62 张票,剩余票数:61 Thread-0正在出售第 62 张票,剩余票数:61 Thread-0正在出售第 61 张票,剩余票数:60 main正在出售第 60 张票,剩余票数:59 Thread-0正在出售第 59 张票,剩余票数:58 main正在出售第 59 张票,剩余票数:57 main正在出售第 57 张票,剩余票数:56 Thread-0正在出售第 57 张票,剩余票数:55 Thread-0正在出售第 55 张票,剩余票数:54 main正在出售第 54 张票,剩余票数:53 Thread-0正在出售第 53 张票,剩余票数:52 main正在出售第 53 张票,剩余票数:52 Thread-0正在出售第 52 张票,剩余票数:51 main正在出售第 51 张票,剩余票数:50 Thread-0正在出售第 50 张票,剩余票数:49 main正在出售第 49 张票,剩余票数:48 Thread-0正在出售第 48 张票,剩余票数:47 main正在出售第 48 张票,剩余票数:47 main正在出售第 47 张票,剩余票数:46 Thread-0正在出售第 46 张票,剩余票数:45 main正在出售第 45 张票,剩余票数:44 Thread-0正在出售第 45 张票,剩余票数:44 main正在出售第 44 张票,剩余票数:43 Thread-0正在出售第 43 张票,剩余票数:42 main正在出售第 42 张票,剩余票数:41 Thread-0正在出售第 41 张票,剩余票数:40 main正在出售第 40 张票,剩余票数:39 Thread-0正在出售第 40 张票,剩余票数:39 Thread-0正在出售第 39 张票,剩余票数:38 main正在出售第 39 张票,剩余票数:38 main正在出售第 38 张票,剩余票数:37 Thread-0正在出售第 37 张票,剩余票数:36 main正在出售第 36 张票,剩余票数:34 Thread-0正在出售第 36 张票,剩余票数:35 main正在出售第 34 张票,剩余票数:33 Thread-0正在出售第 34 张票,剩余票数:33 Thread-0正在出售第 33 张票,剩余票数:32 main正在出售第 32 张票,剩余票数:31 Thread-0正在出售第 31 张票,剩余票数:30 main正在出售第 31 张票,剩余票数:30 main正在出售第 30 张票,剩余票数:29 Thread-0正在出售第 29 张票,剩余票数:28 Thread-0正在出售第 28 张票,剩余票数:27 main正在出售第 27 张票,剩余票数:26 main正在出售第 26 张票,剩余票数:25 Thread-0正在出售第 26 张票,剩余票数:24 Thread-0正在出售第 24 张票,剩余票数:23 main正在出售第 23 张票,剩余票数:22 Thread-0正在出售第 22 张票,剩余票数:21 main正在出售第 22 张票,剩余票数:20 Thread-0正在出售第 19 张票,剩余票数:18 main正在出售第 20 张票,剩余票数:19 main正在出售第 18 张票,剩余票数:17 Thread-0正在出售第 18 张票,剩余票数:17 main正在出售第 17 张票,剩余票数:16 Thread-0正在出售第 17 张票,剩余票数:16 Thread-0正在出售第 16 张票,剩余票数:15 main正在出售第 15 张票,剩余票数:14 Thread-0正在出售第 14 张票,剩余票数:13 main正在出售第 14 张票,剩余票数:13 main正在出售第 13 张票,剩余票数:12 Thread-0正在出售第 13 张票,剩余票数:11 main正在出售第 11 张票,剩余票数:10 Thread-0正在出售第 11 张票,剩余票数:9 main正在出售第 9 张票,剩余票数:8 Thread-0正在出售第 9 张票,剩余票数:8 main正在出售第 8 张票,剩余票数:7 Thread-0正在出售第 7 张票,剩余票数:6 main正在出售第 6 张票,剩余票数:5 Thread-0正在出售第 6 张票,剩余票数:4 main正在出售第 4 张票,剩余票数:3 Thread-0正在出售第 4 张票,剩余票数:2 Thread-0正在出售第 2 张票,剩余票数:1 main正在出售第 2 张票,剩余票数:0 */
1.程序中存在的不足
出现问题根本原因:
同一个应用程序在同一个cpu上跑,不是并行,而是并发(交替执行)
多线程cpu的调度方式是抢占式,导致最终cpu分配给每个线程的执行时间不同
(此处的抢占式并不是代表不同的线程有抢占cpu分配时间的能力,而是cpu决定给每个线程到的时间不同)
1.需要继承Thread,扩展性不强
每当构建一个Thread子类,都需要重新继承Thread,如果需要将代码根据更细小的类型分类,由于java语言的单继承原则,不能很好的实现
如:我们创建一个tickets方法,想要使用多线程去卖票,而在子类tickets中,可能根据业务逻辑,需要分类得更细,火车票、汽车票、飞机票……这时想要抽象出一个父类tickets,继承一些共有的属性和方法,提高代码的复用,不能很好的实现
2.买重票、超卖
产生重票原因:
一旦启用新线程,即创建对象后调用了start()方法,各个线程就处于就绪状态,cpu会随机挑出一个线程对其分配执行时间。
不同线程都将总票数复制到自己栈中的内存空间中,而cpu分配的执行时间未实现修改总票数的操作,而当再次获得时间是,就会导致不同线程卖重票的情况
负票:同样是因为数据未进行同步,从而让不同的cpu分配时间时,在分配给一个线程的时间更多时,会很快的卖完,导致当分配给另一个线程时,总票数多次自减,而最终导致出现负票的情况。
3.不同线程票的前后不一致
由运行结果可以看出,分支栈和main栈,在买票时出现了前后不一致的问题,
如:
主线程卖第 100 张票,剩余票数:99
主线程卖第 99 张票,剩余票数:98
主线程卖第 98 张票,剩余票数:97
分支线程卖第 100 张票,剩余票数:99当主线程都已卖到97张票了,此时分支线程才刚刚开始第100张票
原因:
其中分支线程只是看见了总票数还剩100张,而未获得更多的cpu时间片去支持该线程执行底层的输出语句,所以只是有等待着下次获得时间片去再次打印,所以出现了主线程都卖出好几张了,分支线程才刚刚卖出前面的票
2.线程的其他方法
1.sleep()方法
1.定义
可以模仿现实中买票所需要的操作时间
让当前线程暂时停下来,睡眠的时间指定的毫秒数决定,此时处于阻塞状态,这时会让出cpu,让其调度其他就绪的线程。线程醒来后返回到就绪状态。
因为阻塞状态需要不能直接回到运行状态,需要重新回到就绪状态让cpu重新分配执行时间
在多线程买票小程序中添加sleep()方法,可以模仿买票时的操作过程。此外,使得一个线程在获得CPU时间片后,更好的运行完整,防止卖重票问题的出现
2.sleep()方法拓展
1.计时器
for (int i = 0; i < 5; i++) { //计时器五秒
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("分支线程休息-->" + (i+1) + "秒");
}
2.打断计时器
interrupt()方法
public class ThreadDemo { public static void main(String[] args) { MyThread myThread = new MyThread(100); myThread.sale(); myThread.interrupt(); System.out.println("停止休息"); } } class MyThread extends Thread { private int num; int cnt = 1; public MyThread() {} public MyThread(int num) { //构造函数,方便在定义对象时传入售票的票数 this.num = num; } public int getNum() { return num; } public void setNum(int num) { this.num = num; } @Override public void run() { sale(); } public void sale() { for (int i = 0; i < 5; i++) { //计时器五秒 try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("分支线程休息-->" + (i+1) + "秒"); cnt++; } } }
3.退出线程
原本idea中包含一个stop()方法表示线程的退出,但是由于本身存在bug,已被弃用
自己设计一个退出线程的方法
package GoodsTrade.administrator; public class ThreadDemo { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); myThread.flag = false; for (int i = 0; i < 5; i++) { //设置5秒后自动醒来 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("分支线程正在沉睡--->" + (i + 1)); } myThread.flag = false; } } class MyThread extends Thread { boolean flag = true; @Override public void run() { if(flag) { try { Thread.sleep(1000 * 60 * 60 * 24); //计时器一天 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("分支线程开始工作"); } else { return; } } }
2.yield()方法
线程的让位,不阻塞直接回到就绪状态
yield()方法也是Thread类的静态方法,作用是暂时中止当前执行的线程对象的运行。若存在其他同优先级线程,则随机调用下一个同优先级线程。若不存在其他同优先级线程,则这个被中断的线程继续。这个方法可以保证cpu不会空闲。
3.join()方法
线程的合并
join()方法令当前线程”加入到“调用join()方法的线程尾部
1.不适用join()方法,直接使用多线程
public class ThreadDemo { public static void main(String[] args) { System.out.println("main线程开始"); MyThread myThread = new MyThread(); myThread.start(); System.out.println("main线程结束"); } } class MyThread extends Thread { boolean flag = true; @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("分支线程-->" + (i + 1)); } } } //运行结果 /* main线程开始 main线程结束 分支线程-->1 分支线程-->2 分支线程-->3 分支线程-->4 分支线程-->5 分支线程-->6 分支线程-->7 分支线程-->8 分支线程-->9 分支线程-->10 */
2.使用join()方法,使得程序虽然开始了多线程,仍然==“看上去”==是顺序执行
package GoodsTrade.administrator; public class ThreadDemo { public static void main(String[] args) { System.out.println("main线程开始"); MyThread myThread = new MyThread(); myThread.start(); try { myThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("main线程结束"); } } class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("分支线程-->" + (i + 1)); } } } // 运行结果 /* main线程开始 分支线程-->1 分支线程-->2 分支线程-->3 分支线程-->4 分支线程-->5 分支线程-->6 分支线程-->7 分支线程-->8 分支线程-->9 分支线程-->10 main线程结束 */
3.程序改进
由上述的分析,发现程序中存在以下不足:
- 需要每个子类都需要继承Thread,而当项目需求提高,要求我们抽象出一个父类时,由于java的单继承原则,不能很好的完成任务
- 会出现不同的线程出现矛盾,线程不同步
- cpu给某个线程分配的时间较多,导致该线程快速买票,而另一个线程中的信息仍然是之前总票的数量,导致一个卖到许多,一个刚刚开始卖。
1.增强项目的扩展性
用实现Runnable接口对象创建Thread
new Thread(() -> { //默认调用run()方法
while(tickets.getNum() > 0) {
operate.sale();
}
}, "窗口1").start();
new Thread(()->{
while(tickets.getNum() > 0) {
operate.sale();
}
}, "窗口2").start();
new Thread(()->{
while(tickets.getNum() > 0) {
operate.sale();
}
}, "窗口3").start();
2.代码冗余
通过lambda表达式和匿名内部类减少代码冗余
public class ThreadDemo {
public static void main(String[] args) {
new Thread(()->{
for (int i = 0; i < 100; i++) {
System.out.println("分支线程在执行" + i);
}
}).start();
for (int i = 0; i < 100; i++) {
System.out.println("main线程在执行" + i);
}
}
}
//此时运行结果与原多线程方法结果一致,但能实现抽象出新父类的功能,大大扩展了项目适用性
3.线程通信
通过同步关键字synchronized(同步锁),解决线程的数据安全问题。
解决脏读,复读问题。
在线程操作资源类时,会先到lock pool查看对象是否被锁,如果被锁,等待在周围直到解锁;若没锁就锁住,开始操作资源类,操作完后再释放,之后回到就绪状态,等待再次争抢锁。
加锁后虽然保持了线程通信,但由于不再线程并发,导致了性能的一定下降。
synchronized关键字的使用,本质上不要求一定是锁的东西为对象,若是字符串、或是自定义的对象类型都可以的,但必须是同一个东西,即不同的线程看见的有锁的东西为同一个。
synchronized关键字放在不同地方结果不同
加了synchronized的地方有了原子性
放在方法上 e.g: public synchronized void sell() {}
此时并不是表示将这个方法锁住,可以想象上厕所锁的不是上厕所这个方法,而锁的是厕所门。
相当于知道方法结束才开锁,锁住的是一个实体(entry)放在方法内的第一句
e.g: synchronized(this) {
if(tickets.getNum() > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(tickets.getNum() > 0) {
int num = tickets.getNum();
System.out.println(Thread.currentThread().getName()
+ " 正在售卖第 " + num + " 张票,剩余:" + (–num));
tickets.setNum(num);
}
}
}此时的结果与第一种放在方法上等价,锁的不是方法体代码块,而是(this)这个操作类的对象 即使synchronized关键字把对象锁住了,但是对象的其他方法并不会被锁住。 因为对象锁的存在,保证了各个线程都可以查看到该对象的markword标志位是否被标记成可用(即相当于各个处于就绪状态的线程等待着分配CPU时间块),可理解为在厕所看厕所的锁是否被锁,被锁时不能使用,没锁时可以使用,使用时将锁锁上。 被上锁的对象其他线程调用时,并不是等待在锁池周围,而是发现上锁后就直接退回到就绪状态,等待调度,最终多次调度让对象上锁的对象让其将运行内容结束后,将对象解锁后回到就行状态,再次等待CPU的调度。
4.多线程卖票小程序
package GoodsTrade.thread;
/**
* 创建线程
*/
public class ThreadDemo {
public static void main(String[] args) {
Tickets tickets = new Tickets(100); //定义对象时,需要传入票数以便继续下面买票的操作
Operate operate = new Operate(tickets);
new Thread(() -> {
while(tickets.getNum() > 0) {
operate.sale();
}
}, "窗口1").start();
new Thread(()->{
while(tickets.getNum() > 0) {
operate.sale();
}
}, "窗口2").start();
new Thread(()->{
while(tickets.getNum() > 0) {
operate.sale();
}
}, "窗口3").start();
}
}
package GoodsTrade.thread;
/**
* 资源类
*/
public class Tickets {
private int num;
public Tickets() {
}
public Tickets(int num) {
this.num = num;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
package GoodsTrade.thread;
/**
* 操作类,实现票的售出
*/
public class Operate implements Runnable{
private Tickets tickets;
//如果是int num = tickets.getNum(),只是一个只拷贝,而不是改变对象的属性
public Operate(Tickets tickets) {
this.tickets = tickets;
}
@Override
public void run() {
sale();
}
public void sale() {
synchronized(this) {
if(tickets.getNum() > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(tickets.getNum() > 0) {
int num = tickets.getNum();
System.out.println(Thread.currentThread().getName()
+ " 正在售卖第 " + num + " 张票,剩余:" + (--num));
tickets.setNum(num);
}
}
}
}
}
//运行结果
/*
窗口1 正在售卖第 100 张票,剩余:99
窗口1 正在售卖第 99 张票,剩余:98
窗口1 正在售卖第 98 张票,剩余:97
窗口1 正在售卖第 97 张票,剩余:96
窗口1 正在售卖第 96 张票,剩余:95
窗口1 正在售卖第 95 张票,剩余:94
窗口1 正在售卖第 94 张票,剩余:93
窗口1 正在售卖第 93 张票,剩余:92
窗口1 正在售卖第 92 张票,剩余:91
窗口1 正在售卖第 91 张票,剩余:90
窗口1 正在售卖第 90 张票,剩余:89
窗口1 正在售卖第 89 张票,剩余:88
窗口1 正在售卖第 88 张票,剩余:87
窗口1 正在售卖第 87 张票,剩余:86
窗口1 正在售卖第 86 张票,剩余:85
窗口1 正在售卖第 85 张票,剩余:84
窗口1 正在售卖第 84 张票,剩余:83
窗口1 正在售卖第 83 张票,剩余:82
窗口1 正在售卖第 82 张票,剩余:81
窗口1 正在售卖第 81 张票,剩余:80
窗口1 正在售卖第 80 张票,剩余:79
窗口1 正在售卖第 79 张票,剩余:78
窗口1 正在售卖第 78 张票,剩余:77
窗口1 正在售卖第 77 张票,剩余:76
窗口1 正在售卖第 76 张票,剩余:75
窗口1 正在售卖第 75 张票,剩余:74
窗口1 正在售卖第 74 张票,剩余:73
窗口1 正在售卖第 73 张票,剩余:72
窗口1 正在售卖第 72 张票,剩余:71
窗口1 正在售卖第 71 张票,剩余:70
窗口1 正在售卖第 70 张票,剩余:69
窗口1 正在售卖第 69 张票,剩余:68
窗口1 正在售卖第 68 张票,剩余:67
窗口1 正在售卖第 67 张票,剩余:66
窗口1 正在售卖第 66 张票,剩余:65
窗口1 正在售卖第 65 张票,剩余:64
窗口1 正在售卖第 64 张票,剩余:63
窗口1 正在售卖第 63 张票,剩余:62
窗口1 正在售卖第 62 张票,剩余:61
窗口1 正在售卖第 61 张票,剩余:60
窗口1 正在售卖第 60 张票,剩余:59
窗口1 正在售卖第 59 张票,剩余:58
窗口1 正在售卖第 58 张票,剩余:57
窗口1 正在售卖第 57 张票,剩余:56
窗口1 正在售卖第 56 张票,剩余:55
窗口1 正在售卖第 55 张票,剩余:54
窗口1 正在售卖第 54 张票,剩余:53
窗口1 正在售卖第 53 张票,剩余:52
窗口1 正在售卖第 52 张票,剩余:51
窗口3 正在售卖第 51 张票,剩余:50
窗口3 正在售卖第 50 张票,剩余:49
窗口3 正在售卖第 49 张票,剩余:48
窗口3 正在售卖第 48 张票,剩余:47
窗口3 正在售卖第 47 张票,剩余:46
窗口3 正在售卖第 46 张票,剩余:45
窗口3 正在售卖第 45 张票,剩余:44
窗口3 正在售卖第 44 张票,剩余:43
窗口3 正在售卖第 43 张票,剩余:42
窗口3 正在售卖第 42 张票,剩余:41
窗口3 正在售卖第 41 张票,剩余:40
窗口3 正在售卖第 40 张票,剩余:39
窗口3 正在售卖第 39 张票,剩余:38
窗口3 正在售卖第 38 张票,剩余:37
窗口3 正在售卖第 37 张票,剩余:36
窗口3 正在售卖第 36 张票,剩余:35
窗口3 正在售卖第 35 张票,剩余:34
窗口3 正在售卖第 34 张票,剩余:33
窗口3 正在售卖第 33 张票,剩余:32
窗口3 正在售卖第 32 张票,剩余:31
窗口3 正在售卖第 31 张票,剩余:30
窗口3 正在售卖第 30 张票,剩余:29
窗口3 正在售卖第 29 张票,剩余:28
窗口3 正在售卖第 28 张票,剩余:27
窗口3 正在售卖第 27 张票,剩余:26
窗口3 正在售卖第 26 张票,剩余:25
窗口3 正在售卖第 25 张票,剩余:24
窗口3 正在售卖第 24 张票,剩余:23
窗口3 正在售卖第 23 张票,剩余:22
窗口3 正在售卖第 22 张票,剩余:21
窗口3 正在售卖第 21 张票,剩余:20
窗口3 正在售卖第 20 张票,剩余:19
窗口3 正在售卖第 19 张票,剩余:18
窗口2 正在售卖第 18 张票,剩余:17
窗口2 正在售卖第 17 张票,剩余:16
窗口2 正在售卖第 16 张票,剩余:15
窗口2 正在售卖第 15 张票,剩余:14
窗口2 正在售卖第 14 张票,剩余:13
窗口2 正在售卖第 13 张票,剩余:12
窗口2 正在售卖第 12 张票,剩余:11
窗口2 正在售卖第 11 张票,剩余:10
窗口2 正在售卖第 10 张票,剩余:9
窗口2 正在售卖第 9 张票,剩余:8
窗口2 正在售卖第 8 张票,剩余:7
窗口2 正在售卖第 7 张票,剩余:6
窗口2 正在售卖第 6 张票,剩余:5
窗口2 正在售卖第 5 张票,剩余:4
窗口2 正在售卖第 4 张票,剩余:3
窗口2 正在售卖第 3 张票,剩余:2
窗口2 正在售卖第 2 张票,剩余:1
窗口2 正在售卖第 1 张票,剩余:0
*/
剩余:39
窗口3 正在售卖第 39 张票,剩余:38
窗口3 正在售卖第 38 张票,剩余:37
窗口3 正在售卖第 37 张票,剩余:36
窗口3 正在售卖第 36 张票,剩余:35
窗口3 正在售卖第 35 张票,剩余:34
窗口3 正在售卖第 34 张票,剩余:33
窗口3 正在售卖第 33 张票,剩余:32
窗口3 正在售卖第 32 张票,剩余:31
窗口3 正在售卖第 31 张票,剩余:30
窗口3 正在售卖第 30 张票,剩余:29
窗口3 正在售卖第 29 张票,剩余:28
窗口3 正在售卖第 28 张票,剩余:27
窗口3 正在售卖第 27 张票,剩余:26
窗口3 正在售卖第 26 张票,剩余:25
窗口3 正在售卖第 25 张票,剩余:24
窗口3 正在售卖第 24 张票,剩余:23
窗口3 正在售卖第 23 张票,剩余:22
窗口3 正在售卖第 22 张票,剩余:21
窗口3 正在售卖第 21 张票,剩余:20
窗口3 正在售卖第 20 张票,剩余:19
窗口3 正在售卖第 19 张票,剩余:18
窗口2 正在售卖第 18 张票,剩余:17
窗口2 正在售卖第 17 张票,剩余:16
窗口2 正在售卖第 16 张票,剩余:15
窗口2 正在售卖第 15 张票,剩余:14
窗口2 正在售卖第 14 张票,剩余:13
窗口2 正在售卖第 13 张票,剩余:12
窗口2 正在售卖第 12 张票,剩余:11
窗口2 正在售卖第 11 张票,剩余:10
窗口2 正在售卖第 10 张票,剩余:9
窗口2 正在售卖第 9 张票,剩余:8
窗口2 正在售卖第 8 张票,剩余:7
窗口2 正在售卖第 7 张票,剩余:6
窗口2 正在售卖第 6 张票,剩余:5
窗口2 正在售卖第 5 张票,剩余:4
窗口2 正在售卖第 4 张票,剩余:3
窗口2 正在售卖第 3 张票,剩余:2
窗口2 正在售卖第 2 张票,剩余:1
窗口2 正在售卖第 1 张票,剩余:0
*/