跳过正文
  1. 文章/
  2. Java/
  3. JavaSE/
  4. JavaSE高级/

4、多线程

·8521 字·18 分钟· loading · loading · ·
Java JavaSE JavaSE高级
GradyYoung
作者
GradyYoung
目录
JavaSE高级 - 点击查看当前系列文章
§ 4、多线程 「 当前文章 」

程序、进程、线程的区别
#

  • 程序(program)
    • 概念:是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。
  • 进程(process)
    • 概念:程序的一次执行过程,或是正在运行的一个程序
    • 说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
  • 线程(thread)
    • 概念:进程可进一步细化为线程,是一个程序内部的一条单一的执行路径(顺序控制流),可以共享所属进程的数据
    • 说明:线程作为调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。

多线程程序的优点
#

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。

  2. 提高计算机系统CPU的利用率

  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

何时需要多线程
#

  1. 程序需要同时执行两个或多个任务。
  2. 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
  3. 需要一些后台运行的程序时。

并发和并行
#

  • 并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
  • 并发:一个CPU(采用分时间片)执行多个任务。比如:秒杀、多个人做同一件事,同一个时间只能做一件事

创建多线程
#

一个Java应用程序java.exe,其实至少两个线程:

  • 主线程main(),必然存在
  • 垃圾回收线程gc(),必然存在
  • 异常处理线程,当然如果发生异常,会影响主线程,有可能存在

方式一:继承Thread类
#

  1. 创建一个继承于Thread类的子类
  2. 重写Thread类的run():将此线程执行的操作声明在run()中 (线程要完成的任务)
  3. 创建Thread类的子类的对象
  4. 通过此对象调用start():启动当前线程,调用当前线程的run()
  • 注意点
    • 我们启动一个线程,必须调用start(),不能调用run()的方式启动线程。

    • 如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start().

// 继承Thread
static class MyThread extends Thread{
    // 重写run
    @Override
    public void run() {
        while (true){
            System.out.println(Thread.currentThread().getName());
        }
    }
}

public static void main(String[] args) throws Exception {
    // 创建两个线程
    MyThread t1 = new MyThread();
    t1.start();
    MyThread t2 = new MyThread();
    t2.start();
}

方式二:实现Runnable接口
#

  1. 创建一个实现了Runnable接口的类
  2. 实现类去实现Runnable中的抽象方法:run()
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  5. 通过Thread类的对象调用start()
// 实现Runnable
static class MyThread implements Runnable{
    // 实现run
    @Override
    public void run() {
        while (true){
            System.out.println(Thread.currentThread().getName());
        }
    }
}

public static void main(String[] args) throws Exception {
    // 创建两个线程
    Thread t1 = new Thread(new MyThread());
    t1.start();
    Thread t2 = new Thread(new MyThread());
    t2.start();
}

继承Thread和实现Runable的对比
#

开发中:优先选择实现Runnable接口的方式

  • 原因:

    • 实现的方式没类的单继承性的局限性
    • 实现的方式更适合来处理多个线程共享数据的情况。
  • 联系:

    • public class Thread implements Runnable
  • 相同点:

    • 两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
    • 目前两种方式,要想启动线程,都是调用的Thread类中的start()

方式三:实现Callable接口
#

  1. 创建一个实现Callable的实现类
  2. 实现call方法,将此线程需要执行的操作声明在call()
  3. 创建Callable接口实现类的对象
  4. 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
  6. 获取Callable中call()方法的返回值:get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
  • 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?

    • call()可以有返回值的。
    • call()可以抛出异常,被外面的操作捕获,获取异常的信息
    • Callable是支持泛型的
  • 缺点

    • 调用get()方法,会导致主线程阻塞
// 实现Runnable
static class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        return Thread.currentThread().getName();
    }
}

public static void main(String[] args) throws Exception {
    // 创建FutureTask
    FutureTask<String> futureTask = new FutureTask<>(new MyThread());
    // 创建Thread
    Thread t1 = new Thread(futureTask);
    t1.start();
    // 阻塞获取返回值
    System.out.println(futureTask.get());
}

Thread类
#

java.lang.Thread

