Day10

1.多线程

1.1 相关概念

1.1.1 程序、进程和线程

  1. 程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码
  2. 进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。如:运行中的 QQ,运行中的网易音乐播放器。
  • 每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建、运行到消亡的过程。(生命周期)
  • 程序是静态的,进程是动态的。
  • 进程作为操作系统调度和分配资源的最小单位(亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。
  • 现代的操作系统,大都是支持多进程的,支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos 窗口等软件。
  1. 线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。一个进程中至少有一个线程。
  • 一个进程同一时间若并行执行多个线程,就是支持多线程的。
  • 线程作为 CPU 调度和执行的最小单位。
  • 一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

1.1.2 线程调度

  1. 分时调度:所有线程轮流使用 CPU 的使用权,并且平均分配每个线程占用 CPU 的时间。
  2. 抢占式调度:让优先级高的线程以较大的概率优先使用 CPU。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java 使用的为抢占式调度。

1.1.3 多线程程序的优点

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  2. 提高计算机系统 CPU 的利用率。
  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。

1.2 创建和启动线程

  1. Java 语言的 JVM 允许程序运行多个线程,使用 java.lang.Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。
  2. Thread 类的特性:
  • 每个线程都是通过某个特定 Thread 对象的 run()方法来完成操作的,因此把 run()方法体称为线程执行体。
  • 通过该 Thread 对象的 start()方法来启动这个线程,而非直接调用 run()。
  • 要想实现多线程,必须在主线程中创建新的线程对象。

1.2.1 方式1:继承Thread类

Java 通过继承 Thread 类来创建并启动多线程的步骤如下:

  1. 定义 Thread 类的子类,并重写该类的 run()方法,该 run()方法的方法体就代表了线程需要完成的任务。
  2. 创建 Thread 子类的实例,即创建了线程对象。
  3. 调用线程对象的 start()方法来启动该线程。其目的是①启动线程;②调用当前线程的run()方法。
public class Thread1 extends Thread{
    public static void main(String[] args) {
        PrintNumber t1 = new PrintNumber();
        t1.start();
    }
}

class PrintNumber extends Thread{
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0){
                System.out.println(i);
            }
        }
    }
}

1.2.2 方式2:实现 Runnable 接口

Java 有单继承的限制,当我们无法继承 Thread 类时,那么该如何做呢?在核心类库中提供了 Runnable 接口,我们可以实现 Runnable 接口,重写 run()方法,然后再通过 Thread 类的对象代理启动和执行我们的线程体 run()方法。
步骤如下:

  1. 定义 Runnable 接口的实现类,并重写该接口的 run()方法,该 run()方法的方法体同样是该线程的线程执行体。
  2. 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 参数来创建Thread 对象,该 Thread 对象才是真正 的线程对象。
  3. 调用线程对象的 start()方法,启动线程。调用 Runnable 接口实现类的 run 方法。
public class Thread2{
    public static void main(String[] args) {
    	//创建当前实现类的对象
        EvenNumberPrint t1 = new EvenNumberPrint();
        //将此对象作为参数传递到Thread类的构造器中,创建Thread类的实例
        Thread thread = new Thread(t1);
        //Thread类的实例调用start()方法
        thread.start();
    }
}

class EvenNumberPrint implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0){
                System.out.println(i);
            }
        }
    }
}

1.2.3 两种方式的对比

  1. 共同点:
  • 启动线程使用的都是Thread类中定义的start()方法。
  • 创建的线程对象,都是Thread类或其子类的实例。
  1. 不同点:一个是类的继承,一个是接口的实现。

建议使用实现Runnable接口的方式。因为①Runnable实现方式避免了类的单继承的局限性;②更适合处理有共享数据的问题;③实现了代码和数据的分离。

1.3 Thread 类的常用结构

1.3.1 构造器

  1. public Thread() :分配一个新的线程对象。
  2. public Thread(String name) :分配一个指定名字的新的线程对象。
  3. public Thread(Runnable target) :指定创建线程的目标对象,它实现了 Runnable 接口中的 run 方法。
  4. public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

1.3.2 常用方法

  1. public void run() :此线程要执行的任务在此处定义代码。
  2. public void start() :导致此线程开始执行; Java 虚拟机调用此线程的 run 方法。
  3. public String getName() :获取当前线程名称。
  4. public void setName(String name):设置该线程名称。
  5. public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在Thread 子类中就是 this,通常用于主线程和 Runnable 实现类。
  6. public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  7. public static void yield():yield 只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这
    个不能保证,完全有可能的情况是,当某个线程调用了 yield 方法暂停之后,线程调度器又将其调度出来重新执行。
  8. void join() :等待该线程终止。
  • void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果 millis 时间到,将不再等待。
  • void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
  1. public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。

