Java ConcurrencyJ8 Home « Java Concurrency

We start this lesson by looking at thread definition, instantiation and execution using the java.lang.Thread and java.lang.Runnable classes. After this we investigate the different states in which a thread can exist, and identify ways in which a thread can transition from one state to another. We finish the lesson by examining Java's locking mechanisms at the class and object level and how we can synchronize methods and blocks of code to protect static or instance variables from concurrent access problems.

Lets take a look at the points outlined at Oracle Java SE 8 Programmer I for this part of the certification.

  • Java Concurrency
    1. Create worker threads using Runnable, Callable and use an ExecutorService to concurrently execute tasks.
    2. Identify potential threading problems among deadlock, starvation, livelock, and race conditions.
    3. Use synchronized keyword and java.util.concurrent.atomic package to control the order of thread execution.
    4. Use java.util.concurrent collections and classes including CyclicBarrier and CopyOnWriteArrayList.
    5. Use parallel Fork/Join Framework.
    6. Use parallel Streams including reduction, decomposition, merging processes, pipelines and performance.

Thread BasicsTop

When we talk about concurrency in this section we are referring to two or more threads of execution running simultaneously, although the same terminology can be applied to concurrent processes. You can think of each thread as separate stacks that operate independently of each other. Whether this is on a single processor, where the underlying operating system schedules the threads to run, so they appear to be running in tandem, or on multiple processors where the threads are actually running in tandem, as far as we are concerned the threads are running concurrently.

Whenever we start up the JVM a thread is created, that usually calls the main() method of the invoking class. This initial thread is a non-daemon thread, also known as a user thread. When we create our own threads, these can be user or daemon threads. So what's the difference? Well daemon threads are background threads that are subordinate to the thread that created them. When the creating thread dies, so do any daemon threads created by it. Every thread we create takes on the characteristics of the thread used to create it, so user threads beget user threads and daemon threads beget daemon threads. Created threads are also given a priority, which is initially the same as the thread used in its creation. We can of course override the default characteristics of thread creation using methods of the Thread class, which are discussed in the Concurrency - Thread Basics lesson, and we can also alter thread priorities as shown in the Concurrency - Thread Priorities lesson.


The JVM will continue executing threads until either of the following happens:

  • All user threads have died in one of the following ways:
    • By returning from the call to the run() method.
    • By throwing an exception that has propagated beyond the run() method.
  • The exit() method of the Runtime class has been called and the security manager has allowed the exit operation to occur.

There are two ways of creating a thread in Java:

  1. By declaring a subclass of the Thread class where the subclass we create should override the run() method of the Thread class.
    1. See Creating A Single Thread
    2. See Creating Multiple Threads
    for examples of how to subclass the Thread class.
  2. By declaring a class that implements the Runnable interface.
    1. See Creating A Single Thread
    2. See Creating Multiple Threads
    for examples of how to implement the Runnable interface.

So which type of thread instantiation should we use? When we subclass the Thread class our threads have to inherit from Thread and therefore we are not only limited to that classes functionality but can't extend anything else. Fortunately for us we can also execute independent threads by implementing the Runnable interface which declares a single run() method that gets executed when the thread is started. Using this approach we are free to extend another class if we need to for added functionality.

This is possible because several of the Thread classes overloaded constructors accept a Runnable object as a parameter. So as long as we implement this interface and use our implementing class in the threads construction, then the thread knows to use the implementing classes overridden run() method when the thread is started.



Different Thread StatesTop

Before we look at some of the methods of the Thread class that alter state, it's worth spending a little time talking about the differing states a thread can exist in and how a thread can transition between the varying states. To make understanding easier we will use a slideshow as we go through the life of a thread; just press the button below to step through each slide.

threadstate1 threadstate2 threadstate3 threadstate4 threadstate5

In this slide we have created a new Thread object, which is just like any other object.

In this slide we have called the start() method on this Thread object and the thread has now transitioned to a runnable thread of execution.

In this slide the thread scheduler has selected this thread to run and the run() method of this thread starts. The thread may return to a runnable state via the thread scheduler or through program code.

In this slide we are showing a blocked state which the thread may enter for any number of reasons. Eventually a blocked thread will return to a runnable state. A thread may just run to completion without ever being blocked and then die.

In this slide the run() method of this thread has ended or an exception has propogated beyond the run() method and the thread is dead.

Press the button below to cycle through our Thread States explanation.


Interrupting A ThreadTop

The user threads we create don't die with the main() method and even daemon threads continue processing until their run() method ends. The overridden run() methods in our lesson examples showed this by displaying thread output after the main() method ended. We can interrupt a thread using the interrupt() method. The interrupt() method doesn't actually stop a thread from running, but sets a flag in the thread that indicates that an interrupt has been requested. The flag must be checked in the run() method for its effect to have any impact.

See the interrupt() method for an example, where we use the sleep() method, in which we have to specify a try block that checks to see if this thread has been interrupted and then throws the InterruptedException for this thread.

