DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • How to Convert XLS to XLSX in Java
  • Recurrent Workflows With Cloud Native Dapr Jobs
  • Java Virtual Threads and Scaling
  • Java’s Next Act: Native Speed for a Cloud-Native World

Trending

  • AI's Dilemma: When to Retrain and When to Unlearn?
  • Rust and WebAssembly: Unlocking High-Performance Web Apps
  • Debugging Core Dump Files on Linux - A Detailed Guide
  • How to Convert Between PDF and TIFF in Java
  1. DZone
  2. Coding
  3. Java
  4. Java Concurrency and Multi-Threading

Java Concurrency and Multi-Threading

In this post, we'll discuss various concepts we commonly use in concurrent programming and how to resolve synchronization, race conditions, and deadlocks.

By 
Pratik Joshi user avatar
Pratik Joshi
·
Feb. 09, 22 · Analysis
Likes (5)
Comment
Save
Tweet
Share
63.7K Views

Join the DZone community and get the full member experience.

Join For Free

The Java platform was designed to support concurrent programming, with basic concurrency support in the Java programming language and the Java class libraries. How often do we think about concurrency while writing new code or while doing a code review with the team? A small bug could lead to endless hours of debugging applications for a production issue that is not easy to reproduce locally. 

Why do we need concurrent programming? We are in the age where we work with machines that are equipped with multi-core CPUs. The code you deliver should be optimized to run on such machines, utilizing the hardware to its fullest.  While designing a concurrent system, we should not interfere with other processes running on the CPU.  We want our application to run in its own black box without impacting other processes. These days we have multicore CPUs that can easily handle this but how was this done back in the days when the single-core was used?

On a single-core CPU, a commonly used scheduling technique Time-Slice was used by the CPU. CPU time or processing time was divided into small chunks called slices. A scheduler is responsible for assigning the CPU time. The process will get its assigned time from the scheduler to finish the job. The scheduler listens to states/events from processes to move to the next process. This switching from one task to another happens at a very high speed. The user experience is not affected for him everything is running at the same time. There are numerous algorithms and techniques that can be used to handle scheduling.

As a developer working on concurrent systems, how can we ensure we design optimal solutions? To answer this question, let's first look into threads. 

Thread is a bunch of instructions we want to execute in a certain way. In your code, you are waiting to print an array while that array is being written from the database by another thread. Here the reader will either write an incomplete array or be empty depending on what is present in the array at that moment, this is known as a race condition. 

Let’s take singleton class as an example and try to see how threads work and try to study race conditions under the hood.

Java
 
public class SingletonExample {
	private static SingletonExample instance;

	private SingletonExample() {}

	public static SingletonExample getInstance() {
		if(instance == null){
		instance = new SingletonExample();
		}
	return instance;
	}
}

 

CPU time 

Thread1

Thread2

Slice1 - 0 ms

Check if the instance of SingletonExample is null

Waiting

Slice2 - 1 ms

the instance is null at this point, so it enters if block

Waiting

Slice3- 2 ms

Thread Scheduler pauses Thread1

Is in a runnable state

Slice4 - 3 ms

Waiting

Check if an instance of SingletonExample is null

Slice5 - 4 ms

Waiting

The instance is null at this point so it enters if block

Slice6 - 5 ms

Waiting

Creates an instance of SingletonExample

Slice7 - 6 ms

Is in a runnable state

Thread Scheduler pauses Thread2

Slice8 - 7ms

Since it has already been checked for null, will create
 a new instance of SingletonExample


For example, there are 2 threads Thread1 and Thread2, both are calling getInstance() of our SingletonExample class. The above table depicts the action performed in the time slice by each thread.

Until Slice7, we do not see any issue and the code executes as expected. However, in slice8 scheduler returns to Thread1 and it skips the null check because that was already performed in slice1. Due to this, we create another instance of our SingletonExample, erasing the instance created by Thread2. This is a race condition, a common bug introduced in concurrent programming.

To solve the race condition encountered above, we can use the concept of synchronization. By using synchronized method declaration we can overcome this race condition, let’s look at sample code.

Java
 
public class SingletonExample {
	private static SingletonExample instance;

	private SingletonExample() {}

	public static synchronized SingletonExample getInstance() {
		if(instance == null){
			instance = new SingletonExample();
		}
	return instance;
	}
}


To synchronize getInstance(), we need to protect the method from being accessed by threads. Every object in java has a lock and key for synchronization, if Thread1 tries to use a protected block of code, it will request for the key. The lock object will check if the key is available, if the key is available, it will give the key to Thread1 and execute the code. 

Now, Thread2 requests for the key, since the object does not have the key (the key is possessed by Thread1), Thread2 will wait until it becomes available to use. This way we ensure getInstance() does not enter into race conditions.

