目录

synchronized

synchronized锁的特点

synchronized锁级别

无锁

偏向锁

轻量级锁

重量级锁

自旋锁

锁消除

ReentrantLock

公平锁和非公平锁

AQS

ReetrantLock加锁的实现(默认非公平锁)

ReentrantLock公平锁


针对并发JAVA中提供了JVM级别的隐式锁即synchronized关键字,jvm进行操作加锁与解锁。除此之外在JUC包下Lock接口也提供了类似的锁机制,例如ReentrantLock可重入锁。

在两种锁选择使用上的个人观点

我认为不能绝对的认为Lock优于关键字,其实在synchronized引入偏向锁等优化后在并发不是很高的情况下两种方式性能差别并不大,而且synchronized关键字更加方便省心,例如它可以避免异常带来的死锁问题(JVM会帮我们释放),而Lock就必须手动unlock释放。因此我认为只有当并发相当高的时候,或者确实需要lock独有的比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票等这些特性时使用Lock才是最合适的。

synchronized

JVM的关键字,是采用C++实现的。在java1.6版本之后,jvm内置锁进行了一系列的优化,如:锁粗化、锁消除、偏向锁、轻量级锁、重量级锁等,synchronized的效率相对于之前纯Mutex Lock实现方式得到提升,优化后的实现是每个对象里面隐式的存在一个叫monitor(对象监视器)的对象,当监视器monitor对象被线程持有时,Monitor对象中的count就会进行+1,当线程释放monitor对象时,count又会进行-1操作。用count来表示monitor对象是否被持有。但是监视器锁升级为重量级锁后本质还是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。

如果synchronized修饰的是普通方法,那锁作用于类的对象,即每个对象都有自己的一把锁多个对象间互不干扰。

如果synchronized修饰的是类的静态方法,那锁作用于class类,即类的多有对象公用一把锁。

synchronized锁的特点

  1. synchronized 中的锁是非公平的;
  2. synchronized 中的锁是可重入的;
  3. synchronized 锁可以自动释放。

synchronized锁级别

synchronized锁升级过程依次为:无锁 <===> 偏向锁 ——> 轻量级锁 ——> 重量锁 。锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态

无锁

对象的monitor对象并没有被线程所持有,代表的是对象处于无锁状态。

偏向锁

偏向锁指的是这个锁会偏向于第一个获得它的线程。由于大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,JDK1.6后面引起的一项锁优化技术,在无锁竞争的情况下,一个线程会先检测对象头 Mark Word 中的标志位 Tag 是否为 01,如果是01则通过一次CAS操作来尝试将MarkWord对象头中的Thread ID字段设置为自己的线程号,如果设置成功,则获得锁,那么以后线程再次进入和退出同步块时,就不需要使用CAS来获取锁,只是简单的测试一个对象头中的MarkWord中的ThreadId是否存是当前线程的的ID。

偏向锁的升级过程

当下一个线程参与到偏向锁竞争时,会先判断 MarkWord 中保存的线程 ID 是否与这个线程 ID 相等,不相等则说明存在锁的竞争,立即撤销偏向锁,升级成轻量级锁。

  1. 当线程A访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程A再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是当前线程A获取锁对象),则无需使用CAS来加锁、解锁;
  2. 如果不一致(其他线程要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程A的threadID),那么需要查看Java对象头中记录的线程A是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程B)可以竞争将其设置为偏向锁;
  3. 如果不一致且占有偏向锁的线程A存活,那么立刻查找该线程(线程A)的栈帧信息,如果还是需要继续持有这个锁对象,就暂停当前线程A,撤销偏向锁,升级为轻量级锁,如果线程A不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程B或者设置失败升级为轻量锁

轻量级锁

偏向锁设置失败导致锁升级为轻量锁后,两个线程之间竞争采用CAS来改变状态的操作。

轻量锁的升级过程

