Core Java

Introduction to Threads and Concurrency

This article is part of our Academy Course titled Java Concurrency Essentials.

In this course, you will dive into the magic of concurrency. You will be introduced to the fundamentals of concurrency and concurrent code and you will learn about concepts like atomicity, synchronization and thread safety. Check it out here!

1. Basic know-how about threads

Concurrency is the ability of a program to execute several computations simultaneously. This can be achieved by distributing the computations over the available CPU cores of a machine or even over different machines within the same network.

To achieve a better understanding of parallel execution, we have to distinguish between processes and threads. Processes are an execution environment provided by the operating system that has its own set of private resources (e.g. memory, open files, etc.). Threads in contrast are processes that live within a process and share their resources (memory, open files, etc.) with the other threads of the process.

The ability to share resources between different threads makes threads more suitable for tasks where performance is a significant requirement. Though it is possible to establish an inter-process communication between different processes running on the same machine or even on different machines within the same network, for performance reasons, threads are often chosen to parallelize the computation on a single machine.

In Java, processes correspond to a running Java Virtual Machine (JVM), whereas threads live within the same JVM and can be created and stopped by the Java application dynamically during runtime. Each program has at least one thread: the main thread. This main thread is created during the start of each Java application and it is the one that calls the main() method of your program. From this point on, the Java application can create new Threads and work with them.

This is demonstrated in the following source code. Access to the current Thread is provided by the static method currentThread() of the JDK class java.lang.Thread:

	public class MainThread {
		
		public static void main(String[] args) {
			long id = Thread.currentThread().getId();
			String name = Thread.currentThread().getName();
			int priority = Thread.currentThread().getPriority();
			State state = Thread.currentThread().getState();
			String threadGroupName = Thread.currentThread().getThreadGroup().getName();
			System.out.println("id="+id+"; name="+name+"; priority="+priority+"; state="+state+"; threadGroupName="+threadGroupName);
		}
	}

As you can see from the source code of this simple application, we access the current Thread directly in the main() method and print out some of the information provided about it:

	id=1; name=main; priority=5; state=RUNNABLE; threadGroupName=main

The output reveals some interesting information about each thread. Each thread has an identifier, which is unique in the JVM. The name of the threads helps to find certain threads within external applications that monitor a running JVM (e.g. a debugger or the JConsole tool). When more than one threads are executed, the priority decides which task should be executed next.

The truth about threads is that not all threads are really executed simultaneously, but the execution time on each CPU core is divided into small slices and the next time slice is assigned to the next waiting thread with the highest priority. The scheduler of the JVM decides based on the thread’s priorities which thread to execute next.

Next to the priority, a thread also has a state, which can be one of the following:

  • NEW: A thread that has not yet started is in this state.
  • RUNNABLE: A thread executing in the Java virtual machine is in this state.
  • BLOCKED: A thread that is blocked waiting for a monitor lock is in this state.
  • WAITING: A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
  • TIMED_WAITING: A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
  • TERMINATED: A thread that has exited is in this state.

Our main thread from the example above is of course in the state RUNNABLE. State names like BLOCKED indicate here already that thread management is an advanced topic. If not handled properly, threads can block each other which in turn cause the application to hang. But we will come to this later on.

Last but not least the attribute threadGroup of our thread indicates that threads are managed in groups. Each thread belongs to a group of threads. The JDK class java.lang.ThreadGroup provides some methods to handle a whole group of Threads. With these methods we can for example interrupt all threads of a group or set their maximum priority.

2. Creating and starting threads

Now that we have taken a closer look at the properties of a thread, it is time to create and start our first thread. Basically there are two ways to create a thread in Java. The first one is to write a class that extends the JDK class java.lang.Thread:

	public class MyThread extends Thread {
		
		public MyThread(String name) {
			super(name);
		}

		@Override
		public void run() {
			System.out.println("Executing thread "+Thread.currentThread().getName());
		}
		
		public static void main(String[] args) throws InterruptedException {
			MyThread myThread = new MyThread("myThread");
			myThread.start();
		}
	}

