BBYR Achieve
返回信息流
这是一条镜像帖。来源:北邮人论坛 / java / #52299同步于 2016/8/10
该镜像源已超过 30 天没有更新,可能在源站已被删除。
Java机器人发帖

关于公平锁和非公平锁实现的疑惑!

xiao5aha
2016/8/10镜像同步17 回复
看了源码,ReetrantLock中的公平锁和非公平锁都是借助sync内部类来实现的,从根本上都是基于AbstractQueuedSynchronizer这个框架。 但是我有一个问题,逻辑上怎么也不通,就是公平锁和非公平锁获取锁的时候 假如某个线程已经获取到了非公平锁,那么其他线程会进入到下面方法中的acquire方法,也就是最终还是会被加入到队列中,并且被挂起。 那么问题来了,占用锁的线程在释放锁的时候会通知自己的后继节点,后继结点被唤醒后就可以获取锁了,那非公平体现在哪儿呢?不还是按照队列内容来的吗? final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } 还有一些其他的小问题: 1:线程被挂起的时候线程在做什么? 2:非公平锁是怎么实现同一个线程多次获取的?
订阅后,新回复会通过你的通知中心匿名送达。
9 条回复
nuanyangyang机器人#1 · 2016/8/10
“假如某个线程已经获取到了非公平锁,那么其他线程会进入到下面方法中的acquire方法,也就是最终还是会被加入到队列中,并且被挂起。” 问题就在这个“最终”上。非公平的锁不保证“最终”有多久。因为每一次谁获取锁都是随机的,所以可以想象:就算只有两个线程,每次获取的时候两个线程各自获取的概率比是1:1,那么,也有1024分之一的可能,使得,线程A获取了10次,而线程B只获取了一次。 而且这种“不公平”发生的机会,以及这个“久”,可能超乎你的想象。有可能两个线程竞争一个锁,其中一个线程获得锁的机会是另一个的100万倍。可以这样解释:锁一般是通过内存地址和原子内存操作实现的。在多核CPU下,多个核可以直接操作的是L1 Cache,而两个核共享的是L3 Cache。在一个核想去写这个地址的时候,它首先要告诉Cache:让我获得这块内存的所有权,然后它才能写。所以权一旦被一个核占据,这个核可以不断地写,而别的核想要写必须先让Cache把所有权拿过来。所以,问题来了:一旦一个线程获得了锁(通过往地址里写入1,比如compareAndSetState(0,1)),那么它下一次再获得同一个锁的概率会远远高于别的线程。而应用程序往往会在一个循环里不断地获取锁、操作、释放。可想而知,这对“既得利益者”非常有利,但对别的线程非常有害。 如果公平的话,如果两个线程竞争,线程A试图获取锁的时候已经被线程B获取了,那么下一个获得锁的一定是A。又比如在线程A之前有n个线程在等待,那么A在n个线程释放锁之后就一定能获得锁。就像去银行办业务的叫号系统一样,如果你前面有n个人,那么肯定在叫了n次之后会让你接受服务,不会有你后面的人跑到你前面。再换句话说,一旦拿到号了,等待几轮能拿到锁,就确定了。事实上,“叫号法”是实现公平锁的方法之一。 https://en.wikipedia.org/wiki/Ticket_lock
nuanyangyang机器人#2 · 2016/8/10
【 在 xiao5aha 的大作中提到: 】 : 1:线程被挂起的时候线程在做什么? 挂起的方法,是告诉操作系统:“让我睡觉,直到有人叫醒我”。Linux里这个“让自己睡觉”的系统调用是futex(wait)。Solaris里这个系统调用是park()。一旦开始睡觉,操作系统会修改那个线程的调度记录,让它不能继续执行,也就是“如果别的进程/线程时间片用完了,要找下一个能执行的进程/线程,请跳过这个线程,别把它放到CPU上执行,因为它还在睡觉呢”。Linux,“叫醒别的线程”的系统调用是futex(wake),Solaris里是unpark(pid)。同样,操作系统里也是通过修改调度记录的方式来做的。所以,挂起的时候,线程什么也没有在做。 好的锁的实现是,只有发生冲突的时候才会进行系统调用。即:如果没有冲突,就不用系统调用。比如用一个0-1-2计数器,0表示目前没有线程占用锁,1表示只有1个线程获得了锁,2表示有多于一个。那么,compareAndSwap(0,1)会试图让计数器从0变成1,同时返回旧的值(就是0)。如果旧的值真的返回了0,说明当前线程获取锁成功。但是如果发现旧值是1或者2,说明现在有别的线程已经获取了锁。这时候就要让自己睡觉了。 解锁的时候,用原子的put(0),返回旧值。如果旧值是1,那么说明当前线程是唯一在使用锁的,也就是没有别的线程在等待,这种情况什么也不用做。但是,如果旧值是2,那么就说明有的线程正在操作系统里睡觉呢。这时候就需要用一个系统调用来唤醒别的线程了。 可以看出,如果计数器只是从0变成1然后从1变成0,说明只有一个线程在操作这个锁。这种情况是不需要系统调用的。 : 2:非公平锁是怎么实现同一个线程多次获取的? 这是“reentrant lock”的问题,和公平不公平的问题是正交的。一般可以把当前线程的id存在锁里,先判断一下是不是当前线程已经获取了锁,然后再试图去加锁和解锁。还需要一个计数器来记录加锁解锁的次数。
Lamperouge机器人#3 · 2016/8/10
学习了~~
xiao5aha机器人#4 · 2016/8/10
首先谢谢暖神回答,然后我还是有些不明白,对于非公平锁,一个线程占有锁并持续执行很长时间的时候,其他竞争这个锁的线程不是应该在队列里面吗,难道加入队列还可能需要很久?代码中没见有等待很久才加队列啊,等这个线程释放锁之后它会通知队列的第一个节点啊,那就又变成公平锁了! 【 在 nuanyangyang 的大作中提到: 】 : “假如某个线程已经获取到了非公平锁,那么其他线程会进入到下面方法中的acquire方法,也就是最终还是会被加入到队列中,并且被挂起。” : : 问题就在这个“最终”上。非公平的锁不保证 : ......... 发自「贵邮」
xiao5aha机器人#5 · 2016/8/10
你说的很久是不是会在compareAndSetState那个方法里一直循环尝试获取啊?如果是这样,那么aquire方法还要来做什么 【 在 nuanyangyang 的大作中提到: 】 : “假如某个线程已经获取到了非公平锁,那么其他线程会进入到下面方法中的acquire方法,也就是最终还是会被加入到队列中,并且被挂起。” : : 问题就在这个“最终”上。非公平的锁不保证 : ......... 发自「贵邮」
Lamperouge机器人#6 · 2016/8/10
我觉得暖神的意思可能是:当2个线程同时竞争1个非公平锁时,如果A线程得到了锁,那么当线程A执行完同步方法释放锁的时候,可能这个时候B还没有进去,此时A和B再次竞争的话,还是有可能A得到锁。如果按照公平锁的定义,A竞争得到锁之后,之后不管发生什么,都是B得到锁。@nuanyangyang 不知道是不是这个意思 【 在 xiao5aha 的大作中提到: 】 : 首先谢谢暖神回答,然后我还是有些不明白,对于非公平锁,一个线程占有锁并持续执行很长时间的时候,其他竞争这个锁的线程不是应该在队列里面吗,难道加入队列还可能需要很久?代码中没见有等待很久才加队列啊,等这个线程释放锁之后它会通知队列的第一个节点啊,那就又变成公平锁了! : : 发自「贵邮」
xiao5aha机器人#7 · 2016/8/10
B没有进去,那么B阻塞停留在哪里B在干什么,源码里没看到有自旋 【 在 Lamperouge 的大作中提到: 】 : 我觉得暖神的意思可能是:当2个线程同时竞争1个非公平锁时,如果A线程得到了锁,那么当线程A执行完同步方法释放锁的时候,可能这个时候B还没有进去,此时A和B再次竞争的话,还是有可能A得到锁。如果按照公平 : ......... 发自「贵邮」
Lamperouge机器人#8 · 2016/8/10
可能B竞争锁结束后,没有分配到CPU的时间片 【 在 xiao5aha 的大作中提到: 】 : B没有进去,那么B阻塞停留在哪里B在干什么,源码里没看到有自旋 : : 发自「贵邮」
nuanyangyang机器人#9 · 2016/8/10
【 在 xiao5aha 的大作中提到: 】 : 首先谢谢暖神回答,然后我还是有些不明白,对于非公平锁,一个线程占有锁并持续执行很长时间的时候,其他竞争这个锁的线程不是应该在队列里面吗,难道加入队列还可能需要很久?代码中没见有等待很久才加队列啊,等这个线程释放锁之后它会通知队列的第一个节点啊,那就又变成公平锁了! : : 发自「贵邮」 “加入”队列虽然很快,但是关键是在那个持有锁的线程释放锁的那个时候,会发生什么。如果是非公平锁,即使叫醒了,当前线程也会和刚醒过来的那个线程再次竞争这个锁。这个时候,当前线程仍然有更高的胜算。 根本上,不公平性的根源就是:任何线程,一调用lock(),就可以用compareAndSwap来试图获得锁,也就是:新来的人,先试着插队,如果插队不成,然后才到队尾排队。 对比一下 NonfairSync: http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/concurrent/locks/ReentrantLock.java#210 和FairSync: http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/concurrent/locks/ReentrantLock.java#228 就会发现NonfairSync新人上来就插队,然后才排队。但FairSync是直接去排队。 再看看unlock(),其实就是sync.release(1): http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/concurrent/locks/AbstractQueuedSynchronizer.java#1196 这个方法在NonfairSync和FairSync里都一样:就是叫醒队里的下一个线程。 而排队的线程,是在这里等待的: http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/util/concurrent/locks/AbstractQueuedSynchronizer.java#AbstractQueuedSynchronizer.acquireQueued(java.util.concurrent.locks.AbstractQueuedSynchronizer.Node%2Cint) 它虽然已经在队列里了,还是要和新来的线程竞争锁。