多线程(补全)

一、多线程基础

1.进程与线程

线程是进程内一个相对独立的、可调度的执行单元,又称为轻量级进程。和进程类似,线程也有运行、就绪、阻塞等状态。线程必须拥有父进程。系统没有为线程分配资源,它与进程中的其他线程共享该进程的共享资源。同一进程的不同线程共享相同的地址空间,它们可以访问相同的变量和对象,实现线程间的信息共享。线程间的通信简单而有效,上下文切换非常快。

进程相当于一个应用程序(以QQ为例),进程间不能通信(因为内存不共享),QQ中的每一个聊天窗口便是线程。

2.多线程的好处

多线程开启时,start()让jvm重新开辟了一个新的栈,并run()方法压栈,此时线程处于就绪状态,cpu随机挑选一个栈分配执行时间,执行完毕后再次到就绪状态,所以,多线程的本质仍然是顺序栈执行

多线程程序是指一个程序中包含多个执行流,它是实现并发机制的有效手段。从逻辑的观点看,多线程意味着一个程序的多个语句快同时执行,但不等于多次启动一个程序

根本的好处是:“线程操作资源类”

引入多线程设计原因

  1. 某些应用具有内在的多控制流结构,这些控制流具有合作性质,需要共享内容。采用多线程易于对问题建模
  2. 在需要多控制流的应用中,多线程比多进程在速度上具有绝对优势。统计表明,线程的建立速度约比进程的建立速度快100倍
  3. 采用多线程可以提高处理机与设备之间的并行性
  4. 在多处理机的硬件环境中,多线程可并行执行,从而可提高资源利用率和进程推进速度。

3.run()和start()方法

  1. run()方法是新线程启动的前提,start()方法是新线程启动的标志
  2. 当调用start()时,会自动告诉jvm机,此时我们需要开辟新的栈空间
  3. 当开辟完新的栈空间后,start()方法会自动弹栈,在新栈中,run()方法压栈
  4. 如果我们直接调用新对象的run()方法,此时不会造成多线程并发,因为此时相当于代码由上至下的顺序执行
  5. 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.阻塞状态(*)

阻塞状态其实是睡眠、资源阻塞、等待三种状态的结合体,此时线程仍是活的,只是缺乏运行的条件。

引起阻塞状态的因素:

  1. 睡眠:线程调用sleep()方法睡眠一段时间
  2. 资源阻塞:线程在等待一种资源
  3. 等待:调用了wait()方法使线程挂起,直到线程得到notify()或notifyAll()消息

阻塞状态时,会让出cpu,让其他保持就绪状态的线程获得可能的执行时间(cpu时间片)

5.死亡状态

如果线程的run()方法执行完毕,线程正常结束;或者线程执行过程中抛出一个未捕获的异常或错误,线程异常结束。结束后,线程处于死亡状态

一旦线程死亡,就不能复生。如果死亡状态任然调用start()方法,会出现运行时异常IllegalThreadException

5.多线程数据安全问题

数据安全问题的出现因为存在着原子性。

本质不是多线程导致了资源争抢,而是打破了方法执行的完整性,导致最后的“读脏数据、重复读数据”等的问题。因此使用到synchronized同步关键字解决数据安全问题


二、多线程引例

1.多线程步骤

构建Thread子类对象引发多线程的步骤如下:

  1. 构建Thread子类,重写其中的run()方法
  2. 创建线程对象
  3. 线程对象调用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.程序改进

由上述的分析,发现程序中存在以下不足:

  1. 需要每个子类都需要继承Thread,而当项目需求提高,要求我们抽象出一个父类时,由于java的单继承原则,不能很好的完成任务
  2. 会出现不同的线程出现矛盾,线程不同步
  3. 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的地方有了原子性

  1. 放在方法上 e.g: public synchronized void sell() {}
    此时并不是表示将这个方法锁住,可以想象上厕所锁的不是上厕所这个方法,而锁的是厕所门。
    相当于知道方法结束才开锁,锁住的是一个实体(entry)

  2. 放在方法内的第一句
    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
*/




  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值