Java 多线程 从无到有
个人总结:望对屏幕对面的您有所帮助
一. 线程概述
进程:
有独立的内存控件和系统资源 应用程序的执行实例
启动当前电脑任务管理器:taskmgr
进程是程序(任务)的执行过程,它持有资源(共享内存,共享文件)和线程。
线程:
进程中执行运算的最小的单位,可完成一个独立的顺序控制流程(执行路径)
CPU调度和分派的基本单位
一个线程的生命周期:
· 新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
· 就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
· 运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
· 阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。
· 死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
线程的状态转换:
1. 新建状态(New):新创建了一个线程对象。
2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
多线程:
线程是系统中最小的执行单元;同一进程中可以有多个线程;线程共享进程的资源。
定义:
如果一个进程中 同时运行了多个线程 ,用来完成不同的工作,则称之为 “多线程”
多个线程交替占用CPU资源,而非真正的并行执行()
优点:
1)充分利用CPU资源
2)简化编程模型
每个线程控制一个指针
3)带来良好的用户体验
创建线程的方法
有两种:
1. 实现Runnable接口
拥有唯一方法:run()
方法 run 的常规协定是,它可能执行任何所需的动作。
JavaJDK API 给出的解释:
Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。
设计该接口的目的是为希望在活动时执行代码的对象提供一个公共协议。例如,Thread 类实现了 Runnable。激活的意思是说某个线程已启动并且尚未停止。
此外,Runnable 为非 Thread 子类的类提供了一种激活方式。通过实例化某个 Thread 实例并将自身作为运行目标,就可以运行实现 Runnable 的类而无需创建 Thread 的子类。大多数情况下,如果只想重写 run() 方法,而不重写其他 Thread 方法,那么应使用 Runnable 接口。这很重要,因为除非程序员打算修改或增强类的基本行为,否则不应为该类创建子类。
2. 继承Thread类本身
Thread类:
Java提供了java.lang.Thread类支持多线程编程
1. Thread类常用的构造方法:
序号 |
方法描述 |
1 |
public Thread() 分配新的 Thread 对象。这种构造方法与 Thread(null, null, gname) 具有相同的作用,其中 gname 是一个新生成的名称。自动生成的名称的形式为 "Thread-"+n,其中的 n 为整数。 |
2 |
public Thread(Runnable target) 分配新的 Thread 对象。这种构造方法与 Thread(null, target,gname) 具有相同的作用,其中的 gname 是一个新生成的名称。自动生成的名称的形式为 “Thread-”+n,其中的 n 为整数。 参数: target - 其 run 方法被调用的对象。 |
3 |
public Thread(Runnable target,String name) 分配新的 Thread 对象。这种构造方法与 Thread(null, target, name) 具有相同的作用。 参数: target - 其 run 方法被调用的对象。 name - 新线程的名称。 |
4 |
public Thread(String name) 分配新的 Thread 对象。这种构造方法与 Thread(null, null, name) 具有相同的作用。 参数: name - 新线程的名称。 |
2. Thread类的一些重要方法(对象调用):
序号 |
方法描述 |
1 |
public void start() |
2 |
public void run() |
3 |
public final void setName(String name) |
4 |
public final void setPriority(int priority) |
5 |
public final void setDaemon(boolean on) |
6 |
public final void join(long millisec) |
7 |
public void interrupt() |
8 |
public final boolean isAlive() |
3. Thread类的静态方法(直接调用):
序号 |
方法描述 |
1 |
public static void yield() |
2 |
public static void sleep(long millisec) |
3 |
public static boolean holdsLock(Object x) |
4 |
public static Thread currentThread() |
5 |
public static void dumpStack() |
二. 主线程
1. 主线程
每一个进程至少只有一个线程
1)main()方法即为主线程入口
2)产生其他子线程的线程
3)必须最后完成执行,因为它执行各种关闭动作
简单操作:
2. 常见线程名词解释
主线程:JVM调用程序main()所产生的线程。
当前线程:这个是容易混淆的概念。一般指通过Thread.currentThread()来获取的进程。
后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM的垃圾回收线程就是一个后台线程。
前台线程:是指接受后台线程服务的线程,其实前台后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。可以通过isDaemon()和setDaemon()方法来判断和设置一个线程是否为后台线程。
三. 继承Thread 类创建线程
1. 定义一个类继承Thread类
建议:类名以Thread结尾,(清晰)
public class MyThread extends Thread{
}
2. 重写run()方法,编写线程执行体
执行的操作
逻辑处理代码
3. 创建线程对象,调用start()方法启动线程
类 对象 = new 类(); 对象.start();//启动线程 //将会执行步骤二中重写后的run()方法
当多个线程同时启动时:
线程间交替执行,顺序不定,根据CPU分配的时间片觉得的(不可预测)
每个线程分别执行,几条不同的执行过程,交替并行执行
为什么不直接调用run()方法?
如果直接调用run()方法:
执行的线程是main()/主 线程,并且不是并行执行
1. 只有主线程一条执行路径
2. 依次调用相应次数的run()方法
3. 相当于单线程
4. Run()方法是给底层编译器用的,程序员只能用start()
执行结构图:
如果是调用Start()方法:
1)不同子线程获取到CPU时间片时,执行其线程的相应操作
2)多态执行路径,主线程和子线程并行交替执行
多个线程同时执行?
1. 多个线程交替执行,不是真正的 “并行”
2. 线程每次执行时长由分配的CPU时间片长度觉得
广义解释:
线程中的方法比较有特点,比如:启动(start),休眠(sleep),停止等,多个线程是交互执行的(cpu在某个时刻。只能执行一个线程,当一个线程休眠了或者执行完毕了,另一个线程才能占用cpu来执行)因为这是cpu的结构来决定的,在某个时刻cpu只能执行一个线程,不过速度相当快,对于人来将可以认为是并行执行的。
四. 实现Runnable 接口创建线程
1. 定义类实现Runnable接口
public class MyRunnable impleents Runnable{
}
2. 实现run()方法,编写线程执行体
run()方法中编写线程执行的代码
3. 创建线程对象,调用start()方法启动线程
实现Runnable接口创建的线程最终还是要通过将自身实例作为参数传递给Thread然后执行
语法:
Thread actress=new Thread(Runnable target ,String name);
例如:
Thread actressThread=new Thread(new Actress(),"Ms.runnable");
actressThread.start();
分析:
使用方式或限制等,与 继承Thread类 大相径庭
方法必须实现
1. 创建Runnable实现类对象
2. 将实现类对象交于Thread对象
3. 启动线程
4. 优点
1.可以避免java的单继承的特性带来的局限性;
2.适合多个相同程序的代码去处理同一个资源情况,把线程同程序的代码及数据有效的分离,较好的体现了面向对象的设计思想。开发中大多数情况下都使用实现Runnable接口这种方法创建线程。
5. 分析:
计算机CPU处理器在同一时间同一个处理器同一个核只能运行一条线程,当一条线程休眠之后,另外一个线程才获得处理器时间
三 or 四.比较两种创建线程的方式
1. 继承Thread类
1) 编写简单,可直接操作线程
2) 适用于但继承
2. 实现Runnable接口
1)避免但继承的局限性
2)便于共享资源
3. 结论
(Java只适用于单根继承)
推荐使用 实现Runnable接口 方式创建线程
五. 线程的状态和调度
1. 线程的状态
五个状态:
创建 就绪 阻塞 运行 死亡
2. 线程调度的方法
线程调度指按照特定机制为多个线程分配CPU的使用权
4. 线程唤醒:
Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
注意:Thread中suspend()和resume()两个方法在JDK1.5中已经废除。原因:有死锁倾向。
六. 线程优先级和线程休眠
1. 调整线程优先级:
线程的优先级
线程优先级由1 - 10表示,1最低,默认优点级为5
优先级高的线程获得CPU资源概率较大
调整线程优先级
setPriority()方法
取值:
线程的优先级代表哪个线程优先获取CPU资源
每一个Java线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:
static int MAX_PRIORITY
线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY
分配给线程的默认优先级,取值为5。
补充:
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。
线程的优先级有继承关系,比如A线程中创建了B线程,那么B将和A具有相同的优先级。
JVM提供了10个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用Thread类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
2. 线程睡眠:
使用:
让线程暂时睡眠指定时长,线程进入阻塞状态
睡眠过后线程会再进入可运行状态
sleep()方法:
Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
注:调度sleep()方法需处理InterruptedException异常
3. 线程等待:
Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
七. 线程强制执行
使线程暂停执行,等待其他线程结束后再继续执行本线程
1. 线程加入:
join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
注:需处理InterruptedException异常
Mills:以毫秒为单位的等待时长
Nanos:要等待的附加纳秒时长
主线程将被执行,执行调用join()方法的子线程
当此子线程执行完毕后,再返回执行主线程
(其他线程避让(阻塞),此线程强制加入执行)
八. 线程的礼让
让当前线程让出CPU资源,不再参与资源抢占
暂停当前线程,允许其他具有相同优先级的线程获得运行机会(不一定会执行)
改线程处于就绪状态,不转为阻塞状态
只能提供一种可能,但是不能保证一定会实现礼让
1. 线程让步:
Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
九. 线程的不安全问题
当多个线程共享同一资源同时访问一个数据的时候,一个线程未完成全部操作的时候,其他的线程来修改数据数据,会造成线程不安全问题
争用条件:
1、当多个线程同时共享访问同一数据(内存区域)时,每个线程都尝试操作该数据,从而导致数据被破坏(corrupted),这种现象称为争用条件
2、原因是,每个线程在操作数据时,会先将数据初值读【取到自己获得的内存中】,然后在内存中进行运算后,重新赋值到数据。
3、争用条件:线程1在还【未重新将值赋回去时】,线程1阻塞,线程2开始访问该数据,然后进行了修改,之后被阻塞的线程1再获得资源,而将之前计算的值覆盖掉线程2所修改的值,就出现了数据丢失情况。
系统占用CPU资源:随机性
十. 同步方法和同步代码块
线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。
同步方法:
使用synchronzed修饰的方法控制对类成员变量的访问
Synchronized就是为当前的线程声明一个锁
一次只允许有一个线程进入执行
语法:
访问修饰符 synchronized 返回类型 方法名 (参数列表){ ... ... }
or
Synchronized 访问修饰符 返回类型 方法名 (参数列表){ ... ... }
同步代码块:
使用synchronized关键字修饰的代码块
语法:
Synchronized(syncObject){
//需要同步的代码块
}
解析:
1)yncObject为需同步的对象,通常为this
2)效果与同步方法相同
3)避免数据不安全问题
4)一次只允许有一个线程进入
互斥与同步:守恒的能量
1、线程的特点,共享同一进程的资源,同一时刻只能有一个线程占用CPU
2、由于线程有如上的特点,所以就会存在多个线程争抢资源的现象,就会存在争用条件这种现象
3、为了让线程能够正确的运行,不破坏共享的数据,所以,就产生了同步和互斥的两种线程运行的机制
4、线程的互斥(加锁实现):线程的运行隔离开来,互不影响,使用synchronized关键字实现互斥行为,此关键字即可以出现在方法体之上也可以出现在方法体内,以一种块的形式出现,在此代码块中有线程的等待和唤醒动作,用于支持线程的同步控制
5、线程的同步(线程的等待和唤醒:wait()+notifyAll()):线程的运行有相互的通信控制,运行完一个再正确的运行另一个
6、锁的概念:比如private final Object lockObj=new Object();
7、互斥实现方式:synchronized关键字
synchronized(lockObj){---执行代码----}加锁操作
lockObj.wait();线程进入等待状态,以避免线程持续申请锁,而不去竞争cpu资源
lockObj.notifyAll();唤醒所有lockObj对象上等待的线程
8、加锁操作会开销系统资源,降低效率
## 同步和锁定
1、锁的原理
Java中每个对象都有一个内置锁
当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。
当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。
一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。
释放锁是指持锁线程退出了synchronized同步方法或代码块。
关于锁和同步,有一下几个要点:
1)、只能同步方法,而不能同步变量和类;
2)、每个对象只有一个锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?
3)、不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
4)、如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。
5)、如果线程拥有同步和非同步方法,则非同步方法可以被多个线程*访问而不受锁的限制。
6)、线程睡眠时,它所持的任何锁都不会释放。
7)、线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
8)、同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
9)、在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。
多个并发线程访问同一资源的同步代码块时
1. 同一时刻只能有一个线程进入synchronized (this )同步代码块
2. 当一个线程访问一个synchronized (this) 同步代码块时,其他synchronized (this) 同步代码块同样被锁定
3. 当一个线程访问一个synchronized (this) 同步代码块时,其他线程可以访问该资源的非synchronized (this) 同步代码
## 线程同步小结
1、线程同步的目的是为了保护多个线程访问一个资源时对资源的破坏。
2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他同步方法。
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。
## 深入剖析互斥与同步
互斥的实现(加锁):synchronized(lockObj); 保证的同一时间,只有一个线程获得lockObj.
同步的实现:wait()/notify()/notifyAll()
注意:wait()、notify()、notifyAll()方法均属于Object对象,而不是Thread对象。
void notify()
唤醒在此对象监视器上等待的单个线程。
void notifyAll()
唤醒在此对象监视器上等待的所有线程。
void wait()
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
当然,wait()还有另外两个重载方法:
void wait(long timeout)
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。
void wait(long timeout, int nanos)
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。
同步是两个线程之间的一种交互的操作(一个线程发出消息另外一个线程响应)
关于等待/通知,要记住的关键点是:
必须从同步环境内调用wait()、notify()、notifyAll()方法。线程不能调用对象上等待或通知的方法,除非它拥有那个对象的锁。
wait()、notify()、notifyAll()都是Object的实例方法。与每个对象具有锁一样,每个对象可以有一个线程列表,他们等待来自该信号(通知)。线程通过执行对象上的wait()方法获得这个等待列表。从那时候起,它不再执行任何其他指令,直到调用对象的notify()方法为止。如果多个线程在同一个对象上等待,则将只选择一个线程(不保证以何种顺序)继续执行。如果没有线程等待,则不采取任何特殊操作。
千万注意:
当在对象上调用wait()方法时,执行该代码的线程立即放弃它在对象上的锁。然而调用notify()时,并不意味着这时线程会放弃其锁。如果线程荣然在完成同步代码,则线程在移出之前不会放弃锁。因此,只要调用notify()并不意味着这时该锁变得可用。
多个线程在等待一个对象锁时候使用notifyAll():
在多数情况下,最好通知等待某个对象的所有线程。如果这样做,可以在对象上使用notifyAll()让所有在此对象上等待的线程冲出等待区,返回到可运行状态。
### 如何理解同步:Wait Set
Critical Section(临界资源)Wait Set(等待区域)
wait set 类似于线程的休息室,访问共享数据的代码称为critical section。一个线程获取锁,然后进入临界区,发现某些条件不满足,然后调用锁对象上的wait方法,然后线程释放掉锁资源,进入锁对象上的wait set。由于线程释放释放了理解资源,其他线程可以获取所资源,然后执行,完了以后调用notify,通知锁对象上的等待线程。
Ps:若调用notify();则随机拿出(这随机拿出是内部的算法,无需了解)一条在等待的资源进行准备进入Critical Section;若调用notifyAll();则全部取出进行准备进入Critical Section。
十一. 死锁
个人理解:
多个线程各有自己的锁,都想拿到自己的锁,但谁都不想放开自己的锁
多个线程执行不同的上锁代码块但共享同一资源,双方都抢不到CPU时间片,就形成了死锁
资料解释:
死锁--两个线程都在等待对方完成,造成程序的停滞
死锁的条件:
1)两个或两个以上的线程在活动
2)某个线程拿到一个锁以后,还想拿第二个锁,造成锁的嵌套
十二. 生产者和消费者问题
生产者和消费者问题,生产者不断生成,消费者不断取走生产者生成的产品
生产者生产出信息之后将其放到一个区域之中,之后消费者从此区域里取出数据
使用Object类中的Wait(线程等待)方法 与 notifyAll(唤醒所有线程)方法
十三. 线程池
优点:
1、线程是稀缺资源,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以重复使用。
2、可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。
为什么使用线程池?
线程缺乏统一管理,占用过多系统资源
缺乏更多功能,如定时执行,定期执行等
使用线程池的好处
1)重用存在的线程,减少对象创建,消亡开销
2)有效控制最大并发数,提高系统资源使用率
3)定时执行,定期执行
实现原理
(线程比作员工,线程池比作一个团队,核心池比作团队中核心团队员工数,核心池外的比作外包员工)
1. 有了新需求,先看核心员工数量超没超出最大核心员工数,还有名额的话就新招一个核心员工来做
· 需要获取全局锁
2. 核心员工已经最多了,HR 不给批 HC 了,那这个需求只好攒着,放到待完成任务列表吧
3. 如果列表已经堆满了,核心员工基本没机会搞完这么多任务了,那就找个外包吧
· 需要获取全局锁
4. 如果核心员工 + 外包员工的数量已经是团队最多能承受人数了,没办法,这个需求接不了了
使用:
线程池所在包:java.util.concurrent
*接口是Excutor,(子接口)真正的线程池接口是ExecutorService
Java.util.concurrent.Executors类提供创建线程池的方法
ThreadPoolExcecutor类的使用
(自定义线程池)
构造器中各个参数的含义
。corePoolSize: 核心池的大小
。maximumPoolSize: 线程池最大线程数
keepAliveTime: 表示线程没有任务执行时最多保持多久时间会终止
unit: 参数keepAliveTime的时间单位
workQueue: 一个阻塞队列,用来存储等待执行的任务
threadFactory: 线程工厂,主要用来创建线程
handler: 表示当拒绝处理任务时的策略
· corePoolSize:核心线程池数量
· 在线程数少于核心数量时,有新任务进来就新建一个线程,即使有的线程没事干
· 等超出核心数量后,就不会新建线程了,空闲的线程就得去任务队列里取任务执行了
· maximumPoolSize:最大线