那些你需要掌握的synchronized的实现原理

一、synchronized介绍以及基本使用

1.1、synchronized的变动

JDK1.6之前,一般认为synchronized是重量级锁,操作系统底层的Mutex Lock来实现的,需要用户态与和心态进行切换,很耗性能。

  • JDK1.5之前,能够协调线程间对共享变量的访问的机制只有synchronizedvolatile,但是这样存在一些局限性
  • JDK1.5新增了ReentrantLock,它的出现给了我们另外一个选择,当synchronized重量级锁不适用时,可以选择它
  • JDK1.6对synchronized进行了各种优化,有些情况就没有那么重了,整体性能与ReentrantLock持平,由于ReentrantLock是在synchronized的基础上提供了更多的功能,于是咋用synchronized能够实现需求的情况下优先考虑synchronized。

1.2、对象锁和类锁

1.2.1、获取对象锁的两种用法
  • 同步代码块(synchronized(this),synchronized(类实例对象)),锁是括号中的实例对象
public class SynchronizedDemo1 {
    public void method() {
        synchronized (this) {
            System.out.println("方法执行");
        }
    }
}
public class SynchronizedDemo2{
     private static Object lock = new Object(); // 对象实例
    
     public void method(){
         synchronized (lock){
             System.out.println("方法执行");
         }
     }
}
  • 同步非静态方法(synchronized method),锁是当前对象的实例对象
    • 使用如下
public class SynchronizedDemo3 {
    public synchronized void method() {
        System.out.println("方法执行");
    }
}
1.2.2、获取类锁的两种方法
  • 同步代码块(synchronized(类.class)),锁是小括号中的类对象(Class 对象)
public class SynchronizedDemo4{
     public void method(){
         synchronized (SynchronizedDemo4.class){
             System.out.println("方法执行");
         }
     }
}
  • 同步静态方法(synchronized static method),锁是当前对象的类对象(Class对象)
public class SynchronizedDemo5 {
    public static synchronized void method() {
        System.out.println("方法执行");
    }
}

注意: 以上是对获取对象锁和获取类锁使用方法的介绍,想要测试锁竞争情况下的实际情况,可以在同步代码块中或者方法体重使用循环或者sleep来进行测试

对象锁和类锁的总结
  • 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块
  • 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞;
  • 若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞
  • 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞,反之亦然‘
  • 同一个类的不同对象的对象锁互不干扰
  • 类锁由于也是一种特殊的对象锁,因此表现和上述1,2,3,4一致,而由于一个类只有一把对象锁,所以同一个类的不同对象使用类锁将会是同步的
  • 类锁和对象锁互不干扰

二、Java对象头

在讲接下来的内容之前先说一下Java对象头相关的知识。前边说了,类锁也是特殊的对象锁,对象上锁,我们需要在对象中记录锁信息。

2.1、Java对象头的长度

对象头的长度有两种,分别是3字宽和2字宽(1字宽=4字节=32bit),3字宽的长度对应的对象类型是数组类型,非数组类型使用2字宽

长度内容说明
32/64bitMark Word存储对象的hashCode或锁信息
32/64bitClass Metadata Address存储到对象类型数据的指针
32/64bitArray length数组的长度(如果当前对象是数组)

2.2、Mark Word 的存储结构及状态变化

锁相关的当然是锁信息了(借图),下图是
在这里插入图片描述

三、锁的升级与对比

3.1、锁的状态介绍

JDK1.6中对synchronized进行了优化,这之后锁有了四种状态:

  • 无锁
  • 偏向锁:偏向锁的设计是为了减少同一线程获取锁的代价。大多数情况下,锁不会存在多线程竞争,总是由同一个线程多次获得。为了避免每次都要加锁,设计出了偏向锁,对象头中的mark word线程id记录当前的线程id并且是否是偏向锁的值为1。同一线程下次再次访问时,检测线程id一样,则不需要进行同步,直接继续执行代码。
  • 轻量级锁(乐观锁):轻量级锁是由偏向锁升级来的,偏向锁时期如果有第二个线程来竞争锁则锁会升级为轻量级锁。通常是两个线程交替执行同步块或者同步方法,有线程持有锁的时候,其他线程稍稍自旋等待一下
  • 重量级锁(悲观锁):如果某个线程持有锁太久,即其他线程自旋太久,其他线程此时也要对该对象进行访问,此时可以说是出现了激烈的竞争,那么轻量级锁就会升级为重量级锁

