在读不少并发文章中,会说起各类各样锁如公平锁,乐观锁等等,这篇文章介绍各类锁的分类。介绍的内容以下:html
公平锁/非公平锁
可重入锁
独享锁/共享锁
互斥锁/读写锁
乐观锁/悲观锁
分段锁
偏向锁/轻量级锁/重量级锁
自旋锁
上面是不少锁的名词,这些分类并非全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每一个锁的名词进行必定的解释。并着重介绍一下偏向锁/轻量级锁/重量级锁java
1、java中的锁分类算法
公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并非按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会形成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,经过构造函数指定该锁是不是公平锁,默认是非公平锁。非公平锁的优势在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。因为其并不像ReentrantLock是经过AQS的来实现线程调度,因此并无任何办法使其变成公平锁。编程
可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。
对于Java ReentrantLock而言, 他的名字就能够看出是一个可重入锁,其名字是Re entrant Lock从新进入锁。
对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可必定程度避免死锁。使用synchronized时,当一个线程获得一个对象锁后,再次请求此对象锁时,是能够再次获得该对象的锁的。数组
synchronized void setA() throws Exception{ Thread.sleep(1000); setB(); } synchronized void setB() throws Exception{ Thread.sleep(1000); }
上面的代码就是一个可重入锁的一个特色,若是不是可重入锁的话,setB可能不会被当前线程执行,可能形成死锁。安全
独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。多线程
对于Java ReentrantLock而言,其是独享锁。可是对于Lock的另外一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是很是高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是经过AQS来实现的,经过实现不一样的方法,来实现独享或者共享。
对于Synchronized而言,固然是独享锁。并发
互斥锁/读写锁
上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。函数
互斥锁指一次最多只能有一个线程持有的锁。
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock性能
乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操做,必定是会发生修改的,哪怕没有修改,也会认为修改。所以对于同一个数据的并发操做,悲观锁采起加锁的形式。悲观的认为,不加锁的并发操做必定会出问题。
乐观锁则认为对于同一个数据的并发操做,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重复的方式更新数据。乐观的认为,不加锁的并发操做是没有事情的。
从上面的描述咱们能够看出,悲观锁适合写操做很是多的场景,乐观锁适合读操做很是多的场景,不加锁会带来大量的性能提高。
悲观锁在Java中的使用,就是利用各类锁。
乐观锁在Java中的使用,是无锁编程,经常采用的是CAS算法,典型的例子就是原子类,经过CAS自旋实现原子操做的更新。
分段锁
分段锁实际上是一种锁的设计,并非具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是经过分段锁的形式来实现高效的并发操做。
咱们以ConcurrentHashMap来讲一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即相似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每一个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当须要put元素的时候,并非对整个hashmap进行加锁,而是先经过hashcode来知道他要放在那一个分段中,而后对这个分段进行加锁,因此当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
可是,在统计size的时候,可就是获取hashmap全局信息的时候,就须要获取全部的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操做不须要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操做。
偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,而且是针对Synchronized。在Java 5经过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是经过对象监视器在对象头中的字段来代表的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。下降获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另外一个线程所访问,偏向锁就会升级为轻量级锁,其余线程会经过自旋的形式尝试获取锁,不会阻塞,提升性能。
重量级锁是指当锁为轻量级锁的时候,另外一个线程虽然是自旋,但自旋不会一直持续下去,当自旋必定次数的时候,尚未获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其余申请的线程进入阻塞,性能下降。
自旋锁
在Java中,自旋锁是指尝试获取锁的线程不会当即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减小线程上下文切换的消耗,缺点是循环会消耗CPU。
典型的自旋锁实现的例子,能够参考自旋锁的实现
2、偏向锁/轻量级锁/重量级锁的对比
在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。这几个状态会随着竞争状况逐渐升级。锁能够升级但不能降级,意味着偏向锁升级为轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提升得到锁和释放锁的效率。
1. 偏向锁
HotSpot 的做者通过以往的研究发现大多数状况下锁不只不存在多线程竞争,并且老是由同一线程屡次得到,为了让线程得到锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,之后该线程在进入和退出同步块时不须要进行CAS操做来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,若是测试成功,表示线程已经得到了锁,
若是测试失败,则须要再测试下 Mark Word中偏向锁的标识是否设置成 1(表示当前是偏向锁),若是没有设置,则使用 CAS 竞争锁,若是设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程。
(1) 偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,因此当其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
偏向锁的撤销,须要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,而后检查持有偏向锁的线程是否活着,若是线程不处于活动状态,则将对象头设置成无锁状态,若是线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么从新偏向于其余线程,要么恢复到无锁或者标记对象不适合做为偏向锁,最后唤醒暂停的线程。
2. 轻量级锁
(1) 轻量级锁加锁
线程在执行同步块以前,JVM会先在当前线程的栈桢中建立用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。而后线程尝试使用 CAS 将对象头中的Mark Word替换为指向锁记录的指针。若是成功,当前线程得到锁,若是失败,表示其余线程竞争锁,当前线程便尝试使用自旋来获取锁。
若是有两条以上的线程争用同一个锁,那轻量级锁就再也不有效,要膨胀为重量级锁,锁标志的状态值变为”10”,Mark Word中存储的就是指向重量级(互斥量)的指针。
(2) 轻量级锁解锁
轻量级解锁时,会使用原子的 CAS 操做来将Displaced Mark Word替换回到对象头,若是成功,则表示没有竞争发生。若是失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它如今认为只可能有一个线程来访问它,因此当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操做,并将对象头中的ThreadID改为本身的ID,以后再次访问这个对象时,只须要对比ID,不须要再使用CAS在进行操做。
一旦有第二个线程访问这个对象,由于偏向锁不会主动释放,因此第二个线程能够看到对象时偏向状态,这时代表在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,若是挂了,则能够将对象变为无锁状态,而后从新偏向新的线程,若是原来的线程依然存活,则立刻执行那个线程的操做栈,检查该对象的使用状况,若是仍然须要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。若是不存在使用了,则能够将对象回复成无锁状态,而后从新偏向。
轻量级锁认为竞争存在,可是竞争的程度很轻,通常两个线程对于同一个锁的操做都会错开,或者说稍微等待一下(自旋),另外一个线程就会释放锁。 可是当自旋超过必定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程之外的线程都阻塞,防止CPU空转。
偏向锁/轻量级锁/重量级锁的优缺点对比
锁 | 优势 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不须要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 若是线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提升了程序的响应速度 | 若是始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间 同步块执行速度很是快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量 同步块执行速度较长 |
参考:
《java并发编程的艺术》