- 如何理解线程和进程的区别
- 协程简介
- Java中线程的生命周期状态转移
- 进程的特征
- 进程的调度方式
- 进程通信的方式
- 线程通信的方式
- 线程中几个常见方法的区别
- 多线程中与interrupt相关的方法
- 多线程中的interrupt一定会抛出异常么?
- 多线程一定比单线程好?
- 什么是下上下文切换?
- 如何减少上下切换的方法
- 线程死锁与死锁的避免方式
- 死锁的预防&解决
- 什么是线程池
- 线程池设计时核心线程数和最大线程数要考虑什么因素
- 线程池的基本组成
- 线程池分别适合什么场景
- 线程池当队列中的任务都执行完毕之后会对线程进行什么操作
- ThreadLocal是什么?
- ThreadLocal的应用场景
- ThreadLocal可能存在的问题
本文系记录对Java中多线程基础的学习资料,如有异议,欢迎联系我讨论修改。PS:图侵删!图片丢失请访问https://github.com/zx950519/zx950519.github.io/blob/master/_posts/2019-11-04-Java%E5%AD%A6%E4%B9%A0%E7%B3%BB%E5%88%97%E4%B9%8B%E5%A4%9A%E7%BA%BF%E7%A8%8B%E5%9F%BA%E7%A1%80.md
如何理解线程和进程的区别
进程:
- 进程是操作系统结构的基础
- 程序在一个数据集合上运行的过程
- 系统进行资源分配和调度的独立单位
线程:
- 轻装进程
- 进程中独立运行的子任务
- CPU调度和分派的基本单位
- 基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
- 一个程序至少有一个进程,一个进程至少有一个线程
- 线程的划分尺度小于进程,使得多线程程序的并发性高
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
- 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
- 多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配
- 线程的创建和切换开销比进程小
协程简介
进程(Process)和线程(Thread)是os通过调度算法,保存当前的上下文,然后从上次暂停的地方再次开始计算,重新开始的地方不可预期,每次CPU计算的指令数量和代码跑过的CPU时间是相关的,跑到os分配的cpu时间到达后就会被os强制挂起,开发者无法精确的控制它们。协程(Coroutine)是一种轻量级的用户态线程,实现的是非抢占式的调度,即由当前协程切换到其他协程由当前协程来控制。目前的协程框架一般都是设计成 1:N 模式。所谓 1:N 就是一个线程作为一个容器里面放置多个协程。那么谁来适时的切换这些协程?答案是有协程自己主动让出 CPU,也就是每个协程池里面有一个调度器,这个调度器是被动调度的。意思就是他不会主动调度。而且当一个协程发现自己执行不下去了(比如异步等待网络的数据回来,但是当前还没有数据到),这个时候就可以由这个协程通知调度器,这个时候执行到调度器的代码,调度器根据事先设计好的调度算法找到当前最需要 CPU 的协程。切换这个协程的 CPU 上下文把 CPU 的运行权交个这个协程,直到这个协程出现执行不下去需要等等的情况,或者它调用主动让出 CPU 的 API 之类,触发下一次调度。
优点:
- 协程更加轻量,创建成本更小,降低了内存消耗
- 协作式的用户态调度器,减少了 CPU 上下文切换的开销,提高了 CPU 缓存命中率。进程 / 线程的切换需要在内核完成,而协程不需要,协程通过用户态栈实现,更加轻量,速度更快。在重 I/O 的程序里有很大的优势
- 减少同步加锁,整体上提高了性能。协程方案基于事件循环方案,减少了同步加锁的频率。但若存在竞争,并不能保证临界区,因此该上锁的地方仍需要加上协程锁
- 可以按照同步思维写异步代码,即用同步的逻辑,写由协程调度的回调
缺点:
- 在协程执行中不能有阻塞操作,否则整个线程被阻塞(协程是语言级别的,线程,进程属于操作系统级别)
- 需要特别关注全局变量、对象引用的使用
- 协程可以处理 IO 密集型程序的效率问题,但是处理 CPU 密集型不是它的长处
适用场景:
- 高性能计算,牺牲公平性换取吞吐
- IO Bound 的任务
- Generator 式的流式计算,消除 Callback Hell(回调地狱)
Java中线程的生命周期状态转移
进程的特征
- 动态性
- 并发性
- 独立性
- 异步性
- 结构性
进程的调度方式
基本方式:剥夺/非剥夺
基本原则:CPU利用率;系统吞吐量;周转时间;等待时间;响应时间
实际方案:
- 先来先服务
- 短作业优先
- 高响应比
- 时间片轮转
- 多级反馈队列
进程通信的方式
- 共享存储,通过一片共享区域进行读写操作实现进程之间的信息交换。在操作时需要使用同步互斥工具(PV操作)。共享存储分为低级的基于数据结构的共享和高级的基于存储区的共享
- 消息传递,利用os提供的发送原语和接收原语进行交换。消息传递分为直接通信和间接通信。直接通信采用进程间发消息的方式,将消息挂在接收进程的消息缓冲队列上,接收进程从该队列上取得消息。间接通信则将消息发送到某个中间实体上,称其为信箱。
- 管道,是指用于连接一个读进程和一个写进程以实现他们之间通信的一个共享文件——.pipe文件。写进程向管道以字符流写入数据,读进程从管道中接收输出。管道必须提供同步、互斥并能确定对方存在的方法
- 信号量机制,信号量的本质就是计数器,在访问临界资源并进入临界区时,使用os提供的PV操作对计数器进行修改
线程通信的方式
- 等待(wait)/通知机制(notify)
- 使用方法join
- 管道输入输出流,使用内存作为传输媒介
- 生产者-消费者问题模型
- 使用condition控制线程通信
- 使用阻塞队列(BlockingQueue)控制线程通信
线程中几个常见方法的区别
yield() 与 wait()
- wait()是让线程由“运行状态”进入到“等待(阻塞)状态”,yield()是让线程由“运行状态”进入到“就绪状态”
- wait()是会线程释放它所持有对象的同步锁,而yield()方法不会释放锁
sleep() 与 wait()
- wait()的作用是让当前线程由“运行状态”进入“等待(阻塞)状态”的同时,也会释放同步锁。sleep()的作用是也是让当前线程由“运行状态”进入到“休眠(阻塞)状态”
- wait()会释放对象的同步锁,而sleep()则不会释放锁
- sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然 保持者,当指定的时间到了又会自动恢复运行状态;而wait()也会让出CPU,但是必须经过notify()或notifyAll()后才能参与竞争CPU
- sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用
- sleep是Thread类的方法,wait是Object类中定义的方法
run() 与 start()
- run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的
- 用start()方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
join(long)与sleep(long)
join(long)在内部使用wait(long)实现,故join(long)具有释放锁的特点。sleep(long)不释放锁。
多线程中与interrupt相关的方法
在Java中有三个和interrupt相关的方法,分别是:
- interrupt:interrupt方法是用于中断线程的,调用该方法的线程的状态将被置为”中断”状态。注意:线程中断仅仅是设置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出InterruptedException的方法,比如这里的sleep,以及Object.wait等方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。并且在抛出异常后立即将线程的中断标示位清除,即重新设置为false,之所以重置中断表示是为了保证一个中断应该只被处理一次。抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求
- interrupted:用于判断线程是否处于中断状态。额外地,静态方法interrupted将会在第一次调用后清除中断状态
- isInterrupted:用于判断线程是否处于中断状态。isInterrupted不具有清除状态的功能
多线程中的interrupt一定会抛出异常么?
不会,下述情况会抛出异常:
- 如果线程堵塞在object.wait()、Thread.join()和Thread.sleep(),将会抛出InterruptedException,同时清除线程的中断状态
- 如果线程堵塞在java.nio.channels.InterruptibleChannel的IO上,Channel将会被关闭,线程被置为中断状态,并抛出java.nio.channels.ClosedByInterruptException
- 如果线程堵塞在java.nio.channels.Selector上,线程被置为中断状态,select方法会马上返回,类似调用wakeup的效果
多线程一定比单线程好?
多线程虽然可以带来更好的并发能力,但是并发编程并不能提高程序运行的速度,还会带来很多衍生问题,例如:内存泄漏、上下文切换、死锁等问题。单核运行多线程程序,在同一时间仅由一个线程在运行。此外,系统还要频繁地切换上下文,带来巨大的开销,效率反而更低。
什么是下上下文切换?
当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
切换流程:
- 挂起某进程,将该进程在CPU中的状态(上下文)存储于内存中的某处
- 在内存中检索下一个进程的上下文并将其在CPU的寄存器中恢复
- 跳转到程序计数器所指向的位置,以恢复该进程在程素中
切换原因:
- 在当前执行任务的时间用完后,系统CPU正常调度下一个任务
- 当前执行任务遇到IO阻塞,调度器将当前任务挂起,执行下一个任务
- 多个任务抢占锁资源,当前任务没抢到锁资源,被调度器挂起,继续下一任务
- 用户代码挂起当前任务,让出CPU时间
- 硬件中断
如何减少上下切换的方法
- 基于无锁并发编程
- CAS算法
- 使用最少线程
- 协程
线程死锁与死锁的避免方式
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。避免方式:
- 破坏互斥条件:不靠谱
- 破坏请求与保持条件:一次性申请所有资源
- 破坏不剥夺条件:申请不到新资源时,主动释放已占有的资源
- 破坏循环等待条件:按某一顺序申请资源,释放资源则反序释放
死锁的预防&解决
- 超时法
- 银行家算法
银行家算法:当一个进程申请使用资源的时候,银行家算法通过先 试探 分配给该进程资源,然后通过安全性算法判断分配后的系统是否处于安全状态,若不安全则试探分配作废,让该进程继续等待。
什么是线程池
线程池(Thread Pool)可用于限制应用程序中同一时刻运行的线程数。每启动一个新线程都会有相应的性能开销,每个线程都需要给栈分配一些内存,频繁执行这一操作可能会导致性能损耗。我们可以把并发执行的任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程。只要池里有空闲的线程,任务就会分配给一个线程执行。在线程池的内部,任务被插入一个阻塞队列(Blocking Queue ),线程池里的线程会去取这个队列里的任务。当一个新任务插入队列时,一个空闲线程就会成功的从队列中取出任务并且执行它。线程池经常应用在多线程服务器上。每个通过网络到达服务器的连接都被包装成一个任务(Task)并且传递给线程池。
线程池设计时核心线程数和最大线程数要考虑什么因素
要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析:
- 任务的性质:CPU密集型任务、IO密集型任务、混合型任务
- 任务的优先级:高、中、低
- 任务的执行时间:长、中、短
- 任务的依赖性:是否依赖其他系统资源,如数据库连接等
对于不同性质的任务来说,CPU密集型任务应配置尽可能少的线程,如配置CPU个数+1的线程数,IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1,而对于混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。常见的估值公式为:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 ) * CPU数目
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32s 线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
线程池的基本组成
- 线程池管理器
- 工作线程
- 任务接口
- 任务队列
线程池分别适合什么场景
- newSingleThreadExecutor:单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务具有有序性
- newFixedThreadExecutor(n):固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行
- newCacheThreadExecutor:可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行
- newScheduleThreadExecutor:大小无限制的线程池,支持定时和周期性的执行线程
线程池当队列中的任务都执行完毕之后会对线程进行什么操作
对于Cache类型的线程池,当前线程数大于corePoolSize时,会根据超时时间逐步缩减线程总数,直至线程总数为corePoolSize。
ThreadLocal是什么?
ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。ThreadLocalMap用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。
ThreadLocal的应用场景
- 多线程场景下,每个线程需要单独的变量实例
- 存储用户Session
- 解决线程安全问题
ThreadLocal可能存在的问题
ThreadLocal中的key基于弱引用,而value则基于强引用。当发生GC时,可能会出现value强引用保留,key弱引用被回收。解决方法是手动调用remove()方法清除不使用的kv对。