Java高并发编程(一)

快速认识线程

在计算机的世界中,当我们在讨论并行的时候,实际上是指,一系列的任务在计算机中同时运行,比如说我们在浏览网页的时候还能打开音乐播放器,当我们在撰写邮件的时候,收件箱还能接收新的邮件。在单CPU的计算机中,其实并没有真正的并行,它只不过是CPU时间钟快速轮转带给你的错觉,而这种错觉让你产生了它们是在同一时刻同时运行的。当然,如果是多核CPU,那么并行运行还是真实存在的。

什么是线程

这是从百度上面摘抄下来的线程的概念:

  • 线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
    线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
  • 同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
    一个进程可以有很多线程,每条线程并行执行不同的任务。
  • 在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。

  • 现在的操作系统都是支持多任务的执行,对于计算机来说每一个任务就是一个进程,在每一个进程内部至少要有一个线程是在运行的,有的时候,线程也被称为轻量级的进程。
  • 线程是程序执行的一个路径,每一个线程都有自己的局部变量表、程序计数器(指向正在执行的指令指针)以及各自的生命周期,现代的操作系统中一般不止一个线程在运行,当启动了一个Java虚拟机(JVM)时,从操作系统开始就会创建一个新的进程(JVM进程),JVM进程中将会派生或者创建很多的线程。

创建和启动线程

代码如下:此时是先后执行,不能同时的去做处理

public class TryConcurrency {

    public static void main(String[] args) {
        readFromDatabase();
        writeDataToFile();
    }

