Concurrent and parallel programming¶
Introduction¶
Concurrent programming involves designing and creating programs that, in the execution phase, consist of at least two concurrent units (each of them being a sequential process), along with ensuring synchronization between them. These entities can be threads or processes.
Thread and process¶
When we launch our applications, we start a new process at the operating system level. We can get a list of such processes using the ps
command. Multiple threads can exist within one process. Threads have a common address space and open system structures (such as open files), processes in turn have independent address spaces.
NOTE: The command
ulimit -a
will tell us the currently configured resource limits for the operating system.
Thread¶
Thread is the execution thread in the program. The Java Virtual Machine allows an application to run multiple threads simultaneously. Each thread has a priority. Higher priority threads run before lower priority threads.
When the JVM starts, normally the main thread which calls the main
method is executed. Then we can start other threads from the main thread. These threads run until one of the following occurs:
- the threads will finish their work
- thread will throw an exception
- the method
System.exit ()
(from any thread) will be called.
A Java thread is represented by the Thread
class. We can create threads in many ways, e.g .:
- extending the
Thread
class and overriding therun
method - by implementing the functional interface
Runnable
.
Inheriting from the Thread class¶
If you want to define a new thread and decide to extend the Thread
class, all code that should execute in a separate thread must go to therun
method. The example shows creating a new thread by extending the Thread
class. Additionally, a thread is started using the start ()
method. The main thread and the thread created by us, at the end of their operation, print their identifier with Thread.currentThread().GetId()
:
public class ThreadsExample {
public static void main(String[] args) {
new HelloWorldThread().start();
System.out.println(Thread.currentThread().getId());
}
}
class HelloWorldThread extends Thread {
@Override
public void run() {
System.out.println("Hello World from another Thread");
System.out.println(Thread.currentThread().getId());
}
}
A sample output of the program may look like this:
1
Hello World from another Thread
14
NOTE: Inheriting from
Thread
is NOT RECOMMENDED. If you want to create a new thread, let's use theRunnable
interface.
Runnable¶
Another, better way to create a thread is to declare a class that implements the Runnable
interface with one abstract, argumentlessrun
method. A Runnable
instance may be passed as an argument to theThread
class constructor. We start the created thread using the start
method.
The next example runs two separate threads using the Runnable
interface, one by defining a separate class, the second time by using [lambda] (functional_programming.md # lambda-expressions):
public class ThreadsExample {
public static void main(String[] args) {
new Thread(new HelloWorldRunnableThread()).start();
new Thread(() -> System.out.println("Hello from another thread implemented with lambda")).start();
}
}
class HelloWorldRunnableThread implements Runnable {
@Override
public void run() {
System.out.println("Hello World from another Thread");
}
}
Breaking a thread¶
Depending on the circumstances and the state of the application, we may occasionally want to interrupt the work being performed by a certain thread. However, it is impossible to break the thread in a trivial way. The Thread
class provides astop
method, but we shouldn't use it. At most, we can send a request to stop such a thread. The programmer, in the code of a separate thread, decides what to do with this fact. We send such a signal by calling the interrupt
method, available on theThread
class instance. Depending on the state of the thread, there are two possible situations:
- the thread throws an
InterruptedException
exception based on the signal -
the thread can check if it has received a signal (stop request) with the method:
isInterrupted
, where when called, the stop request not information is deletedinterrupted
, which, in addition to information whether a stop signal has been sent, also resets the status.
The following examples show how we can use these methods:
public class ThreadsExample {
public static void main(String[] args) {
final Thread sleepingThread = new Thread(new SleepingThread());
sleepingThread.start();
sleepingThread.interrupt(); // sending a stop request
}
}
class SleepingThread implements Runnable {
@Override
public void run() {
System.out.println("I will go to sleep");
try {
Thread.sleep(3000L);
} catch (InterruptedException e) { // catching an InterruptedException if an interrupt signal was sent while the sleep method is executing
System.out.println("I was interrupted during sleep");
}
System.out.println("I am exiting");
}
}
The possible (and most likely) output of the program above is:
I will go to sleep
I was interrupted during sleep
I am exiting
The next example creates a separate thread that checks during its execution whether a stop request has been sent in the meantime:
public class ThreadsExample {
public static void main(String[] args) {
final Thread sleepingThread = new Thread(new SleepingThread());
sleepingThread.start();
sleepingThread.interrupt();
}
}
class SleepingThread implements Runnable {
@Override
public void run() {
final List<Integer> ints = new ArrayList<>();
for (int idx = 0; idx < 1000; idx++) {
ints.add(new Random().nextInt());
}
if (Thread.currentThread().isInterrupted()) { // or resetting the status of Thread.interrupted()
System.out.println("I was interrupted...");
return;
}
final int sum = ints.stream().mapToInt(value -> value).sum();
System.out.println("Sum is " + sum);
}
}
The example above will print the sum of the generated numbers to the screen, only if the interrupt ()
method is called after the signal is checked by the thread represented by the SleepingThread
class.
Synchronization¶
While creating a multi-threaded application, we must remember that in such an application:
- there is one heap, regardless of the number of threads
- each running thread creates a separate stack (stack)
Therefore, in a multithreaded application, we must take into account the fact that an object on the heap at one time can be changed by many threads. In order to avoid this, i.e. the object can be accessed only in a single thread at the same time, we can use the synchronization mechanism.
Java introduces two basic ways to synchronize:
- method synchronization
- code block synchronization
Both of the above methods are implemented using the synchronized
keyword.
The synchronization problem can be seen in the following code snippet. We create two threads in it that modify * the same * instance of the Pair class. At the end of each thread, we list the final values of the left
andright
fields to the screen. Even though we started the program with the values 0
and0
, respectively, and both threads incremented them 100 times, we most likely will not get the value 200
for these fields.
public class ThreadsExample {
public static void main(String[] args) {
final Pair pair = new Pair(0, 0);
new Thread(new DummyPairIncrementer(pair)).start();
new Thread(new DummyPairIncrementer(pair)).start();
}
}
class Pair {
private Integer left;
private Integer right;
public Pair(final Integer left, final Integer right) {
this.left = left;
this.right = right;
}
public void incrementLeft() {
left++;
}
public void incrementRight() {
right++;
}
public Integer getLeft() {
return left;
}
public Integer getRight() {
return right;
}
}
class DummyPairIncrementer implements Runnable {
private final Pair pair;
public DummyPairIncrementer(final Pair pair) {
this.pair = pair;
}
@Override
public void run() {
for (int idx = 0; idx < 100; idx++) {
pair.incrementLeft();
pair.incrementRight();
}
System.out.println(pair.getLeft() + " " + pair.getRight());
}
}
The problem described in the example is that increment is one instruction in the context of the code we are writing, but to the processor it is actually * three * instructions. Those are:
- (1) retrieving the current value from the memory
- (2) adding one to the downloaded value
- (3) save increased value to memory.
If one thread performs the operation (1), but does not yet perform the operation (3), and during this time the other thread manages to perform the operation (1), the two increments will result in increasing the value by 1, not by 2.
Synchronization of the method¶
In order to synchronize the method, we add the keyword synchronized
to its declaration. When a method is synchronized, the calling thread has exclusive access to it until it completes. To correct the problem in the previous example, we need to synchronize the following methods:
public synchronized void incrementLeft() {
left++;
}
public synchronized void incrementRight() {
right++;
}
Block synchronization¶
Block synchronization performs exactly the same mechanism as method synchronization, but it can reduce the scope of data synchronization only to individual instructions related to, e.g., a class field. In order to implement block synchronization, the synchronized
keyword is also used, but additionally between()
we insert the object that we want to access in a concurrency in a sequential manner.
If we decided to use code block synchronization in the synchronized methods from the previous example, we could change their implementation, e.g. to:
public void incrementLeft() {
System.out.println("Out of synchronized block");
synchronized (this) {
left++;
System.out.println("In synchronized block");
}
System.out.println("Out of synchronized block");
}
public void incrementRight() {
System.out.println("Out of synchronized block");
synchronized (this) {
right++;
System.out.println("In synchronized block");
}
System.out.println("Out of synchronized block");
}
NOTE: We should perform synchronization on the final objects/fields.
Join¶
We often use additional threads in applications to calculate certain data, which we then process, e.g. in the main thread. Before we can start processing, we are forced to wait for all threads enumerating data to finish. To wait for the thread to end, we need to use the join
method. Overloads are available:
- no argument, waiting for the thread to exit
- versions with arguments, where we can give the number of milliseconds (and optionally nanoseconds), meaning the maximum waiting time for the thread to terminate.
Another example shows how we can use the join
method:
public class ThreadsExample {
public static void main(String[] args) throws InterruptedException {
final List<Integer> ints = new ArrayList<>();
final Thread threadA = new Thread(new SimpleThread(ints));
final Thread threadB = new Thread(new SimpleThread(ints));
threadA.start();
threadB.start();
threadA.join(1000L);
threadB.join(1000L);;
System.out.println(ints.size());
}
}
class SimpleThread implements Runnable {
private final List<Integer> ints;
SimpleThread(final List<Integer> ints) {
this.ints = ints;
}
@Override
public void run() {
synchronized (this.ints) {
ints.add(new Random().nextInt());
}
}
}
Deadlock¶
In a situation where several threads block each other indefinitely, this is called a deadlock. The program is unable to complete the indicated operation due to the permanent mutual locking of resources. It can be described as follows:
- A is waiting for B because:
- B waits for A.
Deadlock is presented in the example below:
public class DeadLockExample {
public static void main(String[] args) throws InterruptedException {
final String r1 = "r1";
final String r2 = "r2";
Thread t1 = new Thread() {
public void run() {
synchronized (r1) {
System.out.println("Thread 1: Locked r1");
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
synchronized (r2) {
System.out.println("Thread 1: Locked r2");
}
}
}
};
Thread t2 = new Thread() {
public void run() {
synchronized (r2) {
System.out.println("Thread 2: Locked r1");
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
synchronized (r1) {
System.out.println("Thread 2: Locked r2");
}
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Exiting? No I will never reach this line of code because threads will NOT join");
}
}
-
The thread
t1
is blocking access to the resourcer1
, then tries to force access to the resourcer2
. -
The thread
t2
blocks access to the resourcer2
, then tries to access the resourcer1
.
Since neither thread t1
frees access to resourcer1
, nor does thread t2
frees access tor2
, this causes a so-called Deadlock
.
Thread coordination¶
The synchronized
keyword is used to prevent unwanted interaction of threads. However, it is not a sufficient measure to ensure that the threads work together. Often times, there may be a need not to perform a specific operation until a certain condition is met.
Queue<Runnable> runnableQueue = new LinkedList<>();
while (consumerQueue.isEmpty()) {
// waiting, waiting and still waiting for something to appear
}
actionQueue.poll().run();
The above code fragment addresses the presented problem, but it is also very ineffective, because it executes continuously while waiting. The same problem can be solved with the wait
andnotify
/notifyAll
methods.
wait¶
The thread calls the wait
method on the given object when it expects something to happen (usually in the context of that object), e.g. an object state change to be performed by another thread and which is implemented e.g. by changing the value of some variable - object fields). Calling the wait
method blocks the thread, and the method on which the operation is being called must be synchronized. Another thread can change the state of the object and notify the waiting thread about it (using the notify
or notifyAll
method).
Queue<Runnable> runnableQueue = new LinkedList<>();
while (runnableQueue.isEmpty()) {
try {
wait();
} catch (InterruptedException e) {
System.err.println("Oops");
}
}
runnableQueue.poll().run();
notify and notifyAll¶
The object is unblocked when another thread calls the notify
ornotifyAll
method for the same object where the thread is waiting:
- Calling
notify
unblocks one of the waiting threads, which can be any of them. - The
notifyAll
method unblocks all threads waiting on the object. - The call to
notify
ornotifyAll
must be in a synchronized block / method.
Thread coordination diagram¶
public class ThreadsExample {
public static void main(String[] args) throws InterruptedException {
final Customer customer = new Customer();
final Thread withDrawThread = new Thread(new WithdrawThread(customer));
final Thread depositThreadA = new Thread(new DepositThread(customer));
final Thread depositThreadB = new Thread(new DepositThread(customer));
withDrawThread.start();
depositThreadA.start();
depositThreadB.start();
}
}
class Customer {
private int availableAmount = 0;
synchronized void withdraw(int amountToWithdraw) {
System.out.println("Trying to withdraw " + amountToWithdraw + " PLN");
while (availableAmount < amountToWithdraw) {
System.out.println("Not enough money! Waiting for transfer!");
try {
wait();
} catch (InterruptedException e) {
System.err.println("Oops");
}
}
System.out.println("Withdraw successful!");
}
synchronized void deposit(final int amountToDeposit) {
System.out.println("Depositing " + amountToDeposit + " PLN");
availableAmount += amountToDeposit;
notify();
}
}
class WithdrawThread implements Runnable {
private final Customer customer;
WithdrawThread(final Customer customer) {
this.customer = customer;
}
@Override
public void run() {
customer.withdraw(1000);
}
}
class DepositThread implements Runnable {
private final Customer customer;
DepositThread(final Customer customer) {
this.customer = customer;
}
@Override
public void run() {
customer.deposit(500);
}
}
In the example above, the WithdrawThread
thread is paused (wait
) until you have sufficient funds in your account. Each payment will trigger the thread (notify
). The thread WithdrawThread
will not finish running until the givenCustomer
has the required funds.
Callable and Future¶
A generic function interface representing a task that can return either a result or an exception with the argumentless call ()
method. The Callable interface is similar to the Runnable
interface, except that therun
method cannot return any result. Both interfaces are similar to each other due to their potential use in multithreaded service.
public class GetRequest implements Callable<String> {
@Override
public String call() throws Exception {
return "Dummy http response";
}
}
The Future
is the interface that represents the future result of the async method, which will eventually be returned in the future after the operation has finished processing. The operation operation value can be retrieved using the get ()
method, which works similarly to the join
method in theThread
class, ie it blocks the current thread and waits for the expected result to be available.
ExecutorService¶
When creating multi-threaded applications, we rarely use a low-level API and manage threads manually. Whenever possible, we should use the so-called thread pool, which is a group of threads managed by an external entity. One of such mechanisms in Java is the ExecutorService
interface, which simplifies the execution of tasks in asynchronous mode, using a pool of threads for this. To create an ExecutorService
instance, we can use a factory, theExecutors
class, which has some useful static methods. The basic ones are:
newSingleThreadExecutor()
- returnsExecutorService
running on one threadnewFixedThreadPool(int nThreads)
- returnsExecutorService
running on the thread pool of the given size.
In addition, we also have:
newCachedThreadPool()
- creates anExecutorService
, which in the absence of a thread could handle a new task, adds a new thread to the pool. Additionally, threads are removed from the pool if it does not get a new task to be performed for one minute.newScheduledThreadPool(int corePoolSize)
- creates anExecutorService
that starts the task after a certain time or at specified intervals.
The code below shows the different ways to create different ExecutorService
instances:
public class ExecutorsCreationExample {
public static void main(String[] args) throws InterruptedException {
final int cpus = Runtime.getRuntime().availableProcessors();
final ExecutorService singleThreadES = Executors.newSingleThreadExecutor(); // single thread pool
final ExecutorService executorService = Executors.newFixedThreadPool(cpus); // pool with threads equal to cpu
final ExecutorService cachedES = Executors.newCachedThreadPool(); // cached thread pool
final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(cpus); // scheduled thread pool with cpu equal number of threads
}
}
Closing the ExecutorService¶
When creating an ExecutorService
, we must remember to manually close it. The following methods are used for this:
shutdown()
- the thread pool will stop accepting new tasks, those started will be completed, and then the pool will be closedshutdownNow()
- Similar toshutdown
,ExecutorService
will stop accepting new tasks, in addition it tries to stop all active tasks, stops processing pending tasks and returns a list of tasks waiting for execution.
Performing tasks¶
In order to perform a task on a thread from the pool, we can use the following methods:
submit()
- performs aCallable
orRunnable
task, e.g .:
public class CallableFutureExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor(); // creating an ExecutorService with a single-threaded pool
Future<String> result = executorService.submit(() -> "I am result of callable!"); // Callable implementation using lambda
try {
System.out.println("Prinint result of the future: " + result.get());
} catch (InterruptedException | ExecutionException e) {
System.err.println("Oops");
}
executorService.shutdown(); // remember to close the ExecutorService manually
}
}
invokeAny()
-ExecutorService
in its thread pool starts executing the list of input jobs. Returns the result of tasks that were started that were successfully completed when the first completed successfully. The remaining unfinished tasks will be canceled.
This behavior is illustrated by another example:
public class HomeTasks {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
List<Callable<String>> tasks = Arrays.asList(
() -> {
System.out.println("Thread: " + Thread.currentThread().getName());
System.out.println("I'm shopping");
Thread.sleep(5000);
System.out.println("Thread: " + Thread.currentThread().getName() + ". Shopping done!");
return "Shopping done!";
},
() -> {
System.out.println("Thread: " + Thread.currentThread().getName());
System.out.println("Washing dishes");
Thread.sleep(2000);
System.out.println("Thread: " + Thread.currentThread().getName() + ". Dishes washed");
return "dishes washed";
},
() -> {
System.out.println("Thread: " + Thread.currentThread().getName());
System.out.println("Cleaning the room");
Thread.sleep(1000);
System.out.println("Thread: " + Thread.currentThread().getName() + ". Room cleaned");
return "Room cleaned";
}
);
try {
String firstResult = executorService.invokeAny(tasks);
System.out.println("FIRST RESULT: " + firstResult);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
executorService.shutdown();
}
}
Possible output from the previous example, e.g.:
Thread: pool-1-thread-2
Washing dishes
Thread: pool-1-thread-1
I'm shopping
Thread: pool-1-thread-2. Dishes washed
Thread: pool-1-thread-2
Cleaning the room
PIERWSZY WYNIK: Dishes washed
Note that 'Room cleaned' was not displayed on the screen as the result of the first completed task was returned faster. The first result is "washed dishes". This fact is due to the fact that our pool has fewer threads than the number of tasks we want to run. Hence, despite the fact that the third Callable
sleeps the shortest, it will start executing only when one of the threads from the pool is free, i.e. it finishes performing an active task.
invokeAll
- executes all of theCallable
tasks and returns a result list of typeList <Future <T>>
.
Let's adapt the code from the previous example to execute the invokeAll
method instead ofinvokeAny
:
public class HomeTasks {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
List<Callable<String>> tasks = Arrays.asList(
() -> {
System.out.println("Thread: " + Thread.currentThread().getName());
System.out.println("I'm shopping");
Thread.sleep(5000);
System.out.println("Thread: " + Thread.currentThread().getName() + ". Shopping done!");
return "Shopping done!";
},
() -> {
System.out.println("Thread: " + Thread.currentThread().getName());
System.out.println("Washing dishes");
Thread.sleep(2000);
System.out.println("Thread: " + Thread.currentThread().getName() + ". Dishes washed");
return "Dishes washed";
},
() -> {
System.out.println("Thread: " + Thread.currentThread().getName());
System.out.println("Cleaning the room");
Thread.sleep(1000);
System.out.println("Thread: " + Thread.currentThread().getName() + ". Room cleaned");
return "Room cleaned";
}
);
try {
List<Future<String>> futures = executorService.invokeAll(tasks);
for (Future<String> future : futures) {
System.out.println(future.get());
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
executorService.shutdown();
}
}
Example output:
Thread: pool-1-thread-1
I'm shopping
Thread: pool-1-thread-2
Washing dishes
Thread: pool-1-thread-2. Dishes washed
Thread: pool-1-thread-2
Cleaning the room
Thread: pool-1-thread-2. Room cleaned
Thread: pool-1-thread-1. Shopping done!
Shopping done!
Dishes washed
Room cleaned
Note that all tasks will always be completed.
ScheduledExecutorService¶
This implementation of ExecutorService
allows you to schedule an operation to run after a certain time or interval. Within the methods of this ExecutorService
implementation, we can distinguish the following methods:
scheduleAtFixedRate
scheduleWithFixedDelay
Each of the above methods returns a special object: ScheduledFuture
, which [inherits] (oop.md # inherit) from the behavior of theFuture
class and, apart from being able to cancel the task, allows you to return the time remaining until the next operation is performed.
scheduleAtFixedRate¶
This method allows you to perform a given action with a delay, and then cyclically every certain period of time, e.g.
public class SchedulerExecutorDemo {
public static void main(String[] args) throws InterruptedException {
DateFormat df = new SimpleDateFormat("dd:MM:yy:HH:mm:ss");
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.scheduleAtFixedRate(() -> {
System.out.println("Start coffee!: " + df.format(Calendar.getInstance().getTime()));
try {
Thread.sleep(5000);
System.out.println("finish coffee!: " + df.format(Calendar.getInstance().getTime()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 1, 6, TimeUnit.SECONDS);
Thread.sleep(15000L);
executorService.shutdown();
}
}
For the example above, each new task execution will start one second after the preceding one completes, the tasks will stop running when we call the shutdown
method.
scheduleWithFixedDelay¶
The scheduleWithFixedDelay
method, similar to thescheduleAtFixedRate
, allows you to perform a given action with a delay, and then cyclically at a certain period of time. The only difference is that the time interval is counted from the end of the preceding task, not the beginning.
public class ScheduledWithFixedDelayDemo {
public static void main(String[] args) throws InterruptedException {
DateFormat df = new SimpleDateFormat("dd:MM:yy:HH:mm:ss");
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.scheduleWithFixedDelay(() -> {
System.out.println("Start coffee!: " + df.format(Calendar.getInstance().getTime()));
try {
Thread.sleep(5000);
System.out.println("finish coffee!: " + df.format(Calendar.getInstance().getTime()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 1, 6, TimeUnit.SECONDS);
Thread.sleep(15000L);
executorService.shutdown();
}
}
For the above example, each new task execution will start 6 seconds after the previous one completes.
Atomic Variables¶
[Earlier] (# Synchronization) we discussed that an operation such as increment from the Java perspective is a single expression, but for the processor it is several operations. We say that an operation is atomic if, while it is being performed, another thread cannot read or change the values of the variables being changed. The java.util.concurrent.atomic
package defines classes that handle atomic operations on single variables. The classes of the Atomics group provide a set of synchronized
operations, and the objects themselves can be safely shared between multiple threads. The types discussed are, for example:
AtomicInteger
AtomicLong
AtomicBoolean
public class AtomicsDemo {
public static void main(String[] args) {
final ExecutorService executorService = Executors.newFixedThreadPool(2);
final AtomicInteger atomicInteger = new AtomicInteger(0);
executorService.submit(new IncrementingThread(atomicInteger));
executorService.submit(new IncrementingThread(atomicInteger));
executorService.shutdown();
}
}
class IncrementingThread implements Runnable {
private final AtomicInteger value;
IncrementingThread(final AtomicInteger value) {
this.value = value;
}
@Override
public void run() {
for (int idx = 0; idx < 1000; idx++) {
value.incrementAndGet();
}
System.out.println(value.get()); // the slower thread will always print 2000
}
}
volatile¶
Simply put, by creating a variable in the program, it can be stored in the main program memory or for optimization in the processor's memory (so-called L2 Cache). In multithreaded applications, it is possible that the value of a variable stored in the processor memory is different than that stored in the main memory. The one in main memory may be an obsolete value, and the current value in the processor's memory is not available for some threads, so our application may not work as expected.
The problem described above can be solved by marking such a variable with the keyword volatile
, which means that the value will always be stored only in the main memory of the application. Importantly, volatile
does not guarantee the atomicity of the operation, ie it is a way of synchronization that produces no less than atomic variables or thesynchronized
keyword.
Another example shows the use of the volatile
keyword:
public class VolatileDemo {
public static volatile boolean shouldStop = false;
public static void main(String[] args) throws InterruptedException {
final ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(new VolatileThread());
while (!shouldStop) {
Thread.sleep(100L);
System.out.println("Waiting for signal to stop checking that volatile boolean");
}
executorService.shutdown();
}
}
class VolatileThread implements Runnable {
@Override
public void run() {
System.out.println("Starting some processing");
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
System.err.println("Oops");
}
System.out.println("Processing finished");
VolatileDemo.shouldStop = true;
}
}
NOTE: We can only use the
volatile
keyword when defining class variables.