Concurrent history

In the earliest days of computers, without an operating system, there was only one way to execute a program: from beginning to end. Any resource will serve the program, and while the computer is using some resources, others will be idle and there will be a waste of resources.

Wasted resources here refer to the situation where resources are idle and not fully used.

The appearance of the operating system has brought concurrency for our program, the operating system enables our program to run multiple programs at the same time, a program is a process, which is equivalent to running multiple processes at the same time.

The operating system is a concurrent system. Concurrency is a very important characteristic of the operating system. The operating system has the ability to process and schedule multiple programs simultaneously, such as multiple I/O devices in input and output at the same time. Device I/O and CPU calculations are performed simultaneously; Multiple system and user programs are launched in memory at the same time and executed alternately. While coordinating and allocating processes, the operating system also assigns different resources to different processes.

Operating system to achieve multiple programs running at the same time to solve the problem that a single program can not do, mainly have the following three points

  • Resource utilizationFor example, when you grant permissions to a folder, the input program cannot accept external input characters until the permissions have been granted. Basically, you can’t do anything else while you’re waiting for the program. If you can run another program while you’re waiting for it, you can greatly improve resource utilization. (The resource doesn’t get tired) because it can’t paddle
  • fairnessDifferent users and programs can use the resources on the computer. An efficient way to run this is to use resources in time slices for different programs, but it is important to note that the operating system can determine the priorities of different processes. Although every process has the right to equal access to resources, when one process releases resources and another process with a higher priority grabs resources, the process with a lower priority cannot obtain resources, which leads to process hunger.
  • convenienceIndividual processes don’t communicate. That’s the nature of communicationInformation exchangeTimely information exchange can be avoidedInformation islandDoing repetitive tasks; A single process can do anything that can be done concurrently, but it’s just a very inefficient way of doing itsequential.

However, sequential programming (also known as serial programming) is not useless, the advantage of serial programming lies in its intuitive and simple, objectively speaking, serial programming is more suitable for the way of thinking of our human brains, but we will not be satisfied with sequential programming, we want it more!! . Resource utilization, fairness and convenience drive the emergence of threads as well as processes.

If you don’t already understand the difference between a process and a thread, let me explain it to you from my years of operating system experience: A process is an application, and a thread is a sequential flow within an application.

There are multiple threads in a process that perform tasks that may be the same or different. Each thread has its own order of execution.

Each thread has its own stack space, which is thread private, as well as some other internal and thread shared resources, as shown below.

In computers, stack refers to the stack and heap refers to the heap

Threads share process-wide resources, such as memory and file handles, but each thread also has its own private contents, such as program counters, stacks, and local variables. The following summarizes the differences between process and thread shared resources

A thread is a lightweight process. Lightness is reflected in the fact that the creation and destruction of a thread is much less expensive than the process.

Note: Any comparison is relative.

In most modern operating systems, threads are the basic unit of scheduling, so our perspective focuses on the exploration of threads.

thread

What is multithreading

Multithreading means that you can run multiple threads in the same application. As we know, instructions are executed in the CPU. Multithreaded applications are like code that has multiple cpus executing the application at the same time.

This is an illusion. The number of threads does not equal the number of cpus. A single CPU will share the CPU’s time slice between multiple threads, performing a switch between each thread within a given time slice, and each thread can also be performed by a different CPU, as shown in the figure below

Concurrency versus parallelism

Concurrent means that applications can perform multiple tasks, but if the computer is only one CPU, so the application fails to perform multiple tasks at the same time, but the application needs to perform multiple tasks, so the computer before starting the next mission, it did not complete the task, just state the staging, task switching, The CPU switches between tasks until the task is complete. As shown in the figure below

Parallelism is when an application breaks down its tasks into smaller subtasks that can be processed in parallel, for example, on multiple cpus.

Strengths and Weaknesses

Reasonable use of threads is an art, reasonable writing an accurate multithreaded program is an art, if the use of threads properly, can effectively reduce the development and maintenance costs of the program.

Java does a good job of implementing development toolsets in user space and providing system calls in kernel space to support multithreaded programming. Java supports a rich class library java.util.concurrent and a cross-platform memory model, while raising the bar for developers. Concurrency has always been a high-order topic. But now, concurrency is becoming a requirement for mainstream developers, too.

Although the benefits of thread a lot, but write correct multithreaded (concurrent) program is a very difficult thing, concurrency bugs tend and bizarre disappeared mysteriously appeared, when do you think there is no problem when it occurs, is difficult to locate a feature of concurrent programs, so based on that you need to have a solid fundamental concurrency. So why does concurrency happen?

Why does concurrency happen

The rapid development of the computer world is inseparable from the rapid development of CPU, memory and I/O devices, but there has always been a problem of speed difference among the three, as can be seen from the memory hierarchy

Inside the CPU is the construction of registers, which are accessed faster than the cache, which is accessed faster than the memory, and the slowest is disk access.

The program is executed in memory. Most of the statements in the program need to access memory, and some of them also need to access I/O devices. According to the leaky bucket theory, the overall performance of the program depends on the slowest operation, which is the disk access speed.

Because CPU speed is too fast, in order to give full play to THE speed advantage of CPU and balance the speed difference of the three, computer system mechanism, operating system and compiler have made contributions, which are mainly reflected as follows:

  • The CPU uses caches to neutralize access speed differences with memory
  • The operating system provides process and thread scheduling, allowing the CPU to execute instructions while simultaneously multiplexing threads, allowing memory and disk to constantly interact, differentCPU time sliceBe able to perform different tasks to balance the differences among the three
  • The compiler provides the order of execution of the optimized instructions so that the cache can be used properly

