Chapter 13 Concurrency Flashcards
Introducing Threads
- A
thread
is the smallest unit of execution that can be scheduled by the operating system. - A
process
is a group of associated threads that execute in the same shared environment. - It follows, then, that a
single-threaded process
is one that contains exactly one thread, - whereas a
multithreaded process
supports more than one thread. - By
shared environment
, we mean that the threads in the same process share the same memory space and can communicate directly with one another. - A
task
is a single unit of work performed by a thread. - A thread can complete multiple independent tasks but only one task at a time.
- By
shared memory
, we are generally referring to static variables as well as instance and local variables passed to a thread.
thread
A thread
is the smallest unit of execution that can be scheduled by the operating system.
process
A process
is a group of associated threads that execute in the same shared environment.
task
A task
is a single unit of work performed by a thread.
shared environment
shared environment
, we mean that the threads in the same process share the same memory space and can communicate directly with one another.
shared memory
shared memory
, we are generally referring to static variables as well as instance and local variables passed to a thread.
Understanding Thread Concurrency
- The property of executing multiple threads and processes at the same time is referred to as concurrency.
- Operating systems use a thread scheduler to determine which threads should be currently executing
- A context switch is the process of storing a thread’s current state and later restoring the state of the thread to continue execution.
- Finally, a thread can interrupt or supersede another thread if it has a higher thread priority than the other thread. A thread priority is a numeric value associated with a thread that is taken into consideration by the thread scheduler when determining which threads should currently be executing. In Java, thread priorities are specified as integer values.
How does the system decide what to execute when there are more threads available than CPUs?
Operating systems use a thread scheduler
to determine which threads should be currently executing, as shown in Figure 13.1. For example, a thread scheduler may employ a round-robin schedule
in which each available thread receives an equal number of CPU cycles with which to execute, with threads visited in a circular order.
context switch
A context switch
is the process of storing a thread’s current state and later restoring the state of the thread to continue execution.
thread priority
A thread priority
is a numeric value
associated with a thread that is taken into consideration by the thread scheduler when determining which threads should currently be executing. In Java, thread priorities are specified as integer
values.
Creating a Thread
**** One of the most common ways to define a task for a thread is by using the *Runnable
instance.
* Runnable is a functional interface that takes no arguments and returns no data.
@FunctionalInterface public interface Runnable { void run(); }
With this, it’s easy to create and start a thread.
new Thread(() -> System.out.print("Hello")).start(); System.out.print("World");
Remember that order of thread execution is not often guaranteed. The exam commonly presents questions in which multiple tasks are started at the same time, and you must determine the result.
Calling run() Instead of start()
System.out.println("begin"); new Thread(printInventory).run(); new Thread(printRecords).run(); new Thread(printInventory).run(); System.out.println("end");
Calling run()
on a Thread
or a Runnable
does not start a new thread.
we can create a Thread and its associated task one of two ways in Java:
More generally, we can create a Thread and its associated task one of two ways in Java:
- Provide a
Runnable
object or lambda expression to the Thread constructor. - Create a class that extends
Thread
and overrides therun()
method.
Creating a class that extends Thread is relatively uncommon and should only be done under certain circumstances, such as if you need to overwrite other thread methods.
Distinguishing Thread Types
- A
system thread
is created by the Java Virtual Machine (JVM) and runs in the background of the application.- For example, garbage collection is managed by a system thread created by the JVM.
- Alternatively, a
user-defined thread
is one created by the application developer to accomplish a specific task. - System and user-defined threads can both be created as
daemon threads
.- A
daemon thread
is one that will not prevent the JVM from exiting when the program finishes. - A Java application terminates when the only threads that are running are daemon threads.
- For example, if garbage collection is the only thread left running, the JVM will automatically shut down.
- A
by default, user-defined threads
are not daemons
, and the program will wait for them to finish.
set thread as daemon, before start()
job.setDaemon(true);
Managing a Thread’s Life Cycle
You can query a thread’s state by calling getState()
on the thread object.
- Every thread is initialized with a NEW state.
- As soon as start() is called, the thread is moved to a RUNNABLE state.
- Does that mean it is actually running?
Not exactly: it may be running, or it may not be. - The RUNNABLE state just means the thread is able to be run.
- Does that mean it is actually running?
- Once the work for the thread is completed (
run()
completes) or an uncaught exception is thrown, the thread state becomes TERMINATED, and no more work is performed. - While in a RUNNABLE state, the thread may transition to one of three states where it pauses its work:
- BLOCKED,
- WAITING,
- or
TIMED_WAITING
.
This figure includes common transitions between thread states, but there are other possibilities. For example, a thread in a WAITING state might be triggered by notifyAll()
.
Likewise, a thread that is interrupted by another thread will exit TIMED_WAITING
and go straight back into RUNNABLE.
Managing a Thread’s Life Cycle
You can query a thread’s state by calling getState()
on the thread object.
- Every thread is initialized with a NEW state.
- As soon as start() is called, the thread is moved to a RUNNABLE state. It may be running, or it may not be. The RUNNABLE state just means the thread is able to be run.
- Once the work for the thread is completed (
run()
completes) or an uncaught exception is thrown, the thread state becomes TERMINATED, and no more work is performed. - While in a RUNNABLE state, the thread may transition to one of three states where it pauses its work:
- BLOCKED, (Waiting to enter synchronized block)
- WAITING, (Waiting indefinitely to be notified)
- or
TIMED_WAITING
. (Waiting a specified time)
A thread that is interrupted by another thread will exit TIMED_WAITING
and go straight back into RUNNABLE.
FIGURE 13.2 Thread states
Managing a Thread’s Life Cycle
- NEW (thread created but no started )
-
RUNNABLE (running or able to be run)
- BLOCKED (waiting to enter synchronized block)
- WAITING (waiting indefinitely to be notified)
- TIMED_WAITING (waiting a specified time)
- TERMINATED (task complete)
create thread -> NEW - start()
-> RUNNABLE - run()
completes-> TERMINATED
RUNNABLE -resource requested -> BLOCKED
RUNNABLE <-resource granted- BLOCKED
RUNNABLE -wait() -> WAITING
RUNNABLE <-notify() - WAITING
RUNNABLE -sleep() -> TIMED_WAITING
RUNNABLE <-time elapsed - TIMED_WAITING
Polling with Sleep
Even though multithreaded programming allows you to execute multiple tasks at the same time, one thread often needs to wait for the results of another thread to proceed. One solution is to use polling. Polling is the process of intermittently checking data at some fixed interval.
We can improve this result by using the Thread.sleep() method to implement polling and sleep for 1,000 milliseconds, aka 1 second:
public class CheckResultsWithSleep { private static int counter = 0; public static void main(String[] a) { new Thread(() -> { for(int i = 0; i < 1_000_000; i++) counter++; }).start(); while(counter < 1_000_000) { System.out.println("Not reached yet"); try { Thread.sleep(1_000); // 1 SECOND } catch (InterruptedException e) { System.out.println("Interrupted!"); } } System.out.println("Reached: "+counter); } }
Polling
Polling
is the process of intermittently checking data at some fixed interval.
Interrupting a Thread
One way to improve this program is to allow the thread to interrupt the main() thread when it’s done:
public class CheckResultsWithSleepAndInterrupt { private static int counter = 0; public static void main(String[] a) { final var mainThread = Thread.currentThread(); new Thread(() -> { for(int i = 0; i < 1_000_000; i++) counter++; mainThread.interrupt(); }).start(); while(counter < 1_000_000) { System.out.println("Not reached yet"); try { Thread.sleep(1_000); // 1 SECOND } catch (InterruptedException e) { System.out.println("Interrupted!"); } } System.out.println("Reached: "+counter); } }
final var mainThread = Thread.currentThread();
mainThread.interrupt();
Calling interrupt() on a thread in the TIMED_WAITING or WAITING state causes the main() thread to become RUNNABLE again, triggering an InterruptedException. The thread may also move to a BLOCKED state if it needs to reacquire resources when it wakes up.
> [!NOTE]
Calling interrupt() on a thread already in a RUNNABLE state doesn’t change the state.
In fact, it only changes the behavior if the thread is periodically checking the Thread.isInterrupted() value state.
interrupt()
Thread.isInterrupted()
Creating Threads with the Concurrency API
java.util.concurrent
- The Concurrency API includes the
ExecutorService interface
, which defines services that create and manage threads.- You first obtain an instance of an ExecutorService interface,
- and then you send the service tasks to be processed.
- The framework includes numerous useful features, such as thread pooling and scheduling.
- It is recommended that you use this framework any time you need to create and execute a separate task, even if you need only a single thread.
Introducing the Single-Thread Executor
ExecutorService service = Executors.newSingleThreadExecutor(); try { System.out.println("begin"); service.execute(printInventory); service.execute(printRecords); service.execute(printInventory); System.out.println("end"); } finally { service.shutdown(); }
Possible output:
begin Printing zoo inventory Printing record: 0 Printing record: 1 end Printing record: 2 Printing zoo inventory
- The Concurrency API includes the
Executors
factory class that can be used to create instances of the ExecutorService object. - we use the newSingleThreadExecutor() method to create the service.
- With a single-thread executor, tasks are guaranteed to be executed sequentially.
- Notice that the end text is output while our thread executor tasks are still running.
- This is because the main() method is still an independent thread from the ExecutorService.
Shutting Down a Thread Executor
- Once you have finished using a thread executor, it is important that you call the shutdown() method.
- A thread executor creates a non-daemon thread on the first task that is executed, so failing to call
shutdown()
will result in your applicationnever terminating
. - The shutdown process for a thread executor involves
- first rejecting any new tasks submitted to the thread executor while continuing to execute any previously submitted tasks.
- During this time, calling isShutdown() will return true, while isTerminated() will return false.
- If a new task is submitted to the thread executor while it is shutting down, a RejectedExecutionException will be thrown.
- Once all active tasks have been completed, isShutdown() and isTerminated() will both return true. Figure 13.3 shows the life cycle of an ExecutorService object.
For the exam, you should be aware that shutdown() does not stop any tasks that have already been submitted to the thread executor.
What if you want to cancel all running and upcoming tasks?
The ExecutorService provides a method called shutdownNow()
, which attempts to stop all running tasks and discards any that have not been started yet. It is not guaranteed to succeed because it is possible to create a thread that will never terminate, so any attempt to interrupt it may be ignored.
Submitting Tasks
You can submit tasks to an ExecutorService instance multiple ways.
1. The first method we presented, execute(), is inherited from the Executor interface, which the ExecutorService interface extends. The execute() method takes a Runnable instance and completes the task asynchronously. Because the return type of the method is void, it does not tell us anything about the result of the task. It is considered a “fire-and-forget” method, as once it is submitted, the results are not directly available to the calling thread.
2. Fortunately, the writers of Java added submit() methods to the ExecutorService interface, which, like execute(), can be used to complete tasks asynchronously. Unlike execute(), though, submit() returns a Future instance that can be used to determine whether the task is complete. It can also be used to return a generic result object after the task has been completed.
-
void execute(Runnable command)
Executes Runnable task at some point in future. -
Future<?> submit(Runnable task)
Executes Runnable task at some point in future and returns Future representing task. -
<T> Future<T> submit(Callable<T> task)
Executes Callable task at some point in future and returns Future representing pending results of task. -
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
Executes given tasks and waits for all tasks to complete. Returns List of Future instances in same order in which they were in original collection. -
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
Executes given tasks and waits for at least one to complete.
Submitting Tasks: execute() vs. submit()
The submit()
method have a return object that can be used to track the result.
void execute(Runnable command)
Executes the given command at some time in the future.
Future<?> submit(Runnable task)
Submits a Runnable task for execution and returns a Future representing that task.
The Future’s get method will return null upon successful completion.
<T> Future<T> submit(Runnable task, T result)
Submits a Runnable task for execution and returns a Future representing that task.
How do we know when a task submitted to an ExecutorService is complete?
Future<V>
instance that can be used to determine this result.
Future<?> future = service.submit(() -> System.out.println("Hello"));
The Future type is actually an interface.
-
boolean isDone()
Returns true if task was completed, threw exception, or was
Future methods
-
boolean isDone()
Returns true if task was completed, threw exception, or was cancelled. -
boolean isCancelled()
Returns true if task was cancelled before it completed normally. -
boolean cancel(boolean mayInterruptIfRunning)
Attempts to cancel execution of task and returns true if it was successfully cancelled or false if it could not be cancelled or is complete. -
V get()
Retrieves result of task, waiting endlessly if it is not yet available. -
V get(long timeout, TimeUnit unit)
Retrieves result of task, waiting specified amount of time. If result is not ready by time timeout is reached, checked TimeoutException will be thrown.
import java.util.concurrent.*; public class CheckResults { private static int counter = 0; public static void main(String[] unused) throws Exception { ExecutorService service = Executors.newSingleThreadExecutor(); try { Future<?> result = service.submit(() -> { for(int i = 0; i < 1_000_000; i++) counter++; }); result.get(10, TimeUnit.SECONDS); // Returns null for Runnable System.out.println("Reached!"); } catch (TimeoutException e) { System.out.println("Not reached in time"); } finally { service.shutdown(); } } }
- The
Executors
class provides factory methods for the executor services provided in this package. -
ExecutorService service = Executors.newSingleThreadExecutor();
Creates an Executor that uses a single worker thread operating off an unbounded queue. -
Future<?> result = service.submit(() -> { for(int i = 0; i < 1000000; i++) counter++;});
Since the return type of Runnable.run() is void, the get() method always returns null when working with Runnable expressions. -
result.get(10, TimeUnit.SECONDS); // Returns null for Runnable
It also waits at most 10 seconds, throwing a TimeoutException on the call toresult.get()
if the task is not done. -
<T> Future<T> submit(Runnable task, T result)
Submits a Runnable task for execution and returns a Future representing that task. The Future’s get method will return the given result upon successful completion.
java.util.concurrent.TimeUnit
Concurrency API use this enum
TimeUnit enum
-
TimeUnit.NANOSECONDS
Time in one-billionths of a second (1/1,000,000,000) -
TimeUnit.MICROSECONDS
Time in one-millionths of a second (1/1,000,000) -
TimeUnit.MILLISECONDS
Time in one-thousandths of a second (1/1,000) -
TimeUnit.SECONDS
Time in seconds -
TimeUnit.MINUTES
Time in minutes -
TimeUnit.HOURS
Time in hours -
TimeUnit.DAYS
Time in days
Introducing Callable
java.util.concurrent.Callable functional interface
-
call()
method returns a value and can throw a checked exception.
~~~
@FunctionalInterface public interface Callable<V> {
V call() throws Exception;
}
~~~</V> - The Callable interface is often **preferable over Runnable, **since it allows more details to be retrieved easily from the task after it is completed.
Example using Callable:
var service = Executors.newSingleThreadExecutor(); try { Future<Integer> result = service.submit(() -> 30 + 11); System.out.println(result.get()); // 41 } finally { service.shutdown(); }
Callable
Waiting for All Tasks to Finish
If we don’t need the results of the tasks and are finished using our thread executor, there is a simpler approach.
- First, we shut down the thread executor using the
shutdown()
method. - Next, we use the
awaitTermination()
method available for all thread executors. The method waits the specified time to complete all tasks, returning sooner if all tasks finish or anInterruptedException
is detected. - call
isTerminated()
after theawaitTermination()
method finishes to confirm that all tasks are finished.
ExecutorService service = Executors.newSingleThreadExecutor(); try { // Add tasks to the thread executor … } finally { service.shutdown(); } service.awaitTermination(1, TimeUnit.MINUTES); // Check whether all tasks are finished if(service.isTerminated()) System.out.println("Finished!"); else System.out.println("At least one task is still running");
Scheduling Tasks
Creates a thread pool that can schedule commands to run after a given delay, or to execute periodically.
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
ScheduledExecutorService methods
-
schedule(Callable<V> callable, long delay, TimeUnit unit)
Creates and executes Callable task after given delay -
schedule(Runnable command, long delay, TimeUnit unit)
Creates and executes Runnable task after given delay -
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
Creates and executes Runnable task after given initial delay, creating new task every period value that passes -
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
Creates and executes Runnable task after given initial delay and subsequently with given delay between termination of one execution and commencement of next
scheduleWithFixedDelay()
scheduleWithFixedDelay()
method creates a new task only after the previous task has finished.
For example, if a task runs at 12:00 and takes five minutes to finish, with a period between executions of two minutes, the next task will start at 12:07.
service.scheduleWithFixedDelay(task1, 0, 2, TimeUnit.MINUTES);
Increasing Concurrency with Pools
A thread pool
is a group of pre-instantiated reusable threads that are available to perform a set of arbitrary tasks.
Executors factory methods
-
ExecutorService newSingleThreadExecutor()
Creates single-threaded executor that uses single worker thread operating off unbounded queue. Results are processed sequentially in order in which they are submitted. -
ScheduledExecutorService newSingleThreadScheduledExecutor()
Creates single-threaded executor that can schedule commands to run after given delay or to execute periodically. -
ExecutorService newCachedThreadPool()
Creates thread pool that creates new threads as needed but reuses previously constructed threads when they are available. -
ExecutorService newFixedThreadPool(int)
Creates thread pool that reuses fixed number of threads operating off shared unbounded queue. -
ScheduledExecutorService newScheduledThreadPool(int)
Creates thread pool that can schedule commands to run after given delay or execute periodically.
Writing Thread-Safe Code
-
Thread-safety
is the property of an object that guarantees safe execution by multiple threads at the same time. - Since threads run in a shared environment and memory space, how do we prevent two threads from interfering with each other?
We must organize access to data so that we don’t end up with invalid or unexpected results. - how to use a variety of techniques to protect data, including
atomic classes
,synchronized blocks
, theLock framework
, andcyclic barriers
.
Understanding Thread-Safety
1: import java.util.concurrent.*; 2: public class SheepManager { 3: private int sheepCount = 0; 4: private void incrementAndReport() { 5: System.out.print((++sheepCount)+" "); 6: } 7: public static void main(String[] args) { 8: ExecutorService service = Executors.newFixedThreadPool(20); 9: try { 10: SheepManager manager = new SheepManager(); 11: for(int i = 0; i < 10; i++) 12: service.submit(() -> manager.incrementAndReport()); 13: } finally { 14: service.shutdown(); 15: } } }
It may output in a different order. Worse yet, it may print some numbers twice and not print some numbers at all! The following are possible outputs of this program:
1 2 3 4 5 6 7 8 9 10
1 9 8 7 3 6 6 2 4 5
1 8 7 3 2 6 5 4 2 9
race condition
the unexpected result of two tasks executing at the same time is referred to as a race condition.
Accessing Data with volatile
The volatile
keyword is used to guarantee that access to data within memory is consistent.
The volatile
attribute ensures that only one thread is modifying a variable at one time and that data read among multiple threads is consistent.
3: private volatile int sheepCount = 0; 4: private void incrementAndReport() { 5: System.out.print((++sheepCount)+" "); 6: }
Unfortunately, this code is not thread-safe and could still result in numbers being missed:
2 6 1 7 5 3 2 9 4 8
The reason this code is not thread-safe is that ++sheepCount
is still two distinct operations.
Put another way, if the increment operator represents the expression sheepCount = sheepCount + 1
, then each read and write operation is thread-safe, but the combined operation is not. Referring back to our sheep example, we don’t interrupt the employee while running, but we could still have multiple people in the field at the same time.
> [!NOTE]
In practice, volatile is rarely used.
We only cover it because it has been known to show up on the exam from time to time.
volatile