我:美女你知道线程是什么东西么
我对象:知道
我(惊讶):哦?那你说说
我对象:我们县城是卢龙
我:。。。
工作几年了,在工作的过程中,从开始自己以为除了正常接触到的线程基本的使用,能接触到线程问题的场景少之又少,到最后发现到处都是线程问题,最后觉得自己还是不够了解线程,花了很长的时间。个人了解线程大概是这样一个阶段。开始刚学习的时候知道线程有两种创建方式,知道怎么运行,然后工作了一段时间发现,大家都在让用线程池去创建线程,不让去自己定义线程运行,虽然当时也不知道是什么意思,但是是按这个方式去做的,再到后来看了美团的一篇关于线程池的文章,发现对线程了解的还是太少,开始补充一些线程方面的知识,过程中每次都会刷新自己的一个认知,知道了线程原来是有自己的生命周期,还可以有返回值,创建线程的方式也那么多,原来我平时写的方法运行其实就是一个线程。原来线程执行的顺序是CPU调度的,线程数设置那么多是为了更好的利用cpu使用率,多线程的情况下会遇到那么多的问题,想要写好多线程的代码原来需要了解到这么多的知识…
简单的将自己的一些理解梳理一下
线程的生命周期
生命周期这个东西,其实在我们平时工作学习的过程中,应该接触过太多了,java对象的生命周期、spring bean的生命周期等等,生命周期的存在本来就是一个必然的东西,任何的行为动作,肯定都不是说能一步到位的。任何形式的生命周期,只是为了让我们可以更好的去控制完成我们预期工作的对象的各个阶段。线程的生命周期简单可以划分为: 创建、就绪、阻塞、停止
,想要看代码中的定义可以查看java.lang.Thread
类内部有一个内部枚举类State
创建
创建状态就是我们刚new
出来一个线程,相当于只是准备好了我们要运行的任务,但是还没有调用start
方法。其实按我自己的理解,我们平时实现线程的方式就两种,一种是自己创建Thread对象,自己创建任务。另外一种是交给线程池创建Thread对象,我们自己创建任务对象,交给线程池执行
下边先简单介绍创建线程的几种方式
Thread
继承Thread类直接实现
package cn.yarne;
/**
* Created by yarne on 2021/8/21.
*/
public class Main extends Thread {
@Override
public void run() {
System.out.println("hello thread:"+Thread.currentThread().getId());
}
public static void main(String[] args) {
new Main().run();
new Main().start();
}
}
上边有个最初级最初级的一个问题是run和start的区别哈,run是直接调用方法,相当于是同步执行,start才是真正启动一个线程,异步执行
Runnable
实现Runnable 接口实现
package cn.yarne;
/**
* Created by yarne on 2021/8/21.
*/
public class Main implements Runnable {
@Override
public void run() {
System.out.println("hello thread:"+Thread.currentThread().getId());
}
public static void main(String[] args) {
Main main = new Main();
main.run();
new Thread(main).start();
}
}
线程池创建
我们使用线程池的去创建的,更确切的说是任务,并不是线程,我把他们分成两种,一种是execute去执行的任务,一种是submit去执行的任务
execute+Runnable
execute执行的就是普通的实现Runnable接口的任务对象,它跟我们上边创建的任务没什么区别,但是我们只需要在调用的时候传入任务,最终执行任务的线程,由线程池管理
package cn.yarne;
import java.util.concurrent.*;
/**
* Created by yarne on 2021/8/21.
*/
public class Main implements Runnable{
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Main());
}
@Override
public void run() {
System.out.println("hello thread:"+Thread.currentThread().getId());
}
}
submit+Callable/Runnable
submit和execute的意思差不多,唯一不同的是,submit是有返回值的
,它可以通过返回值Future
的get()方法,得到具体的返回值,如果没有就是Null,但是通过submit执行的任务,需要等它的任务执行完毕,是阻塞的。
,下边列出三种创建任务的方式
submit(Runnable)
这种和普通的线程结果一样,但是要等到任务线程执行完毕
package cn.yarne;
import java.util.concurrent.*;
/**
* Created by yarne on 2021/8/21.
*/
public class Main implements Runnable{
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<?> submit = executorService.submit(new Main());
System.out.println(submit.get());
executorService.shutdown();
}
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello thread:"+Thread.currentThread().getId());
}
}
submit(Runnable,Result)
这种方式和上边一样,不同的是可以自己定义返回结果
package cn.yarne;
import java.util.concurrent.*;
/**
* Created by yarne on 2021/8/21.
*/
public class Main implements Runnable{
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<?> submit = executorService.submit(new Main(),1);
System.out.println(submit.get());
executorService.shutdown();
}
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello thread:"+Thread.currentThread().getId());
}
}
submit(Callable)
最常用的是这种,因为只有这种方式才能真正的拿到线程的返回值,通过实现Callable接口创建一个任务对象
,重写call方法直接定义任务以及返回值
package cn.yarne;
import java.util.concurrent.*;
/**
* Created by yarne on 2021/8/21.
*/
public class Main implements Callable{
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<?> submit = executorService.submit(new Main());
System.out.println(submit.get());
executorService.shutdown();
}
@Override
public Object call() throws Exception {
System.out.println("hello thread:"+Thread.currentThread().getId());
return "1";
}
}
到底有几种创建线程的方式?
一种
,其实不管是那种方式去创建,最终真正的线程对象创建只有一种,就是new Thread
,包括线程池也一样,线程池创建线程的方式是用工厂模式去创建的,我们点开它默认的工厂类其实就可以看到,依然是用new Thread去创建的线程
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
就绪
线程在调用start
之后进入了就绪状态,为什么说是就绪状态而不是运行状态呢,因为当我们调用start方法之后,其实并不是说这个线程就立马开始执行,只是告诉cpu
我这个可以运行了,但是具体cpu在什么时候才能执行到这个线程,并不在我们的控制范围之。不过在我们正常程序中的语义其实就是运行状态
阻塞
阻塞状态概括了Thread
类中的三种状态
WAITING
有三个方法在执行之后,会让线程进入到WAITING
状态
* <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>
Object.wait
第一个是在我们调用了wait()
方法之后,指定的线程进入到waiting状态,需要使用notify()
方法才能唤醒。在这个状态下的线程,不会占用CPU资源,会让出资源,而自己进入到等待区域。点进去wait可以看到进去的其实是Object
类,也就是说Wait()、notify()、notifyAll()
其实都是Object的方法。所以wait并不单单是线程中等待的一个功能,更多的是Object类中它的语义 Wait()、notify()、notifyAll()这三个方法必须在synchronized同步锁内使用,wait方法可以使当前线程让出CPU资源,并且让出锁资源,让其他线程可以正常竞争持有锁,访问锁内资源
,通常在我们想让当前线程暂停,并且让其他线程正常运行当前syschronized同步锁
内资源时使用
Thread.join
第二个是在调用线程的join
方法之后,当前线程进入到waiting状态,同时和wait一样,释放cpu资源和锁资源
,需要等待指定线程停止之后,当前线程才能运行,原本我也总忘记join
是干啥的,但是后续发现一个能简单理解的一句话:thread1.join()的意思就是,当前线程需要等待thread1这个线程运行结束之后,才能继续往下执行
LockSupport.park
第三个是在调用LockSupport
中的park
方法之后,当前线程进到waiting状态,需要等待手动调用unpark
方法之后,才能继续运行。和wait的不同之处是,park方法在waiting的状态下,不会释放锁资源,会一直占用
,这里有个要提一下的是,如果先调了unpark
方法的话,不论当前线程下边有几个park,都不会再生效了
BLOCKED
阻塞状态其实是线程在被notify方法唤醒之后
的一个状态,因为当线程被notify方法唤醒的时候,并不会立即进入到拿cpu资源的一个状态,而是说还在处于排队等待拿synchronized锁
的一个状态,拿到锁之后,才能正常执行,进入到正常的状态。
TIMED_WAITING
这个状态其实就是说,线程处于waiting状态,但是并不是说需要等待什么指令唤醒,而是说有一个固定的等待时长,时长到了之后,自动唤醒。这个状态下边五个方法执行之后都可以进入,但是具体执行完或者waiting状态的时候锁资源是不是要释放,并不是统一的,而是根据方法的类型确定
* <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>
Object.wait(long)
释放cpu资源,释放锁资源
Thread.sleep(long)
释放cpu资源,不释放锁资源
Thread.jon(long)
释放cpu资源,释放锁资源
LockSupport.parkNanos(long)
和park一样,时间是纳秒级别
LockSupport.parkUntil(long)
和park一样,时间到了自动unpark
销毁
线程run方法内的任务执行完毕
,进行销毁
线程池
为什么要用线程池
我觉得可以拿单例来比较线程池,如果说我们单例模式的作用是为了不去创建重复的对象,导致资源浪费,那么线程池设计的初衷其实一样,因为创建线程也是一个复杂的过程,如果每次使用都new 一个出来,当大量工作需要同时进行的时候,其实浪费了很多的时间以及资源。用线程池去管理线程的生命周期,可以让线程池内的线程不去重复创建,当成可以循环利用的机器,省去了大量的重复的工作,提高效率减少压力。
如何定义一个线程池
Java默认提供了四种线程池的创建方式,四种分别是
Executors.newSingleThreadExecutor()
单例线程池,单个线程运行Executors.newCachedThreadPoo()
缓存线程池,只要有空闲的cpu资源,可以不停的去创建新的线程,空闲的时候回收掉Executors.newFixedThreadPool(int)
固定大小的线程池, 线程不够的时候,数量一直会扩容到我们设置的最大值,并且不会回收空闲的线程Executors.newScheduledThreadPool(int)
固定大小的线程池,可以设置执行时间,按照我们设置的时间周期执行任务
自定义线程池
各种文档都不推荐我们去使用默认提供的线程池,即使使用很简单,其实是因为我们实际在使用线程池的过程中,很难真正的将默认四种线程池匹配到我们的需求当中。并且可能会造成一些问题,比如单例和固定大小的线程池,虽然看起来可以满足我们的需求,但是内部的阻塞队列是一个不定长的队列,可能会一直累加,造成线程一直拥堵。然后另外两种缓存和任务线程池,他们设置的线程池大小是Integer.MAX_VALUE
,这明显就是有问题的,会导致线程池一直会不限制的一直创建新的线程,如果真的有大量的任务的话,这个线程池直接就把cpu跑满了,还有其他任务的什么事,甚至说内存溢出等等。
如何自定义线程池
其实自定义线程池很简单,和Java默认几种线程池的创建方法是一样的,我们代码点进去其实就可以看到,四种不同类型的线程池只是说用不同的参数做了封装而已。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
...
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
...
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
...
从上边代码可以看出来,不论是哪种实现,最终都是基于ThreadPoolExecutor
线程池的实现,我们自定义线程池,就可以按照他们一样的方式,去用参数做一个定制化
自定义线程池的参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
corePoolSize
: 线程池内保留的核心线程数
量,如果线程池内的数量没有达到核心线程数大小,哪怕有空闲直接可以用的线程,线程池也会去新创建线程。maximumPoolSize
:线程池最大线程数
量,如果核心线程不够了,就会新建,直到最大为止keepAliveTime + TimeUnit
: 空闲线程存活时间以及单位
,保证线程合适的时候去销毁一些空闲的,保证线程池尽量只保证核心线程数BlockingQueue
:工作队列
,如果线程数量达到了核心线程数,新来的线程就开始填入到这个队列中,如果这个队列满了,才回去创建新的线程,直到达到最大线程数为止。这个队列会被空闲的线程消费。ThreadFactory
:这个就是创建线程的工厂
,默认的线程池工厂里的线程名称生成比较简单,只是数字累加,不方便我们排查问题,一般来说都建议我们去按照自己的需求实现这个线程工厂RejectedExecutionHandler
: 拒绝策略,当最大线程数满了都处理不过来的时候,就可以去设置如何去处理新来的任务,默认提供了四种,我们也可以自己去实现AbortPolicy
(默认):拒绝之后会抛出RejectedExecutionException
异常CallerRunsPolicy
: 拒绝之后的任务直接在调用线程池执行的主线程里执行
rejectedExecution
: 任务被拒绝之后,直接将线程池内工作队列中最老的任务删掉,将这个任务添加进去
DiscardPolicy
: 任务被拒绝之后,直接丢弃任务
,不要了
多线程会遇到什么问题
篇幅问题,下篇文章继续
总结
文章主要简单介绍了线程的生命周期、线程池的一些基本概念,围绕着这些概念,梳理了一部分的个人理解,真正的理解线程以及线程池的原理以及使用,会让我们在开发的过程中对线程的把控更加随心所欲,多线程遇到的问题会在接下来的文章中继续说明。