说说自己对于synchronized关键字的了解

synchronized关键字用于解决多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行

值得注意的是,在Java早期,JDK1.6之前,synchronized属于重量级锁,效率低下。

原因在于:

监视器锁【monitor】依赖于底层操作系统的Mutex Lock实现,Java的线程是映射到操作系统的原生线程之上的。如果要挂起或唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转化到内核态,需要消耗比较长的时间

但是,JDK1.6之后,Java官方从JVM层面对synchronized关键字进行了较大的优化,效率不可同日而语。主要的优化有:自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

synchronized关键字的三种使用

  1. 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
  2. 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁

注意:静态成员不属于任何一个实例对象,是类成员!因此,一个线程A调用一个实例对象的非静态synchronized方法,一个线程B调用这个实例对象的所属类的静态synchronized方法,是被允许的。

因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁

  1. 修饰代码块 :给括号内配置的对象加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁

synchronized关键字的底层原理

通过对.class文件反编译可以发现:

  • 同步方法通过ACC_SYNCHRONIZED修饰。
  • 代码块同步使用monitorentermonitorexit两个指令实现。

虽然两者实现细节不同,但其实本质上都是JVM基于进入和退出Monitor对象来实现同步,JVM的要求如下:

  • monitorenter指令会在编译后插入到同步代码块的开始位置,而monitorexit则会插入到方法结束和异常处。
  • 每个对象都有一个monitor与之关联,且当一个monitor被持有之后,他会处于锁定状态。
  • 线程执行到monitorenter时,会尝试获取对象对应monitor的所有权。
  • 在获取锁时,如果对象没被锁定,或者当前线程已经拥有了该对象的锁(可重进入,不会锁死自己),将锁计数器加一,执行monitorexit时,锁计数器减一,计数为零则锁释放。
  • 获取对象锁失败,则当前线程陷入阻塞,直到对象锁被另外一个线程释放。

JDK1.6之后对synchronized关键字进行的优化

https://blog.csdn.net/qq_34337272/article/details/108498442

优化:偏向锁,轻量级锁,自旋锁,适应性自旋锁,锁消除,锁粗化。

锁主要存在的四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

Java对象头的组成

锁存在于Java对象头里,对象头的组成部分:

  • Mark Word:存储对象的hashCode或锁信息等。
  • Class Metadata Address:存储到对象类型数据的指针。
  • Array length:数组的长度(如果当前对象是数组)

Java对象头又存在于Java堆中,堆内存分为三部分:对象头,实例数据和对齐填充。

MarkWord的组成

Java对象头的MardWord中记录了对象和锁的相关信息,无锁状态下,Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。在64位的JVM中,Mark Word为64 bit

在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。锁升级的功能也主要靠MarkWord中锁标志位是否偏向锁标志完成。

锁升级的过程

锁升级的过程:无锁,偏向锁,轻量级锁,重量级锁

偏向锁

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

偏向锁的适用场景

偏向锁主要用于优化:同一线程多次申请同一个锁的竞争,在某些情况下,大部分时间都是同一个线程竞争锁资源的。

偏向锁的加锁

主要流程:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁

  • 如果测试成功,表示线程已经获得了锁。
  • 如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):
    • 如果没有设置,则使用CAS竞争锁。
    • 如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁的撤销

一旦出现其他线程竞争锁资源时,偏向锁就会被撤销。偏向锁的撤销可能需要等待全局安全点【在这个时间点上没有正在执行的字节码】。

  • 首先暂停持有该锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态。
  • 如果持有偏向锁的线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒在暂停的线程

偏向锁的关闭

偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0

如果锁通常处于竞争状态,可以通过- XX:-UseBiasedLocking=false,进入轻量级锁状态。

轻量级锁

如偏向锁存在,如有另一线程竞争锁,且对象头MarkWord中的线程ID与当前线程ID不同,则该线程将会尝试CAS操作获取锁,获取失败,代表锁存在竞争,偏向锁向轻量级锁升级

轻量级锁的加锁

  • 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间【Displaced Mark Word】,并将对象头中的Mark Word复制到锁记录中
  • 然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针
    • 替换成功,则当前线程获得锁。
    • 替换失败,表示其他线程竞争锁,当前线程尝试使用自旋来获取锁。

轻量级锁的解锁

  • 使用原子的CAS操作将【Displaced Mark Word】替换回对象头。

    • 替换成功,表示没有竞争发生。
    • 替换失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

轻量级锁的适用场景

线程交替执行同步块,绝大部分的锁在整个同步周期内都不存在长时间的竞争

锁的优缺点对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,
会带来额外的锁撤销的消耗。
适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行速度较长。

总结

  1. JVM在JDK 1.6中引入了分级锁机制来优化synchronized
  2. 当一个线程获取锁时,首先对象锁成为一个偏向锁
    • 这是为了避免在同一线程重复获取同一把锁时,用户态和内核态频繁切换
  3. 如果有多个线程竞争锁资源,锁将会升级为轻量级锁
    • 这适用于在短时间内持有锁,且分锁交替切换的场景
    • 轻量级锁还结合了自旋锁避免线程用户态与内核态的频繁切换
  4. 如果锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁
  5. 优化synchronized同步锁的关键:减少锁竞争
    • 应该尽量使synchronized同步锁处于轻量级锁偏向锁,这样才能提高synchronized同步锁的性能
    • 常用手段
      • 减少锁粒度:降低锁竞争
      • 减少锁的持有时间,提高synchronized同步锁在自旋时获取锁资源的成功率,避免升级为重量级锁
  6. 锁竞争激烈时,可以考虑禁用偏向锁禁用自旋锁