1.3.3 线程优先级相关方法

每个线程都有一定的优先级,同优先级线程组成先进先出队列(先到先服务),使用分时调度策略。优先级高的线程采用抢占式策略,获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。Thread 类的三个优先级常量:

  • MAX_PRIORITY(10):最高优先级。
  • MIN _PRIORITY (1):最低优先级。
  • NORM_PRIORITY (5):普通优先级,默认情况下 main 线程具有普通优先级。
  1. public final int getPriority() :返回线程优先级。
  2. public final void setPriority(int newPriority) :改变线程的优先级,范围在[1,10]之间。

1.4 多线程的生命周期

Java 语言使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下一些状态:

1.4.1 JDK5.0之前:5种状态

线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。CPU 需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。
Day10_第1张图片

1.4.2 JDK5.0之后:6种状态

  1. NEW(新建):线程刚被创建,但是并未启动。还没调用 start 方法。
  2. RUNNABLE(可运行):这里没有区分就绪和运行状态。因为对于 Java 对象来说,只能标记为可运行,至于什么时候运行,不是 JVM 来控制的了,是 OS 来进行调度的,而且时间非常短暂,因此对于 Java 对象的状态来说,无法区分。
  3. Teminated(被终止):表明此线程已经结束生命周期,终止运行。
  4. 重点说明,根据 Thread.State 的定义,阻塞状态分为三种:BLOCKED、WAITING、TIMED_WAITING
  • BLOCKED(锁阻塞):在 API 中的介绍为:一个正在阻塞、等待一个监视器锁(锁对象)的线程处于这一状态。只有获得锁对象的线程才能有执行机会。比如,线程 A 与线程 B 代码中使用同一锁,如果线程 A 获取到锁,线程 A 进入到 Runnable 状态,那么线程 B 就进入到 Blocked锁阻塞状态。
  • TIMED_WAITING(计时等待):在 API 中的介绍为:一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。当前线程执行过程中遇到 Thread 类的 sleep 或 join,Object 类的 wait,LockSupport 类的 park 方法,并且在调用这些方法时,设置了时间,那么当前线程会进入 TIMED_WAITING,直到时间到,或被中断。
  • WAITING(无限等待):在 API 中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。当前线程执行过程中遇到遇到 Object 类的 wait,Thread 类的join,LockSupport 类的 park 方法,并且在调用这些方法时,没有指定时间,那么当前线程会进入 WAITING 状态,直到被唤醒。
  • 通过 Object 类的 wait 进入 WAITING 状态的要有 Object 的notify/notifyAll 唤醒;
  • 通过 Condition 的 await 进入 WAITING 状态的要有Condition 的 signal 方法唤醒;
  • 通过 LockSupport 类的 park 方法进入 WAITING 状态的要有LockSupport 类的 unpark 方法唤醒
  • 通过 Thread 类的 join 进入 WAITING 状态,只有调用 join方法的线程对象结束才能让当前线程恢复;
    说明:当从 WAITING 或 TIMED_WAITING 恢复到 Runnable 状态时,如果发现当前线程没有得到监视器锁,那么会立刻转入 BLOCKED 状态。
    Day10_第2张图片

1.5 线程安全问题及解决方案

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。Java使用线程的同步机制来解决线程的安全问题。

1.5.1 同步代码块

synchronized(同步监视器){
	//需要被同步的代码
}
  1. 需要被同步的代码,即为操作共享数据(多个线程需要操作的数据)的代码。
  2. 需要被同步的代码,在被synchronized包裹之后,就使得一个线程在操作这些代码的过程中,其他线程必须等待。
  3. 同步监视器,俗称锁。哪个线程获取了锁,那个线程就能执行需要被同步的代码。
  4. 同步监视器可以使用任何一个类的对象充当。但是多个线程必须共用一个同步监视器。
    注意:
  • 在实现Runnable接口的方式中,同步监视器可以考虑使用:this
  • 在继承Thread类的方式种,同步监视器慎用this,可以考虑使用:当前类.class。
package p136;

public class WindowTest {
    public static void main(String[] args) {
        SaleTicket s = new SaleTicket();
        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);
        Thread t3 = new Thread(s);

        t1.start();
        t2.start();
        t3.start();
    }
}

class SaleTicket implements Runnable {
    int ticket = 100;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

1.5.2 同步方法

如果操作共享数据的代码完整的声明在了一个方法中,那么我们就可以将此方法声明为同步方法即可。

  1. 非静态的同步方法,默认同步监视器是this。
  2. 静态的同步方法,默认同步监视器是当前类本身。
package p136;

public class WindowTest1 {
    public static void main(String[] args) {
        SaleTicket s = new SaleTicket();
        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);
        Thread t3 = new Thread(s);

