Java Concurrency & Multi-threading Tutorial
In this Java tutorial we learn how to run multiple sections of code at once with multi-threading.
We discuss how to create and start threads, how to join them so they wait for each other and how to synchronize sections of code to prevent race conditions.
We also discuss how threads can communicate with each other, how to manually interrupt a thread and how to work with thread pools.
- What is concurrency and multi-threading
- How to create a thread from the Thread class in Java
- How to create a thread from the Runnable interface in Java
- How to join threads in Java
- How to synchronize threads in Java
- Inter-thread communication in Java
- How to interrupt a thread in Java
- Thread pools in Java
- Summary: Points to remember
What is concurrency and multi-threading
Concurrency is when sections of code in our application runs at the same time as other sections of code.
Typically, a program would run its code sequentially, section after section. By using threads, we can run sections of our code at the same time as other sections.
As an example, let’s consider two loops. Typically, we have to wait for one loop to finish before the next one can start.
public class Program extends Thread {
public static void main(String[] args) {
// both loops run on a single thread
for (byte i = 1; i <= 5; i++) {
// sleep .5 seconds for demonstration purposes
try { Thread.sleep(500); } catch(InterruptedException e) { System.out.println(e); }
System.out.println("Loop 1, Iteration: " + i);
}
// this loop will wait for the one above
// to finish before it will start
for (byte i = 1; i <= 5; i++) {
try { Thread.sleep(500); } catch(InterruptedException e) { System.out.println(e); }
System.out.println("Loop 2, Iteration: " + i);
}
}
}
In the output we can see that the first loop completes all its iterations before the second loop starts.
Loop 1, Iteration: 1
Loop 1, Iteration: 2
Loop 1, Iteration: 3
Loop 1, Iteration: 4
Loop 1, Iteration: 5
Loop 2, Iteration: 1
Loop 2, Iteration: 2
Loop 2, Iteration: 3
Loop 2, Iteration: 4
Loop 2, Iteration: 5
Placing one of the loops in a separate thread, will allow us to run both at the same time.
public class Program {
public static void main(String[] args) {
Concurrency thread1 = new Concurrency(1);
Concurrency thread2 = new Concurrency(2);
thread1.start();
thread2.start();
}
}
class Concurrency extends Thread {
private int loopNum;
Concurrency(int loopNum) {
this.loopNum = loopNum;
}
@Override
public void run() {
for (byte i = 1; i <= 5; i++) {
// sleep .5 seconds for demonstration purposes
try { Thread.sleep(500); } catch(InterruptedException e) { System.out.println(e); }
System.out.println("Loop " + this.loopNum + ", Iteration: " + i);
}
}
}
This time, the loops will run at the same time.
Loop 2, Iteration: 1
Loop 1, Iteration: 1
Loop 2, Iteration: 2
Loop 1, Iteration: 2
Loop 1, Iteration: 3
Loop 2, Iteration: 3
Loop 1, Iteration: 4
Loop 2, Iteration: 4
Loop 1, Iteration: 5
Loop 2, Iteration: 5
How to create a thread from the Thread class in Java
One of the ways of creating a thread, is by inheriting from the Thread class.
public class Program {
public static void main(String[] args) {}
}
// extend the 'Thread' class
class Concurrency extends Thread {}
In the .run() method we can specify the code we want to have run in a separate thread. We need to override the public void run method in our class.
public class Program {
public static void main(String[] args) {
// class object
Concurrency thread1 = new Concurrency();
}
}
// extend the 'Thread' class
class Concurrency extends Thread {
@Override
public void run() {
// any code in this method
// will be run in a separate
// thread when we start one
for (int i = 1; i <= 5; i++) {
// sleep .5 seconds for demonstration purposes
try { Thread.sleep(500); } catch(InterruptedException e) { System.out.println(e); }
System.out.println("Iteration: " + i);
}
}
}
Our first instinct would be to invoke the run method on the class object, but Java actually provides us with the .start() method that will invoke it for us.
When we call the start method, it will start the thread and then invoke the run method. If we call the run method, it will just work like a normal method.
public class Program {
public static void main(String[] args) {
// class object
Concurrency thread1 = new Concurrency();
Concurrency thread2 = new Concurrency();
// start a thread and invoke 'run'
thread1.start();
thread2.start();
}
}
// extend the 'Thread' class
class Concurrency extends Thread {
@Override
public void run() {
// any code in this method
// will be run in a separate
// thread when we start one
for (int i = 1; i <= 5; i++) {
// sleep .5 seconds for demonstration purposes
try { Thread.sleep(500); } catch(InterruptedException e) { System.out.println(e); }
System.out.println("Iteration: " + i);
}
}
}
In the example above, we create two threads and run the loop in each of them. From the output we can see that they run at the same time, or close enough at least.
Iteration: 1
Iteration: 1
Iteration: 2
Iteration: 2
Iteration: 3
Iteration: 3
Iteration: 4
Iteration: 4
Iteration: 5
Iteration: 5
The loop in the second object doesn’t have to wait for the first one to finish.
We can also do this in a lambda expression .
public class Program {
public static void main(String[] args) {
// start a new thread from
// an anonymous class
new Thread() {
@Override
public void run() {
System.out.println("Hello from inside an anonymous Thread");
}
}.start(); // remember to start() the thread
}
}
How to create a thread from the Runnable interface in Java
Another way of creating a new thread is by implementing the Runnable interface instead of inheriting from the Thread class.
public class Program {
public static void main(String[] args) {
// class object
Concurrency thread1 = new Concurrency();
Concurrency thread2 = new Concurrency();
// start a thread and invoke 'run'
thread1.start();
thread2.start();
}
}
// implements 'Runnable' interface
class Concurrency implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try { Thread.sleep(500); } catch(InterruptedException e) { System.out.println(e); }
System.out.println("Iteration: " + i);
}
}
}
Once again, we override the run method with our own logic. However, when we try to start the thread, the compiler raises an error.
Exception in thread "main" java.lang.Error: Unresolved compilation problems:
The method start() is undefined for the type Concurrency
That’s because the Runnable interface only contains the run method, nothing else. We have two options to deal with this problem.
1. The first option is to create an instance of the Thread class and pass an object of our class (that implements Runnable) to the Thread constructor.
Then we can call the Thread instance’s start method like we did before.
public class Program {
public static void main(String[] args) {
// instance of Thread
// with our class in constructor
Thread thread1 = new Thread( new Concurrency() );
Thread thread2 = new Thread( new Concurrency() );
// start a thread and invoke 'run'
thread1.start();
thread2.start();
}
}
// implements 'Runnable' interface
class Concurrency implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try { Thread.sleep(500); } catch(InterruptedException e) { System.out.println(e); }
System.out.println("Iteration: " + i);
}
}
}
2. The second option is to define our own start method and instantiate a new ‘Thread’ object inside it. Then we call the thread object’s ‘start’ method.
public class Program {
public static void main(String[] args) {
Concurrency thread1 = new Concurrency();
Concurrency thread2 = new Concurrency();
// our custom 'start' method
thread1.start();
thread2.start();
}
}
class Concurrency implements Runnable {
// new thread object
private Thread t;
// custom start method
public void start() {
// create a thread object and
// invoke its start method
if (t == null) {
t = new Thread(this);
t.start();
}
}
@Override
public void run() {
for (byte i = 1; i <= 5; i++) {
try { Thread.sleep(500); } catch(InterruptedException e) { System.out.println(e); }
System.out.println("Iteration: " + i);
}
}
}
As with the Thread class, we can create a new thread with a lambda expression .
public class Program {
public static void main(String[] args) {
// instance of Thread
// with a new Runnable anonymous method
Thread thread1 = new Thread( new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try { Thread.sleep(500); } catch(InterruptedException e) { System.out.println(e); }
System.out.println("Thread 1, Iteration: " + i);
}
}
});
Thread thread2 = new Thread( new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try { Thread.sleep(500); } catch(InterruptedException e) { System.out.println(e); }
System.out.println("Thread 2, Iteration: " + i);
}
}
});
// start a thread and invoke 'run'
thread1.start();
thread2.start();
}
}
This time we create a new instance of the Runnable interface in the Thread class constructor and override the run method as we would normally. Both threads still run concurrently.
Thread 1, Iteration: 1
Thread 2, Iteration: 1
Thread 1, Iteration: 2
Thread 2, Iteration: 2
Thread 1, Iteration: 3
Thread 2, Iteration: 3
Thread 1, Iteration: 4
Thread 2, Iteration: 4
Thread 1, Iteration: 5
Thread 2, Iteration: 5
Java developers typically implement the Runnable interface instead inheriting from the Thread class.
It’s more convenient, favors interface over inheritance, and many methods in the Java API want a Runnable instance passed to them.
In this tutorial lesson we’ll use both the Thread class and Runnable interface as well as their anonymous variants to familiarize you with all of them.
How to join threads in Java
It’s possible to have a situation where one thread depends on another and must wait for it to complete its tasks before it run.
For example, imagine we have two threads, one that fetches data, and one that processes the data the other one fetched.
We can tell the processor thread to wait until the fetcher thread has retrieved some or all of the data needed.
We do this by joining the processor thread with the fetcher thread. Because they’re linked, the processor thread will then wait for the fetcher thread to retrieve the data before it starts its execution.
To join threads we use the .join() method on the object we want to join with another, in the other thread’s run implementation.
public class Program {
public static void main(String[] args) {
// traditional thread
Thread thread1 = new Thread( new Concurrency() );
// anonymous thread
Thread thread2 = new Thread( new Runnable() {
@Override
public void run() {
// join thread 1 to thread 2
// must handle the exception
// that could occur
try {
thread1.join();
}
catch (InterruptedException e) {
System.out.println("I couldn't wait, I was interrupted: " + e);
}
for (int i = 1; i <= 3; i++) {
try { Thread.sleep(500); } catch(InterruptedException e) { System.out.println(e); }
System.out.println("Thread 2, Iteration: " + i);
}
}
});
// start threads
thread1.start();
thread2.start();
}
}
// implements 'Runnable' interface
class Concurrency implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try { Thread.sleep(500); } catch(InterruptedException e) { System.out.println(e); }
System.out.println("Thread 1, Iteration: " + i);
}
}
}
In the example above, we want thread2 to wait until the loop in thread1 finishes before it executes its own loop. So, we invoke the the join method on thread1 inside the run implementation of thread2.
In the ouput we can see that thread2 waits until thread1 has finished.
Thread 1, Iteration: 1
Thread 1, Iteration: 2
Thread 1, Iteration: 3
Thread 1, Iteration: 4
Thread 1, Iteration: 5
Thread 2, Iteration: 1
Thread 2, Iteration: 2
Thread 2, Iteration: 3
We use both a traditional method and a lambda expression simply to show that it’s possible.
How to synchronize threads in Java
It’s possible to end up in a situation where more than one thread try to access and/or mutate the same resource.
For example, if multiple threads try to write to the same file, or one thread is opening the same file another is trying to close at the same time. Both of these situations could corrupt the data or the whole file itself.
The solution is to synchronize multiple threads to make sure that only one thread can access a resource at a time. Java implements this synchronization with a concept called monitors.
Each object in Java is associated with a monitor that a thread can lock or unlock. Only one thread at a time can hold a lock on a monitor.
Java provides us with the synchronized construct to allow us to synchronize whole methods or othe sections of code.
Let’s see an example of why we need this first.
class Program {
public static void main(String args[]) {
Messenger sender = new Messenger();
ThreadMessenger thread1 = new ThreadMessenger("Hello", sender);
thread1.setName("Thread 1");
ThreadMessenger thread2 = new ThreadMessenger("Bye", sender);
thread2.setName("Thread 2");
thread1.start();
thread2.start();
// join threads and
// wait for them to end
try {
thread1.join();
thread2.join();
} catch(InterruptedException e) {
System.out.println("Thread interrupted: " + e);
}
}
}
// class to send messages
class Messenger {
// send a message and log the process
public void sendMessage(String msg) {
System.out.println(Thread.currentThread().getName() + ", Sending message: " + msg);
// wait .5 seconds before
// logging as sent
try {
Thread.sleep(500);
} catch(InterruptedException e) {
System.out.println("Thread interrupted: " + e);
}
System.out.println(Thread.currentThread().getName() + ", Message sent: " + msg);
}
}
// class to send messages in a thread
class ThreadMessenger extends Thread {
private String msg;
Messenger sender;
ThreadMessenger(String msg, Messenger sender) {
this.msg = msg;
this.sender = sender;
}
@Override
public void run() {
this.sender.sendMessage(this.msg);
}
}
The idea is that the messages would send in order, first ‘Hello’, then ‘Bye’.
Thread 1, Sending message: Hello
Thread 1, Message sent: Hello
Thread 2, Sending message: Bye
Thread 2, Message sent: Bye
However, if we run the example a few times, we can see that it doesn’t always do that. Sometimes the messages arrive out of order.
Thread 1, Sending message: Hello
Thread 2, Sending message: Bye
Thread 2, Message sent: Bye
Thread 1, Message sent: Hello
We have two options to synchronize the threads.
1. We can synchronize the whole ‘sendMessage()’ method in the ‘Messenger’ class by adding the synchronized keyword.
class Program {
public static void main(String args[]) {
Messenger sender = new Messenger();
ThreadMessenger thread1 = new ThreadMessenger("Hello", sender);
thread1.setName("Thread 1");
ThreadMessenger thread2 = new ThreadMessenger("Bye", sender);
thread2.setName("Thread 2");
thread1.start();
thread2.start();
// join threads and
// wait for them to end
try {
thread1.join();
thread2.join();
} catch(InterruptedException e) {
System.out.println("Thread interrupted: " + e);
}
}
}
// class to send messages
class Messenger {
// synchronize the whole method
public synchronized void sendMessage(String msg) {
System.out.println(Thread.currentThread().getName() + ", Sending message: " + msg);
// wait .5 seconds before
// logging as sent
try {
Thread.sleep(500);
} catch(InterruptedException e) {
System.out.println("Thread interrupted: " + e);
}
System.out.println(Thread.currentThread().getName() + ", Message sent: " + msg);
}
}
// class to send messages in a thread
class ThreadMessenger extends Thread {
private String msg;
Messenger sender;
ThreadMessenger(String msg, Messenger sender) {
this.msg = msg;
this.sender = sender;
}
@Override
public void run() {
this.sender.sendMessage(this.msg);
}
}
Now when we run the example, the messages are sent in the correct order. The method will lock on an object and any other thread that wants to access it will have to wait until the lock is released.
The first object will lock the method and execute the code, so it will show “Sending” and “Sent”. Only after its done executing the code, will the lock be released and the second object can access it.
Thread 1, Sending message: Hello
Thread 1, Message sent: Hello
Thread 2, Sending message: Bye
Thread 2, Message sent: Bye
2. Our second option is to use a synchronized block. The block is synchronized on some object, it’s allowed to be the calling object (this).
synchronized(object_name) {
}
// or
synchronized(this) {
}
class Program {
public static void main(String args[]) {
Messenger sender = new Messenger();
ThreadMessenger thread1 = new ThreadMessenger("Hello", sender);
thread1.setName("Thread 1");
ThreadMessenger thread2 = new ThreadMessenger("Bye", sender);
thread2.setName("Thread 2");
thread1.start();
thread2.start();
// join threads and
// wait for them to end
try {
thread1.join();
thread2.join();
} catch(InterruptedException e) {
System.out.println("Thread interrupted: " + e);
}
}
}
// class to send messages
class Messenger {
public void sendMessage(String msg) {
System.out.println(Thread.currentThread().getName() + ", Sending message: " + msg);
// wait .5 seconds before
// logging as sent
try {
Thread.sleep(500);
} catch(InterruptedException e) {
System.out.println("Thread interrupted: " + e);
}
System.out.println(Thread.currentThread().getName() + ", Message sent: " + msg);
}
}
// class to send messages in a thread
class ThreadMessenger extends Thread {
private String msg;
Messenger sender;
ThreadMessenger(String msg, Messenger sender) {
this.msg = msg;
this.sender = sender;
}
@Override
public void run() {
// synchronize only a section of code
//
// in this case we synchronize on the
// Messenger object 'sender' that
// contains the 'sendMessage' method
synchronized(sender) {
this.sender.sendMessage(this.msg);
}
}
}
This time we use the synchronized block to synchronize only a section of code.
Synchronization is re-entrant, which means that if a thread acquires an object’s lock, it can keep executing on the synchronized code because it has ownership.
Sometimes, we will see the term Critical Section when discussing threads. Critical section simply refers to the code that’s referencing a shared resource, like a variable, and only one thread at a time should be able to execute a critical section.
We’ll also see the term Thread Safe used. This means that the developer has synchronized the critical sections within the code so that we don’t have to worry about thread interference.
Inter-thread communication in Java
Inter-thread communication allows synchronized threads to communicate with each other.
This communication is a mechanism that pauses a thread that’s running its critical section, allowing another thread to lock the same critical section.
Java provides us with the following methods to implement thread communication.
1. The wait() method will cause the current thread to release the lock and wait until a set amount of time has passed, or another thread invokes the notify or notify all methods.
The current thread must own the object’s monitor, so it must be called from a synchronized mehod.
The wait method must be called from a try..catch block to be able to catch a InterruptedException if it occurs.
try {
wait();
} catch(InterruptedException e) {
// handle exception
}
2. The notify() method wakes up a single thread that’s waiting on this object’s monitor. Any threads waiting on this object will be chosen to be awakened and the choice is arbitrary.
Because we can’t specify a specific thread to be awakened in the notify method’s parameter list, it’s conventional to use the notifyAll method.
notify();
3. The notifyAll() method will wake up all the threads that are waiting on this object’s monitor.
This method is conventionally used instead of the notify method, unless there are a significant amount of threads, that all perform a similar task, waiting for a lock.
notifyAll();
Let’s do a simple breakdown of what happens.
- A thread acquire’s a lock.
- If the wait method is called, the thread goes into a waiting state, otherwise it releases the lock and exits.
- If the notify or notifyAll method is called, the thread goes into a notified (runnable) state.
- The thread is then available to acquire a lock.
- After the task has been completed, the thread releases the lock and exits the monitor state of the object.
Let’s see an example of this in action.
class Customer {
// amount of money in
// customer's account
int amount = 1000;
// method to simulate a withdrawal
public synchronized void withdraw(int amount) {
System.out.println("Trying to withdraw...");
// if the user tries to withdraw
// more than what's in 'this.amount'
// wait for the deposit method to
// finish taking in a deposit
// before withdrawing the money
if(this.amount < amount) {
System.out.println("Balance too small, waiting for deposit");
try {
wait();
} catch (InterruptedException e) {
System.out.println("Thread interrupted: " + e);
}
}
// if deposit has notified that
// it's done, we can withdraw
this.amount -= amount;
System.out.println("Withdrawal completed");
}
// method to simulate a deposit
public synchronized void deposit(int amount) {
System.out.println("Trying to deposit...");
this.amount += amount;
System.out.println("Deposit completed");
// notify any waiting threads
// that the deposit is complete
notifyAll();
}
}
class Program {
public static void main(String args[]) {
// acquire lock on the
// final customer object
final Customer customer = new Customer();
// try to withdraw more money
// than is initially available
new Thread() {
@Override
public void run() {
customer.withdraw(1500);
}
}.start();
// deposit more money, the thread
// above will wait until this
// deposit is completed
new Thread() {
@Override
public void run() {
customer.deposit(1000);
}
}.start();
}
}
The ‘Customer’ class contains two synchronized methods.
The ‘withdraw’ method will try to subtract an amount from the ‘amount’ property. If there isn’t enough it will go into a waiting state, releasing the lock which the ‘deposit’ method can then pick up.
The deposit method will then be able to add to the ‘amount’ property because the withdraw method has released the lock.
After it has completed, it will notify any waiting methods (in this case the withdraw method) that it’s done and is releasing its lock, which the waiting method can then pick up again.
We can imagine a conversation between them.
Withdraw: I can’t complete this task before you complete yours. Deposit, I’ll release the lock so you can work on it.
Deposit: I have completed my task, I’ll release the lock so you can work on it again.
How to interrupt a thread in Java
We can manually interrupt a thread by calling the interrupt method on that thread. The method will break out of the sleeping or waiting state and throw an InterruptedException.
If the thread is not in a sleeping or waiting state, the method won’t interrupt the thread, but will set the interrupt flag to true.
Java provides us with 3 methods to interrupt a thread.
1. The interrupt() method will interrupt the thread if its in a sleeping or waiting state and throw an InterruptedException.
The method is invoked on the thread we want to interrupt, and must be called from a try..catch block.
class Concurrency extends Thread {
@Override
public void run() {
// sleep for 5 seconds, if the
// thread is interrupted, catch
// the exception and print it
try {
sleep(5000);
} catch(InterruptedException e) {
throw new RuntimeException("Thread interrupted: " + e);
}
System.out.println("Thread is running");
}
}
class Program {
public static void main(String args[]) {
Concurrency thread1 = new Concurrency();
thread1.start();
try {
// interrupt the thread
thread1.interrupt();
} catch(Exception e) {}
}
}
The thread in the example above will try to sleep for 5 seconds, but we interrupt it right after it starts and it throws an exception.
Exception in thread "Thread-0" java.lang.RuntimeException: Thread interrupted: java.lang.InterruptedException: sleep interrupted
The output doesn’t show the print message because we didn’t handle the exception. If we handle the exception, the thread will break out of its sleeping state, but will not stop working.
class Concurrency extends Thread {
@Override
public void run() {
// sleep for 5 seconds, if the
// thread is interrupted, catch
// the exception and print it
try {
sleep(5000);
} catch(InterruptedException e) {
System.out.println("Handling exception: " + e);
}
System.out.println("Thread is running");
}
}
class Program {
public static void main(String args[]) {
Concurrency thread1 = new Concurrency();
thread1.start();
try {
// interrupt the thread
thread1.interrupt();
} catch(Exception e) {}
}
}
This time we handle the exception so the thread doesn’t stop working, and the message gets printed.
If the thread isn’t in a sleeping or waiting state, the method won’t interrupt it, but it will set the interrupted flag to true. The programmer can then use the flag to stop the thread at their discretion.
class Concurrency extends Thread {
@Override
public void run() {
System.out.print("Iteration:");
for(int i = 0; i <= 5; i++) {
System.out.print(" " + i);
}
}
}
class Program {
public static void main(String args[]) {
Concurrency thread1 = new Concurrency();
thread1.start();
thread1.interrupt();
}
}
The example above, will set the interrupted flag to true, which is where the ‘isInterrupted’ method comes into play.
2. The isInterrupted() method will return whether the interrupted flag is true or false.
class Concurrency extends Thread {
@Override
public void run() {
System.out.print("Iteration:");
for(int i = 0; i <= 5; i++) {
System.out.print(" " + i);
}
}
}
class Program {
public static void main(String args[]) {
Concurrency thread1 = new Concurrency();
thread1.start();
thread1.interrupt();
System.out.println( thread1.isInterrupted() );
}
}
In the example above, we interrupt the thread and then print to the console if the flag is true or not. Because we interrupted the thread, the flag will be true, but normal execution will still continue.
true
Iteration: 0 1 2 3 4 5
3. The interrupted() method will return if the interrupted flag is true and if the flag is true, it will set it to false.
This method is static and should be accessed in a static way.
class Concurrency extends Thread {
@Override
public void run() {
System.out.print("Iteration:");
for(int i = 0; i <= 5; i++) {
System.out.print(" " + i);
}
}
}
class Program {
public static void main(String args[]) {
Concurrency thread1 = new Concurrency();
thread1.start();
thread1.interrupt();
System.out.println( Thread.interrupted() );
}
}
Because we interrupt the thread, the flag will be true when we invoke the method. The method will then set it to false and we print that to the console.
Again, normal execution will continue.
false
Iteration: 0 1 2 3 4 5
Thread pools in Java
A thread pool is a group of worker threads that wait for a job, and can be reused many times.
When we create a thread pool, a fixed number of threads are created. When there’s a job to do, a thread will be pulled from the pool assigned to a job by the service provider.
When the task is complete, the thread is sent back to the thread pool again.
Before we can work with thread pools, we need to import the Executors and ExecutorService utility packages.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Concurrency implements Runnable {
private int i;
Concurrency(int i) {
this.i = i;
}
@Override
public void run() {
// log the start and end of
// a task for simplicity
System.out.println(Thread.currentThread().getName() + ": Start task " + i);
try {
Thread.sleep(500);
} catch(InterruptedException e) {
System.out.println("Thread interrupted: " + e);
}
System.out.println(Thread.currentThread().getName() + ": End task " + i);
}
}
class Program {
public static void main(String args[]) {
// create a pool of 5 threads
ExecutorService executor = Executors.newFixedThreadPool(5);
for(int i = 0; i <= 5; i++) {
// new job to run
Runnable worker = new Concurrency(i);
// assign a thread in the
// pool to execute the job
executor.execute(worker);
}
}
}
In the example above, we log the start and end of a task performed by a thread worker.
We use the Executors.newFixedThreadPool() method to create 1 pool with 5 threads in the service provider called ‘executor’.
Then, we create a new job in the loop and execute the service provider on that job with the .execute() method. The service provider will assign a thread from the pool to the job.
Summary: Points to remember
- Multithreading allows us to run multiple tasks in code simultaniously, by assigning the task to a separate thread.
- We can create threads by extending the Thread class or implementing the Runnable interface.
- If one thread depends on another, we can join them with the join() method so that they wait for each to finish.
- To prevent race conditions, we synchronize multiple threads to ensure only one thread can access a resource at a time.
- We can add the synchronize keyword to a method definition to synchronize a whole method.
- Alternatively, we can use a synchronize block, that locks on an object, to synchronize only a section of code.
- Threads can communicate with each other by using the wait(), notify() and notifyAll() methods.
- Threads can be manually interrupted with the interrupt() method.
- If a thread is in a wait or sleep state, the method will interrupt, if not the method will set the interrupted flag to true.
- We can use the isInterrupted() method to return the status of the flag.
- The interrupted() method will evaluate if the flag is true, if so it will set it to false.
- Threads in a pool are assigned to tasks by the service provider, which will assign a thread from the pool to a task.