高并发:服务能同时处理多个请求,提高程序性能
java平台中,线程就是一个对象,创建需要分配内存。
与普通对象不同,jvm会为每个线程分配调用栈所需的内存空间,调用栈用于跟踪java方法间的调用关系以及java代码对Native Code(多为 C代码)的调用。
java中的每个线程可能还有一个内核线程(具体与jvm实现有关)与之对应。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jBnNfHDw-1576115827051)(evernotecid://626F545F-28E7-432D-B442-A76BAC946322/appyinxiangcom/14768996/ENResource/p91)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4EOcSMZc-1576115827052)(evernotecid://626F545F-28E7-432D-B442-A76BAC946322/appyinxiangcom/14768996/ENResource/p92)]
eg:
mac中,使用jstack获取线程转储
//进入jdk安装地址
» /usr/libexec/java_home -V
» cd /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home
//获取当前所有引用PID
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home » jps
47697 Launcher
47698 OrtApp
46002
46535 KotlinCompileDaemon
47818 Jps
//获取线程转储
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home » jstack -l 47698
2019-09-23 09:24:48
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.181-b13 mixed mode):
"lettuce-kqueueEventLoop-9-1" #223 daemon prio=5 os_prio=31 tid=0x00007fe6cd166800 nid=0x16313 runnable [0x0000700018ed0000]
java.lang.Thread.State: RUNNABLE
at io.netty.channel.kqueue.Native.keventWait(Native Method)
at io.netty.channel.kqueue.Native.keventWait(Native.java:94)
at io.netty.channel.kqueue.KQueueEventLoop.kqueueWait(KQueueEventLoop.java:149)
at io.netty.channel.kqueue.KQueueEventLoop.kqueueWait(KQueueEventLoop.java:140)
at io.netty.channel.kqueue.KQueueEventLoop.run(KQueueEventLoop.java:216)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:897)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.lang.Thread.run(Thread.java:748)
Locked ownable synchronizers:
- None
...
另外,在jdk安装路径下执行:
//打开jvisualvm
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home » jvisualvm
可打开图形化工具,其中可以获取线程的转储信息。
可用同样方式打开JMC。
竞态(Race Condition):线程间相互干扰,由脏读和更新丢失导致最终产生结果与预期结果不一致的现象。
计算的正确性依赖于相对时间顺序(Relative Time)或线程的交错(Interleaving)。
分析竞态的方法 - 二维表分析法
竞态产生条件:
原子性:
Lock 软件锁;
CAS 硬件锁。
java基本数据类型中,long、double的写操作都不具有原子性。通过添加volatile关键字可以使其具有原子性。
可见性(Visibility): 程序中变量被分配到寄存器(Register)中处理,一个处理器的寄存器无法读取另一个处理器的寄存器。运行在不同处理器的线程共享变量分配到寄存器进行存储,就会出现可见性问题。
缓存一致性协议(Cache Coherence Protocol): 用于读取其他处理器高速缓存
中数据,并更新到该处理器高速缓存中。
缓存同步:一个处理器从自身处理器缓存以外的其他存储部件中读取数据并将其更新到该处理器的高速缓存的过程。
高速缓存、主内存内容是可同步的
。
冲刷处理器缓存(写):为保障可见性,必须使一个处理器对共享变量做的更新最终被写入该处理器的高速缓存或主存中的过程。
刷新处理器缓存(读):若共享变量在处理器读取之前进行了更新,该处理器必须从进行更新操作的处理器的高速缓存或主存中将更新的内容进行缓存同步。
保障可见性,可以使用volatile
:
相对新值:一个线程更新共享变量后,其他线程能读到更新值,这个值称为变量的相对新值。
最新值:一个线程更新共享变量后,其他线程不能读到更新值,这个值称为变量的最新值。
可见性保障仅意味着线程能够读到共享变量的相对新值,并不能保证该线程能够读到最新值。
保障原子性,上述操作process2最终读取到的a值可能是0/1/2
保障可见性,上述操作process2最终读取到的a值为2
java规范保证,一个线程终止,其对共享变量的更新,另一个调用期join方法的线程是可见的
重排序(Reordering):一个处理器上执行多个操作,另一个处理器角度来看可能与目标代码所指定的顺序不一致。
几种内存操作顺序操作:
java平台包含两种编译器:
javac几乎不会执行指令重排序;JIT可能执行指令重排序。
查看JIT编译器动态生成的汇编代码
下载hsdis反编译工具,将hsdis-amd64.dylib文件放到/Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home/jre/lib/server下,和libjvm.dylib同级
文件放置好后,使用命令
java -server -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:PrintAssemblyOptions=intel [JAVA 文件路径]
即可看到指定代码的汇编代码内容.
-XX:LogFile=[xxx/xxx.log]
可将反编译内容输出到指定文件中
现代处理器:“顺序读取” – “乱序执行” – “顺序提交” 也会导致重排序;
ROB(重排序缓冲器)
内存重排序
Load: 从指定RAM地址(通过高速缓存加载)加载数据到寄存器
Store: 将数据存储到指定地址表示的RAM存储单元
貌似串行语义(As-if-serial Semantics)
仅保证重排序不影响单线程程序的正确性。
为保证貌似串行语义,存在数据依赖关系
的语句不会被重排序。若两个操作(指令)访问同一个变量(地址)且其中一个操作(指令)为写操作,那么两个操作之间就存在数据依赖关系。
控制依赖关系
: 允许被重排序,若一条语句的执行结果会决定另外一条语句能够被执行,这两条语句存在控制依赖关系。如if语句中的条件表达式和对应的语句体。
从底层角度,禁止(逻辑上)重排序是通过调用处理器提供的相应指令(内存屏障)来实现的。java会替我们与这类指令打交道,我们只需要使用语言本身提供的机制即可。
线程上下文切换: 一个线程被暂停,另一个线程被选中开始或继续运行的过程
时间片: 一个线程可以连续占用处理器运行的时间长度
切入: 一个线程被操作系统选中占用处理器开始或继续运行
切出: 一个线程被剥夺处理器使用权而暂停运行
上下文: 切入和切出时刻相应线程所执行的任务的进行程度(如计算中间结果、执行到哪条指令等),一般包含通用寄存器的内容和程序计数器的内容。
暂停: 线程由RUNNABLE状态转换为非RUNNABLE状态。
唤醒: 线程由非RUNNABLE状态转换为RUNNABLE状态。
被唤醒的线程并非立即占用处理器运行,当被唤醒的线程被操作系统选中占用处理器继续运行时,操作系统才会恢复其上下文。
由自身因素导致的切出,如下方法会导致自发性上下文切换
I/O操作或等待其他线程持有的锁
由于线程调度器的原因被迫切出
直接开销:
间接开销:
测量:
确定一个多线程程序在某个时间段或某种场景下运行时发生的上下文切换(主要是自发性上下文切换)的次数。
perf stat -e cpu-clock, task-clock, cs, cache-references, cache-misses java [Main Class]
由于资源稀缺性或程序自身的问题和缺陷导致线程一直处于非RUNNABLE状态,或状态处于RUNNABLE状态但是其要执行的任务一直无法进展的现象就被称为线程活性故障
排他性资源:一次只能够被一个线程占用的资源,如处理器、数据库连接、文件等。
资源争用:一个线程占用一个排他性资源进行读写操作而未释放其对资源所有权的时候,其他线程试图访问该资源的现象。
高/低争用:同时试图访问同一个已经被其他线程占用的资源的线程数量多/少。
高并发:处于运行状态(RUNNABLE的子状态RUNNING)的线程数量多,程序运行理想的状态为高并发、低争用。
资源调度:多个线程申请同一个排他性资源的情况下,决定哪个申请者占用该资源的过程。常见特性是它是否能保证公平性。
公平性:资源的申请者是否按照其申请资源的顺序而被授予资源的独占权。
排队:常见资源调度策略,调度器持有一个等待队列,未获取独占权的线程进入队列暂停,待资源释放被唤醒,从队列中移除,若再次申请资源失败,再次进入队列中暂停;由此可见,资源调度可能导致上下文切换。
公平调度策略
:资源未被其他任何线程占用,等待队列为空的情况,资源的申请者才被允许抢占相应资源的独占权。抢占失败的申请者进入等待队列。此策略中资源申请者总是按照先来后到的顺序获得资源的独占权。
非公平调度策略
:允许插队。一个线程释放其资源独占权的时候,等待队列中的一个线程会被唤醒再次申请相应的资源,而在这个过程中另外一个申请该线程的活跃线程可以与这个被唤醒的线程共同参与相应资源的抢占。可能导致饥饿现象。吞吐量更高,但申请者获取相应资源的独占权所需的时间偏差可能较大。
非公平调度策略 | 公平调度策略 |
---|---|
适用于多数线程占用资源较短或资源平均申请时间间隔相对较短的场景 | 适用于多数线程占用资源较长或资源平均申请时间间隔相对较长的场景 |
吞吐率较大 | 吞吐率较小 |
可能导致饥饿状态 | 不会导致饥饿状态 |
等待队列中的线程从被唤醒到继续运行可能需要一段时间,此间新来线程若占用该资源时间不长,完全有可能在被唤醒线程继续执行之前将资源释放,此时可能减少了上下文切换次数。若新来线程占用资源时间太长,被唤醒资源需要重新被暂停进入等待队列中,增加了上下文切换次数。