Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.

Brief introduction:

When a thread starts running, it has its own stack space, and it follows the code step by step until it terminates, just like a script. However, if each thread is isolated from each other, then they have little value; On the other hand, if multiple threads can work together to complete the work, it will bring huge benefits in all aspects.

1. Volatile and synchronized keywords

Note :(without saying too much, you can read my history if necessary)

Java allows multiple threads to access an object or its member variables, and since each thread has a copy of the variable (for faster execution), the data read during program execution is often not up to date.

The keyword volatile can be used to modify a field (a member variable). Its effect, in plain English, is to tell the program that any access to the variable needs to be taken from shared memory, that changes to it must be flushed synchronously to shared memory, and that volatile ensures thread visibility to the variable.

The keyword synchronized can be used in the form of modifying methods or synchronized code blocks. It mainly ensures that multiple threads are in a method or synchronized code block at the same time, and only one thread is in the method or synchronized code block. It ensures the visibility and exclusivity of thread access to variables.

Through the use of javap tools to view the generated class file information to analyze the implementation details of synchronized keyword, the following code is the use of synchronization block and synchronization method.

Code examples:

package com.lizba.p3; /** * <p> * example code for synchronizing methods and code blocks * </p> ** @author: Liziba * @date: 2021/6/15 22:13 */ public class Synchronized {public static void main(String[] args) {// Synchronized (Synchronized. Class) {} // Static method(); } public static synchronized void method() {} }Copy the code

Run javap -v Synchronized. Class in the Synchronized. Class directory

javap -v Synchronized.class
Copy the code

Focus on some outputs:

  • Synchronized code blocks use the Monitorenter and Monitorexit directives

  • The synchronization method uses ACC_SYNCHRONIZED

Conclusion:

Synchronized code blocks and synchronized methods use different approaches to locking. Both essentially acquire a monitor of an object, and this acquisition process is exclusive, meaning that only one thread at a time acquires the monitor protected by synchronized. We know that every object has its own monitor lock, and when the object is called by a synchronized code block or a synchronized method of the object, the thread executing the method must acquire the monitor lock of the thread object before it can access the synchronized block or method. Threads that do not get the monitor (executing the method) are BLOCKED at the entrance to the synchronized code block and synchronized method.

Illustrates the relationship between objects, objects’ monitors, synchronization queues, and execution threads


To summarize the above:

Any thread accessing Object(protected by Synchronized) first obtains the Object monitor. If the fetch fails, the synchronization queue is entered and the thread becomes BLOCKED. When the precursor accessing Object (the thread that acquired the lock) releases the lock, the release wakes up the thread blocking in the synchronization queue to retry the acquisition of the monitor.

2. Wait notification mechanism

How does the producer-consumer pattern work between Java threads, where one thread modifies the value of an object and another object senses the change and acts accordingly?

The easiest way to do this:

Thread.sleep(1000); thread.sleep (1000); thread.sleep (1000); } doSomething();Copy the code

There are problems with the above code:

  1. Failing to ensure timeliness and releasing processor resources through sleep can lead to timeliness issues
  2. It is difficult to reduce overhead, and reducing sleep time to improve timeliness can lead to high processor resource overhead

Java built-in workaround:

These two seemingly contradictory problems are best solved by Java’s built-in wait/notification mechanism, which is available to any Java Object because these methods are defined in the Object superclass.

public final native void notify();
public final native void notifyAll();
public final void wait() throws InterruptedException
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException
Copy the code
Method names describe
notify() Notifies a thread waiting on an object to return from wait() if it has acquired the lock on the object
notifyAll() Notifies all threads waiting on the object
wait() The caller’s thread is in a WAITING state and will return only if it is notified or interrupted by another thread. Note that calling wait() releases the lock
wait(long) Timeout wait for a period of time, if the time is not notified, timeout return. Unit of ms
wait(long, int) More granular control over timeouts can be down to nanoseconds

Description of wait/notification mechanism:

The wait/notification mechanism refers to that thread A calls the wait() method of object O to enter the wait 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 of object O and performs subsequent operations. Wait () and notify()/notifyAll() on O act as a switch signal to complete the waiter-notifyall interaction (as in the original producer-consumer model)

Sample code:

package com.lizba.p3; import com.lizba.p2.SleepUtil; import java.text.SimpleDateFormat; import java.util.Date; / * * * < p > * wait () and notify ()/notifyAll () example code * < / p > * * @ Author: Liziba * @ the Date: 2021/6/15 23:28 */ public class WaitNotify { static boolean flag = true; static Object lock = new Object(); static final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); public static void main(String[] args) { Thread waitThread = new Thread(new Wait(), "waitThread"); waitThread.start(); SleepUtil.sleepSecond(1); Thread notifyThread = new Thread(new Notify(), "notifyThread"); notifyThread.start(); } /** * wait thread, Wait () */ static class implements Runnable{@override public void run() {// synchronized(lock) {// static class implements Runnable{@override public void run() When the conditions are not met, Println (thread.currentThread () + "flag is true. Wait at" +sdf.format(new Date())); Try {// This operation releases the lock lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); Println (thread.currentThread () + "flag is false. Finished at" + sdf.format(new Date()))); }} static class implements Runnable {@override public void run() {// synchronized (lock) {// static class implements Runnable {@override public void run() {// synchronized (lock) {// synchronized (lock) The thread waiting on the lock will not release the lock until the current thread has completed releasing the lock. Println (thread.currentThread () + "hold lock. notify at "+ sdf.format(new Date()))); lock.notifyAll(); flag = false; SleepUtil.sleepSecond(5); Synchronized (lock) {system.out.println (thread.currentThread () + "hold lock again.notify at "+ sdf.format(new Date())); SleepUtil.sleepSecond(5); }}}}Copy the code

View the output:

Note that the above hold Lock again and flag is flase lines may be executed in reverse order.

Conclusion:

  1. Using wait(), notify(), and notifyAll() requires locking the object first
  2. The thread changes from RUNNING to WAITING after calling wait(), and the current thread is placed on the object’s wait queue
  1. After notify() and notifyAll() are called, the waiting thread has to wait until the thread calling notify() and notifyAll() releases the lock before the thread in the waiting queue has a chance to return from wait()
  2. Notify () moves one thread from the wait queue to the synchronization queue, and notifyAll() moves all the wait threads from the wait queue to the synchronization queue. The moved thread changes from WAITING to BLOCKED
  1. The return from wait() is conditional upon obtaining the lock of the object
  2. The wait(), notify(), and notifyAll() mechanisms rely on synchronization so that threads returning from wait() are aware of changes made to variables by other threads

The above process is illustrated:

To summarize the above:

The WaitThread first acquires the lock and then calls the object’s wait() method to release the lock into the object’s wait queue. Because WaitThread releases the lock, NotifyThread then obtains the lock and calls notify() to move WaitThread from WaitQueue to SynchronizedQueue. The state of the WaitThread becomes blocked. After NotifyThread releases the lock, WaitThread acquires the lock again and returns from wait() to continue execution.

3. The classic paradigm of wait/notification

The classic wait/notification paradigm is divided into waiters and notifiers, which follow the following rules, respectively.

The waiting party follows the following rules:

  1. Gets the lock of the object
  2. If the condition is not met, the object’s wait() method is called and the condition is still checked after being notified
  1. If the conditions are met, the corresponding logic is executed
Synchronized (object) {while(condition not met) {object.wait (); } // ToDo... }Copy the code

The notifying party follows the following rules:

  • Gets the lock of the object
  • Change the conditions
  • Notifies all threads waiting on an object
// Example synchronized(object) {change condition objects. notifyAll(); }Copy the code

4. Pipeline input/output flow

The piped I/O stream differs from the normal file I/o stream or network I/o stream in that the piped I/o stream is primarily used for data transfer between threads over the medium of memory.

Concrete implementation of pipeline input/output stream:

  1. PipedInputStream
  2. PipedOutputStream
  1. PipedReader
  2. PipedWriter

1, 2 are byte streams, 3, 4 are character streams.

Sample code:

package com.lizba.p3; import java.io.IOException; import java.io.PipedReader; import java.io.PipedWriter; Pipe flow / * * * < p > * * < / p > * * @ Author: Liziba * @ the Date: 2021/6/16 21:07 */ public class Piped { public static void main(String[] args) throws IOException { PipedWriter out = new PipedWriter(); PipedReader in = new PipedReader(); // Input/output stream connection (error if not connected) out.connect(in); Thread printThread = new Thread(new Print(in), "PrintThread"); printThread.start(); // enter int receive = 0; try { while ((receive = System.in.read()) ! = -1) { out.write(receive); } } finally { out.close(); ** / static class Print implements Runnable {private PipedReader in; public Print(PipedReader in) { this.in = in; } @Override public void run() { int receive = 0; Try {while (true) {if ((receive = in.read())! = -1){ System.out.print((char)receive); } } } catch (IOException e) { e.printStackTrace(); }}}}Copy the code

Example test code:

# Hello liziba # Hello lizibaCopy the code

5, Thread. The join ()

Thread A waits for Thread A to terminate before returning from thread.join (). The thread provides the following API for the join() method:

Public final void join() throws InterruptedException // Final synchronized void join(long millis, int nanos) public final synchronized void join(long millis, int nanos)Copy the code

Sample code:

Set up ten threads, one from 0 to 9. Each thread needs to call the join() method of the previous thread. For example, thread 1 can return from join() only when thread 0 terminates, and thread 2 can return from join() only when thread 1 terminates.

package com.lizba.p3; import com.lizba.p2.SleepUtil; import java.util.concurrent.TimeUnit; / * * * < p > * the join () wait for notification mechanism * < / p > * * @ Author: Liziba * @ the Date: Public class Join {public static void main(String[] args) {// Thread previous = Thread.currentThread(); for (int i = 0; i < 10; i++) { Thread t = new Thread(new Domino(previous), String.valueOf(i)); t.start(); previous = t; } SleepUtil.sleepSecond(5); System.out.println(Thread.currentThread().getName() + " end."); } static class Domino implements Runnable { private Thread thread; public Domino(Thread thread) { this.thread = thread; } @Override public void run() { try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " end."); }}}Copy the code

View the output:

To summarize the above code:

Each thread waits for the termination of the precursor thread before returning from the join(). This involves the wait/notification mechanism, which can be seen in JDK source code:

public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } join() {if (millis == 0) {while (isAlive()) {if (isAlive()) {// Wait () wait(0) if (isAlive()); While (isAlive()) {long delay = millis-now; If (delay <= 0) {break; } // If not, wait(delay); now = System.currentTimeMillis() - base; }}} public final Native Boolean isAlive();Copy the code

6. Use of ThreadLocal

The core principles of ThreadLocal will not be described in detail in this article, but the use of ThreadLocal will be briefly introduced later, and the principle and use of ThreadLocal will be detailed in a separate article later.

ThreadLocal is a thread variable, which is a storage structure with ThreadLocal objects as keys and arbitrary objects as values. This storage structure can be attached to a thread, and we can query a value bound to the thread through a ThreadLocal object.

Sample code:

The following code constructs a class that evaluates method call times.

package com.lizba.p3; import com.lizba.p2.SleepUtil; /** * <p> * * </p> * * @Author: Liziba * @Date: 2021/6/16 22:04 */ public class Profiler { private static final ThreadLocal<Long> TIME_THREAD_LOCAL = new ThreadLocal<Long>() { @Override protected Long initialValue() { return System.currentTimeMillis(); }}; public static final void begin() { TIME_THREAD_LOCAL.set(System.currentTimeMillis()); } public static final Long end() { return System.currentTimeMillis() - TIME_THREAD_LOCAL.get(); } public static void main(String[] args) { Profiler.begin(); SleepUtil.sleepSecond(1); System.out.println("Cost: " + Profiler.end()); }}Copy the code

View the execution result: