banner
bladedragon

bladedragon

"Art of Concurrent Programming" - Reading Notes 04: Inter-thread Communication

Java Concurrency Basics#

Reading notes on Chapter 4 of "The Art of Concurrency", focusing on inter-thread communication.

image

1. Introduction to Threads#

First, let's have a brief introduction to threads.

Reasons for using multiple threads:

1. More processor cores: A thread can only run on one processor core at a time.
2. Faster response time
3. Better programming model

Threads required for running a Java program (jdk1.8)

image

Thread Priority

Operating systems generally use a time-sharing mechanism to schedule running threads. The operating system divides time into time slices, and each thread is allocated a certain number of time slices. When a thread's time slices are used up, the thread is rescheduled and waits for the next allocation. The number of time slices allocated to a thread determines how much processor resources the thread can use.

Some operating systems ignore thread priority settings.

In Java threads, the priority is controlled by an integer member variable priority. The priority ranges from 1 to 10. The priority can be modified using the setPriority(int) method when creating a thread. The default priority is 5. Threads with higher priority are allocated more time slices than threads with lower priority.

Thread States

There are 6 thread states, and a thread can only be in one state at a given time.

  • NEW: The initial state, the thread is created but not yet started by calling the start method.
  • RUNNABLE: The running state, in Java threads, both the ready and running states are referred to as the running state.
  • BLOCKED: The blocked state, indicates that the thread is blocked waiting for a lock.
  • WAITING: The waiting state, waiting for notification or interruption from other threads.
  • TIMED_WAITING: The timed waiting state, can return after a specified time.
  • TERMINATED: The terminated state, indicates that the thread has finished execution.

image

Thread State Transition

image

Java combines the running and ready states in the operating system into a single "running" state.

The "blocked" state is the state when a thread is blocked in a method or code block synchronized by the synchronized keyword (acquiring a lock).

However, in the java.concurrent package, the thread state of a thread blocked by the Lock interface is the waiting state, because the implementation of blocking in the Lock interface in the java.concurrent package uses the relevant methods in the LockSupport class.

Daemon Threads

Daemon threads are threads that provide a general service in the background while the program is running, such as garbage collection threads. They are not essential parts of the program.

When there are no non-daemon threads in a Java virtual machine, the virtual machine will exit. You can set a thread as a daemon thread by calling Thread.setDaemon(true), but it must be set before the thread starts.

Note: When constructing a daemon thread, you cannot rely on the logic in the finally block to ensure the execution of the shutdown or resource cleanup logic, as it may not be executed.

2. Starting and Terminating Threads#

A newly constructed thread object is allocated space by its parent thread, and the various attributes of the child thread are inherited from the parent thread, including a unique ID.

Take a look at the source code of Thread.init()
In the source code of jdk1.8, many judgments about ThreadGroup have been added, probably to make a more complete distinction between thread safety and exceptional situations.

Starting Threads: The start method is used to start a thread. When a thread calls this method, it synchronously informs the virtual machine that the thread should start immediately if the thread scheduler is idle. It is recommended to set the thread name before creating the thread, which is helpful for troubleshooting errors.

Interrupting Threads:

Interrupting a thread can be understood as a flag attribute of a running thread that indicates whether the thread has been interrupted by another thread. It is a convenient way for threads to interact with each other.
A thread can determine whether it has been interrupted by calling the isInterrupted() method, or it can call the static method Thread.interrupted() to reset the interrupt flag in the current thread.

  • When a thread is in a waiting state or a timed waiting state (TIMED_WAITING, WAITING), we can interrupt the thread's waiting by calling the interrupt() method, and the thread will throw an InterruptedException exception.
  • However, when a thread is in the BLOCKED state or the RUNNABLE (RUNNING) state, calling the interrupt() method can only set the thread's interrupt flag to true. Stopping the thread requires us to implement the logic ourselves.

Deprecated APIs: suspend(), resume(), stop()

Reasons: These methods can cause deadlock problems when occupying resources and make threads work in an uncertain state.

Terminating Threads: One way to terminate a thread is to use the interrupt() method, or use a boolean variable to terminate the thread, allowing the thread to have a chance to clean up resources when it terminates.

3. Inter-thread Communication (Focus)#

1. volatile and synchronized Keywords#

`volatile`: Indicates that any access to the variable needs to be obtained from the shared memory, and any changes to it must be synchronized and flushed back to the main memory. It ensures the visibility of variable access by all threads.
`synchronized`: Ensures that at any given time, only one thread can be in a method or synchronized block, ensuring the visibility and exclusivity of variable access. It does not guarantee reordering.

About synchronized: It is essentially the acquisition of a monitor (monitor) for an object, and this acquisition process is exclusive, which means that only one thread can acquire the monitor of an object at a time. Each object has its own monitor.

image

