0.学习准备
- 参考资料
参考书籍《深入理解Java虚拟机》
参考视频《深入理解JVM》(目前学习) - 简单目录:
- 线程安全
- 对象头Mark
- JVM层面的锁优化:
- 偏向锁
- 轻量级锁
- 自旋锁
- Java层面的锁优化:
- 减少锁持有时间
- 减小锁粒度
- 锁分离
- 锁粗化
- 锁消除
- 无锁
- javap
- 简单的字节码执行过程
- 常用的字节码
- 使用ASM生成Java字节码
- JIT即时编译和相关参数
1.线程安全
- 相关的详细内容和例子可以查看多线程相关的笔记:
https://zjxkenshine.github.io/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/ - 测试代码:
模拟一个累加的功能(如投票等)
测试类及输出结果:
出错原因:
ArraList进行扩展时另一个线程尝试进行插入,此时的ArrayList不可用,所以会抛出越界异常。
2.对象头Mark
- Mark Word,对象头的标记,32位
- 描述对象的hash,锁信息,垃圾回收标记,年龄等
- 指向锁记录的指针
- 指向monitor的指针
- GC标记
- 偏向锁线程ID
3.偏向锁,轻量级锁和自旋锁
1)偏向锁:
- 偏向锁简介及使用场景:
- 只有在竞争不激烈的场合才能够使用偏向锁来提高性能
- 在竞争激烈的场合偏向锁会增加系统负担
- 锁会偏向当前已经占有锁的线程
- 将对象头Mark的标记设置为偏向,并将线程ID写入对象头
- 只要没有进程,获得偏向锁的线程在将来进入同步块时不需要同步
- 其他线程请求相同的锁时,偏向模式结束
- 偏向锁的使用参数:
-XX:+UseBiasedLocking
- 默认启用
- 简单例子:
测试代码:(Vector自带同步方法)
测试参数1:默认参数,使用偏向锁public class BasedLockTest { public static List<Integer> numberlist=new Vector<Integer>(); public static void main(String[] args) { long begin =System.currentTimeMillis(); int count=0; int startnum=0; while(count<10000000){ numberlist.add(startnum); startnum+=2; count++; } long end =System.currentTimeMillis(); System.out.println(end-begin+"ms"); } }
测试结果1:
测试参数2:使偏向锁在程序启动时就启用7164ms
测试结果2:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
测试参数3:关闭自旋锁6995ms
测试结果3:-XX:-UseBiasedLocking
7271ms
- 测试结论:
在这种单线程无竞争的情况下使用偏向锁能够提高系统性能。
2)轻量级锁:
- BasicObjectLock:存放在线程栈中的锁对象
包含了两部分:- BasicLock:存放了对象头信息
- 指向持有了该锁对象的指针
- 普通的锁(重量级锁)处理性能不够理想,
轻量级锁是一种快速的锁定方法 - 轻量级锁的加锁过程:
如果对象没有上锁:进行两步操作(交换)- 将对象头的Mark指针保存到锁对象BasicObjectLock中
- 将对象头设置为指向锁的指针(指向栈空间)
- 简单的代码示例如下:
- 轻量级锁的使用环境:
- 如果轻量级锁失败,说明存在竞争,升级为重量级锁(常规锁)
- 没有锁竞争的时候,可以减少传统锁使用OS互斥量产生的性能损耗
- 竞争激烈的时候轻量级锁会做很多额外的操作,影响性能
- 轻量级锁是默认开启的,可以使用如下参数同时关闭轻量级锁和偏向锁:
-XX:+UseHeavyMonitors
3)自旋锁:
- 简介:
当竞争存在时,如果线程可以很快获得锁,那么可以不再OS层挂起线程,可以让线程做几个空操作(自旋) - 使用参数:
JDK1.6:-XX:UseSpining
JDK1.7去掉了这一参数,改为内置实现,默认开启。 - 注意事项:
- 同步块很长,自旋失败,会影响系统性能
- 同步块很短,自旋成功,节省系统挂起切换的过程,提升系统性能
- 只要空转指令的开销小于挂起和切换的开销自旋就是成功的
自旋之后还拿不到锁,还是需要挂起和切换线程,那么就是自旋失败的
4)三种锁的总结:
- 并不是Java语言层面的锁优化方法,是JVM层面的
- 内置于JVM中的锁的优化方法和获取锁的步骤:
- 偏向锁可用会先尝试偏向锁
- 轻量级锁可用会先尝试轻量级锁
- 以上两种锁都失败则尝试自旋锁
- 自旋锁失败则尝试普通锁,使用OS的互斥量在操作系统层挂起
4.Java语言层面进行锁优化
简单目录:
- 减少锁持有时间
- 减小锁粒度
- 锁分离
- 锁粗化
- 锁消除
- 无锁
1)减少锁的持有时间:
- 没有必要做同步的代码就不要放在同步代码块或者同步方法中。
- 优化前的代码:
public synchronized void syncMethod(){ System.out.println("无关代码块1"); System.out.println("需要同步的代码"); System.out.println("无关代码块2"); }
- 优化后的代码:
public void syncMethod(){ System.out.println("无关代码块1"); synchronized(this){ System.out.println("需要同步的代码"); } System.out.println("无关代码块2"); }
- 锁持有时间减少,偏向锁的成功率提高。
2)减小锁粒度:
- 基本思想:
将大对象拆成小对象,大大提升并行度,降低锁竞争
锁竞争降低,偏向锁和轻量级锁的成功率提高 - 典型实现:ConcrrentHashMap(稍后再介绍)
- HashMap的同步实现:
- Collections.synchronizedMap(Map
m)
获得一个同步hash表 - 返回一个synchronizedMap对象,内部的get,put实现如下:
- Collections.synchronizedMap(Map
- ConcrrentHashMap减小锁粒度的实现:
- 分为若干个段(Segment):
Segment<K,V>[] segments
- Segment中维护HashEntry
(表项) - put操作时:
先定位到相应的Segment,再锁定这个Segment,再执行put操作 - 减小锁粒度之后,ConcrrentHashMap允许多个线程同时进入。
- 分为若干个段(Segment):
- 如果锁粒度太细也会引起性能的损耗
3)锁分离:读锁和写锁
- 广义上说也是属于减小锁粒度的一种
减小锁粒度:结构上对对象做分离
锁分离:功能上对锁做分离 - ReadWriteLock:读写锁
在读多写少的情况可以提高系统的性能。
读读不互斥,读写互斥,写写互斥: - 读写分离的思想扩散:
只要操作互相不影响,锁就可以分离。 - LinkedBlockingQueue:阻塞链表(队列)
结构示意图如下:
take操作和put操作是互不影响的,可以将这两种操作的锁分离。
4)锁粗化:
- 通常情况下,为了保证多线程的有效并发,会要求每个线程是有锁的时间尽可能少,即使用完公共资源之后立即释放锁。等待这个锁的其他线程就能尽早得到公共资源。
- 但是物极必反,如果对一个锁一直进行请求,同步和释放,也会消耗系统性能,影响锁的优化。在这种场合就需要泛起到而行之。
- 示例情况:
粗化后的代码: - 极端情况:循环中获取锁
一般情况下是不合道理的,粗化过后的代码:for(int i=0;i<n;i++){ synchronized(lock){ //同步代码 } }
synchronized(lock){ for(int i=0;i<n;i++){ //同步代码 } }
5)锁消除:
- 是一种JVM层面的优化
- 在即时编译时如果发现不可能被共享的锁对象,就消除这个对象的锁。
一般情况下程序员是不会给一些完全不可能同步的对象加锁的。
但是StringBuffer和Verctor等的类内部有锁,在使用这些类对象的时候就隐式的将锁引入到其中了。 - 如以下代码:
StringBuffer的append操作自带了锁,但是在这段代码中完全没有用到锁。
关闭和不关闭锁消除的运行结果测试:循环2000000次
6)无锁:
- 无锁是最好的一种锁方式
- 锁是一种悲观操作:预期竞争是一定存在的
无锁是一种乐观的操作:预期竞争是不存在的 - 锁是在操作之前先定义好如何解决竞争。
无锁是在操作之后遇到同步问题再回来定义解决。 - 无锁的一种实现:CAS
- CAS(Compare And Swap),比较交换技术
- 非阻塞的同步
- CAS(V,E,N):
- V:要更新变量
- E:期望值
- N:新值
- 如果新值满足期望(V=E),那么久赋值给变量V,做完以后将V值返回。
- 在应用层面判断是否被多线程干扰,如果有干扰,则通知线程重试
不停重试直到成功,或者放弃 - 比较交换是一个cpu指令。
- 无锁操作会使程序变复杂,但是会使性能变的更好。
- JUC并发包中的原子操作就是一种CAS无锁的实现:
- java.util.concurrent.atomic包中的都是无锁实现,性能高于一般的有锁操作。
5.javap
class文件反汇编工具
1)javap的简单使用示例:
- Calc类代码如下:
public class Calc{ public int calc(){ int a=500; int b=200; int c=50; return (a+b)/c; } }
- 使用命令:
javap -verbose Calc
得到对应的反汇编的方法: - 汇编方法解释:
前面是一些堆栈参数信息。
第一列的数字为字节码的偏移量(行号)
第二列为指令,第三列为指令的参数 - 关于指令(字节码):
- sipush:将操作数压栈(一个短整型常量)
- istore_n:弹出一个栈帧,放入局部变量表的第n个位置
- binpush:将一个单字节的常量(-128~127)推送至操作数栈顶
- iload_n:将局部变量表的第n个位置的数压入操作数栈
- iadd:相加
- idiv:相除
- 每一个指令都是有相对应的字节码的
2)简单的字节码执行过程:
- 压入操作数500:(0,3)
- 压入操作数200:(4,7)
- 压入单字节操作数50:(8,10)
- 取出局部变量1,2:(11,12)
- 相加并添加局部变量3:(13,14)
- 相除并返回:(15,16)
6.关于字节码
1)字节码简介:
- 字节码在Class文件中的位置:
该类文件中的2A 1B B5 00 20 B1
就是字节码。 - 字节码指令为一个byte类型的整数:
左边的是便于人们理解的指令名称,右边的则是在计算机当中的表示方式。 - 关于上图
2A 1B B5 00 20 B1
字节码的理解:
2)Java中常用的字节码:
- 常量入栈的字节码:
JVM没有寄存器,所有的操作都要通过栈来完成 - 局部变量压栈:
- 出栈装入局部变量:
3)通用型的栈操作指令:
- 通用栈操作(无类型):
- 类型转换字节码:
以i2l为例:- 将int类型转换为long类型
- 执行前,栈:
...,value
- 执行后,栈:
...,result.word1,result.word2
(long需要两个字空间) - 弹出int,扩展为long,并入栈。
- 运算操作:
分为整数的运算和浮点型的运算
最前面的ilfd代表的是数据类型。
都是一些基本的加减乘除的操作。
4)对象,流程控制相关的字节码:
- 对象操作指令:
- new:新建对象
- getfield:得到给定实例对象的值
- putfield:设置给定实例对象的值
- getstatic:获得静态对象
- putstatic:设置静态对象
- 流程控制字节码:
如ifeq byte1 byte2:- 执行前,栈:…,value
- 执行前,栈:…
- value出栈后如果栈顶元素为0则跳转到byte1,byte2指定的字节码处
(byte1<<8)|byte2
,byte左移8为按位或byte2
- 方法调用和返回:
- invokevirtual:对普通的实例方法进调用(动态绑定,有多态)
- invokespecial:对子类调用父类方法等场合进行调用(静态绑定,无多态)
- invokestatic:对静态方法进行调用
- invokeinterface:调用接口方法的指令
- xreturn:统一返回指令(x可以为ilfda或者空)
7.使用ASM生成Java字节码
- asm是java的字节码操作框架,可以动态查看类的信息,动态修改,删除,增加类的方法。
可以用于修改现有的类或者动态产生新的类。 - 目前许多框架如cglib、Hibernate、Spring都直接或间接地使用ASM操作字节码
还有一些软件如Eclipse也使用了ASM操作字节码 - 关于ASM的具体如何使用以后再补充,
可以参考博客:
asm字节码操作 方法的动态修改增加 - ASM可以使先类似于AOP的织入功能
- 在函数开始或者结束时嵌入一些字节码
- 可用于鉴权,日志等
##8.JIT相关参数:
1)JIT简介:
- 在Java当中,单纯的字节码执行的效率是很差的(解释执行),所以需要对热点代码进行编译,编译成机器码再执行。在运行时编译叫做JIT(Just-In-Time)
- JIT的基本思路是将热点代码(执行比较频繁的代码),编译成机器码。
以解释为基础,对热点进行编译。 - 编译技术大约分为两种,一种AOT,只线下(offline)就将源代码编译成目标机器码,这是普遍用在系统程序语言中;另一种是JIT,只及时的编译,但是大部分的JIT引擎,针对的是将IR(中间代码,如JavaByteCode) 在运行时, 有针对性的翻译成机器码。
- JIT示意图:
是否是热点由方法调用计数器和回边计数器来控制:- 调用计数器:方法调用次数
- 回边计数器:方法内循环次数
2)JIT相关参数:
- 可以使用如下JVM参数对编译的方法进行打印:
-XX:CompaileThreshold=1000 -XX:+PrintCompilation
- 三个参数:
-Xint:全部解释执行
-Xcomp:全部编译执行
-Xmixed:混合执行,默认的 - 关于更多的编译的知识,参考《编译原理》。
最后更新: 2018年05月20日 18:06
原始链接: https://zjxkenshine.github.io/2018/05/17/JVM学习笔记(六):锁与字节码执行/