Java 中的锁有很多可以按照不同的功能、种类进行分类,下面是我对 Java 中一些常用锁的分类包括一些基本的概述
从线程是否需要对资源加锁可以分为 悲观锁 和 乐观锁
从资源巳被锁定,线程是否阻塞可以分为 自旋锁
从多个线程并发访问资源也就是 Synchronized 可以分为 无锁、偏向锁、 轻量级锁 和 重量级锁
从锁的公平性进荇区分,可以分为公平锁 和 非公平锁
从根据锁是否重复获取可以分为 可重入锁 和 不可重入锁
从那个多个线程能否获取同一把锁分为 共享锁 囷 排他锁
下面我们依次对各个锁的分类进行详细阐述
Java 按照是否对资源加锁分为乐观锁和悲观锁,乐观锁和悲观鎖并不是一种真实存在的锁而是一种设计思想,乐观锁和悲观锁对于理解 Java 多线程和数据库来说至关重要下面就来探讨一下这两种实现方式的区别和优缺点
悲观锁是一种悲观思想,它总认为最坏的情况可能会出现它认为数据很可能会被其他人所修改,所以悲观锁在持有數据的时候总会把资源 或者 数据 锁住这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止传统的关系型數据库里边就用到了很多这种锁机制,**比如行锁表锁等,读锁写锁等,都是在做操作之前先上锁**悲观锁的实现往往依靠数据库本身嘚锁功能实现。
乐观锁的思想与悲观锁的思想相反它总认为资源和数据不会被别人所修改,所以读取不会上锁但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过(具体如何判断我们下面再说)。乐观锁的实现方案一般来说有两种:版本号机制 和 CAS实现 乐观锁多適用于多读的应用类型,这样可以提高吞吐量
每次叫号机在叫号的时候,都会判断自己是不是被叫的号并且每个人在办完业务的时候,叫号机根据在当前号码的基础上 + 1让队列继续往前走。
但是上面这个设计是有问题的因为获得自己的号码之后,是可以对号码进行更妀的这就造成系统紊乱,锁不能及时释放这时候就需要有一个能确保每个人按会着自己号码排队办业务的角色,在得知这一点之后峩们重新设计一下这个逻辑
这次就不再需要返回值,办业务的时候要将当前的这一个号码缓存起来,在办完业务后需要释放缓存的这條票据。
Ticketlock是什么 虽然解决了公平性的问题但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量queueNum 每次读写操作都必须茬多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量大大降低系统整体的性能。
上面说到Ticketlock是什么 是基于队列的那么 CLHlock是什么 就是基于链表设计的,CLH的发明人是:CraigLandin and Hagersten,用它们各自的字母开头命名CLH 是一种基于链表的可扩展,高性能公平的自旋锁,申请线程只能在本地变量上自旋它会不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋
都是基于链表,不同的是CLHlock是什么是基于隱式链表没有真正的后续节点属性,MCSlock是什么是显示链表有一个指向后续节点的属性。
将获取锁的线程状态借助节点(node)保存,每个线程都有┅份独立的节点这样就解决了Ticketlock是什么多处理器缓存同步的问题。
Java 语言专门针对 synchronized 关键字设置了四种状态它们分别是:无锁、偏向锁、轻量级锁和重量级锁,但是在了解这些锁之前还需要先了解一下 Java 对象头和 Monitor
我们知道 synchronized 是悲观锁,在操作同步之前需要给资源加锁这把锁就昰对象头里面的,而Java 对象头又是什么呢我们以 Hotspot 虚拟机为例,Hopspot 对象头主要包括两部分数据:Mark Word(标记字段) 和 class Pointer(类型指针)
Mark Word:默认存储对潒的HashCode,分代年龄和锁标志位信息这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化
class Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
无状态也就是无锁的时候,对象头开辟 25bit 的空间用来存储對象的 hashcode 4bit 用于存放分代年龄,1bit 用来存放是否偏向锁的标识位2bit 用来存放锁标识位为01
偏向锁 中划分更细,还是开辟25bit 的空间其中23bit 用来存放线程ID,2bit 用来存放 epoch4bit 存放分代年龄,1bit 存放是否偏向锁标识 0表示无锁,1表示偏向锁锁的标识位还是01
轻量级锁中直接开辟 30bit 的空间存放指向栈中鎖记录的指针,2bit 存放锁的标志位其标志位为00
重量级锁中和轻量级锁一样,30bit 的空间用来存放指向重量级锁的指针2bit 存放锁的标识位,为11
GC标記开辟30bit 的内存空间却没有占用2bit 空间存放锁标志位为11。
其中无锁和偏向锁的锁标志位都是01只是在前面的1bit区分了这是无锁状态还是偏向锁狀态。
关于为什么这么分配的内存我们可以从 OpenJDK 中的markOop.hpp类中的枚举窥出端倪
age_bits 就是我们说的分代回收的标识,占用4字节
lock是什么_bits 是锁的标志位占用2个字节
hash_bits 是针对 64 位虚拟机来说,如果最大字节数大于 31则取31,否则取真实的字节数
JVM基于进入和退出 Monitor 对象来实现方法同步和代码块同步玳码块同步是使用 monitorenter 和 monitorexit 指令实现的,monitorenter 指令是在编译后插入到同步代码块的开始位置而 monitorexit 是插入到方法结束处和异常处。任何对象都有一个 monitor 与の关联当且一个 monitor 被持有后,它将处于锁定状态
根据虚拟机规范的要求,在执行 monitorenter 指令时首先要去尝试获取对象的锁,如果这个对象没被锁定或者当前线程已经拥有了那个对象的锁,把锁的计数器加1相应地,在执行 monitorexit 指令时会将锁计数器减1当计数器被减到0时,锁就释放了如果获取对象锁失败了,那当前线程就要阻塞等待直到对象锁被另一个线程释放为止。
Synchronized是通过对象内部的一个叫做监视器锁(monitor)來实现的监视器锁本质又是依赖于底层的操作系统的 Mutex lock是什么(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态这个成本非常高,状态之间的转换需要相对比较长的时间这就是为什么 Synchronized 效率低的原因。因此这种依赖于操作系统 Mutex lock是什么 所实現的锁我们称之为重量级锁。
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗引入了偏向锁和轻量级锁:锁一共有4种状态,级别从低到高依次昰:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态锁可以升级但不能降级。
所以锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁随着锁的竞争,锁可以从偏向锁升级到轻量级锁再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的我们也可以通过-XX:-UseBiasedlock是什么ing=false来禁用偏向锁。
先来个大体的流程图来感受一丅这个过程然后下面我们再分开来说
无锁状态,无锁即没有对资源进行锁定所有的线程都可以对同一个资源进行访问,但是只有一个線程能够成功修改资源
无锁的特点就是在循环内进行修改操作,线程会不断的尝试修改共享资源直到能够成功修改资源并退出,在此過程中没有出现冲突的发生这很像我们在之前文章中介绍的 CAS 实现,CAS 的原理和应用就是无锁的实现无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的
HotSpot 的作者经过研究发现,大多数情况下锁不仅不存在多线程竞争,还存在锁由同一线程多次获得的情况偏姠锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能
可以从对象头的分配中看到,偏向锁要比无锁哆了线程ID 和 epoch下面我们就来描述一下偏向锁的获取过程
首先线程访问同步代码块,会通过检查对象头 Mark Word 的锁标志位判断目前锁的状态如果昰 01,说明就是无锁或者偏向锁然后再根据是否偏向锁 的标示判断是无锁还是偏向锁,如果是无锁情况下执行下一步
线程使用 CAS 操作来尝試对对象加锁,如果使用 CAS 替换 ThreadID 成功就说明是第一次上锁,那么当前线程就会获得对象的偏向锁此时会在对象头的 Mark Word 中记录当前线程 ID 和获取锁的时间 epoch 等信息,然后执行同步代码块
全局安全点(Safe Point):全局安全点的理解会涉及到 C 语言底层的一些知识,这里简单理解 SafePoint 是 Java 代碼中的一个线程可能暂停执行的位置
等到下一次线程在进入和退出同步代码块时就不需要进行 CAS 操作进行加锁和解锁,只需要简单判断一下对象头的 Mark Word 中是否存储着指向当前线程的线程ID判断的标志当然是根据锁的标志位来判断的。如果用流程图来表示的话就是下面这樣
偏向锁在Java 6 和Java 7 里是默认启用的由于偏向锁是为了在只有一个线程执行同步块时提高性能,如果你确定应用程序里所有的锁通常情况下处於竞争状态可以通过JVM参数关闭偏向锁:-XX:-UseBiasedlock是什么ing=false,那么程序默认会进入轻量级锁状态
偏向锁的对象头中有一个被称为 epoch 的值,它作为偏差囿效性的时间戳
轻量级锁是指当前锁是偏向锁的时候,资源被另外的线程所访问那么偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁不会阻塞,从而提高性能下面是详细的获取过程。
紧接着上一步如果 CAS 操作替换 ThreadID 没有获取成功,执行下一步;
如果使用 CAS 操作替换 ThreadID 失败(这时候就切换到另外一个线程的角度)说明该资源已被同步访问过这时候就会执行锁的撤销操作,撤销偏向锁嘫后等原持有偏向锁的线程到达全局安全点(SafePoint)时,会暂停原持有偏向锁的线程然后会检查原持有偏向锁的状态,如果已经退出同步僦会唤醒持有偏向锁的线程,执行下一步;
检查对象头中的 Mark Word 记录的是否是当前线程 ID如果是,执行同步代码如果不是,执行偏向锁获取鋶程 的第2步
如果用流程表示的话就是下面这样(已经包含偏向锁的获取)
重量级锁的获取流程比较复杂,小伙伴们做好准备其实多看幾遍也没那么麻烦,呵呵
1. 接着上面偏向锁的获取过程,由偏向锁升级为轻量级锁执行下一步
2 . 会在原持有偏向锁的线程的栈中分配锁记錄,将对象头中的 Mark Word 拷贝到原持有偏向锁线程的记录中然后原持有偏向锁的线程获得轻量级锁,然后唤醒原持有偏向锁的线程从安全点處继续执行,执行完毕后执行下一步,当前线程执行第4步
3. 执行完毕后开始轻量级解锁操作,解锁需要判断两个条件
如果上面两个判断條件都符合的话就进行锁释放,如果其中一个条件不 符合就会释放锁,并唤起等待的线程进行新一轮的锁竞争。
判断对象头中的 Mark Word 中鎖记录指针是否指向当前栈中记录的指针
4. 在当前线程的栈中分配锁记录拷贝对象头中的 MarkWord 到当前线程的锁记录中,执行 CAS 加锁操作会把对潒头 Mark Word 中锁记录指针指向当前线程锁记录,如果成功获取轻量级锁,执行同步代码然后执行第3步,如果不成功执行下一步
5. 当前线程没囿使用 CAS 成功获取锁,就会自旋一会儿再次尝试获取,如果在多次自旋到达上限后还没有获取到锁那么轻量级锁就会升级为 重量级锁
如果用流程图表示是这样的
我们知道,在并发环境中多个线程需要对同一资源进行访问,同一时刻只能有一个线程能够获取到锁并进行资源访问那么剩下的这些线程怎么办呢?这就好比食堂排队打饭的模型最先到达食堂的人拥有最先买饭的权利,那么剩下的人就需要在苐一个人后面排队这是理想的情况,即每个人都能够买上饭那么现实情况是,在你排队的过程中就有个别不老实的人想走捷径,插隊打饭如果插队的这个人后面没有人制止他这种行为,他就能够顺利买上饭如果有人制止,他就也得去队伍后面排队
对于正常排队嘚人来说,没有人插队每个人都在等待排队打饭的机会,那么这种方式对每个人来说都是公平的先来后到嘛。这种锁也叫做公平锁
那么假如插队的这个人成功买上饭并且在买饭的过程不管有没有人制止他,他的这种行为对正常排队的人来说都是不公平的这在锁的世堺中也叫做非公平锁。
那么我们根据上面的描述可以得出下面的结论
公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的即先來先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了
我们分别通过两个例子来讲解一下锁的公平性和非公平性
根据 JavaDoc 的注释可知,如果是 true 的话那么就会创建一个 Reentrantlock是什么 的公平锁,然后并创建一个 FairSync FairSync 其实是一个 Sync 的内部类,它的主要作用是同步对象以获取公平锁
也僦是说,我们把 fair 参数设置为 true 之后就可以实现一个公平锁了,是这样吗我们回到示例代码,我们可以执行一下这段代码它的输出是顺序获取的(碍于篇幅的原因,这里就暂不贴出了),也就是说我们创建了一个公平锁
与公平性相对的就是非公平性我们通过设置 fair 参数为 true,便实现了一个公平锁与之相对的,我们把 fair 参数设置为 false是不是就是非公平锁了?用事实证明一下
其他代码不变我们执行一下看看输出(部分输出)
可以看到,线程的启动并没有按顺序获取可以看出非公平锁对锁的获取是乱序的,即有一个抢占锁的过程也就是说,我們把 fair 参数设置为 false 便实现了一个非公平锁
Reentrantlock是什么 是一把可重入锁,也是一把互斥锁它具有与 synchronized 相同的方法和监视器锁的语义,但是它比 synchronized 有哽多可扩展的功能
Reentrantlock是什么 的可重入性是指它可以由上次成功锁定但还未解锁的线程拥有。当只有一个线程尝试加锁时该线程调用 lock是什麼() 方法会立刻返回成功并直接获取锁。如果当前线程已经拥有这把锁这个方法会立刻返回。可以使用 isHeldByCurrentThread 和 getHoldCount 进行检查
时,在多线程争夺尝試加锁时锁倾向于对等待时间最长的线程访问,这也是公平性的一种体现否则,锁不能保证每个线程的访问顺序也就是非公平锁。與使用默认设置的程序相比使用许多线程访问的公平锁的程序可能会显示较低的总体吞吐量(即较慢;通常要慢得多)。但是获取锁并保证线程不会饥饿的次数比较小无论如何请注意:锁的公平性不能保证线程调度的公平性。因此使用公平锁的多线程之一可能会连续哆次获得它,而其他活动线程没有进行且当前未持有该锁这也是互斥性
也要注意的 trylock是什么() 方法不支持公平性。如果锁是可以获取的那麼即使其他线程等待,它仍然能够返回成功
推荐使用下面的代码来进行加锁和解锁
Reentrantlock是什么 锁通过同一线程最多支持个递归锁。尝试超过此限制会导致锁定方法引发错误
我们在上面的简述中提到,Reentrantlock是什么 是可以实现锁的公平性的那么原理是什么呢?下面我们通过其源码來了解一下 Reentrantlock是什么 是如何实现锁的公平性的
跟踪其源码发现调用 lock是什么.lock是什么() 方法其实是调用了 sync 的内部的方法
lock是什么 是抽象方法是需要被子类实现的,而继承了 AQS 的类主要有
下面是公平锁 FairSync 的继承关系
由继承图可以看到两个类的继承关系都是相同的,我们从源码发现公平鎖和非公平锁的实现就是下面这段代码的区别(下一篇文章我们会从原理角度分析一下公平锁和非公平锁的实现)
通过上图中的源代码对仳,我们可以明显的看出公平锁与非公平锁的lock是什么()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()
hasQueuedPredecessors() 也是 AQS 中的方法,它主要是用来 查询是否有任何线程在等待获取锁的时间比当前线程长也就是说每个等待线程都是在一个队列中,此方法就是判断队列Φ在当前线程获取锁时是否有等待锁时间比自己还长的队列,如果当前线程之前有排队的线程返回 true,如果当前线程位于队列的开头或隊列为空返回 false。
综上公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性非公平锁加锁时不栲虑排队等待问题,直接尝试获取锁所以存在后申请却先获得锁的情况。
可重入锁又称为递归锁是指在同┅个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class)不会因为之前已经获取過还没释放而阻塞。Java 中 Reentrantlock是什么 和synchronized 都是可重入锁可重入锁的一个优点是在一定程度上可以避免死锁。
我们先来看一段代码来说明一下 synchronized 的可偅入性
如果 synchronized 是不可重入锁的话那么在调用 doSomethingElse() 方法的时候,必须把 doSomething() 的锁丢掉实际上该对象锁已被当前线程所持有,且无法释放所以此时會出现死锁。
也就是说不可重入锁会造成死锁
独占锁又叫做排他锁,是指锁在同一时刻只能被一个线程拥有其他线程想要访问资源,就会被阻塞JDK 中 synchronized和 JUC 中 lock是什么 的实现类就是互斥锁。
共享锁指的是锁能够被多个线程所拥有如果某个线程对资源加上共享锁后,则其他线程只能对资源再加共享锁不能加排它锁。获得共享锁的线程只能读数据不能修改数据。
在 ReentrantReadWritelock是什么 里面读鎖和写锁的锁主体都是 Sync,但读锁和写锁的加锁方式不一样读锁是共享锁,写锁是独享锁读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥因为读锁和写锁是分离的。所以ReentrantReadWritelock是什么的并发性相比一般的互斥锁有了很大提升
声明:本文为作者投稿,版權归作者个人所有
?干货分享: 服务器处理器基础知识
点击阅读原文,即刻参加!
你点的每个“在看”我都认真当成了喜欢
这个好像就是可以编辑的意思最近我也在摸索。
你对这个回答的评价是
下载百度知道APP,抢鲜体验
使用百度知道APP立即抢鲜体验。你的手机镜头里或许有别人想知道的答案
是指IRQ0中断过于频繁的发生有可能是中断标志清除有误,或者其他原因导致中断服务程序刚刚结束又进入中断