锁选择不当带来的影响:在高并发环境下,如果锁的选取不当,直接带来的影响是,系统的吞吐量上不去,高并发成为空谈。
一、锁的分类
在底层实现上来讲,锁一共分为两类:互斥锁和自旋锁。其他上层锁都是基于底层的这两类锁来实现的。
互斥锁:加锁成本比较高,是在内核态完成的,要经过两次上下文切换,第一次是加锁失败后线程进入休眠释放cpu资源,第二次是等锁释放后,通知失败线程获取锁恢复执行。一次上下文切换的时间大概在几纳秒或者几十微秒,可能要比锁住的代码段执行的时间要长。如图所示:
自旋锁:自旋锁正好相反,加锁失败后,并不会释放cpu资源,不会进入休眠状态。而是开启一个循环调用,通过CPU提供的函数CAS(compare and swap)在用户态完成加锁和锁的释放。这种方式也通常称之为“忙等待”。例如java中读写锁的实现:如图
二、悲观锁vs乐观锁(无锁编程)
不论是互斥锁、自旋锁,还是基于以上两种锁实现的上层锁如读写锁等,都是悲观锁。悲观锁认为同时修改资源的概率比较高,因此在访问资源前,先进行加锁,总体效率会比较高。乐观锁,更是一种乐观的思想,认为并发冲突很小,无需加锁,在更新数据时做判断,通常有两种实现方式:版本号、上文提到的CAS(注意上文使用cas是加锁,这里是直接改数据,并没有锁),这种思想也被称为无锁编程。
三、互斥锁、自旋锁、乐观锁(无锁编程)该如何选择
读到这里,我们知道了,互斥锁是通过“线程切换”来应对获取锁失败,自旋锁是通过“忙循环”不释放cpu资源两种不同的思想方式。其他上层的锁应用都是基于这两类锁实现的。java中我们常用的读写锁既可以通过互斥锁实现,也可以通过自旋锁实现。读写锁的优势在于:在写锁没有持有时,多线程可以并发的持有读锁,提高了共享资源的使用率。
根据业务场景选型
如果针对我们的业务,能够明确的区分出“读”和“写”的场景,那么可以使用读写锁。如果能根据埋点统计读写的比例,当读比例比较高时,可以使用jdk8提供的StampedLock,该版本对可以实现乐观读,通过增加了版本号的方式,节省了cas忙循环的等待,因为没有cas操作所以效率会比较高。
当我们不能预判出被锁住代码的执行时长,或执行时长比较长,则适合使用悲观锁
如果我们确定被锁住代码的执行时长比较短,可以选取自旋锁
当并发冲突比较低的场景下,使用乐观锁进行无锁编程。
领取专属 10元无门槛券
私享最新 技术干货