While we enjoy these conveniences, multithreading also brings us challenges. Let’s discuss why concurrency problems arise and what is the origin of multithreading

Security issues with threading

Thread safety is very complex. In the absence of synchronization, the execution of multiple threads is often unpredictable. This is one of the challenges of multi-threading

public class TSynchronized implements Runnable{

    static int i = 0;

    public void increase(a){
        i++;
    }


    @Override
    public void run(a) {
        for(int i = 0; i <1000;i++) {
            increase();
        }
    }

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

        TSynchronized tSynchronized = new TSynchronized();
        Thread aThread = new Thread(tSynchronized);
        Thread bThread = new Thread(tSynchronized);
        aThread.start();
        bThread.start();
        System.out.println("i = "+ i); }}Copy the code

And what we’re going to see is that the value of I is different every time, which is not what we expected, so why is that? Let’s first analyze the running process of the program.

TSynchronized implements the Runnable interface and defines a static variable I, then increments the value of I each time in the increase method and loops through its implementation of the run method for 1000 times.

Visibility problem

In the era of single-cpu, all threads share a single CPU, and the problem of consistency between CPU cache and memory is easy to solve

If I were to graph it I think it would look something like this

In the multi-core era, because there are multiple cores, each core can run a thread independently, and each CPU has its own cache, the data consistency between CPU cache and memory is not so easy to solve. When multiple threads execute on different cpus, these threads operate on different CPU caches

Since I is a static variable, it is not protected by any thread-safety measures. Multiple threads can modify the value of I concurrently, so we consider I not thread-safe. This result is caused by the fact that the I values read by aThread and bThread are not visible to each other. So this is a thread-safety issue due to visibility.

#### atomic problems

A seemingly ordinary program produces different results because two threads, aThread and bThread, execute alternately. But the root cause is not the creation of two threads, multithreading is just a necessary condition for thread-safety, and ultimately the root cause appears in the i++ operation.

What’s wrong with this operation? Isn’t that an operation that increments I? I ++ is equal to > I is equal to I + 1. Why is that a problem?

Since I ++ is not an atomic operation, if you think about it, I ++ actually has three steps: read I, perform I + 1, and then reassign the value of I + 1 to I (write the result to memory).

When the two threads start running, each thread reads the value of I into the CPU cache, performs the + 1 operation, and writes the value after + 1 to memory. Since threads have their own stacks and program counters, they do not exchange data with each other, so aThread + 1 writes data to memory, and bThread + 1 writes data to memory. Since the execution period of the CPU time slice is uncertain, the bThread will read the data in memory before the aThread has written to memory, perform the + 1 operation, and write back to memory, overwriting the value of I, causing the effort of the aThread to fail.

Why is there a problem with thread switching above?

Let’s start by considering the order in which the two threads execute under normal circumstances (that is, without thread-safety issues)

As you can see, when aThread completes the i++ operation, the operating system switches the thread by aThread -> bThread. This is the ideal operation. If the operating system switches the thread during any read/add/write phase, thread-safety issues will arise. For example, see the following figure

At the beginning, I = 0 in memory, aThread reads the value in memory and reads it into its own register, performs the +1 operation, and aThread switch occurs. BThread executes, reads the value in memory and reads it into its own register, and aThread switch occurs. AThread -> bThread writes the value of its register back to memory. BThread writes the value of its register back to memory. It’s one. I was overwritten in memory.

We mentioned atomicity above, so what is atomicity?

Atomic operations in concurrent programming are operations that run completely independently of any other process. Atomic operations are mostly used in modern operating systems and parallel processing systems.

Atomic operations are typically used in the kernel, which is the main component of the operating system. However, most computer hardware, compilers, and libraries also provide atomic operations.

In load and store, computer hardware reads and writes memory words. To match, increase, or decrease values, atomic operations are typically performed. During atomic operations, the processor can complete reads and writes during the same data transfer. Thus, no other input/output mechanism or processor can perform a memory read or write task until the atomic operation is complete.

In simple terms, all or none of the atomic operations are performed. The atomicity of database transactions also evolved based on this concept.

Order problem

In concurrent programming, there is also the troublesome problem of order, which, as the name implies, is the order in which instructions are executed. A very obvious example is class loading in the JVM

This is a diagram of how a class is loaded by the JVM, also known as the life cycle of a class. A class goes through five stages: loading, connecting, initializing, using, and unloading. The execution sequence of these five processes is certain, but in the connection stage, it can also be divided into three processes, namely, verification, preparation and analysis. The execution sequence of these three stages is not determined, but usually carried out in cross, and another stage will be activated during the execution of one stage.

The order problem is usually caused by the compiler, which sometimes does the wrong thing by changing the order of instructions in order to optimize the system performance.

Activity problem

Multithreading also brings activity problems. How do you define activity problems? Activity problems are concerned with whether something is going to happen.

If each thread in a group of threads is waiting for an event to occur that can only be triggered by the threads in the group that are waiting, this can cause a deadlock.

In simple terms, each thread is waiting for resources to be released by other threads, and other resources are waiting for resources to be released by each thread. In this case, no thread is able to release its own resources first, resulting in a deadlock, and all threads will wait indefinitely.

Necessary conditions for deadlocks

