An in-depth analysis of the principle of JAVA multithreading

1. Thread basics


1.1 Threads and processes


1.1.1 Process


●A program consists of instructions and data, but to run these instructions and to read and write data, the instructions must be loaded to the CPU and the data must be loaded to the memory. Devices such as disks and networks are also needed during the execution of instructions. Processes are used to load instructions, manage memory, and manage IO.


●When a program is run, the code of this program is loaded from the disk into the memory, and a process is opened at this time. 


● A process can be regarded as an instance of a program. Most programs can run multiple instance processes at the same time (such as notepad, drawing, browser, etc.), and some programs can only start one instance process (such as NetEase Cloud Music, 360 Security Guard, etc.).


●The operating system will allocate system resources (CPU time slice, memory and other resources) in units of processes, and a process is the smallest unit of resource allocation.


1.1.2 Threads


●A thread is an entity in a process. A process can have multiple threads, and a thread must have a parent process. 


●A thread is an instruction stream, and the instructions in the instruction stream are handed over to the CPU in a certain order for execution.


●Thread, sometimes called a lightweight process (Lightweight Process, LWP), is the smallest unit of operating system scheduling (CPU scheduling) execution.


1.1.3 The difference between process and thread


1 resource occupation
A process has shared resources, such as memory space, for its internal threads to share


2 Dependency
a process is basically independent of each other, while threads exist within a process and are a subset of a process


3 communication mode
a inter-process communication is more complicated


ⅰ The process communication of the same computer is called IPC (Inter-process communication)
ⅱ The process communication between different computers needs to go through the network and follow a common protocol, such as HTTP
b Thread communication is relatively simple because they share the memory in the process , an example is when multiple threads can access the same shared variable


4Context switching
a thread is lighter, and the cost of thread context switching is generally lower than that of process context switching
b When two threads do not belong to the same process, the switching process is the same as process context switching;
c When two threads belong to For the same process, because virtual memory is shared, resources such as virtual memory remain unchanged when switching, and only data that is not shared, such as private data and registers of threads, need to be switched


1.1.4 The way of inter-process communication


See Operating System - Interprocess Communication for details .

1 Pipeline (pipe) and named pipe (named pipe): Pipelines can be used for communication between parent and child processes with kinship. In addition to the functions of pipelines, named pipes also allow communication between unrelated processes.


2 Signal (signal): The signal is a simulation of the interrupt mechanism at the software level. It is a relatively complex communication method used to notify the process that a certain event has occurred. A process receives a signal and the processor receives an interrupt. The effect of the request can be said to be consistent.


3 Message queue (message queue): The message queue is a linked list of messages, which overcomes the shortcomings of the limited semaphore in the above two communication methods. Processes with write permissions can add new information to the message queue according to certain rules; Processes with read permission on the message queue can read information from the message queue.


4 Shared memory (shared memory): It can be said that this is the most useful way of inter-process communication. It allows multiple processes to access the same memory space, and different processes can see the updates of data in the shared memory in the other process in time. This approach needs to rely on some kind of synchronization operations, such as mutexes and semaphores.


5 Semaphore (semaphore): It is mainly used as a means of synchronization and mutual exclusion between processes and between different threads of the same process.


6 socket (socket): This is a more general inter-process communication mechanism, which can be used for inter-process communication between different machines in the network, and is widely used.


1.1.5 Ways of communication between threads


Mutual exclusion
refers to the exclusiveness of shared process system resources when accessed by individual threads. When several threads want to use a shared resource, only one thread is allowed to use it at any time, and other threads that want to use the resource must wait until the resource occupant releases the resource.


Synchronization
refers to a restrictive relationship between threads, for example:
The execution of one thread depends on the message of another thread. When it does not get the message of another thread, it should wait until the message arrives before being awakened in a broad sense

. See, mutual exclusion is also a kind of synchronization.

Control Method of Thread Synchronization and Mutual Exclusion


