synchronized高级篇

2018-02-04  本文已影响0人  忆往昔君

上一篇我们简单的了解了一下java中synchronized的用法,但是很多老司机都已经用的很熟练了,就问我:锁的状态记录在哪?class级的锁保存在哪?那么锁升级又是什么?我感觉有必要再来一次学习研究。

一、synchronized的原理:

        上一篇看到class编译之后的文件每个代码块前后都会有monitorenter和monitorexit,这就是jvm加锁和解锁的指令,那它是对谁操作的呢?其实JVM会为每个对象分配一个monitor,而同时只能有一个线程可以获得该对象monitor的所有权。在线程进入时通过monitorenter尝试取得对象monitor所有权,退出时通过monitorexit释放对象monitor所有权。多线程锁竞争都需要通过CAS操作进行获取锁和释放锁。所以jvm锁的实现离不开CAS操作(下文有CAS的介绍)。

对象头信息(Mark Word)

        锁的记录保存在哪?synchronized用的锁是存在对象头里的。对象头里边存放很多关于该对象的自身运行时数据。简称“Mark Word”。其中需要注意的是,jvm中非数组对象用2个word(字宽),数组对象用3个word(额外的存储数组长度)。

二、锁的升级和对比

        java1.6之后对synchronized 性能上的优化,引入了轻量级锁和偏向锁来减少性能消耗,所以不完全认为它是一个重量级锁。1.6中锁有四种状态,分别是无锁,轻量级锁(自旋)、偏向锁、重量级锁的关系: 偏向锁->轻量级锁->重量级锁,而且锁升级之后不可降级。

偏向锁

        偏向锁:在无竞争的情况下把整个同步都消除掉,CAS操作都不做了。

简单的讲,就是在锁对象的对象头中有个ThreaddId字段,这个字段如果是空的,第一次获取锁的时候,就将自身的ThreadId写入到锁的ThreadId字段内,将锁头内的是否偏向锁的状态置为1(上面的标识位),这样下次获取锁的时候,直接检查ThreadId是否和自身线程Id一致,如果一致,则认为当前线程已经获取了锁,这样就提高了效率。但是偏向锁也有一个问题,就是当锁有竞争关系的时候,需要解除偏向锁,使锁进入竞争的状态(jvm偏向锁默认是开启的)。

轻量级锁

  轻量级锁:在无竞争的情况下使用CAS操作对象头,将替换线程ID,和指向锁记录的指针。成功则获得锁,失败则自旋等待获得锁。

机制:每个锁都关联一个请求计数器和一个占有他的线程,当请求计数器为0时,这个锁可以被认为是unhled的,当一个线程请求一个unheld的锁时,JVM记录锁的拥有者,并把锁的请求计数加1,如果同一个线程再次请求这个锁时,请求计数器就会增加,当该线程退出syncronized块时,计数器减1,当计数器为0时,锁被释放(这就保证了锁是可重入的,不会发生死锁的情况)。

自旋锁

    自旋锁:线程处于等待获取锁的过程是阻塞状态,阻塞操作由操作系统完成(在Linxu下通过pthread_mutex_lock函数)。线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。缓解上述问题的办法便是自旋,其原理是:当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程可以稍微等一等(自旋),在Owner线程释放锁后,争用线程可能会立即得到锁,从而避免了系统阻塞。

自旋锁优缺点

        自选锁通过占用CPU时间来避免CUP的用户态和内核态之间的切换,很显然,自旋在多处理器上才有意义,JDK5中引入默认自旋次数为10(用户可以通过-XX:PreBlockSpin进行修改), JDK6中更是引入了自适应自旋(简单来说如果自旋成功概率高,就会允许等待更长的时间(如100次自旋),如果失败率很高,就不做自旋,直接升级为重量级锁。显然,自旋的周期选择显得非常重要,但这与操作系统、硬件体系、系统的负载等诸多场景相关,很难选择,如果选择不当,不但性能得不到提高,可能还会下降,因此大家普遍认为自旋锁不具有扩展性。

重量级锁

        重量级锁:上面的自旋失败,没有获取到对象锁,则进入重锁,等待等待之前线程执行完成并唤醒自己。

三、锁的升级过程:

        第一步:检查对象头信息里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁” ;

        第二步:如果MarkWord不是自己的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空;

        第三步:两个线程都把对象的HashCode复制到自己新建的用于存储锁的记录空间,接着开始通过CAS操作,把共享对象的MarKword的内容修改为自己新建的记录空间的地址的方式竞争MarkWord;

        第四步:第三步中成功执行CAS的获得资源,失败的则进入自旋 ;

        第五步:自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败;

        第六步:进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。

四、CAS是什么?

        CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是CPU某个时刻比较两个值是否相等,JVM中通过锁和循环CAS操作实现原子操作。简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。CAS操作是原子性的,JDK中大量使用了CAS来更新数据而防止加锁(synchronized )来保持原子更新,但是系统CPU的CAS指令也是存在问题的。

        1.ABA问题。两个时刻比较值都会存在ABA问题,原来是A,中间变成B,又变回A,CAS检测认为值没有发生变化,但实际上确实发生变化了。Java1.5开始提供了Atomic包,用AtomicStampedReference来解决ABA问题。基本思路是增加版本号,修改的当前值和逾期值是否一致。AtomicStampedReference是通过当前引用和逾期的引用是否相等,来进行CAS操作。

        2.循环时间长开销大。自旋CAS长时间不成功,会给CUP带来巨大的执行开销。

        3.只能保证一个共享变量的原子操作。但是AtomicReference支持将多个变量合并进行CAs操作。

        总结:JVM中除了偏向锁,其他锁的方式都是用到了循环CAS操作进行加锁和锁释放。

五、AQS是什么?

        java.util.concurrent.locks并发包下边的AbstractQueuedSynchronizer,简称AQS将。AQS将线程封装到一个Node里面,并维护一个CHL Node FIFO队列,它是一个非阻塞的FIFO队列,也就是说在并发条件下往此队列做插入或移除操作不会阻塞,是通过自旋锁和CAS保证节点插入和移除的原子性,实现无锁快速插入。其实AbstractQueuedSynchronizer主要就是维护了一个state属性、一个FIFO队列和线程的阻塞与解除阻塞操作。state表示同步状态,它的类型为32位整型,对state的更新必须要保证原子性。这里的队列是一个双向链表,每个节点里面都有一个prev和next,它们分别是前一个节点和后一个节点的引用。需要注意的是此双向链表除了链头其他每个节点内部都包含一个线程,而链头可以理解为一个空节点。

        以上的知识点,是通过网络博客学习以及《Java并发编程艺术》的总结和整理,如理解有误或者错误,欢迎指正。

上一篇下一篇

猜你喜欢

热点阅读