Java thread ensuring Java code is thread-safe

The topic of Thread Safety in Java holds great significance. Java offers support for a multi-threaded environment through Java Threads. It is crucial to note that when multiple threads are created from the same Object, they share its object variables. This can result in data inconsistency when the threads are utilized for both reading and updating the shared data.

Ensuring multiple threads can access without conflicts

Data inconsistency occurs due to the fact that updating a field value is not an indivisible process. It involves three steps: reading the current value, performing the necessary operations to obtain the updated value, and assigning the updated value to the field reference. To illustrate this, we can observe a simple program in which multiple threads are updating the shared data.

package com.scdev.threads;

public class ThreadSafety {

    public static void main(String[] args) throws InterruptedException {
    
        ProcessingThread pt = new ProcessingThread();
        Thread t1 = new Thread(pt, "t1");
        t1.start();
        Thread t2 = new Thread(pt, "t2");
        t2.start();
        //wait for threads to finish processing
        t1.join();
        t2.join();
        System.out.println("Processing count="+pt.getCount());
    }

}

class ProcessingThread implements Runnable{
    private int count;
    
    @Override
    public void run() {
        for(int i=1; i < 5; i++){
            processSomething(i);
        	count++;
        }
    }

    public int getCount() {
        return this.count;
    }

    private void processSomething(int i) {
        // processing some job
        try {
            Thread.sleep(i*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

In the program mentioned above, the value of the variable “count” is incremented by 1 four times in a loop. As there are two threads running simultaneously, the expected value of “count” after both threads finish executing should be 8. However, when running the program multiple times, it is observed that the value of “count” fluctuates between 6, 7, and 8. This inconsistency is due to the fact that even though the operation “count++” appears to be atomic, it is not, resulting in data corruption.

Ensuring thread safety in Java.

In Java, ensuring thread safety is the procedure of making our program secure for use in a multithreaded environment. There are various approaches available to achieve thread safety in our program.

  • Synchronization is the easiest and most widely used tool for thread safety in java.
  • Use of Atomic Wrapper classes from java.util.concurrent.atomic package. For example AtomicInteger
  • Use of locks from java.util.concurrent.locks package.
  • Using thread safe collection classes, check this post for usage of ConcurrentHashMap for thread safety.
  • Using volatile keyword with variables to make every thread read the data from memory, not read from thread cache.

Synchronized keyword in Java

Using synchronization allows us to attain thread-safety, as the JVM ensures that synchronized code is executed by only one thread at any given moment. The synchronized keyword in Java is employed to generate synchronized code, and it utilizes locks on Objects or Classes to guarantee that only a single thread executes the synchronized code.

  • Java synchronization works on locking and unlocking of the resource before any thread enters into synchronized code, it has to acquire the lock on the Object and when code execution ends, it unlocks the resource that can be locked by other threads. In the meantime, other threads are in wait state to lock the synchronized resource.
  • We can use synchronized keyword in two ways, one is to make a complete method synchronized and another way is to create synchronized block.
  • When a method is synchronized, it locks the Object, if method is static it locks the Class, so it’s always best practice to use synchronized block to lock the only sections of method that needs synchronization.
  • While creating a synchronized block, we need to provide the resource on which lock will be acquired, it can be XYZ.class or any Object field of the class.
  • synchronized(this) will lock the Object before entering into the synchronized block.
  • You should use the lowest level of locking, for example, if there are multiple synchronized block in a class and one of them is locking the Object, then other synchronized blocks will also be not available for execution by other threads. When we lock an Object, it acquires a lock on all the fields of the Object.
  • Java Synchronization provides data integrity on the cost of performance, so it should be used only when it’s absolutely necessary.
  • Java Synchronization works only in the same JVM, so if you need to lock some resource in multiple JVM environment, it will not work and you might have to look after some global locking mechanism.
  • Java Synchronization could result in deadlocks, check this post about deadlock in java and how to avoid them.
  • Java synchronized keyword cannot be used for constructors and variables.
  • It is preferable to create a dummy private Object to use for the synchronized block so that it’s reference can’t be changed by any other code. For example, if you have a setter method for Object on which you are synchronizing, it’s reference can be changed by some other code leads to the parallel execution of the synchronized block.
  • We should not use any object that is maintained in a constant pool, for example String should not be used for synchronization because if any other code is also locking on same String, it will try to acquire lock on the same reference object from String pool and even though both the codes are unrelated, they will lock each other.

To ensure thread safety, we must implement the below code modifications in the aforementioned program.

    //dummy object variable for synchronization
    private Object mutex=new Object();
    ...
    //using synchronized block to read, increment and update count value synchronously
    synchronized (mutex) {
            count++;
    }

Let’s examine some instances of synchronization and the lessons we can derive from them.

public class MyObject {
 
  // Locks on the object's monitor
  public synchronized void doSomething() { 
    // ...
  }
}
 
// Hackers code
MyObject myObject = new MyObject();
synchronized (myObject) {
  while (true) {
    // Indefinitely delay myObject
    Thread.sleep(Integer.MAX_VALUE); 
  }
}

Take note that the code used by hackers attempts to grab the lock on the myObject instance and does not release it. As a result, the doSomething() method becomes stuck waiting for the lock, leading to a deadlock and ultimately causing a Denial of Service (DoS) to the system.

public class MyObject {
  public Object lock = new Object();
 
  public void doSomething() {
    synchronized (lock) {
      // ...
    }
  }
}

//untrusted code

MyObject myObject = new MyObject();
//change the lock Object reference
myObject.lock = new Object();

Please observe that the lock Object can be accessed publicly, enabling the execution of synchronized blocks concurrently in multiple threads by modifying its reference. The same situation applies if you possess a private Object but possess a setter method to alter its reference.

public class MyObject {
  //locks on the class object's monitor
  public static synchronized void doSomething() { 
    // ...
  }
}
 
// hackers code
synchronized (MyObject.class) {
  while (true) {
    Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
  }
}

Be aware that the hacker’s code has gained control over the class monitor and is not letting go of it. This will lead to a deadlock situation and result in a Denial of Service (DoS) within the system. Here’s another instance where several threads are simultaneously operating on the same set of String arrays, and after processing, they add their thread names to the respective array values.

package com.scdev.threads;

import java.util.Arrays;

public class SyncronizedMethod {

    public static void main(String[] args) throws InterruptedException {
        String[] arr = {"1","2","3","4","5","6"};
        HashMapProcessor hmp = new HashMapProcessor(arr);
        Thread t1=new Thread(hmp, "t1");
        Thread t2=new Thread(hmp, "t2");
        Thread t3=new Thread(hmp, "t3");
        long start = System.currentTimeMillis();
        //start all the threads
        t1.start();t2.start();t3.start();
        //wait for threads to finish
        t1.join();t2.join();t3.join();
        System.out.println("Time taken= "+(System.currentTimeMillis()-start));
        //check the shared variable value now
        System.out.println(Arrays.asList(hmp.getMap()));
    }

}

class HashMapProcessor implements Runnable{
    
    private String[] strArr = null;
    
    public HashMapProcessor(String[] m){
        this.strArr=m;
    }
    
    public String[] getMap() {
        return strArr;
    }

    @Override
    public void run() {
        processArr(Thread.currentThread().getName());
    }

    private void processArr(String name) {
        for(int i=0; i < strArr.length; i++){
            //process data and append thread name
            processSomething(i);
            addThreadName(i, name);
        }
    }
    
    private void addThreadName(int i, String name) {
        strArr[i] = strArr[i] +":"+name;
    }

    private void processSomething(int index) {
        // processing some job
        try {
            Thread.sleep(index*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

This is the result I get when I execute the program mentioned above.

Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]

The corruption of the String array values occurs due to shared data and lack of synchronization. To ensure thread safety in our program, we can modify the addThreadName() method as follows.

    private Object lock = new Object();
    private void addThreadName(int i, String name) {
        synchronized(lock){
        strArr[i] = strArr[i] +":"+name;
        }
    }

Following this modification, our program is functioning properly and the program is now producing the accurate result.

Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]

I hope you have gained knowledge about thread safety in Java, including thread-safe programming and the utilization of the synchronized keyword. That concludes our discussion.

 

More Java tutors

Addition Assignment Operator mean in Java(Opens in a new browser tab)

Spring MVC HandlerInterceptorAdapter and HandlerInterceptor.(Opens in a new browser tab)

 

Leave a Reply 0

Your email address will not be published. Required fields are marked *


广告
Closing in 10 seconds
bannerAds