MultiThreading in Java
Common Terminology
- Thread : Smallest unit of execution that can be scheduled by Operating System.
- Process : Group of associated threads that execute in the same, shared environment.
- Task : Single unit of work performed by thread.
A Thread can complete multiple independent tasks
but only one task at a time
Type of Threads
- A system thread is created by the JVM and run in the background of the application e.g garbage collection.
- A user defined thread is one created by the application developer to accomplish a specific task.
More on Threads
- The property of executing multiple threads at the same time is referred to as concurrency
- Operating System uses Thread scheduler to determine which thread should be currently executing.
- A Thread scheduler employee strategy e.g Round Robin
- Context Switch is the Process of storing a thread's current state and later restoring the state of thread to continue execution.
Thread Life Cycle
New
-
When we create a new Thread object using new operator, thread state is New Thread. At this point, thread is not alive and it’s a state internal to Java programming.
-
Runnable
- When we call start() function on Thread object, it’s state is changed to Runnable.
-
Running
- When thread is executing, it’s state is changed to Running.
-
Blocked/Waiting
- A thread can be waiting for other thread to finish using thread join or it can be waiting for some resources to available.
-
Dead
- Once the thread finished executing, it’s state is changed to Dead and it’s considered to be not alive.
Concurrency
When an application is capable of executing two tasks virtually at same time, we call it concurrent application. Though here tasks run looks like simultaneously, but essentially they may not. They take advantage of CPU time-slicing feature of operating system where each task run part of its task and then go to waiting state. When first task is in waiting state, CPU is assigned to second task to complete it’s part of task.
Parallelism
It literally physically run parts of tasks OR multiple tasks, at the same time using multi-core infrastructure of CPU, by assigning one core to each task or sub-task.
Thread Priority
A Thread Priority is numeric value associated with a thread that is taken into consideration by the thread scheduler when determining which threads should currently be executed
Constant Variable | Value |
---|---|
Thread.MIN_PRIORITY | 1 |
Thread.NORM_PRIORITY | 5 |
Thread.MAX_PRIORITY | 10 |
Default priority of thread is Thread.NORM
How to create threads ?
- Implement Runnable Interface
package com.multithreading.demo;
public class RunnableDemo implements Runnable{
@Override
public void run() {
System.out.println("Running Runnable Thread");
}
public static void main(String[] args) {
new Thread(new RunnableDemo())
.start();
}
}
package com.multithreading.demo;
public class ThreadDemo extends Thread{
@Override
public void run(){
System.out.println("Running Thread Demo");
}
public static void main(String[] args) {
new ThreadDemo().start();
}
}
- Extending Thread Class
Creating a Thread using anonymous inner class
package com.multithreading.demo;
public class SimpleThread {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Running...");
}
}).start();
}
}
Creating Thread using lambda expression
package com.multithreading.demo;
public class SimpleThread {
public static void main(String[] args) {
new Thread(()->{
System.out.println("Running...");
}).start();
}
}
Sleep and Join
- Sleep : To wait for a specific amount of time.
- Join : Wait until the Thread finishes.
package com.multithreading.demo;
public class SimpleThread {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000L);
System.out.println("Running 1st Thread");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000L);
System.out.println("Running 2nd Thread");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Ended....");
}
}
ExecuterService
- ExecutorService creates and manages thread for you.
- You first obtain an instance of an ExecutorService interface and then you Send the Service tasks to be processed
- Executors factory class can be used to create instance of the ExecutorService object.
package com.multithreading.demo;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleExecutorServiceDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("Thread 1");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("Thread 2");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
// e.printStackTrace();
}
System.out.println("Thread 3");
}
});
} finally {
executorService.shutdownNow();
}
System.out.println(executorService.isShutdown());
System.out.println(executorService.isTerminated());
System.out.println("End");
}
}
SingleThreadExecutor
- With a single thread executor results are guaranteed to be executed in the order in which they are added to the executor service.
- It is important to call shutdown otherwise your application will not terminate.
Safe way of using executor service
package com.multithreading.demo;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleExecutorServiceDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("Thread 1");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("Thread 2");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("Thread 3"+10/0);
}
});
}finally {
executorService.shutdown();
}
System.out.println("End");
}
}
Useful method with ExecutorService
- shutdown() : Rejects New Task submitted to the thread executor while continuing to execute any previously submitted task.
- shutdownNow() : shutdownNow will do the same as shutdown AND will try to cancel the already submitted tasks by interrupting the relevant threads. Note that if your tasks ignore the interruption, shutdownNow will behave exactly the same way as shutdown
- isShutdown() : Returns true when executer service is shutdown.
- isTerminated() : Returns true when all active tasks are completed.
package com.multithreading.demo;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleExecutorServiceDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("Thread 1");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("Thread 2");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
System.out.println("Thread 3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
} finally {
executorService.shutdownNow();
}
System.out.println(executorService.isShutdown());
System.out.println(executorService.isTerminated());
System.out.println("End");
}
}
execute and submit in ExecutorService
- execute : takes a Runnable instance and complete the task asynchronously. It does not tell anything about the result of the task.
- submit : like execute complete task asynchronously but returns a Future object that can be used to determine if a task is completed.
Future
- Future is used to determine the result of an asynchronous task .
- Some of the useful methods which are oftenly used with Future are isDone(), isCancelled() and get().
package com.multithreading.demo;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class SubmitDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Integer> integerFuture = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 2;
}
});
executorService.shutdown();
if (integerFuture.isDone()) {
System.out.println(integerFuture.get());
}
if(integerFuture.isCancelled()){
System.out.println("Your task has been cancelled");
}
}
}
Working with Future
Submitting List of callable using invoke all
package com.multithreading.demo;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ExecuterServiceInvokeAll {
public static void main(String[] args) throws InterruptedException {
List<Callable<Integer>> runnableList = new ArrayList<>();
runnableList.add(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 1;
}
});
runnableList.add(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 2;
}
});
runnableList.add(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 3;
}
});
runnableList.add(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 4;
}
});
ExecutorService executorService = Executors
.newSingleThreadExecutor();
List<Future<Integer>> futureList = executorService.invokeAll(runnableList);
futureList.forEach((e)->{
if(e.isDone()){
try {
System.out.println(e.get());
} catch (InterruptedException e1) {
e1.printStackTrace();
} catch (ExecutionException e1) {
e1.printStackTrace();
}
}
});
}
}
awaitTermination()
This method waits the specified time to complete all tasks, returning sooner if all tasks finish.
package com.multithreading.demo;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ExecuterServiceAwaitTermination {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(()->{
try {
Thread.sleep(1000L);
System.out.println("Thread Running");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executorService.shutdown();
executorService.awaitTermination(2000L,TimeUnit.MILLISECONDS);
if(executorService.isTerminated()){
System.out.println("Terminated");
}else{
System.out.println("On or more tasks still remaining");
}
}
}
Scheduling tasks
- schedule() : Creates and executes a callable task after the given delay
package com.multithreading.demo;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ExecuterServiceSchedulingTasks {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
executorService
.schedule(new Runnable() {
@Override
public void run() {
System.out.println("Task executed after 1 second");
}
},
1,
TimeUnit.SECONDS);
executorService.shutdown();
}
}
Regular Task Scheduling after fixed interval
- scheduleAtFixedRate method creates a new task and submit it to the executor every period, regardless of whether or not the previous task finished.
- scheduleAtFixedDelay creates a new task after the previous task has finished e.g if the task runs at 12:00 and takes 5 minutes to finish, with a period of 2 minutes, then task will starts at 12:07.
package com.multithreading.demo;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ExecuterServiceSchedulingTasks {
public static void main(String[] args){
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
executorService
.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000L);
System.out.println("ScheduleWithFixedDelay Scheduled Task to executed after fixed interval");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
,
0,
1,
TimeUnit.SECONDS);
executorService
.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000L);
System.out.println("ScheduleAtFixedRate Scheduled Task to executed after fixed interval");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
,
0,
1,
TimeUnit.SECONDS);
}
}
Increasing Concurrency with Pools
- newCachedThreadPool() : Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads when they are available.
- newFixedThreadPool() : Creates a thread pool that reuses a fixed number number of threads.
package com.multithreading.demo;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Process implements Runnable{
int id;
public Process(int id) {
this.id = id;
}
@Override
public void run() {
System.out.println("Thread name::"+Thread.currentThread().getName()+" Start :"+id);
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread name::"+Thread.currentThread().getName()+" End :"+id);
}
}
public class ExecuterServiceDemo {
public static void main(String[] args) {
// ExecutorService executorService= Executors.newFixedThreadPool(3);
ExecutorService executorService= Executors.newCachedThreadPool();
for (int i = 0; i <= 30; i++) {
executorService.submit(new Process(i));
}
executorService.shutdown();
}
}
Synchronizing Data Access
- Thread safety in the property of an Object that gaurantees safe execution by multiple threads at the same time.
- Multiple Threads capable of accessing the same objects, we have to make sure to organize our access to this data such that we don't end up witn invalid or unexpected results.
package com.multithreading.demo;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
public class SynchronizeDemo {
int count;
public void incrementCount() {
count++;
}
public void worker1() {
for (int i=1;i<=1000;i++){
incrementCount();
}
}
public void worker2() {
for (int i=1;i<=1000;i++){
incrementCount();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizeDemo synchronizeDemo = new SynchronizeDemo();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronizeDemo.worker1();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronizeDemo.worker2();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(synchronizeDemo.count);
}
}
Synchronizing methods
We can add synchronize modifier to any instance method to synchronize automatically on the object itself.
package com.multithreading.demo;
import java.util.stream.IntStream;
public class SynchronizeDemo {
int count;
synchronized public void incrementCount() {
count++;
}
public void worker1() {
for (int i = 1; i <= 1000; i++) {
incrementCount();
}
}
public void worker2() {
for (int i = 1; i <= 1000; i++) {
incrementCount();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizeDemo synchronizeDemo = new SynchronizeDemo();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronizeDemo.worker1();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronizeDemo.worker2();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(synchronizeDemo.count);
}
}
Synchronized Blocks
- Synchronize block in such a way that each thread that arrives will first check if any threads are in block.
- In this manner a thread acquires the lock.
- If the lock is available, a single Thread will enter the block and acquire the lock and preventing all other threads from entering.
public void incrementCount() {
synchronized(this) {
count++;
}
}
Atomic class
- Atomic is the property of an operation to be carried out as a single unit of execution without any interference by another Thread.
- The concurrency API includes numerous useful classes that are conceptually the same as primitive classes but that support atomic operation e.g AtomicBoolean, AtomicInteger, AtomicLong
package com.multithreading.demo;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;
public class SynchronizeDemo {
AtomicInteger count= new AtomicInteger();
public void incrementCount() {
synchronized (this) {
count.incrementAndGet();
}
}
public void worker1() {
for (int i = 1; i <= 1000; i++) {
count.incrementAndGet();
}
}
public void worker2() {
for (int i = 1; i <= 1000; i++) {
count.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizeDemo synchronizeDemo = new SynchronizeDemo();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronizeDemo.worker1();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronizeDemo.worker2();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(synchronizeDemo.count);
}
}
Wait and Notify
- wait() forces the current thread to wait until some other thread invokes notify() or notifyAll() on the same object.
- notify() for all threads waiting on this object’s monitor (by using any one of the wait() method), the method notify()notifies any one of them to wake up arbitrarily.
- notifyAll() This method simply wakes all threads that are waiting on this object’s monitor.
- wait and notify requires a lock so make sure that you use them under some sync mechanism e.g synchronize block.
package com.multithreading.demo;
public class Demo {
public void worker1(){
synchronized (this) {
System.out.println("Worker1 Started");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Worker1 Done");
}
}
public void worker4(){
synchronized (this) {
System.out.println("Worker 4 Started");
System.out.println("Worker 4 Done");
notify();
}
}
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
demo.worker1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
demo.worker4();
}
}).start();
}
}
Wait with notify
package com.multithreading.demo;
public class Demo {
public void worker1(){
synchronized (this) {
System.out.println("Worker1 Started");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Worker1 Done");
}
}
public void worker2(){
synchronized (this) {
System.out.println("Worker 2 Started");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Worker 2 Done");
}
}
public void worker3(){
synchronized (this) {
System.out.println("Worker 3 Started");
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Worker 3 Done");
}
}
public void worker4(){
synchronized (this) {
System.out.println("Worker 4 Started");
System.out.println("Worker 4 Done");
notifyAll();
}
}
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
demo.worker1();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
demo.worker2();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
demo.worker3();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
demo.worker4();
}
}).start();
}
}
Wait with notifyAll
Producer Consumer using wait and notify
package com.multithreading.demo;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Demo {
List list = new ArrayList();
final int MAX = 10;
void produce() throws InterruptedException {
Random random = new Random();
while (true) {
synchronized (this) {
while (MAX == list.size()) {
wait();
}
list.add(random.nextInt(10));
System.out.println(list);
notify();
}
}
}
void consume() throws InterruptedException {
while (true) {
synchronized (this) {
while (list.size() == 0) {
wait();
}
list.remove(0);
System.out.println(list);
notify();
}
Thread.sleep(1000L);
}
}
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(new Runnable() {
@Override
public void run() {
try {
demo.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
demo.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
Reentract Lock
Reebtract Lock is similar to locking provided by synchronized keyword with extended features. Few of them are as follows:
- Fairness property provides lock to longest waiting thread, in case of contention.
- tryLock() obtains the lock only if the lock is available. This reduce blocking of thread.
package com.multithreading.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
Lock lock = new ReentrantLock(true);
int count;
void increment(){
lock.lock();
count++;
lock.unlock();
}
public void worker1(){
for (int i = 1; i <= 1000; i++) {
increment();
}
}
public void worker2(){
for (int i = 1; i <= 1000; i++) {
increment();
}
}
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
demo.worker1();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
demo.worker2();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(demo.count);
}
}
Deadlock
package com.multithreading.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
Lock lock1 = new ReentrantLock(true);
Lock lock2 = new ReentrantLock(true);
public void worker1(){
lock1.lock();
System.out.println("lock 1 worker 1");
lock2.lock();
System.out.println("lock 2 worker 1");
lock2.unlock();
lock1.unlock();
}
public void worker2(){
lock2.lock();
System.out.println("lock 1 worker 2");
lock1.lock();
System.out.println("lock 2 worker 2");
lock2.unlock();
lock1.unlock();
}
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
demo.worker1();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
demo.worker2();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
Solving Deadlock
1st way : Preserve the order
package com.multithreading.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
Lock lock1 = new ReentrantLock(true);
Lock lock2 = new ReentrantLock(true);
public void worker1(){
lock1.lock();
System.out.println("lock 1 worker 1");
lock2.lock();
System.out.println("lock 2 worker 1");
lock2.unlock();
lock1.unlock();
}
public void worker2(){
lock1.lock();
System.out.println("lock 1 worker 2");
lock2.lock();
System.out.println("lock 2 worker 2");
lock2.unlock();
lock1.unlock();
}
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
demo.worker1();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
demo.worker2();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
2nd way : Use tryLock
package com.multithreading.demo;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
Lock lock1 = new ReentrantLock(true);
Lock lock2 = new ReentrantLock(true);
public void acquireLock(Lock lock1, Lock lock2){
boolean acquireLock1= lock1.tryLock();
boolean acquireLock2= lock2.tryLock();
if(acquireLock1 && acquireLock2){
return;
}
if(acquireLock1){
lock1.unlock();
}
if(acquireLock2){
lock2.unlock();
}
}
public void worker1(){
acquireLock(lock1,lock2);
System.out.println("lock 1 worker 1");
System.out.println("lock 2 worker 1");
lock2.unlock();
lock1.unlock();
}
public void worker2(){
acquireLock(lock2,lock1);
System.out.println("lock 1 worker 2");
System.out.println("lock 2 worker 2");
lock1.unlock();
lock2.unlock();
}
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
demo.worker1();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
demo.worker2();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
signal, signalAll and await
package com.multithreading.demo;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo {
Lock lock = new ReentrantLock(true);
Condition condition = lock.newCondition();
public void worker1() {
try {
lock.lock();
System.out.println("worker 1 Started");
condition.await();
System.out.println("worker 1 Finished");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void worker2() {
try{
lock.lock();
System.out.println("worker 2 Started");
System.out.println("worker 2 Finished");
condition.signal();
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
demo.worker1();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
demo.worker2();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
Concurrent Collection
- Using Concurrent collections is extremely convenient in practice.
- It prevents us from introducing mistake in own custom implementation such as forgot to synchronize some method.
- e.g ConccurentHashMap, ConcurrentLinkedQueue, ConcurrentLinkedDeque etc
Memory Consistency Error
Following code will throw a ConcurrentModificationException at runtime since the keyset() is not updated after the First element is removed.
package com.multithreading.demo;
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
Map<String, Object> map = new HashMap<>();
map.put("One", 1);
map.put("Two", 2);
for (String key : map.keySet()) {
map.remove(key);
}
}
}
Fixing the memory consistency error
package com.multithreading.demo;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Demo {
public static void main(String[] args) {
Map<String, Object> map = new ConcurrentHashMap<>();
map.put("One", 1);
map.put("Two", 2);
for (String key : map.keySet()) {
map.remove(key);
}
}
}
- ConcurrentHashMap in ordering read/write access such that all access to the class is consistent.
- In the code above the iterator created by KeySet() is updated as soon as an object is removed from the Map.
Understanding Blocking Queue
- A blocking queue is a queue that blocks when you try to dequeue from it and the queue is empty, or if you try to enqueue items to it and the queue is already full.
- A thread trying to dequeue from an empty queue is blocked until some other thread inserts an item into the queue.
Producer Consumer with LinkedBlockingQueue
LinkedBlockingQueue maintains a linked list between elements. Following is the implementation of producer consumer problem via LinkedBlocking Queue.
package com.multithreading.demo;
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ProducerConsumerDemo {
static BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>(10);
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
produce();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
consume();
}
}).start();
}
public static void produce() {
Random random = new Random();
while (true) {
try {
blockingQueue.put(random.nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Produce :: " + blockingQueue);
}
}
public static void consume() {
Random random = new Random();
while (true) {
try {
Thread.sleep(1000L);
Integer element = blockingQueue.take();
System.out.println("Consume :: " + blockingQueue);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Exercise
- Create and Run a Thread using Runnable Interface and Thread class.
- Use sleep and join methods with thread.
- Use a singleThreadExecutor to submit multiple threads.
- Try shutdown() and shutdownNow() and observe the difference.
- Use isShutDown() and isTerminate() with ExecutorService.
- Return a Future from ExecutorService by using callable and use get(), isDone(), isCancelled() with the Future object to know the status of task submitted.
- Submit List of tasks to ExecutorService and wait for the completion of all the tasks.
- Schedule task using schedule(), scheduleAtFixedRate() and scheduleAtFixedDelay()
- Increase concurrency with Thread pools using newCachedThreadPool() and newFixedThreadPool().
Exercise (Cont.)
- Use Synchronize method to enable synchronization between multiple threads trying to access method at same time.
- Use Synchronize block to enable synchronization between multiple threads trying to access method at same time.
- Use Atomic Classes instead of Synchronize method and blocks.
- Coordinate 2 threads using wait() and notify().
- Coordinate mulitple threads using wait() and notifyAll()
- Use Reentract lock for coordinating 2 threads with signal(), signalAll() and wait().
- Create a deadlock and Resolve it using tryLock().
Multithreading Bootcamp
By Pulkit Pushkarna
Multithreading Bootcamp
- 1,939