I have not been eager to write concurrent reasons is that I can not understand the operating system, now, I have put the operating system brush again, this time try to write some concurrent, see if you can write clearly, humble small online beg encourage…… I’m going to take the operating system and concurrency approach at the same time.

Concurrent history

In the earliest days of computing, without an operating system, there was only one process required to execute a program, from beginning to end. Any resource will serve the program, which is bound to waste resources.

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

The operating system brings concurrency to our programs. The operating system makes our programs 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. It has to wait until permissions are granted to accept external input. In general, this means that you cannot perform other tasks while waiting for the program. If you can run another program while waiting for the program, you can greatly improve resource utilization. (The resource doesn’t get tired) because it can’t paddle
  • fairnessDifferent users and programs have the same right to use resources on a computer. A highly efficient operation mode is divided into different program time use of resources, but one thing need to note that the operating system can determine the priority of different processes, while each process can have the right resource fair, but every time after a process before release resources at the same time there is a higher priority process rob resources, This results in low-priority processes not being able to access resources and, over time, process starvation.
  • convenienceIndividual processes can’t communicate. Communication, I think, is actually a kind ofThe lightning rodStrategy, the nature of communicationInformation exchangeTimely information exchange can be avoidedInformation islandDoing repetitive tasks; Anything that can be done concurrently, sequential programming can be done, but it’s very inefficient. It’s one wayThe block type.

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.

Or Ruan Yifeng teacher also gave you easy to understand the explanation

From www.ruanyifeng.com/blog/2013/0…

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

Threads are described as a lightweight process, and lightness is reflected in the fact that thread creation and destruction are much cheaper than process overhead.

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

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.

In guIs, threads can improve the responsiveness of user interfaces, and in server applications, concurrency can improve resource utilization and system throughput.

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 concurrency

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 that can only be triggered by another thread in the group, this can result in 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.

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

Based on the above discussion, we can derive a simple definition: a class is thread-safe if it consistently behaves correctly when accessed by multiple threads.

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.

The implementation of volatile will be discussed later.

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.

Hello, everyone, 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.