Concurrent Programming in Java (Multi Threading)
Ankit Tripathi
Lead Consultant @ ITC Infotech | Master of Science, Full-Stack Development
Today, we all are used to working on systems that can do more than one things at a time. Not just systems, even a single application is expected to do more than one thing at a time. For example, music streaming applications can stream music in the background, while at the same time, users can browse for what they want to play next. These type of softwares are called concurrent softwares.
Considering these requirements, Java also provides support for concurrent programming. This article gives an brief introduction to the Java concurrency support. Java also provides high-level concurrency APIs provided in java.util.concurrent package, but will cover more on that in forthcoming articles.
To understand concurrent programming, its important to first understand the processes and threads. Even if a system has only a single execution core, and can only execute a single thread at a time, it can still have many active threads/processes. But most of the modern computers today have multiple processors with multiple execution cores, thus, making computers better at handling concurrent softwares.
Processes and Threads
Processes are generally seen as equivalent to a program or application. But that's not true, even a single application can contain multiple processes. A process has its own self-containing execution environment (i.e. memory space + run time resources). If an application has multiple processes, then to communicate between these processes, Operating System's IPC resources like socket and pipes can be used. To implement a new process in a Java application, we can use ProcessBuilder object.
But, in Java, concurrent programming for most practical purposes revolves around threads. Threads can be understood as lightweight processes, because, they require lesser resources as compared to a new process. Since threads exist within a process, it shares process' resources. This makes them more efficient, but, also more complex, in terms of inter-thread communication.
Every Java application (from developer's point of view) starts with just one main thread. But, there are multiple system threads running in background for tasks like memory management, etc.
Every Thread in Java, is an instance of Thread class. There are two ways to use a Thread class object. First, directly create a Thread class object, to get more control over thread creation and management. The other way is to pass the tasks to an executor.
How to instantiate and then start a Thread?
There are two ways to create an instance of Thread, and then to start it, we can call the Thread class start method.
1) Use Runnable interface, and define its run method. We can then pass this to the Thread class constructor.
class RunnableDemo implements Runnable {
public void run() {
System.out.println("Hello RunnableDemo");
}
}
public class ThreadDemo1 {
public static void main(String args[]) {
Thread th1 = new Thread(new RunnableDemo());
th1.start();
}
}
2) Create a new class extending Thread class, and override run method.
class ThreadDemo implements Runnable {
public void run() {
System.out.println("Hello ThreadDemo");
}
}
public class ThreadDemo2 {
public static void main(String args[]) {
Thread th2 = new ThreadDemo();
th2.start();
}
}
So which way should we use? The first approach provides more flexibility, as we can extend other classes. It is also useful, if we want to use high-level thread management APIs provided by Java. Second approach is easier to use, but the class can't extend any other class. Thread class defines a number of methods which can be used for direct thread management, and to get information about the thread.
Can we stop (or) pause a running Thread?
Thread class provides Thread.sleep method which can suspend the execution of the current thread. sleep method provides a mean to make processor available for some other thread. We can provide sleep time in either milliseconds or nanoseconds, based on which overloaded method we use. A sleeping thread can also be interrupted (and throws InterruptedException if interrupted by another thread during sleep active phase).
public class SleepMethodDemo {
public static void main(String args[])
throws InterruptedException {
String arrayWithImportantInfo[] = {
"info 1",
"info 2",
"info 3",
"info 4"
};
// will print info with 4 seconds interval
for (int i = 0;
i < arrayWithImportantInfo.length;
i++) {
Thread.sleep(4000); // 4000 milliseconds
System.out.println(arrayWithImportantInfo[i]);
}
}
}
To handle InterruptedException, we can catch the exception inside the method and gracefully return from run method immediately.
public class SleepMethodDemo {
public static void main(String args[]) {
String arrayWithImportantInfo[] = {
"info 1",
"info 2",
"info 3",
"info 4"
};
// will print info with 4 seconds interval
for (int i = 0;
i < arrayWithImportantInfo.length;
i++) {
try {
Thread.sleep(4000); // 4000 milliseconds
} catch (InterruptedException e) {
return;
}
System.out.println(arrayWithImportantInfo[i]);
}
}
}
If a thread doesn't invoke a method that throws InterruptedException. It can check interruption by using Thread.interrupted method.
for (int i = 0; i < inputs.length; i++) {
someTask(inputs[i]);
if (Thread.interrupted()) {
return;
}
}
Internally an interrupt status flag is used for implementing the interrupt mechanism. To set the flag, we can invoke Thread.interrupt method. Now, when a thread checks for interruption using Thread.interrupted (static method), interrupt status is again cleared. To check the interrupt status of another thread, we can use isInterrupted method, which doesn't change the interrupt status flag.
Can we make a Thread wait for another thread?
Thread class provides join method to make a thread wait for completion of another thread. To make current thread wait for a thread 't', we can call the join method as shown below. It also throws InterruptedException, when interrupted.
t.join();
t.join(1000); // to wait for 1 second
Inter-Thread Communication and issues
Threads share resources (like object reference), this makes them very efficient and light-weight. But this also leads to two kinds of errors : thread interference and memory consistency error. To prevent these errors Synchronization is required.
If multiple threads try to access the same resource simultaneously using Synchronization, this can lead to contention between the threads. This can cause suspension of thread execution (Deadlock), or Starvation and livelock.
If multiple threads try to access the same data for their separate operations, it can lead to Interference. Under different circumstances, interference can lead to unpredictable bugs that are difficult to detect and fix.
In some cases, if multiple threads are accessing same data, there is a possibility that they have inconsistent information of that same data. These are called memory consistency errors. To avoid this error , its important to guarantee that changes made by one thread should be visible to other threads, also known as happens-before relationship.
领英推荐
One of the ways to achieve happens-before relationship is synchronization. Synchronization can be achieved using either synchronized methods or synchronized statements.
'volatile' variables : Using volatile variable reduces the chances of getting memory consistency error. Whenever we write to volatile variables, it establishes a happens-before relationship with future reads of that variable. Hence, the changes to the volatile variable are made visible to other threads.
Synchronized Methods in Java
We can make a method synchronized by just adding synchronized keyword to the method declaration.
public class SynchronizedCounterDemo {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
By declaring a method as synchronized, we make sure that only single thread is able to execute that method for an object. All other threads are blocked to invoke this method for the same object. Synchronized method also automatically establishes a happen-before relationship with all the subsequent invocation of the same method for the same object. Hence, the changes to the object are visible to all the threads.
So, if any object is to be used by more than one thread, then we can declare the methods of the corresponding class (which reads or writes object's variables) as synchronized. This prevents thread interference and memory inconsistency errors.
But how does this Synchronization actually work?
Internally Java implements the Synchronization using locks. These intrinsic locks enforce exclusive access to the synchronized area (and thereby access to the object's state). The locks also establish happens-before relationships.
Every object has its intrinsic lock. For a thread to have access to the object's state, it must first acquire the intrinsic lock associated with that object. The thread should also release the lock, once it is done with the work. Also, only a single thread can own the lock at a time, and other threads are blocked.
When a thread calls a synchronized method, it acquires the lock associated with that object. When code returns from that method (even if returned due to an exception), the lock is released.
If the synchronized method is a static method, then the thread must acquire the Class level lock.
Synchronized methods are not the only way to achieve synchroniation. It can also be done using synchronized statements. Synchronized statements requires the programmer to specify the object for which the lock is required.
public void addNotSynchronized() {
synchronized(this) {
this.count++;
}
this.someOtherTasks();
}
Synchronized statement allow us to write both synchronized and unsynchronized code in the same method. As a result, we are able to achieve much finer control over synchronization and helps in improving concurrency, as this reduces unnecessary blocking.
Can a lock be acquired more than once?
Although a thread can't acquire a lock owned by some other thread, but it can acquire a lock it already owns. Confused? Imagine a synchronized method/statement that calls another method that also contains synchronized code, and both these codes use the same lock. If the thread can't acquire the lock again, then the synchronized code would have to take many additional precautions to avoiding the thread blocking itself! So, to enable reduce the code complexity in these scenarios, Java provides support for thread re-acquiring the same lock again. This is known as reentrant synchronization.
Some more Challenges to execution of concurrent code!
The ability of a concurrent application to execute its code on time, known as liveness, faces many problems like deadlock, starvation and livelock.
Deadlock is a situation where multiple threads are blocked forever, because they are waiting for each other!
Starvation described a situation where a thread is unable to make progress due irregular access to the shared resources. This generally happens if greedy threads keep using the shared resource for a very long period.
Similar to Deadlock, the threads can be also be blocked due to frequent responding to each other! This is called Livelock.
Guarded Blocks
Most common way for threads to coordinate their actions is by using guarded block. In this case, block begins only after a certain condition is true. An efficient way to implement this guard is by using Object.wait method. This method suspends the current thread and the wait method doesn't return until another thread hasn't issued notification of task completion.
Always take care to call wait method from inside a loop which continuously checks the condition after which we can resume the current thread's execution.
Also, before calling wait method on an object, the thread must acquire the lock of that object. An easy way to achieve this is by calling wait method from inside a synchronized method.
But, when wait method is called, the thread releases the lock and the execution is suspended. It then waits for some other thread which has the same lock to invoke Object.notifyAll method. This method notifies all the threads waiting on that lock that the action they were waiting for is completed.
public synchronized void guardedBlockDemo() {
while(!conditionTrue) {
try {
wait();
} catch (InterruptedException e) {}
}
System.out.println("All work done");
}
public synchronized notifyGuardedBlockDemo() {
conditionTrue = true;
notifyAll();
}
Just like other methods seen earlier wait method can also throw InterruptedException.
Conclusion
In this article we looked into the low-level APIs provided by Java to support concurrency. These APIs are great for basic concurrent tasks. But for massively concurrent applications to fully utilize the multi core and multi processor computers, more advanced high-level APIs are required. In the coming articles we will look into the high level concurrency objects provided by java.
Ankit Tripathi Please check the more information - https://www.dhirubhai.net/feed/update/urn:li:activity:7225357387342176256