        t1.start();
        t2.start();
        t3.start();
    }
}

class SaleTicket1 implements Runnable {
    int ticket = 100;
    boolean isFlag = true;

    @Override
    public void run() {
        while (isFlag){
            show();
        }
    }

    public synchronized void show() {	//此时的同步监视器为this
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
            ticket--;
        } else {
            isFlag = false;
        }
    }
}

1.5.3 synchronized的优缺点

  1. 优点:解决了线程的安全问题。
  2. 缺点:在操作共享数据时,多线程其实是串行执行,意味着性能较低。

1.6 单例之懒汉式的线程安全问题

package p138;

public class BankTest {
    static Bank b1 = null;
    static Bank b2 = null;

    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                b1 = Bank.getInstance();
            }
        };

        Thread t2 = new Thread() {
            @Override
            public void run() {
                b2 = Bank.getInstance();
            }
        };

        t1.start();
        t2.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(b1);
        System.out.println(b2);
        System.out.println(b1 == b2);
    }
}

class Bank {
    private Bank() {
    }

    private static volatile Bank instance = null;

    //实现线程安全的方式1
    public static synchronized Bank getInstance() {  //同步监视器默认为Bank.class
        if (instance == null) {
            instance = new Bank();
        }
        return instance;
    }

    //实现线程安全的方式2
    public static Bank getInstance() {  //同步监视器默认为Bank.class
        synchronized (Bank.class) {
            if (instance == null) {
                instance = new Bank();
            }
            return instance;
        }
    }

    //实现线程安全的方式3,相较于方式1和方式2来讲,效率更高。
    public static Bank getInstance() {  //同步监视器默认为Bank.class
        if (instance == null) {
            synchronized (Bank.class) {
                if (instance == null) {
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}
/*
 注意:上述方式 3 中,有指令重排问题
 mem = allocate(); 为单例对象分配内存空间
 instance = mem; instance 引用现在非空,但还未初始化
 ctorSingleton(instance); 为单例对象通过 instance 调用构造器
 从 JDK2 开始,分配空间、初始化、调用构造器会在线程的工作存储区一次性完成,然后复制到主存储区。但是需要volatile 关键字,避免指令重排。
 */

1.7 死锁问题

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

1.7.1 产生死锁的原因

  1. 互斥条件
  2. 占用且等待
  3. 不可抢夺(或不可抢占)
  4. 循环等待
    以上 4 个条件,同时出现就会触发死锁。

1.7.2 解决死锁

死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。

  1. 针对条件 1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
  2. 针对条件 2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
  3. 针对条件 3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
  4. 针对条件 4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

1.8 JDK5.0提供的Lock(锁)的使用

  1. JDK5.0 的新增功能,保证线程的安全。与采用 synchronized 相比,Lock 可提供多种锁方案,更灵活、更强大。Lock 通过显式定义同步锁对象来实现同步。同步锁使用Lock 对象充当。
  2. java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。
  3. 在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。ReentrantLock 类实现了 Lock 接口,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。
  4. Lock 锁也称同步锁,加锁与释放锁方法,如下:
  • public void lock() :加同步锁。
  • public void unlock() :释放同步锁。
  1. synchronized和lock的对比:
  • synchronized不管是同步代码块还是同步方法,都需要在结束一对{}之后,释放对同步监视器的调用。
  • Lock是通过两个方法控制需要被同步的代码,更灵活一些。
  • Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。
class Window extends Thread{
    static int ticket = 100;
    //1.创建Lock的实例,需要确保多个线程共用同一个Lock实例,需要考虑将此对象声明为static final
    private static final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true){
            try {
                //2.执行lock方法,锁定对共享资源的调用
                lock.lock();
                if (ticket > 0){
                    try {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket);
                        ticket--;
                    }
                }
                //3.执行unlock方法,释放对共享资源的调用
                lock.unlock();
            }
        }
    }
}

1.9 线程的通信

1.9.1 需要处理线程间通信的理由

当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据。

1.9.2 等待唤醒机制

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。

  • wait():线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用。
  • notify():一旦执行此方法,就会唤醒被wait()的线程中优先级最高的那一个线程。(加果被wait()的多个线程的优先级相同,会随机唤醒一个)。被唤醒的线程从当初被wait的位置继续执行。
  • notifyAll:—旦执行此方法,就会唤醒所有被wait的线程。

注意点:

  1. 此三个方法的使用,必须使用在同步代码块或同步方法中。
  2. Lock需要配合Condition来实现线程之间的通信。
  3. 此三个方法的调用者,必须是同步监视器,否则会报异常。
  4. 此三个方法声明在Object类中。

1.9.3 wait()方法和sleep()方法的区别

