线程 进程 协程
总结: 一个进程包含多个线程,线程包含多个协程.协程是进程点最小单位. 进程需要内核来调度实现上下文切换,但是协程是由用户决定的.CPU真正运行的是线程.但是线程和进程过多会造成消耗大量cup.
- 协程: 线程操作的最小单位,线程的上下文切换是由cpu调度的,协程上下文切换是用户自己决定的,有自己的上下文.一个线程可以有多个协程.
协程优缺点
- 优点 1. 非常快的上下文切换,减少开销 2. 单线程即可实现高并发 3. 由于只有一个进程,不需要加锁.线程安全
- 缺点 文档少不好控制
创建多线程方式
- 集成Thread类,重写run方法
- 实现Runable接口 实现run方法
- 通过Callable和FutureTask方式(有返回值)
public class MyTask implements Callable<Object> {
@Override
public Object call() throws Exception {
System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName());
return "这是返回值";
}
}
public static void main(String[] args) {
FutureTask<Object> futureTask = new FutureTask<>(()->{
System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName());
return "这是返回值";
});
// MyTask myTask = new MyTask();
// FutureTask<Object> futureTask = new FutureTask<>(myTask);
//FutureTask继承了Runnable,可以放在Thread中启动执行
Thread thread = new Thread(futureTask);
thread.setName("demo3");
thread.start();
System.out.println("主线程名称:"+Thread.currentThread().getName());
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
//阻塞等待中被中断,则抛出
e.printStackTrace();
} catch (ExecutionException e) {
//执行过程发送异常被抛出
e.printStackTrace();
}
}
- 通过线程池创建
线程状态
创建 就绪 运行 阻塞 死亡
- 创建: 生成线程对象但是没有调用方法
- 就绪: 调用了start()方法,线程进入就绪状态,但是此时cup还未将时间片分配给线程. 线程从睡眠或者等待状态回来之后也会进入就绪状态
- 运行: 线程成为当前的主线程,即获得cup的使用权,开始执行run逻辑
- 阻塞: 阻塞状态结束后进入就绪状态
- 同步阻塞: 线程在获取锁时失败,进入同步阻塞状态.
- 等待阻塞: 进入等待阻塞的线程需要其它线程做出一定动作. 此时该线程会占用cup. 需要被唤醒,可能也会无线等待.比如调用wati就会进入 watiing状态**(waiting 等待状态,需要被唤醒 需要其它线程做出特定动作),调用sleep就会进入Time-waitting状态(超时等待 不会释放锁,在指定时间后自己醒来)**
线程常见状态
- sleep 属于Thread方法,会自动醒来,睡眠的时候不释放锁.阻塞时进入Time-watting状态,结束后变为Runnable状态
- yield 属于Thread方法,暂停当前线程执行优先级相同的线程.不释放锁也不进入阻塞状态,直接转为就绪状态,不保证其它线程一定会执行到
- join 属于Thread方法,使用后暂停当前主线程,让主线程休眠,不释放锁执行jion进来的线程.
-wait **属于Object方法 调用wait方法时会释放锁,进入等待队列,需要依靠notiy或者notifyAll方法唤醒
使用wait方法不一定会让线程进入阻塞状态,只有作为锁的对象,在其内部调用wait()方法才会进入阻塞状态 调用wati()方法会先进入等待池 调用notify才会进入锁池尝试获取锁
- notify 属于Object类
- notifyAll 属于Object类
举例说几个方法保证线程安全
加锁synchronize/ReentrantLock
volatile 不能保证原子性
AtomicXX类 AtomicInteger
CopyOnWriteArrayList/ConcurrentHashMap等
volatile关键字
和synchronized区别
volatile是轻量级的synchronized,保证了线程的有序性和可见性,共享变量进行了修改后立马可以看到,避免了脏读, 不能保证原子性.但是synchronized可以保证原子性
使用场景: 不能修饰依赖当前值的变量,由于禁止了指令重排序,所以jvm相关优化没了,效率偏弱
关于指令重排
使用双端检锁机制 DCL(double check lock) 加上volatile和代码块中加sync进行两次检索,
为什么会出现脏读
上图为JMM工作图(java内存模型) JMM规定所有变量都存在主内存中,每个线程都有自己的工作内存,不能直接对主内存操作
volatile修饰变量要求每次读取时必须要从主内存获取变量最新值,而且一经修改就必须要写到主内存中,假如线程1对变量v进行修改,那么线程2是可以马上看见
重排指令/happens-before
jvm在编译java代码或者cup在执行jvm字节码的时候,在不改变结果的前提下会对现有指令进行重排,主要目的是优化代码运行效率
指令重排如何避免多线程对数据结果造成影响? 内存屏障(使cup对内存屏障指令之前和之后的存储操作执行结果一样的一种约束)
自我总结:happens-before就是直先行操作数据更新的值一定会被后操作的数据读到,说白点一个把一个变量赋值为1,那后一个变量一定知道a已经变成了1
八大规则
-
程序次序规则:在一个线程内一段代码的执行结果是有序的。就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
-
管程锁定规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
-
volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
-
线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
-
线程终止规则:在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
-
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
-
传递规则:这个简单的,就是happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。
-
对象终结规则:这个也简单的,就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
并发三要素
- 原子性 对数据的操作是不可分割的,一个操作或者多个操作要么成功要么失败.中间不可中断,也不存在上下文切换
int a = 1 //原子操作
a++ //非原子操作
要从主内存读取值到工作内存,将a赋予新值然后再放回去. 可以用synchronized或者lock将这个操作包裹起来变成原子操作,但是volite不行,因为volte不能操作依赖值
- 可见性 一个线程A对共享变量的修改,另一个线程B能够立刻看到
- 有序性: 程序执行的顺序按照代码的先后顺序执行,因为处理器可能会对指令进行重排序
JVM在编译java代码或者CPU执行JVM字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)
进程与线程之间的调度
进程之间的调度有5个模式
- 先来先服务调度
- 高响应比的先调度
- 短作业有点调度
- 时间分片轮询调度
- 优先级调度
进程之间的调度
- 协程式调度(分时调度模式): 线程执行的时间本身是由线程自己决定的,线程执行完工作后主动通知系统执行另一个线程.优点是实现简单.且线程操作自己可知,缺点是如果一个线程出现问题阻塞可能一直都阻塞了.
- 抢占式调度: 由系统控制线程执行时间,不由线程本身决定.不会因为一个线程阻塞而整体阻塞.
java本身就抢占式调度.优先让线程池中优先级高的先分配时间片.
q : wait ,notify本身不就是线程控制吗
a : wait可以让出执行时间,notify后无法获取执行时间而是随机等待
java里面常用的锁
- 悲观锁: 就是很悲观,每次操作数据都会上锁,别的线程再进来时就会阻塞.synchronized
- 乐观锁: 就很乐观,线程操作数据不会上锁,但是在更新的时候会通过版本来判断数据是否有改变.cas是乐观锁,严格来说是通过cas来保证数据的同步性.
小结: 乐观锁适合读多的操作,悲观锁适合写多的操作
- 自旋锁: 一个线程在操作数据的时候上锁了,如果此时另外一个线程尝试获得数据的话将会循环等待,直到获得锁才会退出循环.此状态不会有上下文切换但一直循环获得锁增加cpu消耗,自旋锁不会阻塞
package checkdemo.unsafe.RLock;
import java.util.concurrent.atomic.AtomicReference;
import static java.lang.Thread.sleep;
public class RLockDemo {
//创建源自应用类
AtomicReference atomicReference = new AtomicReference<Thread>();
public void mylock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "\t 进入methodA方法");
while (!atomicReference.compareAndSet(null, thread)) {
}
}
public void unlock() {
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread, null);
System.out.println(thread.getName() + "\t 进入methodB方法");
}
public static void main(String[] args) {
RLockDemo rLockDemo = new RLockDemo();
new Thread(()->{
try {
rLockDemo.mylock();
System.out.println(Thread.currentThread().getName()+"\t 睡5s");
sleep(5000);
rLockDemo.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
try {
rLockDemo.mylock();
System.out.println(Thread.currentThread().getName()+"\t 睡1s");
sleep(1000);
rLockDemo.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"B").start();
}
}
//输出
A 进入methodA方法
A 睡5s
B 进入methodA方法
A 进入methodB方法
B 睡1s
B 进入methodB方法
- 公平锁: 多线程按照顺序来获取锁,比如一个线程组保证每个线程都能获得锁;RenntrantLock
- 非公平锁: 获取锁的方式是随机的,不保证每个线程都能获得锁.synchronized
小结:非公平锁性能高于公平锁,更能重复利用CPU的时间
- 死锁: 两个或者两个以上的资源由于竞争资源或彼此通信造成的阻塞
死锁的必要条件:1. 互斥条件,资源不共享. 2. 请求与保持 线程已经获取资源,单因请求发生阻塞,不释放资源 3. 循环等待条件: 多个线程行成环形链,每个都占用对方 4. 不可抢占:有些资源是不可强占的,当某个线程获得这个资源后,系统不能强行回收,只能由线程使用完自己释放
public class DeadLockDemo {
private static String locka = "locka";
private static String lockb = "lockb";
public void methodA(){
synchronized (locka){
System.out.println("我是A方法中获得了锁A "+Thread.currentThread().getName() );
//让出CPU执行权,不释放锁
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(lockb){
System.out.println("我是A方法中获得了锁B "+Thread.currentThread().getName() );
}
}
}
public void methodB(){
synchronized (lockb){
System.out.println("我是B方法中获得了锁B "+Thread.currentThread().getName() );
//让出CPU执行权,不释放锁
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(locka){
System.out.println("我是B方法中获得了锁A "+Thread.currentThread().getName() );
}
}
}
public static void main(String [] args){
System.out.println("主线程运行开始运行:"+Thread.currentThread().getName());
DeadLockDemo deadLockDemo = new DeadLockDemo();
new Thread(()->{
deadLockDemo.methodA();
}).start();
new Thread(()->{
deadLockDemo.methodB();
}).start();
System.out.println("主线程运行结束:"+Thread.currentThread().getName());
}
}
jvm为了提高锁获取与资源释放而做的优化,针对synchronized的锁升级
- 偏向锁: 一段代码持续被一个线程占用,就会自动获取锁.
- 轻量级锁: 偏向锁被其它线程访问,偏向锁就会升级为自旋锁,其它会不断循环尝试获取锁,不会阻塞且性能会高点
- 重量级锁: 当锁为轻量级锁时,其它线程虽然在自旋,但自旋到一定时间还没获取到锁就会进入阻塞,此时锁升级成为重量级锁.重量级锁会让别的线程进入阻塞,性能也较低
- 可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁.也就是说 线程可以进入任何一个它已经拥有的锁同步着的代码块
- 不可重入锁:若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
小结: 可重入锁能一定程度的避免死锁 synchronized、ReentrantLock 重入锁
- 共享锁 : 也叫读锁,多个线程可以获取到该锁,但是只能读取数据不能做修改跟新
- 非共享锁: 该锁每一次只能被一个线程所持有,加锁后任何线程试图再次加锁的线程会被阻塞,直到当前线程解锁.获得互斥锁的线程即能读数据又能修改数据
Synchronized锁
synchronized用来解决线程安全问题,一般用在静态方法,代码块,普通方法中.是一个非公平,可重入锁
每个对象都有一个锁和一个等待队列,当锁被占用的时候其它线程要进入阻塞等待,锁被释放后随机一个线程可以获取到锁,不保证公平性.
两种形式
- 在方法中时: ++代码生成的字节码文件会生成一个ACC_synchronized标志,线程访问方法时会先检查是否有ACC标志,如果有就去获取monitor,获取成功才会执行,执行完成后会释放monitor,在方法执行期间其它线程无法再次获取monitor,这个被称为隐式同步++
- 在代码块中: ++代码块会生成一个计数器,线程进入代码块通过 monitorenter时,monitor会加一,如果重入锁monitor会再次加一,退出代码块是经过monitorexit方法,计数器减一,当计数器为零的时候才会释放锁让别的线程进入.这个叫显式同步++
jdk1.6对synchronized有什么优化吗
synchronized以前是重量级锁,就是只有重量级锁功能,其它线程会进入阻塞状态,1.6之后升级为偏向锁,线程持续读取一段代码会自动加锁,此时如果有其它线程尝试获取就会升级为轻量级锁,它不会进入阻塞状态而且循环尝试获取锁,循环多次才会进入重量级锁.可以说是优化了获取锁效率减少系统消耗
高性能的Compare and Swap
什么是cas
cas是对比后再交换,是为了保证数据安全性的一种思想,核心包含三个值 内存地址值,预期原值和新值.线程在操作数据的时候如果内存地址值和原值预期值相同时才会对数据进行更新操作.
cas属于乐观锁,较悲观锁有较高新能,sync为悲观锁
++说白点就是线程A操作一个值,内存地址为1,它读的时候如果也为1,内存地址值和预期原值相同了就会进行更新操作,如果读的时候被B线程修改了,它就会自旋直到下次读取成功++
缺点:自旋时间长cpu利用率增加,增加cpu损耗,造成aba问题
CAS存在的ABA问题
如果一个变量V初次读取是A值,并且在准备赋值的时候也是A值,那就能说明A值没有被修改过吗?其实是不能的,因为变量V可能被其他线程改回A值,结果就是会导致CAS操作误认为从来没被修改过,从而赋值给V
给变量加一个版本号即可,在比较的时候不仅要比较当前变量的值 还需要比较当前变量的版本号。
在java5中,已经提供了AtomicStampedReference来解决问题,检查当前引用是否等于预期引用,其次检查当前标志是否等于预期标志,如果都相等就会以原子的方式将引用和标志都设置为新值
并发编程核心底层AQS
AQS是并发的核心思想.属于concurrent.locks包下面的,CountDownLatch、ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的
简单来说 aqs包含三个核心思想
- 计数器state
- 阻塞队列(用来存放未拿到嗦的线程)
- 线程标记(标记线程是谁的)
举例: 线程A调用了Lock方法,state就会+1,并将改锁标记为线程A的,如果A未释放锁且B线程尝试获取锁就会进入阻塞队列等待,当A触发了unlock()方法才有B才有机会拿到锁,但不一定B必能拿到锁
源码中的方法
- acquire(int arg) 类似于加锁操作,tyracquire是尝试获取资源,成功就会返回. tyracquire是独占模式 共享模式下为tyracquireShare
- addWaiter() 根据不同模式将线程加入等待队列尾部.有互斥模式或者共享模式.
- acquireQueued() 使线程在等待队列中获取资源,
- release(int arg) 释放锁
ReentrantLock实现原理
++(自我总结: ReentrantLock是由aqs实现的可重入锁, 在参数中传入true或者false实现公平或者非公平. 其中非公平锁就多了一步判断)++
- 非公平锁在进来后不会查询阻塞队里是否有线程,直接使用cas更新state值,如果为0就尝试获取锁,如果不为0,就走公平锁相同步骤
- 公平锁进来调用tryAcquire方法
- state==0 说明可以获取锁,cas+1进行操作
- state!=0 说明有线程获取锁,则判断是否为同一线程,如果是同一线程 state++
++如果不是同一线程 则进入node队尾 如果没有node节点,则调用enq方法初始化一个++
ReentrantLock和synchronized使用的场景是什么
二者都是悲观锁,适用于并发场景.都是独占锁
- synchronized
- 无法判断锁持有者,是否为重入锁,会造成线程阻塞,只能是非公平锁
- 加锁方式是隐式的,用户不用手动操作,但是不太灵活
- 不用担心线程是否被正确释放
- ReentrantLock
- 通过传入ture 或者false来决定是不是公平锁
- 可以看到是不是重入锁,嗦的持有者
- 需要手动加锁释放锁,复杂场景要保证state使用了一定要释放
- 底层的等待队列是一个node节点
ReentrantReadWriteLock
- 读写锁接口ReadWriteLock接口的一个具体实现,实现了读写锁的分离,
- 支持公平和非公平,底层也是基于AQS实现
- 允许从写锁降级为读锁
流程:先获取写锁,然后获取读锁,最后释放写锁;但不能从读锁升级到写锁
主要思想就是读写分离,读共享,写独占. 写锁可以降级成为读锁,读写共享,适用场景: ReentrantLock是独占锁且可重入的,相比synchronized而言功能更加丰富也更适合复杂的并发场景,但是也有弊端,假如有两个线程A/B访问数据,加锁是为了防止线程A在写数据, 线程B在读数据造成的数据不一致; 但线程A在读数据,线程C也在读数据,读数据是不会改变数据没有必要加锁,但是还是加锁了,降低了程序的性能,所以就有了ReadWriteLock读写锁接口
非读写锁实现
package checkdemo.ReadWrite;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
class MyCache { //模拟资源类
//模拟缓存,多线程缓存中的数据必须要保证可见性,一致性 禁止指令重排,所以用vloatile修饰
private volatile Map<String, String> map = new HashMap<>();
//写操作
public void putMap(String key, String value) {
System.out.println(Thread.currentThread().getName() + "\t 正在写入");
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成");
}
public void getMap(String key) {
System.out.println(Thread.currentThread().getName() + "\t 正在读");
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
String s = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 完成读 " + s);
}
}
/**
* 读写锁demo
*/
public class ReadWriteDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//写操作
for (int i = 0; i < 5; i++) {
final int finalI = i;
new Thread(() -> {
myCache.putMap(finalI + "", finalI + "");
}, String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
final int finalI = i;
new Thread(() -> {
myCache.getMap(finalI + "");
}, String.valueOf(i)).start();
}
}
}public class ReadWriteDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//写操作
for (int i = 0; i < 5; i++) {
final int finalI = i;
new Thread(() -> {
myCache.putMap(finalI + "", finalI + "");
}, String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
final int finalI = i;
new Thread(() -> {
myCache.getMap(finalI + "");
}, String.valueOf(i)).start();
}
}
}public class ReadWriteDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//写操作
for (int i = 0; i < 5; i++) {
final int finalI = i;
new Thread(() -> {
myCache.putMap(finalI + "", finalI + "");
}, String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
final int finalI = i;
new Thread(() -> {
myCache.getMap(finalI + "");
}, String.valueOf(i)).start();
}
}
}
//输出可见 写入一半就被读操作打断了 不合理
1 正在写入
0 正在写入
2 正在写入
3 正在写入
4 正在写入
0 正在读
1 正在读
2 正在读
3 正在读
4 正在读
0 写入完成
1 写入完成
3 写入完成
2 写入完成
4 写入完成
1 完成读 1
0 完成读 null
2 完成读 2
3 完成读 null
4 完成读 4
实现读写分离锁
//核心思想,用ReentrantReadWriteLock实现读写分离
class MyCache { //模拟资源类
private volatile Map<String, String> map = new HashMap<>();
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
//写操作
public void putMap(String key, String value) {
lock.writeLock().lock();
System.out.println(Thread.currentThread().getName() + "\t 正在写入");
try {
TimeUnit.MILLISECONDS.sleep(300);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t 写入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
public void getMap(String key) {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t 正在读");
TimeUnit.MILLISECONDS.sleep(300);
String s = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t 完成读 " + s);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.readLock().unlock();
}
}
}
/**
* 读写锁demo
*/
public class ReadWriteDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
//写操作
for (int i = 0; i < 5; i++) {
final int finalI = i;
new Thread(() -> {
myCache.putMap(finalI + "", finalI + "");
}, String.valueOf(i)).start();
}
for (int i = 0; i < 5; i++) {
final int finalI = i;
new Thread(() -> {
myCache.getMap(finalI + "");
}, String.valueOf(i)).start();
}
}
}
//输出写操作保证了原子性 不可被打断 合理
0 正在写入
0 写入完成
1 正在写入
1 写入完成
2 正在写入
2 写入完成
3 正在写入
3 写入完成
4 正在写入
4 写入完成
0 正在读
1 正在读
3 正在读
2 正在读
4 正在读
2 完成读 2
3 完成读 3
4 完成读 4
1 完成读 1
0 完成读 0
阻塞队列BlockingQueue
并发编程中解决生产消费者模型
- wait()/notify()
- await() / signal()方法
- BlockingQueue阻塞队列
ArrayBlockingQueue
LinkedBlockQueue- put方法将任务存入队尾,如果队列满了就阻塞
- take拿任务,队列空了就阻塞
常见的阻塞队列
- ArrayBlockingQueue: 基于数组实现的队列,需要制定任务大小,FIFO模式先进先出
- LinkedBlockingQueue: 基于链表实现的阻塞队列,如果不指定大小就是Integer.MAX_VALUE,先进先出
- PriorityBlockingQueue: 支持优先级的阻塞队列,可以自定义排序
- DelayQueue: 延迟队列,在指定时间才能获取元素,队列头元素是最接近过期的元素
ConcurrentLinkedQueue
ConcurrentLinkedQueue实现线程安全的原因:
++线程安全三要素: 原子性 可见性 有序性++
- 底层结构是node,链表头部和尾部是head和tail,使用节点变量和内部类属性使用volatile保证了可见性和有序性
- 插入,移除,更新操作使用了cas无锁操作,保证原子性
- 加入多线程并发修改导致cas更新失败,采用for循环保证更新操作成功
线程池
好处:重用存在的线程,减少对象创建销毁的开销,有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,且可以定时定期执行、单线程、并发数控制,配置任务过多任务后的拒绝策略等功能
- newFixedThreadPool 可控制线程最大并发数
- newCachedThreadPool 一个可缓存线程池
- newSingleThreadExecutor 一个单线程化的线程池,用唯一的工作线程来执行任务
- newScheduledThreadPool 一个定长线程池,支持定时/周期性任务执行