什么是线程?这个问题应该是面试中经常被问的问题。那么今天趁时间梳理下线程的相关知识。
一、线程的基础
1、线程的概念
说到线程不得先说进程,所谓进程是系统进程资源分配和调度的一个独立单元,是操作系统分配资源的做小单位。一个进程包含有多个线程;而线程是程序执行的最小单位。
说概念有点不好理解,那我们看下图吧,我的电脑同时打开了多个软件,打开电脑的资源管理器,可以看到每个软件就是一个进程,一个进程呢又有多个线程,比如我的微信进程中就有52个线程再执行的不同的任务
线程和进程有下面不同点
维度 | 进程 | 线程 |
---|---|---|
内存共享 | 每个进程内存是独立的,进程之间不共享内存 | 同一个进程的线程共享其内存 |
上下文切换 | 上下文切换成本高 | 因为是轻量级,所以上下文切换成不低 |
2、并发&并行
**并发:**同一时间应对多个事情的情况,多个线程共用一核cpu,一个通过cpu时间片来回切换处理不同的线程
**并行:**同一时间处理多个事情,多核cpu处理多个线程,比如4核cpu,一个cpu处理一个线程
对于单核cpu而言,操作系统中的任务调度器,同时把cpu时间片分配不同的程序使用,但是线程之间切换非常快,人们根本感觉不出来。所以单核cpu的系统执行程序,微观角度是属于串行,宏观角度是并行
对于多核cpu,多个cpu分别处理不同的程序,这种就是并行
3、线程的创建方法
3.1 继承Thread类
public class MyThred {
public static void main(String[] args) {
myThread myThread = new myThread();
myThread.start();
}
}
class myThread extends Thread{
@Override
public void run() {
System.out.println("Thread... running");
}
}
3.2 实现Runnable接口
public class MyThred {
public static void main(String[] args) {
Thread thread = new Thread(new myRunnAble());
thread.start();
}
}
class myRunnAble implements Runnable{
@Override
public void run() {
System.out.println("my runnable running!!");
}
}
3.3 实现callable接口
public class MyThred {
public static void main(String[] args) throws Exception {
mycallable mycallable = new mycallable();
FutureTask futureTask=new FutureTask(mycallable);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
class mycallable implements Callable{
@Override
public Object call() throws Exception {
System.out.println("my call able running");
return true;
}
}
3.4 通过线程池
public class MyThred {
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.submit(() ->{
System.out.println("Executors is running");
});
executorService.shutdown();
}
}
对比以上四种创建线程的方法,我们可以发现有这些不同
1、返回值:通过callable实现的线程是有返回值的,可以FutureTask的get()方法获取线程返回的结果,而通过Thread和runnable实现的线程是没有返回值的
2、异常处理:通过callable实现的线程的异常是可以向上抛出和内部处理,但是通过thread和runnable实现的异常只能内部捕获处理
4、线程的状态
4.1线程的6种状态
上面我们说了线程的基本概念和创建方法,那么线程具体有哪些状态呢?我们接着来讲。我们直接来看源码,我们进入Thread类找到 State枚举,可以看到关于线程定义了new (新建)、runnable(可执行)、blocked(阻塞)、waiting(等待)、timed_waiting(时间等待)、terminated(终止)6种状态
/**
* A thread state. A thread can be in one of the following states:
* <ul>
* <li>{@link #NEW}<br>
* A thread that has not yet started is in this state.
* </li>
* <li>{@link #RUNNABLE}<br>
* A thread executing in the Java virtual machine is in this state.
* </li>
* <li>{@link #BLOCKED}<br>
* A thread that is blocked waiting for a monitor lock
* is in this state.
* </li>
* <li>{@link #WAITING}<br>
* A thread that is waiting indefinitely for another thread to
* perform a particular action is in this state.
* </li>
* <li>{@link #TIMED_WAITING}<br>
* A thread that is waiting for another thread to perform an action
* for up to a specified waiting time is in this state.
* </li>
* <li>{@link #TERMINATED}<br>
* A thread that has exited is in this state.
* </li>
* </ul>
*
* <p>
* A thread can be in only one state at a given point in time.
* These states are virtual machine states which do not reflect
* any operating system thread states.
*
* @since 1.5
* @see #getState
*/
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
4.2 线程的6种状态的变化规则
1、新的线程创建后,线程就处于new的状态
2、新建的线程执行start()方法,线程处于准备就绪可执行状态,但是这个时候还没有执行权,需要向cpu申请执行权限,获取权限后线程可以执行,没有获取执行权限后线程需要重新申请执行权。这个状态处于runnable状态
3、当程序加锁后,线程再执行时没有获取到锁,线程会进入到blocked状态,重新尝试获取锁,获取到锁重复第2步
4、当线程遇到wait()方法,线程进入waiting状态,直到再次调用notify()或notifyAll()方法,线程才会继续第二步开始执行。其中notify()和notifyAll()的区别在于notify()随机唤醒一个线程,而notifyAll()获取所有处于waiting状态的线程
5、当线程遇到sleep()方法,线程进入计时等待状态(timed_waiting),计时结束后继续第二步
6、线程执行结束后,进入最后一个状态终止terminated
4.4 wait()和sleep()的对比
上面我们提到过wait()和sleep()两种方法,两者均可以让线程处于等待状态,那他们有什么不同点呢
1、从上面的流程图我们不难看出不同点1是wait()等待的线程必须通过notify()或者notifyAll()才能唤醒,而sleep()计时结束后自动唤醒
2、方法的归属不一致,sleep()是Thred类的静态方法,而wait()是object类的成员方法,每个对象都有这个方法
3、wait()方法的调用必须先获取wait对象的锁,而sleep则没有这种限制
wait方法在执行完后会释放对象锁,其他线程可以正常获取该对象锁(我用完了,你们用吧)
sleep在synchronized修饰的代码块中执行时,不回释放对象锁,其他的线程不能获取对象(我睡500ms的时候抱着锁睡觉谁也不别想用)
下面我们结合代码来看下
public class MyThred {
static Object lock =new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
synchronized (lock) {
System.out.println("starting");
try {
lock.wait(1000);
System.out.println("running end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
Thread.sleep(1000);
synchronized (lock){
System.out.println("sleep....");
}
}
}
如上执行顺序是打印了staring后直接打印了sleeping,说明wait没有影响后面的代码执行,后续的方法可以正常获取锁,
starting
sleep....
running end
把lock.wait(1000);改为 Thread.sleep(1000);后执行顺序就是下面的顺序,可见sleep方法在等待的1000ms期间是没有释放锁的
starting
running end
sleep....
二、线程池
1、线程池的原理
创建线程池时是通过实例化 ThredPoolExecutor类实现,我们打开ThredPoolExecutor类的构造方法可以看到,创建线程是有以下几个核心的参数:
1、corePoolSize:核心线程树
2、maximumPoolSize:最大线程数(等于核心线程+空闲线程)
3、keepAliveTime:(非核心线程的等待时间,当线程数大于核心线程时,空闲线程在销毁前最等待新的任务的最大等待时间) when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
4、unit:临时线程最大等待时间的单位
5、workQueue:阻塞队列,当没有空闲的核心线程时,任务会添加到这个队列中等待核心线程释放,当这个队列也满了时,会创建空闲队列执行执行队列中的任务
6、threadFactory:线程工厂,可以定义线程对象的创建,比如线程的名字,是否时守护进程
7、handler:拒绝策略,在线程数大于最大线程数时,会执行拒绝策略,具体有下面四种
a、AbortPolicy() :直接抛出异常
b、CallerRunsPolicy:用调用者所在的线程执行
c、DiscardOldestPolicy():丢弃阻塞队列最前面的任务,执行执行当前任务
d、DiscardPolicy():丢掉任务不予处理
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
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;
}
创建线程有以上的7种核心的参数,那么线程池的执行原理是什么呢,我们来看下面的图
1、提交任务
2、判断当前线程数是否大于核心线程,如果不大于核心线程,核心线程会直接执行任务,否则继续判断阻塞队列是否已满
3、阻塞队列如果没有满,任务添加到阻塞队列中,核心或者非核心线程执行完任务后会自动检查队列中是否存在未执行的任务,执行对应的任务,反之继续判断当前线程数是否大于最大线程数
4、如果当前线程数大于最大线程数,执行对应的拒绝策略,否则创建非核心线程执行对应的任务
2、线程池中的阻塞队列
上面我们说过,当提交任务当前线程数大于核心线程数但是阻塞队列没有满时,任务会被添加到阻塞队列中,那么具体阻塞队列有哪几种实现方式,又有什么区别呢?
常用的用LinkedBlockingQueue和ArrayBlockingQueue
如下代码
ThreadPoolExecutor executor =
new ThreadPoolExecutor(4 ,
10,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3000),
(r, executor) -> log.error("thread pool full,reject"));
LinkedBlockingQueue | ArrayBlockingQueue |
---|---|
基于链表实现 | 基于数据实现 |
长度默认不设置,可以结束设置长素 | 强制设置长度 |
头尾两把锁,效率会高些 | 一把锁,相对应效率会低 |
3、核心线程的设置
前面我们已经对线程池有一定的了解了,那么我们在实际的开发过程中应该设置多少核心线程为好呢?
首先我们先说两个概念,我们运行的程序一般分为下面两种
1、IO密集型任务:文件读写 ,网络请求较多
2、CPU密集型任务:计算型代码、gson转换操作较多
基于不同的任务类型,我们确认核心线程数时,可以参考这样的原则
1、对于高并发,执行时间短。核心线程数:N+1
2、对于并发不高,执行时间长
a、IO密集型任务 核心线程数:2N+1
b、CPU密集型任务 核心线程数:N+1
N:cpu核数,可以通过代码输出
public static void main(String[] args) {
System.out.println("cpu 核数:"+Runtime.getRuntime().availableProcessors());
}
实际打印:cpu 核数:8
4、线程池的种类
我们进入Executors类可清楚的看到Executors类中有很多创建线程池的方法,但是常用的有下面几种,我们一起看下他们的区别
1、newFixedThreadPool
提供一个核心线程数的参数,核心线程数和最大线程数相同
阻塞队列为LinkedBlockingQueue,最大长度是Integer.MAX_VALUE
源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
我们看下面的例子,实例化核心线程数是3的线程池,循环10次提交任务,执行接口看出,并没有第4个线程执行,就说明了这种类型创建的线程方式,任务会进入阻塞队列,等待核心线程空闲后执行,不回创建超过最大线程的线程执行任务
public class ExecutorsTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executorService.submit(new MyRunnable1(i));
}
executorService.shutdown();
}
}
class MyRunnable1 implements Runnable{
int flag;
MyRunnable1(int val){
this.flag=val;
}
@Override
public void run() {
System.out.println("线程:"+Thread.currentThread().getName()+": "+flag );
}
}
执行结果:
线程:pool-1-thread-3: 2
线程:pool-1-thread-2: 1
线程:pool-1-thread-2: 3
线程:pool-1-thread-2: 5
线程:pool-1-thread-3: 4
线程:pool-1-thread-3: 7
线程:pool-1-thread-3: 8
线程:pool-1-thread-3: 9
线程:pool-1-thread-2: 6
线程:pool-1-thread-1: 0
2、newSingleThreadExecutor
通过下面的源码我们可以看到这种线程池,没有入参,固定了一个核心线程数,最大线程数等于核心线程数
由于只有一个线程,它能够保证任务按照顺序执行,先提交的先执行
阻塞队列为LinkedBlockingQueue,最大长度是Integer.MAX_VALUE
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
相较于newFixedThreadPool,我们可以通过打印的结果可以看出,同一个线程顺序执行提交的任务
public class ExecutorsTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
executorService.submit(new MyRunnable1(i));
}
executorService.shutdown();
}
}
class MyRunnable1 implements Runnable{
int flag;
MyRunnable1(int val){
this.flag=val;
}
@Override
public void run() {
System.out.println("线程:"+Thread.currentThread().getName()+": "+flag );
}
}
执行结果:
线程:pool-1-thread-1: 0
线程:pool-1-thread-1: 1
线程:pool-1-thread-1: 2
线程:pool-1-thread-1: 3
线程:pool-1-thread-1: 4
线程:pool-1-thread-1: 5
线程:pool-1-thread-1: 6
线程:pool-1-thread-1: 7
线程:pool-1-thread-1: 8
线程:pool-1-thread-1: 9
3、newCachedThreadPool
可缓存线程池,没有入参,核心线程数为0,最大线程数为Integer.MAX_VALUE
相较于上面两种阻塞队列不同这种线程池的阻塞队列为SynchronousQueue,它是一种不存储元素的队列,每次插入的操作都必须等待一个移除操作
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
我们来实际测试下可缓存线程池,循环10次提交任务,通过打印结果我们可以看出,我们线程池创建了9个线程执行这10个任务,那么为什么会有一个线程执行了两个任务呢,是因为当任务在提交后会判断线程池存在临时线程否,如果存在临时线程,临时线程会执行对应的任务,如果不存在临时线程会创建新的线程执行任务。我们从结果中看到线程6执行了两个任务也是这个原因了
public class ExecutorsTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.submit(new MyRunnable1(i));
}
executorService.shutdown();
}
}
class MyRunnable1 implements Runnable{
int flag;
MyRunnable1(int val){
this.flag=val;
}
@Override
public void run() {
System.out.println("线程:"+Thread.currentThread().getName()+": "+flag );
}
}
执行结果:
线程:pool-1-thread-3: 2
线程:pool-1-thread-1: 0
线程:pool-1-thread-2: 1
线程:pool-1-thread-4: 3
线程:pool-1-thread-6: 5
线程:pool-1-thread-8: 7
线程:pool-1-thread-7: 6
线程:pool-1-thread-9: 8
线程:pool-1-thread-6: 9
线程:pool-1-thread-5: 4
4、newScheduledThreadPool
可延迟执行线程池,从名称可以看出这种类型在最大的区别就是提供了可以延迟执行的方法
提供一个核心线程数的入参
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
我们直接从执行结果来分析它的特点,这种方式有几个不同之处:
1、创建的对象是ScheduledExecutorService
2、提交了任务不是用的submit,而是ScheduledExecutorService中的schedule,并设置任务执行的延迟时间
从结果可以看出,我在打印了当前时间后,延迟了指定的时间后才执行提交的任务
public class ExecutorsTest {
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
System.out.println(LocalDateTime.now());
for (int i = 0; i < 10; i++) {
executorService.schedule(new MyCallAble1(i),3, TimeUnit.SECONDS);
}
executorService.shutdown();
}
}
class MyCallAble1 implements Callable{
int flag;
MyCallAble1(int val){
this.flag=val;
}
@Override
public Object call() throws Exception {
System.out.println("线程:"+Thread.currentThread().getName()+": "+flag+ LocalDateTime.now());
return null;
}
}
5、Executors创建线程池的缺点
上面我们聊完了Executors创建线程池的方法,但是有些时候是不推荐使用Executors创建线程池的
因为上述前两种阻塞队列的最大长度和第三种可缓存线程池的最大线程数均是Integer.MAX_VALUE,可能会导致oom,所以还是推荐通过ThreadPoolExecutor实现
三、ThreadLocal类
ThreadLocal类实现了让每个线程各自使用自己的资源对象,避免出现线程安全问题。
我们可以从下面的源码看出,每个线程在set数据时,底层是维护了一个ThreadLocalMap,当前线程作为key,存储的数据做为value,实现线程之间的隔离
内存泄漏的的问题
由于ThreadLocalMap中的key是弱引用,值为强引用,gc会回收key,但是不会回收value,所以会造成内存泄漏,我们在使用ThreadLocal时,需要主动remove,释放对应的key和value
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}