https://www.cnblogs.com/LemonFive/p/11246086.html?from=timeline&isappinstalled=0
https://juejin.im/post/5b4eec7df265da0fa00a118f#heading-0
https://juejin.im/post/5cfddf48e51d455d877e0d04
https://www.cnblogs.com/YDDMAX/p/5658607.html
大端模式,小端模式扫盲贴:
https://blog.csdn.net/aitangyong/article/details/23204817
《深入理解java虚拟机》第十三章线程安全与锁优化
参考图例:
任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。
java对象头由mark word(标记字段)和 klass pointer(类型指针)两部分组成,mark word存储了对象的对象类型指针,指向类元数据信息。而mark word则存储同步状态、标识、hashcode、GC状态的信息。
**PS:**在64位的jvm中,mark word(占64位),klass point(64位),对象头总计128位。如果应用的对象过多,klass point会浪费大量的内存,可以使用+UseCompressedOops 将指针压缩至32位。
从上图中我们可以看出,判断锁的类型,主要依据后三位。
maven中加入依赖:
<dependency>
<groupId>org.openjdk.jolgroupId>
<artifactId>jol-coreartifactId>
<version>0.8version>
dependency>
创建并打印工具A对象的java对象头,同时查看本机jvm中的字节序。
大端模式 (Big-Endian):就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
小端模式(Little-Endian):就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
import org.openjdk.jol.info.ClassLayout;
/**
* Created by leixingbang on 2019/9/23.
*/
public class A {
boolean flag = false;
public static void main(String[] args) throws InterruptedException {
A a = new A();
//查看jvm是大端模式还是小端模式,打印结果:LITTLE_ENDIAN
System.out.println(ByteOrder.nativeOrder());
Thread.sleep(3000L);
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
验证本机为32位jvm:
C:\Users\Administrator>java -d64
Error: This Java instance does not support a 64-bit JVM.
Please install the desired version.
C:\Users\Administrator>java -d32
用法: java [-options] class [args...]
(执行类)
或 java [-options] -jar jarfile [args...]
(执行 jar 文件)
输出结果:
小端模式的高低位与上图mark word的组成结构图相反:
第一行(4个字节):表明对象正处于无锁状态。且分代年龄为1,对象的hashcode为0
第二行为(4个字节):32位的klass point
第三行为(1个字节):1个字节的boolean对象
第四行(7个字节):补齐对象用。
JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。这个延时的时间大概为4s左右,具体时间因机器而异。
当然我们也可以设置JVM参数 -XX:BiasedLockingStartupDelay=0 来取消延时加载偏向锁。
当我们在idea中加入上述参数后,可以发现,输出结果发生变化。
具体为:
LITTLE_ENDIAN
com.crossoverjie.concurrent.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0d 00 00 00 (00001101 00000000 00000000 00000000) (13)
4 4 (object header) 28 19 04 15 (00101000 00011001 00000100 00010101) (352590120)
8 1 boolean A.flag false
9 7 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
加入jvm参数:-XX:BiasedLockingStartupDelay=0后,在没有加入synchronized关键字的场景下,我们可以看到后三位为:101,倒数第三位为1,标志着启动了偏向锁。但是我们也同时看到,剩余25位(偏向的线程标识)全部为0,说明没有偏向任何线程。
可以看做为,此时偏向锁正处于可偏向状态,是一种特殊的无锁状态。
我们将程序稍加改动:
public class A {
boolean flag = false;
/**
* jvm参数:-XX:BiasedLockingStartupDelay=0
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
A a = new A();
//查看jvm是大端模式还是小端模式,打印结果:LITTLE_ENDIAN
System.out.println(ByteOrder.nativeOrder());
synchronized (a) {
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
}
此时输出结果为:
LITTLE_ENDIAN
com.crossoverjie.concurrent.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0d f8 22 00 (00001101 11111000 00100010 00000000) (2291725)
4 4 (object header) 28 19 04 15 (00101000 00011001 00000100 00010101) (352590120)
8 1 boolean A.flag false
9 7 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
**解析:**加入synchronized关键字,且加入不延迟启动偏向锁的jvm参数后,我们可以发现在mark word中包含了具体的偏向线程Id.
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID即可,处理流程如下:
1.检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
2.若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
3.如果测试线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);
4.通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
5.执行同步代码块
偏向锁的释放采用了 一[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nifKbO2S-1570523174649)(C:\Users\Administrator\Desktop\9.23日技术分享\偏向锁的撤销流程.jpg)]种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争,一旦出现竞争则由偏向锁就升级为轻量锁
1.2.5 在不启用偏向锁的前提下,默认一开始使用的就是轻量锁
/**
* 在不启用偏向锁的情况下,默认一开始就是轻量锁
*/
@Test
public void testLightWeightLock(){
A a = new A();
synchronized (a) {
System.out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
返回结果:
com.crossoverjie.concurrent.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) c4 ee 30 02 (11000100 11101110 00110000 00000010) (36761284)
4 4 (object header) 40 ca 05 15 (01000000 11001010 00000101 00010101) (352700992)
8 1 boolean A.flag false
9 7 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
最后两位为00(剩余30位为指向栈中锁记录的指针),可以看出,在不启用偏向锁的前提下,当使用sychronized关键字对对象进行加锁时默认就使用了轻量锁。
public void testHeightWeightLock() throws InterruptedException {
LockTest lockTest = new LockTest();
Thread thread1 = new Thread(){
@Override
public void run() {
synchronized (lockTest){
long start = System.currentTimeMillis();
System.out.println("thread1 locking1");
System.out.println(ClassLayout.parseInstance(lockTest).toPrintable());
//打印对象头耗时约2.5s
System.out.println(String.format("cost=%s",System.currentTimeMillis()-start));
try {
//thread1退出同步代码块,且没有死亡
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1 locking2");
System.out.println(ClassLayout.parseInstance(lockTest).toPrintable());
System.out.println(String.format("thread1 end,cost=%s...........",System.currentTimeMillis()-start));
}
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
synchronized (lockTest){
System.out.println("thread2 locking");
System.out.println(ClassLayout.parseInstance(lockTest).toPrintable());
}
//再次进入
synchronized (lockTest){
System.out.println("thread2 locking2");
System.out.println(ClassLayout.parseInstance(lockTest).toPrintable());
}
}
};
thread1.start();
//让thread1执行完同步代码块中方法,线程2启动时,线程1还没有执行完
Thread.sleep(3000);
thread2.start();
thread1.join();
thread2.join();
}
打印结果:
thread1 locking1
com.crossoverjie.concurrent.LockTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 4c fb 13 16 (01001100 11111011 00010011 00010110) (370408268)
4 4 (object header) 48 ca 05 15 (01001000 11001010 00000101 00010101) (352701000)
8 1 boolean LockTest.flag false
9 7 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
cost=1989
thread1 locking2
com.crossoverjie.concurrent.LockTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) ae dd fd 01 (10101110 11011101 11111101 00000001) (33414574)
4 4 (object header) 48 ca 05 15 (01001000 11001010 00000101 00010101) (352701000)
8 1 boolean LockTest.flag false
9 7 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
thread1 end,cost=3993...........
thread2 locking
com.crossoverjie.concurrent.LockTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) ae dd fd 01 (10101110 11011101 11111101 00000001) (33414574)
4 4 (object header) 48 ca 05 15 (01001000 11001010 00000101 00010101) (352701000)
8 1 boolean LockTest.flag false
9 7 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
thread2 locking2
com.crossoverjie.concurrent.LockTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) ae dd fd 01 (10101110 11011101 11111101 00000001) (33414574)
4 4 (object header) 48 ca 05 15 (01001000 11001010 00000101 00010101) (352701000)
8 1 boolean LockTest.flag false
9 7 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
从轻量锁到重量锁膨胀分为两种情况:
(1)两个同时获取a实例的markword(很难模拟两个线程在无锁状态时同时获取到a实例的markword),然后同时cas时,一个成功,其他失败然后自旋超过次数,会升级 (2)后来的获取者到达时,发现a实例已经是轻量锁状态,则直接升级
从打印的对象头的信息,我们可以看出,线程1最开始是轻量量锁(10)。线程2在线程1启动后,CAS尝试获取锁,发现失败(锁已经被线程1获取到),且锁当前已经是轻量锁。于是将锁膨胀为重量锁【为上述说明的第二种情况】(此时线程1仍持有锁)。当线程1释放锁后线程2得到的锁是重量锁,且线程2再次获取时,锁不会降级(仍然为竞争时膨胀后的重量锁)。
我们再在threa2.join后再加一个打印对象头,发现对象头中的锁又降为了轻量锁,思考下为什么?
synchronized (lockTest){
System.out.println("main thread locking");
System.out.println(ClassLayout.parseInstance(lockTest).toPrintable());
}
打印结果:
main thread locking
com.crossoverjie.concurrent.LockTest object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) f6 4a 17 02 (11110110 01001010 00010111 00000010) (35080950)
4 4 (object header) c0 ca 4d 14 (11000000 11001010 01001101 00010100) (340642496)
8 1 boolean LockTest.flag false
9 7 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
完整的为:
@Test
public void testHeightWeightLock() throws InterruptedException {
LockTest lockTest = new LockTest();
Thread thread1 = new Thread(){
@Override
public void run() {
synchronized (lockTest){
long start = System.currentTimeMillis();
System.out.println("thread1 locking1");
System.out.println(ClassLayout.parseInstance(lockTest).toPrintable());
//打印对象头耗时约2.5s
System.out.println(String.format("cost=%s",System.currentTimeMillis()-start));
try {
//thread1退出同步代码块,且没有死亡
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1 locking2");
System.out.println(ClassLayout.parseInstance(lockTest).toPrintable());
System.out.println(String.format("thread1 end,cost=%s...........",System.currentTimeMillis()-start));
}
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
synchronized (lockTest){
System.out.println("thread2 locking");
System.out.println(ClassLayout.parseInstance(lockTest).toPrintable());
}
//再次进入
synchronized (lockTest){
System.out.println("thread2 locking2");
System.out.println(ClassLayout.parseInstance(lockTest).toPrintable());
}
}
};
thread1.start();
//让thread1执行完同步代码块中方法,线程2启动时,线程1还没有执行完
Thread.sleep(3000);
thread2.start();
thread1.join();
thread2.join();
synchronized (lockTest){
System.out.println("main thread locking");
System.out.println(ClassLayout.parseInstance(lockTest).toPrintable());
}
}
轻量锁到重量锁膨胀的流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-syxHkLsV-1570523174651)(C:\Users\Administrator\Desktop\9.23日技术分享\轻量锁到重量锁膨胀的流程.jpg)]
重量锁:思想类似于GC算法中的引用计数法,每个对象都是监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权。monitorenter时候,计数器加1,monitorexit时候,计数器减1.当计数器为0时候,意味着释放了锁。
eg:
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("Method 1 start");
}
}
}
编译后,javap -v SynchronizedDemo.class 后的对应字节码:
public void method();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String Method 1 start
9: invokevirtual #4 // Method java/io/PrintStream.println:
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
使用加锁对象的对象头中的lock word(指向栈中锁记录的指针)与本线程的栈的地址进行比较,如果相同则说明当前线程已经获取锁。
使用对象头中的ThreadId与当前线程的ThreadId进行对比,如果相同,则说明当前线程已经获取锁。
/**
* -XX:BiasedLockingStartupDelay=0
从fa的最后两位10可知为重量锁, 前面为重量锁的指针
升级为重量锁两种情况
(1)两个同时获取a实例的markword(很难模拟两个线程在无锁状态时同时获取到a实例的markword),然后同时cas时,一个成功,其他失败然后自旋超过次数,会升级
(2)后来的获取者到达时,发现a实例已经是轻量锁状态,则直接升级
这里打印重量锁的原因应该是后者,
* @throws InterruptedException
*/
@Test
public void testHeightWeightLock() throws InterruptedException {
LockTest lockTest = new LockTest();
Thread thread1 = new Thread(){
@Override
public void run() {
synchronized (lockTest){
long start = System.currentTimeMillis();
System.out.println("thread1 locking");
System.out.println(ClassLayout.parseInstance(lockTest).toPrintable());
//打印对象头耗时约2.5s
System.out.println(String.format("cost=%s",System.currentTimeMillis()-start));
try {
//thread1退出同步代码块,且没有死亡
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(String.format("thread1 end,cost=%s...........",System.currentTimeMillis()-start));
}
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
synchronized (lockTest){
System.out.println("thread2 locking");
System.out.println(ClassLayout.parseInstance(lockTest).toPrintable());
}
//再次进入
synchronized (lockTest){
System.out.println("thread2 locking2");
System.out.println(ClassLayout.parseInstance(lockTest).toPrintable());
}
}
};
thread1.start();
//让thread1执行完同步代码块中方法,线程2启动时,线程1还没有执行完
Thread.sleep(3000);
thread2.start();
thread1.join();
thread2.join();
}
定义: 编译器在运行时,对对于一些在代码上要求同步,但是检测到不可能存在数据竞争的锁进行消除。eg:
public class Erase {
public void testErase(){
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
}
}
StringBuffer.append方法前加入了synchronized关键字,但是编译后的字节码没有mointorenter以及monitorexit:
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
字节码:
public void testErase();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class java/lang/StringBuffer
3: dup
4: invokespecial #3 // Method java/lang/StringBuffer."":()V
7: astore_1
8: aload_1
9: ldc #4 // String a
11: invokevirtual #5 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
14: pop
15: aload_1
16: ldc #6 // String b
18: invokevirtual #5 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
21: pop
22: aload_1
23: ldc #7 // String c
25: invokevirtual #5 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
28: pop
29: return
在编写程序代码时候,总是推荐将同步块作用范围变得尽可能小,只有在共享数据的实际作用域才进行同步。但是如果一个连续的操作频繁的对同一个对象进行加锁和解锁,即便没有现成竞争也会产生不必要的损耗。jvm会将加锁同步范围拓展,称为锁粗化
eg:
for(int i=0;i<size;i++){
synchronized(lock){
}
粗化后示例:
synchronized(lock){
for(int i=0;i<size;i++){
}
}
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。
所以引入自旋锁,何谓自旋锁?
所谓自旋锁,就是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。
自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。自旋等待不能替代阻塞,虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。
如果通过参数-XX:PreBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如多自旋一两次就可以获取锁),是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。那它如何进行适应性自旋呢?
线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。
上的浪费。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。
如果通过参数-XX:PreBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如多自旋一两次就可以获取锁),是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。那它如何进行适应性自旋呢?
线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。