☆* o(≧▽≦)o *☆嗨~我是小奥
个人博客:小奥的博客
CSDN:个人CSDN
Github:传送门
面经分享(牛客主页):传送门
文章作者技术和水平有限,如果文中出现错误,希望大家多多指正!
如果觉得内容还不错,欢迎点赞收藏关注哟! ❤️
synchronized
是Java中的关键字,是一种同步锁,他修饰的对象有以下几种:
无论synchronized关键字加在方法上还是对象上,
实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免不必要的同步控制。
在JVM中,对象在内存中存储的布局可以分为三个区域,分别是对象头、示例数据以及填充数据。
而Java对象头则是实现synchronized的锁对象的基础。
synchronized
用的锁是存在 Java 对象头里的。
在 32 位虚拟机中,1 字宽等于 4 字节,即 32bit.
Monitor的结构
Java 对象头里的 Mark Word 里默认存储对象的 HashCode、分代年龄和锁标记位。另外,数组还会有长度的标识。
在运行期间,Mark Word里存储的数据会随着锁的标志位的变化而变化。
1、开始时Monitor
中的owner
为null
2、当线程1执行synchronized(obj)
时就会将Monitor
的所有者owner
置为线程1,Monitor
中只能有1个Owner
,obj对象中的Mark Word
指向Monitor
,把对象原有的Mark Word
存储线程栈中的锁记录中。
3、线程1上锁的过程中,如果有其他线程来执行synchronized(obj)
,就会进入EntryList BLOCKED
。
4、线程1执行完同步代码块的内容,根据 obj
对象头中 Monitor
地址寻找,设置 Owner
为空,把线程栈的锁记录中的对象头的值设置回 MarkWord
。
5、唤醒 EntryList
中等待的线程来竞争锁,竞争是非公平的,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞。
(4)字节码层面分析
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
0 new #2 // new Object
3 dup
4 invokespecial #1 : ()V> // invokespecial :()V,非虚方法
7 astore_1 // lock引用 -> lock
8 aload_1 // lock (synchronized开始)
9 dup // 一份用来初始化,一份用来引用
10 astore_2 // lock引用 -> slot 2
11 monitorenter // 【将 lock对象 MarkWord 置为 Monitor 指针】
12 getstatic #3
15 ldc #4
17 invokevirtual #5
20 aload_2 // slot 2(lock引用)
21 monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
22 goto 30 (+8)
25 astore_3 // any -> slot 3
26 aload_2 // slot 2(lock引用)
27 monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
28 aload_3
29 athrow
30 return
实现原理:同步代码块通过moniterenter
、moniterexit
关联到一个monitor
对象,进入时设置Owner
为当前线程,计数+1,退出-1,除了正常入口的moniterenter
,还在异常入口的地方加入了moniterexit
指令。
synchronized 是可重入、不公平的重量级锁,所以可以对其进行优化。
在 Java SE 1.6 中,锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。
锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 // 随着竞争的增加,只能锁升级,不能降级
解决问题:大多数情况下,**线程不仅不存在多线程竞争,而且锁总是由同一线程多次获得,在没有其他线程竞争锁时,线程每次重入锁仍然需要进行CAS操作,造成性能的损耗。**为了让多线程获得锁的代价更低而引入了偏向锁。
偏向锁加锁:
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作:
偏向锁撤销:
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。
偏向锁关闭:
// 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数
-XX:BiasedLockingStartupDelay=0 // 关闭偏向锁的延迟
-XX:-UseBiasedLoacking=false // 关闭偏向锁
JDK8延迟4每秒开启偏向锁的原因:当程序刚开始执行时,会有很多的线程来争抢锁,如果开启偏向锁效率反而会降低。
撤销偏向锁的场景
批量撤销和批量重偏向:
从偏向锁的加锁解锁过程中就可以看出,当只有一个线程反复获取锁的是皇后,偏向锁带来的性能开销可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。
批量撤销(解决场景):在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。
批量重偏向(解决场景):一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。
原理:以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。
每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。
当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。
加锁:线程在执行同步块之前,JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中,官方称为 Displaced Mark Word
。然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
解锁:轻量级解锁时,会使用原子的 CAS 操作将 Displaced Mark Word
替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
EntryList BLOCKED
EntryList
中 BLOCKED
线程。重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
当一个线程尝试获取锁(重量级锁)时,如果锁已经被其他线程持有,该线程会进入自旋状态,不断地重试获取锁,直到获取到为止。自旋锁避免了线程切换带来的开销,但是如果锁被持有的时间较长,自旋锁可能会导致CPU资源的浪费。
锁消除是一种编译器优化技术,编译器可以在编译过程中分析代码,并根据程序的特性来消除一些不必要的锁操作。
比如在单线程程序中使用锁,锁会变成多余的开销,编译器可以消除这些锁操作,从而提高程序的性能。
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除(同步消除:JVM 逃逸分析)
锁粗化是一种优化技术,可以将多个连续的锁操作合并成一个锁操作。例如,如果程序中存在多个连续的对同一个锁的加锁和解锁操作,锁粗化可以将它们合并为一个锁操作,从而减少锁的竞争和开销,提高程序的性能。