【JUC(二)】Java中的同步与锁

1、synchronized

1.1 synchronized的解释

synchronized(锁对象)
{
    // 临界区
}

synchronized:对象锁,保证了临界区内代码的原子性,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程获取这个对象锁时会阻塞,保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换

互斥和同步都可以采用 synchronized 关键字来完成,区别:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

性能:

  • 线程安全,性能差
  • 线程不安全性能好,假如开发中不会存在多线程安全问题,建议使用线程不安全的设计类

1.2 synchronized的用法

  1. 同步代码块+对象锁
IncreaseAndDecrease inde = new IncreaseAndDecrease();
private static int counter = 0;
public void increase(){
    synchronized (inde){
        counter++;
    }
}
  1. 同步方法+对象锁
private static int counter = 0;
public synchronized void increase(){
        counter++;
}
// 等价于:
public void increase(){
	synchronized(this){
        counter++;
    }
}
  1. 同步静态方法+对象锁
private static int counter = 0;
public static synchronized void increase(){
        counter++;
}
// 等价于:
public void increase(){
	synchronized(当前类.class){
        counter++;
    }
}

用法小总结

  1. 非静态方法的默认锁为 this,静态方法的锁默认为Class实例。
  2. 在某一个时刻内,之能有一个线程持有锁,无论几个方法。
  3. 更多案例可以参考:线程八锁

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");
}
线程安全的设计建议
  1. 设计程序时,不该暴露给外部的方法一定要私有化,防止子类重写。
  2. 暴露给外部的方法,考虑是否需要final,防止子类重写。
  3. 当前设计的类,是否需要被继承,如果不需要可直接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

markword

/*
    情况一:调用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的工作流程

ObjectMonitor

  • 当多个线程同时访问一段同步代码时,首先会进入 _EntryList 队列中。
  • 当某个线程获取到对象的 Monitor 后进入临界区域,并把 Monitor 中的 _owner 变量指向同步对象,同时Monitor中的计数器 _count 加1。即获得对象锁。
  • 若持有Monitor的线程调用 wait() 方法,将释放当前持有的Monitor,_owner变量恢复为null,_count自减1,同时该线程进入 _WaitSet 集合中等待被唤醒。
  • 在_WaitSet 集合中的线程会被再次放到_EntryList 队列中,重新竞争获取锁。
  • 若当前线程执行完毕也将释放Monitor并复位变量的值,以便其他线程进入获取锁。
    【photo】

注意:

  1. synchronized 必须是进入同一个对象的 Monitor 才有上述的效
  2. 不加 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");
    }
}
  1. JVM中每个Java对象都会对应一个C++实现的 ObjectMonitor

  2. 当对程序进行加锁时,lock 对象的对象头中的markword被设置为指向 ObjectMonitor的指针(重量锁的情况,其他锁有些许区别)。

  3. 当线程抢到锁后,ObjectMonitor对象中的owner属性会被设置为当前线程对象,代表当前线程已经持有锁。ObjectMonitor对象会被锁定,其他线程无法获得锁。

  4. 当前线程释放锁,owner设置为 Null,并唤醒其他线程进行争枪锁。

2.3 synchronized 的升级过程

在 Java 早期版本中,synchronized属于重量级锁,效率低下。为什么呢?因为监视器锁(monitor)是依赖于底层的操作系统来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

无锁(CAS) -> 偏向锁 -> 轻量级锁 -> 重量级锁	// 随着竞争的增加,只能锁升级,不能降级

Synchronized用的锁是存在Java对象头里的MarkWord中,锁升级功能主要依赖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所有权,如果获取到了,即获取到了锁,会在Monitorowner中存放当前线程的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();
    }
}

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值