  1. 相同点:一旦执行,当前线程都会进入阻塞状态。
  2. 不同点:
  • 声明的位置不同:wait()方法声明在Object类中;sleep()方法声明在Thread类中且为静态方法。
  • 使用的场景不同:wait()方法只能使用在同步代码块或同步方法中;sleep()方法可以在任何需要使用的场景中。
  • 使用在同步代码块或同步方法中:如果wait()方法和sleep()方法都使用在同步代码块或同步方法中,wait()方法一旦执行,会释放同步监视器;而sleep()方法一旦执行,不会释放同步监视器。
  • 结束阻塞的方式:wait()方法到达指定时间自动结束阻塞或通过被notify()唤醒结束阻塞;sleep()方法到达指定时间自动结束阻塞。
package p139;

/**
 * 两个线程交替打印1-100之间的数
 */

public class PrintNumberTest {
    public static void main(String[] args) {
        PrintNumber p = new PrintNumber();
        Thread t1 = new Thread(p,"线程1");
        Thread t2 = new Thread(p,"线程2");
        t1.start();
        t2.start();
    }
}

class PrintNumber implements Runnable{
    private int number = 1;
    @Override
    public void run() {
        while (true){
            synchronized (this) {
                notify();   //唤醒线程
                if (number <= 100){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":"+ number);
                    number++;
                    try {
                        wait(); //线程一旦执行此方法,就进入等待状态,会同时释放对同步监视器的调用
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else {
                    break;
                }
            }
        }
    }
}
package p139;

/**
 * 生产者和消费者问题
 */

/**
 * 店员
 */
class Clerk{
    private int product = 0;   //产品数量

    /**
     * 增加产品数量
     */
    public synchronized void addProduct(){
        if (product >= 20){
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            product++;
            System.out.println(Thread.currentThread().getName()
                    +"生产了第"+product+"个产品");

            //唤醒
            notifyAll();
        }
    }

    /**
     * 减少产品数量
     */
    public synchronized void minusProduct(){
        if (product <= 0){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else {
            System.out.println(Thread.currentThread().getName()
                    +"消费了第"+product+"个产品");
            product--;

            //唤醒
            notifyAll();
        }
    }
}

/**
 * 生产者
 */
class Producer extends Thread{
    //将Clerk进行共享
    private Clerk clerk;
    //在创建Producer时,通过构造器实例化Clerk对象
    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        while (true){
            System.out.println("生产商品");
            clerk.addProduct();
        }
    }
}

/**
 * 消费者
 */
class Consumer extends Thread{
    //将Clerk进行共享
    private Clerk clerk;
    //在创建Consumer时,通过构造器实例化Clerk对象
    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        while (true){
            System.out.println("消费商品");
            clerk.minusProduct();
        }
    }
}

public class ProducerAndConsumerTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer pro1 = new Producer(clerk);
        Consumer con1 = new Consumer(clerk);
        pro1.setName("生产者1");
        con1.setName("消费者1");
        pro1.start();
        con1.start();
    }
}

1.10 创建线程的其他两种方式

1.10.1 实现Callable(JDK5.0中新增)

  1. 与使用 Runnable 相比,Callable 功能更强大些。
  • 相比 run()方法,可以有返回值。
  • 方法可以使用throws的方式处理异常。
  • 使用泛型参数,可以指明具体的call()的返回值类型(需要借助 FutureTask 类,获取返回结果)。
  1. Future 接口(了解)
  • 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutureTask 是 Futrue 接口的唯一的实现类。
  • FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。
  1. 缺点:如果在主线程中需要获取分线程call()的返回值,则此时的主线程是阻塞状态的。

1.10.2 使用线程池(JDK5.0中新增)

  1. 线程池的优点:
  • 提高了程序执行的效率。(因为线程已经提前创建好)
  • 提高了资源的复用率。(因为执行完的线程并未销毁,而是可以继续执行其他的任务)
  • 可以设置相关的参数,对线程池中的线程的使用进行管理。
  1. corePoolSize:核心池的大小。
  2. maximumPoolSize:最大线程数。
  3. keepAliveTime:线程没有任务时最多保持多长时间后会终止。
public class ThreadPool {
    public static void main(String[] args) {
        //1.提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
        service1.setMaximumPoolSize(50);
        //2.执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());    //适用于Runnable
        service.execute(new NumberThread1());   //适用于Runnable
        service.submit(Callable callable);  //适用于Callable
        //3.关闭连接池
        service.shutdown();
    }
}

你可能感兴趣的:(Java,SE,开发语言,java)