说说并发与并行的区别?
- 并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
- 并行: 单位时间内,多个任务同时执行。
并发、并行、串行、同步、异步
-
并发:并发编程又叫多线程编程。并发当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。这种方式我们称之为并发(Concurrent)。
-
并行
当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
-
串行
并行和串行指的是任务的执行方式。串行是指多个任务时,各个任务按顺序执行,完成一个之后才能进行下一个。并行指的是多个任务可以同时执行,异步是多个任务并行的前提条件。
-
同步、异步
-
同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
-
异步: 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
同步和异步的区别最大在于异步的话调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
指的是能否开启新的线程。同步不能开启新的线程,异步可以。
异步:异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。
异步和多线程并不是一个同等关系,异步是最终目的,多线程只是我们实现异步的一种手段。异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回而可以做其它的事情。实现异步可以采用多线程技术或则交给另外的进程来处理。 -
进程和线程的区别
1.多进程之间不共享资源,多线程之间共享资源,那么就会存在资源竞争的问题
2.进程是操作系统资源分配的最小单位,线程是CPU调度的最小单位(CPU四核八线程)
3.进程里面默认有一个主线程,进程里面可以有多个线程,线程必须依赖进行而存在,不能独立存在。
举个例子:一个工厂相当于一个进程,一个工人相当于一个线程,一个工厂里面可以存在多个工人。
多进程和多线程的区别
什么是上下文切换?
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文的切换流程如下。
- 挂起一个进程(线程),将这个进程在CPU中的状态(上下文信息)存储于内存的PCB(TCB)中。
- 在PCB中检索下一个进程的上下文并将其在CPU的寄存器中恢复。
- 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行)并恢复该进程。
线程的基本方法
线程等待:wait方法
线程睡眠:sleep方法
线程让步:yield方法
调用yield方法会使当前线程让出(释放)CPU执行时间片,与其他线程一起重新竞选CPU时间片 。
一般优先级高的线程更有可能竞选得到。
线程中断:interrupt方法
interrupt方法用于向线程发送一个终止的通知。
-
如果线程处于被阻塞状态,那么该线程将立即退出被阻塞状态,并且抛出一个InterrupedException异常.
-
如果线程处于运行状态,那么会将该线程的中断标志设置为true.被设置中断标志的线程将继续正常运行,不受影响.
所以,需要调用interrupt方法的线程配合中断,在正常运行任务时,经常检查本线程的中断标志,如果被设置了中断标志就自行停止线程。
线程加入:join方法
Thread类中的join方法的主要作用就是同步,它可以使得线程之间的并发执行变为串行执行。
即如果在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。
join方法也可以带参数。如A线程中调用B.join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并发执行。需要注意的是,jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。
线程唤醒:notify方法
- wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
- wait()使当前线程阻塞,前提是必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
- 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。
- notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁。
static Object lock = new Object();
static boolean flag = true;
static class Wait inplements Runnable{
public void run(){
//加锁,拥有lock的Monitor
synchronized (lock){
//当条件不满足的时候,继续wait,同时释放了lock的锁
while(flag){
lock.wait();//会释放锁
}
//条件满足,完成工作
//do something
}
}
}
static class Notifly inplements Runnable{
public void run(){
//加锁,拥有lock的Monitor
synchronized (lock){
//获取lock的锁,然后进行通知,通知时不会释放lock的锁,直到
//当前线程释放了lock后,wait线程才能起来。
lock.notify();
flag = false;
}
}
}
//Wait线程首先获取了lock对象的锁,然后调用对象的wait()方法,放弃了锁并进入了对象的等待队列中,进入等待状态。
//由于Wait线程释放了对象的锁,Notify线程获取了对象的锁,并调用对象的notify()方法,将Wait线程从等待队列移至同步队列,此时Wait线程的状态变为阻塞状态。
//Notify线程释放锁之后,Wait线程再次获取到锁并开始完成工作。
后台守护进程:setDaemon方法
setDaemon方法用于定义一个守护进程,也叫做“服务进程”,该线程时后台进程,有一个特性,即为用户线程提供公共服务,在没有用户线程时会自动离开。如垃圾回收线程。
说说 sleep() 方法和 wait() 方法区别和共同点?
- 两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。
- sleep方法属于Thread类,wait方法属于Object类;
- sleep方法暂停指定的时间,让出CPU给其它线程,但其监控状态依然保持,在指定的时候过后又会自动恢复运行状态。
- 调用sleep方法,线程不会释放对象锁;wait() 方法被调用后,会释放锁,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。所以run方法不可以手动调用,不然就是普通方法了
终止线程的四种方式
1.正常运行结束
指线程体执行完成,线程自动结束。
2.使用退出标志退出线程
可以用volatile关键字定义一个变量,当满足某种条件时,触发关闭线程。
public class ThreadSafe extends Thread{
public volatile boolean exit = false;
public void run(){
while(!exit){
//执行业务逻辑代码
}
}
}
volatile关键字:这个关键字可以使exit线程同步安全,也就是说在同一时间只能有一个线程修改exit的值。
3.使用interrupt方法终止线程
- 线程处于运行状态时,通过修改中断标志为True,线程配合结束。
- 线程处于阻塞状态时,捕获InterrupedException异常来结束。
4.使用stop方法终止线程:不安全
已被弃用。不安全,可能会导致数据不一致等问题。
Java线程的创建方式
1.继承Thread类
public class MyThread extends Thread{
private int i=0;
@Override
public void run(){
}
}
2.实现Runnable接口
其实Thread类也是实现Runnable接口来的。所以我们可以直接实现Runnable接口,可以避免单继承局限。
public class ChildrenClassThread extends SuperClass inplements Runnable{
private int i=0;
@Override
public void run(){
}
}
3.实现Callable接口
实现Callable接口,并重写call方法。可以实现有返回值的线程。
public class CallableThreadTest implements Callable<Integer> {
@Override
public Integer call() throws Exception {
}
}
4.基于线程池
线程池
线程池的本质就是使用了一个线程安全的工作队列(jobs)连接工作者线程和客户端线程,客户端线程讲任务放入工作队列后便返回,而工作者线程则不断从工作队列中取出工作并执行。当工作队列为空时,所有的工作者线程均等待在工作队列上(jobs.wait()),当有客户端提交一个任务之后会通知任意一个工作者线程(notify),随着大量的任务被提交,更多的工作者线程会被唤醒。
为什么要用线程池?
池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。(线程池可以控制线程数,有效的提升服务器的使用资源,避免由于资源不足而发生宕机等问题)
执行execute()方法和submit()方法的区别是什么呢?
-
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
-
submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,
四种线程池及其运用场景
-
CachedThreadPool创建一个可缓存线程池,如果线程池有可回收线程,可灵活回收空闲线程,若无可回收,则新建线程。(corePoolSize=0, maximumPoolSize=Integer.MAX_VALUE)
不足:这种方式虽然可以根据业务场景自动的扩展线程数来处理我们的业务,但是最多需要多少个线程同时处理缺是我们无法控制的;
优点:如果当第二个任务开始,第一个任务已经执行结束,那么第二个任务会复用第一个任务创建的线程,并不会重新创建新的线程,提高了线程的复用率;
-
FixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。(使用的是无界队列,所以最大线程数不会超过corePoolSize)
优点:两个结果综合说明,FixedThreadPool的线程数是可以进行控制的,因此我们可以通过控制最大线程来使我们的服务器打到最大的使用率,同事又可以保证及时流量突然增大也不会占用服务器过多的资源。
-
ScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
-
SingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。(使用无界队列)
如何创建线程池
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 返回线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM(out of memory)。
- CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
方式一:通过构造方法实现
方法二:通过Executor 框架的工具类Executors来实现
略
ThreadPoolExecutor 类分析
ThreadPoolExecutor
类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生
ThreadPoolExecutor
构造函数重要参数分析
ThreadPoolExecutor 3 个最重要的参数:
- corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
- maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor
其他常见参数:
- keepAliveTime:当线程池中的线程数量大于
corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁; - unit :
keepAliveTime
参数的时间单位。 - threadFactory :executor 创建新线程的时候会用到。
- handler :饱和策略。关于饱和策略下面单独介绍一下。
线程池核心线程池数量设置
cpu密集型的任务 一般设置 线程数 = 核心数N + 1
io密集型的任务 一般设置 线程数 = 核心数N*2 + 1
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,线程池中的线程数设置得少一些,减少线程上下文的切换
ThreadPoolExecutor
饱和策略(拒绝策略)
定义:如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor
定义一些策略:
-
ThreadPoolExecutor.AbortPolicy:抛出
RejectedExecutionException
来拒绝新任务的处理。 -
ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
-
ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
-
ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃队列中最早的未处理的任务请求,并尝试提交当前任务。
注意:默认拒绝策略是直接抛出异常。
线程池原理分析
多线程的应用场景,线程池?
多线程处理后台任务
多线程异步处理任务
多线程分布式计算
eg:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优事、库存、图片等等聚合起来,展示给用户,这个响应是越快的越好,用户体验就好。
这一个请求会对应着多个逻辑的处理,这些逻辑之前没有相互依赖关系,那么我们就可以使用线程池中的多个线程去并发的执行,最后再将结果进行合并即可,这样比使用单线程顺序执行的速度快很多的,并且这些任务是不需要进行缓冲的,所以不用设置线程池中的阻塞队列,直接设置尽可能大的核心线程数和最大线程数即可。
线程池的大小设置
CPU密集:意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集型任务只有在真正的多核CPU上才可能得到加速(通过多线程),现代CPU都是多核的。
IO密集型:即该任务需要大量的IO,即大量的阻塞。例如:数据库插入数据时,此时就存在磁盘IO,将数据插入到数据库中,此时CPU就是处于空闲时间,这段时间就浪费掉了CPU的运算能力。
针对上面的两种类型的任务,应该怎么设置线程池的大小呢?
CPU密集型的任务,设置线程池时,尽可能设置少点线程,因为过多线程会产生大量上下文切换,不利于CPU持续进行运算;此时大小设置为CPU核数就好。
IO密集型的任务,设置线程池时,应该尽可能设置多点线程,因为过多的线程使CPU在IO阻塞时,还会调度其它的线程,从而充分利用其运算性能,可以设置为CPU的两倍或更多些。
多进程和多线程的优缺点,应用场景
多进程的优点:
1、每个进程相互独立,不影响主程序的稳定性,子进程崩溃没关系;基于这个特性,常常会用多进程来实现守护服务器的功能。
2、通过增加CPU,就可以容易扩充性能;
3、可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
4、每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大。
多进程的缺点:
0、创建进程的代价非常大,OS要给其分配资源;进程切换消耗大;
1、逻辑控制复杂,需要和主程序交互;
2、需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算 多进程调度开销比较大;
3、最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+多CPU+轮询方式来解决问题……
4、方法和手段是多样的,关键是自己看起来实现方便有能够满足要求,代价也合适。
多线程优点:
0、线程切换比进程切换快
1、无需跨进程边界;
2、程序逻辑和控制方式简单;
3、所有线程可以直接共享内存和变量等;交互数据非常方便;
4、线程方式消耗的总资源比进程方式好。
多线程缺点:
1、每个线程与主程序共用地址空间,受限于2GB地址空间;
2、线程之间的同步和加锁控制比较麻烦;
3、一个线程的崩溃可能影响到整个程序的稳定性;
评论区