As can be seen above, the class MyThread extends the Thread class and overrides the method run(). The method run() gets executed once the virtual machine starts our Thread. As the virtual machine has to do some work in order to setup the execution environment for the thread, we cannot call this method directly to start the thread. Instead we call the method start() on an instance of the class MyThread. As this class inherits the method stop() from its superclass, the code behind this method tells the JVM to allocate all necessary resources for the thread and to start it. When we run the code above, we see the output “Executing thread myThread”. In contrast to our introduction example, the code within the method run() is not executed within the “main” thread but rather in our own thread called “myThread”.

The second way to create a thread is a class that implements the interface Runnable:

	public class MyRunnable implements Runnable {

		public void run() {
			System.out.println("Executing thread "+Thread.currentThread().getName());
		}
		
		public static void main(String[] args) throws InterruptedException {
			Thread myThread = new Thread(new MyRunnable(), "myRunnable");
			myThread.start();
		}
	}

The main difference to the subclassing approach is the fact that we create an instance of java.lang.Thread and provide an instance of the class that implements the Runnable interface as an argument to the Thread constructor. Next to this instance we also pass the name of the Thread, so that we see the following output when we execute the program from command line: “Executing thread myRunnable”.

Whether you should use the subclassing or the interface approach, depends to some extend on your taste. The interface is a more light-weight approach as all you have to do is the implementation of an interface. The class can still be a subclass of some other class. You can also pass your own parameters to the constructor whereas subclassing Thread restricts you to the available constructors that the class Thread brings along.

Later on in this series we will get to know about thread pools and see how to start multiple threads of the same type. Here we will again use the Runnable approach.

3. Sleeping and interrupting

Once we have started a Thread, it runs until the run() method reaches it end. In the examples above the run() method did nothing more than just printing out the name of the current thread. Hence the thread finished very soon.

In real world applications you will normally have to implement some kind of background processing where you let the thread run until it has for example processed all files within a directory structure, for example. Another common use case is to have a background thread that looks every n seconds if something has happened (e.g. a file has been created) and starts some kind of action. In this case you will have to wait for n seconds or milliseconds. You could implement this by using a while loop whose body gets the current milliseconds and looks when the next second has passed. Although such an implementation would work, it is a waste of CPU processing time as your thread occupies the CPU and retrieves the current time again and again.

