常见的锁策略
常见的锁策略
定义:处理冲突的过程中,设计到不同的处理方式.
乐观锁VS悲观锁
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁.(在加锁之前,预估当前锁冲突出现的概率比较大,因此加锁的时候就会做更多工作).
特性:加锁开销大,加锁速度更慢,但是整个过程不容易出现问题.
乐观锁
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何做.(加锁之前预估当前出现锁冲突的概率不大,因此在加锁时不会做太多工作.)
特性:加锁开销小,加锁速度更快,但可能引入一些其他问题(消耗更多cpu资源).
重量级锁VS轻量级锁
锁的核心特性"原子性",这样的机制追根溯源是CPU这样的硬件设备提供的.
CPU提供了"原子操作指令".
操作系统基于CPU的原子指令,实现了mutex互斥锁.
JVM基于操作系统提供的互斥锁,实现了synchronized和ReentrantLock等关键字和类.
注意:synchronized并不仅仅对mutex进行封装,在synchronized内部还进行了很多其它的工作.
重量级锁
加锁机制重度依赖了OS提供的mutex
大量的内核态用户转换
很容易引发线程的调度
这两个操作,成本比较高,一旦涉及到用户态和内核态的转换,就意味着沧海桑田.
加锁的开销更大,加锁速度更慢->重量级锁,一般就是悲观锁.
轻量级锁
加锁机制尽量不适用mutex,而是尽量在用户态代码完成,实在搞不定了,再使用mutex.
少量的内核态用户态转换
不太容易引发线程调度.
加锁的开销更小,加锁的速度更快->轻量级锁,一般就是乐观锁.
注:
轻量重量是加锁之后,对结果的评价.
悲观乐观是加锁之前,对未发生的事情进行的预估.
整体来说,这两种角度,描述的是同一件事情.
自旋锁VS挂起等待锁
自旋锁
按之前的方式,线程在抢锁失败后进入阻塞状态,放弃cpu,需要过很久才能再次被调度.
但实际上,大部分情况下,虽然当前抢锁失败,但过不了多久,锁就会被释放.没必要放弃cpu.这个时候就可以使用自旋锁来解决这样的问题.
反复快速执行的过程-->自旋.
一旦其它线程释放锁,能第一时间拿到锁.使用自旋的前提就是预期锁冲突概率不大,其它线程释放了锁,就能第一时间拿到.? 但如果万一当前加锁的线程特别多,自旋的意义就不大,白白浪费cpu.
自旋锁的伪代码:
while (抢锁(lock) == 失败) {}
如果获取失败,立即再尝试获取到锁,无限循环,直到获取到锁为止.第一次获取锁失败,第二次的尝试会在极短时间内到来.
一旦锁被其它线程释放,就能第一时间获取到锁.
自旋锁是轻量级锁的典型表现.
优点:没有放弃cpu,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁.
缺点:如果锁被其它线程持有的时间比较久,那么就会持续地消耗cpu资源.
挂起等待锁
挂起等待锁就是我们之讲过的wait(),notify()方法的那一部分.通常涉及到线程的同步和协调.
详细见等待和通知-CSDN博客
挂起等待锁是悲观锁的典型体现.?
因此可以适用于锁冲突激烈的情况
挂起等待锁是重量级锁的典型体现.
进行挂起等待时,就需要内核调度器介入了,这一块完成的操作就多了.?
优点:使线程不需要再循环中不断地检查某个条件是否满足,这有助于节省CPU资源.可有效减少线程对共享资源的竞争,提高程序执行效率.
缺点:如果不谨慎使用挂起等待锁,可能导致死锁的发生,即多个线程相互等待对方释放资源,但彼此无法继续执行.
公平锁VS非公平锁
假设三个线程A,B,C.? ?A先尝试获取锁,获取成功.然后B再尝试获取锁,获取失败,阻塞等待;然后C也尝试获取锁,C也获取失败,也阻塞等待.
当线程A释放锁的时候,会发生啥呢?
公平锁:遵守"先来后到".B比C先来的.当A释放锁之后,B就能先于C获得到锁.
非公平锁:不遵守"先来后到".B和C都有可能获取到锁.
注意:
操作系统内部的线程调度可以看作是随机的.如果不做任何额外的限制,锁就是非公平锁.如果要实现公平锁,就需要依赖额外的数据结构(队列),来记录线程们的先后顺序.(因此,使用公平锁,天然就可以避免线程饿死问题).
读写锁
多线程之间,数据的读取方之间都不会产生线程安全的问题,但数据的写入方互相之间以及和读者之间都需要进行互斥.如果两种场景下都用同一个锁,就会产生极大的性能消耗.所以读写锁应运而生.
比如两个线程读本身就是线程安全的,不需要互斥,而且大部分操作都是读.
如使用synchronized加锁,两线程读会互斥,产生阻塞,造成性能消耗.
读写锁就能将并发读之间的锁冲突开销省下了,对于性能提升就明显了.
读写锁分为两种情况:(1)加读锁.(2)加写锁.
一个线程加读锁,另一个线程只能读不能写.
一个线程加写锁,另一个线程不能读也不能写.
Java标准库提供了ReentrantReadWriteLock类,这里不介绍,请自行查阅.
相关面试题
1.你是如何理解乐观锁和悲观锁的,具体怎么实现的?
悲观锁认为多个线程访问同一个共享变量冲突概率较大,会每次在访问共享变量之前都去真正加锁.乐观锁认为多个线程访问同一个共享变量冲突概率不大.并不会真正加锁,而是尝试访问数据.在访问的同时识别当前的数据是否会出现访问冲突.
悲观锁的实现就是先加锁(比如借用系统中的mutex),获取到锁再操作数据.获取不到锁就等待.?
2.介绍一下读写锁.
读写锁就是将读操作和写操作分开进行加锁.
读锁和读锁之间不互斥
写锁和写锁之间互斥
读锁和写锁之间互斥
读写锁主要用在"频繁读,不频繁写"的场景中.?
3.什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
如果获取锁失败,立即尝试再次获取到锁,无限循环,直到获取到锁为止.第一次获取锁失败,第二次的尝试会在极短的时间内到来.一旦锁被其它线程释放,就能第一时间获取到锁.
相比于挂起等待锁.
优点:没有放弃CPU资源,一旦锁被释放就能第一时间获取到锁,更高效.在锁持有时间比较短的场景下非常有用.
缺点:如果锁的持有时间较长,就会浪费CPU资源.?
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!