There are four causes of deadlocks, and breaking one of them can break the deadlock

  • Mutually exclusive: A process uses the allocated resources exclusively. That is, only one process occupies a resource in a period of time. If another process requests the resource at this time, the requester can only wait until the process holding the resource is released.
  • Request and hold conditions: a process that has held at least one resource makes a request for a new resource that has been occupied by another process. In this case, the requesting process blocks but retains possession of other resources that it has obtained.
  • Undeprivable condition: a resource acquired by a process cannot be deprivable until it is used up. It can only be released when it is used up.
  • Circular wait: when a deadlock occurs, there must be a ring chain corresponding to the process.

In other words, each thread in the deadlocked thread collection is waiting for a resource held by another deadlocked thread. But because none of the threads can run, none of the resources can be released, so none of the threads can be woken up.

If deadlocks are silly, live locks are self-defeating, using an idiom.

In some cases, when a thread realizes that it cannot acquire the next lock it needs, it will try to politely release the acquired lock and then wait a very short time to try again. Imagine this scenario: when two people meet in a narrow lane, both want to give way to the other side, the same pace will cause both sides to move forward.

Now imagine a pair of parallel threads using two resources. When each thread fails in its attempt to acquire another lock, both threads release their own lock and try again, and the process repeats. Obviously, there is no thread blocking, but the thread still does not execute down, a condition we call livelock.

If what we expect never happens, we can have activity problems, such as infinite loops in a single thread

while(true) {... }for(;;) {}Copy the code

In multithreading, for example, aThread and bThread both require some kind of resource, and aThread keeps holding on to the resource, and bThread keeps not executing, which can cause activity problems, and bThread will starve, as we’ll see later.

Performance issues

Is closely related to the activity in question is a performance issue, if the activity in question is the final result, so is caused by the results of process performance issues concern, performance problems have many aspects: such as the service time is too long, low throughput, resource consumption is too high, such problems also exist in multiple threads.

One of the most important performance factors in multithreading is the thread Switch we mentioned above, also known as Context Switch, which is very expensive.

The computer has a context that switches resources, registers, and program counters. Context switches generally refer to these context-switched resource, register state, program counter changes, etc.

In context switching, context is saved and restored, locality is lost, and a lot of time is spent on thread switching instead of thread running.

Why is thread switching so expensive? Switching between threads involves the following steps

Switching the CPU from one thread to another involves suspending the current thread, saving its state, such as registers, then reverting to the state of the thread to be switched, loading a new program counter, and the thread switch is actually complete; At this point, the CPU stops executing thread-switch code and instead executes new thread-associated code.

Several ways to cause thread switching

Switching between threads is usually a concern at the operating system level, so what are the ways to cause thread context switching? Or what are the triggers for thread switching? There are several ways to cause a context switch

  • The current task is completed, and the system CPU normally schedules the next thread that needs to run
  • The thread scheduler suspends the currently executing task when it encounters a blocking operation such as I/O and continues to schedule the next task.
  • Multiple tasks concurrently preempt lock resources. The current task does not obtain lock resources and is suspended by the thread scheduler to continue scheduling the next task.
  • The user’s code suspends the current task, such as the thread executing the sleep method, freeing the CPU.
  • Use hardware interrupts to cause a context switch

Thread safety

In Java, threads and locks must be used correctly to achieve thread-safety, but these are only one way to meet thread-safety requirements. To write correct thread-safe code, its core is to manage state access operations. The most important is the most Shared and Mutable state. Only shared and mutable variables have problems, private variables do not, refer to program counters.

The state of an object can be understood as data stored in instance variables or static variables, shared meaning that a variable can be accessed by multiple threads at the same time, mutable meaning that the variable changes over its lifetime. Whether a variable is thread-safe depends on whether it is accessed by multiple threads. For variables to be safely accessible, they must be decorated by a synchronization mechanism.

If synchronization is not used, there are two main ways to avoid multithreaded access to shared variables

  • Do not share variables between multiple threads
  • Set shared variables to immutable

We’ve talked so much about thread safety, but what is thread safety?

What is thread safety

Code that can be safely called by multiple threads at the same time is called thread-safe, and if a piece of code is safe, there are no race conditions for that piece of code. Race conditions occur only when multiple threads share a resource.

Based on the above discussion, we can draw a simple conclusion: a class is thread-safe when accessed by multiple threads and the class consistently behaves correctly.

A single thread is a multi-thread with one thread. A single thread must be thread-safe. Reading the value of a variable does not create a security problem because the value of the variable is not modified no matter how many times it is read.

atomic

We mentioned the concept of atomicity above, and you can think of atomic operations as an indivisible whole with only two outcomes, either all performed or all rolled back. You can put the atomicity as a marriage, a man and a woman can only produce two kinds of results, and well say scattered scattered, general can regard him as a man’s life a kind of atomicity, of course we don’t rule out time management (thread), we know that the thread is bound to be accompanied by security issues, There are also two outcomes for men going out, which correspond to two outcomes of safety: thread-safe (fine) and thread-unsafe (loose).

A race condition

With the above thread switching foundation, it is easy to define the race condition, it refers to two or more threads at the same time to modify a shared data, thus affecting the correctness of the program, this is called race condition, thread switching is the induction factor leading to the appearance of race conditions. To illustrate, let’s look at a piece of code

public class RaceCondition {
  
  private Signleton single = null;
  public Signleton newSingleton(a){
    if(single == null){
      single = new Signleton();
    }
    returnsingle; }}Copy the code

In the code above, a race condition is involved, which is that when single is judged to be empty, a thread switch occurs, another thread executes, when single is judged to be empty, a new operation is performed, and then the thread switches back to the previous thread. Execute the new operation, and there will be two Singleton objects in memory.

Locking mechanism

