Thread synchronization can be controlled by synchronized/wait/notify before jdk5, and lock/condition has been added after jdk5. What are the connections and differences between them? This article uses an example to show you step by step:
Let's start with an example of bounded caching:
abstract class BaseBoundedBuffer<V> { private final V[] buff; private int tail; private int head; private int count; protected BaseBoundedBuffer(int capacity){ this.buff = (V[])new Object[capacity]; } protected synchronized final void doPut(V v){//Deposit buff[tail] = v; tail++; if(tail == buff.length){ tail = 0; } count++; } protected synchronized final V doTake(){//get V v = buff[head]; buff[head] = null; head++; if(head == buff.length){ head = 0; } count--; return v; } protected synchronized final boolean isFull(){//Is it full? return count == buff.length; } protected synchronized final boolean isEmpty(){//Is it empty? return count == 0; } }
class GrumpBoundedBufer<V> extends BaseBoundedBuffer<V>{ public GrumpBoundedBufer(int size){ super(size); } public synchronized void put(V v)throws BufferFullException{ if(isFull()){//When you save, if it's full, throw an exception throw new BufferFullException(); } doPut(v); } public synchronized V take()throws BufferEmptyException{ if(isEmpty()){//When fetching, if it's empty, throw an exception throw new BufferEmptyException(); } return doTake(); } }
Of course, the above implementations are very unfriendly and throw exceptions if the priori conditions are not met. However, under multi-threaded conditions, the priori conditions will not remain unchanged. The elements in the queue are constantly changing. So we use polling and sleep to improve it.
The disadvantage of this method of rotation training + dormancy:class SleepyBoundedBufer<V> extends BaseBoundedBuffer<V>{ public SleepyBoundedBufer(int size){ super(size); } public void put(V v) throws InterruptedException { while(true){ synchronized(this){ if(!isFull()){//If it is not full, it can be saved. doPut(v); return; } } //If it's full, sleep for one second and try again. Thread.sleep(1000); } } public V take() throws InterruptedException { while(true){ synchronized(this){ if(!isEmpty()){//If it's not empty, you can take it. return doTake(); } } //If it's empty, sleep for 1 second and try again Thread.sleep(1000); } } }
(1) How long is the right time to sleep?
(2) Give the caller a new requirement to handle InterruptedException, because sleep throws this exception.
If there is a way of thread hanging, it can ensure that when a certain condition becomes true, the thread can wake up in time, that's great! uuuuuuuuuuu That's what conditional queues do.
Implementing internal condition queues:
This is also the solution before jdk5.class BoundedBufer<V> extends BaseBoundedBuffer<V>{ protected BoundedBufer(int size) { super(size); } public synchronized void put(V v) throws InterruptedException { while(isFull()){//Notice here while, not if. wait();//If it is full, suspend the current thread } doPut(v);//If you are not satisfied, you can save it. notifyAll();//When saved, wake up all waiting threads, because there may be threads waiting for fetch, which can be fetched after putting in. } public synchronized V take() throws InterruptedException { while(isEmpty()){//Notice here while, not if. wait();//If it is empty, suspend the current thread } V v = doTake();//If it's not empty, take it out. notifyAll();//Then wake up all the waiting threads, because the threads may be waiting to be put, and they can be put after being taken out. return v; } }
Conditional queue can make a group of threads (called wait set) wait for the relevant conditions to become true in some way. Conditional queue elements are different from general queue elements are data items, and conditional queue elements are threads. Each java object has an internal lock and an internal condition queue. An object's internal locks and internal condition queues are linked together. Object.wait automatically releases the lock and requests os to suspend the current thread, which gives other threads the opportunity to acquire the lock and modify the state of the object. When the thread is awakened, it retrieves the lock. After calling wait, the thread enters the internal condition queue of the object and waits. After calling notify, it selects a waiting thread from the internal condition queue of the object and wakes up. Because there will be multiple threads waiting in the same conditional queue for different reasons, it is dangerous to use notify instead of notify All! Some threads are blocked when take(). The condition it waits for is that the queue is not empty. Some threads are blocked when put(). The condition it waits for is that the queue is not full. If notify is always blocking the thread on take after call take(), it will hang up!
BoundedBufer put and take is a very conservative approach. Notfy All is added to or removed from the queue each time. The following optimization can be done:
It is from empty to non-empty or from full to unsatisfactory that a thread needs to be awakened from the conditional queue.
This is just a little trick, which will increase the complexity of the program, not advocated! uuuuuuuuuuuclass ConditionalBoundedBufer<V> extends BaseBoundedBuffer<V>{ protected ConditionalBoundedBufer(int size) { super(size); } public synchronized void put(V v) throws InterruptedException { while(isFull()){ wait(); } boolean isEmpty = isEmpty(); doPut(v); if(isEmpty){//From empty to non-empty, you need to wake up (actually, you need to wake up those take threads, not put threads) notifyAll(); } } public synchronized V take() throws InterruptedException { while(isEmpty()){ wait(); } boolean isFull = isFull(); V v = doTake(); if(isFull){//From full to unsatisfactory, you need to wake up (instead of take threads, you actually need to wake up those put threads) notifyAll(); } return v; } }
From empty to non-empty, wake-up should be those blocked on take(), from full to unsatisfactory wake-up should be those blocked on put(), and notify All will wake up all waiting threads in all conditional queues, which shows that the internal conditional queue has a defect: internal locks can only be used. There is a conditional queue associated with it. Explicit condition appears to solve this problem.
Just as Lock provides richer features than internal locks, condition also provides richer and more flexible functions than internal condition queues. A lock can have more than one condition, and a condition can only be associated with one Lock.
class ConditionBoundedBufer<T> {//Using explicit conditional variables, the HLL debuts private Lock lock = new ReentrantLock(); private Condition notEmpty = lock.newCondition(); private Condition notFull = lock.newCondition(); private final T[] items = (T[])new Object[100]; private int head,tail,count; //Blockage, up to notFull public void put(T t) throws InterruptedException { lock.lock(); try{ while(count == items.length){ notFull.await();//Waiting for non full } items[tail] = t; tail ++; if(tail == items.length){ tail = 0; } count++; notEmpty.signal();//Wake up the blocked threads that execute take() }finally{ lock.unlock(); } } //Blocking until notEmpty public T take() throws InterruptedException { lock.lock(); try{ while(count == 0){ notEmpty.await();//Waiting is not empty. } T t = items[head]; items[head] = null; head ++; if(head == items.length){ head = 0; } count--; notFull.signal();//Wake up threads that are blocked by putting () return t; }finally{ lock.unlock(); } } }
So far, all the above problems have been perfectly solved!
I hope the above will help you understand wait & notify, lock & condition, and welcome you to watch my two video courses.
Detailed description of performance monitoring and tuning in Java production environment https://coding.imooc.com/class/241.html
Java Second Kill System Optimizing High Performance and High Concurrent Practice https://coding.imooc.com/class/168.html