文章目录
- 前言
- 一、常见的锁策略
- 1.1 乐观锁与悲观锁
- 1.2 重量级锁与轻量级锁
- 1.3 自旋锁与挂起等待锁
- 1.4 可重入锁与不可重入锁
- 1.5 公平锁与非公平锁
- 1.6 互斥锁与读写锁
- 二、synchronized的优化策略
- 2.1 锁升级
- 2.2 锁消除
- 2.3 锁粗化
前言
多线程进阶的内容在面试中容易考,但是在工作开发中很少用到。
一、常见的锁策略
并非局限于java,其它变成语言或者其它系统级别的组件,但凡涉及到锁,都和接下来谈到的锁策略有一定关系。
1.1 乐观锁与悲观锁
加锁的时候要预测发生冲突的概率是大还是小,如果说加锁后预测冲突发生概率小,要做的善后工作少,加锁的开销小(时间、系统资源),那么这把锁就是乐观锁。如果说加锁后预测冲突发生概率大,要做的善后工作多,加锁的开销大(时间、系统资源),那么这把锁就是悲观锁。
那么java中的我们学过很多的synchronized是悲观锁还是乐观锁?
事实上synchronized既是悲观锁也是乐观锁,它是一种自适应的锁,它能够统计出当前锁冲突的次数,进行判定锁冲突的概率低还是高,当冲突概率高时就按照悲观锁的处理方式来执行(速度更快),当冲突概率低时就按照乐观锁的处理方式来执行(做的工作更多)。
乐观锁一般只涉及到用户态的操作,要做的工作少。悲观锁往往是要通过内核完成一些操作,要做的工作就多。
1.2 重量级锁与轻量级锁
加锁的过程做的多指的就是重量,加锁的过程中做的少就是轻量,因此一般来说乐观锁就是轻量级锁,悲观锁就是重量级锁,实际交流过程中这两组概念可能会混着用。
1.3 自旋锁与挂起等待锁
自旋锁是轻量级锁的一种典型的实现方式。
如图cpu在空转,陷入一种忙等的状态。通过消耗资源来确保第一时间获取到锁。
挂起等待锁是重量级锁的一种典型实现方式,借助系统的线程调度机制,当尝试加锁锁被占用了就会出现锁冲突,此时尝试加锁的线程就会被挂起(阻塞状态),此时这个线程就不会参与调度了。直到要加的锁被释放后,系统才会重新唤醒这个线程去重新获取锁。从这个过程可以看出,挂起等待是很慢的,因为线程一旦阻塞起来这个过程是不可控的,可能会经历很长的时间,但是这个方法对cpu的使用很少。
java中的synchronized轻量锁使用自旋锁实现(基于CAS机制实现),重量锁使用挂起等待锁实现(调用系统API,通过内核)。
1.4 可重入锁与不可重入锁
java中的synchronized就是可重入锁,一个线程,针对这把锁连续加锁两次不会产生死锁。不可重入锁就相反,针对一把锁连续加锁两次就会产生死锁。
1.5 公平锁与非公平锁
公平锁指的就是严格按照先来后到的顺序来获取锁,哪个线程等待的时间长就先得到锁。非公平锁就是指若干个线程各凭本事随机获取得到锁,这和线程的等待顺序无关,java中的synchronized就属于非公平锁。
这里不禁提出一个问题,什么叫公平?我按照先来后到的顺序是一种公平,我每个线程一起竞争得到锁的概率随机也不失为一种公平。这里其实涉及到历史的问题,因为设计这个概念的大佬就是这样描述公平锁的,所以我们也只能听大佬的了。
1.6 互斥锁与读写锁
互斥锁的概念比较简单,java中的synchronized就是互斥的。
读写锁是一个比较特殊的锁,我们之前学过数据库中的事务的隔离性里面给读加锁给写加锁,这里的读写锁跟那个不一样,在事务那里加锁降低了并发能力,但在这里加锁是为了提高并发能力。java中的读写锁是这样设定的:
(1)读锁和读锁不会产生互斥。
(2)写锁和写锁之间会产生互斥。
(3)读锁和写锁之间会产生互斥。
我在这里说一下我的理解,读写锁就是将锁细化了,当在多线程的情况下读取数据是不会产生线程安全问题的,因此读锁之间无需互斥,这样降低了锁冲突的概率从而提高了并发能力。相反在多线程的情况下写数据是有问题的,因此写锁之间需要互斥来保证线程安全。
二、synchronized的优化策略
synchronized既是悲观锁也是乐观锁,既是重量级锁也是轻量级锁,轻量级锁的实现基于自旋锁,重量级锁的实现基于挂起等待锁,是可重入锁,不是读写锁,是非公平锁。
2.1 锁升级
synchronized这种自适应锁的锁升级的过程如下图:
在这个过程中主要要理解一下偏向锁。
首次使用synchronized给对象进行加锁时不是真的加锁,而是做一个标记(非常轻量非常快,几乎没有开销),这就是偏向锁。如果一直没有别的线程尝试给这个对象加锁,那么就会一致保持这种偏向锁的状态直至释放解锁。(解锁也就是改变标记,也几乎没有开销)上述过程中就相当于没有任何加锁操作,速度非常快。但是如果在偏向锁的状态下也有别的线程尝试给该对象加锁,那么偏向锁就会立即升级为轻量级锁从而发生互斥保证线程安全。偏向锁就是“懒”这个字的具体体现,能不加锁就不加锁,能晚加锁就晚加锁,这样就能在很多时候把开销省下来。
上述的锁的升级过程对一个锁对象来说是不可逆的,只能升级不能降级。
2.2 锁消除
锁消除是一种编译器优化策略,你代码中有加锁操作,编译器以及JVM会对其进行判定,看这个地方是否真的需要加锁,如果不需要就会自动将加锁操作给优化掉。最典型的例子就是在一个线程内使用synchronized。编译器优化前和优化后的效果需要一致,所以这里是比较保守的,作用有限,本质上也是为了在程序员感知不到的情况下去提高效率。
2.3 锁粗化
首先了解一个概念就是锁粒度,指的就是在加锁的范围内包含的代码越多就认为锁的粒度越粗,反之就是锁的粒度越细。
锁的粗化也是一种优化策略,比如说有些逻辑中需要频繁的加锁,此时我们就可以进行锁的粗化,将多段逻辑包含到一个锁当中。