作者:小傅哥
博客:https://bugstack.cn
沉淀、分享、成长,让自己和他人都能有所收获!????
感觉什么都不会,从哪开始呀!
这是最近我总能被问到的问题,也确实是。一个初入编程职场的新人,或是一个想重新努力学习的老司机,这也不会,那也不会,总会犯愁从哪开始。
讲道理,毕竟 Java 涉及的知识太多了,要学应该是学会学习的能力,而不是去背题、背答案,拾人牙慧是不会有太多收益的。
学习的过程要找对方法,遇到问题时最好能自己想想,你有哪些方式学会这些知识。是不感觉即使让你去百度搜,你都不知道应该拿哪个关键字搜!只能拿着问题直接找人问,这样缺少思考,缺少大脑撞南墙的过程,其实最后也很难学会。
所以,你要学会的是自我学习的能力,之后是从哪开始都可以,重要的是开始和坚持!
谢飞机,小记
,周末逛完奥特莱斯,回来就跑面试官家去了!
谢飞机:duang、duang、duang,我来了!
面试官:来的还挺准时,洗洗手吃饭吧!
谢飞机:嘿嘿…
面试官:你看我这块鱼豆腐,像不像 synchronized 锁!
谢飞机:啊!?
面试官:飞机,正好问你。synchronized、volatile,有什么区别呀?
谢飞机:嗯,volatile 保证可见性,synchronized 保证原子性!
面试官:那不用 volatile,只用 synchronized 修饰方式,能保证可见性吗?
谢飞机:这…,我没验证过!
面试官:吃吧,吃吧!一会给你个 synchronized 学习大纲,照着整理知识点!
HotSpot虚拟机 markOop.cpp 中的 C++ 代码注释片段,描述了 64bits 下 mark-word 的存储状态,也就是图 15-1 的结构示意。
这部分的源码注释如下:
64 bits:--------unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)size:64 ----------------------------------------------------->| (CMS free block)unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
源码地址:jdk8/hotspot/file/vm/oops/markOop.hpp
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)
、实例数据(Instance Data)
和对齐填充(Padding)
。
-XX:-UseCompressedOops
)后,长度为8字节。其指向的位置是对象对应的Class对象(其对应的元数据对象)的内存地址。另外,在mark-word锁类型标记中,无锁,偏向锁,轻量锁,重量锁,以及GC标记,5种类中没法用2比特标记(2比特最终有4种组合00
、01
、10
、11
),所以无锁、偏向锁,前又占了一位偏向锁标记。最终:001为无锁、101为偏向锁。
为了可以更加直观的看到对象结构,我们可以借助 openjdk
提供的 jol-core
进行打印分析。
引入POM
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-cli --><dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-cli</artifactId> <version>0.14</version></dependency>
测试代码
public static void main(String[] args) { System.out.println(VM.current().details()); Object obj = new Object(); System.out.println(obj + " 十六进制哈希:" + Integer.toHexString(obj.hashCode())); System.out.println(ClassLayout.parseInstance(obj).toPrintable());}
运行结果
# Running 64-bit HotSpot VM.# Using compressed oop with 3-bit shift.# Using compressed klass with 3-bit shift.# Objects are 8 bytes aligned.# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) 12 4 (loss due to the next object alignment)Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
在 Run-->Edit Configurations->VM Options
配置参数 -XX:-UseCompressedOops
关闭指针压缩。
运行结果
java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 12 0c 53 (00000001 00010010 00001100 01010011) (1393299969) 4 4 (object header) 02 00 00 00 (00000010 00000000 00000000 00000000) (2) 8 4 (object header) 00 1c b9 1b (00000000 00011100 10111001 00011011) (465116160) 12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
接下来,我们调整下测试代码,看下哈希值在对象头中具体是怎么存放的。
测试代码
public static void main(String[] args) { System.out.println(VM.current().details()); Object obj = new Object(); System.out.println(obj + " 十六进制哈希:" + Integer.toHexString(obj.hashCode())); System.out.println(ClassLayout.parseInstance(obj).toPrintable());}
运行结果
0x2530c12
关于这个倒过来的问题是因为,大小端存储导致;
mark-word结构
如图 15-5 最右侧的 3 Bit(1 Bit标识偏向锁,2 Bit描述锁的类型)是跟锁类型和GC标记相关的,而 synchronized 的锁优化升级膨胀就是修改的这三位上的标识,来区分不同的锁类型。从而采取不同的策略来提升性能。
在HotSpot虚拟机中,monitor是由C++中ObjectMonitor实现。
synchronized 的运行机制,就是当 JVM 监测到对象在不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
那么三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁。当一个 Monitor 被某个线程持有后,它便处于锁定状态。
Monitor 主要数据结构如下:
// initialize the monitor, exception the semaphore, all other fields// are simple integers or pointersObjectMonitor() { _header = NULL; _count = 0; // 记录个数 _waiters = 0, _recursions = 0; // 线程重入次数 _object = NULL; // 存储 Monitor 对象 _owner = NULL; // 持有当前线程的 owner _WaitSet = NULL; // 处于wait状态的线程,会被加入到 _WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; // 单向列表 FreeNext = NULL ; _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0;}
源码地址:jdk8/hotspot/file/vm/runtime/objectMonitor.hpp
_WaitSet
、_EntryList
,用来保存 ObjectWaiter 对象列表。锁????执行效果如下:
如图 15-06,每个 Java 对象头中都包括 Monitor 对象(存储的指针的指向),synchronized 也就是通过这一种方式获取锁,也就解释了为什么 synchronized() 括号里放任何对象都能获得锁????!
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败。
案例代码
private static volatile int counter = 0;public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { Thread thread = new Thread(() -> { for (int i1 = 0; i1 < 10000; i1++) { add(); } }); thread.start(); } // 等10个线程运行完毕 Thread.sleep(1000); System.out.println(counter);}public static void add() { counter++;}
这段代码开启了 10 个线程来累加 counter,按照预期结果应该是 100000。但实际运行会发现,counter 值每次运行都小于 10000,这是因为 volatile 并不能保证原子性,所以最后的结果不会是10000。
修改方法 add(),添加 synchronized:
public static void add() { synchronized (AtomicityTest.class) { counter++; }}
这回测试结果就是:100000 了!
因为 synchronized 可以保证统一时间只有一个线程能拿到锁,进入到代码块执行。
反编译查看指令码
javap -v -p AtomicityTest
public static void add(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=0 0: ldc #12 // class org/itstack/interview/AtomicityTest 2: dup 3: astore_0 4: monitorenter 5: getstatic #10 // Field counter:I 8: iconst_1 9: iadd 10: putstatic #10 // Field counter:I 13: aload_0 14: monitorexit 15: goto 23 18: astore_1 19: aload_0 20: monitorexit 21: aload_1 22: athrow 23: return Exception table:
同步方法
ACC_SYNCHRONIZED
这是一个同步标识,对应的16进制值是 0x0020
这10个线程进入这个方法时,都会判断是否有此标识,然后开始竞争 Monitor 对象。
同步代码
monitorenter
,在判断拥有同步标识 ACC_SYNCHRONIZED
抢先进入此方法的线程会优先拥有 Monitor 的 owner ,此时计数器 +1。monitorexit
,当执行完退出后,计数器 -1,归 0 后被其他进入的线程获得。在上一章节 volatile 篇中,我们知道它保证变量对所有线程的可见性。最终的效果就是在添加 volatile 的属性变量时,线程A修改值后,线程B使用此变量可以做出相应的反应,比如 while(!变量)
退出。
那么,synchronized
具备可见性吗,我们做给例子。
public static boolean sign = false;public static void main(String[] args) { Thread Thread01 = new Thread(() -> { int i = 0; while (!sign) { i++; add(i); } }); Thread Thread02 = new Thread(() -> { try { Thread.sleep(3000); } catch (InterruptedException ignore) { } sign = true; logger.info("vt.sign = true while (!sign)") }); Thread01.start(); Thread02.start();}public static int add(int i) { return i + 1;}
这是两个线程操作一个变量的例子,因为线程间对变量 sign
的不可见性,线程 Thread01 中的 while (!sign) 会一直执行,不会随着线程 Thread02 修改 sign = true 而退出循环。
现在我们给方法 add 添加 synchronized
关键字修饰,如下:
public static synchronized int add(int i) { return i + 1;}
添加后运行结果:
23:55:33.849 [Thread-1] INFO org.itstack.interview.VisibilityTest - vt.sign = true while (!sign)Process finished with exit code 0
可以看到当线程 Thread02 改变变量 sign = true 后,线程 Thread01 立即退出了循环。
注意:不要在方法中添加 System.out.println() ,因为这个方法中含有 synchronized 会影响测试结果!
那么为什么添加 synchronized 也能保证变量的可见性呢?
因为:
as-if-serial
,保证不管编译器和处理器为了性能优化会如何进行指令重排序,都需要保证单线程下的运行结果的正确性。也就是常说的:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的。
这里有一段双重检验锁(Double-checked Locking)的经典案例:
public class Singleton { private Singleton() { } private volatile static Singleton instance; public Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }}
为什么,synchronized 也有可见性的特点,还需要 volatile 关键字?
因为,synchronized 的有序性,不是 volatile 的防止指令重排序。
那如果不加 volatile 关键字可能导致的结果,就是第一个线程在初始化初始化对象,设置 instance 指向内存地址时。第二个线程进入时,有指令重排。在判断 if (instance == null) 时就会有出错的可能,因为这会可能 instance 可能还没有初始化成功。
synchronized 是可重入锁,也就是说,允许一个线程二次请求自己持有对象锁的临界资源,这种情况称为可重入锁????。
那么我们就写一个例子,来证明这样的情况。
public class ReentryTest extends A{ public static void main(String[] args) { ReentryTest reentry = new ReentryTest(); reentry.doA(); } public synchronized void doA() { System.out.println("子类方法:ReentryTest.doA() ThreadId:" + Thread.currentThread().getId()); doB(); } private synchronized void doB() { super.doA(); System.out.println("子类方法:ReentryTest.doB() ThreadId:" + Thread.currentThread().getId()); }}class A { public synchronized void doA() { System.out.println("父类方法:A.doA() ThreadId:" + Thread.currentThread().getId()); }}
测试结果
子类方法:ReentryTest.doA() ThreadId:1父类方法:A.doA() ThreadId:1子类方法:ReentryTest.doB() ThreadId:1Process finished with exit code 0
这段单例代码是递归调用含有 synchronized 锁的方法,从运行正常的测试结果看,并没有发生死锁。所有可以证明 synchronized 是可重入锁。
synchronized锁对象的时候有个计数器,他会记录下线程获取锁的次数,在执行完对应的代码块之后,计数器就会-1,直到计数器清零,就释放锁了。
之所以,是可以重入。是因为 synchronized 锁对象有个计数器,会随着线程获取锁后 +1 计数,当线程执行完毕后 -1,直到清零释放锁。
关于 synchronized 锁????升级有一张非常完整的图,可以参考:
synchronized 锁有四种交替升级的状态:无锁、偏向锁、轻量级锁和重量级,这几个状态随着竞争情况逐渐升级。
synchronizer源码:/src/share/vm/runtime/synchronizer.cpp
// NOTE: must use heavy weight monitor to handle jni monitor exitvoid ObjectSynchronizer::jni_exit(oop obj, Thread* THREAD) { TEVENT (jni_exit) ; if (UseBiasedLocking) { Handle h_obj(THREAD, obj); BiasedLocking::revoke_and_rebias(h_obj, false, THREAD); obj = h_obj(); } assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj); // If this thread has locked the object, exit the monitor. Note: can't use // monitor->check(CHECK); must exit even if an exception is pending. if (monitor->check(THREAD)) { monitor->exit(true, THREAD); }}
XX:-UseBiasedLocking=false
偏斜锁会延缓 JIT 预热进程,所以很多性能测试中会显式地关闭偏斜锁,偏斜锁并不适合所有应用场景,撤销操作(revoke)是比较重的行为,只有当存在较多不会真正竞争的 synchronized 块儿时,才能体现出明显改善。
当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),JVM虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
自旋锁的默认大小是10次,可以调整:-XX:PreBlockSpin
如果自旋n次失败了,就会升级为重量级的锁。重量级的锁,在 1.3 Monitor 对象中已经介绍。
之前一直了解到 Java 不会进行锁降级,但最近整理了大量的资料发现锁降级确实是会发生。
When safepoints are used?
Below are few reasons for HotSpot JVM to initiate a safepoint:Garbage collection pauses Code deoptimization Flushing code cache Class redefinition (e.g. hot swap or instrumentation)Biased lock revocation Various debug operation (e.g. deadlock check or stacktrace dump)
Biased lock revocation
,当 JVM 进入安全点 SafePoint的时候,会检查是否有闲置的 Monitor,然后试图进行降级。
synchronized
锁涉及到了较多的C++源码分析学习,源码地址:https://github.com/JetBrains/jdk8u_hotspot