首页 > 编程知识 正文

互斥锁和排他锁,java并发面试题

时间:2023-05-04 01:28:51 阅读:39387 作者:1598

这个问题很好啊。 锁定是常见的同步概念,我听说过“锁定”或“解锁”。 当然学术上)和acquire )发布)。

正好pthread包含这些锁的API,而C 11只包含其中的一部分。 现在通过pthread的API展开回答。

互斥量)。

多重排除(mutex )即排他量)排他体)。 这是人们常说的互斥锁。

名称中不包含lock,但是叫它lock也没什么问题。 mutex无疑是最常见的多线程同步方案。 其思想简单粗暴,多线程共享互斥量,线程之间竞争。 锁定的线程可以进入关键节执行代码中。

//声明互斥量pthread_mutex_t mtx

//初始化//Pthread_mutex_init(MTX,NULL )

//锁定//Pthread_mutex_lock(MTX )

//Pthread_mutex_unlock(MTX )解锁;

销毁//Pthread_mutex_destroy(MTX )

mutex是“休眠等待”(sleep waiting )类型的锁定,如果线程无法夺取独占锁定,线程将进入休眠状态。 好处是节省CPU资源,缺点是休眠唤醒需要一点时间。 从Linux 2.6版开始,mutex完全通过futex的API实现,大大减少了内部系统调用开销。

有趣的是,pthread锁定通常包含trylock函数。 例如,对于排他量:

ret=pthread_mutex_trylock(MTX;

if(0==ret ) { //yqdyl工作.

pthread_mutex_unlock(MTX;

}elseif(ebusy==ret )//锁定正在使用;

}

pthread_mutex_trylock用于在非块模式下请求独占量。 施加锁定也有同样的非块模式,就像各种IO函数有noblock的模式一样。

当线程尝试锁定时,如果该锁定已被其他线程锁定,则该线程将被阻止,直到acquire成功。 但是有时候我们不希望这样。 如果锁定到其他线程,pthread_mutex_trylock将返回特殊的错误代码。 yqdyl返回0,成功了,但只有成功了,我们才能解锁并在后面进行解锁操作!

C 11开始部署多线程库。 其中还包括互斥的API:std:muxtex。

另外,根据同一线程是否可以多次锁定,排他量分为被称为“递归排他量”recursive mutex和“可重新锁定”reentrant lock的两种

即,也称为"非递归互斥量" non-recursive mute ()、"不可再锁定" non-reentrant mutex

同一线程多次锁定非递归独占量可能会导致死锁。 递归排他量没有这个风险。 C 11有递归独占量的API:STD :3360 recursive _ mutex。 对于pthread,可以通过将PTHREAD_MUTEX_RECURSIVE属性添加到mutex中来使用递归独占量。

//声明互斥量pthread_mutex_t mtx

//声明互斥量的属性变量pthread_mutexattr_t mtx_attr

//初始化互斥量的属性变量pthread _ mutexattr _ init (MTX _ attr );

//递归独占量属性pthread _ mutexattr _ settype (MTX _ attr,PTHREAD_MUTEX_RECURSIVE );

//向互斥量pthread_mutext_init(MTX,mutext_attr )分配属性;

但是,递归排他量,或者可再锁定的使用需要抑制。 Stevens大神生前在《APUE》说:“使用它是十分棘手的,只有在没有其他解决方案的情况下才能使用。”

“可锁定”概念和称谓的人气往往是Java语言的功劳。

条件变量)

标题的条件锁定应该是指条件变量。 请注意,条件变量不是锁定,而是线程之间的通信机制,几乎总是与互斥量一起使用。 因此,互斥量和条件变量一般是成套出现的。 例如,C 11也有条件变量API:STD :3360 condition _ variable。

对于pthread :

//声明互斥量pthread_mutex_t mtx

//声明条件变量pthread_cond_t cond

.

//初始化//Pthread_mutex_init(MTX,NULL )

Phread_cond_init(cond,NULL );

锁定pthre

ad_mutex_lock(&mtx);

// yqdyl功,等待条件变量触发pthread_cond_wait(&cond, &mtx);

...

// 加锁pthread_mutex_lock(&mtx);

pthread_cond_signal(&cond);

...

// 解锁pthread_mutex_unlock(&mtx);

// 销毁pthread_mutex_destroy(&mtx);

pthread_cond_wait函数会把条件变量和互斥量都传入。并且多线程调用的时候条件变量和互斥量一定要一一对应,不能一个条件变量在不同线程中wait的时候传入不同的互斥量。否则是未定义结果。

关于是先解锁互斥量还是先进行条件变量的通知,是另外一个比较大的议题。有种论断说:先解锁互斥量再通知条件变量可以减少多余的上下文切换,进而提高效率。这种说法是基于一种实现假设:先通知条件变量,再解锁。可能让其他等待条件变量的线程被唤醒了,但是此时互斥量还没解锁,从而再次陷入休眠。然而对于另外一些实现,比如Linux系统,则通过等待变形(wait morphing)

另外在使用条件变量的过程中有个稍微违反直觉的写法:那就是使用while而不是if来做判断状态是否满足。这样做的原因有二:避免惊群;

避免某些情况下线程被虚假唤醒(即没有pthread_cond_signal就解除了阻塞)。

比如半同步/半reactor的网络模型中,在工作线程消费fd队列的时候:

