Recommendations for Writing Effective Multithreaded Applications in Java

Osman Yasal
7 min readJul 20, 2021
https://www.tynker.com/community/projects/play/angry-caveman/582c84fd55c8b24e308b4576

Writing multithreaded applications today is inevitable. In order to do it right i would like to suggest some good practices as below.

Use marker annotations

Multithreaded applications mostly have sequential jobs as well, one of the good practices is to differ the multithreaded jobs from sequential ones and we could do this explicitly via marker annotations. Here’s some

  • Class annotations

We use three class-level annotations to describe a class’s intended thread-safety promises: @Immutable, @ThreadSafe, and @NotThreadSafe.

-Immutable of course means that the class is immutable(can not be changed after creation) and implies @ThreadSafe

-If a class is not annotated as thread-safe, it should be presumed not to be thread-safe, but if you want to make it extra clear, use @NotThreadSafe.

These annotations are beneficial to both users and maintainers. Users can see immediately whether a class is thread-safe, and maintainers can see immediately whether thread-safety guarantees must be preserved.

  • Method annotations

@ThreadSafe should be used on methods also, in order to emphasize the differentiation between thread-safe and non-thread-safe methods.

-In addition to that we could use @GuardedBy annotation to emphasize the guarding lock explicitly. Here’s some formats.

-@GuardedBy(“this”), meaning the intrinsic lock is the containing object

-@GuardedBy(“fieldName”), meaning the lock associated with the object referenced by the named field.

-@GuardedBy(“ClassName.fieldName”), like @GuardedBy(“fieldName”), but referencing a lock object held in a static field of another class;

-@GuardedBy(“methodName()”), meaning the lock object that is returned by calling the named method;

-@GuardedBy(“ClassName.class”), meaning the class literal object for the named class.

Use thread pools over low-level threads

https://365psd.com/vector/3d-gear-vector-free-34150

There’re a few ways to create a thread we can either implement Runnable interface and override run() method or we can extends our class with thread class. Both of these methods create low-level threads.

You should know that thread creation is not a cheap operation, it takes time. Creation a thread when neded and refunding it decreases the applications performance. To exploit using threads, we should used thread pools.

Thread pools simply creates some threads once, when application starts and re-uses them when needed. Thus we save extra time.

We could use executor service for thread pool creation.
for more information see Introduction to thread pools in java

Use intrinsic locks over synchronized keyword

Before starting this i want you to know that intrinsic locks can do whatever synchronized keyword does. As you may guess intrinsic locks offer more functionality than synchronized keyword.

Let’s compare the performance

https://flylib.com/books/en/2.558.1/performance_considerations.html

as you can see above, intrinsic locks has more throughput over synchronized keyword in java5. but java6 and later versions of java, they both almost have the same throughput.

  • Structure of synchronized keyword

-synchronized keyword can be exists either method level or as block. The key point is the lock is released at the end of the block or method that is defined. for more information see concurrency in java doc.

  • Advantages of using intrinsic locks

-Fortunately intrinsic locks give the control of acquiring and releasing the lock to the developer. Moreover, we don’t need to release the lock inside the method that we first acquired. We may acquire the lock from A() method and release it from B().

-if 2 or more threads want to execute a synchronized block, while a thread acquire and execute the block the others must wait until the first thread release the lock. Fortunately intrinsic locks gives us a option like tryLock(), tryLock(long time, TimeUnit unit) if we can’t acquire the lock at the moment or given time, the thread is not blocked, it may pursue to the other jobs or retries acquiring the lock.

concluding this section i would like you to know that both synchronized and intrinsic locks have the same memory operations. You may choose both synchronized and intrinsic locks for simple operations but you have no choice to use intrinsic locks if you want more flexibility.

Be careful while using intrinsic locks

This is the safe structure of using intrinsic locks, it’s curutial to release the acquired lock after execution of the block. To ensure this we can wrap the execution block with try-finally and in the finally block we release the lock even if there’s an throwed exeception from the block.

Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} catch(Exception ex){ // do nothing. }
finally {
l.unlock();
}

for more information see Lock in the javadoc.

Use Immutable objects as possible

The data-race problem comes to the surface If our program uses 2 and more threads. This problem simply says that we can not sure what is the last value of the variable when multiple threads operate on it at the same time unless we define an order policy(using locking or synchronized).
One of the solutions to preventing data-race is to define the data-class immutable, so it can not be changed after creation thus you won’t need any synchronization policy and you save time.

https://medium.com/@JennieZChu/mutable-and-immutable-objects-in-python-aaad0018ebc1

Keep the synchronized blocks short

Here’s another life saving advice. Keeping synchronized bloks short icreases the ratio of the parallel code and reduces the amount of the serial code, thus our throughput increases according to the Amdahl’s law.

https://en.wikipedia.org/wiki/Amdahl%27s_law

if we increase the parallel portion of the code, our speedup also increases even we use the same number of processors.

Avoid over-usage & nasted of synchronization

The one thing that you should put in your head is every synchronized peace of code decreases our application performance so, it’s essential to keep synchronized blocks short and avoid using nasted synchronization.
Nasted synchronizations are tricky, it may not be seen clearly at first galence but you should make sure that there’s no synchronized block in the library that you use in the synchronized block. for example if we use concurrentHashMap to store your key-value pairs it’s nonsense access this map over external lock.

https://medium.com/@wiemzin/getting-started-with-recursion-schemes-using-matryoshka-f5b5ec01bb

Use read-write locks as if possible over simple intrinsic locks

Let’s say you’ve a hasMap that you keep your key-value pairs in it and it’s used by multiple threads. To avoid data-race problem one good approach is to wrap accesses to this map with a synchronized block(either keyword or some locks) thus to read and write operations happens via acquiring the lock and the mutual exclusion is on the surface. The questation is “why don’t we allow multiple threads to read the hasMap? they does’t change it’s status do they?” This is the cradle point of the read-write locks.

simply speaking: read-write locks allow accessing multiple threads to an object for reading purpose but if one of them wants to alter the object then it blocks all readers until the writer thread finishes it’s job.
while multiple readers are allowed at the same time, only one writer thread can alter the object at a given time.
Here’s the performance comparison of ReentrantReadWriteLock and ReentrantLock

reading-writing a map using two different locking mechanishm.

Be aware of the Heisenbug

https://tr.pinterest.com/pin/10977592811619308/

Our multithreaded program might work well most of the time and appears it’s bug-free. The biggest question is “are you 100% sure?”

Heisenbugs are actually some glitches in our program. These glitches appears for most of the reasons. While most of the time our program runs all right, when we try Nth time, it does’t work well so hiesenbugs are the most dengerous bugs. If our program have one of these, you may think our program is playing a russian-rulete at the background and sooner or later it’s going to carsh.
So It’s essential to have a recovery process defined in our program, loging the actions and tracing them is one of the methods of eliminating the heisenbugs.

And one last thing,
Writing multithreaded applications is highly tricky! you must be very careful or you may never now what’s about the happen.

Thanks for the reading ❤

--

--

Osman Yasal

Building technology to help people to live their the best life experience.