如果在有限次数之后仍然没有获取到锁,或者竞争获取过程中存在两条或两条以上的线程竞争同一个锁,则轻量级锁会膨胀成重量级锁。

重量级锁

当一个锁被两条或两条以上的线程竞争的时候,这时候轻量级锁就会演变成重量级锁。重量级锁的竞争锁不使用自旋锁,而是阻塞,即当线程竞争锁失败后该线程会被阻塞,当占有锁的线程释放锁后会唤醒被阻塞的竞争线程。重量级锁不会再降级。

自旋锁

当两个线程去竞争同一把锁时,一个线程获取成功,一个线程获取失败,这时可能会出现获取成功的线程持有锁的时间非常短,如果这时候将获取失败的线程进行挂起的话,会造成功线程上下文的切换,到时候又需要唤醒线程,这时我们可以让获取失败的线程进行一个自旋,无需将线程挂起。等到锁释放。但是这种方案适用于锁被占用的时间很短的情况,如果锁被持有的时间很长,然后线程将会一直处于自旋状态,白白消耗处理器资源。自旋等待的时间必须要有一定的限度,如果超过此数还是没有获取到,则将线程挂起。

自旋次数的默认值是10次,可以使用JVM参数-XX:PreBlockSpin来进行更改。JDK1.5默认10次,1.6中时自适应: 自旋时间不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。

看了一些资料关于自旋锁还是有疑惑的地方,就是自旋发生在什么时候,有资料说轻量锁中竞争时通过自旋来竞争锁,也有资料说只有重量级锁中存在自旋的竞争方式?欢迎大佬留言观点

锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其它线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无法进行。

这种情况其实是针对那些非自定义的同步代码,例如线程安全的StringBuffer,加入我们在使用StringBuffer时定义在方法内部,即它的作用域只在当前方法内,那么每个线程都是单独的,不存在资源共享竞争。但是StringBuffer内部的append方法是同步方法,这时候在JVM编译时就会根据逃逸分析的数据把锁消除,因为这时候时没有必要加锁的。

通过分析可以知道锁消除时发生在偏向锁中的,因为是在同步快执行的过程中发生的

ReentrantLock

ReentrantLock 是 JDK 实现的,在java.util.concurrent.locks 包下。

实现方式:ReentrantLock是使用队列同步器AbstractQueuedSynchronizer(AQS)和CAS操作来实现锁的获取和释放,具体可以看下面的ReentrantLock代码分析部分。

  1. 需要lock()和unlock()方法配合try/finally语句块来完成并发同步操作
  2. ReentrantLock 默认情况下也是非公平的,也可以是公平的:使用Lock lock = new ReentrantLock(true)创建公平锁

 ReentrantLock 类内部有NonfairSync和FairSync两个内部类,分别是公平锁和非公平锁,这两个内部类是继承自另一个Sync内部类实现的,Sync又继承了AbstractQueuedSynchronizer类。所以总体上ReentrantLock是基于AQS实现的。

公平锁和非公平锁

在ReentrantLock 锁的实现中,公平锁和非公平锁是对于线程竞争锁的方式而言的。

如果一个线程想得到锁,首先必须要进入一个队列排队进行锁的竞争,这就是公平锁;

如果一个线程在加入队列排队之前就可以进行锁的竞争,那这就是非公平锁。

AQS

AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包。AQS是一个同步框架,分为独占模式和共享模式。它提供通用机制来原子性管理同步状态、阻塞和唤醒线程,以及维护被阻塞线程的队列,可以用于实现synchronized关键字之外的锁机制。上面提到了ReentrantLock就是基于AQS实现的。

