Java多线程知识点
这里先写出一部分的知识点在后续的增加中
线程
线程和进程的区别
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:
- 进程:操作系统分配资源的最小单位,其中一个进程可以包含多个线程;
- 线程:资源调度的基本单位,因为线程之前的切换消耗的能源比较低,高并发一般操作的就是这里;
并发和并行的区别
- 并发是指在同一时间段内交替执行的程序,而并行是指在同一时刻一起执行
线程的实现方式
这里一共介绍三种方式,基础的方法是继承Thread方法,这种方式受Java当继承的局限性代码就变的不够灵活了,但是接口可以重复实现,我们直接实现Runnable再使用Thread调用,由于run方法没有返回值,如果我们想让调用的方法有返回值时,我们在可以通过实现Callable接口的方式来获得返回值。
- 实现 Runnable接口,重写其中的run方法
- 这里做一点点延伸,run方法直接被调用的话是不会启动线程的,只会在调用的线程中执行
- 调用之后再调用star()才会启动线程且将线程调整到了就绪态(这时只要获得时间片就可以启动了,然后再调用run的方法)
- 继承Thread线程类,重写run方法启动时直接创建调用star方法就好了(这里其实也是实现Runnable方法)
- 实现Callable接口,这个接口和Runnable接口就不一样了,这个接口并不能直接传入Thread使用,并且需要重写的方法是call()
- 原因:由于thread 类的构造方法中根本就没有 Callable 这个类型的参数 , 这时我们无法直接使用 Callable 来实现多线程的
- 如何解决:借助Java的多态性,使用FutureTask类做一个中间转换,将FutureTask传入到Thread类即可,FutureTask.get()方法可以获得返回值。
FutureTask<String> stringFutureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
return "hello world";
}
});
new Thread(stringFutureTask,"A").start();
线程的启动方式
- 如果单单调用实现或者类中的方法时,这种操作不是启动了一个新的线程,只是用原线程执行了这个方法
- 调用start()启动是启动了线程并且将线程调整到了就绪态(这时只要获得时间片就可以启动了,然后再调用run的方法)
线程的状态
我们上面提到了调用start()方法后线程就会进入到了就续态,那么我们了解一下线程一共有几个状态,以及每个状态的含义
- 新建态(New):当new一个新的线程时,这个线程就进入了新建的状态。
- 就绪态(Runnable):当线程调用了start()方法后,线程就进入了就绪态,等待获得CPU的使用权
- 运行态(Running):获取了时间片(即CPU的使用权)就可以执行run()方法的内容,到达运行态了
- 阻塞态(Blocked):线程在获取锁失败时(因为锁被其它线程抢占),它会被加入锁的同步阻塞队列,然后线程进入阻塞状态。此时如果在下一次争夺中获取到了锁,就会到达就绪态进行排队
- 等待状态(WAITING):此时也叫条件等待状态,当线程的运行条件不满足时调用wait()或await()方法使线程进入等待状态,等待条件满足时,使用notify()或notifyAll()方法唤醒 ,由于这种方式在等待前就释放掉了锁, 被唤醒后进入阻塞态争夺锁资源
- 限时等待状态(Timed Waiting):限时等待主打的就时一个限时,即调用Thread.sleep()方法或者带有超时限定的Object.wait()、Thread.join()方法等进行等待,等待结束之后自己可以唤醒自己,由于此种方式不会释放锁资源,唤醒后直接进入就绪态等待获得CPU使用权
- 终止态(TERMINATED):线程执行完了或者因异常退出了run()方法,该线程结束生命周期
线程的锁
我们这里先不做过多对锁做解释,我们先简单的对于锁做一个字面意思的理解,就是相当于给我们写出的代码加一个锁,这把锁可以加到一个变量也可以加到一个方法上。
死锁
死锁的大白话解释
讲到锁这里我想先给大家介绍一下死锁,用大白话来举个例相当于你用有两个或者多个锁,锁子互相锁着对方的钥匙,你要进门就需要打开这些锁,但是现在你哪个钥匙都需要打开对方才行,都拿不到,这种现象就时死锁的状态。
死锁具备的条件
- 互斥条件 :该资源在同一时刻只能有一个线程占用(相当于钥匙只能被一个锁拿着)
- 请求与保持条件 :该资源在同一时刻只能有一个线程占用(相当于上面解释的,从别的地方拿钥匙的时候,不能把自己手里的钥匙扔了)
- 不可被剥夺条件:自己获得的资源没用完之前不能被强行的剥夺掉(相当上面说的自己已经锁住了,不打开自己拿不走现在手里的钥匙)
- 循环等待:多个资源之间形成一个环形的资源等待关系(大家都拿着别人钥匙但是都不能给别人)
如何避免死锁的发生(破坏上述至之一就行)
- 破坏互斥条件:这东西不能破坏
- 破坏请求与保持:直接一下子申请所有资源,不请求了
- 破坏不剥夺条件:自己要的资源申请不到了就直接摆烂,自己的也释放了
- 破坏循环等待条件:资源进行排序,大家排队按照次序进行访问
公平锁和非公平锁
这里先提前和大家介绍一下这两锁的区别,方便下面内容的理解
- 公平锁:十分公平,严格遵守先来后到的顺序
- 非公平锁:可以进行cpu的插队操作(这里多嘴一下,CPU的插队操作是操作系统层面上的,多个线程有执行的时间长,有执行的时间短,一般情况下是不能按照公平锁来运行程序的、如果按照公平锁时间长的先进行排队,就会造成时间较短的线程一直处于排队的情况,会造成体验感极差的现象)
synchronized和lock的区别
synchronized | lock | |
---|---|---|
是一个关键字 | 是一个接口 | |
是否响应中断 | 只能等待获取锁 | lock中可以使用interrupt中断等待 |
判断是否获取到了锁 | 不支持 | 通过trylock来判断是否获取了锁 |
自己解锁 | 支持 | 不支持需要自己手动解锁 |
使用上 | 适合少量的代码同步问题 | 适用于大量的代码同步问题 |
是否能尝试获得锁 | 否 | 是 |
是否是非公平锁 | 是非公平锁 | 可以通过设置是公平锁还是非公平锁,默认是非公平锁(Lock lock = new Reentrant(true); //ture表示公平锁,先来先得) |
线程池相关的问题
什么是线程池
- 有一些基础不是很好的同学可能不是很了解,顾名思义线程池最本质就是一种池化技术,线程就相当于池中的船,如果每次使用我们都需要进行创建船、用完了之后我们再销毁
使用线程池的好处
- 降低资源消耗,每个线程都可以被重复利用
- 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行
- 提高线程的可管理性,更方便管理
线程池的创建方式
一共是四种创建方式但是在阿里的手册中不推荐使用这几种,这里简述一下想要深入了解的可以自己私下查询一下
- newSigleThreadExector :创建一个单线程的线程池,池中只有一个线程在直接,保证线程执行的顺序
- newFixedThreadPool:创建一个单线程的线程池,池中只有一个线程在直接,保证线程执行的顺序
- newSigleThreadExector :遇强则强,不对大小进行限制,是服务器最大的线程数
- newScheduledThreadPool:定时周期的执行任务
为什么阿里操作手册使用原生的创建方式
- FixedThreadPool和SigleThreadExector请求队列的最大长度设置的长度过大,可能会造成OOM溢出的问题
- CachedThreadPool和ScheduledThreadPool最大线程数设置的过大,也可能回导致OOM异常
线程池创建时的七个参数
线程池创建时的七个参数
- 核心线程数量(corePoolSize)
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。 - 最大线程的数量(maximumPoolSize)
线程池同时存在的最大线程数量,当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列(后面会介绍)中。如果队列也已满,则会去创建一个新线程来出来这个处理。 - 工作队列的长度(workQueue)
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务 - 空闲线程存活时间(keepAliveTime)
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定 - 空闲线程存活时间单位(unit)
keepAliveTime的计量单位 - 线程的拒绝策略(handler)
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,应该怎么处理就是拒绝策略 - 线程工厂(threadFactory)
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
七个参数作用的大白话解释
这里可能有朋友不太了解,我先用一段大白话解释一下这些参数的作用。因为线程如果一直存在是占用资源的,并且使用线程池就是避免频繁的创建和消耗,所以我们需要找一个平衡值,所以就有了核心线程和最大线程。
这七个参数的使用就是,核心线程是一直存在的,此时进入的线程数量超过了核心线程的数量,就会先放到工作队列中,如果工作队列满了,此时就代表着单单只凭借核心线程工作已经不能满足我们现在的要求,所以我们需要接着创建新的线程来更快的消耗,直到达到了最大线程数,如果此时消耗还是赶不上新增的速度,此时就需要有拒绝策略来处理后面再进来的请求,如果现在任务少了,并且现在线程数量大于核心线程的数量,设置的空闲线程存活的时间和时间单位就会用上用场,超过这个界限时线程将会被销毁,此外上面所有线程被创建时都会使用设置参数时使用的线程工厂。
线程池的四种拒绝策略
- DisCardPolicy(默认的) :当任务无法被提交给线程池时,会直接丢弃该任务,没有任何提示或处理,适用于对任务提交失败不敏感的场景,对任务丢失没有特殊要求。
- DisCardolddesPolicy:当任务无法被提交给线程池时,会丢弃最早的一个任务,然后尝试再次提交,适用于对新任务优先级比较高的场景,可以丢弃旧的任务以保证及时处理新任务。
- CallerRunsPolicy:当任务无法被提交给线程池时,会由提交任务的线程自己执行该任务,适用于对任务提交失败要求较低的场景,通过调用线程来执行任务,避免任务丢失。
- AbortPolicy:是ThreadPoolExecutor的默认拒绝策略,当任务无法被提交给线程池时,会直接抛出RejectedExecutionException异常,适用于对任务提交失败要求敏感的场景,需要明确知道任务是否被接受并执行。