JVM架构模型
JVM架构模型
指令集架构分类:
基于栈的指令集架构(JVM)
设计简单,对系统资源要求不高(只有进栈出栈操作)
使用零地址指令方式分配,避开了寄存器的分配难题
由于使用零地址指令,其执行过程依赖于操作栈。指令集更小(但会花费更多的指令去完成一项操作,8字节对齐)
不需要硬件支持,可移植性更好,更好实现跨平台。
基于寄存器的指令集架构(X86: PC,Android的Davlik虚拟机)
指令集架构完全依赖硬件,可移植性差,但也因此具有更好的性能和更高的执行效率
大部情况下以一地址指令,二地址指令,三地址指令为主;指令集更大16字节对齐,但完成一项操作会花费更少的指令
总结: 由于跨平台的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
优点: 跨平台,指令集小,编译器容易实现;缺点: 性能下降,实现同样的功能需要更多的指令。
tips:反编译命令: javap -v xxx.class
Java多线程基础
Java多线程基础
线程是一个轻量级的“进程”, 是程序最小的执行单位。了解多线程,我们从线程的生命周期、基本操作、线程安全等几个方面来入手。
生命周期
在Java中一个线程的生命周期状态有以下六种(都在Thread.State枚举中):
1 |
|
可以简单画一张图来描述Java线程的生命周期:
基本操作
其实在上面的线程状态图中已经包含了大部分线程的基本操作了:
new 一个线程对象,用Thread#start()启动线程。
Thread#start()
会让这个线程执行Thread#run()
,但Thread#run
默认的实现为空,故在new
一个线程对象时需要重写(@Override
)run()
方法,实现自己的任务逻辑。
tips: start()会调用native方法start0(),继而由 JVM 来实现多线程的控制,因为需要系统调用来控制时间分片
我们可以通过继承Thread类来重写run方法,实现自定义任务。
Java是单继承(只能继承一个类)多实现(可以实现多个接口),由于单继承的局限性,更推荐通过实现Runnable
接口的run()
方法来实现自定义任务(实际上该接口也只有这一个方法).
Thread类有一个重要的构造方法: public Thread(Runnable target)
,看到target我们自然而然就回想到代理模式。实际上Thread和Runnable就是一种静态代理的实现,参考以下代码(JDK8部分源码)
1 |
|
线程终止与中断
线程终止的情况:
- 正常执行结束
- 调用Thread#stop()方法暴力终止。由于其会把执行一半的线程强行终止,可能还会引起数据不一致问题,目前已被废弃(@Deprecated)
stop已被废弃,JDK提供了一个更为强大的支持,线程中断:
1 |
|
线程中断并不会使线程立马终止,而是给线程发送一个通知,告知线程目标需要终止,线程具体在什么时候退出,由目标线程自行决定(通过isInterrupted判断状态,决定怎么退出以及退出逻辑)。
关于中断还有中断响应这个概念,所有抛出InterruptedException的方法都可以响应中断,例如:
1 |
|
tips: 对抛出的InterruptedException进行捕获会清除中断标记,如果后续还需要判断中断状态,需要再次设置中断标记Thread.currentThread.interrupt()
wait()和notify()。
wait() 和 notify()是Object类中的两个方法。用于对多线程协作进行支持。
obj.wait()并不是随便可以调用的,它 必须包含在对应synchronized(obj) 语句中 ,因为无论是wait()还是notify()都需要先获得一个目标对象的监视器 (源码中wait()的注释This method should only be called by a thread that is the owner of this object's monitor.)
。notify() 也一样。
obj.wait()使持有obj对象锁的线程进入等待,并释放资源 让其他等待obj对象的线程可以正常执行,obj.notify()会 随机 唤醒一个等待中的线程, 但不会立即释放资源 需要等待当前线程的synchronized语句执行完毕才会释放,还有一个obj.notifyAll()会唤醒所有等待的线程.
tips: 实际上, hotspot JVM 中notify()是顺序唤醒的。参考文章
tips: sleep()方法也会让线程等待若干时间,但并不释放锁资源,也不能被通知唤醒。 但wait() 和 sleep() 都可响应中断。sleep()会使线程进入TIMED_WAITING状态
tips: 使用jstack命令可以查看当前系统的线程信息。配合jps使用
tips: 推荐wait()和notify()配合使用来替换挂起suspend()和继续执行resume()这两个已被废弃的方法。线程挂起suspend()并不会释放资源,若使用不当,resume()方法在suspend()之前执行了,还会导致死锁。被挂起的线程状态还是Runnable
join() 和 yield()
join() 字面意思是加入,但其在Java中其实也是一种等待,其核心代码如下,调用了wait(0)
方法.但join()不用notify通知唤醒,它会在被阻塞线程执行完毕后自动释放资源。
适用场景: 线程A的输入依赖与线程B的输出,则就需要使用B.join(),等待B执行完毕,A再继续执行
1 |
|
tips: 由于join() 实际调用wait() 故在开发中不用在Thread对象上使用wait() , notify(),因为这样可能会和系统api相互影响产生预期之外的错误。
yield()是一个静态native方法,使用yield() 会使当前线程让出CPU,之后和其他线程再次竞争CPU资源。
使用场景:一个线程不怎么重要,或者优先级很低,怎可以在适当的时候调用yield() 让出资源,给其他线程更多机会。
其他操作
- 设置后台守护进程(Daemon) ,如垃圾回收线程,JIT线程
1 |
|
- 设置线程优先级(Priority), 优先级从1到10,数字越大优先级越高,高的优先级并不表示一定会最先执行.Thread类默认给了三个优先级 1, 5, 10.
1 |
|
线程安全概念
使用多线程可以提升效率但是也不能牺牲正确性,多线程操作临界区数据时若没有进行恰当的处理,则很有可能会产生冲突。最简单的处理办法就是使用synchronized关键字来实现线程间的同步,更加强大的控制则需要对JUC并发包进行学习。
synchronized关键字除了用于同步,确保线程安全外还可以保证线程间的可见性和有序性。可见性完全可以替代volatile关键字,只是没有volatite使用方便;有序性,被synchronized修饰的代码块内部是串行执行的,必然可以保证有序性。
tips: 并发下的ArrayList可能会抛出下标越界的异常,这是由于ArrayList在扩容过程中,内部一致性遭到破坏,由于没有锁的保护其他线程访问到了不一致的内部状态,导致下标越界。 或者没有异常信息但是最终结果不一致,这是由于多线程访问冲突,使得保存容器大小的变量被不正常访问,同时有多个线程对ArrayList的同一位置进行赋值,并发下的HashMap也可能会出现这个问题。
并发与JMM(2)——指令重排与Happen-Before规则
指令重排
在多线程的有序性里边我们已经初步了解了指令重排,但是需要注意的是,在单个线程里指令执行的顺序一定是一致的,也就是说指令重排必须保证串行语义的一致性,不能因为指令重排导致串行语义的逻辑发生问题。
tips: 指令重排可以保证串行语义的一致,但无法保证多线程间的语义一致
为什么要进行指令重排? 无他,为了性能。
在Cpu执行一条指令时,简单来说可以分为如下几步:
- 取指令 IF
- 译码和取寄存器操作数 ID
- 执行或者有效地址计算 EX
- 存储器访问 MEM
- 写回 WB
CPU在实际工作中执行一组指令,为了性能,并不是等前一条指令执行完毕再依次执行下一条指令,而是采用流水线的工作原理:
在实际执行一个程序时,流水线之间的衔接不可能总是完美的,如 A = B + C 这个操作:
图中的红叉就表示流水线中断,CPU进入等待,一旦发生中断,这个时间点执行的所有步骤都会向后延(最后的SW操作执行到ID也进入等待)极大影响效率。
为了避免这种情况发生就需要进行指令重排,在不影响串行语义的情况下对指令进行重新排序,尽可能提高效率。
Happen-Before规则
Java虚拟机和执行系统再进行指令重排时需要遵守Happen-Before规则。
Happen-Before规则是用来阐述操作之间的内存可见性。用来保证正确同步的多线程程序的执行结果不被改变
望文生义,Happen-Before很容易理解为前一个操作执行完毕再执行后一个操作,即两个遵守happen-before规则的操作是按顺序先后执行的,这样理解对,但不完全对。
站在开发者的角度来说,这样理解好像没什么毛病,毕竟系统执行程序的结果是符合预期的(语义未改变);但是对于Java虚拟机来说,只要保证第一个操作的执行结果对第二个操作可见即可,如果指令重排后的执行结果和happen-before规则执行的结果一致则允许进行重排。
一张图理解happen-before:
Happen-Before规则的基本原则:
- 程序顺序原则: 单个线程内保证语义的串行性。
- volatile规则: volatile变量的写,先发生与读,保证volatile变量的可见性。
- 锁规则: 解锁(unlock)必须发生再随后的加锁(lock)前。
- 传递性: A先于B, B先于C ,那么A必然先于C。
- 线程的start()方法先于它的每一个动作。
- 线程的所有操作优先于线程的终结(Thread.join())
- 线程的中断(interrupt()) 先于被中断线程的代码。
- 对象的构造函数执行、结束优先于finalize() 方法。