003 - 并发与多线程处理及应用
2022-02-28 11:01:13 1 举报
AI智能生成
003 - 并发与多线程处理及应用
作者其他创作
大纲/内容
操作系统与进程
没有操作系统
在计算机最早期时,没有操作系统,执行程序只需要一种方式,那就是从头到尾依次执行。
任何资源都会为这个程序服务,在计算机使用某些资源时,其他资源就会空闲,就会存在浪费资源的情况。
浪费资源指的是资源空闲,没有充分使用的情况
操作系统的出行
为我们的程序带来了并发性
操作系统是什么?
操作系统是一个并发系统,并发性是操作系统非常重要的特征
操作系统具有同时处理和调度多个程序的能力, 比如
多个IO设备,同时在输入输出
设备IO和CPU计算,同时进行
内存中同时有多个系统和用户程序被启动交替、穿播地执行。
操作系统在协调和分配进程的同时,操作系统也会为不同进程分配不同的资源。
现代操作系统可以同时管理一个计算机系统中的多个进程,即可以让计算机系统中的多个进程轮流使用CPU资源。
程序与进程之间的关系
程序是一段静态的代码,它是应用软件执行的蓝本。
操作系统使程序能够同时运行多个程序,
一个程序就是一个进程,也就相当于同时运行多个进程
进程是程序的一次动态执行过程,
它对应了从代码加载、执行至执行完毕的一个完整过程,这个过程也是进程本身从产生、发展至消亡的过程。
操作系统实现多个程序同时运行,解决了单个程序无法做到的问题
资源利用率
单个进程存在资源浪费的情况,
举个例子
当你在为某个文件夹赋予权限时,输入程序无法接受外部的输入字符,只有等到权限赋予完毕后才能接受外部输入。
总的来讲,就是在等待程序时无法执行其他工作。
如果在等待程序时可以运行另一个程序,那么将会大大提高资源的利用率。
(资源并不会觉得累)因为它不会划水~
公平性
不同的用户和程序都能够使用计算机上的资源。
一种高效的运行方式是:为不同的程序划分时间片,来使用资源
但是有一点需要注意,操作系统可以决定不同进程的优先级。
进程饥饿
虽然每个进程都有能够公平享有资源的权利
但是当有一个进程释放资源后的同时有一个优先级更高的进程抢夺资源,
就会造成优先级低的进程无法获得资源,进而导致进程饥饿。
便利性
单个进程是是不用通信的,
通信的本质就是信息交换,及时进行信息交换能够避免信息孤岛,做重复性的工作;
顺序编程(也称为串行编程)
任何并发能做的事情,单进程也能够实现,只不过这种方式效率很低,它是一种顺序性的。
但是,顺序编程(也称为串行编程)也不是一无是处的
串行编程的优势在于其直观性和简单性,客观来讲, 串行编程更适合我们人脑的思考方式
但是我们并不会满足于顺序编程, we want it more!!!
资源利用率、公平性和便利性促使着进程出现的同时,也促使着线程的出现。
线程是什么?多线程是什么?多线程与进程之间的关系
进程和线程的区别
进程是一个应用程序
进程:当一个程序进入内存,即变成一个进程
线程是应用程序中的一条顺序流。
线程:轻量级进程,线程是进程的执行单元
一个程序运行后,至少一个进程,一个进程至少包含一个线程。
每个线程都有自己的执行顺序
每个线程都有自己的栈空间,这是线程私有的
在计算机中,一般堆栈指的就是栈,而堆指的才是堆
还有一些其他线程内部的和线程共享的资源
线程会共享进程范围内的资源,例如内存和文件句柄,
但是每个线程也有自己私有的内容,比如程序计数器、栈以及局部变量。
下面汇总了进程和线程共享资源的区别
进程和线程共享资源的区别
线程是比进程更小的执行单位
在大多数现代操作系统中,都以线程为基本的调度单位,所以视角着重放在对线程的探究。
线程就是进程中的一条顺序流
线程是比进程更小的执行单位,进程中的线程
一个进程在其执行过程中,可以产生多个线程,形成多条执行线索,
进程中会有多个线程来完成一些任务,这些任务有可能相同有可能不同
注意:任何比较都是相对的。
线程是一种轻量级的进程,轻量级体现在线程的创建和销毁要比进程的开销小很多。
线程是什么?
线程间可以共享进程中的某些内存单元(包括代码与数据)
每条线索,即每个线程也有它自身的产生、存在和消亡的过程。
线程的中断与恢复可以增加节省系统的开销。
什么是多线程
多线程意味着你能够在同一个应用程序中运行多个线程
同一个应用程序中运行多个线程,
指令是在CPU中执行的, 多线程应用程序就像是具有多个CPU在同时执行应用程序的代码。
其实这是一种假象, 线程数量并不等于CPU数量,
单个CPU将在多个线程之间共享CPU的时间片,在给定的时间片内执行每个线程之间的切换, 每个线程也可以由不同的CPU执行
每个线程也可以由不同的CPU执行
每个线程也可以由不同的CPU执行
Java中的线程与多线程机制
Java的多线程机制
Java语言的一大特性点就是内置对多线程的支持。
Java线程也是一种对象, 它和其他对象一样。
Java中的main()方法
每个Java应用程序都有一个缺省的主线程。
是一条特殊的线程
JVM创建的main线程是一条主执行线程
Java中的方法都是由main方法发起的
class Hello {
public static void main(String args[]) {
while(true) {
System.out.println("hello");
}
while(true) {
System.out.println("您好");
}
}
}
public static void main(String args[]) {
while(true) {
System.out.println("hello");
}
while(true) {
System.out.println("您好");
}
}
}
在main方法中, 照样可以创建其他的线程(执行顺序流) , 这些线程可以和main方法共同执行应用代码。
注 应用程序中的main方法中的参数args能接受用户从键盘键入的字符串
比如,使用解释器java.exe来执行主类
C:\2000\>java Example9_5 12.89 35 78
这时,程序中的args[0]、arg[1]、arg[2]分别得到字符串12.89、35和78。
JVM让线程轮流执行
在Java中, 每一条Java线程就像是JVM的一条顺序流, 就像是虚拟CPU一样来执行代码。
JVM快速地把控制从一个线程切换到另一个线程。这些线程将被轮流执行,使得每个线程都有机会使用CPU资源。
JVM一直要等到Java应用程序中的所有线程都结束之后,才结束Java应用程序 。
JVM让线程轮流执行
Java中的Thread表示线程, Thread是java.lang.Thread类或其子类的实例
线程分类(用户线程与守护线程)
用户线程
一般是程序中创建的线程
如果还有一个或以上的非守护线程,则JVM不会退出。
用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束
守护线程
定义
守护进程(Daemon)是运行在后台的一种特殊进程。
指运行时在后台提供的一种服务线程
又称后台(daemon) 线程,服务线程
特性
这种线程不是属于必须的
只要有任何非后台线程还在运行,程序就不会终止。
当所有非后台线程结束时,程序也就停止了,同时会终止所有的后台线程。
为用户线程 提供 公共服务,在没有用户线程可服务时会自动离开。
为用户服务的线程,当所有用户线程停止时才会被终止,如JVM的垃圾回收
优先级
守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
如何设置
通过Thread.setDaemon(true)方法设置守护线程
通过setDaemon(true)来设置线程为“守护线程”
将一个用户线程设置为守护线程的方式是在 线程对象创建 之前 用线程对象的setDaemon方法。
在Daemon线程中产生的新线程也是Daemon的。
线程则是JVM级别的
以Tomcat 为例,如果你在Web 应用中启动一个线程,这个线程的生命周期并不会和Web应用程序保持同步。
也就是说,即使你停止了Web应用,这个线程依旧是活跃的。
举例:垃圾回收线程
垃圾回收线程就是一个经典的守护线程,
当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,
所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。
它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
生命周期
独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。
当JVM中所有的线程都是守护线程时,JVM就可以退出了
如果还有一个或以上的非守护线程,则JVM不会退出。
样例代码
样例代码
在每次的循环中会创建10个线程, 并把每个线程设置为后台线程, 然后开始运行, for循环会进行十
次, 然后输出信息, 随后主线程睡眠一段时间后停止运行。在每次run循环中, 都会打印当前线程的信
息, 主线程运行完毕, 程序就执行完毕了。
次, 然后输出信息, 随后主线程睡眠一段时间后停止运行。在每次run循环中, 都会打印当前线程的信
息, 主线程运行完毕, 程序就执行完毕了。
因为daemon是后台线程, 无法影响主线程的执行。
但是当你把daemon.setDaemon(true) 去掉时, while(true) 会进行无限循环, 那么主线程一直在执
行最重要的任务,所以会一直循环下去无法停止。
行最重要的任务,所以会一直循环下去无法停止。
多线程优势和劣势
优势
合理使用线程是一门艺术,合理编写一道准确无误的多线程程序更是一门艺术
如果线程使用得当,能够有效的降低程序的开发和维护成本
Java很好的在用户空间实现了开发工具包, 并在内核空间提供系统调用来支持多线程编程
Java支持了丰富的类库JUC和跨平台的内存模型, 同时也提高了开发人员的门槛
并发一直以来是一个高阶主题,但是现在,并发也成为了主流开发人员的必备素质
劣势
虽然线程带来的好处很多, 但是编写正确的多线程(井发) 程序是一件极困难的事情
并发程序的Bug往往会诡异地出现,又诡异的消失
在当你认为没有问题的时候它就出现了,难以定位是并发程序的一个特征,在此基础上你需要有扎实的并发基本功
在享受这些便利的同时,多线程也为我们带来了挑战
多线程带来的挑战:线程安全性问题
并发和并行的关系
并发
意味着应用程序会执行多个的任务,
但是如果计算机只有一个CPU, 那么应用程序无法同时执行多个的任务,但是应用程序又需要执行多个任务,
所以计算机在开始执行下一个任务之前,它并没有完成当前的任务,
只是把状态暂存, 进行任务切换, CPU在多个任务之间进行切换, 直到任务完成。
图解并发
图解并发
并行
是指应用程序将其任务分解为较小的子任务, 这些子任务可以并行处理,
例如在多个CPU上同时进行。
图解并行
图解并行
并发为什么会出现呢?
计算机世界的快速发展离不开CPU、内存和IO设备的高速发展, 但是这三者一直存在速度差异性问题,
存储器的层次结构
存储器的层次结构
根据漏桶理论,程序整体性能,取决于最慢的操作,也就是磁盘访问速度
说明
CPU内部是寄存器的构造
寄存器的访问速度要高于高速缓存
高速缓存的访问速度要高于内存
程序是在内存中执行的
程序里大部分语句都要访问内存
最慢的是磁盘访问
有些程序还需要访问I/O设备
因为CPU速度太快了, 所以为了发挥CPU的速度优势, 平衡这三者的速度差异,三方面 都做出了贡献
如何发挥CPU的速度优势 ?
【计算机体系机构】CPU使用缓存,来中和和内存的访问速度差异
【操作系统】
提供进程和线程调度, 让CPU在执行指令的同时分时复用线程
让内存和磁盘不断交互, 不同的CPU时间片能够执行不同的任务, 从而均衡这三者的差异
【编译程序】提供优化指令的执行顺序,让缓存能够合理的使用
多线程带来的安全性问题(重点)
线程安全性
什么是线程安全性
多个线程可以同时安全调用的代码,称为线程安全的
非常复杂的,这也是多线程带来的挑战之一
一个变量是否是线程安全的,取决于它是否被多个线程访问
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
什么是线程安全问题
本质在于线程对共享变量的操作的原子性,可见性,有序性不能同时满足,
解决线程安全问题的关键
使其同时满足三个特征
原子性
可见性
有序性
单线程的特点
单线程就是一个线程数量为1的多线程
单线程一定是线程安全的。
读取某个变量的值不会产生安全性问题,因为不管读取多少次,这个变量的值都不会被修改。
线程安全性的特点
要编写正确无误的线程安全的代码,其核心就是对状态访问操作进行管理。
对象的状态可以理解为存储在实例变量或者静态变量中的数据
使变量能够被安全访问(实现线程安全性、满足线程安全性的方式)
正确的使用线程和锁
在Java中, 有很多种方式来对共享和可变的资源进行加锁和保护
采用同步机制
在没有采用同步机制的情况下,多个线程中的执行操作往往是不可预测的
通过同步机制来对变量进行修饰。
避免多线程对共享变量的访问,两种方式
不要在多线程之间共享变量
将共享变量置为不可变的
最重要的就是
共享(Shared)
某个变量可以被多个线程同时访问
可变(Mutable) 的状态
变量在生命周期内会发生变化。
只有共享和可变的变量才会出现问题, 私有变量不会出现问题,参考程序计数器。
下面给出一段代码,来看看安全性问题体现在哪
安全性问题体现在哪
这段程序输出后会发现,i的值每次都不一样,这不符合我们的预测
那么为什么会出现这种情况呢?我们先来分析一下程序的运行过程。
TSynchronized实现了Runnable接口, 并定义了一个静态变量i, 然后在increase方法中每
次都增加i的值, 在其实现的run方法中进行循环调用, 共执行1000次。
次都增加i的值, 在其实现的run方法中进行循环调用, 共执行1000次。
Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,
提供了一系列和并发处理相关的关键字,比如synchronized、volatile、final、concurren 包等
可见性问题
单核CPU时代
所有的线程共用一个CPU
CPU缓存和内存的一致性问题容易解决
CPU和内存之间,如图所示
CPU和内存之间,如图所示
多核时代
因为有多核的存在, 每个核都能够独立的运行一个线程,每颗CPU都有自己的缓存
这时CPU缓存与内存的数据一致性就没那么容易解决了
当多个线程在不同的CPU上执行时, 这些线程操作的是不同的CPU缓存
CPU和内存之间,如图所示
CPU和内存之间,如图所示
由于可见性导致的线程安全问题
因为i是静态变量,没有经过任何线程安全措施的保护
多个线程会并发修改i的值,所以我们认为1不是线程安全的,
导致这种结果的出现是由于a Thread和b Thread中读取的i值彼此不可见
所以这是由于可见性导致的线程安全问题。
可见性(Visibility)
是指当一个线程修改了共享变量的值,其他线程也能够立即得知这个通知。
一个线程修改了某个变量的值,这个新值对其它线程来说是立即可见的
一条线程修改完一个共享变量后,另一个线程若访问这个变量将会访问到修改后的值
主要操作细节就是修改值后将值同步至主内存
volatile保证了可见性
volatile 值使用前都会从主内存刷新
synchronized同步块的可见性
由“对一个变量执行 unlock 操作之前,必须先把此变量同步会主内存中( store、write 操作)”这条规则获得。
final 可以保证可见性
被 final 修饰的字段在构造器中一旦完成,并且构造器没有把 “this” 的引用传递出去( this 引用逃逸是一件很危险的事情),
其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final 字段的值。
原子性问题
什么是原子性呢?
可以把原子性操作想象成为一个不可分割的整体
一组操作必须一起完成,中途不能被中断。
它的结果只有两种
全部执行
全部回滚/全部不执行
并发编程的原子性操作是完全独立于任何其他进程运行的操作
原子操作多用于现代操作系统和并行处理系统中。
原子操作通常在内核中使用,因为内核是操作系统的主要组件。
但是,大多数计算机硬件,编译器和库也提供原子性操
在加载和存储中,计算机硬件对存储器字进行读取和写入,
为了对值进行匹配、增加或者减小操作,一般通过原子操作进行。
在原子操作期间,处理器可以在同一数据传输期间完成读取和写入.
这样,其他输入/输出机制或处理器无法执行存储器读取或写入任务,直到原子操作完成为止,
数据库事务的原子性也是基于这个概念演进的
将原子性类比于婚姻关系
男人和女人只会产生两种结果
好好的
说散就散
一般男人的一生都可以把他看成是原子性的一种,当然不排除时间管理(线程切换)的个例
线程切换必然会伴随着安全性问题
男人要出去浪也会造成两种结果
线程安全(好好的)
线程不安全(说散就散)
原子性(Atomicity)
由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write。
大致可以认为基本数据类型的操作是原子性的。同时 lock 和 unlock 可以保证更大范围操作的原子性。
而 synchronize 同步块操作的原子性是用更高层次的字节码指令 monitorenter 和 monitorexit 来隐式操作的。
示例
看起来很普通的一段程序,却因为两个线程a Thread和b Thread交替执行产生了不同的结果。
但是,根源不是因为创建了两个线程导致的,多线程只是产生线程安全性的必要条件,最终的根源出现在i++这个操作上。
这个操作怎么了?这不就是一个给i递增的操作吗?也就是i++=>i=i+1,这怎么就会产生问题了?
因为i++不是一个原子性操作,仔细想一下,i++其实有三个步骤
读取i的值
执行i+1操作
把i+1得出的值重新赋给i(将结果写入内存)
当两个线程开始运行后, 每个线程都会把i的值读入到CPU缓存中, 然后执行+1操作, 再把+1之后的值写入内存。
因为线程间都有各自的虚拟机栈和程序计数器,他们彼此之间没有数据交换
所以当aThread执行+1操作后, 会把数据写入到内存, 同时bThread执行+1操作后, 也会把数据写入到内存
因为CPU时间片的执行周期是不确定的, 所以会出现当aThread还没有把数据写入内存时,
bThread就会读取内存中的数据, 然后执行+1操作, 再写回内存, 从而覆盖i的值, 导致aThread所做的努力白费
为什么上面的线程切换会出现问题呢?
正常情况下
(即不会出现线程安全性问题的情况下)两条线程的执行顺序
图解
正常情况下
可以看到, 当aThread在执行完整个i++的操作后, 操作系统对线程进行切换
由a Thread-b Thread, 这是最理想的操作,
异常情况下
一旦操作系统在任意读取/增加/写入阶段产生线程切换, 都会产生线程安全问题。
图解
异常情况下
最开始时, 内存中i=0, aThread读取内存中的值并把它读取到自己的寄存器中, 执行+1操作,此时发生线程切换
bThread开始执行, 读取内存中的值并把它读取到自己的寄存器中, 此时发生线程切换,
线程切换至aThread开始运行, aThread把自己寄存器的值写回到内存中, 此时又发生线程切换,
由aThread→bThread, 线程bThread把自己寄存器的值+1然后写回内存,
写完后内存中的值不是2,而是1,内存中的i值被覆盖了,
有序性问题
有序性是什么?
在并发编程中还有带来让人非常头疼的有序性问题
有序性顾名思义就是顺序性,在计算机中指的就是指令的先后执行顺序。
程序代码执行的结果不受JVM指令重排序的影响
禁止进行指令重排序
有序性问题一般是编译器带来的
编译器有的时候确实是好心办坏事,它为了优化系统性能,往往更换了指令的执行顺序。
(JVM类加载顺序)
有序性(Ordering)
如果在被线程内观察,所有操作都是有序的;
“线程内表现为串行的语义”
如果在一个线程中观察另一个线程,所有操作都是无序的。
“指令重排”现象
“工作内存与主内存同步延迟”现象
Java通过两个关键字来保证线程之间操作的有序性
volatile 自身就禁止指令重排
而 synchronize 则是由一条规则获得,
“一个变量在同一时刻指允许一条线程对其进行 lock 操作”
这条规则决定了持有同一个锁的两个同步块只能串行的进入。
Happens-Before规则
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在一个happen-before的关系
重排序
为了减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则对代码的执行顺序进行调整,从而提高执行效率。
一个非常显而易见的例子就是JVM中的类加载
图解JVM中的类加载
图解JVM中的类加载
这是一个JVM加载类的过程图, 也称为类的生命周期
类从加载到JVM到卸载一共会经历五个阶段
加载
连接
验证
准备
解析
初始化
使用
卸载
活跃性问题(死锁与活锁)
多线程还会带来活跃性问题
如何定义活跃性问题呢?
活跃性问题,关注的是某件事情是否会发生
这种情况会导致死锁
如果一组线程中的每个线程都在等待一个事件的发生
而这个事件只能由该组中正在等待的线程触发
每个线程都在等待其他线程释放资源,而其他资源也在等待每个线程释放资源,这样没有线程抢先释放自己的资源
这种情况会产生死锁,所有线程都会无限的等待下去。
换句话说,死锁线程集合中的每个线程都在等待另一个死锁线程占有的资源,但是由于所有线程都不能
运行,它们之中任何一个资源都无法释放资源,所以没有一个线程可以被唤醒。
运行,它们之中任何一个资源都无法释放资源,所以没有一个线程可以被唤醒。
什么是死锁(deadlock)?
两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行,结果就是陷入了无尽的循环/无限的等待。
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。
死锁产生的四个必要条件?死锁的产生?有哪些情况会造成死锁?
互斥条件
指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。
资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程释放,
请求和保持条件
指进程已经保持至少一个资源,但又提出了新的资源请求
而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持占有
进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
不剥夺条件
指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放
进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
循环等待条件
指在发生死锁时,必然存在一个进程对应的环形链。
在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所申请的资源。
解决方法?如何处理多线程死锁的问题?如何解决死锁问题?+4
解决任一条件
造成死锁的原因有四个,破坏其中一个即可破坏死锁
如何确保N个线程可以访问N个资源同时又不导致死锁?
一种非常简单的避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁。
因此,如果所有的线程都是以同样的顺序加锁和释放锁,就不会出现死锁了。
活锁Clive lock)
如果说死锁很痴情的话,那么活锁用一则成语来表示就是弄巧成拙,
某些情况下,当线程意识到它不能获取所需要的下一个锁时,就会尝试礼貌的释放已经获得的锁,然后
等待非常短的时间再次尝试获取
等待非常短的时间再次尝试获取
想像一下这个场景:当两个人在狭路相逢时,都想给对方让路,相同的步调会导致双方都无法前进。
现在假想有一对并行的线程用到了两个资源。它们分别尝试获取另一个锁失败后,两个线程都会释放自
己持有的锁,再次进行尝试,这个过程会一直进行重复
己持有的锁,再次进行尝试,这个过程会一直进行重复
很明显,这个过程中没有线程阻塞,但是线程仍然不会向下执行
如果期望的事情一直不会发生,就会产生活跃性问题,比如单线程中的无限循环
单线程中的无限循环
在多线程中, 比如aThread和bThread都需要某种资源
aThread一直占用资源不释放,
bThread一直得不到执行, 就会造成活跃性问题,
b hread线程会产生饥饿
性能问题(线程上下文切换)
与活跃性问题密切相关的是性能问题
活跃性问题,关注的是最终的结果
性能问题,关注的就是造成结果的过程
性能问题有很多方面,在多线程中这样的问题同样存在
服务时间过长
吞吐率过低
资源消耗过高
线程上下文切换是什么?
上下文切换(Context Switch)是什么?
上下文切换(Context Switch)
在多线程中一个非常重要的性能因素
那就是线程切换,也称为上下文切换(Context Switch)
在计算机世界中, 老外都喜欢用Context上下文这个词,这个词涵盖的内容很多
时间片轮转的方式使多个任务在同一颗CPU上执行变成了可能。
巧妙地利用了时间片轮转的方式
CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,
在加载下一任务的状态后,继续服务下一任务,任务的状态保存及再加载,
图解上下文切换
线程上下文切换
一般指
上下文切换的资源
上下文
是指某一时间点 CPU 寄存器和程序计数器的内容。
寄存器状态
寄存器
是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。
寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。
程序计数器的变化
程序计数器
是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。
进程
(有时候也称做任务)是指一个程序运行的实例。
在Linux系统中,线程就是能并行运行并且与他们的父进程(创建他们的进程)共享同一地址空间(一段内存区域)和其他资源的轻量级的进程。
PCB-“切换桢”
上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行切换,
上下文切换过程中的信息是保存在进程控制块PCB中的。
进程控制块(PCB, process control block)
PCB还经常被称作“切换桢”(switchframe)。
信息会一直保存到CPU的内存中,直到他们被再次使用。
上下文切换的活动
1. 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处。
2. 在内存中检索下一个进程的上下文并将其在 CPU 的寄存器中恢复。
3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程在程序中。
上下文切换操作开销很大
在上下文切换中,会保存和恢复上下文,丢失局部性
把大量的时间消耗在线程切换上,而不是线程运行上
为什么线程切换会开销如此之大呢?
线程间的切换会涉及到以下几个步骤
将CPU从一个线程切换到另一线程涉及
挂起当前线程
保存其状态, 例如寄存器
恢复到要切换的线程的状态
加载新的程序计数器, 此时线程切换实际上就已经完成了
此时, CPU不再执行线程切换代码,进而执行新的和线程关联的代码
引起线程切换的几种方式
说明
线程间的切换一般是操作系统层面需要考虑的问题
那么引起线程上下文切换有哪几种方式呢?
或者说线程切换有哪几种诱因呢?
引起线程上下文切换的原因、几种引起上下文切换的方式
1. 当前执行任务的时间片用完之后,系统CPU正常调度下一个任务;
当前正在执行的任务完成, 系统的CPU正常调度下一个需要运行的线程
2. 当前执行任务碰到IO阻塞,调度器将此任务挂起,继续下一任务;
当前正在执行的任务遇到I/O等阻塞操作,线程调度器挂起此任务,继续调度下一个任务。
3. 多个任务抢占锁资源,当前任务没有抢到锁资源,被调度器挂起,继续下一任务;
多个任务并发抢占锁资源,当前任务没有获得锁资源,被线程调度器挂起,继续调度下一个任务。
4. 用户代码挂起当前任务,让出CPU时间;
用户的代码挂起当前任务, 比如线程执行sleep方法, 让出CPU.
5. 硬件中断;
使用硬件中断的方式引起上下文切换
对共享和可变资源进行加锁和保护的方式
【加锁机制】内置的加锁机制:synchronized关键字
synchronized关键字
synchronized 关键字在需要原子性、可见性和有序性这三种特性的时候都可以作为其中一种解决方案,
看起来是“ 万能” 的,的确, 大部分并发控制操作都能使用synchronized 来完成。
Java提供一种内置的机制对资源进行保护
Java 提供的一个并发控制的关键字,用起来很简单。
Java 中用于解决并发情况下数据同步访问的一个很重要的关键字。
之所以在处理多线程问题时可以不用考虑太多,就是因为这个关键字屏蔽了很多细节。
被synchronized 修饰的代码块及方法,在同一时间,只能被单个线程访问。
当想要保证一个共享资源在同一时间只会被一个线程访问到时,可以在代码中使用它对类或者对象加锁。
synchronized修饰的内容
修饰一个静态方法,锁住了什么?
类
线程想执行对应同步代码,需要获得类锁。
修饰成员方法,锁住了什么?
对象
线程想执行对应同步代码,需要获得对象锁。
synchronized关键字实践
问题1:观察如下代码,推测执行结果,先打印的是哪个方法?
多个线程访问共享资源的同步方法
答案1:
分析1:在main主线程中启动A,B两个线程,故意让主线程休眠500毫秒目的是为了演示效果让A线程有足够的时间先启动;因为同步方法锁定的是res对象,所以A线程抢先获取了对象实例锁,执行了increase方法,decrease必须等待上一个锁释放之后才能执行,所以执行结果是先打印increase方法;
问题2:观察如下代码,推测执行结果,先打印的是哪个方法?
多个线程访问共享资源的静态同步方法
答案2:
分析2:静态同步方法的锁是当前对象,increase和decrease都是静态同步方法,所以当A线程启动后首先获取了Resource对象锁,decrease要执行也必须等待increase执行完释放资源后才能执行,即先打印increase方法,如上图所示;
问题3:观察如下代码,推测执行结果,先打印的是哪个方法?
多个线程访问共享资源的静态同步方法和非静态同步方法
答案3:
分析3:
synchronized修饰的同步方法锁定的是当前对象实例,而 static synchronized方法锁定的是当前对象,上面两个方法虽然调用的都是Resource类的方法,但是锁定的资源并不同互不干扰,也即当A线程获取对象实例并锁定的时候,并不影响B线程执行decrease方法,所以打印结果就先打出了decrease方法,而后打印increase方法。
多线程资源锁知识点:
非静态同步方法用的都是同一把锁,即对象实例;
静态同步方法,锁的是当前的Class对象;
上面的代码可以将decrease方法改成普通的方法,即不用synchrnoized修饰,看看会先打印哪个方法?
子主题
synchronized关键字的用法
synchronized 既可以修饰方法也可以修饰代码块。
同步方法
同步方法
同步代码块
synchronized关键字对资源进行保护的代码块,俗称同步代码块
同步代码块(Synchronized Block)
同步代码块(Synchronized Block)
同步代码块(Synchronized Block)
synchronized关键字的实现原理
反编译后的字节码指令
反编译后的字节码指令
通过反编译后代码可以看出:
对于同步方法,JVM 采用ACC_SYNCHRONIZED 标记符来实现同步。
对于同步代码块,JVM 采用monitorenter、monitorexit 两个指令来实现同步。
对于同步方法,ACC_SYNCHRONIZED
方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED 标志。
当某个线程要访问某个方法时,会检查是否有ACC_SYNCHRONIZED,
如果有设置,则需要先获得监视器锁,
然后开始执行方法,方法执行之后再释放监视器锁。
这时,如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。
执行过程中,发生了异常
值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,
那么在异常被抛到方法外面之前监视器锁会被自动释放。
JVM 采用ACC_SYNCHRONIZED 标记符,来实现同步。
对于同步代码块,monitorenter、monitorexit
JVM 采用monitorenter、monitorexit 两个指令,来实现同步。
JVM 采用两个指令来实现同步。
为了保证原子性,需要通过两个字节码指令
monitorenter 指令理解为加锁
monitorexit 理解为释放锁
每个对象维护着一个记录着被锁次数的计数器
未被锁定的对象的该计数器为0,
当一个线程获得锁(执行monitorenter)后,该计数器自增变为1 ,
当同一个线程再次获得该对象的锁的时候,计数器再次自增。
当同一个线程释放锁(执行monitorexit 指令)时,计数器再自减。
当计数器为0 时。锁将被释放,其他线程便可以获得锁。
基于Monitor 实现的
对于同步方法,ACC_SYNCHRONIZED
对于同步代码块,monitorenter、monitorexit
ObjectMonitor 类
在Java 虚拟机(HotSpot)中,Monitor 是基于C++实现的,由ObjectMonitor 实现。
提供了几个方法
如enter、exit、wait、notify、notifyAll 等
synchronized
加锁时,会调用objectMonitor 的enter 方法,
解锁时,会调用objectMonitor 的exit方法。
synchronized 可以保证原子性、有序性和可见性
synchronized 与原子性
什么是原子性?
原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。
为什么会发生原子性问题?
线程是CPU调度的基本单位。CPU 有时间片的概念,会根据不同的调度算法进行线程调度。
当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU 使用权。
所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。
在Java 中,为了保证原子性,提供了两个高级的字节码指令
monitorenter
monitorexit
synchronized 为了保证原子性,需要通过字节码指令monitorenter 和monitorexit,
原理
通过这两个字节码指令指令,可以保证被synchronized 修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。
保证了原子性
线程1 在执行monitorenter 指令时,会对Monitor 进行加锁,加锁后其他线程无法获得锁,除非线程1 主动解锁。
即使在执行过程中,由于某种原因,比如CPU 时间片用完,线程1 放弃了CPU,但是,他并没有进行解锁。
而由于synchronized 的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完
结论
在Java 中,可以使用synchronized 来保证方法和代码块内的操作是原子性的。
synchronized 与可见性
可见性是什么?
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
为什么产生可见性问题?
Java 内存模型规定了所有的变量都存储在主内存中,
每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,
线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
所以,就可能出现线程1 改了某个变量的值,但是线程2 不可见的情况。
通过保证共享变量的可见性,来从侧面对对象进行加锁。
当一个线程修改一个共享变量时,另外一个线程能够看见这个修改的值。
图解可见性
被synchronized 修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。
synchronized为了保证可见性
对一个变量解锁之前,必须先把此变量同步回主存中。
这样解锁后,后续线程就可以访问到被修改后的值。
synchronized 关键字锁住的对象,其值是具有可见性的。
synchronized 与有序性
有序性是什么?
即程序执行的顺序按照代码的先后顺序执行。
为什么产生有序性问题?
除了引入了时间片以外,
由于处理器优化和指令重排等,CPU 还可能对输入代码进行乱序执行,
这就是可能存在有序性问题。
比如:load->add->save 有可能被优化成load->save->add 。
synchronized 是无法禁止指令重排和处理器优化的,synchronized 无法避免上述提到的问题。
为什么还说synchronized 也提供了有序性保证呢?
扩展一下有序性的概念
如果在本线程内观察,所有操作都是天然有序的。
如果在一个线程中观察另一个线程,所有操作都是无序的。
和as-if-serial 语义有关
不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。
编译器和处理器无论如何优化, 都必须遵守as-if-serial 语义。
as-if-serial 语义
保证了单线程中,指令重排是有一定的限制的
,而只要编译器和处理器都遵守了这个语义,
那么就可以认为单线程程序是按照顺序执行的。
当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰。
由于synchronized 修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。
synchronized关键字有三种保护机制
对方法进行加锁,确保多个线程中只有一个线程执行方法
对某个对象实例进行加锁,确保多个线程中只有一个线程对对象实例进行访问;
对类对象进行加锁,确保多个线程只有一个线程能够访问类中的资源。
synchronized关键字与内置锁或监视器锁
每个Java对象都可以用做一个实现同步的锁
线程在进入同步代码之前会自动获得锁, 并且在退出同步代码时自动释放锁
而无论是通过正常执行路径退出还是通过异常路径退出,获得内置锁的唯一途径就是进入这个由锁保护的同步代码块或方法。
内置锁(Instrinsic Lock)
监视器锁(Monitor Lock)
synchronized关键字的语义:互斥
互斥意味着独占, 最多只有一个线程持有锁
当线程A尝试获得一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁
如果线程B不释放锁的话,那么线程A将会一直等待下去
synchronized关键字的语义:可重入
线程A获得线程B持有的锁时,线程A必须等待或者阻塞,但是获取锁的线程B可以重入
重入的意思可以用一段代码表示
获取doSomething方法锁的线程,可以执行doSomethingElse方法
执行完毕后,可以重新执行doSomething 0方法中的内容,
锁重入也支持子类和父类之间的重入
synchronized的可重入怎么实现?
每个锁关联
一个线程持有者
一个计数器
当计数器为0时,表示该锁没有被任何线程持有,那么任何线程都可以获得该锁而调用方法。
当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器记为1。
此时其它线程请求该锁,则必须等待,而持有锁的线程如果再次请求这个锁,就可以再次拿到锁,同时计数器会递增。
当线程退出一个synchronized方法/块时,计数器会递减。
如果计数器为0,则释放该锁
同步代码块,基于monitorenter、monitorexit指令
JVM 采用monitorenter、monitorexit 两个指令,来实现同步。
JVM 采用两个指令来实现同步。
为了保证原子性,需要通过两个字节码指令
monitorenter 指令理解为加锁
monitorexit 理解为释放锁
每个对象维护着一个记录着被锁次数的计数器
未被锁定的对象的该计数器为0,
当一个线程获得锁(执行monitorenter)后,该计数器自增变为1 ,
当同一个线程再次获得该对象的锁的时候,计数器再次自增。
当同一个线程释放锁(执行monitorexit 指令)时,计数器再自减。
当计数器为0 时。锁将被释放,其他线程便可以获得锁。
synchronized 与锁优化
synchronized 其实是借助Monitor 实现的,
加锁时,调用objectMonitor的enter方法
解锁时,调用objectMonitor的exit方法
JDK1.6 之前
synchronized的实现才会直接调用ObjectMonitor 的enter 和exit
这种锁被称之为重量级锁。
JDK1.6 中
出现对锁进行了很多的优化
进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化
(自旋锁在1.4 就有,只不过默认的是关闭的,jdk1.6 是默认开启的)
这些操作都是为了在线程之间更高效的共享数据,解决竞争问题。
关于自旋锁、锁粗化和锁消除可以参考:深入理解多线程(五)—— Java 虚拟机的锁优化技术。
【加锁机制】轻量级的加锁机制:volatile关键字
volatile
volatile是Java关键字,用来保证有序性和可见性。
Java语言提供了一种稍弱的同步机制,即volatile变量
用来确保将变量的更新操作通知到其他线程。
Java内存模型JMM
高速缓存
和synchronized的区别
轻量级的synchronized"
一种轻量级的synchronized, 也就是一种轻量级的加锁方式
volatile 通常被比喻成"轻量级的synchronized",
也是Java 并发编程中比较重要的一个关键字。
比sychronized更轻量级的同步锁
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,
因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
volatile适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。
volatile 是一个变量修饰符
和synchronized 不同,
volatile只能用来修饰变量。
volatile无法修饰方法及代码块等。
volatile的执行成本较低
volatile的执行成本要比synchronized低很多
volatile不会引起线程的上下文切换。
synchronized 可以保证原子性、有序性和可见性
volatile 却只能保证有序性和可见性
volatile和synchronize的区别?+1
很多语言中都有volatile 这个关键字
不仅仅在Java 语言中有,在很多语言中都有的
而且其用法和语义也都是不尽相同的
都可以用来声明变量或者对象。
volatile关键字的实现原理
为了提高处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。
但是由于引入了多级缓存,就存在缓存数据不一致问题。
但是,对于volatile 变量,当对volatile 变量进行写操作时,JVM 会向处理器发送一条lock 前缀的指令,将这个缓存中的变量回写到系统主存中。
但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,
所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议。
缓存一致性协议
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,
当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,
当处理器要对这个数据进行修改操作时,会强制重新从系统内存里把数据读到处理器缓存里。
所以,如果一个变量被volatile 所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。
而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。
这就保证了一个volatile 在并发编程中,其值在多个缓存中是可见的。
volatile 变量具备两种特性
volatile关键字的作用(变量可见性、禁止重排序)
volatile只能保证可见性和有序性
可见性
可见性是什么?
指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。
为什么产生可见性问题?
Java 内存模型规定了所有的变量都存储在主内存中,
每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,
线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
所以,就可能出现线程1 改了某个变量的值,但是线程2 不可见的情况。
volatile 与可见性
变量可见性
通过保证共享变量的可见性,来从侧面对对象进行加锁。
当一个线程修改一个共享变量时,另外一个线程能够看见这个修改的值。
图解可见性
volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。
因此,volatile可以保证数据的可见性,可以使用volatile 来保证多线程操作时变量的可见性。
其一是保证该变量对所有线程可见,这里的可见性
嗅探机制 强制失效
处理器嗅探总线
当被volatile修饰的变量进行写操作时,这个变量将会被直接写入共享内存,而非线程的专属存储空间。
volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
根本原因是在写入后执行了一个空操作,使得cpu的cache写入内存
当读取一个被volatile修饰的变量时,会直接从共享内存中读,而非线程专属的存储空间中读。
有序性
有序性是什么?
即程序执行的顺序按照代码的先后顺序执行。
为什么产生有序性问题?
除了引入了时间片以外,
由于处理器优化和指令重排等,CPU 还可能对输入代码进行乱序执行,
这就是可能存在有序性问题。
比如:load->add->save 有可能被优化成load->save->add 。
volatile可以禁止指令重排优化
普通的变量
仅仅会保证在该方法的执行过程中所依赖的赋值结果的地方都能获得正确的结果,
而不能保证变量的赋值操作的顺序与程序代码中的执行顺序一致。
被volatile修饰的变量
被volatile修饰的变量的操作, 会严格按照代码顺序执行,
比如:load->add->save 的执行顺序就是:load、add、save。
volatile 可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行。这就保证了有序性。
lock 前缀指令 内存屏障
源代码->编译器优化重排序->指令级并行重排序->内存系统重排序->最终执行的指令序列
volatile在指令间加上了内存屏障
内存屏障指的是重排序的时候不能把后面的指令重排序到内存屏障之前的位置。
volatile 与有序性
禁止指令重排序
volatile 禁止了指令重排。
禁止重排序
happens-before
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
as-if-serial
volatile不能保证原子性
原子性指的是一组操作必须一起完成,中途不能被中断。
volatile不能保证原子性
volatile 是不能保证原子性的。
什么是原子性?
原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。
为什么会发生原子性问题?
线程是CPU调度的基本单位。CPU 有时间片的概念,会根据不同的调度算法进行线程调度。
当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU 使用权。
所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。
volatile 和原子性的例子
volatile 和原子性的例子
以上代码比较简单,就是创建10 个线程,然后分别执行1000 次i++操作。
正常情况下,程序的输出结果应该是10000,但是,多次执行的结果都小于10000。
这其实就是volatile 无法满足原子性的原因。
为什么会出现这种情况呢,那就是因为虽然volatile 可以保证inc 在多个线程之间的可见性。但是无法inc++的原子性。
volatile只能保证单次读/写的原子性,i++这种操作不能保证原子性
i++这种操作不能保证原子性
(i++的过程是读取i值,把i+1,再把i+1赋给i)。
这个过程中任何一步都可能会被其它线程改动
像i=1这种的就可以保证原子性
volatile能确保long、double读写的原子性
java内存模型保证声明为volatile的long和double变量的get和set操作是原子的。
volatile 的用法
volatile 的用法比较简单,
只需要在声明一个可能被多线程同时访问的变量时,使用volatile 修饰就可以了。
volatile的应用场景
值得说明的是对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。在某些场景下可以代替Synchronized。但是,volatile的不能完全取代Synchronized的位置,只有在一些特殊的场景下,才能适用volatile。
总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:
(1)对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean flag = true)。
(2)该变量没有包含在具有其他变量的不变式中,不同的volatile变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用 volatile。
多个变量之间或者某个变量的当前值与修改后值之间没有约束。
状态标志
全局变量
一读多写
在以下两个场景中可以使用volatile 来代替synchronized:
1、运算结果并不依赖变量的当前值,或者能够确保只有单一的线程会修改变量的值。
2、变量不需要与其他状态变量共同参与不变约束。
除以上场景外, 都需要使用其他方式来保证原子性, 如synchronized或者JUC包。
volatile 的用法 之 使用双重锁校验的形式实现单例
如以上代码,是一个比较典型的使用双重锁校验的形式实现单例的
其中使用volatile 关键字修饰可能被多个线程同时访问到的singleton。
再来看一下双重校验锁实现的单例,已经使用了synchronized,为什么还需要volatile?
MESI
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取
锁bus
volitale会一直嗅探 cas 不断循环无效交互 导致带宽达到峰值
总线风暴
原理图解
声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。
如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
跳出死循环
使用原子类来保证线程安全
原子类其实就是rt.jar下面以atomic开头的类
图解
图解原子类
使用JUC工具包下的线程安全的集合类
使用java.util.concurrent工具包下的线程安全的集合类
使用JUC工具包下的线程安全的集合类来确保线程安全
JUC java.util.concurrent
竞态条件和关键区域
竞态条件是什么?
指的就是两个或多个线程同时对一共享数据进行修改, 从而影响程序运行的正确性时
在关键代码区域发生的一种特殊条件。
关键区域是什么?
关键区域是由多个线程同时执行的代码部分
关键区域中的代码执行顺序会造成不一样的结果。
竞态条件产生原因
如果一段代码是安全的,那么这段代码就不存在竞态条件
仅仅当多个线程共享资源时,才会出现竞态条件
如果多个线程执行一段关键代码,而这段关键代码会因为执行顺序不同而造成不同的结果时,那么这段代码就会包含竞争条件。
线程切换,是导致竞态条件出现的诱导因素
样例代码
样例代码,在上面的代码中, 涉及到一个竞态条件
那就是判断single的时候, 如果single别断为空, 此时发生了线程切换,
另外一个线程执行, 判断single的时候, 也是空, 执行new操作,
然后线程切换回之前的线程, 再执行new操作, 那么内存中就会有两个Singleton对象。
Java内存模型
主内存
工作内存
线程开始运行时会将所需的变量从主内存中拷贝一份到工作内存中,在线程运行结束后再写入主内存
主内存和工作内存的交互
read,write,lock,unlock,assgin,use,load,save
并发模型
并发模型是什么?
系统中的线程如何协作完成并发任务。
不同的并发模型以不同的方式拆分任务,线程可以以不同的方式进行通信和协作,
使用并发模型作用 ?
可以使用不同的并发模型来实现并发系统
为什么并发模型和分布式模型非常相似?
分布式系统模型
分布式系统模型中是进程彼此进行通信
然而本质上,进程和线程也非常相似。
分布式系统面临的挑战和问题
进程通信
网络异常
远程机器挂掉
并发模型
并发模型中是线程彼此进行通信
然而本质上,进程和线程也非常相似。
并发模型面临的挑战和问题
CPU故障
网卡问题
硬盘问题
因为并发模型和分布式模型很相似,因此他们可以相互借鉴
并发模型其实和分布式系统模型非常相似
例如,用于线程分配的模型,就类似于分布式系统环境中的负载均衡模型
分布式模型的思想就是借鉴并发模型的基础上推演发展来的。
并发模型的一个重要的方面是线程是否应该共享状态
状态
其实就是数据
认识两个状态
具有共享状态
意味着在不同线程之间共享某些状态
比如一个或者多个对象,当线程要共享数据时,就会造成竞态条件或者死锁等问题。
当然,这些问题只是可能会出现,具体实现方式取决于你是否安全的使用和访问共享对象。
共享状态
具有独立状态
独立的状态表明状态不会在多个线程之间共享
如果线程之间需要通信,可以访问不可变的对象来实现,这是最有效的避免并发问题的一种方式
使用独立状态让我们的设计更加简单,因为只有一个线程能够访问对象,即使交换对象,也是不可变的对象。
独立状态
经典的并发模型
(1)并行Worker并发模型
并发Worker模型是什么?
第一个并发模型是并行worker模型
Java并发模型中非常常见的一种模型。
许多JUC包下的井发工具都使用了这种模型
图解并行Worker
并行Worker
客户端会把任务交给代理人(Delegator) , 然后由代理人把工作分配给不同的工人(worker)
核心思想
主要有两个进程
代理人(Delegator)
工人(worker)
流程
Delegator负责接收来自客户端的任务并把任务下发, 交给具体的Worker进行处理
Worker处理完成后把结果返回给Delegator,
在Delegator接收到Worker处理的结果后对其进行汇总, 然后交给客户端。
优点
很容易理解
为了提高系统的并行度,可以增加多个Worker完成任务。
它会将一个任务拆分成多个小任务, 并发执行, Delegator在接受到Worker的处理结果后就会返回给Client
整个Worker->Delegator-Client的过程是异步
缺点
为什么共享状态会变得很复杂?
实际的并行Worker要比我们图中画出的更复杂
图解
主要是并行Worker通常会访问内存或共享数据库中的某些共享数据。
主要是并行Worker通常会访问内存或共享数据库中的某些共享数据。
这些共享状态可能会使用一些工作队列来保存等
业务数据
数据缓存
数据库的连接池
在线程通信中, 线程需要确保共享状态是否能够让其他线程共享,
而不是仅仅停留在CPU缓存中,让自己可用
未抢占到资源的线程会阻塞
多线程在访问共享数据时,会丢失并发性
因为操作系统要保证只有一个线程能够访问数据,这会导致共享数据的争用和抢占。
线程需要避免竞态条件,死锁和许多其他共享状态造成的并发问题。
减少争用,提高性能的方式
现代的非阻塞并发算法
缺点:非阻塞算法比较难以实现。
可持久化的数据结构
(Persistent datastructures)
可持久化的数据结构在修改后,始终会保留先前版本。
因此,如果多个线程,同时修改一个可持久化的数据结构,并且一个线程对其进行了修改,则修改的线程会获得对新数据结构的引用。
缺点
看不到新添加的元素
一个持久列表会将新元素添加到列表的开头,并返回所添加的新元素的引用
,但是其他线程仍然只持有列表中先前的第一个元素的引用,他们看不到新添加的元素。
持久化的数据结构在硬件性能上表现不佳
比如链表(LinkedList)
列表中的每个元素都是一个对象, 这些对象散布在计算机内存中。
现代CPU的顺序访问往往要快的多, 因此使用数组等顺序访问的数据结构则能够获得更高的性能。
CPU高速缓存可以将一个大的矩阵块,加载到高速缓存中, 并让CPU在加载后,直接访问CPU高速缓存中的数据。
对于链表, 将元素分散在整个RAM上, 这实际上是不可能的。
虽然可持久化的数据结构是一个新的解决方法,但是这种方法实行起来却有一些问题
无状态的worker
共享状态可以由其他线程所修改
因此, worker必须在每次操作共享状态时重新读取, 以确保在副本上能够正确工作,
不在线程内部保持状态的worker成为无状态的worker
作业顺序是不确定的
无法保证首先执行或最后执行哪些作业。
任务A在任务B之前分配给worker, 但是任务B可能在任务A之前执行。
(2)流水线并发模型
流水线是什么?
第二种并发模型就是我们经常在生产车间遇到的流水线并发模型
每道程序都在自己的线程中运行,彼此之间不会共享状态,这种模型也被称为无共享并发模型
图解流水线设计模型的流程图
这种组织架构就像是工厂中装配线中的worker
每个worker只完成全部工作的一部分, 完成一部分后, worker会将工作转发给下一个worker
使用流水线并发模型,通常被设计为非阻塞I/0
当没有给worker分配任务时, worker会做其他工作。
非阻塞IO,意味着当worker开始IO操作, worker不会等待IO调用完成。
例如,从网络中读取文件
因为IO操作很慢, 所以等待IO非常耗费时间。
在等待IO的同时, CPU可以做其他事情, IO操作完成后的结果,将传递给下一个worker。
非阻塞IO的流程图
非阻塞IO的流程图
在实际情况中,任务通常不会按着一条装配线流动
由于大多数程序需要做很多事情,因此需要根据完成的不同工作在不同的worker之间流动
根据完成的不同工作在不同的worker之间流动
任务还可能需要多个worker共同参与完成
任务还可能需要多个worker共同参与完成
响应式-事件驱动系统
使用流水线模型的系统,有时也被称为晾应式或者事件驱动系统,
这种模型会根据外部的事件作出响应, 事件可能是某个HTTP请求或者某个文件完成加载到内存中。
Actor模型
Actor模型是什么?
一个并发模型
在Actor模型中, 每一个Actor其实就是一个Worker, 每一个Actor都能够处理任务。
它定义了一系列系统组件应该如何动作和交互的通用规则
著名的使用这套规则的编程语言是Erlang。
Actor模型的原理
一个参与者Actor对接收到的消息做出响应, 然后可以创建出更多的Actor或发送更多的消息, 同时准备接收下一条消息。
图解Actor模型
图解Actor模型
Channels模型
Channels模型是什么?
在Channel模型中, worker通常不会直接通信
Channels模型的原理
与此相对的, 他们通常将事件发送到不同的通道(Channel) 上, 然后其他worker可以在这些通道上获取消息
有的时候worker不需要明确知道接下来的worker是谁
他们只需要将作者写入通道中, 监听Channel的worker可以订阅或者取消订阅, 这种方式降低了worker和worker之间的耦合性
Channels的模型图
Channel的模型图
优点
不会存在共享状态
因为流水线设计能够保证worker在处理完成后再传递给下一个worker,
所以worker与worker之间不需要共享任何状态, 也就无需考虑并发问题。
甚至可以在实现上把每个worker看成是单线程的一种。
有状态worker
因为worker知道没有其他线程修改自身的数据, 所以流水线设计中的worker是有状态的,
有状态的意思是,可以将需要操作的数据保留在内存中,有状态通常比无状态更快。
更好的硬件整合
因为你可以把流水线看成是单线程的,而单线程的工作优势在于,它能够和硬件的工作方式相同,
因为有状态的worker通常在CPU中缓存数据, 这样可以更快地访问缓存的数据。
使任务更加有效的进行
可以对流水线井发模型中的任务进行排序,一般用来日志的写入和恢复。
缺点
任务会涉及多个worker, 因此可能会分散在项目代码的多个类中
任务会涉及多个worker, 因此可能会分散在项目代码的多个类中。
因此,很难确定每个worker都在执行哪个任务。
流水线的代码编写也比较困难
流水线的代码编写也比较困难,
设计许多嵌套回调处理程序的代码通常被称为回调地狱, 回调地狱很难追踪debug.
(3)函数性并行
函数式并发是什么?
最近才提出的一种并发模型
JDK 1.7中的ForkAndJoinPool类就实现了函数性并行的功能。
Java 8提出了stream的概念, 使用并行流,也能够实现大量集合的迭代,
函数式
使用函数调用来实现
消息的传递就相当于是函数的调用。
并行
相当于是各个CPU单独执行各自的任务。
基本思路
传递给函数的参故都会被拷贝,因此在函数之外的任何实体都无法操纵函数内的数据,
这使得函数执行类似于原子操作,每个函数调用都可以独立于任何其他函数调用执行
当每个函数调用独立执行时, 每个函数都可以在单独的CPU上执行。
难点
函数的调用流程以及哪些CPU执行了哪些函数,
跨CPU函数调用会带来额外的开销。
Java中如何创建和启动线程、JAVA线程实现/创建方式
Java中, 传统创建线程的方式主要有三种
通过继承Thread类来创建线程
继承Thread类
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。
启动线程的唯一方法就是通过Thread类的start()实例方法。
start()方法是一个native方法,它将启动一个新线程,并执行run()方法。
图解
代码
说明
Thread的子类创建线程
在Java语言中,用Thread类或子类创建线程对象。
在编写Thread类的子类时,需要重写父类的run()方法,
其目的是规定线程的具体操作,否则线程就什么也不做,因为父类的run()方法中没有任何操作语句。
示例代码
通过继承Thread类来创建线程
创建步骤
定义一个线程类,使其继承Thread类, 并重写其中的run方法
run方法内部就是线程要完成的任务
因此run方法也被称为执行体
创建了Thread的子类
上面代码中的子类是TJavaThread
启动方法
注意, 并不是直接调用run方法来启动线程, 而是使用start方法来启动线程。
当然run方法可以调用, 这样的话就会变成普通方法调用, 而不是新创建一个线程来调用了。
当然run方法可以调用
这样的话, 整个main方法只有一条执行线程也就是main线程, 由两条执行线程变为一条执行线程
整个main方法只有一条执行线程也就是main线程, 由两条执行线程变为一条执行线程
Thread构造器
只需要一个Runnable对象, 调用Thread对象的start方法为该线程执行必须的初始化操作,
然后调用Runnable的run方法, 以便在这个线程中启动任务。
上面使用了线程的join方法, 它用来等待线程的执行结束,
如果我们不加join方法, 它就不会等待tJavaThread的执行完毕,输出的结果可能就不是10088
可以看到, 在run方法还没有结束前, run就被返回了
也就是说, 程序不会等到run方法执行完毕就会执行下面的指令。
优势
编写比较简单;
可以使用this关键字直接指向当前线程, 而无需使用Thread.currentThread()来获取当前线程。
可在子类中增加新的成员变量,使线程具有某种属性,
也可以在子类中新增加方法,使线程具有某种功能。
劣势
在Java中, 只允许单继承的原则,所以使用继承的方式,子类就不能再继承其他类。
Java不支持多继承,Thread类的子类不能再扩展其他的类
通过实现Runnable接口来创建线程
实现Runnable接口
实现Runnable接口这种方式更受欢迎,因为这不需要继承Thread类。
如果自己的类已经extends另一个类,就无法直接extends Thread,此时,可以实现一个Runnable接口。
在应用设计中已经继承了别的对象的情况下,这需要多继承(而Java不支持多继承),只能实现接口。
new Runnable
代码
代码1
代码2
说明
使用Runnable接口(Runnable接口与目标对象 )
用Thread类直接创建线程对象。
使用Thread创建线程通常使用的构造方法是:Thread(Runnable target)
该构造方法中的参数是一个Runnable类型的接口
在创建线程对象时必须向构造方法的参数传递一个实现Runnable接口类的实例,
该实例对象称作所创线程的目标对象,当线程调用start()方法后,一旦轮到它来享用CPU资源,目标对象就会自动调用接口中的run()方法。
示例代码
通过实现Runnable接口来创建线程
关于run()方法启动的次数
对于具有相同目标对象的线程,当其中一个线程享用CPU资源时,目标对象自动调用接口中的run方法,这时,run方法中的局部变量被分配内存空间,当轮到另一个线程享用CPU资源时,目标对象会再次调用接口中的run方法,那么,run()方法中的局部变量会再次分配内存空间。也就是说run()方法已经启动运行了两次,分别运行在不同的线程中,即运行在不同的时间片内
线程的主要创建步骤
首先定义Runnable接口, 并重写Runnable接口的run方法
run方法的方法体,同样是该线程的线程执行体。
创建线程实例
可以使用上面代码这种简单的方式创建
也可以通过new出线程的实例来创建,如下所示
通过new出线程的实例来创建线程实例
再调用线程对象的start方法,来启动该线程
优点
线程在使用实现Runnable的同时也能实现其他接口
非常适合多个相同线程来处理同一份资源的情况,体现了面向对象的思想。
Runnable接口执行的是独立的任务, Runnable接口不会产生任何返回值
劣势
编程稍微繁琐
如果要访问当前线程, 则必须使用Thread.currentThread()方法。
通过Callable和Future来创建线程
Callable<Class>、Future有返回值线程
有返回值的任务必须实现Callable接口
执行Callable任务后,可以获取一个Future的对象
future对象
判断任务是否完成
future.isDone()
返回执行结果
future.get()
中断线程的执行
future.cancel()
futureTask
future接口的唯一实现类
既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
在该对象上调用get就可以获取到Callable任务返回的Object了,
有返回值
一般和ExecutorService配合来使用
再结合线程池接口ExecutorService,就可以实现传说中有返回结果的多线程了。
具体代码
代码
无返回值的任务必须Runnable接口。
示例
Java SE 5引入了Callable接口,使用Callable接口来创建线程
优点
既能够实现多个接口, 也能够得到执行结果的返回值。
如果希望在任务完成后能够返回一个值的话, 可以实现Callable接口。
Java SE 5引入了Callable接口
劣势
Callable和Runnable接口还是有一些区别的
Callable执行的任务有返回值, 而Runnable执行的任务没有返回值
Callable(重写) 的方法是call方法, 而Runnable(重写) 的方法是run方法。
call方法可以抛出异常, 而Runnable方法不能抛出异常
使用线程池来创建线程
什么是线程池?基本概念
线程池可以看做是线程的集合
池化思想的来源
创建和销毁对象很费时间,
创建一个对象要申请内存及其他资源,
在对象创建之后JVM试图跟踪每一个对象,虚拟机就能在对象销毁之后进行垃圾回收
提高效率的一个方案就是尽可能减少创建和销毁对象的次数
使用线程池的好处/主要特点
基于线程池的方式能更好的节省资源
线程和数据库连接这些资源都是非常宝贵的资源。
那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。
可以使用缓存的策略,也就是使用线程池。
实现了线程的重用
线程复用
请求到来时 ,线程池给这个请求分配一个空闲的线程
任务完成后,回到线程池中等待下次任务(而不是销毁)
减少了创建和销毁对象的开销
事先创建若干个可执行的线程放入一个池中,
需要时从池中获取,不用自己创建
用完了可以放回池子里,不用销毁
隔离线程环境
管理线程
比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;
因此,通过配置独立的线程池,将较慢的交易服务与搜索服务隔开,避免个服务线程互相影响。
线程池非常高效的,很容易实现和使用。
控制最大并发数
实现任务线程队列缓存略和拒绝机制。
实现某些与时间相关的功能,如定时执行、周期执行等。
常见的四种线程池/ExecutorService创建线程的几种方式
newFixedThreadPool
可重用固定线程数
有新任务时,根据当前线程池的线程数量,确定后续步骤
创建一个可重用固定线程数的线程池,
固定大小的线程池
使用有限的线程集来启动多线程
超出固定线程数后将排队
在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。
如果在所有线程处于活动状态时,提交附加任务,则在有可用线程之前,附加任务将在队列中等待。
如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。
在某个线程被显式地关闭之前,池中的线程将一直存在。
好处
以共享的无界队列方式来运行这些线程。
可以一次性的预先执行高昂的线程分配, 因此可以限制线程的数量。
可以节省时间,因为不必为每个任务都固定的付出创建线程的开销。
创建参数
LinkedBlockingQueue(无界)
线程存活时间:永久存活
核心线程数:n(用户指定)
最大线程数:n(用户指定)
实例
newFixedThreadPool实例
代码
创建线程池的代码
newSingleThreadExecutor
只有一个线程
这个线程池只有一个线程
线程数量为1的FixedThreadPool
创建单个线程的线程池
Executors.newSingleThreadExecutor()返回一个线程池
确保任意时刻都只有唯一一个任务在运行。
如果有多个任务,将会排队
如果向SingleThreadPool一次性提交了多个任务,那么这些任务将会排队
SingleThreadPool会序列化所有提交给他的任务, 并会维护它自己(隐藏) 的悬挂队列
每个任务都会在下一个任务开始前结束,所有的任务都将使用相同的线程
该线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去
创建参数
LinkedBlockingQueue(无界)
线程存活时间:永久存活
核心线程数:1
最大线程数:1
实例
newSingleThreadExecutor的实例
从输出的结果就可以看到,任务都是挨着执行的。
实例中,给任务分配了五个线程,但是这五个线程不像是之前看到的有换进换出的效果
它每次都会先执行完自己的那个线程,然后余下的线程继续走完这条线程的执行路径。
newCachedThreadPool
按需创建
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
可以接受一个线程池对象, 创建一个根据需要创建新线程的线程池
调用 execute 将重用以前构造的线程(如果线程可用)。
当有新任务时,直接新建线程
如果现有线程没有可用的,则创建一个新线程并添加到池中。
为每个任务都创建一个线程
好处
对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。
终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
因此,长时间保持空闲的线程池不会使用任何资源
创建参数
SynchronousQueue
线程存活时间:60s
核心线程数:0
最大线程数:Interget.MAX_VALUE
newCachedThreadPool的构造方法
在它们可用时,重用先前构造的线程, 并在需要时,使用提供的ThreadFactory创建新线程。
newCachedThreadPool的构造方法
实例
CachedThreadPool的实例
Executors.newCachedThreadPool
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
创建参数
DelayedWorkQueue
一个按超时时间升序排序的队列
使用了优先级队列的无界阻塞队列,支持延时获取,
所谓延时队列就是消费线程将会延时一段时间来消费元素。
队列里的元素要实现Delay接口。
线程存活时间:永久存活
核心线程数:用户指定
最大线程数:Integer.MAX_VALUE
代码
代码
newWorkStealingPool
常见接口和类
该框架中用到了这几个类
Executor
Executors
ExecutorService
ThreadPoolExecutor
Callable和Future、FutureTask
类继承关系
类继承关系
类继承关系
Executor框架
Executor简化了并发编程
Executor虽然不是传统线程创建的方式之一
但是它却成为了创建线程的替代者,从而简化了并发编程。
一个执行线程的工具
严格意义上讲,Executor并不是一个线程池,而只是一个执行线程的工具。
使用Executor框架来创建线程池
应用程序可以使用Executor框架来创建线程池
Java中的线程池是通过Executor框架实现的
核心
Executor在客户端和任务之间提供了一个间接层;
与客户端直接执行任务不同这个中介对象将执行任务。
好处
Executor允许你管理异步任务的执行, 而无须显示地管理线程的生命周期。
顶级接口Executor介绍
顶级接口,只有一个excute方法
Java里面线程池的顶级接口是Executor
只有一个 void excute(Runnable task)方法,用户执行任务
使用如下操作来替换线程创建
使用如下操作来替换线程创建
真正的线程池接口ExecutorService介绍
真正的线程池接口是ExecutorService
Executor的子接口,定义了sumbit,invokeAll,invokeAny等方法
ExecutorService对象是使用静态的Executors创建的, 这个方法可以确定Executor类型
Executor的默认实现, 也是Executor的扩展接口
Executors类,为这些Executor,提供了方便的工厂方法
对shutDown的调用,可以防止新任务提交给ExecutorService, 这个线程在Executor中所有任务完成后退出。
ThreadFactory
ThreadFactory是一个接口
按需要创建线程的对象。
使用线程工厂替换了Thread或者Runnable接口的硬连接, 使程序能够使用特殊的线程子类,优先级等。
它只有一个方法就是创建线程的方法
只有一个方法就是创建线程的方法
ThreadFactory的基本使用demo
ThreadFactory的基本使用
子主题
Executors.defaultThreadFactory方法
提供了一个更有用的简单实现
它在返回之前,将创建的线程上下文设置为已知值
ThreadPoolExecutor
ThreadPoolExecutor类提供了线程池的扩展实现。
ThreadPoolExecutor的构造方法/构造函数
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
corePoolSize、maximumPoolSize、largestPoolSize 有意思
1. corePoolSize:
核心线程数
默认没线程等任务来了才调用 除非调用了 预创建线程 一个或者全部
指定了线程池中的线程数量。
2. maximumPoolSize
最大线程数
指定了线程池中的最大线程数量。
3. keepAliveTime:
空闲时间
当前线程池数量超过corePoolSize时,多余的空闲线程的存活时间,即多次时间内会被销毁。
没有执行任务多久会终止 当线程池的线程数大于核心线程才会起作用 调用allowCoreThreadTimeOut会起作用
4. unit:
单位
keepAliveTime的单位。
5. workQueue
缓冲队列
阻塞队列策略
任务队列,被提交但尚未被执行的任务。
有界队列
ArrayBlockingQueue
有界队列
常见队列
ArrayBlockingQueue
遵循FIFO的队列
LinkedBlockingQueue
有节的LinkedBlockingQueue
PriorityBlockingQueue
优先级队列
可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量(因为任务数量少了)
加锁保证安全 一直死循环阻塞 队列不满就唤醒
入队
阻塞调用方式 put(e)或 offer(e, timeout, unit)
阻塞调用时,唤醒条件为超时或者队列非满(因此,要求在出队时,要发起一个唤醒操作)
进队成功之后,执行notEmpty.signal()唤起被阻塞的出队线程
阻塞调用时,唤醒条件为超时或者队列非满(因此,要求在出队时,要发起一个唤醒操作)
进队成功之后,执行notEmpty.signal()唤起被阻塞的出队线程
在进行某项业务存储操作时,建议采用offer进行添加,可及时获取boolean进行判断,如用put要考虑阻塞情况(队列的出队操作慢于进队操作),资源占用。
同步移交
SynchronousQueue
不存储元素的阻塞队列,因此超出核心线程数的任务会创建新的线程来指执行。
无界队列
LinkedBlockingQueue
常用的为无界的LinkedBlockingQueue
无界 当心内存溢出
当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM
DelayedWorkQueue
一个按超时时间升序排序的队列
使用了优先级队列的无界阻塞队列,支持延时获取,所谓延时队列就是消费线程将会延时一段时间来消费元素。队列里的元素要实现Delay接口。
6. threadFactory:
工厂方法
线程工厂,用于创建线程,一般用默认的即可。
7. handler
拒绝策略是什么?
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。
这时候我们就需要拒绝策略机制合理的处理这个问题。
拒绝策略,当任务太多来不及处理,如何拒绝任务。
拒绝策略、JDK内置的拒绝策略如下
抛异常AbortPolicy
直接抛出异常,阻止系统正常运行。
直接抛出异常
丢弃DiscardPolicy
丢弃当前任务
该策略默默地丢弃无法处理的任务,不予任何处理。
如果允许任务丢失,这是最好的一种方案。
重试CallerRunsPolicy
抛回调用者的线程处理
只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。
显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
丢弃最早提交的DiscardOldestPolicy
丢弃最老的任务
丢弃最老的一个请求,
也就是即将被执行的一个任务,并尝试再次提交当前任务。
自定义拒绝策略
以上内置拒绝策略均实现了RejectedExecutionHandler接口
若以上策略仍无法满足实际需要,完全可以自己扩展RejectedExecutionHandler接口。
执行过程
使用Hash表维护线程的引用
submit
使用future获取任务的执行结果
AbstractExecutorService
实现了sumbit,invokeAll()等方法
线程池工作过程、实现原理
核心线程->队列->最大线程->拒绝策略
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量 < corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量 >= corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,且正在运行的线程数量 < maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
d) 如果队列满了,且正在运行的线程数量 >= maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数 > corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
每一个 Thread 的类都有一个 start 方法。
当调用start启动线程时,Java虚拟机会调用该类的 run 方法。
那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。
可以继承重写 Thread 类,在其 start 方法中添加不断循环调用传递过来的 Runnable 对象。
这就是线程池的实现原理。
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,
如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。
图解Java线程池工作过程/实现原理
图解Java线程池工作过程
请简述一下线程池的运行流程?
当需要任务大于核心线程数,把任务往存储任务队列里放,
当存储队列满了,就增加线程池创建的线程数量,
当线程数量达到最大,就开始执行拒绝策略。
线程池运行状态
running
自然是运行状态,指可以接受任务执行队列里的任务
shutdown
SHUTDOWN 指调用了 shutdown() 方法,不再接受新任务了,但是阻塞队列里的任务得执行完毕。
stop
STOP 指调用了 shutdownNow() 方法,不再接受新任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务。
tidying
所有任务都执行完毕,在调用 shutdown()/shutdownNow() 中都会尝试更新为这个状态。
terminated
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态
所有线程销毁
有个Volatile的状态码
线程池的关闭
Shutdown
线程池状态变为shutdown
ShutdownNow
线程状态变为stop
中断线程池中的某一个线程的方法
可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
线程池的配置
IO密集型
尽量使用较小的线程池,一般为CPU核心数+1
Cpu密集型
可以使用稍大的线程池,一般为2*CPU核心数。
混合型
4个组成部分
1. 线程池管理器
用于创建并管理线程池
2. 工作线程
线程池中的线程
3. 任务接口
每个任务必须实现的接口,用于工作线程调度其运行
4. 任务队列
用于存放待处理的任务,提供一种缓冲机制
实际使用
商品详情界面
批处理
源码
Worker
Worker是线程池中的线程
继承AQS,实现runnable
既是一个可执行的任务,又可以达到锁的效果
初始state=-1
这样构造的原因主要是为了实现对中断的控制
1.worker未运行时
1.shutdown()线程池时,会对每个worker tryLock()上锁,tryAcquire是尝试通过CAS将state由0设置为1,因此会失败
2.shutdownNow()线程池时,不用tryLock()上锁,但调用worker.interruptIfStarted()终止worker时也要求state=0,因此也会失败
2.worker运行时
runWorker中,会对正在运行中的worker加锁,所以如果调用了shutdown()方法,中断也会失败,但是如果调用shutdownNow()方法,该方法会通过worker.interruptIfStarted来中断任务
addWorker
1.判断线程池状态,如果状态正常,进入下一步
2.比较当前线程池的线程数和最大线程数/核心线程数的大小(由参数core确定比较对象),如果没有超过的话,进入下一步,否则返回false
3.在线程池自带的成员变量ReentrantLock的加锁的情况下,向Workers的HashSet中添加新创建的worker实例,添加完成后解锁,并start该worker实例,worker.start()方法底层其实调用的就是runWorker()方法
基本概念:创建新线程并执行
runWorker
1.将state设置为0
2.在mainLock.lock的情况下,进行task.run
3.在finally块中释放锁mainLock.unlock
4.在使用getTask方法去阻塞队列中获取锁
常见问题
原理是什么?
高并发线程池ThreadPool的使用方法?
你了解线程池吗?
原生线程池的方式?
线程池的拒绝策略?
丢弃当前任务
丢弃最老的任务
直接抛出异常
抛回调用者的线程处理
线程池的执行流程?
核心线程->队列->最大线程->拒绝策略
为什么要用线程池?
使用线程池的好处有哪些?
是否有用过ThreadPoolExecutor来管理线程?
如何在线程中启动其他线程 ?
线程通过调用start()方法将启动该线程,使之从新建状态进入就绪队列排队,
一旦轮到它来享用CPU资源时,就可以脱离创建它的主线程独立开始自己的生命周期了。
前面的例子中,都是在主线程中启动其他线程,实际上也可以在任何一个线程中启动另外一个线程。
线程的状态与生命周期、线程生命周期(状态)
说明
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。
在线程的生命周期中,它要经过5种状态
新建(New)
就绪(Runnable)
运行(Running)
阻塞(Blocked)
死亡(Dead)
新建的线程在它的一个完整的生命周期中,通常要经历如下的四种状态
尤其是当线程启动以后,它不可能一直"霸占"着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换
图解线程的状态
图解线程的状态
新建状态(NEW)
四种新建方法
继承Thread类创建线程
实现Runnable接口创建线程
使用Callable和Future创建线程
使用线程池例如用Executor框架
当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,
此时仅由JVM为其分配内存,并初始化其成员变量的值
运行状态(RUNNING)
如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。
线程必须调用start( )方法(从父类继承的方法)通知JVM,这样JVM就会知道又有一个新一个线程排队等候切换了。
一旦轮到它来享用CPU资源时,此线程的就可以脱离创建它的主线程独立开始自己的生命周期了。
线程死亡、结束(DEAD)
线程运行完毕
处于死亡状态的线程不具有继续运行的能力。线程释放了实体。
线程会以下面三种方式结束,结束后就是死亡状态。
正常结束
1. run()或call()方法执行完成,线程正常结束。
异常结束
2. 线程抛出一个未捕获的Exception或Error。
调用stop
3. 直接调用该线程的stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
就绪状态(RUNNABLE)
当线程对象调用了start()方法之后,该线程处于就绪状态。
JVM会为其创建方法调用栈和程序计数器,等待调度运行。
调用thread.start()
start
sleep休眠超时
阻塞状态(BLOCKED)
阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。
直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态
阻塞的情况分三种
等待阻塞(o.wait->等待对列)
调用wait,lockSupport.park()等方法
运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
同步阻塞(lock->锁池)
竞争锁失败
运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
其他阻塞(sleep/join)
运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
线程相关的基本方法、线程的常用方法
图解线程基本方法
图解线程基本方法
线程等待(wait)
wait
【不同类归属】Object的方法
而wait()方法,则是属于Object类中的。
【是否释放对象锁】调用wait方法的时候线程会放弃对象锁
当调用wait()方法时,线程会放弃对象锁
进入等待此对象的等待锁定池,
只有针对此对象调用notify()方法后,
本线程才进入对象锁定池,准备获取对象锁进入运行状态。
导致当前线程进入WATING状态
wait(): 强迫一个线程等待。
调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,
需要注意的是调用wait()方法后,会释放对象的锁。
因此,wait方法一般用在同步方法或同步代码块中。
线程睡眠/休眠(sleep)
sleep
【不同类归属】Thread的方法
对于sleep()方法,首先要知道该方法是属于Thread类中的。
sleep(int millsecond)
【是否释放对象锁】调用sleep()方法过程,线程不会释放对象锁
sleep方法导致了程序暂停执行指定的时间,(暂停指定时间),让出cpu给其他线程
但他监控状态依旧保持着,当指定时间到了又会自动恢复运行状态。
sleep()方法让出cpu给其他线程,
在调用sleep方法的过程中,线程不会释放对象锁
sleep不会释放当前占有的锁
导致线程进入TIMED-WATING状态
执行sleep()后转入阻塞状态
sleep导致当前线程休眠,
sleep()给其他线程机会时,不考虑优先级。
影响任务行为的一种简单方式就是,使线程体眠
选定给定的休眠时间, 调用它的sleep方法
强迫一个线程睡眠N毫秒。
优先级高的线程可以在它的run()方法中调用sleep方法来使自己放弃CPU资源,休眠一段时间。
使用TimeUnit替换Thread.sleep方法
一般使用的TimeUnit这个时间类,替换Thread.sleep方法
关于Time Unit中的sleep方法和Thread.sleep方法的比较
https://www.cnblogs.com/xiadongqing/p/9925567.html
TimeUnit使用的样例代码
实例
线程让步、作出让步(yield)
yield会使当前线程让出CPU执行时间片,与其他线程一起重新竞争CPU时间片。
执行yield()后转入就绪状态
yield()只会给同级或更高级的线程机会
一般情况下,优先级高的线程有更大的可能性成功竞争得到CPU时间片,
但这又不是绝对的,有的操作系统对线程优先级并不敏感。
如果知道一个线程已经在run方法中运行的差不多了, 那么它就可以给线程调度器一个提示:
我已经完成了任务中最重要的部分, 可以让给别的线程使用CPU了,这个暗示将通过yield方法作出
Thread.yield()是建议执行切换CPU, 而不是强制执行CPU切换。
对于任何重要的控制或者在调用应用时, 都不能依赖于yield方法
实际上, yield方法经常被滥用
线程中断、终止线程(interrupt)
中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。
这个线程本身并不会因此而改变状态(如阻塞,终止等)。
调用interrupt()方法并不会中断一个正在运行的线程。
也就是说处于Running状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。
若调用sleep()而使线程处于TIMED-WATING状态,这时调用interrupt()方法,会抛出InterruptedException,从而使线程提前结束TIMED-WATING状态。
许多声明抛出InterruptedException的方法(如Thread.sleep(long mills方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用isInterrupted()方法将会返回false。
中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。
比如,你想终止一个线程thread的时候,可以调用thread.interrupt()方法,在线程的run方法内部可以根据thread.isInterrupted()的值来优雅的终止线程。
interrupt()
一个占有CPU资源的线程可以让休眠的线程调用interrupt()方法“吵醒”自己,
即导致休眠的线程发生InterruptedException异常,从而结束休眠,重新排队等待CPU资源。
有4种原因的中断
JVM将CPU资源从当前线程切换给其他线程,使本线程让出CPU的使用权处于中断状态
线程使用CPU资源期间,执行了sleep(int millsecond)方法,使当前线程进入休眠状。
线程使用CPU资源期间,执行了wait( )方法。
线程使用CPU资源期间,执行某个操作进入阻塞状态。
终止线程四种方式
正常运行结束
程序运行结束,线程自动结束。
使用退出标志退出线程
一般run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。
它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。
使用一个变量来控制循环,
例如:最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出
代码示例
代码示例
定义了一个退出标志exit,当exit为true时,while循环退出,exit的默认值为false.
在定义exit时,使用了一个Java关键字volatile,这个关键字的目的是使exit同步,也就是说在同一时刻只能由一个线程来修改exit的值。
Interrupt方法结束线程
线程中断
public static boolean interrupted()
测试当前线程是否已经中断,并将线程状态设置为false
public boolean isInterrupted()
测试线程是否已经中断。线程的中断状态不受该方法的影响
public void interrupt()
中断线程,设置中断标识为为true
InterruptedException
对线程调用interrupt()时,如果该线程处于阻塞或者等待状态,那么就会抛出 InterruptedException
使用interrupt()方法来中断线程有两种情况:
(1)线程处于阻塞状态
如使用了sleep,同步锁的wait,socket中的receiver,accept等方法时,会使线程处于阻塞状态。
当调用线程的interrupt()方法时,会抛出InterruptException异常。
阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。
通常很多人认为只要调用interrupt方法线程就会结束,实际上是错的,
一定要先捕获InterruptedException异常,之后通过break来跳出循环,才能正常结束run方法。
(2)线程未处于阻塞状态
使用isInterrupted()判断线程的中断标志来退出循环。
当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。
代码讲解
使用isInterrupted()判断线程的中断标志来退出循环。
stop方法终止线程(线程不安全)(不推荐)
程序中可以直接使用thread.stop()来强行终止线程,
但是stop方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,
为什么不推荐使用stop方法来终止线程?
不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。
一般任何进行加锁的代码块,都是为了保护数据的一致性,
如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性
其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。
等待线程终止(join)
加入一个线程、线程联合
一个线程可以在其他线程上调用join方法, 其效果是等待一段时间直到第二个线程结束才正常执行。
一个线程A在占有CPU资源期间,可以让其它线程调用join()和本线程联合
如:B.join(A);
称A在运行期间联合了B
如果某个线程在另一个线程t上调用t.join() 方法, 此线程将被挂起, 直到目标线程t结束才回复(可以用t.isAlive) 返回为真假判断) 。
也可以在调用join时带上一个超时参数, 来设置到期时间, 时间到期, join方法自动返回。
对join的调用也可以被中断, 做法是在线程上调用interrupted方法, 这时需要用到tr...catch子句
Join方法等待线程死亡,话句话说,它会导致当前运行的线程停止执行,直到它加入的线程完成其任务
实例代码
子主题
join() 方法
等待其他线程终止
join(): 等待线程终止。
在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的宠幸。
为什么要用join()方法?
很多情况下,主线程生成并启动了子线程,需要用到子线程返回的结果,也就是需要主线程需要在子线程结束后再结束,这时候就要用到 join() 方法。
线程唤醒(notify)
notify(): 通知一个线程继续运行。
Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,
如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并在对实现做出决定时发生,
线程通过调用其中一个 wait() 方法,在对象的监视器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程,
被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。
类似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。
其他方法
isAlive():判断一个线程是否存活。
线程处于“新建”状态时,线程调用isAlive()方法返回false。
初建线程
在线程的run()方法结束之前,即没有进入死亡状态之前,线程调用isAlive()方法返回true.
重新分配实体的线程
currentThread(): 得到当前线程。
该方法是Thread类中的类方法,
可以用类名调用,
该方法返回当前正在使用CPU资源的线程。
activeCount(): 程序中活跃的线程数。
enumerate(): 枚举程序中的线程。
isDaemon(): 一个线程是否为守护线程。
setDaemon(): 设置一个线程为守护线程。
setName(): 为线程设置一个名称。
setPriority(): 设置一个线程的优先级。
getPriority()::获得一个线程的优先级。
start()
线程调用该方法将启动线程,使之从新建状态进入就绪队列排队,一旦轮到它来享用CPU资源时,就可以脱离创建它的线程独立开始自己的生命周期了。
run()
Thread类的run()方法与Runnable接口中的run()方法的功能和作用相同,
都用来定义线程对象被调度之后所执行的操作,
都是系统自动调用而用户程序不得引用的方法。
stop()和suspend()为何不推荐使用?
stop()会解除由线程获取的所有锁定,如果线程处于不连贯状态,很难检查出问题所在。
suspend()容易发生死锁,调用suspend()时,目标线程会停下来,但仍然持有之前的锁定。
线程间通信
wait/notify/notifyAll
synchronized/lock
thread方法
thread.join()
thread.yield()
管道通信
管道流pipeStream
是一种特殊的流,用于在不同线程间直接传送数据。
一个线程发送数据到输出管道,另一个线程从输入管道中读数据。
如何在两个线程之间共享数据
Java里面进行多线程通信的主要方式就是共享内存的方式,
共享内存主要的关注点有两个:
可见性
有序性
原子性
锁解决了原子性的问题
理想情况下,希望做到“同步”和“互斥”。
有以下常规实现方法:
将数据抽象成一个类,并将对这个数据的操作作为这个类的方法,这么设计可以和容易做到同步,只要在方法上加”synchronized“
将Runnable对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个Runnable对象调用外部类的这些方法。
线程同步
在处理多线程问题时,必须注意这样一个问题:当两个或多个线程同时访问同一个变量,并且一个线程需要修改这个变量,应对这样的问题作出处理。
在使用多线程解决许多实际问题时,可能要把某些修改数据的方法用关键字synchronized来修饰。
什么是线程同步
若干个线程都需要使用一个synchronized修饰的方法.
即程序中的若干个线程都需要使用一个方法,而这个方法用synchronized给予了修饰。
请分析同步方法和同步代码块的区别是什么?
同步方法默认用this或当前类对象作为锁
同步代码块可以选择以什么来加锁。
多个线程调用synchronized方法必须遵守同步机制:
当一个线程使用这个方法时,其他线程想使用这个方法时就必须等待,直到线程使用完该方法。
通过同步避免切换的影响
当一个线程使用CUP资源时,即使线程没有完成自己的全部操作,JVM也可能会中断当前线程的执行,把CPU的使用权切换给下一个排队等待的线程,当前线程将等待CPU资源的下一次轮回,然后从中断处继续执行
如果在程序的设计中,线程都需要做某些相同的操作,而且希望一个线程完整地完成这些操作后,其他线程才可以进行这些操作,那么就可以把这些操作封装到一个synchronized方法中。
在同步方法中使用wait()、notify() 和notifyAll()方法
wait()方法
可以中断方法的执行,使本线程等待,暂时让出CPU的使用权,并允许其它线程使用这个同步方法。
notifyAll()方法
通知所有的由于使用这个同步方法而处于等待的线程结束等待。
曾中断的线程就会从刚才的中断处继续执行这个同步方法,并遵循“先中断先继续”的原则。
notify()方法
只是通知处于等待中的线程的某一个结束等待。
线程异常捕获
由于线程的本质, 使你不能捕获从线程中逃逸的异常,
一旦异常逃出任务的run方法, 它就会向外传播到控制台, 除非你采取特殊的步骤捕获这种错误的异常
在Java 5之前, 可以通过线程组来捕获, 但是在Java 5之后, 就需要用Executor来解决问题, 因为线程组不是一次好尝试,
案例讲解
下面的任务会在run方法的执行期间抛出一个异常,
并且这个异常会抛到run方法的外面, 而且main方法无法对它进行捕获
为了解决这个问题, 我们需要修改Executor产生线程的方式, Java 5提供了一个新的接口
Thread.UncaughtExceptionHandLer, 它允许你在每个Thread上都附着一个异常处理器。
Thread.UncaughtExceptionHandLer, 它允许你在每个Thread上都附着一个异常处理器。
Thread.UncaughtExceptionHandLer.uncaughtException() 会在线程因未捕获临近死亡时被调用。
在程序中添加了额外的追踪机制, 用来验证工厂创建的线程会传递给Uncaught Exception Handler,
可以看到, 未捕获的异常是通过uncaught Exception来捕获的。
可以看到, 未捕获的异常是通过uncaught Exception来捕获的。
线程调度、优先级与进程调度算法
背景
线程调度器对每个线程的执行都是不可预知的,随机执行的
那么有没有办法告诉线程调度器哪个任务想要优先被执行呢?
实际场景:请给这个骑手马上派单
解决方案
通过设置线程的优先级状态,告诉线程调度器哪个线程的执行优先级比较高
JVM中的线程调度器负责管理线程
处于就绪状态的线程首先进入就绪队列排队等候CPU资源,同一时刻在就绪队列中的线程可能有多个。
Java调度器的任务是使高优先级的线程能始终运行,一旦时间片有空闲,则使具有同等优先级的线程,以轮流的方式顺序使用时间片。
Java虚拟机(JVM)中的线程调度器负责管理线程
线程调度器把线程的优先级分为10个级别
调度器把线程的优先级分为10个级别
分别用Thread类中的类常量表示。
尽管JDK有10个优先级, 但是一般只有三种级别
MAX_PRIORITY
NORM_PRIORITY
MIN_PRIORITY
优先级是否会导致死锁问题?
线程调度器倾向于让优先级较高的线程优先执行,然而,这并不意味着优先级低的线程得不到执行
也就是说,优先级不会导致死锁的问题。优先级较低的线程只是执行频率较低,
优先级的相关代码
代码
toString()方法被覆盖, 以便通过使用Thread.toString() 方法来打印线程的名称。
可以改写线程的默认输出, 这里采用了Thread【pool-1-thread-1,10,main】这种形式的输出。
通过输出, 你可以看到, 最后一个线程的优先级最低, 其余的线程优先级最高。
注意, 优先级是在run开头设置的,在构造器中设置它们不会有任何好处,因为这个时候线程还没有执行任务。
两种线程调度的方式
抢占式调度
抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,
系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。
在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
图解抢占式调度
图解抢占式调度
协同式调度
协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,
这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。
线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,
但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
图解协同式调度
图解协同式调度
JVM的线程调度实现(抢占式调度)
java使用的线程调使用抢占式调度,
Java中线程会按优先级分配CPU时间片运行,且优先级越高越优先执行,
但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,
反之,优先级低的分到的执行时间少,但不会分配不到执行时间。
线程让出cpu的情况
当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),
例如调用yield()方法。
当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上。
当前运行线程结束,即运行完run()方法里面的任务。
进程调度算法
优先调度算法
先来先服务调度算法(FCFS)
当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机,特点是:算法比较简单,可以实现基本上的公平。
短作业(进程)优先调度算法(SJF)
短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。该算法未照顾紧迫型作业。
高优先权优先调度算法
为了照顾紧迫型作业,使之在进入系统后便获得优先处理,引入了最高优先权优先(FPF)调度算法。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程。
非抢占式优先权算法
在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。
抢占式优先权调度算法
在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。
高响应比优先调度算法
在批处理系统中,短作业优先算法是一种比较好的算法,其主要的不足之处是长作业的运行得不到保证。如果我们能为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时间的增加而以速率a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机。该优先权的变化规律可描述为:
子主题
(1) 如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业。
(2) 当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时间愈长,其优先权愈高,因而它实现的是先来先服务。
(3) 对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待时间足够长时,其优先级便可升到很高,从而也可获得处理机。简言之,该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折衷。当然,在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。
基于时间片的轮转调度算法
时间片轮转法
在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几ms 到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。
多级反馈队列调度算法
(1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。
(2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。
(3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。
在多级反馈队列调度算法中,如果规定第一个队列的时间片略大于多数人机交互所需之处理时间时,便能够较好的满足各种类型用户的需要。
ThreadLocal
ThreadLocal定义
ThreadLocal,很多地方叫做线程本地变量
也有些地方叫做线程本地存储
(线程本地存储)
ThreadLocal 用来解决什么问题?ThreadLocal特性
提供线程内的局部变量
这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
ThreadLocal提供了线程的局部变量,且不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。
可以从尽量减少临界区范围,使用 ThreadLocal,减少线程切换、使用读写锁或 copyonwrite 等机制这些方面来回答。
基本用法
set(value)
首先获取当前thread的ThreadLocalMap
如果map已经初始化,则将kv存入map中
否则初始化map(此时构造函数已经将kv存入)
向ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
get()
从ThreadLocalMap获取值,key是ThreadLocal对象
remove()
initialValue()
底层实现:ThreadLocalMap
ThreadLocalMap
ThreadLocalMap
(线程的一个属性)
是ThreadLocal的内部类
用Entry进行存储,Entry的key的类型为ThreadLocal
每个Thread维护了一个ThreadLocalMap的成员变量,这是实现ThreadLocal的核心
每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
ThreadLocalMap其实就是线程里面的一个属性,它在Thread类中定义
ThreadLocal.ThreadLocalMap threadLocals = null;
图解
图解ThreadLocalMap
hash冲突解决方法:线性探测法
内存泄漏
ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,这时候ThreadLocalMap里面就会存在key为null但是value不为null的entry项。如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
解决方法
调用threadLocal.remove()方法来清理key为null的元素
根本原因
由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用
为什么key要用弱引用
引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用
如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
应用场景/使用场景
数据库连接
Session管理
图解
单个线程
线程上下文信息存储
常见问题
多线程的基础问题
在生产上,是否有使用过多线程?如何使用的?多线程的使用场景?+2
同步阻塞的方式有哪些?
并行和并发有什么区别?
如何实现一个生产者与消费者模型?
可以尝试通过锁、信号量、线程通信、阻塞队列等不同方式实现。
进程和线程的区别是什么?+1
进程是执行着的应用程序,而线程是进程内部的一个执行序列。一个进程可以有多个线程。线程又叫做轻量级进程。
守护线程是什么?
如何尽可能提高多线程并发性能?
ThreadLocal 是如何实现的?可以重点回答 ThreadLocal 不是用来解决多线程共享变量的问题,而是用来解决线程数据隔离
在 java 程序中怎么保证多线程的运行安全?
什么是死锁?
怎么防止死锁?
多线程的常见问题
开启Java多线程的方法?创建线程有几种不同的方式?你喜欢哪一种?为什么?有三种方式可以用来创建线程?
线程间是怎么进行通信的?
主要可以介绍一下 wait/notify 机制,共享变量的 synchronized 或者 Lock 同步机制等。
volatile
CountDownLatch
CyclicBarrier
线程有哪些状态?
创建线程有哪几种方式?
说一下 runnable 和 callable 有什么区别?
请说明线程的基本状态及状态间的关系?
新建状态
运行状态
销毁状态
就绪状态
(万事俱备,只欠cpu)
阻塞状态
可能因为调用wait()进入等待池
可能调用同步方法进入等锁池
可能sleep()等待休眠,发生I/O中断。
解释下线程的几种可用状态
线程在执行过程中,可以处于下面几种状态:
就绪(Runnable):线程准备运行,不一定立马就能开始执行。
运行中(Running):进程正在执行线程的代码。
等待中(Waiting):线程处于阻塞的状态,等待外部的处理结束。
睡眠中(Sleeping):线程被强制睡眠。
I/O阻塞(Blocked on I/O):等待I/O操作完成。
同步阻塞(Blocked on Synchronization):等待获取锁。
死亡(Dead):线程完成了执行。
就绪(Runnable):线程准备运行,不一定立马就能开始执行。
运行中(Running):进程正在执行线程的代码。
等待中(Waiting):线程处于阻塞的状态,等待外部的处理结束。
睡眠中(Sleeping):线程被强制睡眠。
I/O阻塞(Blocked on I/O):等待I/O操作完成。
同步阻塞(Blocked on Synchronization):等待获取锁。
死亡(Dead):线程完成了执行。
线程的 run()和 start()有什么区别?+1
start
start()方法来启动线程,真正实现了多线程运行。
这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码。
通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
run
方法run()称为线程体
它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行run函数当中的代码。
Run方法运行结束, 此线程终止,然后CPU再调度其它线程。
讲讲你知道的Java中线程安全的类有哪些?+1
sleep() 和 wait() 有什么区别?+1
notify()和 notifyAll()有什么区别?
线程池的问题
创建线程池有哪几种方式?
线程池都有哪些状态?
线程池中 submit()和 execute()方法有什么区别?
JUC的问题
ThreadLocal 是什么?有哪些使用场景?
说一下 atomic 的原理?
synchronized的问题
当一个线程进入对象的synchronized方法A之后,其它线程是否可进此对象的synchronized方法B?
不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。
A方法的对象锁已经被取走,试图进入B方法的线程只能在等锁池里等待对象锁。
在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?
监视器和锁在Java虚拟机中是一块使用的。
监视器监视一块同步代码块,确保一次只有一个线程执行同步代码块。
每一个监视器都和一个对象引用相关联。
线程在获取锁之前不允许执行同步代码。
synchronized 和 ReentrantLock 区别是什么?
synchronized 和 Lock 有什么区别?
说一下 synchronized 底层实现原理?
synchronized 和 volatile 的区别是什么?
锁常见问题
如何对线程进行加锁?
同步加锁的方式有哪些?
如何实现乐观锁与悲观锁?
有几种线程锁的类型?
读写锁适用于什么场景?
可以回答读写锁适合读并发多,写并发少的场景,另外一个解决这种场景的方法是 copyonwrite。
多线程锁的升级原理是什么?
0 条评论
下一页