网上关于Java中锁的话题能够说资料至关丰富,但相关内容总感受是一大串术语的罗列,让人云里雾里,读完就忘。本文但愿能为Java新人作一篇通俗易懂的整合,旨在消除对各类各样锁的术语的恐惧感,对每种锁的底层实现浅尝辄止,可是在须要时可以知道去查什么。前端
首先要打消一种想法,就是一个锁只能属于一种分类。其实并非这样,好比一个锁能够同时是悲观锁、可重入锁、公平锁、可中断锁等等,就像一我的能够是男人、医生、健身爱好者、游戏玩家,这并不矛盾。OK,国际惯例,上干货。java
Java中有两种加锁的方式:一种是用synchronized关键字,另外一种是用Lock接口的实现类。算法
形象地说,synchronized关键字是自动档,能够知足一切平常驾驶需求。可是若是你想要玩漂移或者各类骚操做,就须要手动档了——各类Lock的实现类。编程
因此若是你只是想要简单的加个锁,对性能也没什么特别的要求,用synchronized关键字就足够了。自Java 5以后,才在java.util.concurrent.locks包下有了另一种方式来实现锁,那就是Lock。也就是说,synchronized是Java语言内置的关键字,而Lock是一个接口,这个接口的实现类在代码层面实现了锁的功能,具体细节不在本文展开,有兴趣能够研究下AbstractQueuedSynchronizer类,写得能够说是牛逼爆了。bash
ReentrantLock、ReadLock、WriteLock 是Lock接口最重要的三个实现类。对应了“可重入锁”、“读锁”和“写锁”,后面会讲它们的用途。并发
ReadWriteLock实际上是一个工厂接口,而ReentrantReadWriteLock是ReadWriteLock的实现类,它包含两个静态内部类ReadLock和WriteLock。这两个静态内部类又分别实现了Lock接口。函数
咱们中止深究源码,仅从使用的角度看,Lock与synchronized的区别是什么?在接下来的几个小节中,我将梳理各类锁分类的概念,以及synchronized关键字、各类Lock实现类之间的区别与联系。性能
锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并非特指某个锁(Java中没有哪一个Lock实现类就叫PessimisticLock或OptimisticLock),而是在并发状况下的两种不一样策略。学习
悲观锁(Pessimistic Lock), 就是很悲观,每次去拿数据的时候都认为别人会修改。因此每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放。优化
乐观锁(Optimistic Lock), 就是很乐观,每次去拿数据的时候都认为别人不会修改。因此不会上锁,不会上锁!可是若是想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。若是修改过,则从新读取,再次尝试更新,循环上述步骤直到更新成功(固然也容许更新失败的线程放弃操做)。
悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种必定好于另外一种。像乐观锁适用于写比较少的状况下,即冲突真的不多发生的时候,这样能够省去锁的开销,加大了系统的整个吞吐量。但若是常常产生冲突,上层应用会不断的进行重试,这样反却是下降了性能,因此这种状况下用悲观锁就比较合适。
说到乐观锁,就必须提到一个概念:CAS
什么是CAS呢?Compare-and-Swap,即比较并替换,也有叫作Compare-and-Set的,比较并设置。
一、比较:读取到了一个值A,在将其更新为B以前,检查原值是否仍为A(未被其余线程改动)。
二、设置:若是是,将A更新为B,结束。[1]若是不是,则什么都不作。
上面的两步操做是原子性的,能够简单地理解为瞬间完成,在CPU看来就是一步操做。
有了CAS,就能够实现一个乐观锁:
data = 123; // 共享数据
/* 更新数据的线程会进行以下操做 */
flag = true;
while (flag) {
oldValue = data; // 保存原始数据
newValue = doSomething(oldValue);
// 下面的部分为CAS操做,尝试更新data的值
if (data == oldValue) { // 比较
data = newValue; // 设置
flag = false; // 结束
} else {
// 啥也不干,循环重试
}
}
/*
很明显,这样的代码根本不是原子性的,
由于真正的CAS利用了CPU指令,
这里只是为了展现执行流程,本意是同样的。
*/
复制代码
这是一个简单直观的乐观锁实现,它容许多个线程同时读取(由于根本没有加锁操做),可是只有一个线程能够成功更新数据,并致使其余要更新数据的线程回滚重试。 CAS利用CPU指令,从硬件层面保证了操做的原子性,以达到相似于锁的效果。
由于整个过程当中并无“加锁”和“解锁”操做,所以乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试CAS的算法而已!
有一种锁叫自旋锁。所谓自旋,说白了就是一个 while(true) 无限循环。
刚刚的乐观锁就有相似的无限循环操做,那么它是自旋锁吗?
感谢评论区 养猫的虾的指正。
不是。尽管自旋与 while(true) 的操做是同样的,但仍是应该将这两个术语分开。“自旋”这两个字,特指自旋锁的自旋。
然而在JDK中并无自旋锁(SpinLock)这个类,那什么才是自旋锁呢?读完下个小节就知道了。
前面提到,synchronized关键字就像是汽车的自动档,如今详细讲这个过程。一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡同样。那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁。
初次执行到synchronized代码块的时候,锁对象变成偏向锁(经过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个得到它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是本身(持有锁的线程ID也在对象头里),若是是则正常往下执行。因为以前没有释放锁,这里也就不须要从新加锁。若是自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。这里要明确一下什么是锁竞争:若是多个线程轮流获取一个锁,可是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。
在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否可以被成功获取。获取锁的操做,其实就是经过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,若是是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,而后线程将当前锁的持有者信息修改成本身。
长时间的自旋操做是很是消耗资源的,一个线程持有锁,其余线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫作忙等(busy-waiting)。若是多个线程用一个锁,可是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,容许短期的忙等现象。这是一种折衷的想法,短期的忙等,换取线程在用户态和内核态之间切换的开销。
显然,此忙等是有限度的(有个计数器记录自旋次数,默认容许循环10次,能够经过虚拟机参数更改)。若是锁竞争状况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将本身挂起(而不是忙等),等待未来被唤醒。在JDK1.6以前,synchronized直接加剧量级锁,很明显如今获得了很好的优化。
一个锁只能按照 偏向锁、轻量级锁、重量级锁的顺序逐渐升级(也有叫锁膨胀的),不容许降级。
感谢评论区 酷帅俊靓美的问题:
偏向锁的一个特性是,持有锁的线程在执行完同步代码块时不会释放锁。那么当第二个线程执行到这个synchronized代码块时是否必定会发生锁竞争而后升级为轻量级锁呢?
线程A第一次执行完同步代码块后,当线程B尝试获取锁的时候,发现是偏向锁,会判断线程A是否仍然存活。 若是线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,以后线程A继续执行,线程B自旋。可是 若是判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级。
可重入锁的字面意思是“能够从新进入的锁”,即容许同一个线程屡次获取同一把锁。好比一个递归函数里有加锁操做,递归过程当中这个锁会阻塞本身吗?若是不会,那么这个锁就是可重入锁(由于这个缘由可重入锁也叫作递归锁)。
Java里只要以Reentrant开头命名的锁都是可重入锁,并且JDK提供的全部现成的Lock实现类,包括synchronized关键字锁都是可重入的。若是你须要不可重入锁,只能本身去实现了。网上不可重入锁的实现真的不少,就不在这里贴代码了。99%的业务场景用可重入锁就能够了,剩下的1%是什么呢?我也不知道,谁能够在评论里告诉我?
若是多个线程申请一把公平锁,那么当锁释放的时候,先申请的先获得,很是公平。显然若是是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其余优先级排序的。
对ReentrantLock类而言,经过构造函数传参能够指定该锁是不是公平锁,默认是非公平锁。通常状况下,非公平锁的吞吐量比公平锁大,若是没有特殊要求,优先使用非公平锁。
对于synchronized而言,它也是一种非公平锁,可是并无任何办法使其变成公平锁。
可中断锁,字面意思是“能够响应中断的锁”。
这里的关键是理解什么是中断。Java并无提供任何直接中断某线程的方法,只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你中止运行”的请求(线程B也能够本身给本身发送此请求),但线程B并不会马上中止运行,而是自行选择合适的时机以本身的方式响应中断,也能够直接忽略此中断。也就是说,Java的中断不能直接终止线程,而是须要被中断的线程本身决定怎么处理。这比如是父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则彻底取决于本身。[2]
回到锁的话题上来,若是线程A持有锁,线程B等待获取该锁。因为线程A持有锁的时间过长,线程B不想继续等待了,咱们可让线程B中断本身或者在别的线程里中断它,这种就是可中断锁。
在Java中,synchronized就是不可中断锁,而Lock的实现类都是可中断锁,能够简单看下Lock接口。
/* Lock接口 */
public interface Lock {
void lock(); // 拿不到锁就一直等,拿到立刻返回。
void lockInterruptibly() throws InterruptedException; // 拿不到锁就一直等,若是等待时收到中断请求,则须要处理InterruptedException。
boolean tryLock(); // 不管拿不拿获得锁,都立刻返回。拿到返回true,拿不到返回false。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 同上,能够自定义等待的时间。
void unlock();
Condition newCondition();
}复制代码
读写锁实际上是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁)。
看下Java里的ReadWriteLock接口,它只规定了两个方法,一个返回读锁,一个返回写锁。
记得以前的乐观锁策略吗?全部线程随时均可以读,仅在写以前判断值有没有被更改。
读写锁其实作的事情是同样的,可是策略稍有不一样。不少状况下,线程知道本身读取数据后,是不是为了更新它。那么何不在加锁的时候直接明确这一点呢?若是我读取值是为了更新它(SQL的for update就是这个意思),那么加锁的时候就直接加写锁,我持有写锁的时候别的线程不管读仍是写都须要等待;若是我读取数据仅为了前端展现,那么加锁时就明确地加一个读锁,其余线程若是也要加读锁,不须要等待,能够直接获取(读锁计数器+1)。
虽然读写锁感受与乐观锁有点像,可是读写锁是悲观锁策略。由于读写锁并无在更新前判断值有没有被修改过,而是在加锁前决定应该用读锁仍是写锁。乐观锁特指无锁编程,若是仍有疑惑能够再回到第1、二小节,看一下什么是“乐观锁”。
JDK提供的惟一一个ReadWriteLock接口实现类是ReentrantReadWriteLock。看名字就知道,它不只提供了读写锁,而是都是可重入锁。 除了两个接口方法之外,ReentrantReadWriteLock还提供了一些便于外界监控其内部工做状态的方法,这里就不一一展开。
这篇文章经历过一次修改,我以前认为偏向锁和轻量级锁是乐观锁,重量级锁和Lock实现类为悲观锁,网上不少资料对这些概念的表述也很模糊,各执一词。
先抛出个人结论:
咱们在Java里使用的各类锁,几乎全都是悲观锁。synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁。其实只要有“锁对象”出现,那么就必定是悲观锁。由于乐观锁不是锁,而是一个在循环里尝试CAS的算法。
那JDK并发包里到底有没有乐观锁呢?
有。java.util.concurrent.atomic包里面的原子类都是利用乐观锁实现的。
为何网上有些资料认为偏向锁、轻量级锁是乐观锁?理由是它们底层用到了CAS?或者是把“乐观/悲观”与“轻量/重量”搞混了?其实,线程在抢占这些锁的时候,确实是循环+CAS的操做,感受好像是乐观锁。但问题的关键是,咱们说一个锁是悲观锁仍是乐观锁,老是应该站在应用层,看它们是如何锁住应用数据的,而不是站在底层看抢占锁的过程。若是一个线程尝试获取锁时,发现已经被占用,它是否继续读取数据,等后续要更新时再决定要不要重试?对于偏向锁、轻量级锁来讲,显然答案是否认的。不管是挂起仍是忙等,对应用数据的读取操做都被“挡住”了。从这个角度看,它们确实是悲观锁。
退一步讲,也没有必要在这些术语上狠钻牛角尖,最重要的是理解它们的运行机制。想写得尽可能简单一些,却发现洋洋洒洒近万字,只讲了个皮毛。深知本身水平有限,不敢保证彻底正确,只能说路漫漫其修远兮,望指正。