前言
java中的锁可谓是五花八门,各种锁功能相似又不同,有的是概念、有的是java接口、有的是实现类,让你很难找到明显的分界线去区分并记住他们。所以学习锁首先要打消一种想法,就是一个锁只属于一个分类,比如一个锁可以同时是乐观锁、可重入锁,公平锁,就像一个人可以是男人、程序员、健身爱好者。
synchronized与Lock
java代码中两种加锁方式 一种是用synchronized关键字,另一种是用Lock接口的实现类。形象地说,synchronized关键字是自动档,可以满足一切日常驾驶需求。但是如果你想要玩漂移或者各种骚操作,就需要手动档了——各种Lock的实现类,因为Lock的实现类可以通过设置不同的参数改变锁的作用达到灵活适应场景的作用,而synchronized是关键字,底层有jvm实现,很多参数都是写死的。
悲观锁与乐观锁
锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java中没有哪个Lock实现类就叫PessimisticLock或OptimisticLock),而是在并发情况下的两种不同策略。
悲观锁(Pessimistic Lock),就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁,这样别人想拿数据就被挡住,直到悲观锁被释放。比如上面说的synchronized与Lock。
乐观锁(Optimistic Lock), 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,这里的上锁是指互斥性质的上锁,在说明白点就是我加锁了谁也别想碰,除非我释放锁,乐观锁采用的是类似CAS的方式,保证操作数据不会干扰到其他线程。
悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
自旋锁
有一种锁叫自旋锁。所谓自旋,说白了就是一个** while(true) ** 无限循环。
这个自旋锁与Atomic类的while实现的自旋代码不是一回事,下一章AQS会详细讲
synchronized锁升级
前面提到synchronized关键字就像是汽车的自动档。一脚油门踩下去,synchronized会从无锁升级为偏向锁,再升级为轻量级锁,最后升级为重量级锁,就像自动换挡一样。那么自旋锁在哪里呢?这里的轻量级锁就是一种自旋锁。
初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位,说明白点就是锁记住了第一次和他发生关系的线程),字面意思是“偏向于第一个获得它的线程”的锁,执行完同步代码块后,线程并不会主动释放偏向锁。第二次访问如果还是此线程,那么就没有加锁释放锁这一说,正常执行。
一旦有第二个线程加入锁竞争并发现锁是偏向锁,会去断线程A是否仍然存活。如果线程A仍然存活,将线程A暂停,此时偏向锁升级为轻量级锁,之后线程A继续执行,线程B自旋。但是如果判断结果是线程A不存在了,则线程B持有此偏向锁,锁不升级。
在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,比较并设置是原子性发生的。这就算抢到锁了,然后线程将当前锁的持有者信息修改为自己。
自旋锁避免不了的问题就是竞争特别激烈的情况下,其他线程只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做**忙等(busy-waiting)**。显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改)。如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。
可重入锁(递归锁)
可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。
公平锁、非公平锁
如果多个线程申请一把公平锁,那么当锁释放的时候,先申请的先得到,非常公平。显然如果是非公平锁,后申请的线程可能先获取到锁,是随机或者按照其他优先级排序的。
对ReentrantLock类而言,通过构造函数传参可以指定该锁是否是公平锁,默认是非公平锁。一般情况下,非公平锁的吞吐量比公平锁大,如果没有特殊要求,优先使用非公平锁。对于synchronized而言,它也是一种非公平锁,但是并没有任何办法使其变成公平锁。
可中断锁
这里的关键是理解什么是中断。Java并没有提供任何直接中断某线程的方法,只提供了中断机制。何谓“中断机制”?线程A向线程B发出“请你停止运行”的请求(线程B也可以自己给自己发送此请求),但线程B并不会立刻停止运行,而是自行选择合适的时机以自己的方式响应中断,也可以直接忽略此中断。也就是说,Java的中断不能直接终止线程,而是需要被中断的线程自己决定怎么处理。这好比是父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。
读写锁、共享锁、互斥锁
读写锁其实是一对锁,一个读锁(共享锁)和一个写锁(互斥锁、排他锁),Java提供了ReadWriteLock接口和实现类ReentrantReadWriteLock来实现读写锁。
- 读锁:防止读的时候其他线程写,允许读的时候其他线程读
- 写锁:防止写的时候其他线程读或写
使用锁带来的问题
死锁、活锁、饥饿