您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

说说你对 Java 中锁以及 sychronized 实现机制的理解

时间:2022-11-09 11:57:57  来源:  作者: Java精选

理解锁的基础知识

如果想要透彻的理解JAVA锁的来龙去脉,需要先了解以下基础知识。

基础知识之一:锁的类型

按照其性质分类

1)公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。

对于JavaReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁。

由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

2)乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

悲观锁在Java中的使用,就是利用各种锁。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

3)独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。对于JavaReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。

读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于Synchronized而言,当然是独享锁。

4)互斥锁/读写锁

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock,读写锁在Java中的具体实现就是ReentrantReadWriteLock

5)可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Reentrant Lock重新进入锁。对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

基础知识之二:java线程阻塞的代价

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;

如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。

synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。

明确java线程切换的代价,是理解java中各种锁的优缺点的基础之一。

基础知识之三:CAS

CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。

Java5以来,你可以使用
java.util.concurrent.atomic

中的一些原子类来使用CPU中的这些功能:

private AtomicBoolean locked = new AtomicBoolean(false);
public boolean lock() {
return locked.compareAndSet(false, true);

locked变量不再是boolean类型而是AtomicBoolean。这个类中有一个compareAndSet()方法,它使用一个期望值和AtomicBoolean实例的值比较,和两者相等,则使用一个新值替换原来的值。在这个例子中,它比较locked的值和false,如果locked的值为false,则把修改为true。

如果值被替换了,compareAndSet()返回true,否则,返回false。

CAS的ABA问题

1)进程P1在共享变量中读到值为A;

2)P1被抢占了,进程P2执行;

3)P2把共享变量里的值从A改成了B,再改回到A,此时被P1抢占;

4)P1回来看到共享变量里的值没有被改变,于是继续执行;

这个例子你可能没有看懂,维基百科上给了一个活生生的例子——

 

你拿着一个装满钱的手提箱在飞机场,此时过来了一个火辣性感的美女, 然后她很暖昧地挑逗着你,并趁你不注意的时候,把用一个一模一样的 手提箱和你那装满钱的箱子调了个包,然后就离开了,你看到你的手提 箱还在那,于是就提着手提箱去赶飞机去了。

 

小结

前面提到了java的4种锁,他们分别是重量级锁、自旋锁、轻量级锁和偏向锁,不同的锁有不同特点,每种锁只有在其特定的场景下,才会有出色的表现,java中没有哪种锁能够在所有情况下都能有出色的效率,引入这么多锁的原因就是为了应对不同的情况;

前面讲到了重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁,所以现在你就能够大致理解了他们的适用范围,但是具体如何使用这几种锁呢,就要看后面的具体分析他们的特性。

synchronized的实现机制

Java SE1.6里synchronized一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级

锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

自旋锁

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗

但是线程自旋是需要消耗cpu的,说白了就是让cpu在做无用功,如果一直获取不到锁,那线程也不能一直占用cpu自旋做无用功,所以需要设定一个自旋等待的最大时间。

如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

1)自旋锁的优缺点

自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!

但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,占着XX不XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cpu的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭自旋锁。

2)自旋锁的开启

JDK1.6-XX:+UseSpinning开启;

-XX:PreBlockSpin=10为自旋次数;

JDK1.7后,去掉此参数,由jvm控制。

偏向锁

Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。

偏向锁,顾名思义,它会偏向于第一个访问锁的线程。

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。

另外,JVM对那种会有多线程加锁,但不存在锁竞争的情况也做了优化,听起来比较拗口,但在现实应用中确实是可能出现这种情况,因为线程之前除了互斥之外也可能发生同步关系,被同步的两个线程(一前一后)对共享对象锁的竞争很可能是没有冲突的。

对这种情况,JVM用一个epoch表示一个偏向锁的时间戳(真实地生成一个时间戳代价还是蛮大的,因此这里应当理解为一种类似时间戳的identifier)。

如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

它通过消除资源无竞争情况下的同步,进一步提高了程序的运行性能。

1)偏向锁的获取

访问Mark word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。

如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。

如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。

如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)

执行同步代码。

2)偏向锁的释放

偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

3)偏向锁的适用场景

始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致stop the word操作;

在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用;

4)jvm开启/关闭偏向锁

开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

关闭偏向锁:-XX:-UseBiasedLocking

轻量级锁

轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;跟多关于锁面试资料,公众 号Java精选,回复java面试,获取面试资料。

1)加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word

然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。

2)解锁

轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。如果失败,表示有其他线程尝试过获取该锁,表示当前锁存在竞争,锁就会膨胀成重量级锁。则要在释放锁的同时唤醒被挂起的线程。

总结

偏向锁/轻量级锁/重量级锁这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

第一步,检查MarkWord里面是不是放的自己的ThreadId,如果是,表示当前线程是处于 “偏向锁”.跳过轻量级锁直接执行同步体。

第二步,如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。

第三步,两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把共享对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord。

第四步,第三步中成功执行CAS的获得资源,失败的则进入自旋。

第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果自旋失败 第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。

如果线程争用激烈,那么应该禁用偏向锁。

锁优化

以上介绍的锁不是我们代码中能够控制的,但是借鉴上面的思想,我们可以优化我们自己线程的加锁操作;

减少锁的时间

不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放;

减少锁的粒度

它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;

java中很多数据结构都是采用这种方法提高并发操作的效率:

ConcurrentHashMap的锁分段技术

LinkedBlockingQueue也体现了这样的思想,在队列头入队,在队列尾出队,入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高;

锁粗化

锁的粗化则是要增大锁的粒度;

在以下场景下需要粗化锁的粒度:

假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的;

使用读写锁

ReentrantReadWriteLock是一个读写锁,读操作加读锁,可以并发读,写操作使用写锁,只能单线程写;

读写分离

CopyOnWriteArrayListCopyOnWriteArraySet

我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,而是操作容器的副本。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

使用cas

如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用volatiled+cas操作会是非常高效的选择。

 

作者:Myosotis https://segmentfault.com/a/1190000013512810

 

公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!

最近有很多人问,有没有读者交流群!加入方式很简单,公众号Java精选,回复“加群”,即可入群!

微信小程序):3000+道面试题,包含Java基础、并发、JVM、线程、MQ系列、redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计等,在线随时刷题!

------ 特别推荐 ------

特别推荐:专注分享最前沿的技术与资讯,为弯道超车做好准备及各种开源项目与高效率软件的公众号,「大咖笔记」,专注挖掘好东西,非常值得大家关注。点击下方公众号卡片关注

文章有帮助的话,点在看,转发吧!