●Critical section Access public resources or a piece of code through the serialization of multithreading, which is fast and suitable for controlling data access. Critical resources are shared resources that are only allowed to be used by one process at a time. The section of code that accesses critical resources in each process is called a critical section Mutex Designed
to coordinate separate access to a shared resource
Semaphore Designed to control a resource with a limited number of users
Events Used to notify a thread that some event has occurred, thereby initiating the start of a subsequent task.


1.2 Context switching


A context switch is the switching of the CPU (Central Processing Unit) from one process or thread to another.
Context is the contents of the CPU registers and program counter at any point in time.
A register is a small portion of very fast memory inside the CPU (as opposed to the slower RAM main memory outside the CPU) that speeds up the execution of computer programs by providing fast access to frequently used values.
The program counter is a specialized register that indicates the CPU's position in its sequence of instructions and holds the address of the instruction being executed or the address of the next instruction to be executed, depending on the specific system.
For details on process context switching, see Operating System - Process Context Switching .

The switching process can be simply described as that the kernel (i.e. the core of the operating system) performs the following activities on the processes (including threads) on the CPU:


1 suspends the processing of a process and stores the CPU state (i.e. the context) of that process somewhere in memory


2 Get the context of the next process from memory and restore it in the CPU's registers


3 Return to the location indicated by the program counter (that is, return to the line of code where the process was interrupted) to resume the process.
 



1.2.1 Context switching features


●It can only happen in kernel mode. For details, see Kernel Mode VS User Mode  &  Operating System-Linux Memory Management
●It is a feature of multitasking operating system. Multitasking operating system has two situations: concurrent and parallel. Context switches happen because
        ○ processes voluntarily give up their time in the CPU
        ○ or as a result of the scheduler switching when a process has exhausted its CPU time slice.
●Usually occurs in computing-intensive tasks. IO-intensive tasks rarely require CPU context switching, and they can be handled by external devices. Context switching is a huge cost to the system in terms of CPU time, in fact, it may be the most expensive operation on the operating system. Therefore, a major focus in operating system design is to avoid unnecessary context switches as much as possible. One of Linux's many advantages over other operating systems (including some other Unix-like systems) is its extremely low cost of context switching and mode switching.


1.2.2 View CPU context switching



The linux system can view the statistics of  CPU context switching per second for the entire operating system by command statistics CPU context switching data 1

vmstat 1 

The cs column is the statistics of CPU context switching. Of course, CPU context switching is not equivalent to thread switching, and many operations will cause CPU context switching:
        ○thread, process switching
        ○system call
        ○interruption


2 View a process/thread context switch
a using the pidstat command

常用的参数:
-u 默认参数,显示各个进程的 CPU 统计信息
-r 显示各个进程的内存使用情况
-d 显示各个进程的 IO 使用
-w 显示各个进程的上下文切换
-p PID 指定 PID

# 显示进程5598每一秒的切换情况
pidstat -w -p 5598 1


Among them, cswch means active switching, and nvcswch means passive switching. It can be seen from the statistics that the process actively switches nearly 500 times per second, so there are a large number of sleep\wake operations in the code. 


b View from the status information of the process 

cat /proc/5598/status 


1.3 Operating system level thread life cycle


The thread life cycle at the operating system level can basically be described by the "five-state model" shown in the figure below.


●Initial state The thread has been created, but the CPU is not allowed to execute. The creation here refers to being created at the programming language level, the real thread, the operating system has not yet been created.


●Ready state Threads can allocate CPU time slices for execution. The operating system creates threads.


●Run state Threads assigned to CPU time slices.


●Dormant state Release the right to use the CPU. The thread in the running state calls the blocking API, such as reading a file in a blocking manner, or waiting for an event, such as a condition variable, and the thread state will become this state. When the waiting time occurs, the thread will switch from the sleeping state to the ready state.


●Termination state The thread execution is completed, or an exception occurs and enters the termination state. The thread lifetime ends.

These five states are combined for simplification in different programming languages. For example:


●The POSIX Threads specification of the C language combines the initial state and the runnable state


●In the Java language, the runnable state and the running state are combined. These two states are useful at the operating system scheduling level, but the JVM level does not care about these two states, because the JVM leaves thread scheduling to the operating system.