A better approach for such use cases is calling the method sleep() of the class java.lang.Thread like in the following example:

	public void run() {
		while(true) {
			doSomethingUseful();
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

An invocation of sleep() puts the current Thread to sleep without consuming any processing time. This means the current thread removes itself from the list of active threads and the scheduler doesn’t schedule it for the next execution until the second (given in milliseconds) has passed.

Please note that the time passed to the sleep() method is only an indication for the scheduler and not an absolutely exact time frame. It may happen that the thread comes back a few nano or milliseconds earlier or later due to the scheduling that is put in practice. Hence you should not use this method for real-time scheduling purposes. But for most use cases the achieved accuracy is sufficient.

In the code example above you may have noticed the InterruptedException that sleep() may throw. Interrupts are a very basic feature for thread interaction that can be understood as a simple interrupt message that one thread sends to another thread. The receiving thread can explicitly ask if it has been interrupted by calling the method Thread.interrupted() or it is implicitly interrupted while spending his time within a method like sleep() that throws an exception in case of an interrupt.

Let’s take a closer look at interrupts with the following code example:

	public class InterruptExample implements Runnable {

		public void run() {
			try {
				Thread.sleep(Long.MAX_VALUE);
			} catch (InterruptedException e) {
				System.out.println("["+Thread.currentThread().getName()+"] Interrupted by exception!");
			}
			while(!Thread.interrupted()) {
				// do nothing here
			}
			System.out.println("["+Thread.currentThread().getName()+"] Interrupted for the second time.");
		}

		public static void main(String[] args) throws InterruptedException {
			Thread myThread = new Thread(new InterruptExample(), "myThread");
			myThread.start();
			
			System.out.println("["+Thread.currentThread().getName()+"] Sleeping in main thread for 5s...");
			Thread.sleep(5000);
			
			System.out.println("["+Thread.currentThread().getName()+"] Interrupting myThread");
			myThread.interrupt();
			
			System.out.println("["+Thread.currentThread().getName()+"] Sleeping in main thread for 5s...");
			Thread.sleep(5000);
			
			System.out.println("["+Thread.currentThread().getName()+"] Interrupting myThread");
			myThread.interrupt();
		}
	}

Within the main method we start a new thread first, which would sleep for a very long time (about 290.000 years) if it would not be interrupted. To get the program finished before this time has passed by, myThread is interrupted by calling interrupt() on its instance variable in the main method. This causes an InterruptedException within the call of sleep() and is printed on the console as “Interrupted by exception!”. Having logged the exception the thread does some busy waiting until the interrupted flag on the thread is set. This again is set from the main thread by calling interrupt() on the thread’s instance variable. Overall we see the following output on the console:

[main] Sleeping in main thread for 5s...
[main] Interrupting myThread
[main] Sleeping in main thread for 5s...
[myThread] Interrupted by exception!
[main] Interrupting myThread
[myThread] Interrupted for the second time.

What is interesting in this output, are the lines 3 and 4. If we go through the code we might have expected that the string “Interrupted by exception!” is printed out before the main thread starts sleeping again with “Sleeping in main thread for 5s…”. But as you can see from the output, the scheduler has executed the main thread before it started myThread again. Hence myThread prints out the reception of the exception after the main thread has started sleeping.

It is a basic observation when programming with multiple threads that logging output of threads is to some extend hard to predict as it’s hard to calculate which thread gets executed next. Things get even worse when you have to cope with more threads whose pauses are not hard coded as in the examples above. In these cases the whole program gets some kind of inner dynamic that makes concurrent programming a challenging task.

4. Joining Threads

As we have seen in the last section we can let our thread sleep until it is woken up by another thread. Another important feature of threads that you will have to use from time to time is the ability of a thread to wait for the termination of another thread.

Let’s assume you have to implement some kind of number crunching operation that can be divided into several parallel running threads. The main thread that starts the so called worker threads has to wait until all its child threads have terminated. The following code shows how this can be achieved:

	public class JoinExample implements Runnable {
		private Random rand = new Random(System.currentTimeMillis());

		public void run() {
			//simulate some CPU expensive task
			for(int i=0; i<100000000; i++) {
				rand.nextInt();
			}
			System.out.println("["+Thread.currentThread().getName()+"] finished.");
		}

		public static void main(String[] args) throws InterruptedException {
			Thread[] threads = new Thread[5];
			for(int i=0; i<threads.length; i++) {
				threads[i] = new Thread(new JoinExample(), "joinThread-"+i);
				threads[i].start();
			}
			for(int i=0; i<threads.length; i++) {
				threads[i].join();
			}
			System.out.println("["+Thread.currentThread().getName()+"] All threads have finished.");
		}
	}

Within our main method we create an array of five Threads, which are all started one after the other. Once we have started them, we wait in the main Thread for their termination. The threads itself simulate some number crunching by computing one random number after the other. Once they are finished, they print out “finished”. Finally the main thread acknowledges the termination of all of its child threads:

[joinThread-4] finished.
[joinThread-3] finished.
[joinThread-2] finished.
[joinThread-1] finished.
[joinThread-0] finished.
[main] All threads have finished.

You will observe that the sequence of “finished” messages varies from execution to execution. If you execute the program more than once, you may see that the thread which finishes first is not always the same. But the last statement is always the main thread that waits for its children.


 

5. Synchronization

As we have seen in the last examples, the exact sequence in which all running threads are executed depends next to the thread configuration like priority also on the available CPU resources and the way the scheduler chooses the next thread to execute. Although the behavior of the scheduler is completely deterministic, it is hard to predict which threads execute in which moment at a given point in time. This makes access to shared resources critical as it is hard to predict which thread will be the first thread that tries to access it. And often access to shared resources is exclusive, which means only one thread at a given point in time should access this resource without any other thread interfering this access.

A simple example for concurrent access of an exclusive resource would be a static variable that is incremented by more than one thread:

	public class NotSynchronizedCounter implements Runnable {
		private static int counter = 0;

		public void run() {
			while(counter < 10) {
				System.out.println("["+Thread.currentThread().getName()+"] before: "+counter);
				counter++;
				System.out.println("["+Thread.currentThread().getName()+"] after: "+counter);
			}
		}

		public static void main(String[] args) throws InterruptedException {
			Thread[] threads = new Thread[5];
			for(int i=0; i<threads.length; i++) {
				threads[i] = new Thread(new NotSynchronizedCounter(), "thread-"+i);
				threads[i].start();
			}
			for(int i=0; i<threads.length; i++) {
				threads[i].join();
			}
		}
	}

When we take a closer look at the output of this simple application, we see something like:

[thread-2] before: 8
[thread-2] after: 9
[thread-1] before: 0
[thread-1] after: 10
[thread-2] before: 9
[thread-2] after: 11

Here, thread-2 retrieves the current value as 8, increments it and afterwards the value is 9. This is how we would have expected it before. But what the following thread executes may wonder us. thread-1 outputs the current value as zero, increments it and afterwards the value is 10. How can this happen? When thread-1 read the value of the variable counter, it was 0. Then the context switch executed the second thread and when thread-1 had his turn again, the other threads already incremented the counter up to 9. Now he adds one and gets 10 as a result.

The solution for problems like this is the synchronized key word in Java. With synchronized you can create blocks of statements which can only be accessed by a thread, which gets the lock on the synchronized resource. Let’s change the run() method from the last example and introduce a synchronized block for the whole class:

	public void run() {
		while (counter < 10) {
			synchronized (SynchronizedCounter.class) {
				System.out.println("[" + Thread.currentThread().getName() + "] before: " + counter);
				counter++;
				System.out.println("[" + Thread.currentThread().getName() + "] after: " + counter);
			}
		}
	}

The synchronized(SynchronizedCounter.class) statement works like a barrier where all threads have to stop and ask for entrance. Only the first thread that gets the lock on the resources is allowed to pass. Once it has left the synchronized block, another waiting thread can enter and so forth.

With the synchronized block around the output and increment of the counter above the output looks like the following example:

[thread-1] before: 11
[thread-1] after: 12
[thread-4] before: 12
[thread-4] after: 13

Now you will see only subsequent outputs of before and after that increment the counter variable by one.
The synchronized keyword can be used in two different ways. It can either be used within a method as shown above. In this case you have to provide a resource that is locked by the current thread. This resource has to be chosen carefully because the thread barrier becomes a completely different meaning based on the scope of the variable.

If the variable is a member of the current class then all threads are synchronized regarding an instance of the class because the variable sync exists per instance of LocalSync:

	public class LocalSync {
		private Integer sync = 0;

		public void someMethod() {
			synchronized (sync) {
				// synchronized on instance level
			}
		}
	}

Instead of creating a block that covers the whole body of the method, you can also add the keyword synchronized to the method signature. The code below has the same effect as the code above:

	public class MethodSync {
		private Integer sync = 0;

		public synchronized void someMethod() {
			// synchronized on instance level
		}
	}

The main difference between the two approaches is the fact that the first one is finer grained, as you can make the synchronized block smaller than the method body. Please remember that synchronized blocks are executed only by one thread at a time, hence each synchronized block is a potential performance problem, as all concurrently running threads may have to wait until the current thread leaves the block. Hence we should always try to make the block as small as possible.

Most often you will have to synchronize access to some resource that only exists once per JVM. The common means to do that is to use static member variable of a class:

	public class StaticSync {
		private static Integer sync = 0;

		public void someMethod() {
			synchronized (sync) {
				// synchronized on ClassLoader/JVM level
			}
		}
	}

The code above synchronizes all threads that run through the method someMethod() within the same JVM as the static variable only exists once within the same JVM. As you may know, a class is only unique within one JVM if it is loaded by the same class loader. If you load the class StaticSync with multiple class loaders then the static variable exists more than once. But in most day to day applications you won’t have multiple class loaders that load the same class twice, hence you can assume that the static variable exists only once and therefore all threads within the same JVM have to wait at the barrier until they get the lock.

6. Atomic Access

In the previous section we saw how to synchronize access to some complex resource when many concurrent threads have to execute a certain part of code but only one thread should execute it at each point in time. We have also seen that if we do not synchronize access to common resources, operations on these resources interleave and may cause an illegal state.

The Java language provides some basic operations that are atomic and that therefore can be used to make sure that concurrent threads always see the same value:

  • Read and write operations to reference variables and primitive variables (except long and double)
  • Read and write operations for all variables declared as volatile

To understand this in more detail, let’s assume we have a HashMap filled with properties that are read from a file and a bunch of threads that work with these properties. It is clear that we need some kind of synchronization here, as the process of reading the file and update the Map costs time and that during this time other threads are executed.

We cannot easily share one instance of this Map between all threads and work on this Map during the update process. This would lead to an inconsistent state of the Map, which is read by the accessing threads. With the knowledge from the last section we could of course use a synchronized block around each access (read/write) of the map to ensure that the all threads only see one state and not a partially updated Map. But this leads to performance problems if the concurrent threads have to read very often from the Map.

Cloning the Map for each thread within a synchronized block and letting each thread work on a separate copy would be a solution, too. But each thread would have to ask from time to time for an updated copy and the copy occupies memory, which might not be feasible in each case. But there is a more simple solution.

Since we know that write operations to a reference are atomic, we can create a new Map each time we read the file and update the reference that is shared between the threads in one atomic operation. In this implementation the worker threads will never read an inconsistent Map as the Map is updated with one atomic operation:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class AtomicAssignment implements Runnable {
	private static volatile Map<String, String> configuration = new HashMap<String, String>();

	public void run() {
		for (int i = 0; i < 10000; i++) {
			Map<String, String> currConfig = configuration;
			String value1 = currConfig.get("key-1");
			String value2 = currConfig.get("key-2");
			String value3 = currConfig.get("key-3");
			if (!(value1.equals(value2) && value2.equals(value3))) {
				throw new IllegalStateException("Values are not equal.");
			}
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public static void readConfig() {
		Map<String, String> newConfig = new HashMap<String, String>();
		Date now = new Date();
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss:SSS");
		newConfig.put("key-1", sdf.format(now));
		newConfig.put("key-2", sdf.format(now));
		newConfig.put("key-3", sdf.format(now));
		configuration = newConfig;
	}

	public static void main(String[] args) throws InterruptedException {
		readConfig();
		Thread configThread = new Thread(new Runnable() {
			public void run() {
				for (int i = 0; i < 10000; i++) {
					readConfig();
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}, "configuration-thread");
		configThread.start();
		Thread[] threads = new Thread[5];
		for (int i = 0; i < threads.length; i++) {
			threads[i] = new Thread(new AtomicAssignment(), "thread-" + i);
			threads[i].start();
		}
		for (int i = 0; i < threads.length; i++) {
			threads[i].join();
		}
		configThread.join();
		System.out.println("[" + Thread.currentThread().getName() + "] All threads have finished.");
	}
}

The above example is a little more complex, but not hard to understand. The Map, which is shared, is the configuration variable of AtomicAssignment. In the main() method we read the configuration initially one time and add three keys to the Map with the same value (here the current time including milliseconds). Then we start a “configuration-thread” that simulates the reading of the configuration by adding all the time the current timestamp three times to the map. The five worker threads then read the Map using the configuration variable and compare the three values. If they are not equal, they throw an IllegalStateException.

You can run the program for some time and you will not see any IllegalStateException. This is due the fact that we assign the new Map to the shared configuration variable in one atomic operation:

	configuration = newConfig;

We also read the value of the shared variable within one atomic step:

	Map<String, String> currConfig = configuration;

As both steps are atomic, we will always get a reference to a valid Map instance where all three values are equal. If you change for example the run() method in a way that it uses the configuration variable directly instead of copying it first to a local variable, you will see IllegalStateExceptions very soon because the configuration variable always points to the “current” configuration. When it has been changed by the configuration-thread, subsequent read accesses to the Map will already read the new values and compare them with the values from the old map.

The same is true if you work in the readConfig() method directly on the configuration variable instead of creating a new Map and assigning it in one atomic operation to the shared variable. But it may take some time, until you see the first IllegalStateException. And this is true for all applications that use multi-threading. Concurrency problems are not always visible at first glance, but they need some testing under heavy-load conditions in order to appear.

Martin Mois

Martin is a Java EE enthusiast and works for an international operating company. He is interested in clean code and the software craftsmanship approach. He also strongly believes in automated testing and continuous integration.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button