synchronized详解

一、前置

1、解决线程安全并发安全问题的方法

所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问

同步互斥访问的解决办法是设计一个同步器,对多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是: 对象、变量、文件等
注意:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。
同步器采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源。
共享:资源可以由多个线程同时访问。
可变:资源可以在其生命周期内被修改
java提供了两种方式来实现同步互斥访问:synchronized 和 Lock

2、synchronized 和 Lock

(1)synchronized在1.5版本时的状况:这是因为在jdk1.5版本的时候,jdk官方就提供出了 synchronized 锁,但是在1.5版本的时候,synchronized 锁的加锁方式只有一个,就是通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低,也就是比较消耗性能。
(2)Lock锁的出现:由于 synchronized 锁的性能不大好,加的锁都是重要级别的锁,涉及到线程之间的状态切换,要从用户态切换到内核态,所以就有一个人设计了Lock锁,在当时,Lock锁的性能要比 synchronized 好很多。
synchronized锁的优化:后来jdk官方就对synchronized锁进行了优化,成了现在这个样子,性能基本和Lock差不多了。
在这里插入图片描述

二、synchronized底层原理

1、synchronized的加锁方式

synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。

加锁的方式:
(1)同步实例方法,锁是当前实例对象
(2)同步类方法,锁是当前类对象
(3)同步代码块,锁是括号里面的对象

2、synchronized锁的实现过程

synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁,性能较低。当然,JVM内置锁在1.5之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与Lock持平。

synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置与结束位置。
在这里插入图片描述
每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:
在这里插入图片描述

3、monitor

(1)可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被描述为一个对象
(2)与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁
(3)MarkWord锁标识位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有两个队列,_WaitSet_EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

(1)首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
(2)若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
(3)若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。

3、synchronized锁存在的位置

synchronized 锁的信息一般存储在对象的对象头中,对象头里面有一个Mark Word,如果是32位系统的话,是占4个字节的,对象头如下图所示:

在这里插入图片描述

例如在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示:

在这里插入图片描述

三、synchronized锁的优化

synchronized锁的优化一共有四种方法:锁的膨胀升级锁的粗化锁的消除自旋锁

1、锁的膨胀升级(不可逆)

简单的过程为:无锁→偏向锁→轻量级锁→重量级锁

从JDK 1.6 中默认是开启偏向锁和轻量级锁的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。下图为锁的升级全过程:
在这里插入图片描述

1.1、偏向锁

java对于偏向锁的启动是在启动几秒之后才激活。因为jvm启动的过程中会有大量的同步块,且这些同步块都有竞争,如果一启动就启动偏向锁,会出现很多没有必要的锁竞争,由于偏向锁的启动需要时间,而程序启动之后,偏向锁还未启动,导致对象加锁之后,直接从无锁状态跳过了偏向锁状态,因此直接变成了轻量级锁。

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。但当有多个线程同时访问对象时,并且竞争不是特别激烈(激烈意思就是其它线程不用等很久,比如说一两秒那种)时,就会升级成轻量级锁。
总结:也就是当一个对象只有一个线程进行访问时,它的锁就是偏向锁。

1.2、轻量级锁

轻量级锁在执行的过程中,如果有资源争抢的情况,会自己进行自旋(spin,就相当于执行空循环),当然这个自旋有一定的次数,当自旋的次数已经已经达到了,CPU会认为就是抢占资源比较严重的情况,就自己将轻量级锁升级成了重量级锁

当有多个线程同时访问被加锁的对象时,偏向锁会首先升级为轻量级锁,轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场 合,就会导致轻量级锁膨胀为重量级锁。
总结:当多线程竞争不是很激烈(激烈意思就是其它线程不用等很久,比如说一两秒那种),就会是轻量级锁,否则,就升级为重量级锁。

1.3、重量级锁

是OS的一个mutex锁,非常消耗性能,也是一种互斥锁,由操作系统维护。

2、锁的粗化

锁粗化是指将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如果一系列连续的加锁解锁操作,想必程序的性能会下降很多,这时候我们就可以把这些操作放在一个同步块里面进行总的加锁和解锁(前提是这些操作可以放在一起执行的)。

3、锁的消除

逃逸分析:分析当前的锁对象会不会逃出当前线程的控制范围,比如说,方法里面的局部变量,就不会逃出当前线程的范围,当前线程栈销毁后,就会销毁那个局部变量。

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时 进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以 节省毫无意义的请求锁时间,
总结:锁消除是Jvm通过上下文的扫描之后,通过逃逸分析这个锁对象不会有公共资源的竞争,就会进行锁的消除。

4、自旋锁

简单理解就是轻量级锁失败后,升级为重量级锁过程的一种优化方法

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值