《Java并发编程的艺术》学习 ——synchornized

一、synchornized 的实现原理和应用

1.1 Java的对象头

在上面介绍的线程要访问同步方法、代码块的时候,那首先是要获取到锁。那锁到底是什么有存储在什么地方呢?这个和java对象的组成有关。java对象分成3块区域:

  • 对象头
    • 储存对象的运行时数据(Mark Word)
      • 哈希码、锁的状态、持有的锁、GC分代年纪,对象分代年纪、偏向线程ID、偏向时间戳。Mark Word的结构不是固定的,会随着锁的状态变化而变化,这样做的目的也是为了节省空间。
    • 类型指针
      • 指向对象的类元数据的指针,虚拟机通过这个指针来确定这个对象时哪个类的实例
    • 数组长度(只有数组才有)
      • 只有对象才有
  • 实例数据
    • 储存着对象的实际数据,也就是我们在程序中定义的各种类型的字段内容。
  • 对齐填充
    • 虚拟机的对齐方式为8字节对齐,也就是一个对象必须为8个字节的整数倍,如果不是,则通过这个对齐来占位填充。

在运行期间,Java对象头的Mark Word里储存的数据是会随着锁的标志位的变化而变化的。

1.2 Java对象的锁膨胀的过程

Java1.6 之后。为了减少锁的获取和释放带来的性能消耗。加入了偏向锁和轻量级锁。java中,锁的状态一共有4种,级别从高到低。分别是无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。状态随着竞争的情况逐渐升级。锁一旦升级就不能降级,意味着偏向锁升级成轻量级锁之后就不能降级为偏向锁了。不能降级的目的就是为了提高获取锁和释放锁的效率。

一张图读懂synchornized锁膨胀的过程

1.2.1 偏向锁

HotSpot的作者研究发现,大多数情况下,不但没有锁竞争,而且总是由同一个线程多次获取到锁。因为引入了偏向锁。当一个线程访问同步块的时候,通过CAS操作获取到锁的时候,会在对象头和栈帧中的锁记录里面存入偏向锁的线程ID。后面该线程进入和退出的时候不需要在进行CAS操作,只需要比较线程对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果成功则直接获取到锁,如果失败,则比较对象头的标识是不是1(表示当前是偏向锁),如果是1 则尝试使用CAS把对象头的偏向锁指向当前线程,否则使用CAS竞争。

  • 偏向锁的撤销
    • 当有其他线程来竞争锁的时候,持有偏向锁的线程才会进行撤销。
    • 这种撤销必须要等到全局安全点。
    • 具体的步骤
      • 暂停当前持有偏向锁的线程
      • 如果该线程不处于活动状态,则直接把对象头设置成无锁状态
      • 如果该线程仍然活着,拥有偏向锁的栈会被执行,栈中的锁记录和对象头的Mark Word要么偏向于其他线程,要么恢复或者标记对象不适合偏向锁。最终唤醒暂停的线程,升级成轻量级锁。
    • 具体的可以看上图
  • 偏向锁的关闭
    • 偏向锁在Java 6和Java 7中是默认开启的。但是在程序启动几秒后才机会
    • 可以同设置Jvm参数来关闭延迟 -XX:BiasedLockingStartupDelay=0
    • 也可以通过参数关闭 -XX:UseBiasedLocking=false 让程序默认进入到轻量级锁状态

1.2.2 轻量级锁

  • 轻量级锁的加锁
    在线程执行代码块的时候,Jvm会在当前线程的栈帧中创建用于储存锁记录的空间,并且把对象头中的
    MarkWord复制到锁记录中。然后线程尝试使用CAS将对象头中的MarkWord替换成指向锁记录的指针。如果成功,则当前线程获取到锁,如果失败,则表示其他线程竞争成功,当前线程便通过尝试使用 自旋锁来获取锁。当时当自旋达到一定阀值的时候就会升级成重量级锁。

    • 自旋锁:当一个线程去请求某个锁的时候被其他锁占用了,不会马上进入等待队列。而是循环请求,因为很多时候锁会马上被释放,当然如果线程A自旋指定的时间还没有获得锁,仍然会被挂起。
    • 自适应自旋锁:自适应性自旋是自旋的升级、优化,自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。就是上一个线程很容易自旋成功了,那么下次自旋的次数就会变成。否则自旋的次数就减少,免的浪费资源。这就是jvm变聪明了。
  • 轻量级锁的解锁 (???)

    • 轻量级锁解锁的时候,会使用原子的CAS操作将Mark Word替换回到对象头,如果成功则表示没有竞争关系,如果失败,则表示锁存在竞争,锁就会膨胀成重量级锁。