Tags:Java   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
有时候,我们需要知道当前网络的状态来做一些事情,以提升用户体验,这节课,我们来看一下,如何使用 JavaScript 来检测用户是否在线。检测连接状态我们可以利用navigator.onLine API...【详细内容】
2023-01-03  Tags: Java  点击:(7)  评论:(0)  加入收藏
在PC时代,曾有个名为“变速齿轮”的神奇软件,可以加快或减慢系统时间。当时常用来修改游戏速度,可实现外挂一般的效果,很不可思议。本文,将用JavaScript复刻这一功能,实现一个用于...【详细内容】
2022-12-27  Tags: Java  点击:(10)  评论:(0)  加入收藏
了解这33个JavaScript概念绝对会让你将来的职业生涯受益无穷。话不多说,直接进入主题!1. 调用栈 调用栈是解释器(如web浏览器中的JavaScript解释器)跟踪其在调用多个函数的脚本...【详细内容】
2022-12-24  Tags: Java  点击:(12)  评论:(0)  加入收藏
前言据说这个功能最近在抖音上很火,我没有抖音,没有看到。 但是我在掘金和CSDN上看了,相关案例确实很多,但是大家都是借助于了微信服务号,在我看来,效果很不佳。其实我原来的初衷...【详细内容】
2022-12-24  Tags: Java  点击:(9)  评论:(0)  加入收藏
日常办公和软件开发除了可以使用Windows系统以外,还可以使用macOS系统,至于具体使用什么系统取决于你入职公司之后公司给你发的什么电脑,如果是MacBookPro那么就使用macOS开...【详细内容】
2022-12-23  Tags: Java  点击:(18)  评论:(0)  加入收藏
作者:小傅哥 博客:https://bugstack.cn 沉淀、分享、成长,让自己和他人都能有所收获!一、前言 Java学到什么程度可以找工作?最近总看到类似这样的问题,也有一些工作3年左右的小伙...【详细内容】
2022-12-23  Tags: Java  点击:(24)  评论:(0)  加入收藏
[限流基本概念] [限流方案常用算法] [常用的限流方案] [从架构维度考虑限流设计] [具体的实现限流的手段] [限流基本概念] 对一般的限流场景来说它...【详细内容】
2022-12-22  Tags: Java  点击:(0)  评论:(0)  加入收藏
本文讨论了对 HTML 代码可视化文档的需求,并提供了一个免费的 API 解决方案,可将 HTML 字符串转换为 PNG 屏幕截图。 自 20 世纪 80 年代后期的构想以来,超文本标记语言 (HTML)...【详细内容】
2022-12-21  Tags: Java  点击:(16)  评论:(0)  加入收藏
JavaScript奇淫技巧:隐写术本文将用JavaScript实现“图片隐写术”。什么是隐写术?将文本或其它数据写入图片的技术,称为“隐写术”。比如下面这张图中,便隐藏着秘密信息,虽然看起...【详细内容】
2022-12-11  Tags: Java  点击:(27)  评论:(0)  加入收藏
问题Java并发情况下总是会遇到各种意向不到的问题,比如下面的代码:int num = 0;boolean ready = false;// 线程1 执行此方法public void actor1(I_Result r) { if(ready) { r...【详细内容】
2022-12-05  Tags: Java  点击:(22)  评论:(0)  加入收藏
▌简易百科推荐
前言据说这个功能最近在抖音上很火,我没有抖音,没有看到。 但是我在掘金和CSDN上看了,相关案例确实很多,但是大家都是借助于了微信服务号,在我看来,效果很不佳。其实我原来的初衷...【详细内容】
2022-12-24  穆雄雄  稀土掘金  Tags:Java   点击:(9)  评论:(0)  加入收藏
日常办公和软件开发除了可以使用Windows系统以外,还可以使用macOS系统,至于具体使用什么系统取决于你入职公司之后公司给你发的什么电脑,如果是MacBookPro那么就使用macOS开...【详细内容】
2022-12-23  ittirneline  今日头条  Tags:Java   点击:(18)  评论:(0)  加入收藏
[限流基本概念] [限流方案常用算法] [常用的限流方案] [从架构维度考虑限流设计] [具体的实现限流的手段] [限流基本概念] 对一般的限流场景来说它...【详细内容】
2022-12-22     IT架构师联盟   Tags:Java   点击:(0)  评论:(0)  加入收藏
本文讨论了对 HTML 代码可视化文档的需求,并提供了一个免费的 API 解决方案,可将 HTML 字符串转换为 PNG 屏幕截图。 自 20 世纪 80 年代后期的构想以来,超文本标记语言 (HTML)...【详细内容】
2022-12-21   qaseven   网易号  Tags:Java   点击:(16)  评论:(0)  加入收藏
我们在日常开发中,经常会用到一个系统需要链接多个数据库来实现业务的需求,比如多个系统之间数据调用、两个数据之间同步等等。今天给大家分享使用Hutool-db实现多数据源配...【详细内容】
2022-12-19  IT技术分享社区  今日头条  Tags:hutool-db   点击:(15)  评论:(0)  加入收藏
问题Java并发情况下总是会遇到各种意向不到的问题,比如下面的代码:int num = 0;boolean ready = false;// 线程1 执行此方法public void actor1(I_Result r) { if(ready) { r...【详细内容】
2022-12-05  JAVA旭阳  今日头条  Tags:JAVA   点击:(22)  评论:(0)  加入收藏
面试题在之前的内容中我们介绍了字符串。同时,也介绍了字符串的拼接操作,并且分析了几种字符串拼接方式的原理。其实在java8中还提供了一种新的字符串操作方式。就是今天我们...【详细内容】
2022-12-02  架构师面试宝典  今日头条  Tags:Java8   点击:(36)  评论:(0)  加入收藏
1、 我代码就加了一行log日志,结果引发了P1的线上事故2、14个SpringBoot优化小妙招,看完后同事说写代码像写诗!3、 干掉 “重复代码”,这三种方式绝了!4、 我在代码里面故意留个...【详细内容】
2022-12-01  Happy Eng Class    Tags:Java   点击:(18)  评论:(0)  加入收藏
统计用户在线人数在统计用户在人数的时候,我们用到了监听器,监听器大致分为以下三种: ServletRequestListener:用于监听请求的监听接口 HttpSessionListener:用于监听...【详细内容】
2022-11-18  Java精选  网易号  Tags:Java   点击:(29)  评论:(0)  加入收藏
Java多线程的实现方式Java程序中,常见有4种方式实现多线程①继承Thread类②实现Runnable接口③实现Callable接口④使用Executor框架在JDK5之前,创建线程有2种方式,一种是继承Th...【详细内容】
2022-11-17  Esgoon  今日头条  Tags:Java   点击:(40)  评论:(0)  加入收藏
站内最新
站内热门
站内头条