1.4 How to view process threads


windows
●Task manager to view the number of processes and threads, and can also kill the process
tasklist to view the process
taskkill to kill the thread


linux
ps -ef to view all processes
top press uppercase H to switch whether to explicitly thread
ps -fT -p <PID> to view all threads of a process
top -H -p <PID> to view all threads of a process
● kill kills the process


System thread implementation method
LinuxThreads linux/glibc package only implemented LinuxThreads before 2.3.2
NPTL (Native POSIX Thread Library)

can check which thread implementation the system uses through the following command getconf GNU_LIBPTHREAD_VERSION 


Java
jps View all java processes
jstack <PID> View all thread states of a Java process
jconsole GUI view


2. Detailed explanation of Java threads


2.1 Implementation of Java threads


Use Thread or inherit Thread class

public void test() {
    Thread newThread = new Thread() {
        @Override
            public void run() {
            System.out.println("new Thread");
        }
    };

    MyThread myThread = new MyThread();

    newThread.start();
    myThread.start();
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("extend Thread");
    }
}


Implement the Runnable interface

public void test2() {
    Runnable runnable = new Runnable() {

        @Override
            public void run() {
            System.out.println("implement runnable");
        }
    };

    Thread thread = new Thread(runnable);
    thread.start();
}


Use the Callable interface with a return value
Method 1

public void test3() {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    Future<Integer> submit = executorService.submit(new MyCallableTask());
    try {
        System.out.println(submit.get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    executorService.shutdown();
}
class MyCallableTask implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        return new Random().nextInt();
    }
}


way two

public void test4() {
    FutureTask<String> task = new FutureTask<>(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "callable";
        }
    });

    new Thread(task).start();

    try {
        String s = task.get();
        System.out.println(s);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}


use lambdas

new Thread(() -> System.out.println(Thread.currentThread().getName())).start();

Summary
In essence, there is only one way to implement threads in Java, which is to create threads through new Thread(). Calling Thread#start to start a thread will eventually call the Thread#run method.


Why do you say that?
There is nothing to say about the first two, they are all directly calling the Thread#start method. The key lies in the calling method of the third thread pool:
when the method java.util.concurrent.Executors#newFixedThreadPool is called, a ThreadPoolExecutor class will be created

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

The constructor of the ThreadPoolExecutor class will use the Executors.defaultThreadFactory() method to create the DefaultThreadFactory, which contains a newThread method, which will eventually be called in the thread to complete the initialization and call of the thread

public Thread newThread(Runnable r) {
    Thread t = new Thread(group, r,
                          namePrefix + threadNumber.getAndIncrement(),
                          0);
    if (t.isDaemon())
        t.setDaemon(false);
    if (t.getPriority() != Thread.NORM_PRIORITY)
        t.setPriority(Thread.NORM_PRIORITY);
    return t;
}

You can see that it goes to new Thread, and eventually the Thread#start method will be called to start the thread.


Why does Java thread execution call the start method instead of the run method?


The start method is a local method, which corresponds to the creation process of Java thread -> JVM thread -> OS thread; the run method is just a method of Thread object, and running it will not cause the creation of OS thread.


2.2 Java thread implementation principle


The following is the entire process that a Java thread calls the Thread#start method from the Java code level to the operating system level:

 

Coroutines
, English Coroutines, is a kind of existence based on threads, but lighter than threads. Coroutines are not managed by the operating system kernel, but completely controlled by programs (that is, in user mode execution), has properties that are invisible to the kernel.
The advantage of this is that the performance has been greatly improved, and it will not consume resources like thread switching.



Subroutines, or functions, are called hierarchically in all languages. For example, A calls B, B calls C during execution, C returns after execution, B returns after execution, and finally A completes execution. The call of a coroutine is different from a subroutine. The coroutine is interruptible inside the subroutine, and then turns to execute other subroutines, and then returns to continue execution at an appropriate time.

Java

def A():
    print '1'
    print '2'
    print '3'
def B():
    print 'x'
    print 'y'
    print 'z'

 

