Synchonized锁的竞争与升级

Synchonized的实现

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁
synchronized用的锁是存在Java对象头里的,如果对象是数组类型,则虚拟机用3个字宽 (Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。

代码块同步是使用monitorenter 和monitorexit指令实现的

如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

同步方法是使用ACC_SYNCHRONIZED

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。
在这里插入图片描述
32位JVM 的Mark Word的默认存储结构如表2-3所示
在这里插入图片描述

Mark Word的存储结构

在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变 化为存储以下4种数据,如表2-4所示。
在这里插入图片描述

在64位虚拟机下,Mark Word是64bit大小的,,其存储结构如表2-5所示
在这里插入图片描述

对象头中Mark Word与线程中Lock Record

在线程进入同步代码块的时候,如果此同步对象没有被锁定,即它的锁标志位是01,则虚拟机首先在当前线程的栈中创建我们称之为“锁记录(Lock Record)”的空间,用于存储锁对象的Mark Word的拷贝,官方把这个拷贝称为Displaced Mark Word。整个Mark Word及其拷贝至关重要

在这里插入图片描述
锁记录(Lock Record)是线程私有的,每一个被锁住对象的Mark Word都会和一个Lock Record关联(对象头的MarkWord中的Lock Word指向Lock Record的起始地址)

监视器(Monitor)

每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者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对象 )
当多个线程同时访问一段同步代码时:

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

Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向)
监视器Monitor有两种同步方式:互斥与协作
互斥:监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。
协作:JVM通过Object类的wait方法来使自己等待,在调用wait方法后,该线程会释放它持有的监视器,直到其他线程通知它才有执行的机会。一个线程调用notify方法通知在等待的线程,这个等待的线程并不会马上执行,而是要通知线程释放监视器后,它重新获取监视器才有执行的机会。如果刚好唤醒的这个线程需要的监视器被其他线程抢占,那么这个线程会继续等待。Object类中的notifyAll方法可以解决这个问题,它可以唤醒所有等待的线程,总有一个线程执行。
在这里插入图片描述

当一个线程释放监视器时,在入口区和等待区的等待线程都会去竞争监视器,如果入口区的线程赢了,会从2号门进入;如果等待区的线程赢了会从4号门进入。只有通过3号门才能进入等待区,在等待区中的线程只有通过4号门才能退出等待区,也就是说一个线程只有在持有监视器时才能执行wait操作,处于等待的线程只有再次获得监视器才能退出等待状态。

锁的状态

级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态
注意:锁只能升级,不能降级

偏向锁

1、偏向锁的由来

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

2、偏向锁的判定

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程的ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程ID的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,也不能断定此线程没有获取到锁,再去判断偏向锁的标志位是否为1,为1,是偏向锁,则尝试用CAS将对象头的偏向锁指向当前线程(换句话说,就是存储当前线程的ID),如果为0,则证明此偏向锁还没有被使用,就使用CAS进行锁的竞争。

3、偏向锁的撤销
偏向锁使用了一种等到竞争出现就会释放锁的机制,所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程就会释放锁。

4、使用场景
偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下(即线程A尚未执行完同步代码块,线程B发起了申请锁的申请),则一定会转化为轻量级锁或者重量级锁,会导致大量的锁撤销,造成 性能消耗,

自旋锁

前世今生

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。

定义

自旋锁,就是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。

应用场景

首先临界区要足够小,锁占用时间很短,如果临界区大,锁占用时间长,那么就会长时间的循环等待,白白浪费cpu的资源。所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间或次数仍然没有获取到锁,则该线程应该被挂起。

在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。

适应性自旋锁

前世今生
假设我自选了10次,没有获取到锁,进入挂起,这时,也就是第十一次,锁被释放了,这不气得吐血,于是就引入了自适应的自旋锁。

何为自适应
线程如果自旋成功了,那么下次自旋的时候,次数增加一点,因为虚拟机认为既然上次成功了,那么这次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候,自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明.

锁消除

见名知义,就是把锁给取消,加了锁,又把锁给取消,这不是前后矛盾吗?这是由于jvm检测到不可能出现共享数据的竞争,所以就把锁取消,从而提高效率,比如下面的这段:

public void vectorTest(){
    Vector<String> vector = new Vector<String>();
    for(int i = 0 ; i < 10 ; i++){
        vector.add(i + "");
    }
    System.out.println(vector);
}

我们知道vector里面的很多方法是synchronized修饰的,是线程安全的。在运行这个方法时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。而且,局部变量存在虚拟机栈中,是线程私有的。从这个角度分析,也更本不用加锁。

锁粗化

如果是连续的add操作,岂不是重复的加锁与释放锁,可能会导致不必要的性能损耗

private Vector vector;//伪代码
public void vectorTest(){
    for(int i = 0 ; i < 10 ; i++){
        vector.add(i + "");
    }
}

于是乎,就想到扩大锁的范围,在for的外面进行加锁。这便是锁粗化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗罗的1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值