In Java, there are many ways to lock and secure shared and mutable resources. Java provides a built-in mechanism to protect resources: the synchronized keyword, which has three protection mechanisms

  • Lock the method to ensure that only one thread of multiple threads executes the method;
  • Locking an object instance (in our discussion above, variables can be replaced with objects) to ensure that only one thread of multiple threads accesses the object instance;
  • Locks class objects to ensure that only one thread from multiple threads can access resources in the class.

Synchronized keyword A code Block that protects resources is commonly known as a synchronized Block, for example

synchronized(lock){
  // Thread safe code
}
Copy the code

Each Java object can be used as a synchronization Lock. These locks are called intrinsic Lock or Monitor Lock. The thread automatically acquires the lock before entering the synchronized code and releases it when exiting the synchronized code, and the only way to acquire the built-in lock is to enter the synchronized code block or method protected by the lock, whether exiting through a normal execution path or an abnormal path.

Another implied semantics of synchronized is mutual exclusion, which means exclusive. At most, only one thread holds the lock. When thread A attempts to acquire A lock held by thread B, thread A must wait or block until thread B releases the lock. So thread A is going to wait forever.

When thread A acquires the lock held by thread B, thread A must wait or block, but thread B, which acquires the lock, can reentrant, which can be defined in A piece of code

public class Retreent {
  
  public synchronized void doSomething(a){
    doSomethingElse();
    System.out.println("doSomething......");
  }
  