while (1) {

if (pthread_mutex_lock(&mtx) != 0) { // 加锁 ... // 异常逻辑 }

while (!queue.empty()) {

if (pthread_cond_wait(&cond, &mtx) != 0) {

... // 异常逻辑 }

}

auto data = queue.pop();

if (pthread_mutex_unlock(&mtx) != 0) { // 解锁 ... // 异常逻辑 }

process(data); // 处理流程,业务逻辑}

以上伪码摘自我的文章:果冻虾仁:高山仰之可极,谈半同步/半异步网络并发模型​zhuanlan.zhihu.com

read-write lock(读写锁)

鳗鱼钢笔『读写锁』就是对于临界区区分读和写。在读多写少的场景下,不加区分的使用互斥量显然是有点浪费的。此时便该上演读写锁的拿手好戏。

读写锁有一个别称叫『共享-独占锁』。不过单看『共享-独占锁』或者『读写锁』这两个名称,其实并未区分对于读和写,到底谁共享,谁独占。可能会让人误以为读写锁是一种更为泛化的称呼,其实不是。读写锁的含义是准确的:是一种 读共享,写独占的锁。

读写锁的特性:当读写锁被加了写锁时,其他线程对该锁加读锁或者写锁都会阻塞(不是失败)。

当读写锁被加了读锁时,其他线程对该锁加写锁会阻塞,加读锁会成功。

因而适用于多读少写的场景。

// 声明一个读写锁pthread_rwlock_t rwlock;

...

// 在读之前加读锁pthread_rwlock_rdlock(&rwlock);

... 共享资源的读操作

// 读完释放锁pthread_rwlock_unlock(&rwlock);

// 在写之前加写锁pthread_rwlock_wrlock(&rwlock);

... 共享资源的写操作

// 写完释放锁pthread_rwlock_unlock(&rwlock);

// 销毁读写锁pthread_rwlock_destroy(&rwlock);

其实加读锁和加写锁这两个说法可能会造成误导,让人误以为是有两把锁,其实读写锁是一个锁。所谓加读锁和加写锁,准确的说法可能是『给读写锁加读模式的锁定和加写模式的锁定』。

读写锁和互斥量一样也有trylock函数,也是以非阻塞地形式来请求锁,不会导致阻塞。

pthread_rwlock_tryrdlock(&rwlock)

pthread_rwlock_trywrlock(&rwlock)

C++11中有互斥量、条件变量但是并没有引入读写锁。而在C++17中出现了一种新锁:std::shared_mutex。用它可以模拟实现出读写锁。demo代码可以直接参考cppreference:std::shared_mutex - cppreference.com​en.cppreference.com

另外多读少写的场景有些特殊场景,可以用特殊的数据结构减少锁使用:多读单写的线性数据。用数组实现环形队列,避免vector等动态扩张的数据结构,写在结尾,由于单写因而可以不加锁;读在开头,由于多读(避免重复消费)所以需要加一下锁(互斥量就行)。

多读单写的KV。可以使用双缓冲(double buffer)的数据结构来实现。double buffer同名的概念比较多,这里指的是foreground 和 backgroud 两个buffer进行切换的『0 - 1切换』技术。比如实现动态加载(热加载)配置文件的时候。可能会在切换间隙加一个短暂的互斥量,但是基本可以认为是lock free的。

我一张口,你就会发现:无非也就是空间换时间的老套路了。

spinlock(自旋锁)

自旋之名颇为玄妙,第一次听闻常让人略觉高大。但和无数个好似『故意把简单概念复杂化』的计算机术语一样,自旋锁的本质简单的难以置信。

要了解自旋锁,首先了解自旋。什么是自旋(spin)呢?更为通俗的一个词是『忙等待』(busy waiting)。最最通俗的一个理解,其实就是死循环……。

单看使用方法和使用互斥量的代码是差不多的。只不过自旋锁不会引起线程休眠。当共享资源的状态不满足的时候,自旋锁会不停地循环检测状态。因为不会陷入休眠,而是忙等待的方式也就不需要条件变量。

这是优点也是缺点。不休眠就不会引起上下文切换,但是会比较浪费CPU。

// 声明一个自旋锁变量pthread_spinlock_t spinlock;

// 初始化pthread_spin_init(&spinlock, 0);

// 加锁pthread_spin_lock(&spinlock);

// 解锁pthread_spin_unlock(&spinlock);

// 销毁pthread_spin_destroy(&spinlock);

pthread_spin_init函数的第二个参数名为pshared(int类型)。表示的是是否能进程间共享自旋锁。这被称之为Thread Process-Shared Synchronization。互斥量的通过属性也可以把互斥量设置成进程间共享的。pshared有两个枚举值:PTHREAD_PROCESS_PRIVATE:仅同进程下读线程可以使用该自旋锁

PTHREAD_PROCESS_SHARED:不同进程下的线程可以使用该自旋锁

在Linux上的glibc中这两个枚举值分别是0和1(Mac上不是)。所以通常也会看到直接传0的代码。你可能觉得不使用宏,直接用数字硬编码不是一个好习惯。的确,妥妥的Magic Number,但还有一个有趣的事实你需要了解:并不是所有实现都支持自旋锁设置两种pshared。比如

int pthread_spin_init (pthread_spinlock_t *lock, int pshared) {

/* Relaxed MO is fine because this is an initializing store. */

atomic_store_relaxed (lock, 0);

return0;

}

所以直接传0可能也无伤大雅。

自旋锁 VS 互斥量+条件变量 孰优孰劣?肯定要看具体的使用场景,(我好像在说片汤话)。ckdbb不知道在你的使用场景下这两种锁该用哪个的时候,那就是用互斥量吧!或者通过压测的判断,不过大多数时候我们好像并不需要这么一个pthread的自旋锁,知友们可以提供一些自旋锁的使用参考。

内容太多,难免有误,望大家指教。

课后思考:

你还知道哪些锁类型?

或者哪些线程同步机制(不一定叫锁)?

参考

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。