为何选择线程池,不用时创建线程呢?
池化技术:提前准备一些资源,在需要时可以重复使用这些预先准备的资源。常见的池化技术的使用有:线程池、内存池、数据库连接池、HttpClient 连接池。
线程池作为池化技术的一种实践,本质上也是同样的思想,提前备好资源以备不时之需。因此,线程池相比较任务出现再创建线程具有以下的优点:
- 降低资源损耗:通过重复利用已创建的线程降低线程创建和销毁造成的损耗。
- 提高响应速度:当任务到达时,可以不需要等到线程创建完毕就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。
当一个任务进入线程池,发生了什么?
如上图所示,一个新任务进入到线程池时,处理流程如下:
判断核心线程池的线程是否都在执行任务,如果不是,则创建一个工作线程来执行此任务
当核心线程池已满时,进入工作队列等待
当工作队列已满,判断线程池是否达到最大线程数,不是,则创建一个工作线程来执行此任务
如果工作队列和线程池都满了,则交给饱和策略来处理这个任务
进程和线程的区别
在操作系统中,正在运行的程序就是进程。比如:QQ,游戏,idea工具等等
说起进程就要提一下程序。程序是指令和数据的有序集合,本身没有运行的含义,是一个静态的概念。
进程就是执行程序的一次过程,是一个动态的概念。系统资源分配的单位。
通常一个进程中可以包含多个线程,并且,一个进程中至少有一个线程。
线程是CPU调度和执行的单位。
如何使用一个线程池?
JAVA有几种线程池
- newFixedTreadPool方法创建一个固定长度的线程池。
- newCachedTreadPool方法创建一个可缓存的线程池。如果线程池数量超过需求,可以回收空闲线程;如果不够用,则创建新线程。
- newSingleThreadExecutor方法创建一个单线程化的线程池。只用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO、LIFO、优先级)执行。
- newSingleTreadScheduledExecutor方法返回一个ScheduleExectorService对象,线程大小为1,可以在固定延时之后执行,或者周期性执行某个功能。
线程池七个核心参数的含义
- corePoolSize:线程池中常驻的核心线程数。
- maximumThreadPool:线程池中能够同时执行的最大线程数。
- keepAliveTime:多余的空闲线程存活时间。当线程池中超过corePoolSize的线程,达到
keepAliveTime时间就会被销毁,保留corePoolSize数量的线程 - unit:keepAliveTime的时间单位。
- workeQueue:任务队列。线程被提交尚未执行的任务。
- threadFactory:线程工厂。用于创建线程。
- handler:拒绝策略。当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时,如何拒绝来请求的Runnable的策略。
JAVA有几种线程池
- newFixedTreadPool方法创建一个固定长度的线程池。
- newCachedTreadPool方法创建一个可缓存的线程池。如果线程池数量超过需求,可以回收空闲线程;如果不够用,则创建新线程。
- newSingleThreadExecutor方法创建一个单线程化的线程池。只用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO、LIFO、优先级)执行。
- newSingleTreadScheduledExecutor方法返回一个ScheduleExectorService对象,线程大小为1,可以在固定延时之后执行,或者周期性执行某个功能。
都不推荐使用!建议使用自定义线程池!
线程池的创建
我们在创建线程池的过程中,使用底层的new ThreadPollExecutor,
package com.markor.template.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* @describe:
* @author: caichangmeng <modules@163.com>
* @since: 2018/10/22
*/
@Configuration
public class ThreadConfig {
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(3);
taskExecutor.setMaxPoolSize(7);
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
return taskExecutor;
}
}
其源码如下
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
可以看到,线程池创建的七大参数:
- corePoolSize:线程池的基本大小
- runnableTaskQueue:任务队列
- ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂来给线程设置有意义的名字
- RejectedExecutionHandler:饱和策略,默认情况下是AbortPolicy,JDK1.5提供了一下四种策略
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:只用调用者所在线程来运行任务
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
- DiscardPolicy:不处理丢弃
- keepAliveTime:线程池的工作线程空闲后,保持存活的时间
- TimeUnit:线程活动保持时间的单位
向线程池提交任务
目前分为executre()和submit()方法
//execute()提交不需要返回值的任务
threadPool.execute(new Runnable() {
@Override
public void run() {
//TODO
}
});
//submit()提交需要返回值的任务
Callable myCallable = new Callable() {
@Override
public String call() throws Exception {
Thread.sleep(3000);
return "call方法返回值";
}
};
Future future = threadPool.submit(myCallable);
String futureRes = future.get();
关闭线程池
//会停止所有正在执行任务的线程
threadPool.shutdownNow();
//只中断没有执行任务的线程
threadPool.shudown();
简述线程池的处理流程
- 当线程池处理任务时,首先判断核心线程是否已满,没有满则创建核心线程执行。
- 如果核心线程满了,则判断任务队列是否已满,没有满则把任务放到任务队列中。
- 如果任务队列也满了,则判断是否达到最大线程数,没有达到则创建临时线程执行。
- 如果已达到最大线程数,则根据拒绝策略处理任务。
线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?
普通队列只能作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了;
阻塞队列可以保留住当前想要继续入队的任务,使得线程进入wait状态,释放cpu资源,阻塞队列自带阻塞和唤醒的功能。
在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。
合理的配置线程池
1.线程数的配置:
目前公司的项目,用简单的HttpServer暴露prometheus的metrics的内容,设置的是fixThreadPool,线程数设置为5,就是根据Ncpu * 2得到的经验值。
2.建议使用有界队列
有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千。
threadlocal
threadlocal类的作用
threadlocal类用来提供线程内的局部变量,在多线程环境下访问时,多个线程内的变量不会互相干扰。
threadlocal和synchronized的区别
都用于多线程并发访问变量的问题。但是处理角度和思路不同
- synchronized关键字原理:同步机制采用了以时间换空间的方式,只提供一份变量,让不同的线程排队访问。(侧重于多个线程之间访问资源的同步)
- threadlocal类原理:采用了以空间换时间的方式,为每一个线程提供变量的副本,实现同时访问而相互不干扰。(侧重于多个线程访问,让每个线程的数据相互隔离)
volatile
volatile关键字的作用和原理
实现多线程访问共享变量的可见性:一个线程修改了变量,其他的线程可见。
JMM的Java内存模型,共享变量都保存在主内存中。各个线程中使用的都是从主内存中复制的变量副本。使用volatile关键字修饰共享变量时,在第三步子线程修改了共享变量的值并提交给了主内存中,CPU会通过一种机制,让主线程中的该变量副本失效,让后从主内存中重新获取最新的变量副本。
volatile和synchronized的区别
- volatile只能修饰实例变量和类变量;synchronized可以修饰方法和代码块。
- volatile保证数据的可见性,但是不保证数据的原子性(多线程写操作,不保证线程安全); synchronized是一种排它机制,保证可见性,也保证原子性。
- volatile用于禁止指令重排。
线程
线程的生命周期和状态
线程的5个状态:
创建状态:Thread t = new Thread线程一旦创建就进入到新生状态。(new)
就绪状态:当调用start()方法,线程立即进入就绪状态;但并不一定立即被调用执行。
运行状态:获得CPU调度进入运行状态;进入运行状态,线程才开始执行线程体的代码块。
阻塞状态:当调用sleep、wait或同步锁时,线程进入阻塞状态(即代码不再执行),阻塞事件解除后,重新进入就绪状态,等待CPU调度。
死亡状态:线程中断或者结束,线程进入死亡状态;一旦进入死亡状态,就不能再次启动。
详情:https://www.kuangstudy.com/bbs/1363147273537597441#header19
sleep()、wait()、join()、yield()的区别
等待池
等待池是针对wait方法的,当我们调用wait方法时,线程会进入等待池中,等待池中的线程不会去竞争锁,只用调用了notify、notifyall等待池中的线程才会去竞争锁。
notify方法是随机让等待池中的一个线程放入锁池,notifyall是让等待池中所有的线程进入锁池。
锁池
所有需要竞争同步锁的线程会放在锁池中,比如当前对象的锁被一个线程拿到,那其他的线程就需要在锁池中等待,当前面的线程释放锁后,其他线程再去竞争锁,拿到锁的线程就会进入就绪状态,等待CPU的分配。
区别
- sleep是thread类的一个静态方法,wait是Object类的本地方法。
- sleep方法不会释放锁,wait方法会释放锁,并进入到等待队列中。
- sleep方法不依赖于同步器synchronized;但是wait需要依赖synchronized关键字使用。
- sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要唤醒。(不指定时间需要被别人唤醒)。
- sleep一般用于当前线程休眠,或者轮循暂停操作;wait则多用于多线程之间的通信。
- yield()方法执行后,线程进入就绪状态,释放CPU的执行权,让CPU重新进行调度。
- join执行后线程进入阻塞状态;比如B线程调用A线程的join方法,B线程进入阻塞状态,直到A线程线程或中断,才会继续执行B线程的代码。
死锁
什么是死锁?怎么解决死锁
多个线程持有共享资源的一部分,都互相需要对方手里的资源才能执行,产生了僵持,都不往下执行。比如:线程A持有锁A,想要获得锁B;线程B持有锁B,想要获得锁A。
产生死锁的四个必要条件
互斥条件:当资源被一个进程使用时,别的进程不能使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持占用。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生。
互斥条件是非共享资源所必须的,应加以保持不应破坏;所以破坏其他三个条件解决死锁:
1.破坏请求与保持条件:所有的进程在开始运行之前,必须一次性地申请所有需要的资源。如果无法一次性申请,那就进行等待。
2.破坏不剥夺条件: 当某个进程获得一部分资源,要去申请另一个资源时,如果申请不到,那就主动释放自己占用的所有资源。
3.破坏循环等待条件:可以给每个资源标上序号,按序申请,先申请资源序号小的,再申请资源序号大的,就可以避免循环等待。
还有著名的银行家算法避免死锁。
https://www.kuangstudy.com/bbs/1363147273537597441#header35
线程通信的方式
主要是通过wait方法、notify方法进行线程之间的通信。比如:
管程法:并发协作模型“生产者/消费者模式”>管程法
- 生产者:负责生产数据的模块(可能是方法,对象,线程,进程);
- 消费者:负责处理数据的模块(可能是方法,对象,线程,进程);
- 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”,生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据。
信号灯法:并发协作模型“生产者/消费者模式”->信号灯法
设置标志位,通过标志位来进行通知需要等待和需要唤醒的线程。
比如:演员表演时,观众需要等待(true);演员等待时(false),观众进行观看。
https://www.bilibili.com/video/BV1V4411p7EF?p=24
谈谈你对线程安全的理解
线程安全就是内存安全,堆是共享的内存,可以被所有的线程访问。
线程安全
当多个线程访问同一个对象,如果不进行额外的同步控制,调用这个对象的行为都可以获得正确的结果(和单线程执行的结果一致),我们称之为线程安全。
堆
- 堆是进程和线程共有的空间,分全局堆和局部堆;全局堆就是所有没有分配的空间,局部堆就是分配给进程的空间。
- 堆是在操作系统对进程进行初始化的时候分配的,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。
(在Java中,堆是在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。)
栈
栈是每个线程独有的,保存其运行状态和局部自动变量的。
栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。
操作系统在切换线程的时候会自动切换栈。(栈空间不需要在高级语言里面显式的分配和释放)