When the Java class library does not provide a suitable synchronization tool, you need to build a custom synchronization tool.
Structure for blocking state-dependent operations
Copy the code code as follows:
acquire lock on object state;//Request to acquire lock
while(precondition does not hold){//The precondition is not met
release lock;//release the lock first
wait until precondition might hold;//wait until precondition is met
optionlly fail if interrupted or timeout expires;//Execution failed due to interruption or timeout
reacquire lock;//Retry to acquire the lock
}
perform action//execute
release lock;//release lock
Bounded cache implementation base class example
Copy the code code as follows:
public class BaseBoundBuffer<V> {
private final V[] buf;
private int tail;
private int head;
private int count;
@SuppressWarnings("unchecked")
public BaseBoundBuffer(int capacity) {
buf = (V[]) new Object[capacity];
}
public synchronized void doPut(V v) {
buf[tail] = v;
if (++tail == buf.length)
tail = 0;
count++;
}
public synchronized V doTake() {
V v = buf[head];
if (++head == buf.length)
head = 0;
count--;
return v;
}
public final synchronized boolean isFull() {
return count == buf.length;
}
public final synchronized boolean isEmpty() {
return count == 0;
}
}
Blocking implementation method 1: Throw an exception to the caller
Copy the code code as follows:
public synchronized void put1(V v) throws Exception{
if(isFull())
throw new Exception("full error");
doPut(v);
}
Analysis: Exceptions should be used when exceptions occur. It is not appropriate to throw exceptions here; the caller is required to handle the situation where the precondition fails, which does not solve the fundamental problem.
Blocking implementation method 2: through polling and sleep
Copy the code code as follows:
public void put2(V v) throws InterruptedException {
while (true) {//Polling
synchronized (this) {
if (!isFull()) {
doPut(v);
return;
}
}
Thread.sleep(SLEEP_TIME);//Sleep
}
}
Analysis: It is difficult to weigh the sleep time SLEEP_TIME setting. If the setting is too small, the CPU may poll multiple times, consuming more CPU resources; if the setting is too large, the responsiveness will be lower.
Blocking implementation method three: conditional queue
The elements in the condition queue are threads waiting for related conditions one by one. Each Java object can be used as a lock, and each object can also be used as a condition queue, and the wait, notify, and notifyAll methods in Object constitute the API of the internal condition queue. Object.wait will automatically release the lock and request the operating system to suspend the current thread so that other threads can obtain the lock and modify the state of the object. Object.notify and Object.notifyAll can wake up the waiting thread, select a thread from the condition queue to wake up and try to reacquire the lock.
Copy the code code as follows:
public synchronized void put3(V v) throws InterruptedException {
while(isFull())
wait();
doput(v);
notifyAll();
}
Analysis: Get better response, simple and easy to use.
Use conditional queue
1. Conditional predicate
1).Definition: A conditional predicate is a precondition for an operation to become a state-dependent operation. A conditional predicate is an expression composed of individual state variables in the class. For example, the conditional predicate for the put method is "the cache is not empty".
2).Relationship: There is an important ternary relationship in conditional waiting, including locking, wait method and a conditional predicate. Multiple state variables are included in a conditional predicate, and each state variable must be protected by a lock, so the lock must be held before testing the conditional predicate. The lock object and the conditional queue object (and the object where wait and notify methods are called) must be the same object.
3). Constraints: Each call to wait will be implicitly associated with a specific condition predicate. When a specific condition predicate is called, the caller must already hold a lock related to the condition queue. This lock must also protect the component condition predicate. state variables
2. Conditional queue usage rules
1). Usually there is a conditional predicate
2).Always test conditional predicates before calling wait, and test again after returning from wait;
3).Always call wait in the loop;
4). Ensure that the state variables that constitute the condition predicate are protected by a lock, and this lock must be associated with the condition queue;
5). When calling wait, notify and notifyAll, the lock associated with the condition queue must be held;
6). After checking the conditional predicate, do not release the lock before starting to execute the protected logic;
3.Notification
Try to use notifyAll instead of nofify. Because nofify will randomly wake up a thread from dormant state to Blocked state (Blocked state is a thread that is always trying to acquire the lock, that is, once it finds that the lock is available, it will hold the lock immediately), while notifyAll It will wake up all the threads in the condition queue from the dormant state to the Blocked state. Consider this situation, if thread A enters the dormant state because of the condition predicate Pa, and thread B enters the dormant state because of the condition predicate Pb. At this time Pb is true, thread C executes a single notify. If the JVM randomly selects thread A to wake up, then thread A checks that the conditional predicate Pa is not true and then enters the sleep state. From then on, no other thread can be awakened, and the program will always be in sleep state. If you use notifyAll is different. The JVM will wake up all waiting threads in the condition queue from sleep state to Blocked state. Even if a thread is randomly selected and enters sleep state because the condition predicate is not true, other threads will compete for the lock and continue execution. Go down.
4. The standard form copy code of the state dependency method is as follows:
void stateDependentMethod throwsInterruptedException{
synchronized(lock){
while(!conditionPredicate))
lock.wait();
}
//dosomething();
....
notifyAll();
}
Show Condition object
The explicit Condition object is a more flexible alternative and provides richer functionality: multiple waits can exist on each lock, conditional waits can be interruptible or uninterruptible, time-based waits, and fair or Unfair queue operations. A Condition can be associated with a Lock, just like a condition queue is associated with a built-in lock. To create a Condition, call the Lock.newCondition method on the associated Lock. The following code is used to re-implement the bounded cache using display condition variables. The code is as follows:
public class ConditionBoundedBuffer<V> {
private final V[] buf;
private int tail;
private int head;
private int count;
private Lock lock = new ReentrantLock();
private Condition notFullCondition = lock.newCondition();
private Condition notEmptyCondition = lock.newCondition();
@SuppressWarnings("unchecked")
public ConditionBoundedBuffer(int capacity) {
buf = (V[]) new Object[capacity];
}
public void doPut(V v) throws InterruptedException {
try {
lock.lock();
while (count == buf. length)
notFullCondition.await();
buf[tail] = v;
if (++tail == buf.length)
tail = 0;
count++;
notEmptyCondition.signal();
} finally {
lock.unlock();
}
}
public V doTake() throws InterruptedException {
try {
lock.lock();
while (count == 0)
notEmptyCondition.await();
V v = buf[head];
buf[head] = null;
if (++head == buf.length)
head = 0;
count--;
notFullCondition.signal();
return v;
} finally {
lock.unlock();
}
}
}