synchronized以及各种锁

2019-02-18  本文已影响0人  秋笙fine

synchronized是java的一个关键字,它能够将代码块(方法)锁起来。
synchronized是一种互斥锁,一次只能允许一个线程进入被锁住的代码块。
synchronized是一种内置锁/监视器锁。研究过对象头的话就会知道,每个对象都有一个内置锁,synchronized就是使用对象的内置锁来将代码块锁定的。

synchronized优化后,从偏向锁->轻量级锁。不再是原来直接的重量级锁。

使用synchronized保证了线程的原子性和可见性,因为synchronized使用了对象的内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,确保了并发情况下的线程安全。


1550480744110.jpg

如图,引用数据类型/实例对象都会有对象头,对象头的对象标记(mark Copy)用来存储对象的哈希码,GC标记,GC次数,同步锁标记(monitor锁是否被持有),是否偏向锁。
此外,Monitor是线程私有的数据结构,对象头的同步锁标记就是指向其起始地址,monitor锁中有一个owner字段存放拥有该锁的唯一线程标识,所以synchronized使用了对象的内置锁,对象头存储了内置锁信息,monitor作为线程私有数据结构,其owner字段,记录了拥有该锁的线程ID,原理为
原理:

  1. 当对象的monitor内置锁进入数为0时,线程进入,成为锁持有者,monitor记录下ID,将进入数设置为1,
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数+1
  3. 如果其他线程已经占用了monitor内置锁,则该线程进入阻塞状态,直到monitor的进入数位0,可重新尝试获取monitor的所有权。
    以下是monitor的父类ObjectMonitor的状态图:
1550493118428.jpg

有资格的线程进入EntryList,如果monitor内置锁进入数不为0,线程进入阻塞队列,直到为0,则称为owner,锁持有者。

synchronized是悲观锁,是同步阻塞的。CAS是乐观锁,是同步非阻塞的。

JDK1.6对原来的synchronized重量级锁进行了优化,如自旋锁,适应性自旋锁,锁消除,锁粗话,偏向锁,轻量级锁来减少锁操作的开销。

自旋锁:锁住对象后,如果别的线程在EntryList请求,并不会立刻进入阻塞队列(BlockingQueue),而是进行一段无意义的循环(自旋)。
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
何谓自旋锁?
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。怎么等待呢?执行一段无意义的循环即可(自旋)。
自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整;
如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。

适应自旋锁:如果自旋成功,自动增加自旋次数。
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

锁消除:JVM检测到不可能存在共享数据竞争,而消除这个锁。

锁粗化:概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。

轻量级锁
引入轻量级锁的主要目的是在多没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:
获取锁

  1. 判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3);
  2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指正,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3);
  3. 判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;

释放锁
轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  1. 取出在获取轻量级锁保存在Displaced Mark Word中的数据;
  2. 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行(3);
  3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢;

下图是轻量级锁的获取和释放过程

偏向锁
引入偏向锁主要目的是:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。上面提到了轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的。那么偏向锁是如何来减少不必要的CAS操作呢?我们可以查看Mark work的结构就明白了。只需要检查是否为偏向锁、锁标识为以及ThreadID即可,处理流程如下:
获取锁

  1. 检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
  2. 若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
  3. 如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);
  4. 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
  5. 执行同步代码块

释放锁
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:

  1. 暂停拥有偏向锁的线程,判断锁对象石是否还处于被锁定状态;
  2. 撤销偏向苏,恢复到无锁状态(01)或者轻量级锁的状态;

下图是偏向锁的获取和释放流程

重量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

公平锁:线程获得锁的顺序的顺序是先到先得原则,晚来的就必须通过一个CAS,从Entry List到Waiting Set队列末尾去中。

非公平锁:在此场景下,每个线程都要先竞争锁,在竞争失败或当前已被加锁的前提下才会被塞入等待队列,在这种实现下,后到的线程有可能无需进入等待队列直接竞争到锁。

机制:synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁,其它线程只能通过阻塞进入waiting set,而没有自旋锁,轻量级锁等的引入。这也是在JDK1.6未被优化前,引入Lock锁的优势。Lock锁是乐观锁,每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS。

上一篇下一篇

猜你喜欢

热点阅读