Now, about the object holding the key and giving it to the threads, that is accomplished by the JVM. JVM uses the SingletonExample class object to hold the key. If we use the synchronized keyword on the non-static method, the key is held by the instance of the class we are in. A better solution is to create a dedicated synchronized block inside the method and pass the key as a parameter.

Java
 
public class SingletonExample {

	private final Object lock = new Object();

	public getInstance(){
		synchroznied(lock) {
		// logic goes here
		}
	}
}


This technique works well on a single method, but if we have multiple synchronized blocks how do we go about it. Let me show what could possibly go wrong if you start using synchronized blocks across multiple methods.

Java
 
public class DeadlockTest {
	public static Object Lock_Thread1 = new Object();
	public static Object Lock_Thread2 = new Object();

	private static class Thread1 extends Thread {
		public void run() {
			synchronized (Lock_Thread1) {
				System.out.println("Thread 1 - Holding lock_thread1");

				try {
					Thread.sleep(30);
				} catch (InterruptedException e) {
				}
				System.out.println("Thread 1 - Waiting for lock_thread2");
				synchronized (Lock_Thread2) {
					System.out.println("Thread 1 - Holding lock 1 & 2");
				}
			}
		}
	}

	private static class Thread2 extends Thread {
		public void run() {
			synchronized (Lock_Thread2) {
				System.out.println("Thread 2 - Holding lock_thread2");

				try {
					Thread.sleep(30);
				} catch (InterruptedException e) {
				}
				System.out.println("Thread 2 - Waiting for lock_thread1");
				synchronized (Lock_Thread1) {
					System.out.println("Thread 2 - Holding lock 1 & 2");
				}
			}
		}
	}

	public static void main(String args[]) {
		Thread1 T1 = new Thread1();
		Thread2 T2 = new Thread2();
		T1.start();
		T2.start();
	}
}


Output:

 
Output:
Thread 1 - Holding lock_thread1 
Thread 2 - Holding lock_thread2 
Thread 1 - Waiting for lock_thread2 
Thread 2 - Waiting for lock_thread1

esc to exit...


In the above example, we can see, T1 is holding Lock_Thread1 and T2 is holding Lock_Thread2. T2 is waiting for the lock lock_thread1 and T1 is waiting for the lock Lock_Thread2. This situation is known as Deadlock. Both the threads are in a blocked state, and neither will end because each thread is waiting for the other thread to exit.

Deadlock for T1 and T2


This situation arises because of the order in which we obtain the locks. If we change the order of Lock_Thread1and Lock_Thread2, then run the same program the threads will no longer wait forever.

Java
 
public class DeadlockTest {
	public static Object Lock_Thread1 = new Object();
	public static Object Lock_Thread2 = new Object();

	private static class Thread1 extends Thread {
		public void run() {
			synchronized (Lock_Thread1) {
				System.out.println("Thread 1 -  Holding lock_thread1");

				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
				}
				System.out.println("Thread 1 -  Waiting for lock_thread2");

				synchronized (Lock_Thread2) {
					System.out.println("Thread 1 - Holding lock 1 & 2");
				}
			}
		}
	}

	private static class Thread2 extends Thread {
		public void run() {
          //This is changed to use lock for Thread1
			synchronized (Lock_Thread1) {
				System.out.println("Thread 2 -  Holding lock_thread1");

				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
				}
				System.out.println("Thread 2 -  Waiting for lock_thread1");

				synchronized (Lock_Thread2) {
					System.out.println("Thread 2 -  Holding lock 1 & 2");
				}
			}
		}
	}

	public static void main(String args[]) {
		Thread1 T1 = new Thread1();
		Thread2 T2 = new Thread2();
		T1.start();
		T2.start();
	}
}
 
Output:
Thread 1 -  Holding lock_thread1 
Thread 1 -  Waiting for lock_thread2 
Thread 1 - Holding lock 1 & 2 
Thread 2 -  Holding lock_thread1 
Thread 2 -  Waiting for lock_thread1 
Thread 2 -  Holding lock 1 & 2


As we can see, a deadlock situation can be avoided by ensuring the order in which you can obtain the lock on resources that your application needs. A good design would be to use minimal locks and to not share the locks if it is already assigned to another thread. 

Conclusion

I have tried to summarize various concepts we commonly use in concurrent programming. I hope you find this article useful. Be mindful of synchronization, race conditions, and deadlocks when you work on concurrent programming assignments.

Java (programming language)

Opinions expressed by DZone contributors are their own.

Related

  • How to Convert XLS to XLSX in Java
  • Recurrent Workflows With Cloud Native Dapr Jobs
  • Java Virtual Threads and Scaling
  • Java’s Next Act: Native Speed for a Cloud-Native World

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!