-
Notifications
You must be signed in to change notification settings - Fork 286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
关于同步的一点思考-下 #7
Comments
大佬,park之后实在是看不懂了,但是还是感谢大佬的分享! |
@zombiu 没事 以后有需要再回来看看 |
mutex锁不能保护cond->data修改的线程安全,调用signal的线程没有用mutex锁保护修改cond的那段临界区。 |
@11800222 为什么调用signal的时候不用mutex锁而是用cond锁呢? |
pthread_cond_wait/signal这一对本身用cond锁同步就能睡眠唤醒。 |
@11800222 明白了 感谢你的回答! |
很难说我哪儿看不懂,很大一部分依靠作者的注释才能看懂,但是注释又不能解释每个函数,看的太费劲了,如果能加上多个线程交互模拟出来的图会好一些 |
大佬, 作为java程序猿, 看c++ code表示太南了, 只能慢慢啃了, 准备先了解一下, 如果需要再来细读. |
好强!这就是强者的世界吗 |
竟然看懂了 - _ - |
@farmerjohngit 有个地方不太明白想请教一下,Parker::park()里面,对mutex的上锁为什么要用pthread_mutex_trylock而不是pthread_mutex_lock呢? |
这2句话没理解,调用signal 前需要申请mutex锁, 这时候修改cond不是受保护了吗?难道是担心有不申请mutex锁直接修改cond? |
针对cond锁 以下是我的理解仅供讨论: 1 、mutex 锁是保护用户自定义的资源的,就是写在 pthread_mutex_lock 与 pthread_cond_wait 两个函数中间的条件代码, cond 锁是保护 cond 数据的。这样语义更清晰。 2 调用pthread_cond_singnal 前也要申请mutex 锁,因为要修改用户自定义资源。理论上不用cond 锁是可行的,就是用 mutex锁同时保护这两个资源 3 在pthread_cond_wait 的实现中 调用futex_wait 是在while循环中,在futex_wait 前后分别解cond锁和加cond锁,放在while 循环是因为 futex_wait 可能会wait 失败而没有加入wait队列(传入的___futex值与内存位置的值不相等,别的线程调用了pthread_cond_wait、pthread_cond_singnal 都会使cond->__data.__futex 值加一),使用两个锁的原因就是各司其职,语义清晰,每个锁的范围小点减少锁冲突。 |
cond_timed_wait 代码怎么跟你看的不一样? |
睡眠之前有无锁的空隙啊,是把cond锁释放才睡眠的吧。 |
Thread-A-----------------------------------------------Thread-B |
在<关于同步的一点思考-上>中介绍了几种实现锁的方式以及linux底层futex的实现原理
ReentrantLock的实现网上有很多文章了,本篇文章会简单介绍下其java层实现,重点放在分析竞争锁失败后如何阻塞线程。
因篇幅有限,synchronized的内容将会放到下篇文章。
Java Lock的实现
ReentrantLock是jdk中常用的锁实现,其实现逻辑主语基于AQS(juc包中的大多数同步类实现都是基于AQS);接下来会简单介绍AQS的大致原理,关于其实现细节以及各种应用,之后会写一篇文章具体分析。
AQS
AQS是类AbstractQueuedSynchronizer.java的简称,JUC包下的ReentrantLock、CyclicBarrier、CountdownLatch都使用到了AQS。
其大致原理如下:
其中tryAcquire方法是抽象方法,具体实现取决于实现类,我们常说的公平锁和非公平锁的区别就在于该方法的实现。
ReentrantLock
ReentrantLock分为公平锁和非公平锁,我们只看公平锁。
ReentrantLock.lock会调用到ReentrantLock#FairSync.lock中:
FairSync.java
AbstractQueuedSynchronizer.java
可以看到FairSync.lock调用了AQS的
acquire
方法,而在acquire
中首先调用tryAcquire
尝试获得锁,以下两种情况返回true:重入
如果
tryAcquire
失败则调用acquireQueued
阻塞当前线程。acquireQueued
最终会调用到LockSupport.park()
阻塞线程。LockSupport.park
个人认为,要深入理解锁机制,一个很重要的点是理解系统是如何阻塞线程的。
LockSupport.java
park
方法的参数blocker是用于负责这次阻塞的同步对象,在AQS的调用中,这个对象就是AQS本身。我们知道synchronized关键字是需要指定一个对象的(如果作用于方法上则是当前对象或当前类),与之类似blocker就是LockSupport指定的对象。park
方法调用了native方法UNSAFE.park
,第一个参数代表第二个参数是否是绝对时间,第二个参数代表最长阻塞时间。其实现如下,只保留核心代码,完整代码看查看unsafe.cpp
park方法在os_linux.cpp中(其他操作系统的实现在os_xxx中)
park
方法用POSIX的pthread_cond_timedwait
方法阻塞线程,调用pthread_cond_timedwait
前需要先获得锁,因此park
主要流程为:pthread_mutex_trylock
尝试获得锁,如果获取锁失败则直接返回pthread_cond_timedwait
进行等待pthread_mutex_unlock
释放锁另外,在阻塞当前线程前,会调用
OSThreadWaitState
的构造方法将线程状态设置为CONDVAR_WAIT
,在Jvm中Thread状态枚举如下Linux的timedwait
由上文我们可以知道LockSupport.park方法最终是由POSIX的
pthread_cond_timedwait
的方法实现的。我们现在就进一步看看
pthread_mutex_trylock
,pthread_cond_timedwait
,pthread_mutex_unlock
这几个方法是如何实现的。Linux系统中相关代码在glibc库中。
pthread_mutex_trylock
先看trylock的实现,
代码在glibc的
pthread_mutex_trylock.c
文件中,该方法代码很多,我们只看主要代码mutex默认用的是
PTHREAD_MUTEX_NORMAL
类型(与PTHREAD_MUTEX_TIMED_NP
相同);因此会先调用
lll_trylock
方法,lll_trylock
实际上是一个cas操作,如果mutex->__data.__lock==0则将其修改为1并返回0,否则返回1。如果成功,则更改mutex中的owner为当前线程。
pthread_mutex_unlock
pthread_mutex_unlock.c
pthread_mutex_unlock
将mutex中的owner清空,并调用了lll_unlock
方法lowlevellock.h
lll_unlock
分为两个步骤:FUTEX_WAIT
在休眠,所以通过调用系统函数FUTEX_WAKE
唤醒休眠线程FUTEX_WAKE
在上一篇文章有分析,futex机制的核心是当获得锁时,尝试cas更改一个int型变量(用户态操作),如果integer原始值是0,则修改成功,该线程获得锁,否则就将当期线程放入到 wait queue中,wait queue中的线程不会被系统调度(内核态操作)。futex变量的值有3种:0代表当前锁空闲,1代表有线程持有当前锁,2代表存在锁冲突。futex的值初始化时是0;当调用try_lock的时候会利用cas操作改为1(见上面的trylock函数);当调用
lll_lock
时,如果不存在锁冲突,则将其改为1,否则改为2。pthread_cond_timedwait
pthread_cond_timedwait
用于阻塞线程,实现线程等待,代码在glibc的
pthread_cond_timedwait.c
文件中,代码较长,你可以先简单过一遍,看完下面的分析再重新读一遍代码上面的代码虽然加了注释,但相信大多数人第一次看都看不懂。
我们来简单梳理下,上面代码有两把锁,一把是mutex锁,一把cond锁。另外,在调用
pthread_cond_timedwait
前后必须调用pthread_mutex_lock(&mutex);
和pthread_mutex_unlock(&mutex);
加/解mutex锁。因此
pthread_cond_timedwait
的使用大致分为几个流程:pthread_cond_timedwait
调用前)pthread_cond_timedwait
调用后)看到这里,你可能有几点疑问:为什么需要两把锁?mutex锁和cond锁的作用是什么?
mutex锁
说mutex锁的作用之前,我们回顾一下java的Object.wait的使用。Object.wait必须是在synchronized同步块中使用。试想下如果不加synchronized也能运行Object.wait的话会存在什么问题?
如上代码,A线程调用waitTest,这时flag==0,所以准备调用wait方法进行休眠,这时B线程开始执行,调用notifyTest将flag置为1,并调用notify方法,注意:此时A线程还没调用wait,所以notfiy没有唤醒任何线程。然后A线程继续执行,调用wait方法进行休眠,而之后不会有人来唤醒A线程,A线程将永久wait下去!
在有锁保护下的情况下, 当调用condObj.wait时,flag一定是等于0的,不会存在一直wait的问题。
回到
pthread_cond_timedwait
,其需要加mutex锁的原因就呼之欲出了:保证wait和其wait条件的原子性不管是glibc的
pthread_cond_timedwait
/pthread_cond_signal
还是java层的Object.wait
/Object.notify
,Jdk AQS的Condition.await
/Condition.signal
,所有的Condition机制都需要在加锁环境下才能使用,其根本原因就是要保证进行线程休眠时,条件变量是没有被篡改的。注意下mutex锁释放的时机,回顾上文中
pthread_cond_timedwait
的流程,在第2步时就释放了mutex锁,之后调用futex_wait
进行休眠,为什么要在休眠前就释放mutex锁呢?原因也很简单:如果不释放mutex锁就开始休眠,那其他线程就永远无法调用signal方法将休眠线程唤醒(因为调用signal方法前需要获得mutex锁)。在线程被唤醒之后还要在第10步中重新获得mutex锁是为了保证锁的语义(思考下如果不重新获得mutex锁会发生什么)。
cond锁
cond锁的作用其实很简单: 保证对象
cond->data
的线程安全。在
pthread_cond_timedwait
时需要修改cond->data
的数据,如增加__total_seq(在这个cond上一共执行过多少次wait)增加__nwaiters(现在还有多少个线程在wait这个cond),所有在修改及访问cond->data
时需要加cond锁。这里我没想明白的一点是,用mutex锁也能保证
cond->data
修改的线程安全,只要晚一点释放mutex锁就行了。为什么要先释放mutex,重新获得cond来保证线程安全? 是为了避免mutex锁住的范围太大吗?该问题的答案可以见评论区@11800222 的回答:
如何唤醒休眠线程
唤醒休眠线程的代码比较简单,主要就是调用lll_futex_wake。
End
本文对Java简单介绍了ReentrantLock实现原理,对LockSupport.park底层实现
pthread_cond_timedwait
机制做了详细分析。看完这篇文章,你可能还会有疑问:Synchronized锁的实现和ReentrantLock是一样的吗?Thread.sleep/Object.wait休眠线程的原理和LockSupport.park有什么区别?linux内核层的futex的具体是如何实现的?
这些问题,之后的文章会一一解答,尽请期待~
The text was updated successfully, but these errors were encountered: