Top 10 Multithreading and Concurrency Best Practices for Experienced Java Developers
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.
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.
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.
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.
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.
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.
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.
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.
Unlike Exchanger synchronization utility which can be used to implement the single producer-consumer design, blocking queue can also handle multiple producers and consumers.
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.
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.
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.
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.
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.
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.
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.
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.
Thanks for reading this article so far. If you have any questions or feedback then please drop a note.