Multithreading
Components
Components involved in Multithreading in Java:
Threads
- One Java thread corresponds to one CPU thread.
Life Cycle
Thread Scheduler
Task executes for a predefined slice of time and then re-enters pool of ready tasks. Next task decided based on priority and other factors.
Uses Executions#Pre-emptive Scheduling and Executions#Time Slicing.
Managing Threads and Code involving Threads
Creating/Defining Threads
Two ways-
- extend
Thread
class - implement
Runnable
interface
Implement the run()
method -> this is the code which will get executed when the thread runs.
Executing Threads
Triggering Thread Execution
- Call
start()
method -
The part after start method call is executed immediately. If you want to execute some part of the code after the thread execution, use join()
method.
Thread thread = new ApnaThread();
thread.start();
//this part gets executed immediately after start() is called - while the thread is running
thread.join();
//this part gets executed after the thread execution is completed.
Managing Thread Execution
Methods for thread synchronisation
join()
: Waits for the thread to die.sleep(ms)
: Causes the thread to sleep for specified milliseconds.- CS/Languages/Java/Standard Library/Core/Core.
wait()
: stops the function until notified - CS/Languages/Java/Standard Library/Core/Core.
notify()
: notifies other threads
Prioritising Threads
In Thread
class:
NORM_PRIORITY
= 5 (Default priority)MAX_PRIORITY
= 10MIN_PRIORITY
= 1getPriority()
: Returns priority of threadsetPriority(int)
: changes priority of a thread
How to make a block of code thread safe?
-
Thread local fields
- Defining private fields in Thread classes
- Assigning ThreadLocal instances to a field
Like normal class fields but each thread accesses them via a setter-getter and gets independently initialized copy of the field so that each thread has its own state
-
- Concurrent collections achieve thread safety by dividing their data into segments and acquiring locks on different segments.
- Better performance
-
Atomic objects
-
Synchronized Methods
-
Synchronized statements
-
Other objects as lock
-
Volatile fields
-
Reentrant locks
-
Read/write locks
Volatile and Atomic
volatile
is used when there are two threads store and perform operations on same variable. In such case, both threads store the variable in different caches. This results in inaccuracy and inconsistency. Thus, the volatile
keyword lets lang.java.jre.jvm know that this is variable is going to be changed and it is then stored in the common cache.
AtomicInteger
, AtomicLong
and other such classes are used in a similar situation to above, where the threads are accessing and performing some operation on a variable. If we don't define the variable as Atomic, it might happen that the second thread accesses the variable before the first thread is done performing operation on it. This causes inconsistency in the data. The AtomicInteger
makes the integer thread safe by not letting any other thread access until one thread is done.
Thread Pool and ExecutorService
ExecutorService
is an interface which allows us to execute tasks on thread asynchronously
Instantiation using Executors
Factory class
Instance can be retrieved from Executors
factory class using different factory methods based on the number and types of threads needed.
Following are the different types of thread pools/executors:
SingleThreadedExecutor
ExecutorService es = Executors.newSingleThreadExecutor();
FixedThreadPool
ExecutorService es = Executors.newFixedThreadPool(10);
ScheduledThreadPool
In scheduled thread pool, we can schedule tasks of the threads.
ExecutorService es = Executors.newScheduledThreadPool(10);
CachedThreadPool
???
If you use ExecutorService
to initialise a thread pool.
- Threads initialized will pick up tasks from the queue of tasks given to the executor service
- Since multiple threads are accessing this queue simultaneously, executor service uses a Collections Framework#Concurrent Collections.
💡 Use Runtime
class to get the core count and initialise executor service with that.
int coreCount = Runtime.getRuntime().availableProcessors();
Executing Threads
execute(Runnable task)
submit(Runnable task) / submit(Callable<T> task)
invokeAny(Collection<? extends Callable<T>> tasks)
invokeAll(Collection<? extends Callable<T>> tasks);
Shutting down
We can use the following methods after we're done. If we don't, the threads will keep running and JVM won't shut down.
shutdown()
methodshutdownNow()
methodawaitTermination()
method
Deadlocks
Detection
Deadlocks are diffcult to detect as there are multiple locks and threads in Java which run even without us specifying.
Detection using thread dumps
Use either of the following commands to ask the JVM to print info of all the threads running in the application.
Take the process ID of your application and run the following -
kill -3 12704
(where 12704 is the process ID)jstack 11475 > ./out.txt
(will put that log in the specified file) JVM will detect the deadlock itself and mention it.
Detection using Java MXBean
MXBean
is an API which you can run constantly in the background which can detect and tell you when the application goes into a deadlock state.
private static void detectDeadlock(){
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
long[] threadIds = threadBean.findDeadlockedThreads();
boolean deadLock = threadIds != null && threadIds.length > 0;
System.out.println("Deadlocks found: " + deadLock);
}
Prevention
- Include timeout for your locks.
- Have a global order for locks
- Avoiding unnecessary and nested locks altogether
- Joining the threads