  public synchronized void doSomethingElse(a){
    System.out.println("doSomethingElse......");
}
Copy the code

A thread that acquires the lock of the doSomething() method can execute the doSomethingElse() method, and then re-execute the contents of the doSomething() method. Lock reentrant also supports reentrant between subclasses and superclasses, which we will cover later.

Volatile is a lightweight synchronized method of locking that locks objects from the side by ensuring that shared variables are visible. Visibility means that when one thread modifies a shared variable, another thread can see the changed value. Volatile is much cheaper to execute than synchronized because volatile does not cause context switching on threads.

We can also ensure thread-safety by using atomic classes, which are essentially the classes under Rt.jar that begin with atomic

In addition, we can use the thread-safe collection classes in the Java.util.Concurrent toolkit to ensure thread-safety, which implementation classes and how they work will be described later.

Concurrent systems can be implemented using different concurrency models, which describe how threads in the system collaborate to accomplish concurrent tasks. Different concurrency models split tasks in different ways, and threads can communicate and collaborate in different ways.

Race conditions and key areas

A race condition is a special condition that occurs in a critical code area. A critical area is a part of the code executed by multiple threads at the same time. The order of code execution in a critical area can cause different results. If multiple threads execute a critical piece of code that results in different results because of the order in which it is executed, the code contains a race condition.

The concurrency model is very similar to distributed systems

The concurrent model is actually very similar to the distributed system model, in which threads communicate with each other, and in the distributed system model, processes communicate with each other. In essence, however, processes and threads are also very similar. This is why the concurrent model is very similar to the distributed model.

Distributed systems typically face more challenges and problems than concurrent systems such as process communication, possible network exceptions, or remote machine crashes. However, a concurrent model also faces problems such as CPU failures, network card problems, and hard disk problems.

Because the concurrent model is similar to the distributed model, they can be borrowed from each other. For example, the model for thread allocation is similar to the load balancing model in the distributed system environment.

To put it bluntly, the idea of distributed model is derived from the concurrent model.

Recognize the two states

An important aspect of the concurrency model is whether threads should share state, have shared state, or be independent. Shared state means that some state is shared between different threads

States are simply data, such as one or more objects. When threads want to share data, problems such as race conditions or deadlocks can occur. Of course, these problems are only possible, and how they are implemented depends on whether you can safely use and access shared objects.

Independent state means that state is not shared between multiple threads, and if threads need to communicate, they can access immutable objects to do so, which is the most efficient way to avoid concurrency problems, as shown in the figure below

Using independent state makes our design much simpler because only one thread can access objects, and even if they are exchanged, they are immutable.

Concurrency model

Parallel Worker

The first concurrency model is the parallel worker model, where clients delegate tasks to agents, who then assign work to different workers. As shown in the figure below

The core idea of parallel worker is that it mainly has two processes, i.e. agent and worker. Delegator is responsible for receiving the task from the client and delivering the task to the specific worker for processing. The worker returns the result to Delegator after processing. After the Delegator receives the result of the Worker’s processing, it is summarized and handed to the client.

The parallel Worker model is a very common one in Java concurrency models. Many concurrent tools under the java.util.Concurrent package use this model.

Advantages of parallel workers

A very obvious feature of the parallel Worker model is that it is easy to understand. In order to improve the parallelism of the system, you can add multiple workers to complete tasks.

Another advantage of the parallel Worker model is that it can split a task into multiple small tasks and execute them concurrently. The Delegator will return to the Client after receiving the Worker’s processing result. The entire Worker -> Delegator -> Client process is asynchronous.

Disadvantages of parallel workers

Similarly, parallel Worker mode also has some hidden disadvantages

The shared state gets complicated

The actual parallel Worker is more complex than what we draw in the figure, mainly because parallel workers usually access some shared data in memory or shared database.

These shared states may use work queues to hold business data, data caches, connection pools for databases, and so on. In thread communication, threads need to make sure that the shared state can be shared by other threads, rather than just sitting in the CPU cache and making it available to them. Of course, programmers need to consider these issues at design time. Threads need to avoid race conditions, deadlocks, and many other concurrency problems caused by shared states.

Concurrency is lost when multiple threads access shared data, because the operating system must ensure that only one thread can access the data, leading to contention and preemption of shared data. Threads that do not preempt resources will block.

Modern non-blocking concurrent algorithms can reduce contention and improve performance, but non-blocking algorithms are difficult to implement.

Persistent data structures are another option. A persistent data structure always retains the previous version after modification. Therefore, if multiple threads modify a persistent data structure at the same time, and one thread modifies it, the modified thread gets a reference to the new data structure.

Although persistable data structure is a new solution, but this method adopted has some problems, for example, a persistent list will be the beginning of the new elements are added to the list, and returns a reference to add new elements by, but other threads are still only holds the list of previous references the first element, they can’t see the newly added elements.

Persistent data structures such as linkedLists do not perform well on hardware. Each element in the list is an object that is scattered throughout computer memory. Modern cpus tend to have much faster sequential access, so using sequential data structures such as arrays can achieve higher performance. The CPU cache can load a large matrix block into the cache and give the CPU direct access to the data in the CPU cache after loading. With linked lists, it is virtually impossible to scatter elements across the entire RAM.

Stateless worker

The shared state can be modified by other threads, so the worker must re-read it every time it manipulates the shared state to make sure it works correctly on the replica. Workers that do not hold state inside threads become stateless workers.

The order of assignments is uncertain

Another disadvantage of the parallel work model is that the order of jobs is uncertain and there is no guarantee of which jobs will be executed first or last. Task A is assigned to the worker before task B, but task B may be executed before task A.

Assembly line

The second type of concurrency model is the pipeline concurrency model we often encounter in the production shop. The following is a flow chart of the pipeline design model

This organizational structure is just like workers on the assembly line in a factory. Each worker only completes part of all the work, and after completing part of the work, the worker will forward the work to the next worker.

Each program runs in its own thread and does not share state with each other, which is also known as the shared-nothing concurrency model.

The pipelined concurrency model is typically designed for non-blocking I/O, that is, when no work is assigned to the worker, the worker does other work. Non-blocking I/O means that when the worker starts an I/O operation, such as reading a file from the network, the worker does not wait for the I/O call to complete. Because I/O operations are slow, waiting for I/ OS can be time consuming. While waiting for I/O, the CPU can do other things, and the result of the I/O operation will be passed to the next worker. The following is a flowchart for non-blocking I/O

In practice, tasks usually do not flow along an assembly line, and since most programs need to do many things, they need to move between different workers depending on the work done, as shown in the figure below

The task may also require the participation of multiple workers

Reactive – event-driven systems

Systems that use a pipeline model are sometimes referred to as responsive or event-driven systems, which respond to external events, such as an HTTP request or a file being loaded into memory.

The Actor model

In the Actor model, every Actor is actually a Worker, and every Actor can handle tasks.

Simply put, the Actor model is a concurrency model that defines a set of general rules for how system components should behave and interact. The best known programming language to use these rules is Erlang. An Actor Actor responds to the received message and can then create more actors or send more messages while preparing to receive the next message.

Channels model

In the Channel model, workers usually do not communicate directly. In contrast, they usually send events to different channels, and then other workers can get messages on these channels. The following is the Channel model diagram

Sometimes workers do not need to know who the next worker is, they just need to write the author into the Channel. Workers listening to the Channel can subscribe or unsubscribe, which reduces the coupling between workers and workers.

Advantages of assembly line design

Compared with the parallel design model, the pipeline model has some advantages as follows

There is no shared state

Because the assembly line design can ensure that the worker will be transferred to the next worker after the processing is completed, there is no need to share any state between workers, so there is no need to consider the concurrency problem. You can even implement each worker as a single thread.

A state worker

Because workers know that no other threads modify their data, workers in pipeline design are stateful. Stateful means that they can keep the data they need to operate in memory. Stateful is usually faster than stateless.

Better hardware integration

Because you can think of pipelines as single-threaded, and the advantage of single-threaded work is that it can work the same way hardware does. Because stateful workers typically cache data in the CPU, they can access cached data faster.

To make the task more effective

Tasks in the pipelined concurrency model can be sorted, typically for log writing and recovery.

Disadvantages of assembly line design

The disadvantage of the pipelined concurrency model is that tasks can involve multiple workers and therefore may be scattered across multiple classes of project code. Therefore, it is difficult to determine which task each worker is performing. Pipelined code is also difficult to write, and code that designs many nested callback handlers is often referred to as callback hell. Callback hell is hard to track debug.

Functional parallelism

Functional parallelism model is a kind of concurrency model which is put forward recently. Its basic idea is to use function call to implement it. The passing of a message is equivalent to a function call. Arguments passed to the function are copied, so no entity outside the function can manipulate the data inside the function. This causes functions to perform atomic operations. Each function call can be executed independently of any other function call.

When each function call is executed independently, each function can be executed on a separate CPU. In other words, functional parallelism is equivalent to each CPU performing its own task independently.

The ForkAndJoinPool class in JDK 1.7 implements functional parallelism. Java 8 introduced the concept of stream, which can also iterate over a large number of collections using parallel streams.

The difficulty with functional parallelism is knowing the flow of function calls and which cpus execute which functions, and the additional overhead of calling functions across cpus.

As we said earlier, a thread is a sequential stream in a process, and in Java, each Java thread is like a sequential stream in the JVM, executing code like a virtual CPU. The Main () method in Java is a special thread. The main thread created by the JVM is the main execution thread. In Java, methods are initiated by the main method. Within the main method, you can also create other threads (execution sequential streams) that can execute application code in conjunction with the main method.

A Java thread is an object, just like any other. Threads in Java represent threads, and threads are instances of the java.lang.Thread class or its subclasses. Let’s take a look at creating and starting threads in Java.

Create and start a thread

There are three main ways to create threads in Java

