Saturday, July 8, 2023

Top 10 Multithreading and Concurrency Best Practices for Experienced Java Developers

Writing concurrent code is hard and testing correctness with concurrency is even harder. Though Java programming language provides lots of synchronization and concurrency support from language to API level, it eventually comes to an individual's diligence and expertise to write bug-free Java concurrency code. These Java concurrency and multi-threading best practices are a collection of some well-known tips, which help you to write better concurrency code in Java. Some of you may be familiar with these tips but it's often worth revising them time and time again. 

These Java multi-threading and concurrency tips are from my own learning and usage and also inspired by reading books like Effective Java and Java Concurrency in Practice in particular.

I suggest reading Java Concurrency Practice two times to every Java developer, yes, you heard it correctly, TWO times. Concurrency is confusing and difficult to comprehend, much like Recursion to few programmers; and in one reading, you might not get all of it.





10 Java Multithreading and Concurrency Best Practices

The sole purpose of using concurrency is to produce a scalable and faster program. But always remember, speed comes after correctness. Your Java program must follow its invariant in all conditions, which it would if executed in a sequential manner.

If you are new in concurrent Java programming, then take some time to get familiar yourself with different problems that arise due to concurrent execution of the program like deadlock, race conditions, livelock, starvation, etc.

1. Use Local Variables

Always try to use local variables instead of creating a class or instance variables. Sometimes, developers use instance variables to save memory and reusing them, because they think creating local variables every time method invoked may take a lot of memory. One example of this is declaring Collection as a member and reusing them by using the clear() method.

This introduces, a shared state in an otherwise stateless class, which is designed for concurrent execution. Like in the below code, where execute() method is called by multiple threads, and to implement new functionality, you need a temp collection. 

In the original code, a static List was used and the developer's intention was to clear this at the end of execute() method for reuse.

He thought that code is safe because of CopyOnWriteArrayList is thread-safe. What he failed to realize that, since this method gets called by multiple threads, one thread may see data written by other threads in a shared temp List. Synchronization provided by the list is not enough to protect the method's invariant here.

public class ConcurrentTask{
    private static List temp = Collections.synchronizedList(new ArrayList());
 
    @Override
    public void execute(Message message){
        //I need a temporary ArrayList here, use local
        //List temp = new ArrayList();
     
        //add something from Message into List
        temp.add("message.getId()");
        temp.add("message.getCode()");
     
        //combine id and code store result back to message
        temp.clear(); // Let's resuse it
    }
}

Problem :
One Message's data will go to another Message if two calls of multiple threads interleaved. e.g. T1 adds Id from Message 1 then T2 adds Id from Message 2, which happens before List gets cleared, so one of those messages will have corrupted data.

Solution :
1) Add a synchronized block when one thread adds something to the temp list and clear() it. So that, no thread can access List until one is done with it. This will make that part single-threaded and reduce overall application performance by that percentage.

2) Use a local List instead of a global one. Yes, it will take few more bytes, but you are free from synchronization and the code is much more readable. Also, you should be worrying too much about temporary objects, GC and JIT will take care of that.

This is just one of those cases, but I personally prefer a local variable rather than a member variable in multi-threading, until it's part of the design. 


best course to learn Java concurrency and threads




2. Prefer Immutable Classes

Another and most widely known Java multi-threading best practice is to prefer an Immutable class. Immutable classes like String, Integer, and other wrapper classes greatly simplify writing concurrent code in Java because you don't need to worry about their state. Immutable classes reduce the amount of synchronization in code.

Immutable classes, once created, can not be modified. One of the best examples of immutable classes in Java is the java.lang.String, any modification on String e.g. converting it into uppercase, trim, or substring would produce another String object, keeping the original String object intact.



3. Minimize locking scope

Any code which is inside the lock will not be executed concurrently and if you have 5% code inside the lock then as per Amdahl's law, your application performance can not be improved more than 20 times. The main reason for this is that 5% of the code will always be executed sequentially.

