锁与并发

来源:互联网 发布:淘宝hd登录失败 编辑:程序博客网 时间:2024/06/05 07:33
我们在开发中为了保证多线程环境下程序正常运行,经常会与锁打交道。随着锁使用的越来越频繁,虚拟机也在锁的实现上做了很多优化,接下来我们了解下虚拟机对锁都做了哪些优化。
一.锁的基本概念和实现
锁是多线程开发的重要工具,基本作用使保护临界区资源不会同时被多个线程访问而受到破坏。通过锁,可以让多个线程排队,顺序访问临界区的对象,使对象的状态总是保持一致,这就是锁的作用。
1.对象头和锁
在虚拟机中每个对象都有一个对象头,用于保存对象的信息。对象头中有一个称为Mark Word的部分,它是实现锁的关键。它是一个多功能的数据区,可以存放对象的哈希值,对象年龄,锁的指针等信息。一个对象是否占用锁,占用哪个锁,就记录在这个Mark Word中。
以32位系统为例,Mark Word中有25位比特表示对象的哈希值,4位比特表示对象的年龄,1位比特表示是否为偏向锁,2位比特表示锁的信息。即锁是通过对象头的Mark Word标识来实现的。
二.锁在虚拟机中的实现和优化
在多线程程序中,线程之间的竞争是不可避免的,所以虚拟机需要更高效率的处理多线程的竞争,从而提升整体效率。虚拟机通过一系列的优化来提升锁的效率,降低竞争关系,减少锁的竞争。通过一系列方法如引入偏向锁,轻量级锁,自旋锁,锁消除,锁膨胀等手段,提升整体的效率。
1.偏向锁
HotSpot的作者经过研究发现,大多数情况下,锁不紧不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
偏向锁的核心思想是,如果程序没有竞争,就取消已经取得锁的线程同步操作。当一个线程获取锁后,这个锁就进入了偏向模式,此时会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,当这个线程再次请求这个锁时,无需再进行相关加锁,解锁操作,节省操作时间。
偏向锁使用了一种等到竞争出现才会释放锁的机制,当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否或者,如果线程不处于活动状态,则将对象头设置为无所状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
可以通过-XX:+UseBiasedLocking参数启用偏向锁。偏向锁提高了线程之间无竞争情况下的效率,即只有一个线程获取锁的情况下,去掉同步操作。偏向锁在少竞争的情况下,对性能有一定帮助。偏向锁在锁竞争激烈的场合没有什么优化效果,因为大量的竞争会导致持有锁的线程不停切换,锁也很难一直保持在偏向模式。
2.轻量级锁
如果偏向锁失败,Java虚拟机会让线程申请轻量级锁。轻量级锁使用一个称为BasicObjectLock的对象来实现,这个对象内部由一个BasicLock对象和一个持有该锁的Java对象指针组成。BasicObjectLock对象 防止在Java栈的栈帧中,在BasicLock对象内部还维护着displaced_header字段,用于备份对象头部的Mark Word。
(1)轻量级锁加锁
线程在执行同步块之前,JVM会现在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程获得锁,当前线程便尝试使用自旋来获取锁。
(2)轻量级锁解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换会到对象头,如果成功表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
3.锁膨胀
当轻量级锁失败,锁就会膨胀为重量级锁,膨胀为重量级锁后,不会再回到轻量级锁了。
4.自旋锁
自旋锁使线程在没有取得锁时,不被挂起,转而去执行一个空循环,在若干个空循环后,线程如果可以获得锁,则继续执行,如果依然不能获取锁,才会挂起。这样是为了防止线程频繁挂起造成的性能损失。
使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁金正不是很激烈,锁占用时间很短的并发线程,有一定的积极意义。但对于锁金正激烈,占用时间长的并发程序,自旋锁不紧没有意义,还白白浪费了CPU的时间,浪费系统资源。
5.锁消除
锁消除是虚拟机在编译时,通过对运行上下文的扫描,去除不可能存在的共享资源竞争的锁。通过锁消除,可以节省无意义的请求锁时间。
这里有个问题,如果不存在竞争,为什么还会加锁呢?是因为有时候我们隐式的使用了锁,比如StringBuffer,Vector等工具,我们使用的时候其实是在线程安全的环境下的,所以锁是没有必要的,虚拟机检测到这一点,就会消除锁。
三.锁在应用层的优化
1.减少锁持有的时间
简单来说就是只对有必要的地方加锁,从而减少锁持有的时间,如果我们程序中执行方法A,B,C,其中只有B需要加锁,那么我们就不要再整个程序中加锁,这样可以减少锁持有的时间。即只在必要时进行同步。
2.减小锁粒度
减小锁粒度的典型实现就是ConcurrentHashMap,通过将一把大锁分成多个锁(ConcurrentHashMap里默认是16)来减少锁冲突的情况,这样在最好的情况下可以同时有16个线程来操作ConcurrentHashMap,比Hashtable这种全局一把锁的性能提升很多。
3.锁分离
锁分离可以理解为减小锁粒度的一种,比如一个典型实现是LinkedBlockingQueue,LinkedBlockingQueue的实现中,take()函数和put()函数分别实现了从队列中取得数据和往队列中添加数据的功能。虽然两个函数都对当前队列进行了修改,但是由于两个操作分别作用于队列的前端和微端,所以两者并不冲突,因此这里使用了两把锁来实现这两个方法。
4.锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短。但是,如果对同一个锁不停地进行请求,同步和释放,其本身也会消耗宝贵的资源,反而会降低性能。
谓词,虚拟机在遇到一连串连续对同一把锁不断进行请求和释放的操作时,会把所有的锁操作整合成对锁的一次请求,减少对锁的请求同步次数。
此外,现在jdk的很多功能都是基于CAS的无锁方式来实现的,这种方式根本不需要锁,所以性能上比锁要高很多。CAS即为比较并交换(Compare And Swap),通过循环+CAS可以解决大部分需要用到锁的场景。