AQS定义了独占模式的acquire()和release()方法,共享模式的acquireShared()和releaseShared()方法.还定义了抽象方法tryAcquire()、tryAcquiredShared()、tryRelease()和tryReleaseShared()由子类实现,tryAcquire()和tryAcquiredShared()分别对应独占模式和共享模式下的锁的尝试获取,就是通过这两个方法来实现公平性和非公平性,在尝试获取中,如果新来的线程必须先入队才能获取锁就是公平的,否则就是非公平的。每一个基于AQS实现的同步器都会包含两种类型的操作,如下:

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,AQS就是基于CLH队列。仅存在节点之间的关联关系。AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。

  • 至少一个acquire操作。这个操作阻塞调用线程,除非/直到AQS的状态允许这个线程继续执行。
  • 至少一个release操作。这个操作改变AQS的状态,改变后的状态可允许一个或多个阻塞线程被解除阻塞。

ReetrantLock加锁的实现(默认非公平锁)

首先资源state初始化为0,无锁状态。A线程执行lock()通过CAS方法将state设置为1,并设置该线程A独占该锁,加锁成功。

在锁获取之后一直到释放锁之前这期间,A线程自己是可以重复获取此锁的,实现方式是根据当前线程判断如果是持有锁的线程本身进行加锁操作,就对state进行累加,同一个锁最多能重入Integer.MAX_VALUE次,也就是2147483647,这就是可重入的实现方式。

在A线程加锁执行期间,其他线程竞争锁首先执行compareAndSetState失败,然后判断锁是否空闲,如果空闲就再调用acquire()方法尝试再获取一次锁,如果还是失败则调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,这个方法中还是会再调用acquire()方法尝试再获取一次锁,如果仍然失败则操作该线程进入一个FIFO等待队列。然后当前线程被挂起等待唤醒后再次竞争锁。

A线程执行完,通过unlock()来释放锁,如果期间锁不止加了一次就要保证获取多少次就要释放多少次,直到state=0才算完全释放锁,释放后竞争线程就有机会获取该锁了。 

ReentrantLock.lock()方法部分代码片段如下