构造方法
#

  • Thread():创建新的Thread对象
  • Thread(String threadname):创建线程并指定线程名
  • Thread(Runnable target):指定创建线程的目标对象
  • Thread(Runnable target,String threadname):指定创建线程的目标对象,并指定线程名

常用方法
#

  • void start():启动线程并执行run()方法

  • run():线程被调度时执行的操作,不可主动调用!

  • String getName():返回线程名称

  • static Thread currentThread():返回当前线程

  • static void yield():线程让步,线程进入就绪状态

    • 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
    • 若队列没有满足条件的线程,此方法会被忽略
  • join():立刻执行调用此方法线程,其他线程将被阻塞直到调用方法的线程执行完毕,可以保证线程的串行化执行

    • 例如在线程a中,调用线程b的join()方法,线程a会进入阻塞,直到线程b执行完毕
  • static void sleep(long millis):当前正在活动的线程在millis时间段内放弃CPU控制,时间到后重新排队,线程进入阻塞状态

  • stop():强行结束线程生命周期,不推荐使用

  • boolean isAlive():判断线程是否还活着

  • int activeCount():获取线程组中线程的数量

获取和设置优先级
#

getPriority():获取线程的优先级

setPriority(int p):设置线程的优先级

说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。

守护线程和用户线程
#

  • Java中的线程分为两类
    • 守护线程
      • 在调用start()前使用thread.setDaemon(ture)方法将一个用户线程变为守护线程。
      • 它是在后台运行的,当所有用户线程结束时,守护线程也会随之结束。
    • 用户线程
      • 创建的线程默认为用户线程
      • 用户线程是主线程的子线程,当主线程结束时,用户线程也会结束。
Thread userThread = new Thread("User Thread") {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
};

Thread daemonThread = new Thread("Daemon Thread"){
    @Override
    public void run() {
        while (true){
            System.out.println(Thread.currentThread().getName());
        }
    }
};

userThread.start();
// 设置为守护线程,即使自己有死循环,也会随着所有用户线程的结束而退出
daemonThread.setDaemon(true);
daemonThread.start();

线程的生命周期
#

JDK中用Thread.State类定义了线程的几种状态

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态

  • 新建:当一个Thread类或子类对象被声明和创建时
  • 就绪:处于新建状态的线程调用start()方法后,线程进入队列等待CPU时间片,此时已经具备了运行的条件,只是还没分配到CPU资源
  • 运行:就绪的线程被调度,并且获得CPU资源,进行运行状态,执行run()
  • 阻塞:被人为挂起或输入输出操作时,让出CPU并临时中止自己的执行
  • 死亡:线程完成了全部工作或被强制性终止(例如出现异常)

说明: untitle.png

线程同步
#

通过同步机制,来解决线程的安全问题。

方式一:synchronized隐式锁
#

同步代码块
#

synchronized(同步监视器){     
    //需要被同步的代码  
}
  • 一般用于同步操作共享数据(多个线程共同操作的变量)的代码
  • 同步监视器(Object类型),俗称:锁。任何一个类的对象,都可以充当锁。要求**:多个线程必须要共用同一把锁(不变)**。
  • synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁
  • 一把锁只能同时被一个线程获取,没有获得锁的线程只能阻塞等待

同步方法
#

public synchronized void method(){}
  • 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

  • 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。

    • 非静态的同步方法,同步监视器是:this
    • 静态的同步方法,同步监视器是:当前类.class

可重入锁(递归锁)
#

所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程内部可以再次获取本对象上的锁,而其他的线程是不可以的。

可以重复使用的锁,锁的创建只有一次,可以重复调用lock()unlock()

synchronizedReentrantLock都是可重入锁。

public class Demo {
    public static void main(String[] args) {
        Thread t = new Thread("A") {
            @Override
            public void run() {
                // 线程第一次获取锁
                synchronized (Demo.class){
                    for (int i = 0; i < 10000; i++) {
                        // 可重入,再次获取锁
                        synchronized (Demo.class){
                            System.out.println(Thread.currentThread().getName());
                        }
                    }
                }

            }
        };
        t.start();
    }
}

方式二:Lock显式锁
#

