Sunny: Let’s talk about asynchrony and synchronization first.
Synchronous call: The caller waits for the result to return during the call. Asynchronous invocation: During the invocation, the caller does not directly wait for the result to return, but performs other tasks, usually in the form of a callback function.
Instead of going into detail, let’s focus on how Java converts asynchronous calls to synchronous ones. In other words, you need to keep blocking during an asynchronous call until you get the call result. No pun intended, here are five ways to do this, then give an example:
- Use wait and notify
- Service condition lock
- Future
- Using CountDownLatch
- Use the CyclicBarrier
- 0. Construct an asynchronous call
First, you need to write the infrastructure first, in this case building an asynchronous invocation model. Asynchronous calling class:
public class AsyncCall {
private Random random = new Random(System.currentTimeMillis());
private ExecutorService tp = Executors.newSingleThreadExecutor();
//demo1,2,4,5 call the method
public void call(BaseDemo demo){
new Thread(()->{
long res = random.nextInt(10);
try {
Thread.sleep(res*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
demo.callback(res);
}).start();
}
//demo3 calls the method
public Future<Long> futureCall(a){
return tp.submit(()-> {
long res = random.nextInt(10);
try {
Thread.sleep(res*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return res;
});
}
public void shutdown(a){ tp.shutdown(); }}Copy the code
We are primarily concerned with the Call method, which takes a demo argument and starts a thread that performs specific tasks and calls callback functions using The Demo callback method. Notice that the return result is a long integer [0,10], and the result is several, and let the thread sleep for how long — this is mainly to better observe the experimental results and simulate the processing time during the asynchronous call.
The futureCall and shutdown methods, as well as the thread pool TP, are all in preparation for demo3’s implementation using the Future. Demo base class:
public abstract class BaseDemo {
protected AsyncCall asyncCall = new AsyncCall();
public abstract void callback(long response);
public void call(a){
System.out.println("Initiate a call");
asyncCall.call(this);
System.out.println("Call return"); }}Copy the code
BaseDemo is very simple. It contains an instance of an asynchronous calling class, a call method for making an asynchronous call, and of course an abstract method called callback that each demo implements.
1. Use wait and notify
This method actually uses the lock mechanism, directly paste code:
public class Demo1 extends BaseDemo{
private final Object lock = new Object();
@Override
public void callback(long response) {
System.out.println("Get the result.");
System.out.println(response);
System.out.println("End of call"); synchronized (lock) { lock.notifyAll(); }}public static void main(String[] args) {
Demo1 demo1 = new Demo1();
demo1.call();
synchronized (demo1.lock){
try {
demo1.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Main thread contents"); }}Copy the code
As you can see, the main thread blocks with wait after the call, waiting for notify or notifyAll to be invoked in the callback. Note that both wait and notify require the lock of an object to be acquired first. At the end of the main thread, we print the content of the main thread, which is also used to verify the experiment results. If there is no wait and notify, the content of the main thread will print immediately after the content of the call. As in our code above, the main thread content waits for the callback call to finish before printing. Print the result without using a synchronous operation:
A call
The call returns the main thread contents with result 1. The call ends
With a synchronous operation:
A call
The call returns the result 9
End of the call
Main thread content
2. Use conditional locks
The principle is similar to method 1:
public class Demo2 extends BaseDemo {
private final Lock lock = new ReentrantLock();
private final Condition con = lock.newCondition();
@Override
public void callback(long response) {
System.out.println("Get the result.");
System.out.println(response);
System.out.println("End of call");
lock.lock();
try{ con.signal(); }finally { lock.unlock(); }}public static void main(String[] args) {
Demo2 demo2 = new Demo2();
demo2.call();
demo2.lock.lock();
try {
demo2.con.await();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
demo2.lock.unlock();
}
System.out.println("Main thread contents"); }}Copy the code
It’s basically the same as method 1, except that conditional locking is used, and the locking mechanism is different.
The way we use the Future is different, and the asynchronous methods we call are different.
public class Demo3{
private AsyncCall asyncCall = new AsyncCall();
public Future<Long> call(a){
Future<Long> future = asyncCall.futureCall();
asyncCall.shutdown();
return future;
}
public static void main(String[] args) {
Demo3 demo3 = new Demo3();
System.out.println("Initiate a call");
Future<Long> future = demo3.call();
System.out.println("Return result");
while (!future.isDone() && !future.isCancelled());
try {
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("Main thread contents"); }}Copy the code
We call the futureCall method, which submits a Callable to the thread pool TP and returns a Future that we got from call in Demo3. Once we get the Future object, we can close the thread pool. Call asyncCall’s shutdown method.
One thing to note about shutting down the thread pool is that let’s go back to the asyncCall shutdown method:
public void shutdown(a){
tp.shutdown();
}
Copy the code
We found that we simply called the shutdown method of the thread pool, and then we said that it is better not to use TP’s shutdownNow method, which tries to interrupt the task that is being executed in the thread; That is, if we use this method, it is possible that the task corresponding to our future will be interrupted and the execution result will not be available.
Then we pay attention to the contents of the main thread. The blocking of the main thread is realized by ourselves, and the execution status is judged by isDone and isCancelled of the future until the execution is completed or cancelled. We then print the result of the get.
4. Use CountDownLatch
Using CountDownLatch is probably one of the most common ways to program daily, and it feels like a relatively elegant one:
public class Demo4 extends BaseDemo{
private final CountDownLatch countDownLatch = new CountDownLatch(1);
@Override
public void callback(long response) {
System.out.println("Get the result.");
System.out.println(response);
System.out.println("End of call");
countDownLatch.countDown();
}
public static void main(String[] args) {
Demo4 demo4 = new Demo4();
demo4.call();
try {
demo4.countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread contents"); }}Copy the code
As you would normally do, block with the await method on CountDownLatch in the main thread and countDown in the callback to keep the await part of the other thread running.
As in demo1 and demo2, you can set a timeout period for the blocked part of the main thread.
5. Use the CyclicBarrier
CyclicBarrier is similar to CountDownLatch:
public class Demo5 extends BaseDemo{
private CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
@Override
public void callback(long response) {
System.out.println("Get the result.");
System.out.println(response);
System.out.println("End of call");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch(BrokenBarrierException e) { e.printStackTrace(); }}public static void main(String[] args) {
Demo5 demo5 = new Demo5();
demo5.call();
try {
demo5.cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println("Main thread contents"); }}Copy the code
Notice that CyclicBarrier and CountDownLatch are only similar, but there are some differences. For example, one can be thought of as adding, and then running together after adding this number; One is subtraction, minus zero and keep going. One is repeatable; The other one can’t wait, wait, wait.
There are also two things to be aware of when using a CyclicBarrier.
The first thing that distinguishes CountDownLatch from CountDownLatch is that the number of initialization arguments should be set to 2, because the asynchronous call is a thread and the main thread is a thread.
The second point is about the value of the initialization parameter, which is not relevant to the demo here. You need to be careful when programming. If this value is set to a large number, larger than the number of threads in the thread pool, it can easily cause deadlocks.
conclusion
To sum up, this is the need to say several methods. In fact, all methods are based on the same principle, that is, blocking waits for the result in the calling thread and unblocking in the function in the callback.