  • Through inheritanceThreadClass to create a thread
  • By implementingRunnableInterface to create threads
  • throughCallableFutureTo create a thread

Let’s take a look at each of these

Inherit the Thread class to create a Thread

The first way to create a Thread is to inherit the Thread class, as shown in the following example

public class TJavaThread extends Thread{

    static int count;

    @Override
    public synchronized void run(a) {
        for(int i = 0; i <10000;i++){
            count++;
        }
    }

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

        TJavaThread tJavaThread = new TJavaThread();
        tJavaThread.start();
        tJavaThread.join();
        System.out.println("count = "+ count); }}Copy the code

The main steps for creating a thread are as follows

  • Define a Thread class that inherits the Thread class and overwrites the run method. Inside the run method is the task to be completed by the Thread. Therefore, the run method is also calledThe executive body
  • Creates a subclass of Thread. The subclass in the code above isTJavaThread
  • The start method is important to note that it is not called directlyrunMethod is used to start the thread insteadstart Method to start the thread. Of course, the run method can be called, so that it becomes a normal method call, rather than creating a new thread to call.
public static void main(String[] args) throws InterruptedException {

  TJavaThread tJavaThread = new TJavaThread();
  tJavaThread.run();
  System.out.println("count = " + count);
}
Copy the code

In this case, the entire main method has only one thread of execution, the main thread, instead of two threads of execution, one thread of execution

The Thread constructor requires only a Runnable object, calls the start() method of the Thread object to perform the necessary initialization operations for the Thread, and then calls the Runnable run method to start the task in that Thread. We used the thread join method above, which is used to wait for the thread to finish executing. If we had not added the join method, it would not have waited for tJavaThread to finish executing, and the output might not be 10000

As you can see, run is returned before the run method ends. That is, the program does not wait for the run method to finish executing before executing the following command.

Advantages of using inheritance to create threads: it is easier to write; Instead of using thread.currentthread () to get the currentThread, you can use the this keyword to point directly to the currentThread.

Disadvantages of using inheritance to create threads: In Java, only single inheritance is allowed, so subclasses cannot inherit from other classes using inheritance.

Use the Runnable interface to create threads

Alternatively, threads can be created using the Runnable interface, as shown in the following example

public class TJavaThreadUseImplements implements Runnable{

    static int count;

    @Override
    public synchronized void run(a) {
        for(int i = 0; i <10000;i++){
            count++;
        }
    }

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

        new Thread(new TJavaThreadUseImplements()).start();
        System.out.println("count = "+ count); }}Copy the code

The main steps for creating a thread are as follows

  • We first define the Runnable interface and override the Runnable interface’s run method, whose method body is also the thread’s execution body.
  • Create a thread instance, either as simple as the above code or by creating an instance of a new thread, as shown below
TJavaThreadUseImplements tJavaThreadUseImplements = new TJavaThreadUseImplements();
new Thread(tJavaThreadUseImplements).start();
Copy the code
  • Call the start method of the thread object to start the thread.

Threads can implement Runnable and other interfaces at the same time, which is very suitable for multiple same threads to process the same resource, reflecting the idea of object-oriented.

The disadvantage of using the Runnable implementation is that the programming is a bit tedious; if you want to access the currentThread, you must use the thread.currentthread () method.

Use the Callable interface to create threads

The Runnable interface performs a separate task, and the Runnable interface does not return any value. If you want to return a value after the task is completed, you can implement the Callable interface instead of the Runnable interface. Java SE5 introduces the Callable interface, as shown below

public class CallableTask implements Callable {

    static int count;
    public CallableTask(int count){
        this.count = count;
    }

    @Override
    public Object call(a) {
        return count;
    }

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

        FutureTask<Integer> task = new FutureTask((Callable<Integer>) () -> {
            for(int i = 0; i <1000; i++){ count++; }return count;
        });
        Thread thread = new Thread(task);
        thread.start();

        Integer total = task.get();
        System.out.println("total = "+ total); }}Copy the code

I think you already know the benefits of using the Callable interface, the ability to implement multiple interfaces and get the return value of the execution result. There are some differences between the Callable and Runnable interfaces. The main differences are as follows

  • Callable tasks have return values, while Runnable tasks have no return values
  • The Callable method is the call method and the Runnable method is the run method.
  • The Call method can throw an exception, while the Runnable method cannot

Use thread pools to create threads

Let’s start with Executor. Executor is not one of the traditional methods of thread creation, but it is an alternative to thread creation. There are some advantages to using thread pools

  • Using thread pools can reuse threads and control the maximum number of concurrent requests.
  • Implement task thread queuesCaching strategiesandRefused to mechanism.
  • Implement some time-related functions, such as timed execution, periodic execution, etc.
  • Isolate the threading environment. For example, the transaction service and search service are on the same server, and two thread pools are opened respectively, so the resource consumption of the transaction thread is obviously larger. Therefore, a separate thread pool can be configured to separate the slower transaction services from the search services and prevent the service threads from interacting with each other.

You can replace thread creation with the following

new Thread(new(RunnableTask())).start()

/ / replace
  
Executor executor = new ExecutorSubClass() // Thread pool implementation class;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
Copy the code

ExecutorService is the default implementation of Executor and an extended interface to Executor. The ThreadPoolExecutor class provides an extended implementation of thread pools. The Executors class provides a handy factory solution for these Executors. Here are several ways to create threads using ExecutorService

CachedThreadPool