//加锁入口
final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);//非无锁状态走这一步尝试再加锁
}
public final void acquire(int arg) {//尝试再次加锁,若一直失败则加入队列if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {//当前线程已经持有锁则state累加操作int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}
//尝试再次加锁,若失败后操作线程进入队列
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

总结AQS执行加锁操作时有三种可能

  1. 如果没有线程持有这个锁,则请求成功,当前线程直接获取到锁。
  2. 如果当前线程已经持有锁,则使用 CAS 将volatile修饰共享变量state的值进行加1操作,表示自己再次申请了锁,释放锁时减1。这就是可重入性的实现。
  3. 如果其他线程持有这个锁且多次尝试加锁失败,那么将自己添加进等待队列,等待被唤醒。

ReentrantLock公平锁

如果在声明ReentrantLock时,通过Lock lock = new ReentrantLock(true)的方式创建公平锁,那线程竞争锁的逻辑就不太一样了。

主要区别就在于:去掉了上来就尝试加锁的步骤。线程不会先尝试获取,直接判断锁是否空闲,如果不空闲就直接加入队列排队,按FIFO的顺序竞争锁。

synchronized锁的级别和ReentrantLock锁(AQS)相关推荐

  1. java锁的概念,Java ReentrantLock锁机制概念篇

    分享Java锁机制实现原理,细节涉及volatile修饰符.CAS原子操作.park阻塞线程与unpark唤醒.双向链表.锁的公平性与非公平性.独占锁和共享锁.线程等待await.线程中断interr ...

  2. 2021面试 Lock,synch,dcl双检查锁sy+volite,悲观锁,偏向,轻量锁,重量锁,升级12

    0.数据库悲观锁:for update: MySQL实现悲观锁_九色鹿-CSDN博客_mysql悲观锁怎么实现 1. ReentrantLock锁公平与非公平实现.重入原理:  ReentrantLo ...

  3. reentrantlock非公平锁不会随机挂起线程?_程序员必须要知道的ReentrantLock 及 AQS 实现原理...

    专注于Java领域优质技术,欢迎关注 作者:Float_Luuu 提到 JAVA 加锁,我们通常会想到 synchronized 关键字或者是 Java Concurrent Util(后面简称JCU ...

  4. java锁的级别_Java锁的那些事儿之JDK锁(ReentrantLock)

    Java 的jdk级别的锁主要是在JUC包下,包括atomic包下的乐观锁和locks包下的悲观锁.今天来介绍一下locks包下的悲观锁(由于百家号对markdown支持不好,代码只能以图片的方式展示 ...

  5. Java—synchronized和ReentrantLock锁详解

    关注微信公众号:CodingTechWork,一起学习进步. 1 synchronized 1.1 synchronized介绍 synchronized机制提供了对每个对象相关的隐式监视器锁,并强制 ...

  6. 12.synchronized的锁重入、锁消除、锁升级原理?无锁、偏向锁、轻量级锁、自旋、重量级锁

    小陈:呼叫老王...... 老王:来了来了,小陈你准备好了吗?今天我们来讲synchronized的锁重入.锁优化.和锁升级的原理 小陈:早就准备好了,我现在都等不及了 老王:那就好,那我们废话不多说 ...

  7. Java锁详解之ReentrantLock

    文章目录 写在前面 ReentrantLock的重要方法 ReentrantLock使用示例 ReentrantLock的公平和非公平锁 ReentrantLock的重入锁 ReentrantLock ...

  8. JUC多线程:synchronized锁机制原理 与 Lock锁机制

    前言: 线程安全是并发编程中的重要关注点,造成线程安全问题的主要原因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据.因此为了解决这个问题,我们可能需要这样一个方案,当存在多 ...

  9. 打通JAVA与内核系列之一ReentrantLock锁的实现原理

    简介:写JAVA代码的同学都知道,JAVA里的锁有两大类,一类是synchronized锁,一类是concurrent包里的锁(JUC锁).其中synchronized锁是JAVA语言层面提供的能力, ...

最新文章

  1. 线段树 ---- 线段树维护线段相加+滑动变长窗口 2021牛客多校第7场 F xay loves trees
  2. FPGA之道(13)FPGA芯片的应用模式
  3. ASP.NET上传文件对文件类型的高级判断
  4. 发达国家农业模式-国际农民丰收节贸易会:全球农业未来
  5. Linux DISPLAY作用
  6. 在基于简单Vertx Rest的应用程序上为REST资源设置基本响应HTTP标头
  7. 声速的测量的实验原理和应用_创想智控:光学三角测量系统的测量原理与应用...
  8. Android中的IPC机制
  9. Word2Vec学习笔记(五)——Negative Sampling 模型(续)
  10. 使用IDEA+MVN 编译Spark 1.5.2 without hive
  11. AWS 专家教你快速使用 Spring Boot 和 DJL!
  12. pgsql merge方法
  13. BZOJ4141 THUSC2013 魔塔 贪心
  14. 关于铁路订票系统如何改善设计的讨论
  15. 学习记录-app渗透
  16. APP测试基础--小工具介绍(1)
  17. VSCode格式化XML
  18. 玩客云root成功一键获取root权限
  19. 如何完美解决解决win10系统--无法自动修复此计算机问题
  20. HTML+CSS+JavaScript 实现登录注册页面(超炫酷)

热门文章

  1. 波浪螺旋型弹簧的画法
  2. php表单输入内容换行,PHP获取表单textarea数据换行用法示例
  3. png.h缺失 - 安装
  4. SRTP RFC 3711
  5. 劝学篇翻译软件测试,曾国藩家书劝学篇 禀父母·教弟注重看书-曾国藩家书全文翻译-原文,译文,注释-【易文言】-古文,文言文在线翻译网...
  6. 在IE右键添加菜单项和任务栏按钮
  7. 在线Excel转公式工具
  8. 有哪些erp软件适合做东南亚跨境电商?
  9. css3之before、after伪类以及简单实现购物车图标
  10. 金种子集团搭建TurboGate反垃圾邮件网关