You can reduce this amount by minimizing the scope of locking, try to only lock critical sections. One of the best examples of minimizing the scope of locking is double-checked locking idiom, which works by using volatile variables after Java 5 improvements on the Java Memory Model.

Good knowledge of the Java Memory Model is also very important, particularly if you are preparing for Java interviews.

Multithreading and Concurrency Best Practices in Java


4. Prefer Thread Pool Executors instead of Threads

Creating Thread is expensive. If you want a scalable Java application, you need to use a thread pool. Apart from cost, managing thread requires lots of boiler-plate code, and mixing those with business logic reduces readability. 

Managing threads is a framework level task and should be left to Java or any proprietary framework you are using. JDK has a well-built, rich, and fully tested Thread pool also known as the Executor framework, which should be utilized whenever needed.



5. Prefer Synchronization utility over wait notify

This Java multi-threading practice inspires by Java 1.5, which added a lot of synchronization utilities like CyclicBarrier, CountDownLatch, and Semaphore. You should always look to JDK concurrency and synchronization utility, before thinking of wait and notify.

It's much easier to implement the producer-consumer design with BlockingQueue than by implementing them using wait and notify. See those two links to compare yourself.

Also, it's much easier to wait for 5 threads using CountDownLatch to complete their task rather than implementing the same utility using wait and notify. Get yourself familiar with java.util.concurrent package for writing better Java concurrency code.



6. Prefer BlockingQueue for Producer-Consumer Design Pattern

This multi-threading and concurrency best practice is related to earlier advice, but I have made it explicitly because of its importance in real-world concurrent applications. 

Many of concurrency problem is based on producer-consumer design pattern and BlockingQueue is the best way to implement them in Java.

Unlike Exchanger synchronization utility which can be used to implement the single producer-consumer design, blocking queue can also handle multiple producers and consumers. 


best course to learn java concurrency patterns



7. Prefer Concurrent Collections over Synchronized Collection

As mentioned in my post about Top 5 Concurrent Collections in Java, they tend to provide more scalability and performance than their synchronized counterpart. ConcurrentHashMap, which is I guess one of the most popular of all concurrent collections provides much better performance than synchronized HashMap or Hashtable if a number of reader threads outnumber writers.

Another advantage of Concurrent collections is that they are built using a new locking mechanism provided by the Lock interface and better poised to take advantage of the native concurrency construct provided by the underlying hardware and JVM. 

In the same line, consider using CopyOnWriteArrayList in place of synchronized List, if List is mostly for reading purpose with rare updates.



8. Use Semaphore to create bounds

In order to build a reliable and stable system, you must have bounds on resources like database, file system, sockets, etc. In no situation, your code creates or use an infinite number of resources.

Semaphore is a good choice to have a limit on expensive resources like database connection, by the way, leave that to your Connection pool. Semaphore is very helpful in creating bounds and blocking threads if the resource is not available. You can follow this tutorial to learn how to use Semaphore in Java.



9. Prefer synchronized block over synchronized method

This Java multi-threading best practice is an extension of earlier best practices about minimizing the scope of locking.  Using synchronized block is one way to reduce the scope of lock and it also allows you to lock on an object other than "this", which represents the current object. 

Today, your first choice should be the atomic variable, followed by the volatile variable if your synchronization requirement is satisfied by using them.

Only if you need mutual exclusion you can consider using ReentrantLock followed by a plain old synchronized keyword. If you are new to concurrency and not writing code for high-frequency trading or any other mission critical application, stick with synchronized keyword because it's much safer and easy to use. If you are new to the Lock interface, see my tutorial on how to use Lock in a multi-threaded Java program for a step by step guide.



10. Avoid Using static variables

As shown in the first multi-threading best practice, static variables can create lots of issues during concurrent execution. If you happen to use static variables, consider it making static final constants, and if static variables are used to store collections like List or Map then consider using only read-only collections. 

