多线程之旅:锁策略
前面的文章中呢,小编介绍到synchronized这样的一个关键字。
这个关键字呢,是Java推荐使用一个锁来的,而这对于锁的内容呢,其实还是有很多值得分享的。
所以,现在小编来继续分享下关于锁的内容。
比如,今天要分享的锁策略。
那么这个锁策略是比较广泛的内容,包括不限于锁的类型、锁的粒度、锁的实现方式、锁的公平性等。
那么介绍一下锁的类型以及锁实现方式和公平性吧。
锁策略
其实常见的锁类型是也是挺多的。
比如以下:
乐观锁 vs 悲观锁
轻量级锁 vs 重量级锁
公平锁 vs 非公平锁
挂起等待锁 vs 自选锁
可重入锁 vs 不可重入锁
读写锁
那么接下来一一解释它们究竟是何方神圣吧
乐观锁 vs 悲观锁
乐观锁:假设并发冲突很少发生,此时,当更新数据的时候,此时才会检查是否是有冲突
如若是有冲突,那么一般选择是重试或放弃操作,亦或者交给用户来处理。
悲观锁:假设并发冲突是频繁发生的,此时呢,没有更新数据之前,就可以加锁,从而导致其他线程无法访问该资源。
举个例子:
小编去约女孩子看电影。
此时会有两种情况
小编假设这个女孩子是挺忙,可能不一定有空和我一起去看。
那么我会这样先发消息说:你在忙嘛?我下午2点钟约你看电影行不?(相当于加锁操作了)
如若是不忙的,并得到肯定答复后,那么此时这个女孩子就会和我一起看电影了。
如若是很忙的情况下,那小编会等下一段时间,再去和 这个女孩子确定时间。
那么这个就是悲观锁的体现
另一种情况呢
小编假设这个女孩子是不忙的,所以小编是直接找到女孩子了,然后就问她可不可以一起看电影,如若是真的不忙,还答应了,那就一起去了,此时如果是忙的,那么就下次再来(我们是没有加锁的,但是此时也是可以得到访问冲突了)
这就是乐观锁的体现。
而对于Java的synchronized来说
它是自适应的.什么意思呢?
即开始之初是为一个乐观锁,等到锁竞争较为频繁的时候,就会自动切换为悲观锁。
轻量级锁 和 重量级锁
这个轻量级锁是乐观锁的其中一种
重量级锁是悲观锁的其中一种。
轻量级锁做的工作较少,加锁的开销较小,往往是在乐观锁的情况下的。
重量级锁做的工作较多,加锁的开销较大,往往是在悲观锁的情况下的。
同样的,synchronized对于这个也是自适应的。
挂起等待锁 和 自旋锁
这个挂起等待锁也是一个悲观锁/重量级锁的典型实现
自旋锁是一个乐观锁/轻量级锁的典型实现。
当线程获取锁失败的时候,此时线程就会阻塞(挂起),并释放CPU资源
等到锁被释放了,操作系统就会唤醒等待的锁,去竞争锁,这就是挂起等待锁。
显然,有这样的优点:
线程挂起后不占用CPU资源,减少CPU的浪费,适合高竞争的场景
缺点嘛也比较明显
在线程挂起中,它是设计到内核态和用户态的切换,开销较大
响应较慢,毕竟是要系统唤醒。
当线程获取锁失败的时候,线程不是进入阻塞,而是通过循环(自旋)状态不断的尝试获取锁。
这就是自旋锁。
优点嘛,比较明显
就是响应速度较快,毕竟没有挂起。
适合锁竞争不激烈,有锁但时间较短的场景下。
缺点嘛,同样也是比较明显的
自旋期间占用较高的CPU资源。此时可能导致CPU资源的浪费
在synchronized中,对此自旋锁也是自适应的,并在java1.6后就引入了。
公平锁 vs 非公平锁
公平锁:遵循“先来后到”原则,即谁先来,谁先等待,那么锁资源就会分配给谁
非公平锁:就是不遵循“先来后到”原则,即每个线程都有机会获取到锁。
举个例子
就比如,一个非常漂亮的妹子,即使有男朋友,此时身边还是有很多追求者,
例如A(追求两年)、B(追求了1年)、C(追求半年)
此时呢,这个漂亮的妹子分手了,那么接下来的追求者就有机会了。
那么如若这个女生按照公平锁这样的话,
此时A就上位了,毕竟A先来,并且还追求了两年
那么如若是这个女生按照非公平锁这样的话
那么此时A、B、C就都有机会,那么A、B、C就会出现“抢夺”这个女生的情况。
在java中,synchronized,它是非公平锁。
可重入锁 vs 不可重入锁
可重入锁:即允许同一个线程多次获取同一把锁而不会发生死锁
不可重入锁:即不允许同一个线程多次获取同一个锁。
Java里的synchronized是可重入锁。
读写锁
这个读写锁,它加锁的时候分两个情况
一个给读加锁
一个给写加锁
同时呢,这个读写锁也提供两种api,一个是加写锁、一个是加读锁。
但是解锁的api是一样的。
读写锁的出现,是为了应对这样的情况
情况:
一个线程读数据,一个线程写数据
两个线程都在写数据,此时呢,这两种情况有可能会出现多线程安全。
如若,一个线程加读锁,一个线程加写锁,会产生冲突
后者两个线程都加了写锁,此时呢也是会产生冲突的。
Java中的synchronized,它不是读写锁。
ok,那么一些锁策略类型,就介绍到这。
由此可见,java的synchronized是一个类型丰富的锁
1.可重入锁
2.非公平锁
3.乐/悲观锁 自适应
4.轻/重量级锁 自适应
5.自选挂起等待锁 自适应
那么这个自适应,到底是怎么自适应呢?
那么就不得不说synchronized的加锁过程了
synchronized加锁过程
1.锁升级
什么又是锁升级呢?
显然就是由小到大,由轻到重。
这个过程如下:
当使用这个synchronized进行加锁的时候,
此时会处于“偏向锁”状态
那这个偏向锁是什么呢?
它是对正在使用锁的线程进行一个标记,不会涉及真正的加锁,毕竟加锁操作也是会消耗一定资源的。
当线程之间出现竞争,就会升级为“轻量级锁”
继续统计竞争频率,频率上升到一定地步,那么又会升级成一个“重量级锁”
值得注意的是,这个锁升级的过程,它不是可逆的,只能一步步变“大”,变“重”
所以,这个锁升级过程可以表示为:
无锁==》偏向锁==》轻量级锁==》重量级锁
2.锁消除
那什么又是锁消除呢?
锁消除其实一种编译器优化策略。
就是编译器对你写出的代码,进行一定的判定,看看哪里不需要加锁。
如若是本来就是一种单线程环境的话,就不会涉及到多线程安全,此时呢,就会把锁给撤掉。
3.锁粗化
那么什么又是锁粗化呢?
其实呢,这个锁粗化也是一种编译器优化策略。
但是,理解这个锁粗话之前,先理解下锁的粒度。
锁的粒度就是指:一个锁控制代码块或其数据的大小
简单理解为:锁的代码块越大,粒度也就越粗。
但要值得注意的是,这个代码块的代码量,要考虑其引入的方法等。
回到锁粗化中。
锁粗化就是指,把多个细粒度的锁,合并成一个粗粒度的锁。
所以这个synchronized加锁过程中,会涉及到锁升级、锁消除、锁粗化等一些操作。
ok,关于锁策略呢,小编分享到这。