Sunday, November 15, 2015

Low-level synchronization in Java

Low level synchronization is the mechanism used before package java.util.concurrent was introduced into Java 1.5. You may not use it since the modern Java provide more high level sync mechanism since Java 1.5, but understand low-level sync can help you understand high-level counterparts like java.util.concurrent.locks.Condition, which not only duplicates low-level's function but also provides more flexibility.

The low-level sync mechanism requires 2 things:

  • keyword synchronized
  • methods wait/notify(notifyAll)

BTW, you may notice that wait()/notify() methods are build-in nature for every Java Object.

image

In this article, we try to build a simple producer/consumer model. They both work on an ArrayList, but consumer thread will block if the list is empty. As long as a producer thread put in a Integer into the list, the consumer can resume from the blocking and continue. (In reality, java.util.concurrent.BlockingQueue should be used instead write your own one).

0. How it works

In low-level sync, lock can be acquired from every object. we call it monitor lock, monitor, technically speaking, is the object whose lock you are acquiring. In this demo an ArrayList instance (called queue, see blow java definition) is the monitor we try to get lock from. 

Keyword synchronized(...) is used to acquire locks. although it looks like a methods but it's a keyword( You can  NOT Ctrl + click to goes into its implementation like a method of any Object). Synchronized block is always based on an object instance, the monitor, which is the parameter of synchronized(...)

wait() and notify() can only be called after the lock acquired, which means these methods can only be called within synchronized block.

when wait() invoked,  lock will be immediately release. then thread suspend. You may think "thread steps back from running and enters some kind of a  waiting-room" . The thread stays in "waiting-room" until notify() get called. Then this thread will try to get the lock again.After acquired the lock again, the wait() method finishes.

when notify() invoked, "one of the waiting thread gets out of the waiting-room" . normally this thread will soon get CPU time and  run again. notify() don't release lock, so it normally is put just before end of the synchronized block. When synchronized block ends, lock released.  If there are more threads in "waiting-room" which one get out is random. notifyAll() make all waiting threads get out of the waiting room, but only one of them can get lock.  So these thread will run one by one.Running order is not guaranteed.

1. Demo

There are 3 classes, consumer, producer and main class. Firstly define MyProducer class.

class MyProducer implements Runnable {
private List<Integer> queue;

public MyProducer(List<Integer> queue) {
this.queue = queue;
}

public void put(Integer e) throws InterruptedException {
synchronized (queue) {
System.out.printf("put %d to queue\n", e);
queue.add(e);
queue.notify();
}
}

@Override
public void run() {
try {
put(new Integer((int) (Math.random() * 10))); // random 0 ~ 9
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

The producer class implement Runnable interface and just input a random number into to list.  The instance variable queue are pass in by constructor, on which lock will be acquired by call synchronized(queue). The notify() is invoked to notify any thread may stay in the "wait-room" that this queue is not empty anymore.  


Secondly define MyConsumer class.

class MyConsumer implements Runnable {
private List<Integer> queue;

public MyConsumer(List<Integer> queue) {
this.queue = queue;
}

public Integer take() throws InterruptedException {
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait();
}
return queue.remove(0);
}
}

@Override
public void run() {
try {
System.out.printf("Try to take from queue\n");
System.out.printf("take %d from queue\n\n", take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

The consumer will try to get an element out of the list. if the list is empty, consumer thread will wait.


Finally the main class.

public class MyArrayBlockingQueue {
static private List<Integer> queue = new ArrayList<>();

public static void main(String[] args) throws InterruptedException {
MyProducer pr = new MyProducer(queue);
MyConsumer cr = new MyConsumer(queue);

Thread p1 = new Thread(pr);
Thread p2 = new Thread(pr);

Thread c1 = new Thread(cr);
Thread c2 = new Thread(cr);

p1.start();
Thread.sleep(1000);
c1.start();
Thread.sleep(1000);
c2.start();
Thread.sleep(5000);
p2.start();
}
}

The main class creates 2 producer threads and 2 consumer threads. The first producer thread p1 put a integer into the list and first consumer thread c1 take it out. Then second consumer c2 try to take from the empty list, it will block until 5 seconds later, the second producer p2 put integer into the list.


2. what's more.


Now you should understand how low-level synchronization mechanism works and the usage for methods wait()/notify() of every objects. In reality, you should use high-level sync mechanism like Lock, Condition and many other power ways provided in package java.util.concurrent and java.util.concurrent.locks.

0 comments:

Post a Comment

Powered by Blogger.

About The Author

My Photo
Has been a senior software developer, project manager for 10+ years. Dedicate himself to Alcatel-Lucent and China Telecom for delivering software solutions.

Pages

Unordered List