锁升级的方向由上到下进行升级。

以上只是简单介绍,后边在锁升级会详细说说的

3.2、偏向锁

前边介绍了偏向锁是为了让线程获取锁的代价更低而引入的。

  • 当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录中存储指向当前偏向线程的线程id,那么之后该线程访问当前同步块和退出同步块时不需要进行CAS操作来进行加锁和解锁,只需要检测Mark Word中的线程id是否指向当前线程
  • 如果id的确指向该线程,说明该线程已经获取锁,直接执行下面代码。
  • 如果id不是指向该线程,则继续测试Mark Word中的偏向锁标识是否为1,如果不是,则使用CAS去竞争锁(JDK1.6/JDK1.7默认开启偏向锁),如果是,则说明偏向锁已经有了偏向,当前线程则尝试使用CAS去将对象头的偏向锁指向自己(这个时候发生竞争,偏向锁先升级为轻量级锁)
3.2.1、偏向锁的撤销

由于出现了另外一个线程竞争当前锁,此时的场景已经不是偏向锁能够处理的了,于是偏向锁会升级为轻量级锁。既然有锁的升级那么就有偏向锁的撤销

锁撤销的步骤大概分为如下几步

  • 等待全局安全点(这个时间点上没有正在执行的字节码),在全局安全点上暂停当前拥有偏向锁的线程
  • 检测当前线程是否处于活动状态,如果不处于活动状态,那么将对象头设置成无锁状态。如果线程仍然活着,则遍历它的栈中的关于该对象的锁记录,如果找到说明正在持有该锁,这时候就修改他们,让线程认为当前获取到的对象锁是轻量级锁,并且更改对象头中的Mark Word。如果没有持有该锁,那么对线头中Mark Word恢复到无锁状态或者标记对象不适合作为偏向锁
  • 最后唤醒当前线程

以上流程可以看下图:
在这里插入图片描述

3.3、轻量级锁

3.3.1、轻量级锁加锁
  • 线程执行同步块前,JVM会先在当前线程的栈帧中创建名为锁记录(Lock Record)的空间,并且将对象头中的Mark Word复制到锁记录中,并将锁记录中的Owner指向锁对象
  • 如果拷贝成功,则线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,如果替换成功,则代表当前线程成功获取轻量级锁,如果替换失败,则表示其他线程竞争锁,则当前线程尝试使用自旋来获取锁(前提是当前只有一个等待线程),自旋会消耗CPU,但线程不会阻塞,响应更快
3.3.2、轻量级锁的种类

轻量级锁主要有两种:自旋锁和自适应自旋锁

  • 自旋锁:当前轻量级锁有线程持有,如果有另外一个线程来竞争锁时,该线程会在原地进行等待,而不是将它阻塞(阻塞需要借助操作系统进行帮忙,需要内核态与用户态进行切换,太消耗性能),这样能够快速响应。但是原地自旋的时候也是会消耗CPU的,所以轻量级锁适合同步代码块或者方法执行很快的场景

  • 自旋锁的问题

    • 如果持有锁的线程执行时间较长或者多个线程在自选等待,但是锁一旦释放,可能就会被其他线程夺取,而此线程还得继续自旋,这样导致自旋时间较长,白白耗费CPU
    • 对于这个问题我们可以给定一个空循环次数,自旋锁默认情况下,自旋次数为10次,可以通过-XX:PreBlockSpin进行更改,一旦超过这个次数,那么锁会升级为重量级锁
  • 自适应自旋锁:自适应自旋锁就是空循环次数不是固定的,而是根据实际情况改变次数。如果当前需要自旋的线程刚刚成功获取了锁,现在又来获取,虚拟机会认为该线程自旋获取锁的成功率很高,于是会延长该线程的自旋时间。如果该线程获取锁很少成功,那么有可能直接忽略掉自旋过程,直接升级成为重量级锁,避免浪费CPU资源

