问题
In old synchronized block, we used same object to synchronize on, also used wait and notify methods. So they can all refer to same lock. Makes sense.
So when I use class ReentrantLock, why can't I also use same variable to call lock, unlock as well as await and signal? Why do I need to make additional Condition variable?
That is, why I need to do this:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
void doSomething() {
lock.lock();
//some code
condition.await();
//some code
lock.unlock();
}
Instead of this: (wouldn't this type of coding be more logic)?
Lock lock = new ReentrantLock();
void doSomething() {
lock.lock();
//some code
lock.await();
//some code
lock.unlock();
}
EDIT: from docs: A Condition instance is intrinsically bound to a lock. Why design it that way? Why not just have one variable of type Lock which would have await and signal method?
回答1:
The separation of Lock
and Condition
allows you to have more than one Condition
per Lock
, which is documented by Condition:
Condition
factors out theObject
monitor methods (wait
,notify
andnotifyAll
) into distinct objects to give the effect of having multiple wait-sets per object [emphasis added], by combining them with the use of arbitraryLock
implementations.
And Lock:
[
Lock
implementations] allow more flexible structuring, may have quite different properties, and may support multiple associatedCondition
objects [emphasis added].
With that ability you can do things like:
import java.util.Objects;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Stack<E> {
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
private final Object[] elements;
private int size;
public Stack(int capacity) {
elements = new Object[capacity];
}
public E pop() throws InterruptedException {
lock.lockInterruptibly();
try {
while (size == 0) {
notEmpty.await();
}
@SuppressWarnings("unchecked")
E element = (E) elements[--size];
elements[size] = null;
notFull.signal();
return element;
} finally {
lock.unlock();
}
}
public void push(E element) throws InterruptedException {
Objects.requireNonNull(element);
lock.lockInterruptibly();
try {
while (size == elements.length) {
notFull.await();
}
elements[size++] = element;
notEmpty.signal();
} finally {
lock.unlock();
}
}
}
This approach gives two benefits:
- When an element is pushed only a thread waiting to pop an element is signaled and vice versa. In other words, only the thread(s) waiting on a specific
Condition
are signaled. - You don't have to invoke
signalAll()
, meaning only one thread is woken up. - (Bonus) Improves readability of code, at least in my opinion.
Here's the same Stack
class but using synchronized
:
import java.util.Objects;
public class Stack<E> {
private final Object lock = new Object();
private final Object[] elements;
private int size;
public Stack(int capacity) {
elements = new Object[capacity];
}
public E pop() throws InterruptedException {
synchronized (lock) {
while (size == 0) {
lock.wait();
}
@SuppressWarnings("unchecked")
E element = (E) elements[--size];
elements[size] = null;
lock.notifyAll();
return element;
}
}
public void push(E element) throws InterruptedException {
Objects.requireNonNull(element);
synchronized (lock) {
while (size == elements.length) {
lock.wait();
}
elements[size++] = element;
lock.notifyAll();
}
}
}
Notice now that every thread has to wait on the same "condition" and that every waiting thread is notified any time anything happens. You have to notify all waiting threads because you have no finer control over which thread(s) are notified.
来源:https://stackoverflow.com/questions/63331988/why-cant-await-and-signal-methods-be-called-directly-on-object-of-reentrantlock