常用方法
#

  • void lock():获取锁,如果锁不可用,则出于线程调度的目的,当前线程将被禁用,并且在获取锁之前处于休眠状态。
  • boolean tryLock():如果锁可用立即返回true,如果锁不可用立即返回false;
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException:如果锁可用,则此方法立即返回true。 如果该锁不可用,则当前线程将出于线程调度目的而被禁用并处于休眠状态,直到发生以下三种情况之一为止
    • 当前线程获取到该锁
    • 当前线程被其他线程中断,并且支持中断获取锁
    • 经过指定的等待时间如果获得了锁,则返回true,没获取到锁返回false。
  • void unlock():释放锁。释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

ReentrantLock(可重入锁)
#

实现Lock接口,是一种可重入锁

public class Demo {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock(true);
        Thread t = new Thread("A") {
            @Override
            public void run() {
                try {
                    // 线程第一次获取锁
                    lock.lock();
                    for (int i = 0; i < 10000; i++) {
                        // 可重入,再次获取锁
                        lock.lock();
                        System.out.println(Thread.currentThread().getName());

                    }
                }finally {
                    // 释放锁
                    lock.unlock();
                }
            }
        };
        t.start();
    }
}

ReentrantReadWriteLock(读写锁)
#

  • writeLock():获取写锁
  • readLock():获取读锁
    • 两个线程都是写锁:互斥,同步执行

    • 两个线程一写一读:互斥,同步执行

    • 两个线程都是读锁:共享,异步执行

公平锁和非公平锁
#

公平锁的效率比非公平锁低

公平锁总是可以保证让所有线程中等待时间最长的线程先执行

new ReentrantLock(true)new ReentrantReadWriteLock(true)时,参数为true,创建的就是公平锁;不传参默认为false,就是非公平锁

synchronized与Lock的异同
#

  • 相同:
    • 二者都可以解决线程安全问题
  • 不同:
    • synchronized是Java语言的关键字;而Lock是一个接口。
    • synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器;而Lock需要手动的启动同步lock(),同时结束同步也需要手动的实现unlock(),如果没有主动释放锁,就有可能导致出现死锁现象。
    • Lock可以配置公平策略,实现线程按照先后顺序获取锁;而synchronized不可以。
    • Lock提供了trylock()方法可以试图获取锁,获取到或获取不到时,返回不同的返回值让程序可以灵活处理。
    • lock()unlock()可以在不同的方法中执行,程序更加灵活。

死锁问题
#

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

说明:

  1. 出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续
  2. 我们使用同步时,要避免出现死锁。

解决死锁
#

1、调整锁的顺序,避免可能出现的死锁

2、调整锁的范围,避免在一个同步代码块中使用另一个同步代码块

3、使用可重入锁

判断锁会不会释放
#

  • 会释放
    • 线程的同步方法或同步代码块执行结束
    • 线程的同步方法或同步代码块中遇到breakreturn终止了代码块中的循环或方法的执行
    • 线程的同步方法或同步代码块出现ErrorException未处理
    • 线程的同步代码块、同步方法执行了wait()造成线程阻塞并释放锁
  • 不会释放
    • 线程的同步方法或同步代码块中执行Thread.sleep()Thread.yield()
    • 调用了suspend()将线程挂起(避免使用suspend()resume()控制线程)

线程通信
#

使用synchronized强同步
#

  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
  • notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
  • notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
    • wait()notify()notifyAll()三个方法必须使用在同步代码块或同步方法中。
    • wait()notify()notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
    • wait()notify()notifyAll()三个方法是定义在java.lang.Object类中。

注意:wait方法等待的线程,在哪等待,被唤醒后,就从哪里开始执行,可能会出现虚假唤醒的问题,所以建议wait循环使用在while循环中

使用Lock接口下的可重入锁
#

  • 获取Condition对象Condition condition = lock.newCondition();
  • 使用Condition对象的方法
    • await():当前线程等待,对应wait()
    • signal():唤醒一个线程,对应notify()
    • signalAll():唤醒所有线程,对应notifyAll()

sleep()和wait()的异同
#

  • 相同点
    • 一旦执行方法,都可以使得当前的线程进入阻塞状态。
  • 不同点
    • 两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
    • 调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
  • 关于是否释放同步监视器
    • 如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

ThreadLocal类
#

可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据(可以存放线程范围内的局部变量)

实际使用场景
#

在JavaWeb项目里面,我们做MyBatis的工具类的时候,为了可以满足多个线程中,各线程只关闭自己的SqlSession对象的需求,而不会产生线程安全的问题,所以,我们需要将SqlSession对象存入到ThreadLocal对象中

常用方法
#

  • set()用于向ThreadLocal对象中存值
  • get()用于向ThreadLocal对象中取值
  • remove()用于从ThreadLocal对象中删除值

CAS自旋锁
#

解决的问题
#

例如,一个int类型的变量,在高并发的情况下进行加减,通常会导致线程不安全,数值不准确的问题,例如

public class Demo {
    static int count = 0;
    public static void main(String[] args) {
        // 创建100个线程
        for (int i1 = 0; i1 < 100; i1++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        count++;
                    }
                }
            }).start();
        }
		// 确保其他线程都执行完成了,除了GC和主线程
        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        // 结果打印出来的数据不一定是正确的10000,结果比10000少
        System.out.println(count);
    }
}

原因分析
#

由于加减操作例如count++count = count - 1并不是一个原子操作,在执行的时候分了三步:1、读取值;2、修改值;3、写入值。所以例如现在有两个线程A和B,原子性操作如下:

  1. 线程A:读取值为0
  2. 线程B:开始执行
  3. 线程B:读取值为0
  4. 线程B:修改读到的值为1
  5. 线程B:将值1写入变量count
  6. 线程A:开始执行
  7. 线程A:修改读到的值为1
  8. 线程A:将值1写入到变量count

所以造成了变量count的值经过两次加1,应该为2;但是实际上值为1

解决方式
#

1、使用synchronized同步锁的方式
#

缺点是:每次进行非原子操作前,都需要加锁,操作完解锁,效率低下

public class Demo {
    static int count = 0;
    public static void main(String[] args) {
        for (int i1 = 0; i1 < 100; i1++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        // 使用同步代码块将非原子操作同步
                        synchronized (Demo.class){
                            count++;
                        }
                    }
                }
            }).start();
        }
        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(count);
    }
}

2、通过CAS自旋锁的方式进行
#

通过AtomicInteger,可以在线程提交的时候,用自己读到的数值,与现在变量中的数值进行比对。如果一致的话,就写入变量;如果数值已经发生改变的话,那么会进行自旋,也就是重新进行运算,再进行比较

public class Demo {
    static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) {
        for (int i1 = 0; i1 < 100; i1++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        //读取数据并自增,等同于count++
                        count.getAndIncrement();
                    }
                }
            }).start();
        }

        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(count);
    }
}
解决ABA问题
#

但是,自旋锁的方式,有可能会产生ABA的问题,例如现在有三个线程A、B、C,操作流程如下

  1. 线程A:读取值为0
  2. 线程B:开始执行
  3. 线程B:读取值为0
  4. 线程A:开始执行
  5. 线程A:+ 1;验证变量值为0,一致;写入变量;变量值为1
  6. 线程C:开始执行
  7. 线程C:读取值为1;- 1;验证变量为1,一致;写入变量,变量值为0
  8. 线程B:开始执行
  9. 线程B:+ 1;验证变量值和自己读取的都为0,一致,写入变量;变量值为1

这样看下来,虽然变量值和预期的值相同,但是中间变量其实已经修改过了,线程B比较的0已经不是自己当时读到的0了,在某些情况下,可能会出现问题

在CAS的思想上,利用版本戳的思想,从原始数据开始,给每次的数据都加上一个版本,每次对数据发生修改,版本也会进行迭代

具体的实现类,AtomicStampedReference,这个类在实例化时,第二个参数为版本号

public class Demo {
    // 第一个参数为保存的数据,第二个参数为版本戳
    static AtomicStampedReference<Integer> count = new AtomicStampedReference(0,0);
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    // 比较并且设置
                    // 1:比较的原始值,2:设置的新参数值
                    // 3、比较的原始版本戳,4、设置的新版本戳
                    count.compareAndSet(
                            count.getReference()
                            , count.getReference() + 1
                            , count.getStamp()
                            , count.getStamp() + 1
                    );
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    count.compareAndSet(
                            count.getReference()
                            , count.getReference() - 1
                            , count.getStamp()
                            , count.getStamp() + 1
                    );
                }
            }
        }).start();

        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(count.getReference());
        System.out.println(count.getStamp());
    }
}

