synchronized关键字的使用和原理

synchronized关键字的使用和原理

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

1、使用方式:

锁对象:理论上可以是任意的唯一对象

package com.jtc.fe;
 
public class synchronizedDemo {
 
    // 同步静态方法 ——> 锁住class类对象 ——> 全局唯一
    synchronized public static void func1(){}
 	// 同步方法 ——> 锁住实例对象 ——> 每个实例都可以当锁
    synchronized public void func3(){}
    
    // 同步代码块
    public void func2(){
        // ——> 锁住class类对象 ——> 全局唯一
        synchronized (synchronizedDemo.class){}
    }
    public void func4(){
        // ——> 锁住实例对象 ——> 每个实例都可以当锁
        synchronized (this){}
    }
    public void func5(){
        // ——> 锁住字符串"123"
        synchronized ("123"){}
    }
}

注意:

synchronized 修饰的方法的不具备继承性,所以子类是线程不安全的,如果子类的方法也被 synchronized 修饰,两个锁对象其实是一把锁,而且是子类对象作为锁

2、锁原理

Java的对象由三部分组成:对象头 + 实例数据 + 对齐填充。

2.1、对象头

64位对象头由Mark Word、Class Pointer两部分组成,如果对象是数组,则还要加上数组长度,即三部分组成。在这里插入图片描述

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。由于对象需要存储的运行时数据很多,但64位虚拟机给它的空间只有64位Bitmap,所以Mark Word被设计成一个有着动态定义的数据结构,即不同的锁状态存储内容不同。
在这里插入图片描述

如果要查看内存结构图,需要引入依赖:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
import com.example.demo.User;
import org.openjdk.jol.info.ClassLayout;

public class p6 {
    static User user = new User();
    static User[] users = new User[10];
    public static void main(String[] args) {
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
        System.out.println("*******************************************");
        System.out.println(ClassLayout.parseInstance(users).toPrintable());
    }
}

在这里插入图片描述

Mark Word由64位8个字节组成。Class Pointer由64位8个字节组成,但我们使用的64位 JVM会默认使用选项 +UseCompressedOops 开启指针压缩,将指针压缩至32位。即上面截图中的Class Pointer为4个字节32位。

如果在打印对象头前调用HashCode方法,则会变成如下:
在这里插入图片描述

从MarkWord的结构可以看出,在无锁阶段内存分布与上图是一一对应的,HashCode也是一一对应的。

2.2、锁升级

synchronized 是可重入、不公平的重量级锁,所以可以对其进行优化。

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁	// 随着竞争的增加,只能锁升级,不能降级
其中重量级锁还需要Monitor对象配合使用

偏向锁

偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程之后重新获取该锁不再需要同步操作:当锁对象第一次被线程获得的时候进入偏向状态,使用 CAS 操作将线程 ID 记录到 Mark Word。

轻量级锁

当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定或轻量级锁状态。一个对象有多个线程要加锁,但加锁的时间是错开的(没有竞争),可以使用轻量级锁来优化,轻量级锁对使用者是透明的(不可见)。

重量级锁

在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

在重量级锁阶段,每个 Java 对象都可以关联一个 Monitor 对象,Monitor 也是 class,其实例存储在堆中,如果使用 synchronized 给对象上锁之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针,这就是重量级锁。
在这里插入图片描述

工作流程:

  • 开始时 Monitor 中 Owner 为 null

  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,obj 对象的 Mark Word 指向 Monitor,把对象原有的 MarkWord 存入线程栈中的锁记录

  • 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList BLOCKED(双向链表)

  • Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 MarkWord

  • 唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,如果这时有新的线程想要获取锁,可能直接就抢占到了,阻塞队列的线程就会继续阻塞

  • WaitSet 中的 Thread-0,是以前获得过锁,但条件不满足进入 WAITING 状态的线程(wait-notify 机制)

2.3、代码字节码

修饰代码段时:

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 		
7: 	astore_1 				
8: 	aload_1					
9: 	dup						
10: astore_2 				
11: monitorenter 			// 【将 lock对象 MarkWord 置为 Monitor 指针】
12: getstatic 		#3		
15: ldc 			#4		
17: invokevirtual 	#5 		
20: aload_2 				
21: monitorexit 			// 【将 lock对象 MarkWord 重置, 唤醒 EntryList】
22: goto 30
25: astore_3 				
26: aload_2 				
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;

从上面我们可以看出:synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。其中两个 monitorexit 指令是为了保证锁在同步代码块代码正常执行以及出现异常的这两种情况下都能被正确释放。

修饰方法时:

public synchronized void method() {
    System.out.println("synchronized 方法");
}

JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。

总结

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,修饰方法是 ACC_SYNCHRONIZED 标识。

不过两者的本质都是对对象监视器 Monitor 的获取。
参考:https://github.com/Seazean/JavaNote/blob/main/Prog.md
https://blog.csdn.net/zhaocuit/article/details/100208879?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170239663116800182715111%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=170239663116800182715111&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-100208879-null-null.142v96pc_search_result_base9&utm_term=java%E5%AF%B9%E8%B1%A1%E5%A4%B4&spm=1018.2226.3001.4187

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值