The yield() MethodTop

The static yield() method puts the currently running thread back to a runnable state to allow other threads of the same priority a chance to run. Although this sounds a great way to use thread prioritisation and allow equal usage of the CPU in reality there is no guarantee that this will actually happen. What we can guarantee is that the yield() method puts the currently running thread back to a runnable state. After this happens which thread gets chosen by the scheduler is out of our hands and it is very possible that the yielding thread is selected again and goes straight back to executing. Lets have a look at how we can use the yield() method:

See the yield() method for an example.

Connecting ThreadsTop

We have limited control over the order in which threads are run, but we do have some control. We can cease execution of a thread for a specified amount of time using the sleep() method. We can also use the yield() method, to cease execution of a thread up to a specified amount of time and there are also options to prioritize threads as discussed in the Concurrency - Thread Priorities lesson and how to lock threads as discussed in the Concurrency - Synchronization lesson.

The join() MethodTop

There is another option available to us, the join() method of the Thread class which allows us to connect the current thread to a named thread, which is very handy when we want to guarantee that one thread finishes before another.

See the join() method for an example, where we connect the current thread to a named thread.

SynchronizationTop

Running separate threads of execution at the same time is great and we can certainly improve the performance of our programs using multithreading. But what happens when concurrent threads need to modify the contents of a file or some state within an instance? We could end up with corrupt data, lost updates, unstable object state and all sorts of other unpleasantness when multiple threads are all trying to update the same resource at the same time. What we really need is a way to control access to sensitive data so that only one thread has access to the resource at a time. Java deals with this problem using synchronization and in this lesson we investigate how we can control access to our code by synchronizing at the method or block level.

In Java every class has a lock and each instance of a class we instantiate also has it's own lock. Most of the time we write code these locks are transparent and until reading this sentence you may have been totally unaware about Javas locking system. Its only when we have to deal with situations where a resource can be updated concurrently that we have be aware of locking. So how does locking work? Well locking only comes into play when we synchronize our code using the synchronized keyword. As mentioned above we can lock methods or blocks of code this way and when we do this we lock this section of code from being executed by other threads for the duration of the section of code.

The main emphasis of the lesson will be about synchronization of objects, but before we get into this we will talk briefly about static initializer blocks and static methods. We don't need to worry about static initializer blocks as these are inherently thread safe, as they only ever get called once per class-loader. When we apply synchronization to the static methods of a class, only one of the synchronized static methods can execute at any point in time. This type of locking is done at the class level and a class lock is totally independent of any object locks, in much the same way as static members are independent of any instance. The way we synchronize static methods is no different from the way we synchronize non-static methods; just remember they use different locking mechanisms and apply to a class and an instance of a class respectively.

Synchronized MethodsTop

At the method level synchronization is achieved by checking the lock that every instance has associated with it. When a thread tries to enter a synchronized method, it first checks to see if the lock for this object is available, or put another way, unlocked. If it is then the thread is allowed entry to the method and the lock for this object is locked. This applies to all synchronized methods within the class, so once a lock has been set for an object all other synchronized methods are locked out for that object. When the object returns from the method that the lock was set on, the object becomes unlocked, so any threads waiting for this object's lock have a chance to enter a synchronized method and restart the locking/unlocking process. In this way we can guarantee that we only ever enter one synchronized method for an object at a time.

A really important thing to remember here is that we are only excluding other threads of the same object from entering the synchronized code. If we had three threads, representing three different objects, then they all have different locks and therefore could all use the same synchronized method concurrently. Also we don't need to worry about local variables as each thread gets its own set of these on entry to a method. Two threads running the same method concurrently will have different copies of the local variables that will work independently.

This is a lot of information to get our heads around so we will illustrate some synchronization usage with a slide show; just press the button below to step through each slide.

sync1 sync2 sync3 sync4 sync5 sync5

Press the button below to cycle through our Synchronized Methods explanation.

See Using the synchronized Keyword method for example code of the above slideshow.



Synchronized BlocksTop

Using synchroniaztion does come at a cost, when we synchronize a method there is extra processing that needs to be done every time the method is entered to check that the lock is available. There is also the fact that while one thread has the lock, any other threads for this object that need to use the synchronized method are locked out and queued, until the active thread releases the lock. Java also allows us to synchronize a block of code instead of a whole method, which can make the lock time less of an overhead and therefore our synchronized code more efficient.

See Synchronized Blocks for example code using synchronized blocks.

Deadlock ScenarioTop

There is a situation that can arise when using synchronized code that we as programmers need to be aware, which is known as deadlocking. A deadlock can occur when an object is locked and the thread of execution tries to access another object. The other object is also locked and is trying to access this thread of executions locked object. The object locks are basically locking each other out forever and thus are deadlocked.

See Deadlock Scenario for example code that can cause a deadlock.


Related Java Tutorials

Concurrency - Thread Basics
Concurrency - The Runnable Interface
Concurrency - Thread Priorities
Concurrency - Thread Communication
Concurrency - Synchronization