Java Concurrency – Basic Concepts

Defining and Starting a Thread

There are two ways to define a thread: Provide a Runnable object or Subclass Thread. The first approach is more flexible and applicable to high-level thread concurrency APIs. It is also more general because the Runnable object can subclass a class other than Thread.

Synchronization

Synchronization can introduce thread contention, which occurs when two or more threads try to access the same resource simultaneously

  • Thread Interference describes how errors are introduced when multiple threads access shared data.
  • Memory Consistency Errors describes errors that result from inconsistent views of shared memory.
  • Synchronized Methods describes a simple idiom that can effectively prevent thread interference and memory consistency errors.
  • Implicit Locks and Synchronization describes a more general synchronization idiom, and describes how synchronization is based on implicit locks.
  • Atomic Access talks about the general idea of operations that can’t be interfered with by other threads.

Synchronized Methods

The Java programming language provides two basic synchronization idioms: synchronized methods and synchronized statements.

public class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }
}

Synchronized methods enable a simple strategy for preventing thread interference and memory consistency errors: if an object is visible to more than one thread, all reads or writes to that object’s variables are done through synchronized methods. This strategy is effective but can present problems with liveness.

Liveness

The most common kind of liveness problems are deadlockstarvation and livelock.

Deadlock

Deadlock describes a situation where two or more threads are blocked forever, waiting for each other

// Warning: deadlock-prone!
public class LeftRightDeadlock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight() {
  synchronized (left) {
    synchronized (right) {
      doSomething();
    }
  }
}

public void rightLeft() {
  synchronized (right) {
    synchronized (left) {
      doSomethingElse();
    }
 }
}
}

Starvation and Livelock

Starvation and livelock are much less common a problem than deadlock. Starvation describes a situation where a thread is unable to gain regular access to shared resources and is unable to make progress. A thread often acts in response to the action of another thread. If the other thread’s action is also a response to the action of another thread, then livelock may result. As with deadlock, livelocked threads are unable to make further progress. However, the threads are not blocked — they are simply too busy responding to each other to resume work.

Immutable Objects

An object is immutable if its state cannot change after it is constructed and it is useful in concurrent applications. Since they cannot change state, they cannot be corrupted by thread interference. The drawback of immutable objects is that there is a cost of creating a new immutable object. However, we can balance this drawback with many benefits such as decreased overhead due to garbage collection, and the elimination of code needed to protect mutable objects from corruption.

A Strategy for Defining Immutable Objects

  1. Don’t provide “setter” methods as they modify the instance variables or objects referred to by these variables.
  2. Make all instance variables final and private.
  3. Declare the class as final. We don’t want subclasses to override methods. Another approach is to make the constructor private and construct instances in factory methods.
  4. If the instance variables include references to mutable objects, don’t allow those objects to be changed:
    • Don’t provide methods that modify the mutable objects.
    • Don’t share references to the mutable objects. Never store references to external, mutable objects passed to the constructor; if necessary, create copies, and store references to the copies. Similarly, create copies of your internal mutable objects when necessary to avoid returning the originals in your methods.

High-Level Concurrency Objects

Lock Objects

Before Java 5.0, the only mechanisms for coordinating access to shared data were synchronized and volatile. Java 5.0 adds another option: ReentrantLock, which is an alternative with advanced features for when intrinsic locking proves too limited.

What is intrinsic lock?

Synchronization is built around an internal entity known as the intrinsic lock or monitor lock. Every object has an intrinsic lock. By convention, a thread that needs exclusive and consistent access to an object’s fields has to acquire the object’s intrinsic lock before accessing them, and then release the intrinsic lock when it’s done with them [Java Doc].

Lock objects work very much like the implicit locks used by synchronized code. As with implicit locks, only one thread can own a Lock object at a time. 

Lock lock = new ReentrantLock();

try{
    lock.lock();
      //critical section
} finally {
    lock.unlock();
}

Example of Lock protected counter

public class LockCounter {
    private int c = 0;

    private Lock lock = new ReentrantLock();

    public synchronized void increment() {
        try {
            lock.lock();
            this.c++;
        } finally {
            lock.unlock();
        }
    }

    public synchronized void decrement() {
        try {
            lock.lock();
            this.c--;
        } finally {
            lock.unlock();
        }
    }

    public synchronized int value() {
        try {
            lock.lock();
            return this.c; 
        } finally {
            lock.unlock();
        }
    }
}

Executors

In all of the previous examples, there’s a close connection between the task being done by a new thread and the thread itself. However, in large applications, we need to separate thread management and creation from the rest of the application. Objects that encapsulate these functions are known as executors.

  • Executor Interfaces define the three executor object types.
  • Thread Pools are the most common kind of executor implementation.
  • Fork/Join is a framework (new in JDK 7) for taking advantage of multiple processors.

Concurrent Collections

The java.util.concurrent package includes a number of additions to the Java Collections Framework. These are most easily categorized by the collection interfaces provided:

  • BlockingQueue defines a first-in-first-out data structure that blocks or times out when you attempt to add to a full queue, or retrieve from an empty queue.
  • ConcurrentMap is a subinterface of java.util.Map that defines useful atomic operations. These operations remove or replace a key-value pair only if the key is present, or add a key-value pair only if the key is absent. Making these operations atomic helps avoid synchronization. The standard general-purpose implementation of ConcurrentMap is ConcurrentHashMap.
  • ConcurrentNavigableMap is a subinterface of ConcurrentMap that supports approximate matches. The standard general-purpose implementation of ConcurrentNavigableMap is ConcurrentSkipListMap

Atomic Variables

The java.util.concurrent.atomic package defines classes that support atomic operations on single variables. Example of AtomicInteger protected counter without using synchronized methods or Lock interface:

class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {
        c.incrementAndGet();
    }

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }

}

References

  1. https://docs.oracle.com/javase/tutorial/essential/concurrency/index.html
  2. https://jenkov.com/tutorials/java-util-concurrent/lock.html

Leave a Reply

Your email address will not be published. Required fields are marked *