Java并发学习(一)-synchronized及锁的学习

来源:互联网 发布:高中知识点总结软件 编辑:程序博客网 时间:2024/05/21 08:51

Index

这些天开始学习Java并发相关知识,依靠买的些书籍以及网上大佬文章,去解读Java在设计并发方面的思想以及领略魅力。

synchronized介绍

估计有一部分同学接触到并发与同步的时候,基本就是了解一些操作系统方面的并行与并发,例如生产者消费者,银行家算法以及死锁。而去接触从代码角度去防止死锁与并发,就是在接触synchronized关键字的时候开始的,我自己也是一样。
对于synchronized,大体的意思就是同一时间,只能有一个线程进入到其方法或代码块里面,但是JVM如何保证这一切的规则的呢?
下面我将从以下几个方面一一介绍。

CAS术语定义:
这里写图片描述

即比较并交换。

synchronized用法

synchronized保证,是建立在锁的获取的基础上的
Java中每一个对象可以作为锁,这是synchronized实现同步的基础。

synchronized主要可以有下列几种用法:

  • 普通的synchronized同步方法,锁是当前实例对象
  • 静态的synchronized同步方法,锁是当前类的Class对象
  • synchronized同步方法块,锁是括号里面的对象

JVM对同步方法和同步方法块的处理:

对于同步块,JVM会在class编译生成的指令上,在两个synchronized大括号分别插入monitorenter和monitorexit命令。此时以括号里面的对象作为synchronized作为锁。
而对于同步方法,会在javac编程后的字节码里面(invokevirtual)和返回(areturn),没有其他的指令来实现被synchronized修饰的方法。但是会在Class文件的方法表里面,将该方法的access_flags 字段的syncrhonized字段设为1。表示该方法是同步方法,并使用调用该方法的对象作为锁或者该方法所属的Class对象作为锁。

Java对象头

上文说过,每个Java对象都可以作为一把锁,为什么可以作为呢?每一个Java对象都会有一个对象头,首先看Java中一个对象在JVM中的结构:
这里写图片描述

一个实例对象有3部分:

  • Object Header:普通对象是2个字大小,数组对象是3个字大小,32位机子一个字是32位,根据锁的不同状态,存储的东西不同,下面解释。
  • Klass ptr:指向Class字节码在虚拟机内部对象的表示地址。
  • 实例对象的连续字段值。

再来仔细看看对象头(包括上图的klass ptr):
对象头

从上面可以知道,由于操作系统寻址的区别,32和64位上对象头大小有区别,并且当为数组的时候,会有一个字来表示数组的长度。

当对象从无锁到有锁时,对象头(Object Header)里面的字段也会有所不同,如下图给出了全部的规则:
首先是无锁状态:
这里写图片描述
再是开始有锁状态:

这里写图片描述

如上图,当处于无锁状态时,对象头里面存的数据就是作为一个普通对象的数据,有hashCode,gc标志,偏向锁标志,锁标志等。

而当有锁时,就只有后两位不变,其他的字段都会发生变化,从00,到10四种状态。例如轻量级锁中指向栈帧中锁记录的指针,就是存储了对应monitor record的地址,其他这里就不过多解释了,上图一目了然。

或许会有疑问,为什么要这样设计对象头呢?先在看看线程的私有结构monitor。

Monitor Record

一方面,对象可以作为一把锁,里面有指向栈帧锁记录的指针,也就是指向线程中的Monitor Record。
Monitor Record:是线程的私有数据结构,每个线程都有一个可用的Monitor Record队列(可以理解为线程可以拥有多把锁),同时还有个全局的可用的Monitor Record列表。
这样以来,每个被锁住的对象就可以和一个monitor record相关联,对象头中的LockWord(可以理解为上图中轻量级锁的指针)指向monitor record的起始地址。同时,monitor record中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

先看看monitor record的结构:
这里写图片描述

如上图,monitor record一共有6种字段:

  • Owner:存储线程的标识,初始时候为null,表示没有任何线程拥有,如果被拥有,则存储的为该线程的标识。
  • EntryQ:关联一个系统互斥锁(semaphore),用于阻塞试图锁住monitor record失败的线程。
  • RcThis:表示阻塞(blocked)或等待(waiting)在该monitor record上所有线程的个数。
  • Nest:用于实现重入锁的计数。
  • HashCode:用来保存从Object Header上拷贝而来的,其上原有的对象头数据,可能还包括gc。
  • Candidate:就像有一个继任者线程,来接手这个锁。用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(公平性的竞争,从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

锁的优化

这里先不讲几种锁的持有和放弃详细方式,先说说从Java6开始的,对synchronized进行的一系列优化概念。为什么要有优化呢?简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境)如果每次都调用Mutex Lock那么将严重的影响程序的性能。所以引入了锁的优化,如:自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少原有的进入核心态切换从而获取锁操作的开销。

自旋

当一个线程a持有对象o的锁时,线程b也想获取a的锁,b头一次尝试获取失败后,有了自旋锁的优化,此时b不会立刻被挂起,而是进行一段自旋(即循环)操作。这样会带来两个结果:

  • 成功获取了锁,这样就能减少cpu对a,b的切换,减少开销
  • 自旋了一段时间,还是没能够等待到a的结束。这样毫无疑问浪费了cpu的处理时间。

所以就出现了一种折中的方式,通过引入一个自旋的次数,来控制线程自旋的时间,如果某一个线程自旋多次后还没有能够获取到锁,那么它就会失去cpu从而被挂起。

自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;

自适应自旋

虽然自旋锁可以由开发者选择关闭或者开启,以及设定自旋的时常,但是这样终究不够高效,Java开发人员并不能接触到cpu,所以不能够确定自旋锁的次数,所以就出现了自适应自旋锁
在Java 6被引入,所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

锁消除

虽然在代码中可以用加锁的方式来保证数据同步,但是,加了锁的代码块,是否一定就会出现线程竞争的问题呢?当然不会。所以在就引入了锁消除这一优化手段,也就是JVM去除某一段代码的加锁和释放锁的代码。锁消除的依据是通过逃逸分析来确定的。所谓逃逸分析,简单讲就是看你代码块里面变量是否会被外部变量引用。

锁粗化

一般情况下,在使用同步锁的时候,编程的思路是需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么竞争的线程也能先拿到其他数据,并且在关键的并发冲突的代码时间时间就会减少很多。
但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
试想在这样一种情况,一个循环,循环里面的每个操作都要进行加锁释放锁,这样就会引起很大的不必要的开销,所以JVM就会将锁合并,类似与将锁加到循环体之外。

锁的种类

Java6为了减少获得锁和释放锁带来的优化,引入了“偏向锁”和“轻量级锁”。Java6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。减少每次加锁释放锁操作都需要切换到核心态的消耗。

偏向锁

偏向锁是建立在这样一种基础上,多线程竞争少,并且锁总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

  • 当一个线程访问同步块并获取锁时,会在对象头栈帧中的锁记录(Monitor Record)里存储锁偏向的线程ID。(记录)
  • 以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。(测试)
  • 如果测试成功,表示线程已经获得了锁(即这种是偏向锁)
  • 如果测试失败,(获取失败,查看Mark Word)
  • 则需要再测试下Mark Word中偏向锁的标识是否设置成1
  • 如果没有设置,则使用CAS竞争锁(说明已经不是偏向锁了)
  • 如果设置了(是偏向锁),则尝试使用CAS将对象头的偏向锁指向当前线程(即转化为轻量级锁)。

转为为轻量级锁的过程(inflate):
当前线程执行CAS获取偏向锁失败(这一步是偏向锁的关键),表示在该锁对象上存在竞争并且这个时候另外一个线程获得偏向锁所有权。当到达全局安全点(safepoint)时(不是立刻)获得偏向锁的线程被挂起,并从偏向锁所有者的私有Monitor Record列表中获取一个空闲的记录,并将Object设置LightWeight Lock状态并且Mark Word中的LockRecord指向刚才持有偏向锁线程的Monitor record,并且更改标志位为00。最后被阻塞在安全点的线程被释放,进入到轻量级锁的执行路径中,同时被撤销偏向锁的线程继续往下执行同步代码。

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

  • 偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行)。
  • 它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着。
  • 如果线程处于不活动状态,则将对象头设置成无锁状态。
  • 如果线程仍然活着,拥有偏向锁的栈帧会被执行,遍历偏向对象的锁记录。
  • 栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁。
  • 最后唤醒暂停的线程(通过安全点)。

轻量级锁

相对于前面的偏向锁,轻量级锁则使用了处理monitorenter以及monitorexit则使用CAS进行竞争,而CAS并不涉及操作系统层面的用户状态切换及加锁(Mutex Lock)。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒(重量级锁
和偏向锁的差别,轻量级锁整个对象头就只有monitor record地址以及标记字段。

一个线程能够通过两种方式锁住一个对象

  • 1、通过膨胀一个处于无锁状态(状态位01)的对象获得该对象的锁;
  • 2、对象已经处于膨胀状态(状态位00)但LockWord指向的monitor record的Owner字段为NULL,则可以直接通过CAS原子指令尝试将Owner设置为自己的标识来获得锁。

轻量级锁加锁:

  • 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录(Monitor Record)的空间,用于保存获取的锁。
  • 并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。
  • 然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。
  • 如果对象未被加锁,则复制并让MarkWord指向自己的Monitor Record即可,对象已被标识为锁定(inflate),Owner标识为自己,则重入锁(reentrant),直接将锁记录的nest+1即可。否则开始CAS竞争。
  • 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
  • 仍失败则开始准备进入阻塞状态,首先将rfThis的值原子性的加1,由于在加1的过程中可能会被其他线程破坏Object和monitor record之间的关联,所以在原子性加1后需要再进行一次比较以确保LockWord的值没有被改变,当发现被改变后则要重新进行monitorenter过程。同时再一次观察Owner是否为NULL,如果是则调用CAS参与竞争锁,锁竞争失败则进入到阻塞状态。

轻量级锁释放:
先做准备工作,最后自己锁都释放了(包括nest的重入锁),才进行CAS替换。

  • 首先检查该对象是否处于膨胀状态并且该线程是这个锁的拥有者,如果发现不对则抛出异常;
  • 检查Nest字段是否大于1,如果大于1则简单的将Nest减1并继续拥有锁,如果等于1,则进入到下一步;
  • 检查rfThis是否大于0,设置Owner为NULL然后唤醒一个正在阻塞或等待的线程再一次试图获取锁,如果等于0则进入到下一步;
  • 轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

参考:
周志明:《深入理解Java虚拟机》
聊聊并发-synchronized
逃逸分析
synchronized关键字及实现细节
深入分析synchronized实现原理

原创粉丝点击