Leverage Turing Intelligence capabilities to integrate AI into your operations, enhance automation, and optimize cloud migration for scalable impact.
Advance foundation model research and improve LLM reasoning, coding, and multimodal capabilities with Turing AGI Advancement.
Access a global network of elite AI professionals through Turing Jobs—vetted experts ready to accelerate your AI initiatives.
Thread safety is a very important concept in software engineering. In programming, a thread is a unit of execution that runs independently of other threads. It allows for the parallel execution of multiple threads that share the same data structure or objects, and it allows parallel execution of multiple tasks within a single program. When multiple threads share the same data structures or objects, it can lead to inconsistencies or unpredictable behavior if not handled correctly. This is where the concept of thread safety comes in.
In this article, we will be exploring the concepts of thread safety in Java and how to implement it. We will discuss what is Java thread safety, thread-safe collections in java, and how to make a method thread-safe.
Thread safety happens in a function. A function is said to be thread-safe if and only if it produces correct results when called repeatedly from multiple concurrent threads.
In computers, we have multiple cores(CPUs) and if we want to execute multiple functions at the same time, we can do that with the help of threads so that we can achieve one task with one thread and with multiple threads.
Even when this sounds amazingly safe thread has some issues. To understand this we need to understand mutations.
When you have an object, that object will have a variable e.g (a=7) if you can change the value (7) this object is mutable because you can change the value. It is changeable.
When an object is immutable, it means that object has a variable and a value e.g (a=7) that cannot be changed. It is unchangeable.
You may think that mutability is better than immutability because that is what we do as software engineers - we change data most of the time and perform operations. A mutation is important. Multi-threading is also very important because we can have multiple threads but when you combine them (mutation and multi-threading) that is when the problem starts because if you have multiple threads and you have data that is shared by all these threads that will create issues.
If you are simply fetching values you don't have a problem but the moment you change values that is where the problem starts.
Let's see an example here:
class IncrementCounter { //Initial variable count int count;//Increment method which the value of count meaning it will up the values public void increment(){ count++; }
}
// the main methods that class example1{ public static void main(String[] args){ // object of IncrementCounter (c) created from the counter, and we can call or invoke it anytime IncrementCounter c = new IncrementCounter();
System.out.println(c.count); } }</pre><p>In the example, above we were able to create a variable count of type int. We also created a method that increases the value of count each time the method is called. In the main method, we created an object of increment count. The current value of count is 0 if we print c.count. What if we want to change the value of the count? We can do this by calling or invoking increment by updating it. Let's increase the value by 100 with a for-loop:</p><pre>for (int i=1; i <= 100; i++){ c.increment();
} System.out.println(c.count); }
With the implementation above, our job is done. So, the answer is 100. Let's say we want to optimize our code by using multi-threads. So that we will have two threads calling one method. Let's see how that works.
We are going to use two threads, the first one will call increment 500 times and the second one will call increment by 500 times and in total, we will have 1000 times. Let's do it.
CounterIncrement c = new CounterIncrement(); Thread one = new Thread(new Runnable(){ public void run() { for (int i=1; i <= 500; i++){ c.increment(); } } });Thread two = new Thread(new Runnable(){ public void run() { for (int i=1; i <= 500; i++){ c.increment(); } } }); one.start(); two.start();
one.join(); two.join();
System.out.println(c.count);
It will work because we have two threads, and it will improve the performance. It will work faster than normal code. But the output we are expecting is 1000, and when we run the code the value keeps changing. This means there is no consistency in our code. What we want is thread safety.
Thread safety is when you have multiple threads working on the same method and the method changes the value of your data, that means it is not thread-safe. When your method is not thread-safe you will get inconsistent values. That is not good for us because we have data that we want to be consistent with.
Make sure that your method is not changing values. You can simply make your class immutable,
but then we want to be able to change values.
When you have multiple threads working with the same method, the methods are going in to perform
operations. Our operation which is:
public void increment(){ count++; }
The above looks like an atomic operation, but it is not, a lot of things are happening here. It is fetching the current value which is 100 for a count then it is adding the value by 1 that is 100 + 1 and assign this 101 value to count, so it is not a one-step process. It is a three-step process and it's not an atomic process. This that is where the problem starts because if two threads are reaching the same point, both will ask for thevalue of the count. The count will respond with my value is 100. Thread 1 will assign 101, thread 2 will assign 101 and that is where we are missing our data.
We don't want the two threads to work with the same count at the same time. So we want thread safety here. This means this method will be executed only by one thread.
There are multiple ways of doing this:
1. By using the synchronized keyword in the method: The synchronized keyword can be used to make any shared resource thread-safe, not just collections. By declaring a method as synchronized, the Java virtual machine acquires a lock on the object that the method belongs to, which ensures that only one thread can execute the method at a time. This helps to avoid race conditions and other synchronization-related issues, making the shared resource thread-safe.
class CounterIncrement { // Initial variable count int count;// Increment method which the value of count meaning it will up the values public synchronized void increment(){ count++; }
}
// the main method public class Example1 { public static void main(String[] args) throws InterruptedException { // object of CounterIncrement (c) created from counter, and we can call or invoke it anytime CounterIncrement c = new CounterIncrement(); Thread one = new Thread(new Runnable(){ public void run() { for (int i=1; i <= 1000; i++){ c.increment(); } } });
Thread two = new Thread(new Runnable(){ public void run() { for (int i=1; i <= 1000; i++){ c.increment(); } } }); one.start(); two.start(); one.join(); two.join(); System.out.println(c.count); }
}
the code above shows us that only one thread can be called at a time. this means if
Thread one = new Thread(new Runnable(){ public void run() { for (int i=1; i <= 1000; i++){ c.increment(); } } }); is called Thread two = new Thread(new Runnable(){ public void run() { for (int i=1; i <= 1000; i++){ c.increment(); } } }); has to wait.
2. By using Atomic integer: We may say we don't want to use synchronize we can use atomic integer instead of int. so we create an object of AtomicInteger count like this:
import java.util.concurrent.atomic.AtomicInteger;class CounterIncrement { // Initial variable count AtomicInteger count = new AtomicInteger(0);
// Increment method which the value of count meaning it will up the values public void increment(){ count.incrementAndGet(); }
}
// the main methods that public class Example1 { public static void main(String[] args) throws InterruptedException { // object of IncrementCounter (c) created from counter, and we can call or invoke it anytime CounterIncrement c = new CounterIncrement(); Thread one = new Thread(new Runnable(){ public void run() { for (int i=1; i <= 1000; i++){ c.increment(); } } });
Thread two = new Thread(new Runnable(){ public void run() { for (int i=1; i <= 1000; i++){ c.increment(); } } }); one.start(); two.start(); one.join(); two.join(); System.out.println(c.count); }
}
AtomicInteger count = new AtomicInteger(); we will change count++ to count.incrementAndGet() and when you print this you will get the same result.
Thread-safe collections in Java can be applicable in situations where multiple threads are accessing and modifying a shared collection simultaneously. The use of thread-safe collections can help prevent race conditions, where two or more threads try to access or modify the same data at the same time, leading to unpredictable or incorrect results.
java.util.concurrent.ConcurrentHashMap, java.util.concurrent.CopyOnWriteArrayList, and java.util.concurrent.BlockingQueue.
These collections are designed to be thread-safe, meaning that they can be safely used by multiple threads without the need for external synchronization.
For example, a concurrent hash map can be used to store data that is being accessed and modified by multiple threads. The use of a concurrent hash map would ensure that the data stored in it is accessed and modified in a way that is safe and consistent, even when multiple threads are trying to access and modify it simultaneously.
It is important to note that while thread-safe collections can help prevent race conditions, they can also come with performance overhead and may not always be the best choice, depending on the specific use case and requirements. It's important to choose the appropriate collection based on the requirements of the application and the expected behavior of the threads.
Here is an example of using a thread-safe collection in Java using concurrent HashMap:
import java.util.concurrent.ConcurrentHashMap;public class Example { public static void main(String[] args) { // Create a concurrent hash map to store data that will be accessed by multiple threads ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Populate the map with some initial data map.put("Key1", 1); map.put("Key2", 2); map.put("Key3", 3); // Create and start a thread to modify the map Thread one = new Thread(() -> { map.put("Key1", 10); map.put("Key2", 20); }); one.start(); // Create and start another thread to modify the map Thread two = new Thread(() -> { map.put("Key3", 30); map.remove("Key1"); }); two.start(); // Wait for the threads to complete try { one.join(); two.join(); } catch (InterruptedException e) { // Handle the exception System.out.println("Exception occured: " + e.getMessage()); } // Print the final state of the map System.out.println("Final map: " + map);
} }
In this example, two threads are created, "one" and "two", which both modify a concurrent hash map in different ways. The use of a concurrent hash map ensures that the data stored in it is accessed and modified in a way that is safe and consistent, even when multiple threads are trying to access and modify it simultaneously. The threads are started separately and then joined to ensure that both threads complete their execution before the final state of the map is printed.
We’ve learned about thread safety in Java, thread safety collections, and their implementations. We also learned how to make a method thread-safe. We learned about mutation and immutability. Try the various methods discussed above and see the difference they make to the speed at which you can access data in Java, the methods, and their implementations.
Jacob has more than two years of experience as a technical writer and software engineer. He is a skilled technical writer who can clearly explain complex concepts to a broad audience.