Assuming that it is executed by a coroutine, during the execution of A, it can be interrupted at any time to execute B, and B may also be interrupted during the execution process and then execute A. The result may be: 1 2 xy 3 z.
The characteristic of the coroutine is that it is executed by a thread.
Advantages of coroutines
        ●Thread switching is scheduled by the operating system, and coroutines are scheduled by users themselves, thus reducing context switching and improving efficiency.
        ●The default stack size of a thread is 1M, while the coroutine is lighter, close to 1k. So more coroutines can be opened in the same memory.
        ●No need for multi-threaded locking mechanism: Because there is only one thread, there is no conflict of writing variables at the same time. In the coroutine, shared resources are controlled without locking, and only need to judge the state, so the execution efficiency is much higher than that of multi-threading.
Coroutines are suitable for scenarios that are blocked and require a lot of concurrency (network io). Not suitable for heavy computing scenarios.


Coroutine framework in Java
kilim quasar


2.3 Java thread scheduling mechanism


Thread scheduling refers to the process in which the system allocates processor usage rights for threads. There are two main scheduling methods, namely cooperative thread scheduling and preemptive thread scheduling.


cooperative thread scheduling


Thread execution time is determined by the thread. After the thread finishes executing its task, it must actively notify the system to switch to another thread.
Benefits:
        1 Simple implementation, context switching is visible to threads
        2 No thread synchronization issues
Disadvantages:
        1 Execution time is uncontrollable. If a thread blocks, the CPU may remain blocked.
Preemptive thread scheduling
The operating system allocates an execution time slice for each thread, and the thread switching is determined by the OS. Benefits:
1. The thread execution time is controllable, and the entire CPU will not be blocked because one thread is blocked.


Preemptive thread scheduling embodiment of Java thread


It is hoped that the system can allocate more time to some threads and less time to some threads, which can be done by setting the thread priority. The Java language has a total of 10 levels of thread priority (Thread.MIN_PRIORITY to Thread.MAX_PRIORITY). When two threads are in the ready state at the same time, the thread with higher priority is more likely to be selected and executed by the system. But the priority is not very reliable, because Java threads are implemented by mapping to the system's native threads, so thread scheduling ultimately depends on the operating system.


2.4 Java thread life cycle


There are six states of threads in the Java language, namely:
1NEW (initialization state)
2RUNNABLE (runnable state + running state)
3BLOCKED (blocked state)
4WAITING (unlimited waiting)
5TIMED_WAITING (timed waiting)
6TERMINATED (terminated state)
in operation At the system level, BLOCKED, WAITING, and TIMED_WAITING in Java threads are a state, that is, the dormant state we mentioned earlier. That is to say, as long as the Java thread is in one of these three states, the thread will never have the right to use the CPU.


yield will not be like park, wait, join and other operations, it involves heavyweight operations such as context switching, and can be executed again soon.


From the perspective of JavaThread, JVM defines some states for Java Thread objects (jvm.h)

*/


From the perspective of OSThread, JVM also defines some thread states for external use, such as the state of threads in the thread stack information output by jstack (osThread.hpp)


2.5 Thread common methods

sleep

  • Calling sleep will make the current thread enter the TIMED_WAITING state from Running, and will not release the object lock
  • Other threads can use the interrupt method to interrupt the sleeping thread. At this time, the sleep method will throw InterruptedException and clear the interrupt flag
  • The thread after the sleep end may not be executed immediately
  • sleep is the same as yield when the incoming parameter is 0

yield

  • Yield will release CPU resources, let the current thread enter the Runnable state from Running, let the thread with higher priority (at least the same) get the execution opportunity, and will not release the object lock;
  • Assuming that the current process only has the main thread, after calling yield, the main thread will continue to run because there is no thread with a higher priority than it;
  • The specific implementation depends on the task scheduler of the operating system

join

After waiting for the thread calling the join method to end, the program continues to execute. It is generally used in scenarios where the asynchronous thread finishes executing the result before continuing to run.