1.2.3 重量级锁

使用了monitor的对象的就是重量级锁,因为monitor的实现依赖于底层操作系统的mutex互斥原语,而操作系统实现线程之间的切换的时候需要从用户态转到内核态,这个转成过程开销比较大。所以下面两种方式就是重量级锁

  • 同步方法的时候
    Jvm采用的是 ACC_synchoronized

标记符来实现的同步,这个是因为jvm在调用方法是会验证方法是不是有ACC——synchoronized 的标记符。如果设置了改标志,执行线程会线获取到monitor对象,然后执行方法,在该方法的运行期间,其他线程是无法获取到monitor对象的,只有执行完任务了才能获取,释放了monitor对象 才能进入到代码块。

  • 同步代码块的时候

对于同步代码块,是由 monitorenter和monitorexit 指令来实现的同步。monitorenter是获取monitor的所有权,mointorexit是释放monitor的所有权。

monitorenter的顺序是,

  • 获取到monitor的所有权,进入数+1
  • 如果该线程已经拥有了次方法的monitor,有重新进入到monitor 在进入数 +1 这个就是锁的重入
  • 其他线程进入到阻塞状态,直到monitor的进入数为0 在重新获取到monitor的所有权
    monitorexit 则表示该线程必须是montor的所有权。并把进入数减去1 知道为0 线程退出monitor。

这两个同步方式实际都是通过获取monitor和释放monitor来实现同步的,而monitor的实现依赖于底层操作系统的mutex互斥原语,而操作系统实现线程之间的切换的时候需要从用户态转到内核态,这个转成过程开销比较大。

1.2.4 锁的优缺点

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和非同步块只有纳秒级别的差距如果存在线程竞争,回带来额外的锁撤销消耗使用于一个线程访问同步块
轻量级锁竞争的线程不会阻塞,提高来线程的响应速度如果始终得不到锁竞争的线程,是使用自旋消耗CPU追求响应 同步块执行速度非常快
重量级锁线程竞争不使用自旋,不会消耗CPU线程阻塞,响应时间缓慢追求吞吐量,同步块执行速度较长

1.2.5 其他介绍

  • 重量级锁的实现是基于底层操作系统的mutex互斥原语来实现的,这个开销是很大的。所以Jvm对synchronized做了优化,jvm先利用 对象头实现锁的功能,如果线程的竞争过大则将锁升级到重量级锁,这也就是monitor对象。当然jvm对锁的优化还不仅仅只有这些,还引入了自旋锁和自适应自旋锁,锁消除、轻量级锁和锁消除。
  • 自旋锁:当一个线程去请求某个锁的时候被其他锁占用了,不会马上进入等待队列。而是循环请求,因为很多时候锁会马上被释放,当然如果线程A自旋指定的时间还没有获得锁,仍然会被挂起。
  • 自适应自旋锁:自适应性自旋是自旋的升级、优化,自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。就是上一个线程很容易自旋成功了,那么下次自旋的次数就会变成。否则自旋的次数就减少,免的浪费资源。这就是jvm变聪明了。
  • 锁消除:锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
  • 锁粗壮:需要让同步块的作用范围尽可能小,这样做的目的是为了使需要同步的操作数量尽可能小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
  • 轻量级锁:轻量级锁是相对于使用底层操作系统mutex互斥原语实现同步的重量级锁而言的,因为轻量级锁同步的实现是基于对象头的Mark Word
  • 偏向锁:就是消除在无竞争情况下的同步原语操作,进一步提高程序的性能。锁会偏向于第一个获得它的程序

二、synchornized 和 Lock的对比

java1.5之后 提供了lock同步锁,Lock锁的基本实现是通过乐观锁实现的。但是由于Lock锁也是阻塞之后被挂起,所以它依然是悲观锁。


从性能上说,在并发量不高、竞争不激烈的情况下,Synchronized同步锁是由于具有分级锁的优势,性能上和lock上差不多。但是在高负载、高并发的情况下,Synchronzied同步锁由于竞争激烈会升级到重量级的锁(synchronized的膨胀),性能没有Lock稳定。

Lock锁的实现原理

Lock锁是基于Java实现的锁,Lock是一个接口类,在常用的实现类中有是ReentrantLock、ReentrantReadWriteLock 他们都是依赖的赖于AbstractQueuedSynchronizer(AQS)类实现的

AQS类结构中包含一个 基于链表实现的等待队列(CLH队列),用于存储所有阻塞的线程,AQS中还有一个state变量,该变量对ReentrantLock来说表示锁状态。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值