If you are thinking of reusing Collection to save memory, please see the example in the first best practice to learn how static variables can cause problems in concurrent programs.



11. Prefer Lock over synchronized keyword

This is a bonus multi-threading best practice, but it's double edge sword at the same time. Lock interface is powerful but every power comes with responsibility. Different locks for reading and write operation allow us to build scalable data structures like ConcurrentHashMap, but it also requires a lot of care during coding.

Unlike synchronized keyword, the thread doesn't release lock automatically. You need to call unlock() method to release a lock and the best practice is to call it on the finally block to ensure release in all conditions. here is an idiom to use explicitly lock in Java :

lock.lock();
try {
    //do something ...
} finally {
  lock.unlock();
}


By the way, this article is in line with 10 JDBC best practices and 10 code comments best practices, if you haven't read them already, you may find them worth reading. As some of you may agree that there is no end to best practices, It evolves and gets popular with time. If you guys have any advice, experience, which can help anyone writing concurrent programs in Java, please share.


That's all on this list of Java multithreading and concurrency best practices. Once again, reading Concurrency Practice in Java and Effective Java is worth reading again and again. Also developing a sense for concurrent execution by doing code review helps a lot with visualizing problems during development. On a closing note, let us know what best practices you follow while writing concurrent applications in Java?



Other Java Concurrency Articles you may like
  • The Complete Java Developer RoadMap (roadmap)
  • 5 Courses to Learn Java Multithreading in-depth (courses)
  • Difference between CyclicBarrier and CountDownLatch in Java? (answer)
  • How does Exchanger works in Java Multithreading (tutorial)
  • 21 Tech Skills Java Developer can learn (skills)
  • Top 50 Multithreading and Concurrency Questions in Java (questions)
  • 10 Best courses to learn Java in depth (courses)
  • Understanding the flow of data and code in Java program (answer)
  • How to avoid deadlock in Java? (answer)
  • How to do inter-thread communication in Java using wait-notify? (answer)
  • 10 Tips to become a better Java Developer (tips)
  • 5 Essential Skills to Crack Java Interviews (skills)
  • Top 5 Books to Master Concurrency in Java (books)
  • 10 Advanced Java Courses for Experienced Devs (courses)

Thanks for reading this article so far. If you have any questions or feedback then please drop a note.

2 comments :

SARAL SAXENA said...

Hi Javin ,

Just want to add with respect to If you're simply locking an object, I'd prefer to use synchronized

Example:

Lock.acquire();
doSomethingNifty(); // Throws a NPE!
Lock.release(); // Oh noes, we never release the lock!
You have to explicitly do try{} finally{} everywhere.

Whereas with synchronized, it's super clear and impossible to get wrong:

synchronized(myObject) {
doSomethingNifty();
}
That said, Locks may be more useful for more complicated things where you can't acquire and release in such a clean manner. I would honestly prefer to avoid using bare Locks in the first place, and just go with a more sophisticated concurrency control such as a CyclicBarrier or a LinkedBlockingQueue, if they meet your needs.

I've never had a reason to use wait() or notify() but there may be some good ones.

SARAL SAXENA said...

Also please check the imple Lock implementation:

public class Lock{

private boolean isLocked = false;

public synchronized void lock()
throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}

public synchronized void unlock(){
isLocked = false;
notify();
}
}

Notice the while(isLocked) loop, which is also called a "spin lock". Spin locks and the methods wait() and notify() are covered in more detail in the text Thread Signaling. While isLocked is true, the thread calling lock() is parked waiting in the wait() call. In case the thread should return unexpectedly from the wait() call without having received a notify() call (AKA a Spurious Wakeup) the thread re-checks the isLocked condition to see if it is safe to proceed or not, rather than just assume that being awakened means it is safe to proceed. If isLocked is false, the thread exits the while(isLocked) loop, and sets isLocked back to true, to lock the Lock instance for other threads calling lock()

Post a Comment