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.