《蹲坑也能进大厂》多线程系列 - 自旋锁/共享锁/独占锁详解

Posted 花哥编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《蹲坑也能进大厂》多线程系列 - 自旋锁/共享锁/独占锁详解相关的知识,希望对你有一定的参考价值。

前言

多线程系列我们前面已经更新过很多章节,强烈建议小伙伴按照顺序学习:

《蹲坑也能进大厂》多线程系列文章目录[1]

在上中两篇文章我们介绍了悲观/乐观锁、可重入/不可重入锁、公平/非公平锁等概念,今天继续介绍剩下几种常见的锁。

正文

锁的总览

锁可以从不同的角度进行分类,这些分类并不是互斥的,比如一个人既可以是医生,又可以是父亲,也可以是一样女人。分类总览如图:

自旋锁和非自旋锁

概念

再介绍一下什么是自旋,它是指当线程获取资源失败时,不会进入阻塞,而是一直在原地循环,不断尝试获取锁,直到锁被释放,这个等待的线程马上就可以去获得锁。

自旋锁:多个线程申请锁时,首先会尝试获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁。此类型的锁就是自旋锁。

非自旋锁:当某线程无法获取锁时,该线程会被直接挂起,并且不再消耗CPU,当其他线程释放锁后,被挂起的线程会被唤醒。此类型锁即为非自旋锁。

这里用一张流程图来看会更加形象。

使用场景

多核处理器中,如果线程等待锁的时间很短,短到比线程两次上下文切换时间要少的情况下,此时应当用非自旋锁。如果预计线程等待锁的时间较长,至少比两次线程上下文切换的时间要长,建议使用非自旋锁。单核处理器中,一般建议不要使用自旋锁。因为在同一时间只有一个线程是处在运行状态,那如果运行线程发现无法获取锁,只能等待解锁,但因为自身不挂起,所以那个获取到锁的线程没有办法进入运行状态,只能等到运行线程把操作系统分给它的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。如果加锁的代码经常被调用,但竞争情况很少发生时,应该优先考虑使用自旋锁,自旋锁的开销比较小,互斥量的开销较大。

自旋锁优缺点

自旋锁减少了线程在阻塞和唤醒之间切换的频率,那么就是降低了操作系统在用户态和内核态的切换,降低了cpu的损耗,提高了线程在竞争期间的性能。 自旋锁的时间无法判断,如果线程持有锁的时间短,小于或者等于自旋的时间,那么就很舒服,自旋过了刚好获得了锁,但是很多时候这个锁持有时间长短就要看代码设计了,所以无法预测。

共享锁和独占锁

概念

共享锁(读锁) 是一种乐观锁,它允许一个资源可以同时被多个读操作访问,或者被一个 写操作访问,但是两者不能同时进行。java的并发包中提供了ReentrantReadWriteLock

独占锁(写锁) 是一种悲观的加锁策略,同一时刻只能被一个线程拥有。对ReentrantLockSynchronized而言都是独占锁。如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

对ReentrantReadWriteLock,其读锁是共享锁,其写锁是独占锁。读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。这里代码演示ReentrantReadWriteLock的读写锁使用:

public class ReentrantReadWriteLockDemo {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

public static void main(String[] args) {
//两个读操作,两个写操作
new Thread(()->read(),"Thread-1").start();
new Thread(()->read(),"Thread-2").start();
new Thread(()->write(),"Thread-3").start();
new Thread(()->write(),"Thread-4").start();
}
//读操作
public static void read(){
readLock.lock();
System.out.println(Thread.currentThread().getName()+"正在执行【读锁】");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"正在释放【读锁】");
readLock.unlock();
}
}
//写操作
public static void write(){
writeLock.lock();
System.out.println(Thread.currentThread().getName()+"正在执行【写锁】");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName()+"正在释放【写锁】");
writeLock.unlock();
}
}
}

执行结果:

结果分析:同一时间可以执行多个读操作,但是写操作只能排队执行。

读写锁优点

在没有读写锁之前,虽然可以保证线程安全,但是由于多个读操作不能同时进行,造成资源浪费。在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是没有阻塞的,极大的提高了程序的执行效率。

使用规则

多个线程只申请读锁,都可以申请到如果一个线程已经占用了读锁,其他申请写锁的线程会阻塞,知道读锁被释放如果一个线程占用了写锁,此时其他线程想申请写锁或读锁,都会被阻塞。总结就是要么多读(同一时刻有多个读操作),要么一写(同一时刻有一个写操作),两者不会同时出现。

可中断锁和不可中断锁

定义

可中断锁顾名思义就是可以中断的锁。如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

不可中断锁也就是和可中断锁性质相反,在等待过程中不可以中断自己或被其他线程中断。Java中Synchronized就是不可中断锁。

总结

以上就是关于的介绍,目前的分类分为三个章节全部讲完了,这些知识理论性较强,需要慢慢消化,同时篇幅有限,要想真正掌握还需要多拓展,多尝试在不同场景使用不同类型的锁。

点关注,不迷路

以上就是本期全部内容,如有纰漏之处,请留言指教,非常感谢。我是花GieGie ,有问题大家随时留言讨论 ,我们下期见

以上是关于《蹲坑也能进大厂》多线程系列 - 自旋锁/共享锁/独占锁详解的主要内容,如果未能解决你的问题,请参考以下文章

多线程编程之自旋锁

多线程编程之自旋锁

JUC - 多线程之悲观锁乐观锁,读写锁(共享锁独享锁),公平非公平锁,可重入锁,自旋锁,死锁

iOS - 互斥锁&&自旋锁 多线程安全隐患(转载)

多线程编程之读写锁

「动手实践系列之自旋锁设计」如何用Go优雅的实现高性能自旋锁