Drone shot of highway junction

Concurrency in Java

Threads

By initializing and launching an object which adheres to specific implementations, we can create another thread, which is independent of the other operations described in main method, and we can execute them simultaneously.

There’re two ways to create a thread; extending Thread class and implementing Runnable interface.

  • To extend Thread class:
    • Initialize an instance of the class which extends Thread class.
    • Call start method.
  • To implement Runnable interface:
    • Initialize an instance of the class which implements Runnable interface.
    • Initialize an instance of Thread class, by calling the constructor with an input of the previous instance.
    • Call start method.

In either way, what we have to adhere to is overriding run method, and describing operations in it.

Interrupts

An interrupt is an abstract indication to a thread that it should stop what it’s doing and do something else, which is issued by another thread. In the context of Java, the interrupt mechanism is implemented using an internal flag known as the interrupt status, which is configured for each thread.

We can manipulate the interrupt status through the following three methods.

  • The method Thread.interrupt sets the interrupt status for the thread invoking it.
  • The static method Thread.interrupted returns the interrupt status of the thread where the method is invoked, and clear the status.
  • The method Thread.isInterrupted returns the interrupt status of the thread invoking the method, without changing the status.

For the interrupt mechanism to work correctly, we want interrupted thread to handle that interrupt.

How does a thread handle interrupts? If our thread only invokes methods that throw InterruptedException, we’ll catch that with a try-catch statement. Actually, some methods declared in java.lang.Thread are designed to clear the interrupt status of the thread where they are invoked, and throw InterruptedException if the thread receives an interrupt.

Java
// how to handle interrupts
class MyThread implements Runnable {
    @override
    public void run(){
        try {
            Thread.sleep(1000); // it throws InterruptedException when the thread receives an interrupt
        } catch (InterruptedException e){
            return; // terminate
        }
    }
}

On the other hand, if a thread goes a long time without invoking a method that throws InterruptedException, we can invoke the static method Thread.interrupted to check if it has received an interrupt.

Java
// how to handle interrupts
while(condition){
    doSomething(); // it doesn't throw InterruptedException
    if(Thread.interrupted){
        return; // terminate
    }
}

Additionally, throwing InterruptedException enables us to code interrupt handlings in a catch clause.

Java
// how to handle interrupts
while(condition){
    doSomething(); // it doesn't throw InterruptedException
    if(Thread.interrupted){
        throw new InterruptException();
    }
}

Concurrency Control

Synchronization: Exclusive and Consistent Access to Shared Resources

Synchronization ensures exclusive and consistent access to the same resource when multiple threads attempt to access it simultaneously. Synchronization is achieved using an internal entity known as the intrinsic lock that every object has. A threads that needs exclusive and consistent access to an object’s fields has to acquire the object’s intrinsic lock, and release when it’s done.

As long as an object’s intrinsic lock is acquired, other threads cannot access the object’s fields. Additionally, when a thread releases an intrinsic lock, the happen-before relationship is established, which ensures that all changes made by the thread are visible to every other thread. Technically, synchronization provides two options: synchronized methods, and synchronized statements.

When a thread invokes a synchronized method, it acquires the intrinsic lock for the method’s object, and it releases the lock when it returns. As for a static synchronized method, a thread acquires the intrinsic lock for the Class object associated with the class, which enforces exclusive and consistent access to the static fields.

On the other hand, when using a synchronized statement, we have to specify an object whose lock we acquire. With synchronized statements, we can enforce synchronizing access to the object’s fields, and avoid synchronizing invoking of other object’s method in one method.

Java
// how to use synchronized statements
void doSomething(){
    synchronized(this){
        foo(); // manipulate the object's fields
    }
    otherObject.bar(); // invoke other object's method
}

Additionally, in the following example, when invoking the method increment1, we don’t need to acquire an intrinsic lock for the field count2. Thus, synchronized statements are also used to improve concurrency by limiting objects for which the intrinsic lock is acquired.

Java
// how to use synchronized statements
class Foo {
    int count1 = 0;
    int count2 = 0;
    Object lock1 = new Object();
    Object lock2 = new Object();

    void increment1(){
        synchronized(lock1){
            count1++;
        }
    }

    void increment2(){
        synchronized(lock2){
            count2++;
        }
    }
}

Those synchronization technologies are known for being reentrant. Reentrant synchronization ensures that synchronized code can invoke a method which contains another synchronized code with the same intrinsic lock without causing a deadlock.

Volatile: Consistent Access to Shared Resources

As we discussed, synchronization ensures exclusive and consistent access to resources. On the other hand, volatile keyword, only used for variables, ensures consistent access but doesn’t ensure its exclusiveness.

Java
// how to use a volatile keyword
class Bar {
    volatile boolean flag;
}

Guarded Blocks: Coordinating among Multiple Threads

When threads have to coordinate their operations, a well-known solution is the guarded block. In the guarded block, two threads communicate with each other through a shared variable.

In the following case, our method must not proceed until a shared variable has been set by another thread. It’s wasteful since it executes continuously while waiting.

Java
// guarded block
void guardedFoo(){
    while(!foo){}
    doSomething();
}

A more efficient approach is to use the method Object.wait and have it enclosed in a synchronized statement or method. When a thread invokes the method Object.wait, it suspend until another thread sends a notification that some special event has occurred by using Object.notifyAll.

Java
// guarded block with Object.wait and notifyAll
void synchronized guardedFoo(){
    while(!foo){
        try{
            wait();
        } catch (InterruptedException e){}
    }
    doSomething();
}

void synchronized notifyFoo(){
    foo = true;
    notifyAll();
}

Lock Objects

The package java.util.concurrent.locks provides higher level APIs for concurrency control. Lock objects allow us to acquire and release a lock flexibly.

The biggest advantage of Lock objects over synchronized methods or statements is their ability to back out of an attempt to acquire a lock, which is also known as an effective way to avoid making deadlocks. There’re well-known methods as follows:

  • Lock.lock acquires the lock if it’s available. If it’s not available, the thread will wait until the lock acquired by another thread is released.
  • Lock.unlock release the lock.
  • Lock.tryLock tries to acquire the lock without waiting. It returns true if it acquires the lock successfully, and false otherwise.
Java
// how to use java.util.concurrent.locks.Lock
class LockSample {
    private final Lock lock = new ReentrantLock();

    // example 1
    // wait until the thread acquires the lock
    void method1(){
        lock.lock();
        try {
            // do something
        } finally {
            lock.unlock();
        }
    }

    // example 2
    // try to lock without waiting
    void method2(){
        if(lock.tryLock()){
            try {
                // do something
            } finally {
                lock.unlock();
            }
        }
    }

    // example 3
    // wait until the thread acquires the lock for the specified time
    void method3(){
        if(lock.tryLock(1000, TimeUnit.MILLISECOND)){
            try {
                // do something
            } finally {
                lock.unlock();
            }
        } else {
            // time out
        }
    }
}

Reference


Posted

in

Tags: