自旋、偏向锁、轻量级锁、重量级锁区别

java中每个对象都可作为锁,锁有四种级别,按照量级从轻到重分为:无锁、偏向锁、轻量级锁、重量级锁。并且锁只能升级不能降级。

在讲这三个锁之前,我先给大家讲清楚自旋和对象头的概念。

自旋
现在假设有这么一个场景:有两个线程A,B在竞争一个锁,假设A拿到了,这个时候B被挂起阻塞,一直等待A释放了锁B才得到使用权。在操作系统中阻塞和唤醒是一个耗时操作,如果A在很短的时间内就释放了锁,当这个时间与阻塞唤醒比较起来更短的时候,我们将B挂起,其实不是一个最优的选择。 
自旋是指某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁。虽然CPU的时间被消耗了,但是比线程下文切换时间要少。这个时候使用自旋是划算的。 
如果是单核处理器,一般建议不要使用自旋锁。因为只有单个处理器,自旋占用的时间片使得代价很高。 
而偏向锁、轻量锁、重量锁也是一个锁围绕着如何使得程序运行的更加“划算”而进行改变的。

对象头
HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机的对象头(Object Header)包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。 
在32位的HotSpot虚拟机 中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志 位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。

偏向锁
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令。 
当只有一个线程去竞争锁的时候,我们不需要阻塞,也不需要自旋,因为只有一个线程在竞争,我们只要去判断该偏向锁中的ThreadID是否为当前线程即可。如果是就执行同步代码,不是就尝试使用CAS修改ThreadID,修改成功执行同步代码,不成功就将偏向锁升级成轻量锁。

轻量锁
获取轻量锁的过程与偏向锁不同,竞争锁的线程首先需要拷贝对象头中的Mark Word到帧栈的锁记录中。拷贝成功后使用CAS操作尝试将对象的Mark Word更新为指向当前线程的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁。如果更新失败,那么意味着有多个线程在竞争。 
当竞争线程尝试占用轻量级锁失败多次之后(使用自旋)轻量级锁就会膨胀为重量级锁,重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。

重量锁
重量级锁的加锁、解锁过程和轻量级锁差不多,区别是:竞争失败后,线程阻塞,释放锁后,唤醒阻塞的线程,不使用自旋锁,不会那么消耗CPU,所以重量级锁适合用在同步块执行时间长的情况下。

基础知识之一:锁的类型
锁从宏观上分类,分为悲观锁与乐观锁。

乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。

java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。

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

如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。
synchronized会导致争用不到锁的线程进入阻塞状态,所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。

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

重量级锁Synchronized

在JDK1.5之前都是使用synchronized关键字保证同步的,Synchronized的作用相信大家都已经非常熟悉了;

它可以把任意一个非NULL的对象当作锁。

作用于方法时,锁住的是对象的实例(this);
当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。
Synchronized的实现
实现如下图所示;

它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。

Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;

Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;

Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;

OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;

Owner:当前已经获取到所资源的线程被称为Owner;

!Owner:当前释放锁的线程。

JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将一部分线程移动到EntryList中作为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。

OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。

处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。

Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,如果获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。

Synchronized的底层原理

对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象。

实现的原理:

synchronized是基于Monitor来实现同步的。
Monitor从两个方面来支持线程之间的同步:互斥执行、协作

  • 1、Java 使用对象锁 ( 使用 synchronized 获得对象锁 ) 保证工作在共享的数据集上的线程互斥执行。
  • 2、使用 notify/notifyAll/wait 方法来协同不同线程之间的工作。
  • 3、Class和Object都关联了一个Monitor。

Monitor 的工作机理

  • 1:线程进入同步方法中。
  • 2:为了继续执行临界区代码,线程必须获取 Monitor 锁。如果获取锁成功,将成为该监视者对象的拥有者。任一时刻内,监视者对象只属于一个活动线程(The Owner)
  • 3:拥有监视者对象的线程可以调用 wait() 进入等待集合(Wait Set),同时释放监视锁,进入等待状态。
  • 4:其他线程调用 notify() / notifyAll() 接口唤醒等待集合中的线程,这些等待的线程需要重新获取监视锁后才能执行 wait() 之后的代码。
  • 5:同步方法执行完毕了,线程退出临界区,并释放监视锁

synchronized的锁优化

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

参考:https://blog.csdn.net/Cs_hnu_scw/article/details/79635874 
参考:https://blog.csdn.net/zqz_zqz/article/details/70233767 
参考:https://blog.csdn.net/makecontral/article/details/79435933

(0)

相关推荐