线程池
#

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。

  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用

  • 好处:

    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理
  • 相关API

    • JDK 5.0起提供了线程池相关API:ExecutorServiceExecutors
    • 线程池所在的包:java.util.concurrent

线程池的执行顺序
#

1、用户提交任务,判断核心线程是否正在处理,如果核心线程有空闲,直接使用核心线程执行

2、如果核心线程没有空闲,判断队列是否满了,如果没有满,就把任务放进队列中

3、如果队列已经满了,那么判断线程池中线程+新任务线程是否大于最大线程数,如果大于,抛出异常,拒绝执行

4、如果小于等于最大线程数,创建新的线程,执行任务

image-20230427110025494

线程池的状态
#

状态 说明
RUNNING 允许提交并处理任务
SHUTDOWN 不允许提交新的任务,但是会处理完已提交的任务
STOP 不允许提交新的任务,也不会处理阻塞队列中未执行的任务,并设置正在执行的线程的中断标志位
TIDYING 所有任务执行完毕,池中工作的线程数为0,等待执行terminated()勾子方法
TERMINATED terminated()勾子方法执行完毕
  • 线程池的shutdown()方法,将线程池由 RUNNING(运行状态)转换为 SHUTDOWN状态
  • 线程池的shutdownNow()方法,将线程池由RUNNING 或 SHUTDOWN 状态转换为 STOP 状态。

SHUTDOWN 状态 和 STOP 状态 先会转变为 TIDYING 状态,最终都会变为 TERMINATED

线程池的创建
#

1、ThreadPoolExecutor(建议)
#

/* 创建线程池
 * 参数1:线程池创建后,核心线程数
 * 参数2:最大线程数
 * 参数3:核心线程外,新创建的线程空闲时,最大存活时间
 * 参数4:最大存活时间的单位
 * 参数5:用于存放任务的阻塞队列
 * 参数6:用于创建线程的线程工厂
 * 参数7:任务被拒绝后的策略
 */
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        2
        , 5
        , 60
        , TimeUnit.SECONDS
        , new ArrayBlockingQueue<>(5)
        , new ThreadPoolExecutor.AbortPolicy()
);
拒绝策略
#
策略 描述
ThreadPoolExecutor.AbortPolicy() 抛出RejectedExecutionException异常。默认策略
ThreadPoolExecutor.CallerRunsPolicy() 由向线程池提交任务的线程来执行该任务
ThreadPoolExecutor.DiscardPolicy() 抛弃当前的任务
ThreadPoolExecutor.DiscardOldestPolicy() 抛弃最旧的任务(最先提交而没有得到执行的任务)

线程池中,提交任务submit()与execute()之间的区别
#

  • execute()是专门用来提交Runnable接口任务的;submit()既可以提交Runable任务也可以提交Callable任务

  • execute()没有返回结果;submit()可以使用Future接收线程的返回结果

  • execute()方法中不能处理任何异常;submit()支持处理任务中的异常,使用Funture.get()

2、Executors
#

可以使用Executors的静态方法,来创建线程池

创建方法 线程池 阻塞队列 使用场景
newSingleThreadExecutor() 单线程的线程池 LinkedBlockingQueue 适用于串行执行任务的场景,⼀个任务⼀个任务地执行
newFixedThreadPool(n) 固定数目线程的线程池 LinkedBlockingQueue 适用于处理CPU密集型的任务,适用执行长期的任务
newCachedThreadPool() 可缓存线程的线程池 SynchronousQueue 适用于并发执行大量短期的小任务
newScheduledThreadPool(n) 定时、周期执行的线程池 DelayedWorkQueue 周期性执行任务的场景,需要限制线程数量的场景
例如
#
public static void main(String[] args) {
    // 创建一个固定大小的线程池:
    ExecutorService es = Executors.newFixedThreadPool(4);
    Future<Integer> future = es.submit(new Task("a"));
    System.out.println(future);

    // 关闭线程池:
    es.shutdown();
}

阿里巴巴Java开发手册规定:线程池不允许使用Executors去创建,要求通过new ThreadPoolExecutor()的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

JavaSE高级 - 点击查看当前系列文章
§ 4、多线程 「 当前文章 」