public class ThreadJoinDemo {
    public static void main(String[] sure) throws InterruptedException {

        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("t begin");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t finished");
            }
        });
        long start = System.currentTimeMillis();
        t.start();
        //主线程等待线程t执行完成
        t.join();

        System.out.println("执行时间:" + (System.currentTimeMillis() - start));
        System.out.println("Main finished");
    }
}

stop

The stop() method has been abandoned by jdk, because the stop() method is too violent, forcibly terminating the half-executed thread.

This method will release the object lock, causing the thread to be forced to end halfway through execution, which may lead to data inconsistency.

public class ThreadStopDemo {
    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                    try {
                        Thread.sleep(60000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "执行完成");
            }
        });
        thread.start();
        Thread.sleep(2000);
        // 停止thread,并释放锁
        thread.stop();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "等待获取锁");
                synchronized (lock) {
                    System.out.println(Thread.currentThread().getName() + "获取锁");
                }
            }
        }).start();

    }
}

How to gracefully stop a thread?

Java provides an interrupt mechanism to gracefully stop threads, see below.

2.6 Java thread interruption mechanism

Java does not provide a safe, direct way to stop a thread, but provides an interrupt mechanism. The interrupt mechanism is a cooperative mechanism, that is to say, another thread cannot be terminated directly through the interrupt, but the interrupted thread needs to handle it by itself. The interrupted thread has complete autonomy. It can choose to stop immediately, stop after a period of time, or not stop at all.

To put it simply, when thread A is running, if it wants to interrupt thread B, a flag will be set for thread B. In the code implementation of thread B, there can be a checkpoint for the flag. When the flag bit is found to indicate interruption, thread B calls Its own code logic to handle interrupt requests, for example, it can stop itself or ignore it.

API use

  • interrupt(): Set the interrupt flag bit of the thread to true without stopping the thread
  • isInterrupted(): Determine whether the interrupt flag bit of the current thread is true, and will not clear the interrupt flag bit
  • Thread.interrupted(): Determine whether the interrupt flag bit of the current thread is true, clear the interrupt flag bit, and reset it to fasle

When using the interrupt mechanism, you must pay attention to whether there is a situation where the interrupt flag bit is cleared, that is, carefully distinguish the following two situations:

testisInterrupted()

public class ThreadInterruptTest {
    static int i = 0;

    public static void main(String[] args) {
        System.out.println("begin");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    i++;
                    System.out.println(i);
                    //Thread.interrupted()  清除中断标志位
                    //Thread.currentThread().isInterrupted() 不会清除中断标志位
                    if (Thread.currentThread().isInterrupted()) {
                        System.out.println("=========");
                    }
                    if (i == 10) {
                        break;
                    }

                }
            }
        });

        t1.start();
        //不会停止线程t1,只会设置一个中断标志位 flag=true
        t1.interrupt();
    }
}

testThread.interrupted()

public class ThreadInterruptTest {
    static int i = 0;

    public static void main(String[] args) {
        System.out.println("begin");
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    i++;
                    System.out.println(i);
                    //Thread.interrupted()  清除中断标志位
                    //Thread.currentThread().isInterrupted() 不会清除中断标志位
                    if (Thread.interrupted()) {
                        System.out.println("=========");
                    }
                    if (i == 10) {
                        break;
                    }

                }
            }
        });

        t1.start();
        //不会停止线程t1,只会设置一个中断标志位 flag=true
        t1.interrupt();
    }
}

Use the interrupt mechanism to gracefully stop threads

while (!Thread.currentThread().isInterrupted() && more work to do) {
    do more work
}

public class StopThread implements Runnable {

    @Override
    public void run() {
        int count = 0;
        while (!Thread.currentThread().isInterrupted() && count < 1000) {
            System.out.println("count = " + count++);
        }
        System.out.println("线程停止: stop thread");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new StopThread());
        thread.start();
        Thread.sleep(5);
        thread.interrupt();
    }
}

Can you feel interrupted during sleep