synchronized关键字与ReentrantLock的区别

共同点

  • 都是可重入锁:自己可以再次获取自己的内部锁【避免一个线程获取锁之后,再次尝试获取锁时造成的死锁】。同一线程每次获取锁,计数器加一,释放锁,计数器减一,计数为0,代表完全释放该锁。

不同点

  • synchronized依赖于JVM实现,ReentrantLock依赖于API。

  • 相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:

    • 等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
    • 可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
    • 可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。

参考资料

【Java并发编程】synchronized相关面试题总结的更多相关文章

  1. Java并发编程75道面试题及答案

    1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User). 任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon( ...

  2. Java并发编程73道面试题及答案 —— 面试稳了

    今天主要整理一下 Java 并发编程在面试中的常见问题,希望对需要的读者有用. 1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User). 任 ...

  3. Java并发编程73道面试题及答案

    原文出处:https://blog.csdn.net/qq_34039315/article/details/7854931 1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线 ...

  4. JAVA并发编程:相关概念及VOLATILE关键字解析

    一.内存模型的相关概念 由于计算机在执行程序时都是在CPU中运行,临时数据存在主存即物理内存,数据的读取和写入都要和内存交互,CPU的运行速度远远快于内存,会大大降低程序执行的速度,于是就有了高速缓存 ...

  5. Java并发编程-synchronized指南

    在多线程程序中,同步修饰符用来控制对临界区代码的访问.其中一种方式是用synchronized关键字来保证代码的线程安全性.在Java中,synchronized修饰的代码块或方法不会被多个线程并发访 ...

  6. Java并发编程-synchronized

    多线程的同步机制对资源进行加锁,使得在同一个时间,只有一个线程可以进行操作,同步用以解决多个线程同时访问时可能出现的问题.同步机制可以使用synchronized关键字实现.synchronized关 ...

  7. java并发编程--Synchronized的理解

    synchronized实现锁的基础:Java中每一个对象都可以作为锁,具体表现为3种形式. (1)普通同步方法,锁是当前实例对象 (2)静态同步方法,锁是当前类的Class对象 (3)同步方法块,锁 ...

  8. Java并发编程 | Synchronized原理与使用

    Java提供了多种机制实现多线程之间有需要同步执行的场景需求.其中最基本的是Synchronized ,实现上使用对象监视器( Monitor ). Java中的每个对象都是与线程可以锁定或解锁的对象 ...

  9. Java并发编程面试题 Top 50 整理版

    本文在 Java线程面试题 Top 50的基础上,对部分答案进行进行了整理和补充,问题答案主要来自<Java编程思想(第四版)>,<Java并发编程实战>和一些优秀的博客,当然 ...

随机推荐

  1. nohup 命令的使用

    nohup 命令的使用 1. nohup简介 nohup 命令运行由 Command参数和任何相关的 Arg参数指定的命令,忽略所有挂断(SIGHUP)信号.在注销后使用 nohup 命令运行后台中的 ...

  2. Cobalt Strike简单使用

    ---恢复内容开始--- 一.介绍: 后渗透测试工具,基于Java开发,适用于团队间协同作战,简称“CS”. CS分为客户端和服务端,一般情况下我们称服务端为团队服务器,该工具具有社工功能(社会工程学 ...

  3. 如何理解算法时间复杂度的表示法O(n²)、O(n)、O(1)、O(nlogn)等?

    先从 来说,理论上哈希表就是O(1).因为哈希表是通过哈希函数来映射的,所以拿到一个关键字,用哈希函数转换一下,就可以直接从表中取出对应的值.和现存数据有多少毫无关系,故而每次执行该操作只需要恒定的时 ...

  4. elaticsearch

  5. 算法-搜索(4)ISAM算法

    ISAM技术是一种典型的多叉搜索树结构,它使用了3级索引结构:主索引.柱面索引.磁道索引 所有数据记录在基本区按关键码升序排序,后一磁道所有关键码均大于前一磁道.在某一磁道插入新记录时,如果原来该磁道 ...

  6. 2020.5.26 第六篇 Scrum冲刺博客

    Team:银河超级无敌舰队 Project:招新通 项目冲刺集合贴:链接 目录 一.每日站立会议 1.1 会议照片 1.2 项目完成情况 二.项目燃尽图 三.签入记录 3.1 代码/文档签入记录 3. ...

  7. Object中toString方法

    在Java中,所有的对象都是继承自Object,自然继承了toString方法,在当使用System,out.println()里面为一个对象的引用时,自动调用toString方法将对象打印出来.如果 ...

  8. Node.js小项目——学生信息管理系统

    这是迄今为止第一次接触后端的东西,是一个很小的项目,但是对于前端学习入门很好.我是先学了VUE框架再学的Node,学起来比较轻松,不过每个人都有自己的学习方法️ 一.项目描述 学生信息管理系统,可以实 ...

  9. SecureCRT 关键字高亮显示

    grep命令红色高亮关键字 1.  左边侧栏 Session Manage 右键Sessions --Properties 2 Terminal--Appearance Current color s ...

  10. 时间转换c#

    简要 本篇文章主要介绍基于.net(c#)封装的时间帮助类.主要功能是将时间戳与DateTime类型相互转换. 1.其中包括将毫秒级时间戳(13位).秒级时间戳(10位)转换为DateTime. 2. ...