多条件 vs 多锁
Multiple conditions vs Multiple locks
对于特定的线程安全数据结构,我需要保护对中央数据结构(即字节数组)的访问。在这种情况下,我选择使用 ReentrantLocks,因为它的公平政策以及创建多个条件的高级功能。
并发的条件比较复杂,列举如下:
- 必须专门保护中央字节数组(即一次一个线程)。
- 两个访问方法(foo 和 bar)必须能够 运行 同时进行(如果它们试图访问中央字节数组则在内部阻塞)。
- 对任何方法(foo 和 bar)的调用必须是互斥的(即从不同线程多次调用 foo 将导致一个线程阻塞)。
在我最初的实现中,我选择实现两个嵌套锁如下:
ReentrantLock lockFoo = new ReentrantLock(true);
ReentrantLock lockCentral = new ReentrantLock(true);
Condition centralCondition = lockCentral.newCondition();
public void foo(){
// thread-safe processing code here
lockFoo.lock();
lockCentral.lock();
try{
// accessing code here
try{
// waits upon some condition for access
while(someCondition){
centralCondition.await();
}
}catch(InterruptedException ex){
// handling code here
}
// more processing
}finally{
lockCentral.unlock();
lockFoo.unlock();
}
}
该结构在方法 bar
中是等效的,只需要另一个锁对象 lockBar
。此外,为简单起见,该代码已将我更复杂的多条件等待和信号减少为单一条件。
使用这个,我不禁觉得代码似乎不必要的复杂和晦涩,因为不仅嵌套了两个锁,而且它们共享一个 try-finally,更不用说 lockCentral
可能在 lockFoo
期间被多次释放和重新获取。
相反,我尝试重新组织外部锁(lockFoo
和 lockBar
)作为 lockCentral
的条件,如下所示:
ReentrantLock lockCentral = new ReentrantLock(true);
Condition fooCondition = lockCentral.newCondition();
Condition centralCondition = lockCentral.newCondition();
boolean isInFoo = false;
public void foo(){
// thread-safe processing code here
lockCentral.lock();
try{
// implement method exclusiveness via fooCondition
try{
while(isInFoo){
fooCondition.await();
}
isInFoo = true;
}catch(InterruptedException ex){
return;
}
// accessing code here
try{
// waits upon some condition for access
while(someCondition){
centralCondition.await();
}
}catch(InterruptedException ex){
// handling code here
}
// more processing
}finally{
isInFoo = false;
fooCondition.signal();
lockCentral.unlock();
}
}
经过一些检查,我无法决定是前者更好还是后者更好(尤其是包含那个随机布尔值)。简化代码的想法似乎导致 更长 代码,在这种情况下非常违反直觉。
是否有一些惯例或令人信服的理由来论证:
每个锁定上下文使用一个锁(以前的代码,其中不同的锁定原因共享不同的锁)。
每个锁定资源使用一个锁(后一种代码,其中要保护的中央结构使用单个锁,其他所有内容都作为访问所述结构的条件实现)。
后一个代码与前一个代码的不同之处在于手动实现锁 lockFoo 和 fooCondition(bar-相关部分).
因为这样的锁实现考虑到 foo 临界区几乎与 central 临界区相同,所以保证更快如果 foo()
上没有争用(在这种情况下永远不会等待 fooCondition)。
除了性能方面的原因,前者的代码是更可取的,因为它是自我记录的。此外,它还可以扩展到需要在没有 lockCentral 的情况下访问受 lockFoo 保护的数据的情况。在那种情况下,手动实现锁会失去其性能增益。
在您的原始示例中,lockFoo
和 lockBar
锁是完全多余的,因为 foo()
和 bar()
都无法在不锁定 [=15= 的情况下完成任何工作] 锁。除非您更改程序的设计,否则 lockCentral
是您唯一需要的锁。
你说你认为你的第一个例子是 "too complicated",但你的第二个例子 更 复杂。看起来您只是想用您自己设计的锁定代码替换 lockFoo
和 lockBar
。但这有什么意义呢?它不会做任何与您的第一个示例所做的不同的事情。
锁定的目的到底是什么?你说 "Calls to any method (foo and bar) need to be exclusive"。这是错误的开始:不要使用锁来保护 方法 ;使用锁来保护 data.
这是什么"central byte array?"线程对它做了什么? 为什么需要保护?
foo() 操作的数据是什么?为什么需要保护? bar() 操作的数据是什么?为什么 that 需要保护?
最重要的是,foo() 数据和 bar() 数据都需要与中央字节数组同时受到保护吗?
在一个设计良好的程序中,线程应该在不持有任何锁的情况下完成大部分工作:
SomeObject someObject;
SomeOtherObject someOtherObject;
boolean success = false;
while (! success) {
someLock.lock();
try {
someObject = getLocalCopyOfSomeData();
someOtherObject = getLocalCopyOfSomeOtherData();
} finally {
someLock.unlock();
}
doTheRealWork(someObject, someOtherObject);
someLock.lock();
try {
success = updateCentralCopyOf(someObject) || updateCentralCopyOf(someOtherObject);
} finally {
someLock.unlock();
}
}
对于特定的线程安全数据结构,我需要保护对中央数据结构(即字节数组)的访问。在这种情况下,我选择使用 ReentrantLocks,因为它的公平政策以及创建多个条件的高级功能。
并发的条件比较复杂,列举如下:
- 必须专门保护中央字节数组(即一次一个线程)。
- 两个访问方法(foo 和 bar)必须能够 运行 同时进行(如果它们试图访问中央字节数组则在内部阻塞)。
- 对任何方法(foo 和 bar)的调用必须是互斥的(即从不同线程多次调用 foo 将导致一个线程阻塞)。
在我最初的实现中,我选择实现两个嵌套锁如下:
ReentrantLock lockFoo = new ReentrantLock(true);
ReentrantLock lockCentral = new ReentrantLock(true);
Condition centralCondition = lockCentral.newCondition();
public void foo(){
// thread-safe processing code here
lockFoo.lock();
lockCentral.lock();
try{
// accessing code here
try{
// waits upon some condition for access
while(someCondition){
centralCondition.await();
}
}catch(InterruptedException ex){
// handling code here
}
// more processing
}finally{
lockCentral.unlock();
lockFoo.unlock();
}
}
该结构在方法 bar
中是等效的,只需要另一个锁对象 lockBar
。此外,为简单起见,该代码已将我更复杂的多条件等待和信号减少为单一条件。
使用这个,我不禁觉得代码似乎不必要的复杂和晦涩,因为不仅嵌套了两个锁,而且它们共享一个 try-finally,更不用说 lockCentral
可能在 lockFoo
期间被多次释放和重新获取。
相反,我尝试重新组织外部锁(lockFoo
和 lockBar
)作为 lockCentral
的条件,如下所示:
ReentrantLock lockCentral = new ReentrantLock(true);
Condition fooCondition = lockCentral.newCondition();
Condition centralCondition = lockCentral.newCondition();
boolean isInFoo = false;
public void foo(){
// thread-safe processing code here
lockCentral.lock();
try{
// implement method exclusiveness via fooCondition
try{
while(isInFoo){
fooCondition.await();
}
isInFoo = true;
}catch(InterruptedException ex){
return;
}
// accessing code here
try{
// waits upon some condition for access
while(someCondition){
centralCondition.await();
}
}catch(InterruptedException ex){
// handling code here
}
// more processing
}finally{
isInFoo = false;
fooCondition.signal();
lockCentral.unlock();
}
}
经过一些检查,我无法决定是前者更好还是后者更好(尤其是包含那个随机布尔值)。简化代码的想法似乎导致 更长 代码,在这种情况下非常违反直觉。
是否有一些惯例或令人信服的理由来论证:
每个锁定上下文使用一个锁(以前的代码,其中不同的锁定原因共享不同的锁)。
每个锁定资源使用一个锁(后一种代码,其中要保护的中央结构使用单个锁,其他所有内容都作为访问所述结构的条件实现)。
后一个代码与前一个代码的不同之处在于手动实现锁 lockFoo 和 fooCondition(bar-相关部分).
因为这样的锁实现考虑到 foo 临界区几乎与 central 临界区相同,所以保证更快如果 foo()
上没有争用(在这种情况下永远不会等待 fooCondition)。
除了性能方面的原因,前者的代码是更可取的,因为它是自我记录的。此外,它还可以扩展到需要在没有 lockCentral 的情况下访问受 lockFoo 保护的数据的情况。在那种情况下,手动实现锁会失去其性能增益。
在您的原始示例中,lockFoo
和 lockBar
锁是完全多余的,因为 foo()
和 bar()
都无法在不锁定 [=15= 的情况下完成任何工作] 锁。除非您更改程序的设计,否则 lockCentral
是您唯一需要的锁。
你说你认为你的第一个例子是 "too complicated",但你的第二个例子 更 复杂。看起来您只是想用您自己设计的锁定代码替换 lockFoo
和 lockBar
。但这有什么意义呢?它不会做任何与您的第一个示例所做的不同的事情。
锁定的目的到底是什么?你说 "Calls to any method (foo and bar) need to be exclusive"。这是错误的开始:不要使用锁来保护 方法 ;使用锁来保护 data.
这是什么"central byte array?"线程对它做了什么? 为什么需要保护?
foo() 操作的数据是什么?为什么需要保护? bar() 操作的数据是什么?为什么 that 需要保护?
最重要的是,foo() 数据和 bar() 数据都需要与中央字节数组同时受到保护吗?
在一个设计良好的程序中,线程应该在不持有任何锁的情况下完成大部分工作:
SomeObject someObject;
SomeOtherObject someOtherObject;
boolean success = false;
while (! success) {
someLock.lock();
try {
someObject = getLocalCopyOfSomeData();
someOtherObject = getLocalCopyOfSomeOtherData();
} finally {
someLock.unlock();
}
doTheRealWork(someObject, someOtherObject);
someLock.lock();
try {
success = updateCentralCopyOf(someObject) || updateCentralCopyOf(someOtherObject);
} finally {
someLock.unlock();
}
}