This simplifies concurrent programming. Executors provide a layer of indirection between clients and tasks; Instead of the client performing the task directly, the mediation object will perform the task. Executor allows you to manage the execution of asynchronous tasks without explicitly managing the thread lifecycle.

public static void main(String[] args) {
  ExecutorService service = Executors.newCachedThreadPool();
  for(int i = 0; i <5; i++){ service.execute(new TestThread());
  }
  service.shutdown();
}
Copy the code

CachedThreadPool creates a thread for each task.

Note: The ExecutorService object is created by using the static Executors. This method determines the Executor type. The call to shutDown prevents new tasks from being submitted to the ExecutorService, which exits when all tasks in the Executor are complete.

FixedThreadPool

FixedThreadPool allows you to start multithreading with a limited set of threads

public static void main(String[] args) {
  ExecutorService service = Executors.newFixedThreadPool(5);
  for(int i = 0; i <5; i++){ service.execute(new TestThread());
  }
  service.shutdown();
}
Copy the code

Having a FixedThreadPool allows you to pre-perform expensive thread allocations once and for all, thus limiting the number of threads. This saves time because you don’t have to pay the overhead of creating threads for every task.

SingleThreadExecutor

SingleThreadExecutor is a FixedThreadPool with 1 thread. If multiple tasks are submitted to SingleThreadPool at once, these tasks will be queued and each task will end before the next one starts. All tasks will use the same thread. SingleThreadPool serializes all tasks submitted to it and maintains its own (hidden) suspended queue.

public static void main(String[] args) {
  ExecutorService service = Executors.newSingleThreadExecutor();
  for(int i = 0; i <5; i++){ service.execute(new TestThread());
  }
  service.shutdown();
}
Copy the code

As you can see from the output, the tasks are executed next to each other. I assigned five threads to the task, but instead of switching in and out as we saw before, it would execute its own thread each time, and then the remaining threads would continue through the thread’s execution path. You can use SingleThreadExecutor to ensure that there is only one task running at any given time.

dormancy

A simple way to affect the behavior of a task is to hibernate a Thread, call its sleep() method for a given sleep time, and replace thread.sleep () with TimeUnit, as shown in the following example:

public class SuperclassThread extends TestThread{

    @Override
    public void run(a) {
        System.out.println(Thread.currentThread() + "starting ..." );

        try {
            for(int i = 0; i <5; i++){if(i == 3){
                    System.out.println(Thread.currentThread() + "sleeping ...");
                    TimeUnit.MILLISECONDS.sleep(1000); }}}catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread() + "wakeup and end ...");
    }

    public static void main(String[] args) {
        ExecutorService executors = Executors.newCachedThreadPool();
        for(int i = 0; i <5; i++){ executors.execute(newSuperclassThread()); } executors.shutdown(); }}Copy the code

For a comparison of the sleep() and Thread.sleep() methods in TimeUnit, see the following blog post

(www.cnblogs.com/xiadongqing)…

priority

As mentioned above, the thread scheduler’s execution of each thread is unpredictable and random, so is there any way to tell the thread scheduler which task it wants to be executed first? You can set the priority of the thread state, tell the thread scheduler which thread of execution priority is higher, please give the rider will send single, thread scheduler tend to make the higher priority thread priority, however, this does not mean that the lower priority thread is not performed, that is to say, the priority does not lead to a deadlock. Lower priority threads simply execute less frequently.

public class SimplePriorities implements Runnable{

    private int priority;

    public SimplePriorities(int priority) {
        this.priority = priority;
    }

    @Override
    public void run(a) {
        Thread.currentThread().setPriority(priority);
        for(int i = 0; i <100; i++){ System.out.println(this);
            if(i % 10= =0){ Thread.yield(); }}}@Override
    public String toString(a) {
        return Thread.currentThread() + "" + priority;
    }

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        for(int i = 0; i <5; i++){ service.execute(new SimplePriorities(Thread.MAX_PRIORITY));
        }
        service.execute(newSimplePriorities(Thread.MIN_PRIORITY)); }}Copy the code

The toString() method is overridden to print the name of the Thread by using thread.tostring (). Thread[pool-1-thread-1,10,main] you can override the default output of threads.

From the output, you can see that the last thread has the lowest priority and the remaining threads have the highest priority. Note that the priorities are set at the beginning of run, and there is no benefit in setting them in the constructor because the thread has not yet executed the task.

Although the JDK has 10 priorities, there are usually only MAX_PRIORITY, NORM_PRIORITY, and MIN_PRIORITY.

To make concessions

As we mentioned above, knowing that a thread has run its full length in the run() method can give the thread scheduler a hint that I have completed the most important part of the task and can let another thread use the CPU. This hint will be given by the yield() method.

It is important to note that Thread.yield() suggests a CPU switch, not forces it.

You can’t rely on the yield() method for any significant control or when calling applications; in fact, the yield() method is often abused.

A background thread

A daemon thread is a service thread provided by the runtime in the background that is not required. The program stops when all non-background threads terminate, and all background threads are terminated at the same time. Conversely, the program does not terminate as long as any non-background threads are running.

public class SimpleDaemons implements Runnable{

    @Override
    public void run(a) {
        while (true) {try {
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread() + "" + this);
            } catch (InterruptedException e) {
                System.out.println("sleep() interrupted"); }}}public static void main(String[] args) throws InterruptedException {
        for(int i = 0; i <10; i++){ Thread daemon =new Thread(new SimpleDaemons());
            daemon.setDaemon(true);
            daemon.start();
        }
        System.out.println("All Daemons started");
        TimeUnit.MILLISECONDS.sleep(175); }}Copy the code

