1、线程池
1.1、什么是线程池?
线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池中等待下一个任务
1.2、为什么要使用线程池?
- 降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。使用线程池可以进行统一的分配,调优和监控。
1.3、线程池的创建方式
-
使用Executor创建线程池
newFixedThreadPool:使用
LinkedBlockingQueue
实现,定长线程池。public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
newSingleThreadExecutor:使用
LinkedBlockingQueue
实现,一池只有一个线程。public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
newCachedThreadPool:使用
SynchronousQueue
实现,变长线程池。public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
newScheduledThreadPool :创建定时执行任务的线程池 ,创建一个定时执行任务的线程池,底层实现默认使用的是 DelayedWorkQueue
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
-
自定义线程池
package com.song.gulimall.product.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.*; /* * * @program: gulimall * @description * @author: swq * @create: 2021-03-24 23:15 **/ @Configuration public class MyThreadPoolConfig { @Bean public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) { return new ThreadPoolExecutor(pool.getCoreSize(), pool.getMaxSize(), pool.getKeepAliveTime(), TimeUnit.SECONDS, new LinkedBlockingDeque<>(100000), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); } } package com.song.gulimall.product.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /* * * @program: gulimall * @description * @author: swq * @create: 2021-03-24 23:20 **/ @ConfigurationProperties(prefix = "spring.thread") @Component @Data public class ThreadPoolConfigProperties { private Integer coreSize; private Integer maxSize; private Integer keepAliveTime; }
总结:
- 项目必须使用自定义的线程池,
- FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
- CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
1.4、ThreadPoolExecutor
创建线程池的7个参数?
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
- corePoolSize:线程池中的常驻核心线程数,在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
- maximumPoolSize:线程池能够容纳同时执行的最大线程数,此数值必须大于等于1
- keepAliveTime:多余的空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务(阻塞队列)
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
- handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数时,如何来拒绝
1.5、线程池的拒绝策略
什么是线程池的拒绝策略?
等待队列也已经排满了,再也塞不下新任务了,同时线程池中的max线程也达到了,无法继续为新任务服务,阻止新任务进入的一种方式。
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:调用者运行,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案
7.6、线程池的底层原理
底层架构
工作流程
解释:
- 在创建了线程池后,等待提交过来的任务请求。当调用execute()方法添加一个请求任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
- 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
- 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小
总结:
当向线程池提交一个任务后,如果当前的线程数小于核心线程数(corePoolSize),那么就会立即创建线程执行任务,如果当前线程数大于核心线程数,而且阻塞队列容量未满的情况下,会放入到阻塞队列中,如果阻塞队列也满了,但是当前的线程运行数小于最大线程数,那么就会进行扩容,创建非核心线程来立即执行这个任务,如果阻塞队列满了,当前的运行线程数也达到了最大值,那么线程池就会启动拒绝策略。如果有线程的闲置时间超过线程的最大存活时间,那么就会关闭这个线程。
1.7、线程池的参数(最大线程数)
如何查看机器的逻辑处理器个数
System.out.println(Runtime.getRuntime().availableProcessors());
CPU 密集型
- CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
- CPU密集型任务配置尽可能少的线程数量,一般公式:CPU核数+1个线程的线程池
IO 密集型
- 由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2。
- IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。 - IO密集型时,大部分线程都阻塞,故需要多配置线程数:参考公式:CPU核数/(1-阻塞系数),阻塞系数在0.8~0.9之间,比如8核CPU:8/(1-0.9)=80个线程数
2、线程8锁问题
2.1、代码
/* *
* 资源类
*/
class Resource {
public static synchronized void sendEmail() {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("*******sendEmail");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void sendMs() {
System.out.println("*******sendMs");
}
public void sayHello() {
System.out.println("*****sayHello");
}
}
测试demo
public class LockDemo {
public static void main(String[] args) throws Exception {
Resource resource = new Resource();
new Thread(() -> resource.sendEmail(), "A").start();
Thread.sleep(100);
new Thread(() -> resource.sendMs(), "B").start();
}
}
2.2、锁的8问?
2.2.1、问题
1. 标准访问(两个普通同步方法 sendEmail 和 sendMs),1个资源,两线程中间睡眠 100 毫秒,先打印邮件还是短信?
2. 标准访问(两个普通同步方法 sendEmail 和 sendMs),1个资源,在 sendEmail() 方法中睡眠 4 秒,先打印邮件还是短信?
3. 添加普通的 hello() 方法,1个资源,先打印邮件还是 hello?
4. 2个资源,先打印邮件还是短信?
5. 2个静态同步方法(sendEmail 和 sendMs),1个资源,先打印邮件还是短信?
6. 2个静态同步方法(sendEmail 和 sendMs),2个资源,先打印邮件还是短信?
7. 1个静态同步方法(sendEmail),1个普通同步方法(sendMs),1个资源,先打印邮件还是短信?
8. 1个静态同步方法(sendEmail),1个普通同步方法(sendMs),2个资源,先打印邮件还是短信?
2.2.2、答案
1. 标准访问,先打印邮件
2. 邮件设置暂停4秒方法,先打印邮件
对象锁
一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
其他的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法,
锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronized方法
3. 新增sayHello方法,先打印sayHello
加个普通方法后发现和同步锁无关
4. 两个资源,先打印短信
换成两个对象后,不是同一把锁了,情况立刻变化
5. 两个静态同步方法,同一个资源,先打印邮件
6. 两个静态同步方法,同两个资源,先打印邮件,锁的同一个字节码对象
静态同步方法锁的是Class,是全局锁
synchronized实现同步的基础:java中的每一个对象都可以作为锁。
具体表现为一下3中形式。
对于普通同步方法,锁是当前实例对象,锁的是当前对象this,
对于静态同步方法,锁是当前类的class对象
对于同步方法块,锁的是synchronized括号里配置的对象。
7. 一个静态同步方法,一个普通同步方法,同一个资源,先打印短信
8. 一个静态同步方法,一个普通同步方法,同二个资源,先打印短信
静态同步方法锁的是CLass,普通同步方法锁的是对象 加锁的位置不一样,相互无影响
2.2.3、总结
1.当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁,
但是别的实例对象的普通同步方法因为跟该实例对象的普通同步方法用的是不同的锁,
所以无需等待该实例对象已获取锁的普通同步方法释放锁就可以获取他们自己的锁。
2.这两把锁(this/Class)是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有影响的。
3.所有的静态同步方法用的也是同一把锁--类对象本身(Class),
一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,
不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类产生的实例对象