@Override
public void run() {
    int count = 0;
    while (!Thread.currentThread().isInterrupted() && count < 1000) {
        System.out.println("count = " + count++);

        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    System.out.println("线程停止: stop thread");
}

The thread in dormancy is interrupted, the thread can feel the interrupt signal, and will throw an InterruptedException exception, clear the interrupt signal at the same time, and set the interrupt flag bit to false. This will cause the while condition Thread.currentThread().isInterrupted() to be false, and the program will exit when the condition count < 1000 is not met. If you do not manually add the interrupt signal in the catch and do not do any processing, the interrupt request will be blocked, which may cause the thread to fail to stop correctly.

try {
    Thread.sleep(1);
} catch (InterruptedException e) {
    e.printStackTrace();
    //重新设置线程中断状态为true
    Thread.currentThread().interrupt();
}

Summarize

sleep can be interrupted and an interrupt exception is thrown: sleep interrupted, clear the interrupt flag bit

Wait can be interrupted by throwing an interrupt exception: InterruptedException, clearing the interrupt flag

2.7 Communication between Java threads

volatile

Volatile has two major features, one is visibility, and the other is order, prohibiting instruction reordering, and visibility is to allow communication between threads.

public class VolatileTest {
    private static volatile boolean flag = true;

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (flag) {
                        System.out.println("trun on");
                        flag = false;
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (!flag) {
                        System.out.println("trun off");
                        flag = true;
                    }
                }
            }
        }).start();
    }
}

Waiting for wake-up mechanism/waiting for notification mechanism

The waiting wakeup mechanism can be implemented based on the wait and notify methods. Call the wait method of the thread lock object in a thread, and the thread will enter the waiting queue and wait until it is woken up.

public class WaitDemo {
    private static Object lock = new Object();
    private static boolean flag = true;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    while (flag) {
                        try {
                            System.out.println("wait start .......");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    System.out.println("wait end ....... ");
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                if (flag) {
                    synchronized (lock) {
                        if (flag) {
                            lock.notify();
                            System.out.println("notify .......");
                            flag = false;
                        }

                    }
                }
            }
        }).start();
    }
}

LockSupport is a tool used in JDK to realize thread blocking and wake-up. It has the following characteristics:

  • The thread calls park to wait for "permission", and calls unpark to provide "permission" to the specified thread.
  • Use it to block the thread in any occasion, and you can specify any thread to wake up, and you don't have to worry about the order of blocking and waking up operations, but you should pay attention to the effect of waking up multiple times in a row and waking up once.
public class LockSupportTest {

    public static void main(String[] args) {
        Thread parkThread = new Thread(new ParkThread());
        parkThread.start();

        System.out.println("唤醒parkThread");
        LockSupport.unpark(parkThread);
    }

    static class ParkThread implements Runnable{

        @Override
        public void run() {
            System.out.println("ParkThread开始执行");
            LockSupport.park();
            System.out.println("ParkThread执行完成");
        }
    }
}

Pipeline input and output streams

The difference between pipeline input/output stream and ordinary file input/output stream or network input/output stream is that it is mainly used for data transmission between threads, and the transmission medium is memory. The pipeline input/output stream mainly includes the following four specific implementations:

  • Byte-oriented
    PipedOutputStream, PipedInputStream
  • Character-oriented
    PipedReader, PipedWriter
public class Piped {
    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        // 将输出流和输入流进行连接,否则在使用时会抛出IOException
        out.connect(in);

        Thread printThread = new Thread(new Print(in), "PrintThread");

        printThread.start();
        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 ((receive = in.read()) != -1) {
                    System.out.print((char) receive);
                }
            } catch (IOException ex) {
            }
        }
    }
}

Thread#join()

Join can be understood as thread merging. When a thread calls the join method of another thread, the current thread blocks and waits for the thread called the join method to finish executing before continuing. Therefore, the benefits of join can guarantee the execution order of threads, but if The join method of the calling thread has actually lost the meaning of parallelism. Although there are multiple threads, they are still serial in nature. The implementation of the final join is actually based on the waiting notification mechanism.

Guess you like

Origin blog.csdn.net/peterjava123/article/details/130217747