3.3.3、轻量级锁解锁

解锁时,会使用CAS操作将锁记录中复制的那一份Mark Word替换回对象头中,如果成功则表示没有竞争发生,如果失败则表示当前锁存竞争比较严重,那么锁就会膨胀成为重量级锁

在这里插入图片描述

3.4、重量级锁

  • 重量级锁是依赖对象内部的monitor锁实现的,而monitor又是依赖操作系统的MuteLock(互斥锁) 实现的,所以重量级锁又称为互斥锁
  • 重量级锁状态下,竞争的线程是不会进行自旋的,而是直接阻塞,且不会降级为轻量级锁,当持有锁的线程释放掉锁之后会唤醒这些线程,然后展开新一轮的锁争夺
  • 每次阻塞或者唤醒线程都需要操作系统来进行帮忙,涉及到用户态转换为内核态,会消耗很多时间,所以重量级锁开销非常大,适用于吞吐量大的场景,同步代码块较长

3.5、三种锁的优缺点

在这里插入图片描述

四、其他锁操作

4.1、锁消除

  • JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁

StringBuffer为什么是线程安全的,比如,执行append方法,源码中,该方法是synchronized类型的同步方法,能够保证只有一个线程操作,比如:

StringBuffer s = new StringBuffer();
s.append(str1).append(str2);

s只会在append方法中使用,不可能被其他线程引用,因此s属于不可能共享的资源,JVM会自动消除内部的锁,即第二个append锁会被消除

4.2、锁粗化

锁粗化是一种极端情况:

  • 通过扩大加锁的范围,避免反复枷锁和解锁

比如循环s.append(参数)100次,JVM能够检测到对同一个对象多次加锁,于是就会粗化该锁,粗化到整个循环操作的外部加同步锁

本文是博主学习总结,结合了网上其他文章的一些观点总结而成,如有不妥,请及时指出

参考资料: 《Java并发编程的艺术》

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
2023年,Java开发1-2年的程序员应该对Spring、MySQL和多线程技术有一定的掌握程度。以下是每个技术点的详细说明: 1. Spring框架: - Spring IoC(控制反转)和DI(依赖注入):了解IoC和DI的原理和概念,能够使用注解或XML配置实现依赖注入。 - Spring AOP(面向切面编程):了解AOP的概念和原理,能够使用切面进行日志记录、事务管理等操作。 - Spring MVC:熟悉Spring MVC框架的基本原理和使用方法,能够进行Web应用的开发和请求处理。 - Spring Boot:掌握Spring Boot的基本概念和使用方法,能够快速搭建和配置Spring应用。 2. MySQL数据库: - 数据库基础知识:了解数据库的基本概念、表设计和SQL语言,能够编写常见的SQL查询和更新语句。 - 数据库连接和事务管理:掌握数据库连接池的使用,了解事务的概念和ACID特性,能够进行事务管理和异常处理。 - 索引和性能优化:了解索引的原理和创建方法,能够进行索引优化和查询性能调优。 3. 多线程技术: - 线程基础知识:了解线程的概念和生命周期,能够创建和启动线程,并进行基本的线程同步和通信。 - 线程安全:了解线程安全的概念和常见的线程安全问题,能够使用synchronized关键字或Lock接口实现线程安全。 - 并发集合类:熟悉Java并发包中的并发集合类(如ConcurrentHashMap、ConcurrentLinkedQueue)的使用。 - 线程池:了解线程池的概念和使用方法,能够创建和配置线程池,并进行线程调度和任务管理。 请注意,以上只是技术点的基本要求,实际工作中可能还会涉及到更深入的知识和技能。因此,持续学习和实践是提高技术水平的关键。同时,根据具体的项目需求和行业趋势,也可能会有其他相关技术需要掌握

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值