1、synchronized
1.1 synchronized的解释
synchronized(锁对象)
{
// 临界区
}
synchronized:对象锁,保证了临界区内代码的原子性,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程获取这个对象锁时会阻塞,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
互斥和同步都可以采用 synchronized 关键字来完成,区别:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
性能:
- 线程安全,性能差
- 线程不安全性能好,假如开发中不会存在多线程安全问题,建议使用线程不安全的设计类
1.2 synchronized的用法
- 同步代码块+对象锁
IncreaseAndDecrease inde = new IncreaseAndDecrease();
private static int counter = 0;
public void increase(){
synchronized (inde){
counter++;
}
}
- 同步方法+对象锁
private static int counter = 0;
public synchronized void increase(){
counter++;
}
// 等价于:
public void increase(){
synchronized(this){
counter++;
}
}
- 同步静态方法+对象锁
private static int counter = 0;
public static synchronized void increase(){
counter++;
}
// 等价于:
public void increase(){
synchronized(当前类.class){
counter++;
}
}
用法小总结
- 非静态方法的默认锁为 this,静态方法的锁默认为Class实例。
- 在某一个时刻内,之能有一个线程持有锁,无论几个方法。
- 更多案例可以参考:线程八锁
1.3 线程安全分析
分析程序是否有线程安全问题,主要就是看是否有多个线程共享同一个资源变量。
例子1:下面的例子中,两个线程操作的都是局部变量,不存在共享问题,所以没有线程安全问题。
package chapter02;
import java.util.ArrayList;
public class ThreadSafeAnalysis {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
ThreadSafe threadSafe = new ThreadSafe();
threadSafe.method01(200);
}
},"Thread 01").start();
new Thread(new Runnable() {
@Override
public void run() {
ThreadSafe threadSafe = new ThreadSafe();
threadSafe.method01(200);
}
},"Thread 02").start();
}
}
class ThreadSafe{
public void method01(int loopnumber){
// list 为局部变量故而,每个线程调用method01方法都会创建一个list对象
// 所以不会有线程安全问题
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopnumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list){
list.add("1");
}
public void method3(ArrayList<String> list){
list.remove(0);
}
}
例子2:将上面例子中的 method3
修改如下。这样就会出现两个线程同时访问 list(【Thread 01 与 Thread 03】或者【Thread 02 与 Thread 03】)。这样就会出现线程安全问题。
public void method3(ArrayList<String> list){
new Thread(()->{
list.remove(0);
},"Thread 03");
}
线程安全的设计建议
- 设计程序时,不该暴露给外部的方法一定要私有化,防止子类重写。
- 暴露给外部的方法,考虑是否需要final,防止子类重写。
- 当前设计的类,是否需要被继承,如果不需要可直接final修饰来保证线程安全。
1.4 常见的线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- JUC包下的类
2、对象头与锁(Monitor)
JVM中对象头的方式有以下两种(以32位JVM为例):
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
Klass Word:指向对象所属的类对象。
# 数组对象
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
2.1 对象头的组成
2.1.1 Mark Word
这部分主要用来存储对象自身的运行时数据,如 hashcode、gc
分代年龄等。mark word
的位长度为 JVM 的一个 Word 大小,也就是说32位JVM的 Mark word 为32位,64位JVM为64位。为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
- biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
- age:4位的 Java 对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是
-XX:MaxTenuringThreshold
选项最大值为15的原因。 - identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法
System.identityHashCode()
计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor
中。 - thread:持有偏向锁的线程ID。
- epoch:偏向时间戳。
- ptr_to_lock_record:指向栈中锁记录的指针。
- ptr_to_heavyweight_monitor:指向管程Monitor的指针。
2.1.2 从Java对象看 Mark Word
/*
情况一:调用hashcode
10进制的Hash码:1554874502
16进制的Hash码:5c ad 80 86
2进制的Hash码:1011100101011011000000010000110
01011100101011011000000010000110
0 4 (object header) 01 86 80 ad (00000001 10000110 10000000 10101101) (-1384086015)
4 4 (object header) 5c 00 00 00 (01011100 00000000 00000000 00000000) (92)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
情况二:不调用HashCode
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)
*/
public static void main(String[] args) {
Object o = new Object();
// System.out.println("10进制的Hash码:"+o.hashCode());
// System.out.println("16进制的Hash码:"+Integer.toHexString(o.hashCode()));
// System.out.println("2进制的Hash码:"+Integer.toBinaryString(o.hashCode()));
System.out.println("无锁状态的对象头:");
System.out.println(ClassLayou t.parseInstance(o).toPrintable());
}
情况二:不调用HashCode
第一行 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)
首先从第二行的最后一个字节开始,往前逐个字节的排列,可以得到如下二进制序列:
00000000 00000000 00000000 0 0000000 00000000 00000000 00000000 0 0000 0 01
|---------unused(25位)----| |------hashcode(31位)------------| |-unused(1位)-| |-age-| |-偏向锁位置-| |-锁标志位置-|
- 情况一:调用hashcode方法
10进制的Hash码:1554874502
16进制的Hash码:5c ad 80 86
2进制的Hash码:1011100101011011000000010000110
1011100101011011000000010000110
0 4 (object header) 01 86 80 ad (00000001 10000110 10000000 10101101) (-1384086015)
4 4 (object header) 5c 00 00 00 (01011100 00000000 00000000 00000000) (92)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
首先从第二行的最后一个字节开始,往前逐个字节的排列,可以得到如下二进制序列:
00000000 00000000 00000000 01011100 10101101 10000000 10000110 00000001
对其进行划分:
00000000 00000000 00000000 0 1011100 10101101 10000000 10000110 0 0000 0 01
|---------unused(25位)----| |------hashcode(31位)------------| |-unused(1位)-| |-age-| |-偏向锁位置-| |-锁标志位置-|
加锁之后的markword有什么变化,在2.3节进行分析。
2.2 Monitor
Monitor 被翻译为监视器或管程。在HotSpot虚拟机中,Monitor是基于C++的ObjectMonitor
类实现的,其主要成员包括:
- _owner:指向持有ObjectMonitor对象的线程
- _WaitSet:存放处于wait状态的线程队列,即调用wait()方法的线程
- _EntryList:存放处于等待锁block状态的线程队列
- _count:约为_WaitSet 和 _EntryList 的节点数之和
- _cxq: 多个线程争抢锁,会先存入这个单向链表
- _recursions: 记录重入次数
每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例存储在堆中,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁。
2.2.1 ObjectMonitor的工作流程
- 当多个线程同时访问一段同步代码时,首先会进入
_EntryList
队列中。 - 当某个线程获取到对象的 Monitor 后进入临界区域,并把 Monitor 中的
_owner
变量指向同步对象,同时Monitor中的计数器 _count 加1。即获得对象锁。 - 若持有Monitor的线程调用 wait() 方法,将释放当前持有的Monitor,_owner变量恢复为null,_count自减1,同时该线程进入 _WaitSet 集合中等待被唤醒。
- 在_WaitSet 集合中的线程会被再次放到_EntryList 队列中,重新竞争获取锁。
- 若当前线程执行完毕也将释放Monitor并复位变量的值,以便其他线程进入获取锁。
注意:
- synchronized 必须是进入同一个对象的 Monitor 才有上述的效
- 不加 synchronized 的对象不会关联监视器,不遵从以上规则
2.2.2 从字节码的角度理解加锁原理
- 情况一:同步代码块
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 // 初始化Object对象
7: astore_1 // 将lock引用存入本地变量表1的位置
8: aload_1 // 加载lock (synchronized开始)
9: dup // 一份用来初始化,一份用来引用
10: astore_2 // 将复制的lock对象存入本地变量表2的位置
11: monitorenter // 【将 lock对象 MarkWord 置为 Monitor 指针】
12: getstatic #3 // System.out
15: ldc #4 // "ok"
17: invokevirtual #5 // invokevirtual println:(Ljava/lang/String;)V
20: aload_2 // slot 2(lock引用)
21: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
22: goto 30
// 下面是同步代码块出现异常后的操作
25: astore_3 //异常对象存入本地变量表3的位置 any -> slot 3
26: aload_2 // slot 2(lock引用)
27: monitorexit // 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
28: aload_3 // 加载异常
29: athrow // 将异常抛出
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 lock Ljava/lang/Object;
解释:
1 通过异常 try-catch 机制,确保一定会被解锁
2 方法级别的 synchronized 不会在字节码指令中有所体现
- 情况二:synchronized 普通同步方法
调用指令将会检查方法的ACC_SYNCHRONIZED
访问标志是否被设置,如果设置了,执行线程会将现持有monitor锁,然后再执行该方法,最后在方法完成(无论是否正常结束)时释放monitor。
code:
public synchronized void test2(){
System.out.println("分析非静态方法加锁后的字节码信息");
}
Bytecode:
public synchronized void test2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String 分析非静态方法加锁后的字节码信息
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 13: 0
line 14: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lchapter02/ByteCodeAnalysis;
- 情况三:synchronized 静态同步方法
ACC_STATIC、ACC_SYNCHRONIZED
访问标志区分该方法是否是静态同步方法。
public static synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
....
2.2.3 为什么每个对象都可以作为一个锁?
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
-
在
JVM
中每个Java对象都会对应一个C++实现的ObjectMonitor
。 -
当对程序进行加锁时,
lock
对象的对象头中的markword
被设置为指向ObjectMonitor
的指针(重量锁的情况,其他锁有些许区别)。 -
当线程抢到锁后,
ObjectMonitor
对象中的owner
属性会被设置为当前线程对象,代表当前线程已经持有锁。ObjectMonitor
对象会被锁定,其他线程无法获得锁。 -
当前线程释放锁,
owner
设置为Null
,并唤醒其他线程进行争枪锁。
2.3 synchronized 的升级过程
在 Java 早期版本中,synchronized
属于重量级锁,效率低下。为什么呢?因为监视器锁(monitor)是依赖于底层的操作系统来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized
较大优化,所以现在的 synchronized
锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
无锁(CAS) -> 偏向锁 -> 轻量级锁 -> 重量级锁 // 随着竞争的增加,只能锁升级,不能降级
Synchronized
用的锁是存在Java
对象头里的MarkWord
中,锁升级功能主要依赖MarkWord
中锁标志位和释放偏向锁标志位。
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| threadID:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
- 偏向锁(Biased):
MarkWord
存储的是偏向的线程ID - 轻量锁:
MarkWord
存储的是指向线程栈中Lock Record
的指针 - 重量锁:
MarkWord
存储的是指向堆中的monitor
对象(系统互斥量指针)
2.3.0 无锁(001)
参见:2.1.2 从Java对象看 Mark Word
2.3.1 偏向锁(101)
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作:
- 当锁对象第一次被线程获得的时候进入偏向状态,标记为 101,同时使用 CAS 操作将线程 ID 记录到 Mark Word。如果 CAS 操作成功,这个线程以后进入这个锁相关的同步块,查看这个线程 ID 是自己的就表示没有竞争,就不需要再进行任何同步操作
- 当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定或轻量级锁状态
偏向锁:单线程竞争,当线程A第一次竞争到锁时,通过修改MarkWord
中的偏向线程ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。
理论落地:
技术实现:
有关偏向锁的命令:
案例:
public static void BiasedLock(){
/**
* 这里偏向锁在JDK6以上默认开启,开启后程序启动4秒后才会被激活,可以通过JVM参数来关闭延迟
* 开启偏向锁: -XX:+UseBiasedLocking
* 关闭偏向锁的延迟 -XX:BiasedLockingStartupDelay=0
*/
// try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
Object o = new Object();
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
运行结果:
通过如下命令,可以查看JVM
相关参数的默认值。
java -XX:+PrintFlagsInitial | grep BiasedLock*
注意:
由于偏向锁启动有延迟,故而不加入
-XX:BiasedLockingStartupDelay=0
参数或者TimeUnit.SECONDS.sleep(5);
。上述代码会直接进入轻量级锁的状态**(锁标志位:00),而不是偏向锁(偏向锁位+锁标志位:101)**。由于维护成本过高,在JDK15以后会逐步废弃偏向锁。
2.3.2 轻量级锁(00)
概念:多线程竞争,但是任意时候最多只有一个线程竞争,即不存在锁竞争太激烈的情况,也就没有线程阻塞。
主要作用:有线程来参与锁的竞争,但是获取锁的冲突时间极短,本质是自旋锁CAS。
轻量锁的获取:
轻量级锁的加锁与释放
轻量级锁的加锁
JVM
会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,官方称为Displaced Mark Word
。若一个线程获得锁时发现是轻量级锁,会把锁的MarkWord
复制到自己的Displaced Mark Word
里面。然后线程尝试用CAS
将锁的MarkWord
替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
自旋CAS:不断尝试去获取锁,能不升级就不升级,尽量不要阻寨。
轻量级锁的释放
在释放锁时,当前线程会使用CAS
操作将Displaced Mark Word
的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS
操作会失败,此时会释放锁并唤醒被阻寨的线程。
自旋锁的次数
java6之前
java6之后
自适应自选锁:线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM
认为既然上次成功了,那么这一次也很大概率会成功。如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。
轻量锁和偏向锁的区别和不同
- 争夺轻量级锁失败时,自旋尝试抢占锁
- 轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁
2.3.3 重量级锁(10)
有大量的线程参与锁的竞争,冲突性很高。
Java中synchronized的重量级锁,是基于进入和退出Monitor
对象实现的。在编译时会将同步块的开始位置插入monitorenter
指令,在结束位置插入monitorexit
指令。
当线程执行到monitorenter
指令时,会尝试获取对象所对应的Monitor
所有权,如果获取到了,即获取到了锁,会在Monitor
的owner
中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor
。
2.3.4 小总结-面试中的高频考点
锁升级发生后,hashcode去哪啦
锁升级为轻量级或重量级锁后,Mark Word中保存的分别是线程栈帧里的锁记录指针和重量级锁指针,己经没有位置再保存哈希码,GC年龄了,那么这些信息被移动到哪里去了呢?
下面描述来源于:《深度理解Java虚拟机》第三版。
计算过哈希的对象,无法在进入偏向锁状态。
处于偏向锁的对象,当需要计算哈希时,该对象的偏向状态会被撤销,升级为重量级锁。而重量级锁对应
ObjectMonitor
对象有字段记录锁对象的Markword
,自然也就可以存储hashcode
代码验证:
public static void BiasedLock(){
/**
* 这里偏向锁在JDK6以上默认开启,开启后程序启动几秒后才会被激活,可以通过JVM参数来关闭延迟
* 开启偏向锁: -XX:+UseBiasedLocking
* 关闭偏向锁的延迟 -XX:BiasedLockingStartupDelay=0
*/
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
Object o = new Object();
System.out.println("不进行hash计算");
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
// 情况一:进行hashcode计算,对象无法进入偏向锁状态。
Object o2 = new Object();
System.out.println("情况一:没有取得锁,进行hashcode计算,对象无法进入偏向锁状态。");
System.out.println(o2.hashCode()); // 计算hashcode
synchronized (o2) { // 因为o2计算过hashcode,故而会直接进入轻量级锁。
System.out.println(ClassLayout.parseInstance(o2).toPrintable());
}
/*
情况二:取得偏向锁后,进行hashcode计算。对象会升级为重量级锁。
*/
Object o3 = new Object();
System.out.println("情况二:取得偏向锁后,进行hashcode计算。对象会升级为重量级锁。");
synchronized (o3) { // 因为o2计算过hashcode,故而会直接进入轻量级锁。
// 计算hash之前
System.out.println("计算hash之前");
System.out.println(ClassLayout.parseInstance(o3).toPrintable());
System.out.println(o3.hashCode());
System.out.println("计算hash之后");
System.out.println(ClassLayout.parseInstance(o3).toPrintable());
}
}
输出结果:通过锁标记位置,可以发现锁的变化过程。
开始
不进行hash计算
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 c8 88 00 (00000101 11001000 10001000 00000000) (8964101)
4 4 (object header) dd 01 00 00 (11011101 00000001 00000000 00000000) (477)
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
情况一:没有取得锁,进行hashcode计算,对象无法进入偏向锁状态。
1724731843
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) e8 f6 cf 38 (11101000 11110110 11001111 00111000) (953153256)
4 4 (object header) cd 00 00 00 (11001101 00000000 00000000 00000000) (205)
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
情况二:取得偏向锁后,进行hashcode计算。对象会升级为重量级锁。
计算hash之前
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 c8 88 00 (00000101 11001000 10001000 00000000) (8964101)
4 4 (object header) dd 01 00 00 (11011101 00000001 00000000 00000000) (477)
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
1305193908
计算hash之后
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) da fc 78 24 (11011010 11111100 01111000 00100100) (611908826)
4 4 (object header) dd 01 00 00 (11011101 00000001 00000000 00000000) (477)
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
结束
2.4 公平锁与非公平锁
- 公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似于排队买票,先来的人先买,后来的人再队尾排着,这是公平的。例如:
Lock lock = new ReentrantLock(true)
表示公平锁,先来先得。 - 非公平锁:是指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级反转或者饥饿的状态(某个线程一直得不到锁)。例如:
Lock lock = new ReentrantLock(true)
表示非公平锁,后来的也可能先获得锁,默认为非公平锁。
2.4.1 为什么要有公平锁与非公平锁?
答:公平与非公平各有优劣,更具不同的情况使用不同的锁。如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省了很多线程切换的时间,吞吐量自然就上去了;否则就用公平锁,大家公平使用。
非公平锁能更充分地利用CPU的时间片,尽量减少CPU空间状态时间。
2.4.2 为什么默认是非公平的?
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得很大,所以就减少了线程的开销。
2.5 各种锁的优缺点
3、JIT编译器对锁的优化
3.1 锁消除
每次的锁对象都是一个新的对象,无法达到同步的效果。在编译时期会忽略该同步代码块。
public void m1() {
//锁消除问题,JIT会无视它,synchronized(o)每次new出来的,都不存在了,非正常的
Object o = new Object();
synchronized (o) {
System.out.println("-----------hello LockClearUpDemo" + "\t" + o.hashCode() + "\t" + object.hashCode());
}
}
3.2 锁粗化
同一个对象锁,合并为统一的同步代码块,避免重复的加锁释放锁的操作。
public static void main(String[] args) {
new Thread(() -> {
synchronized (objectLock) {
System.out.println("111111111111");
}
synchronized (objectLock) {
System.out.println("222222222222");
}
synchronized (objectLock) {
System.out.println("333333333333");
}
synchronized (objectLock) {
System.out.println("444444444444");
}
//底层JIT的锁粗化优化
synchronized (objectLock) {
System.out.println("111111111111");
System.out.println("222222222222");
System.out.println("333333333333");
System.out.println("444444444444");
}
}, "t1").start();
}
}
参考
-
B站黑马JUC教学视频:https://www.bilibili.com/video/BV16J411h7Rd
-
B站尚硅谷JUC教学视频:https://www.bilibili.com/video/BV1ar4y1x727
-
《深入理解Java虚拟机》第三版——周志明