    private static void readFromDatabase() {
        //read data from database and handle it
        try {
            println("Begin read data from db.");
            Thread.sleep(1000*10L);
            println("Read data done and start handle it.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        println("The data handle finish and successfully.");
    }

    private static void writeDataToFile() {
        //read data from database and handle it
        try {
            println("Begin write data to file.");
            Thread.sleep(1000*10L);
            println("Write data done and start handle it.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        println("The data handle finish and successfully.");
    }

    private static void println(String message) {
        System.out.println(message);
    }
}

运行结果为:两个方法是先后执行的,并不是同时运行的

Begin read data from db.
Read data done and start handle it.
The data handle finish and successfully.
Begin write data to file.
Write data done and start handle it.
The data handle finish and successfully.

而此时,如果我们想要它们能同时运行的话,我们可以这样来写:

public class TryConcurrency {

    public static void main(String[] args) {
        new Thread(TryConcurrency::readFromDatabase).start();
        writeDataToFile();
    }

    private static void readFromDatabase() {
        //read data from database and handle it
        try {
            println("Begin read data from db.");
            Thread.sleep(1000*10L);
            println("Read data done and start handle it.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        println("The data handle finish and successfully.");
    }

    private static void writeDataToFile() {
        //read data from database and handle it
        try {
            println("Begin write data to file.");
            Thread.sleep(1000*10L);
            println("Write data done and start handle it.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        println("The data handle finish and successfully.");
    }

    private static void println(String message) {
        System.out.println(message);
    }
}

此时的运行结果为:此时两个方法就是并行的

Begin write data to file.
Begin read data from db.
Write data done and start handle it.
Read data done and start handle it.
The data handle finish and successfully.
The data handle finish and successfully.


线程的生命周期详解

下图为线程的生命周期:
线程的生命周期
线程的生命周期大体可以分为5个主要阶段:

  1. 线程的new状态:
    当我们用关键字new创建一个Thread对象时,此时它并不处于执行状态,因为还没有调用start方法启动该线程,在还没有调用start方法之前,该线程根本不存在,和用new关键字创建普通对象没有什么区别。当调用了start方法后则进入Runnable状态。
  2. 线程的Runnable状态(准备状态)
    线程对象在进入到Runnable状态就必须要调用start方法,那么此时才是真正的在JVM进程中创建了一个线程,那么线程在一经启动就可以立即执行吗?答案是否定的,线程的是否执行和进程一样得听令于CPU的调度,那么我们把这个中间状态称为可执行状态(Runnable),也就是说它具备可执行的资格,但是并没有真正的被执行起来,而是在等待CPU的调度。
  3. 线程的Running状态(运行状态)
    一旦当进入到Runnable状态的线程得到了CPU的调度,那么该线程则进入到了Running状态。在Running状态下可以发生如下的状态转换:
    • 直接进入Terminated状态,比如说调用了jdk已经不推荐使用的stop方法或者判断某个逻辑标识
    • 进入Blocked(阻塞)状态,比如说调用了sleep或者wait方法,从而进入到waitSet(等待队列)中
    • 进行某一个阻塞的IO操作,比如说因为网络数据的读写而进入到了阻塞状态
    • 获取某一个锁资源,从而进入到阻塞队列中,进入阻塞状态
    • 由于CPU的调度器轮询使该线程放弃执行,进入到Runnable(准备状态)
    • 线程主动调用yield方法,放弃CPU执行权,进入到Runnable(准备状态)
  4. 线程的Blocked状态(阻塞状态)
    进入到阻塞状态,可以进行如下的状态转换
    • 直接进入Terminated状态,比如说调用了jdk已经不推荐使用的stop方法或者意外死亡(JVM Crash)
    • 线程阻塞的操作结束,进入到Runnable状态
    • 线程完成了指定的休眠时间,进入到Runnable状态
    • wait中的线程被其他线程notify/notifyall唤醒,进入到Runnable状态
    • 线程获取到某一个锁资源,进入到Runnable状态
    • 线程在阻塞过程中被打断,比如其他线程调用了interrupt方法,进入到Runnable状态
  5. 线程的Teminated状态
    Teminated是线程的一个最终状态,在该状态中的线程不会切换到其他的任何状态,线程进入到Teminated状态,则意味着该线程的整个生命周期结束了,下面几种情况会进入到该状态:
    • 线程运行正常结束,结束整个生命周期
    • 线程运行出错意外结束
    • JVM Crash,导致所有的线程都结束

线程的start方法剖析

Thread类中的start方法源码如下:

    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

从源码,我们可以看出一个线程不可以调用start()方法两次,否则则会抛出IllegalThreadStateException异常;
实际上,Thread类中的start方法和run方法是一个模板方法设计模式的实现;


总结:

  • Java应用程序中的main函数是一个线程,就被JVM启动的时候调用,线程的名字叫main
  • 实现一个线程,必须要创建Thread类的实例,重写run方法,并且调用start方法
  • 在JVM启动后,实际上有多个线程,但是至少有一个非守护线程
  • 当你调用一个线程start方法的时候,此时至少有两个线程,一个是调用你的线程,还有一个是执行run方法的线程
  • 线程的生命周期分为new、runnable、running、block、terminate

Thread实战案例

用Thread模拟在银行排队办理业务的场景:假设有3个窗口,我们用3个线程去模拟叫号的过程;


现在是一个柜台的情况:

public class TicketWindow extends Thread{
    /**
     * 柜台的名字
     */
    private final String name;

    /**
     * 最大的叫号数
     */
    private final int MAX = 50;

    /**
     * 被叫的号码数
     */
    private int index = 1;

    public TicketWindow(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        while (index <= MAX) {
            System.out.println("柜台:" + name + ",当前的号码是:" + index++);
        }
    }
}

我们启动线程:

public class Blank {
    public static void main(String[] args) {
        TicketWindow ticketWindow = new TicketWindow("一号柜台");
        ticketWindow.start();
    }
}

运行的结果如下:

柜台:一号柜台,当前的号码是:1
柜台:一号柜台,当前的号码是:2
柜台:一号柜台,当前的号码是:3
柜台:一号柜台,当前的号码是:4
柜台:一号柜台,当前的号码是:5
柜台:一号柜台,当前的号码是:6
柜台:一号柜台,当前的号码是:7
柜台:一号柜台,当前的号码是:8
柜台:一号柜台,当前的号码是:9
柜台:一号柜台,当前的号码是:10
柜台:一号柜台,当前的号码是:11
柜台:一号柜台,当前的号码是:12
柜台:一号柜台,当前的号码是:13
柜台:一号柜台,当前的号码是:14
柜台:一号柜台,当前的号码是:15
柜台:一号柜台,当前的号码是:16
柜台:一号柜台,当前的号码是:17
柜台:一号柜台,当前的号码是:18
柜台:一号柜台,当前的号码是:19
柜台:一号柜台,当前的号码是:20
柜台:一号柜台,当前的号码是:21
柜台:一号柜台,当前的号码是:22
柜台:一号柜台,当前的号码是:23
柜台:一号柜台,当前的号码是:24
柜台:一号柜台,当前的号码是:25
柜台:一号柜台,当前的号码是:26
柜台:一号柜台,当前的号码是:27
柜台:一号柜台,当前的号码是:28
柜台:一号柜台,当前的号码是:29
柜台:一号柜台,当前的号码是:30
柜台:一号柜台,当前的号码是:31
柜台:一号柜台,当前的号码是:32
柜台:一号柜台,当前的号码是:33
柜台:一号柜台,当前的号码是:34
柜台:一号柜台,当前的号码是:35
柜台:一号柜台,当前的号码是:36
柜台:一号柜台,当前的号码是:37
柜台:一号柜台,当前的号码是:38
柜台:一号柜台,当前的号码是:39
柜台:一号柜台,当前的号码是:40
柜台:一号柜台,当前的号码是:41
柜台:一号柜台,当前的号码是:42
柜台:一号柜台,当前的号码是:43
柜台:一号柜台,当前的号码是:44
柜台:一号柜台,当前的号码是:45
柜台:一号柜台,当前的号码是:46
柜台:一号柜台,当前的号码是:47
柜台:一号柜台,当前的号码是:48
柜台:一号柜台,当前的号码是:49
柜台:一号柜台,当前的号码是:50


现在,我们来模拟3个柜台的叫号过程:

public class TicketWindow extends Thread{
    /**
     * 柜台的名字
     */
    private final String name;

    /**
     * 最大的叫号数
     */
    private final int MAX = 50;

    /**
     * 被叫的号码数
     */
    private int index = 1;

    public TicketWindow(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        while (index <= MAX) {
            System.out.println("柜台:" + name + ",当前的号码是:" + index++);
        }
    }
}

现在,我们启动3个线程:

public class Blank {
    public static void main(String[] args) {
        TicketWindow ticketWindow1 = new TicketWindow("一号柜台");
        ticketWindow1.start();

        TicketWindow ticketWindow2 = new TicketWindow("二号柜台");
        ticketWindow2.start();

        TicketWindow ticketWindow3 = new TicketWindow("三号柜台");
        ticketWindow3.start();
    }
}

执行的结果如下:

柜台:一号柜台,当前的号码是:1
柜台:一号柜台,当前的号码是:2
柜台:一号柜台,当前的号码是:3
柜台:一号柜台,当前的号码是:4
柜台:一号柜台,当前的号码是:5
柜台:二号柜台,当前的号码是:1
柜台:二号柜台,当前的号码是:2
柜台:一号柜台,当前的号码是:6
柜台:二号柜台,当前的号码是:3
柜台:三号柜台,当前的号码是:1
柜台:二号柜台,当前的号码是:4
柜台:一号柜台,当前的号码是:7
柜台:二号柜台,当前的号码是:5
柜台:二号柜台,当前的号码是:6
柜台:二号柜台,当前的号码是:7
柜台:二号柜台,当前的号码是:8
柜台:二号柜台,当前的号码是:9
柜台:二号柜台,当前的号码是:10
柜台:二号柜台,当前的号码是:11
柜台:二号柜台,当前的号码是:12
柜台:二号柜台,当前的号码是:13
柜台:二号柜台,当前的号码是:14
柜台:二号柜台,当前的号码是:15
柜台:二号柜台,当前的号码是:16
柜台:二号柜台,当前的号码是:17
柜台:二号柜台,当前的号码是:18
柜台:二号柜台,当前的号码是:19
柜台:二号柜台,当前的号码是:20
柜台:二号柜台,当前的号码是:21
柜台:二号柜台,当前的号码是:22
柜台:二号柜台,当前的号码是:23
柜台:二号柜台,当前的号码是:24
柜台:二号柜台,当前的号码是:25
柜台:二号柜台,当前的号码是:26
柜台:二号柜台,当前的号码是:27
柜台:二号柜台,当前的号码是:28
柜台:二号柜台,当前的号码是:29
柜台:二号柜台,当前的号码是:30
柜台:二号柜台,当前的号码是:31
柜台:二号柜台,当前的号码是:32
柜台:二号柜台,当前的号码是:33
柜台:二号柜台,当前的号码是:34
柜台:二号柜台,当前的号码是:35
柜台:三号柜台,当前的号码是:2
柜台:三号柜台,当前的号码是:3
柜台:三号柜台,当前的号码是:4
柜台:二号柜台,当前的号码是:36
柜台:二号柜台,当前的号码是:37
柜台:二号柜台,当前的号码是:38
柜台:二号柜台,当前的号码是:39
柜台:二号柜台,当前的号码是:40
柜台:二号柜台,当前的号码是:41
柜台:二号柜台,当前的号码是:42
柜台:二号柜台,当前的号码是:43
柜台:二号柜台,当前的号码是:44
柜台:二号柜台,当前的号码是:45
柜台:二号柜台,当前的号码是:46
柜台:二号柜台,当前的号码是:47
柜台:二号柜台,当前的号码是:48
柜台:二号柜台,当前的号码是:49
柜台:二号柜台,当前的号码是:50
柜台:一号柜台,当前的号码是:8
柜台:一号柜台,当前的号码是:9
柜台:一号柜台,当前的号码是:10
柜台:一号柜台,当前的号码是:11
柜台:一号柜台,当前的号码是:12
柜台:一号柜台,当前的号码是:13
柜台:一号柜台,当前的号码是:14
柜台:一号柜台,当前的号码是:15
柜台:一号柜台,当前的号码是:16
柜台:一号柜台,当前的号码是:17
柜台:一号柜台,当前的号码是:18
柜台:一号柜台,当前的号码是:19
柜台:一号柜台,当前的号码是:20
柜台:一号柜台,当前的号码是:21
柜台:一号柜台,当前的号码是:22
柜台:一号柜台,当前的号码是:23
柜台:一号柜台,当前的号码是:24
柜台:一号柜台,当前的号码是:25
柜台:一号柜台,当前的号码是:26
柜台:一号柜台,当前的号码是:27
柜台:一号柜台,当前的号码是:28
柜台:一号柜台,当前的号码是:29
柜台:一号柜台,当前的号码是:30
柜台:一号柜台,当前的号码是:31
柜台:一号柜台,当前的号码是:32
柜台:一号柜台,当前的号码是:33
柜台:一号柜台,当前的号码是:34
柜台:一号柜台,当前的号码是:35
柜台:一号柜台,当前的号码是:36
柜台:一号柜台,当前的号码是:37
柜台:一号柜台,当前的号码是:38
柜台:一号柜台,当前的号码是:39
柜台:一号柜台,当前的号码是:40
柜台:一号柜台,当前的号码是:41
柜台:一号柜台,当前的号码是:42
柜台:一号柜台,当前的号码是:43
柜台:一号柜台,当前的号码是:44
柜台:一号柜台,当前的号码是:45
柜台:一号柜台,当前的号码是:46
柜台:一号柜台,当前的号码是:47
柜台:一号柜台,当前的号码是:48
柜台:一号柜台,当前的号码是:49
柜台:一号柜台,当前的号码是:50
柜台:三号柜台,当前的号码是:5
柜台:三号柜台,当前的号码是:6
柜台:三号柜台,当前的号码是:7
柜台:三号柜台,当前的号码是:8
柜台:三号柜台,当前的号码是:9
柜台:三号柜台,当前的号码是:10
柜台:三号柜台,当前的号码是:11
柜台:三号柜台,当前的号码是:12
柜台:三号柜台,当前的号码是:13
柜台:三号柜台,当前的号码是:14
柜台:三号柜台,当前的号码是:15
柜台:三号柜台,当前的号码是:16
柜台:三号柜台,当前的号码是:17
柜台:三号柜台,当前的号码是:18
柜台:三号柜台,当前的号码是:19
柜台:三号柜台,当前的号码是:20
柜台:三号柜台,当前的号码是:21
柜台:三号柜台,当前的号码是:22
柜台:三号柜台,当前的号码是:23
柜台:三号柜台,当前的号码是:24
柜台:三号柜台,当前的号码是:25
柜台:三号柜台,当前的号码是:26
柜台:三号柜台,当前的号码是:27
柜台:三号柜台,当前的号码是:28
柜台:三号柜台,当前的号码是:29
柜台:三号柜台,当前的号码是:30
柜台:三号柜台,当前的号码是:31
柜台:三号柜台,当前的号码是:32
柜台:三号柜台,当前的号码是:33
柜台:三号柜台,当前的号码是:34
柜台:三号柜台,当前的号码是:35
柜台:三号柜台,当前的号码是:36
柜台:三号柜台,当前的号码是:37
柜台:三号柜台,当前的号码是:38
柜台:三号柜台,当前的号码是:39
柜台:三号柜台,当前的号码是:40
柜台:三号柜台,当前的号码是:41
柜台:三号柜台,当前的号码是:42
柜台:三号柜台,当前的号码是:43
柜台:三号柜台,当前的号码是:44
柜台:三号柜台,当前的号码是:45
柜台:三号柜台,当前的号码是:46
柜台:三号柜台,当前的号码是:47
柜台:三号柜台,当前的号码是:48
柜台:三号柜台,当前的号码是:49
柜台:三号柜台,当前的号码是:50

上面的这种方式显然是不合理的,每一个线程都从1叫到了50


我们再来进行改造一下:为共享变量添加static关键字

public class TicketWindow extends Thread{
    /**
     * 柜台的名字
     */
    private final String name;

    /**
     * 最大的叫号数
     */
    private static final int MAX = 50;

    /**
     * 被叫的号码数
     */
    private static int index = 1;

    public TicketWindow(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        while (index <= MAX) {
            System.out.println("柜台:" + name + ",当前的号码是:" + index++);
        }
    }
}

我们再来运行这3个线程看看运行结果:

public class Blank {
    public static void main(String[] args) {
        TicketWindow ticketWindow1 = new TicketWindow("一号柜台");
        ticketWindow1.start();

        TicketWindow ticketWindow2 = new TicketWindow("二号柜台");
        ticketWindow2.start();

        TicketWindow ticketWindow3 = new TicketWindow("三号柜台");
        ticketWindow3.start();
    }
}

此时的运行结果为:

柜台:一号柜台,当前的号码是:1
柜台:二号柜台,当前的号码是:2
柜台:二号柜台,当前的号码是:4
柜台:一号柜台,当前的号码是:3
柜台:一号柜台,当前的号码是:6
柜台:一号柜台,当前的号码是:7
柜台:二号柜台,当前的号码是:5
柜台:二号柜台,当前的号码是:10
柜台:二号柜台,当前的号码是:11
柜台:一号柜台,当前的号码是:9
柜台:三号柜台,当前的号码是:8
柜台:一号柜台,当前的号码是:13
柜台:二号柜台,当前的号码是:12
柜台:二号柜台,当前的号码是:16
柜台:二号柜台,当前的号码是:17
柜台:二号柜台,当前的号码是:18
柜台:二号柜台,当前的号码是:19
柜台:二号柜台,当前的号码是:20
柜台:二号柜台,当前的号码是:21
柜台:二号柜台,当前的号码是:22
柜台:二号柜台,当前的号码是:23
柜台:二号柜台,当前的号码是:24
柜台:二号柜台,当前的号码是:25
柜台:二号柜台,当前的号码是:26
柜台:二号柜台,当前的号码是:27
柜台:二号柜台,当前的号码是:28
柜台:二号柜台,当前的号码是:29
柜台:二号柜台,当前的号码是:30
柜台:二号柜台,当前的号码是:31
柜台:二号柜台,当前的号码是:32
柜台:二号柜台,当前的号码是:33
柜台:二号柜台,当前的号码是:34
柜台:二号柜台,当前的号码是:35
柜台:二号柜台,当前的号码是:36
柜台:二号柜台,当前的号码是:37
柜台:二号柜台,当前的号码是:38
柜台:二号柜台,当前的号码是:39
柜台:二号柜台,当前的号码是:40
柜台:二号柜台,当前的号码是:41
柜台:二号柜台,当前的号码是:42
柜台:二号柜台,当前的号码是:43
柜台:二号柜台,当前的号码是:44
柜台:二号柜台,当前的号码是:45
柜台:二号柜台,当前的号码是:46
柜台:二号柜台,当前的号码是:47
柜台:二号柜台,当前的号码是:48
柜台:二号柜台,当前的号码是:49
柜台:二号柜台,当前的号码是:50
柜台:一号柜台,当前的号码是:15
柜台:三号柜台,当前的号码是:14


接下来,我们用Runnable接口将线程中的执行逻辑执行单元从控制中抽取出来:让多个线程共用这个Runnable的实例

public class TicketWindowRunnable implements Runnable {

    /**
     * 最大的叫号数
     */
    private final int MAX = 50;

    /**
     * 被叫的号码数
     */
    private int index = 1;

    @Override
    public void run() {
        while (index <= MAX) {
            System.out.println(Thread.currentThread().getName() + "的号码是:" + index++);
        }
    }
}

调用start方法来启动线程:

public class Blank2 {
    public static void main(String[] args) {
        final TicketWindowRunnable ticketWindowRunnable = new TicketWindowRunnable();
        Thread windowThread1 = new Thread(ticketWindowRunnable, "一号柜台");
        Thread windowThread2 = new Thread(ticketWindowRunnable, "二号柜台");
        Thread windowThread3 = new Thread(ticketWindowRunnable, "三号柜台");
        windowThread1.start();
        windowThread2.start();
        windowThread3.start();
    }
}

执行结果如下:虽然只有50号码,但是其实在下面的执行结果是有线程安全问题的,号码为1的出现过了两次

一号柜台的号码是:1
三号柜台的号码是:2
二号柜台的号码是:1
三号柜台的号码是:4
三号柜台的号码是:6
一号柜台的号码是:3
三号柜台的号码是:7
二号柜台的号码是:5
三号柜台的号码是:9
一号柜台的号码是:8
三号柜台的号码是:11
二号柜台的号码是:10
三号柜台的号码是:13
一号柜台的号码是:12
三号柜台的号码是:15
二号柜台的号码是:14
三号柜台的号码是:17
一号柜台的号码是:16
三号柜台的号码是:19
二号柜台的号码是:18
三号柜台的号码是:21
一号柜台的号码是:20
三号柜台的号码是:23
二号柜台的号码是:22
三号柜台的号码是:25
一号柜台的号码是:24
三号柜台的号码是:27
二号柜台的号码是:26
三号柜台的号码是:29
二号柜台的号码是:30
一号柜台的号码是:28
二号柜台的号码是:32
三号柜台的号码是:31
三号柜台的号码是:35
二号柜台的号码是:34
一号柜台的号码是:33
一号柜台的号码是:38
一号柜台的号码是:39
一号柜台的号码是:40
二号柜台的号码是:37
三号柜台的号码是:36
二号柜台的号码是:42
一号柜台的号码是:41
二号柜台的号码是:44
三号柜台的号码是:43
二号柜台的号码是:46
一号柜台的号码是:45
二号柜台的号码是:48
三号柜台的号码是:47
二号柜台的号码是:50
一号柜台的号码是:49


策略模式在Runnable和Thread中的应用
策略模式


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值