文章目录
锁的存在就是要保证一个资源片段在多线程的竞争处理的情况下,最终资源处理的结果是原子的。
之前文章讲了synchronized的一些知识,我们了解了synchronized关键字在Java中的含义,以及内部锁升级的过程。在我们工作或者学习过程中,会遇到许许多多的锁的概念,例如悲观锁、乐观锁、自旋锁、读写锁、独占锁、互斥锁、共享锁、可重入锁、不可重入锁、排他锁、偏向锁、公平锁、非公平锁、轻量级锁、重量级锁、分段锁
。这么多的概念,应该如何区分它们的含义,如何更好的去区分并且使用这些锁呢?其实有很多都是重复的概念,只要我们理解了就好。
悲观锁&乐观锁
从锁的最终实现方式来说,一般就分为悲观锁和乐观锁
悲观锁
悲观锁
是说,悲观的认为当前线程在操作的时候,总会有其他的线程要来更改数据,所以不论悲观锁内部要做什么操作,都先要加锁
,然后各个线程通过竞争这把锁来达到同一时间只有一个线程拿到锁
,并且同步操作内部资源
的目的。synchronized
和lock
都是悲观锁的实现。
如何理解加锁?
通过之前文章对synchronized的了解我们知道,synchronized可以加载方法上锁住整个方法,也可以锁住某段代码,但是最终synchronzied都是使用对象作为锁,加锁的意思其实就是说,把某个方法或者某段代码,用某个对象作为锁,加到这片作用域上,这个对象就是加上去的锁。
如何理解拿到锁?
对于悲观锁来说,锁上其实是通过更改在内存中对象头对应monitor
的标记,表示当前线程对当前对象锁的占用。所有的线程都通过去更改这个锁标记,来达到加锁的一个目的,最终对象锁中标记的哪个线程,就是哪个线程拿到了当前的对象锁,这个对象锁作用的代码块,就只能由这个线程来执行。
乐观锁
乐观锁
是说,乐观的认为当前线程在做操作的时候,不会有其他线程来更改操作数据,所以不会去执行上边说过的加锁的流程,只在提交的时候,判断当前要更改的资源,是否是之前读取到的值
,如果不是的话就处理失败,后续再考虑是否重新去操作或者就此结束。这种就是通过无锁
的方式,达到操作资源的原子性
的目的。这种操作其实也叫CAS
操作,使用的地方非常多,java中的原子类Atomic就是通过这种方式实现原子操作
CAS
CAS是Compare And Swap
,是一个比较替换的逻辑操作方式,用来实现原子操作,在java中很多地方用到了这种操作方式,除了提到过的Atomic类,甚至上边说的在竞争锁
的过程中,都是通过这种方式保证拿锁的过程是一个原子性操作。还有就是数据库中也可以通过这种方式对某一个数据的更改达到原子性操作,类似于锁
的一个目的,用一个数据库的更改语句很好的能解释这种操作。
update user set name="李四" where name="张三"; --如果当前用户的名字还是张三,就改成李四。
自旋锁
上边说到乐观锁使用了一个CAS的操作
,如果操作失败,就可以考虑是否重新去操作或者就此结束。自旋锁
就是它的一种操作。因为正常来说,我们修改一个数据,一般都是想要修改成功,如果一次操作没成功就失败,可能并不是我们想要的结果。自旋锁它不会就此停止操作,释放CPU的资源,而是继续通过自旋的操作,不断去执行CAS
,直到成功为止。java在1.6之前使用-XX:+UseSpinning
参数来开启自旋锁,在1.6之后,自动开启。
优缺点
- 优点:自旋锁的优势是既然我们要保证资源一定要更改,那就不需要执行失败之后释放cpu资源,然后等下次再拿到cpu资源执行操作,而是通过不释放cpu资源,
减少cpu切换的成本
。 - 缺点:优点是不释放cpu资源,缺点也是不释放cpu资源,如果自旋锁一直操作不成功,那么cpu资源一直占用,如果累计过多,那么cpu就都被自旋锁占满了,导致cpu过载,甚至直接其他所有使用cpu的操作无法执行
自适应自旋锁
为了优化上边提到过的自旋锁的缺点,java1.6除了默认开启自旋锁,并且将自旋锁进行了优化,原本是一直自旋占用cpu,优化之后是说,如果在自旋锁过程中,当前的线程在之前已经修改成功过,那么就会认为当前这个线程有很大的几率会修改成功那就增加这个线程自旋的时间,如果发现当前线程自旋失败的几率比较大,那就会减少这个线程自旋的时间,甚至直接阻塞。通过减少自旋的时间,保证cpu不会被一直死自旋的线程占用
。
读写锁
读写锁ReadWriteLock
是java中的一个接口,它内部有两个锁实现,一个是读锁,一个是写锁,ReentrantReadWriteLock
是这个接口的实现,它通过对AQS
的使用,实现读锁写锁的功能,并且完成读写锁切换的过程。上边我有提到过java中的lock其实是悲观锁的实现,悲观锁虽然能完全保证原子性,但是有个问题就是,它不论内部做的是什么操作,都只能保证同一时间只有一个线程能执行,哪怕只是一个读取的操作。这样的话肯定能想到的就是性能不够完全发挥出来,由此产生了读写锁,读写锁的意思是同一时间要么当前状态是读锁,要么是写锁,读锁的状态下同一时间可以有多个线程同时做读取操作,写锁的状态下同一时间只能有一个线程对数据进行操作,并且在读锁和写锁不能同时产生,需要互相竞争
。暂时只是简单介绍一下,后续我会增加文章去详细介绍读写锁的实现以及切换过程。
独占锁、互斥锁、排他锁&共享锁
独占就是互斥的,互斥也就是说互相排挤,无法同时使用。这三个是相同的概念,就是说这个锁只能由一个线程进行操作,而共享锁的意思就是说,虽然是锁着的,但是可以同时让多个线程同时使用。这个可以用我们上边提到的读写锁来解释读写锁中分为读锁和写锁,写锁只能有一个线程来写,那写锁就是独占锁、也叫互斥锁和排它锁,而读锁可以有多个线程同时读取,所以读锁也叫共享锁
除了读写锁中的读锁是共享锁,其他任何形式的锁,只要是同一时间只能有一个线程进行操作,他们都是独占、互斥、排他锁。
可重入锁&不可重入锁
正常我们只要有加锁的操作,一般作用于代码块或者方法上,如果同时有多个方法同时加了相同的对象锁,并且又产生了互相调用甚至递归的情况,线程拿到锁之后,又遇到了拿锁的操作。这种情况下,允不允许线程可以继续拿到这个锁,这就产生了锁是否可以重入的一个状态。
不可重入锁会造成什么问题?
如果发生了我上边提到的这种情况,并且当前锁的规则是不能继续进入拿到锁,那么后续发生的情况就可能是,当前线程占用这这个对象锁,但是却不能继续再拿到一次,需要等待这个对象锁释放才能继续进行,那么就会导致线程一直无法得到第二个锁,发生死锁的情况。
可重入锁
可重入锁在我之前的synchronized的文章中有提到对象monitor监视器中有一个_recursions字段
,专门来保存synchronized锁进入的数量,每次进入就+1,释放一次就-1,当为0的时候就是彻底释放了锁。也就是说,synchronized就是一个可重入锁
,他可以支持重复进入相同锁的代码块。
偏向锁、轻量级锁、重量级锁
这三个锁是synchronized锁升级过程中出现的概念,这个在我上边文章有详细解释,这边只是简单做介绍
偏向锁
正常来说,我们的程序如果某个线程拿到了这个对象锁,其实在很大情况下,下次还是这个线程来拿,偏向锁就是对这种行为的优化,不让同一个线程一直来拿锁的时候,还需要一直去竞争锁。偏向锁有两种行为:
第一个线程拿到锁之后,将对象头开启偏向锁的标识,并且将当前线程的线程ID记录到对象头中,如果下次继续来拿锁,发现对象头是偏向锁,并且这个线程ID记录的是自己,那么就不用再去做任何操作,直接就能进入到对应的锁资源内部做操作
。另外一个线程来拿锁的时候,发现是偏向锁,但是线程ID不是自己,那就会多判断对应的线程是否还在锁使用状态中,如果没有使用,就将线程ID改为自己的,并且进入到锁资源内部
轻量级锁
轻量级锁的意思就是说,线程虽然发生锁竞争,但是是轻量级的,只有当前线程在尝试拿锁。
轻量级锁就是发生在偏向锁行为之后,当进行偏向锁第2个行为的时候,发现对应的线程还在锁状态中,还在使用,那就会将锁状态升级为轻量级锁。拿锁的方式就是通过重复做上边提到过的CAS
操作(也就是说当前是自旋锁
),更改对象monitor锁监视器中的_owner字段,如果这次操作将这个字段的值改成自己的线程ID,那就是拿到了锁。
重量级锁
重量级锁是为了防止在轻量级锁阶段自旋锁
的竞争导致cpu被无意义占用的问题,将抢锁的线程进行阻塞,保证程序正常运转。
在轻量级锁的行为过程中,如果当前线程获取锁的时候,发现已经是轻量级锁,如果_owner字段存的是自己的线程信息,那么就是发生了重入,继续进行操作,如果_owner字段存的是其他线程的值,还没被释放,无法更改成功,就将对象锁升级为重量级锁,并且将自己阻塞
。因为发生了阻塞需要唤醒,所以在线程在锁释放的过程中,如果发现有线程竞争过锁,并且没有拿到发生了阻塞,线程在释放锁时候,还需要唤醒之前阻塞的线程
。
公平锁&非公平锁
公平与非公平说的是如果发生多个线程争夺锁的过程中,被阻塞之后,唤醒再次拿锁的时候是否有一个顺序问题。
公平锁
先来竞争锁的,先被唤醒得到锁。保证先被阻塞的线程最先可以放开。
非公平锁
没有先后顺序,不知道谁会下一个拿到锁。synchronized就是一个非公平锁。
实现
abstract static class Sync extends AbstractQueuedSynchronizer {
...
static final class NonfairSync extends Sync {
...
static final class FairSync extends Sync {
...
除了synchronized,java中还有一种对公平非公平锁的实现就是AQS
,Sync类
通过继承AQS
,并且提供了两个子类,一个是NonfairSync
就是非公平锁,一个是FairSync
就是公平锁。具体实现细节我会专门用一篇文章来讲解。
分段锁
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {`
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
分段锁其实算是一个我硬凑的概念,我们说的分段锁其实就是ConcurrentHashMap中,实现内部分成16段,把每一段都锁上,细粒度的控制每个段的值的变化,保证ConcurrentHashMap可以同时支持16个线程同时操作,提高并发,但是最终都是使用synchronized实现的锁控制
我拿出里面put的一个方法可以看下。具体到synchronized其实就已经讲过很多了,我们理解就可以。
总结
本片文章主要介绍了java中比较常见的一些锁的概念,理解了各种类型锁的作用以及使用,可以帮助我们在并发场景下,通过对锁合理的使用,既能保证系统的性能,又能保证系统的稳定正确运行。