In each loop, 10 threads are created, each thread is set as a background thread, and then it starts running. The for loop goes through 10 times, outputs a message, and then the main thread sleeps for a while and stops running. In each run loop, information about the current thread is printed, and the program is finished when the main thread finishes running. Because daemons are background threads, they cannot affect the execution of the main thread.

But when you remove daemon.setdaemon (true), while(true) loops indefinitely, so the main thread keeps doing the most important task, so it can’t stop.

ThreadFactory

Objects that create threads as needed. Replacing hardwiring of the Thread or Runnable interface with Thread factories enables programs to use special Thread subclasses, priorities, and so on. The common creation method is

class SimpleThreadFactory implements ThreadFactory {
  public Thread newThread(Runnable r) {
    return newThread(r); }}Copy the code

Executors. DefaultThreadFactory method provides a simple implementation is more useful, it will create a thread context before it returns is set to the known values

ThreadFactory is an interface that has only one method that creates a thread

public interface ThreadFactory {

    // Build a new thread. The implementation class may initialize priorities, names, background thread status, thread groups, and so on
    Thread newThread(Runnable r);
}
Copy the code

Let’s look at an example of a ThreadFactory

public class DaemonThreadFactory implements ThreadFactory {

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setDaemon(true);
        returnt; }}public class DaemonFromFactory implements Runnable{

    @Override
    public void run(a) {
        while (true) {try {
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread() + "" + this);
            } catch (InterruptedException e) {
                System.out.println("Interrupted"); }}}public static void main(String[] args) throws InterruptedException {
        ExecutorService service = Executors.newCachedThreadPool(new DaemonThreadFactory());
        for(int i = 0; i <10; i++){ service.execute(new DaemonFromFactory());
        }
        System.out.println("All daemons started");
        TimeUnit.MILLISECONDS.sleep(500); }}Copy the code

Executors. NewCachedThreadPool can accept a thread pool objects, create a thread pool according to the need to create a new thread, but will be available when they reuse previous thread structure, and use to provide ThreadFactory when you want to create a new thread.

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
  return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                60L, TimeUnit.SECONDS,
                                new SynchronousQueue<Runnable>(),
                                threadFactory);
}
Copy the code

Join a thread

A thread can call the join() method on another thread, and the effect is to wait until the second thread finishes before executing properly. If a thread calls the t.in () method on another thread T, the thread will be suspended until the target thread t terminates (t.isalive () returns true or false).

You can also call the JOIN with a timeout parameter to set the expiration time. When the expiration time expires, the join method returns automatically.

Calls to join can also be interrupted by calling the interrupted method on the thread using a try… Catch clause

public class TestJoinMethod extends Thread{

    @Override
    public void run(a) {
        for(int i = 0; i <5; i++){try {
                TimeUnit.MILLISECONDS.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("Interrupted sleep");
            }
            System.out.println(Thread.currentThread() + ""+ i); }}public static void main(String[] args) throws InterruptedException {
        TestJoinMethod join1 = new TestJoinMethod();
        TestJoinMethod join2 = new TestJoinMethod();
        TestJoinMethod join3 = new TestJoinMethod();

        join1.start();
// join1.join();join2.start(); join3.start(); }}Copy the code

The join() method waits for the thread to die. In other words, it causes the currently running thread to stop executing until the thread it joined completes its task.

Thread exception catching

Because of the nature of threads, you can’t catch exceptions that escape from the thread. Once an exception escapes the task’s run method, it will propagate out to the console, unless you take special steps to catch such bad exceptions. Prior to Java5, you could catch exceptions through thread groups, but after Java5, You need to use executors to solve the problem, because thread groups are not a good attempt.

The following tasks throw an exception during the execution of the run method that is thrown outside the run method and cannot be caught by the main method

public class ExceptionThread implements Runnable{

    @Override
    public void run(a) {
        throw new RuntimeException();
    }

    public static void main(String[] args) {
        try {
            ExecutorService service = Executors.newCachedThreadPool();
            service.execute(new ExceptionThread());
        }catch (Exception e){
            System.out.println("eeeee"); }}}Copy the code

In order to solve this problem, we need to modify the Executor Thread way, Java 5 interface provides a new Thread. UncaughtExceptionHandler, it allows you to on each Thread has an exception handler. Thread. UncaughtExceptionHandler. UncaughtException () will be posted in a Thread for not captured near death is invoked.

public class ExceptionThread2 implements Runnable{

    @Override
    public void run(a) {
        Thread t = Thread.currentThread();
        System.out.println("run() by " + t);
        System.out.println("eh = " + t.getUncaughtExceptionHandler());
      
      	// Manually throw an exception
        throw newRuntimeException(); }}/ / Thread. UncaughtExceptionHandler interface, create an exception handler
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught "+ e); }}public class HandlerThreadFactory implements ThreadFactory {

    @Override
    public Thread newThread(Runnable r) {
        System.out.println(this + " creating new Thread");
        Thread t = new Thread(r);
        System.out.println("created " + t);
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        System.out.println("ex = " + t.getUncaughtExceptionHandler());
        returnt; }}public class CaptureUncaughtException {

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool(new HandlerThreadFactory());
        service.execute(newExceptionThread2()); }}Copy the code

An additional tracing mechanism has been added to the program to verify that the factory created thread is passed to UncaughtExceptionHandler. As you can see, uncaught exceptions are caught by uncaughtException.

Hello, MY name is Cxuan. I have handwritten four PDFS, including Java basic summary, HTTP core summary, computer basic knowledge and operating system core summary. I have sorted them into PDFS.