Any thread accessing an object must first acquire the monitor of the object. If the acquisition fails, the thread enters the synchronization queue and the thread state becomes BLOCKED. When the predecessor (the thread that has acquired the lock) releases the lock, the release operation wakes up the thread blocked in the synchronization queue, allowing it to reattempt to acquire the monitor.

2. Wait/Notify Mechanism (Producer-Consumer Model)#

The wait/notify mechanism refers to a situation where a thread A calls the wait method of an object O to enter a waiting state, and another thread B calls the notify or notifyAll method of object O. After receiving the notification, thread A returns from the wait method and continues with the subsequent operations.

image

The code example here is not valid, the wait thread is not properly awakened, and no reordering occurs.
The reason is that the locks obtained are not the same, resulting in the inability to unlock and lock properly. The synchronized object needs to be set as public static to ensure that the same object is obtained.
///The example is not about this issue, it is because there is no wait... 😂

image

Note:

(1) When using wait, notify, and notifyAll, the calling object must be locked first.

(2) After calling the wait method, the thread state changes from RUNNING to WAITING, and the current thread is placed in the waiting queue of the object.

(3) After the notify or notifyAll method is called, the waiting thread will not return from the wait method. The thread that calls notify or notifyAll needs to release the lock, and then the waiting thread has a chance to return from the wait method.

(4) The notify method moves the waiting thread from the waiting queue to the synchronization queue. The thread that is moved changes from WAITING to BLOCKED.

(5) The premise for returning from the wait method is to acquire the lock of the calling object.

3. Classic Wait/Notify Pattern#

Waiter: 1. Acquire the lock of the object.
         2. If the condition is not met, call the object's wait method, and after being notified, check the condition again.
         3. If the condition is met, execute the corresponding logic.
         Pseudocode:
               synchronized (object) {
                   while (condition is not met) {
                       object.wait();
                   }
                   Corresponding processing logic
               }

Notifier: 1. Acquire the lock of the object.
          2. Change the condition.
          3. Notify all threads waiting on the object.
          Pseudocode:
               synchronized (object) {
                   Change the condition
                   object.notify();
               }

4. Piped Input/Output Streams#

Piped input/output streams are different from regular file input/output streams or network input/output streams. They are mainly used for data transfer between threads, and the medium for transfer is memory.

PipedOutputStream, PipedInputStream (for byte data)

PipedReader, PipedWriter (for character data)

For piped type streams, you must call the connect method to bind them before using them, otherwise an exception will be thrown.

The default size of the input buffer for piped streams is 1024 bytes.

Detailed Explanation of Pipes

Note when writing sample code:
1. write writes, read reads.
2. Use System.out.print instead of System.out.println when printing. The latter will add a line break at the end.

5. Thread.join()#

Meaning: The current thread waits for the thread to terminate before returning from thread.join().
There are also two methods with timeout features: join(long millis) and join(long millis, int nanos) (if the thread does not terminate within the given time, the method will return).

join() is equivalent to wait(0), as long as the specified thread releases the lock, it can preempt the lock
If a specified time is added, the first time represents the wake-up time set by the parameter, and then the loop checks the thread liveness. If the thread is always alive, it starts to pass in the difference between the set value and the current time.

6. ThreadLocal#

ThreadLocal is a thread variable, which is a storage structure with a ThreadLocal object as the key and an arbitrary object as the value. This structure is attached to a thread, which means that a thread can query a value bound to it based on a ThreadLocal object.

It is a thread-safe local variable.

4. Application Examples#

1. Timeout Pattern#

When calling a method, wait for a period of time (usually a given time period). If the method can get a result within the given time period, return the result immediately; otherwise, return a default result.

public synchronized Object get(long mills) throws InterruptedException {
	long future = System.currentTimeMillis() + mills;
	long remaining = mills;
	// While the timeout is greater than 0 and the result does not meet the requirements
	while ((result == null) && remaining > 0) {
		wait(remaining);
    	remaining = future - System.currentTimeMillis();
	}
	return result;
}

Typical case: Database connection pool pattern

Code omitted

CountDownLatch:

  • The CountDownLatch class makes a thread wait until all other threads have finished executing.
  • It is implemented using a counter. The initial value of the counter is the number of threads. Each time a thread finishes executing, the counter is decremented. When the counter reaches 0, it means that all threads have finished executing, and the thread waiting on the latch can resume its work.

2. Thread Pool#

Clients can submit jobs to the thread pool for execution using the execute(Job) method, and the client itself does not need to wait for the job to complete. In addition to the execute(Job) method, the thread pool interface provides methods for increasing/decreasing worker threads and shutting down the thread pool.

Here, worker threads refer to threads that repeatedly execute jobs, and each job submitted by the client enters a work queue and waits for the worker threads to process.

The essence of a thread pool is to use a thread-safe work queue to connect worker threads and client threads. The client thread puts the task into the work queue and returns, while the worker thread continuously takes the work from the work queue and executes it.

典型案例:实现一个 web 服务器

Code omitted
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.