线程安全(上)--完全搞懂synchronized(从偏向锁到重量级锁)

接触过线程安全的同窗想必都使用过synchronized这个关键字,在java同步代码快中,synchronized的使用方式无非有两个:java

  1. 经过对一个对象进行加锁来实现同步,以下面代码。数组

synchronized(lockObject){    //代码}
  1. 对一个方法进行synchronized声明,进而对一个方法进行加锁来实现同步。以下面代码安全

public synchornized void test(){    //代码}

但这里须要指出的是,不管是对一个对象进行加锁仍是对一个方法进行加锁,实际上,都是对对象进行加锁优化

也就是说,对于方式2,实际上虚拟机会根据synchronized修饰的是实例方法仍是类方法,去取对应的实例对象或者Class对象来进行加锁。spa

对于synchronized这个关键字,可能以前你们有听过,他是一个重量级锁,开销很大,建议你们少用点。但你们可能也据说过,但到了jdk1.6以后,该关键字被进行了不少的优化,已经不像之前那样不给力了,建议你们多使用。操作系统

那么它是进行了什么样的优化,才使得synchronized又深得人心呢?为什么重量级锁开销就大呢?线程

想必你们也都据说太轻量级锁,重量级锁,自旋锁,自适应自旋锁,偏向锁等等,他们都有哪些区别呢?
刚才和你们说,锁是加在对象上的,那么一个线程是如何知道这个对象被加了锁呢?又是如何知道它加的是什么类型的锁呢?3d

基于这些问题,下面我讲一步一步讲解synchronized是如何被优化的,是如何从偏向锁到重量级锁的。指针

锁对象

刚才咱们说,锁其实是加在对象上的,那么被加了锁的对象咱们称之为锁对象,在java中,任何一个对象都能成为锁对象。
为了让你们更好着理解虚拟机是如何知道这个对象就是一个锁对象的,咱们下面简单介绍一下java中一个对象的结构。
java对象在内存中的存储结构主要有一下三个部分:对象

  1. 对象头

  2. 实例数据

  3. 填充数据
    这里强调一下,对象头里的数据主要是一些运行时的数据。
    其简单的结构以下

长度 内容 说明
32/64bit Mark Work hashCode,GC分代年龄,锁信息
32/64bit Class Metadata Address 指向对象类型数据的指针
32/64bit Array Length 数组的长度(当对象为数组时)

从该表格中咱们能够看到,对象中关于锁的信息是存在Markword里的。
咱们来看一段代码

LockObject lockObject = new LockObject();//随便建立一个对象synchronized(lockObject){    //代码}

当咱们建立一个对象LockObject时,该对象的部分Markword关键数据以下。

bit fields 是否偏向锁 锁标志位
hash 0 01

从图中能够看出,偏向锁的标志位是“01”,状态是“0”,表示该对象尚未被加上偏向锁。(“1”是表示被加上偏向锁)。该对象被建立出来的那一刻,就有了偏向锁的标志位,这也说明了全部对象都是可偏向的,但全部对象的状态都为“0”,也同时说明全部被建立的对象的偏向锁并无生效。

偏向锁

不过,当线程执行到临界区(critical section)时,此时会利用CAS(Compare and Swap)操做,将线程ID插入到Markword中,同时修改偏向锁的标志位。

所谓临界区,就是只容许一个线程进去执行操做的区域,即同步代码块。CAS是一个原子性操做

此时的Mark word的结构信息以下:

bit fields   是否偏向锁 锁标志位
threadId epoch 1 01

此时偏向锁的状态为“1”,说明对象的偏向锁生效了,同时也能够看到,哪一个线程得到了该对象的锁。

那么,什么是偏向锁?

偏向锁是jdk1.6引入的一项锁优化,其中的“偏”是偏爱的偏。它的意思就是说,这个锁会偏向于第一个得到它的线程,在接下来的执行过程当中,假如该锁没有被其余线程所获取,没有其余线程来竞争该锁,那么持有偏向锁的线程将永远不须要进行同步操做。
也就是说:
在此线程以后的执行过程当中,若是再次进入或者退出同一段同步块代码,并再也不须要去进行加锁或者解锁操做,而是会作如下的步骤:

  1. Load-and-test,也就是简单判断一下当前线程id是否与Markword当中的线程id是否一致.

  2. 若是一致,则说明此线程已经成功得到了锁,继续执行下面的代码.

  3. 若是不一致,则要检查一下对象是否仍是可偏向,即“是否偏向锁”标志位的值。

  4. 若是还未偏向,则利用CAS操做来竞争锁,也便是第一次获取锁时的操做。

若是此对象已经偏向了,而且不是偏向本身,则说明存在了竞争。此时可能就要根据另外线程的状况,多是从新偏向,也有多是作偏向撤销,但大部分状况下就是升级成轻量级锁了。
能够看出,偏向锁是针对于一个线程而言的,线程得到锁以后就不会再有解锁等操做了,这样能够省略不少开销。假若有两个线程来竞争该锁话,那么偏向锁就失效了,进而升级成轻量级锁了。
为何要这样作呢?由于经验代表,其实大部分状况下,都会是同一个线程进入同一块同步代码块的。这也是为何会有偏向锁出现的缘由。
在Jdk1.6中,偏向锁的开关是默认开启的,适用于只有一个线程访问同步块的场景。

锁膨胀

刚才说了,当出现有两个线程来竞争锁的话,那么偏向锁就失效了,此时锁就会膨胀,升级为轻量级锁。这也是咱们常常所说的锁膨胀

锁撤销

因为偏向锁失效了,那么接下来就得把该锁撤销,锁撤销的开销花费仍是挺大的,其大概的过程以下:

  1. 在一个安全点中止拥有锁的线程。

  2. 遍历线程栈,若是存在锁记录的话,须要修复锁记录和Markword,使其变成无锁状态。

  3. 唤醒当前线程,将当前锁升级成轻量级锁。
    因此,若是某些同步代码块大多数状况下都是有两个及以上的线程竞争的话,那么偏向锁就会是一种累赘,对于这种状况,咱们能够一开始就把偏向锁这个默认功能给关闭

轻量级锁

锁撤销升级为轻量级锁以后,那么对象的Markword也会进行相应的的变化。下面先简单描述下锁撤销以后,升级为轻量级锁的过程:

  1. 线程在本身的栈桢中建立锁记录 LockRecord。

  2. 将锁对象的对象头中的MarkWord复制到线程的刚刚建立的锁记录中。

  3. 将锁记录中的Owner指针指向锁对象。

  4. 将锁对象的对象头的MarkWord替换为指向锁记录的指针。

对应的图描述以下(图来自周志明深刻java虚拟机)

以后Markwork以下:

bit fields 锁标志位
指向LockRecord的指针 00

注:锁标志位”00”表示轻量级锁
轻量级锁主要有两种

  1. 自旋锁

  2. 自适应自旋锁

自旋锁

所谓自旋,就是指当有另一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个得到锁的线程释放锁以后,这个线程就能够立刻得到锁的。
注意,锁在原地循环的时候,是会消耗cpu的,就至关于在执行一个啥也没有的for循环。
因此,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就可以得到锁了。
经验代表,大部分同步代码块执行的时间都是很短很短的,也正是基于这个缘由,才有了轻量级锁这么个东西。

自旋锁的一些问题

  1. 若是同步代码块执行的很慢,须要消耗大量的时间,那么这个时侯,其余线程在原地等待空消耗cpu,这会让人很难受。

  2. 原本一个线程把锁释放以后,当前线程是可以得到锁的,可是假如这个时候有好几个线程都在竞争这个锁的话,那么有可能当前线程会获取不到锁,还得原地等待继续空循环消耗cup,甚至有可能一直获取不到锁。

基于这个问题,咱们必须给线程空循环设置一个次数,当线程超过了这个次数,咱们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁
默认状况下,自旋的次数为10次,用户能够经过-XX:PreBlockSpin来进行更改。

自旋锁是在JDK1.4.2的时候引入的

自适应自旋锁

所谓自适应自旋锁就是线程空循环等待的自旋次数并不是是固定的,而是会动态着根据实际状况来改变自旋等待的次数。
其大概原理是这样的:
假如一个线程1刚刚成功得到一个锁,当它把锁释放了以后,线程2得到该锁,而且线程2在运行的过程当中,此时线程1又想来得到该锁了,但线程2尚未释放该锁,因此线程1只能自旋等待,可是虚拟机认为,因为线程1刚刚得到过该锁,那么虚拟机以为线程1此次自旋也是颇有可能可以再次成功得到该锁的,因此会延长线程1自旋的次数
另外,若是对于某一个锁,一个线程自旋以后,不多成功得到该锁,那么之后这个线程要获取该锁时,是有可能直接忽略掉自旋过程,直接升级为重量级锁的,以避免空循环等待浪费资源。

轻量级锁也被称为非阻塞同步乐观锁,由于这个过程并无把线程阻塞挂起,而是让线程空循环等待,串行执行。

重量级锁

轻量级锁膨胀以后,就升级为重量级锁了。重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操做系统的MutexLock(互斥锁)来实现的,因此重量级锁也被成为互斥锁
当轻量级所通过锁撤销等步骤升级为重量级锁以后,它的Markword部分数据大致以下

bit fields 锁标志位
指向Mutex的指针 10

为何说重量级锁开销大呢

主要是,当系统检查到锁是重量级锁以后,会把等待想要得到锁的线程进行阻塞,被阻塞的线程不会消耗cup。可是阻塞或者唤醒一个线程时,都须要操做系统来帮忙,这就须要从用户态转换到内核态,而转换状态是须要消耗不少时间的,有可能比用户执行代码的时间还要长。
这就是说为何重量级线程开销很大的。

互斥锁(重量级锁)也称为阻塞同步悲观锁

总结

经过上面的分析,咱们知道了为何synchronized关键字为什么又深得人心,也知道了锁的演变过程。
也就是说,synchronized关键字并不是一开始就该对象加上重量级锁,也是从偏向锁,轻量级锁,再到重量级锁的过程。
这个过程也告诉咱们,假如咱们一开始就知道某个同步代码块的竞争很激烈、很慢的话,那么咱们一开始就应该使用重量级锁了,从而省掉一些锁转换的开销。

以下图锁的变化过程:

相关文章
相关标签/搜索