Java Synchronization and Locks
This article will introduce us to power of synchronization and locks in Java. This article use Java 8 syntax but most of it should also work on older versions of Java.
Let’s look at each of the concepts we’re going to present and differ between them as well.
Synchronization
Synchronization in java is the capability to control the access of multiple threads to any shared resource like a file opened to write. Java Synchronization is a better option when we want to allow only one thread to access the shared resource.
A shared resource can be:
- File opened for write operations.
- Access to tape drivers.
Synchronization is done in two situations,
- To prevent thread interference.
- To prevent a consistency problem.
Let’s discover an example to each of the above mentioned problems and present its solution using Synchronization.
Thread Interference
Thread interference is much simpler than what we’re going to study. The basic idea is that java doesn't guarantee that a single statement of java code executes atomically.
In the following example, there is no synchronization, so output is inconsistent. Let's see the following example:
Class Table {
// method not synchronized
void printNthTable(int n){
for(int i=1;i<=5;i++){
System.out.println(n*i);
try {
Thread.sleep(400);
}
catch(Exception e){
System.out.println(e);
}
}
}
}
class MyThread1 extends Thread {
Table t;
MyThread1(Table t) {
this.t=t;
}
public void run(){
t.printNthTable(5);
}
}
class MyThread2 extends Thread {
Table t;
MyThread2(Table t) {
this.t=t;
}
public void run() {
t.printNthTable(100);
}
}
class TestSynchronization1 {
public static void main(String args[]) {
Table obj = new Table(); //only one object
MyThread1 t1 = new MyThread1(obj);
MyThread2 t2 = new MyThread2(obj);
t1.start();
t2.start();
}
}
Output:
5
100
10
200
15
300
20
400
25
500
How to avoid? We avoid them by enforcing exclusive access, one thread at a time.
By just adding a synchronized keyword in the method as shown below, we can avoid the thread interference:
synchronized void printNthTable(int n){
for(int i=1;i<=5;i++){
System.out.println(n*i);
try{
Thread.sleep(400);
}catch(Exception e){System.out.println(e);}
}
}
}
Memory Consistency error
This is the 2nd error in which we need to perform the synchronization.
Memory consistency errors occur when different threads have inconsistent views of what should be the same data. Fortunately, the programmer does not need a detailed understanding of these causes. All that is needed is a strategy for avoiding them.
The key to avoid memory consistency errors is understanding the happens-before relationship. This relationship is simply a guarantee that memory writes by one specific statement are visible to another specific statement. To see this, consider the following example. Suppose a simple int field is defined and initialized:
int counter = 0;
The counter field is shared between two threads, A and B. Suppose thread A increments counter:
counter++;
Then, shortly afterwards, thread B prints out counter:
System.out.println(counter);
If the two statements had been executed in the same thread, it would be safe to assume that the value printed out would be "1". But if the two statements are executed in separate threads, the value printed out might well be "0", because there's no guarantee that thread A's change to counter will be visible to thread B — unless the programmer has established a happens-before relationship between these two statements.
Locks
Instead of using implicit locking via the synchronized keyword the Concurrency API supports various explicit locks specified by the Lock interface. Locks support various methods for finer grained lock control thus are more expressive than implicit monitors.
Multiple lock implementations are available in the standard JDK which will be demonstrated in the following examples. Let’s take a look at an interface ReadWriteLock.
ReadWriteLock
The interface ReadWriteLock specifies a type of lock maintaining a pair of locks for read and write access. The idea behind read-write locks is that it's usually safe to read as long as nobody is writing to the same variable. So the read-lock can be held simultaneously by multiple threads as long as no threads hold the write-lock. This can improve performance and throughput in case those reads are more frequent than writes.
ExecutorService executor = Executors.newFixedThreadPool(2);
Map<String, String> map = new HashMap<>();
ReadWriteLock lock = new ReentrantReadWriteLock();
executor.submit(() -> {
lock.writeLock().lock();
try {
sleep(1);
map.put("foo", "bar");
} finally {
lock.writeLock().unlock();
}
});
The above snippet first obtains a write-lock in order to put a new value to the map after sleeping for one second. Before this task is finished two other tasks are being submitted trying to read the entry from the map and sleep for one second:
Runnable readTask = () -> {
lock.readLock().lock();
try {
System.out.println(map.get("foo"));
sleep(1);
} finally {
lock.readLock().unlock();
}
};
executor.submit(readTask);
executor.submit(readTask);
stop(executor);
When you execute this code snippet, you'll notice that both read tasks have to wait the whole second until the write task has finished. After the write lock has been released both read tasks are executed in parallel and print the result simultaneously to the console. They don't have to wait for each other to finish because read-locks can safely be acquired concurrently as long as no write-lock is held by another thread.
Semaphores
In addition to locks, the Concurrency API also supports counting semaphores. Whereas locks usually grant exclusive access to variables or resources, a semaphore is capable of maintaining whole sets of permits. This is useful in different scenarios where you have to limit the amount concurrent access to certain parts of your application.
Here's an example of how to limit access to a long running task simulated by sleep(5):
ExecutorService executor = Executors.newFixedThreadPool(10);
Semaphore semaphore = new Semaphore(5);
Runnable longRunningTask = () -> {
boolean permit = false;
try {
permit = semaphore.tryAcquire(1, TimeUnit.SECONDS);
if (permit) {
System.out.println("Semaphore acquired");
sleep(5);
} else {
System.out.println("Could not acquire semaphore");
}
} catch (InterruptedException e) {
throw new IllegalStateException(e);
} finally {
if (permit) {
semaphore.release();
}
}
}
IntStream.range(0, 10)
.forEach(i -> executor.submit(longRunningTask));
stop(executor);
The executor can potentially run 10 tasks concurrently but we use a semaphore of size 5, thus limiting concurrent access to 5. It's important to use a try/finally block to properly release the semaphore even in case of exceptions.
Executing the above code results in the following output:
Semaphore acquired
Semaphore acquired
Semaphore acquired
Semaphore acquired
Semaphore acquired
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore
The semaphores permit access to the actual long running operation simulated by sleep(5) up to a maximum of 5. Every subsequent call to tryAcquire() elapses the maximum wait time out of one second, resulting in the appropriate console output that no semaphore could be acquired.
Thread Synchronization
There are two types of thread synchronizations:
- Mutual Exclusive
- Synchronized method.
- Synchronized block.
- static synchronization.
- Cooperation (Inter-thread communication in java)
Synchronization vs. Locks
If you're simply locking an object, I'd prefer to use synchronized
Example:
Lock.acquire();
doSomethingNifty(); // Throws a NPE!
Lock.release(); // Oh noes, we never release the lock!
You have to explicitly do try{} finally{} everywhere.
Whereas with synchronized, it's super clear and impossible to get wrong:
synchronized(myObject) {
doSomethingNifty();
}
That said, Locks may be more useful for more complicated things where you can't acquire and release in such a clean manner. I would honestly prefer to avoid using bare Locks in the first place, and just go with a more sophisticated concurrency control such as a CyclicBarrier or a LinkedBlockingQueue, if they meet your needs.
Conclusion
This article introduced you to Synchronization and Locks in Java and how to use them. It also presented a compatible and comprehensive difference between them.
Recent Stories
Top DiscoverSDK Experts
Compare Products
Select up to three two products to compare by clicking on the compare icon () of each product.
{{compareToolModel.Error}}
{{CommentsModel.TotalCount}} Comments
Your Comment