In-depth analysis of threads

The difference and connection between threads and processes:

Why are there threads? (1) Concurrent programming, with the development of multi-core CPUs, has become a rigid demand. (2) Although multi-process can also achieve concurrent programming effects, the creation and destruction of processes is too heavy.

1. The process can handle the problem of multiple concurrent programming, but the process is too heavy, mainly referring to its resource allocation and resource recovery, the process consumes a lot of resources, the speed is slow, and the cost of creating, destroying, and scheduling a process is relatively high.

2. The emergence of threads effectively solves this problem. It can not only deal with the problem of concurrent programming, but also because it can realize resource sharing (mainly referring to memory and file descriptor tables). Memory sharing means that the objects of thread 1new are in Threads 2, 3, and 4 can be used directly. File descriptor table sharing means that the file opened by thread 1 can be used directly in threads 2, 3, and 4. The first process has a relatively high overhead, and subsequent processes can share it with The first process shares resources, which reduces resource consumption and increases speed

 To give a more vivid example:

There is one person who wants to make a hundred chickens. In order to speed up, we can consider doing it with another person, and arrange a room and a table for each person.

But this consumes more resources, which is multi-process. But if we let two people do it on the same table in the same room, this can reduce resource overhead, and the room and table share

This is multithreading.

3. The process contains multiple threads, and each thread corresponds to a PBC. The pids of the PCBs in the same process are the same, and the memory pointer and the file descriptor table in the same process are the same.

4. There are multiple threads in the process, each thread is independently scheduled on the CPU, each thread has its own execution logic, thread operation is the basic unit of system scheduling execution.

5. Increasing the number of threads does not mean that the speed can be increased all the time. The number of CPU cores is limited.

6. Thread safety issues: Because resources can be shared between threads, it is easy to cause resource contention problems, which will lead to abnormal problems of threads, and then take away the entire process.

1. Thread creation:

// 定义一个Thread类,相当于一个线程的模板
class MyThread01 extends Thread {
    // 重写run方法// run方法描述的是线程要执行的具体任务@Overridepublic void run() {
        System.out.println("hello, thread.");
    }
}
 

public class Thread_demo01 {
    public static void main(String[] args) {
        // 实例化一个线程对象
        MyThread01 t = new MyThread01();
        // 真正的去申请系统线程,参与CPU调度
        t.start();
    }
}
// 创建一个Runnable的实现类,并实现run方法
// Runnable主要描述的是线程的任务
class MyRunnable01 implements Runnable {
    @Overridepublic void run() {
        System.out.println("hello, thread.");
    }
}

public class Thread_demo02 {
    public static void main(String[] args) {
        // 实例化Runnable对象
        MyRunnable01 runnable01 = new MyRunnable01();
        // 实例化线程对象并绑定任务
        Thread t = new Thread(runnable01);
        // 真正的去申请系统线程参与CPU调度
        t.start();
    }
}
* 通过Thread匿名内部类的方法创建一个线程

public class Thread_demo03 {public static void main(String[] args) {
        Thread t = new Thread(){
            // 指定线程任务
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        };
        // 真正的申请系统线程参与CPU调度
        t.start();
    }
}
* 通过Runnable匿名内部类创建一个线程

public class Thread_demo04 {public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            // 指定线程的任务
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());                
            }
        });
        // 申请系统线程参与CPU调度
        t.start();
    }
}
* 通过Lambda表达式的方式创建一个线程

public class Thread_demo05 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            // 指定任务:任务是循环打印当前线程名
            while (true) {
                System.out.println(Thread.currentThread().getName());
                try {
                    // 休眠1000ms
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 申请系统线程参与CPU调度
        t.start();
    }
}

Two. Several common methods of the Thread class:

getId() get id

getName() Get the thread name

getState() Gets the state of the current thread

getPriority() priority

isDaemon() is a background thread

isAlive() is alive

isInterrupted() is interrupted

Three. Thread execution:

1. Threads are executed concurrently and are preemptively scheduled.

public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                //System.out.println("hello");
                System.out.println("1");

            }
        });
        thread.start();
        //System.out.println("world");
        System.out.println("2");

    }

Like this code, we can't determine whether to print "1" or "2" first

2. The difference between the start method and the run() method:

The run() method is the task to be executed. We created a thread and just sorted out the tasks. The start method actually creates the thread and lets the kernel create a PCB. At this time, the PCB represents a real thread to execute. Tasks inside the run() method

3. Once the thread is executed, the pcb in the kernel will be released, and the thread in the operating system will be gone

public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                //System.out.println("hello");
                System.out.println("1");

            }
        });
        thread.start();
        //System.out.println("world");
        System.out.println("2");

    }

Like this code, the thread that executes the main method will disappear after executing the statement "2", and the thread like thread will disappear after executing the run method, but the thread object still exists, when it does not point to any object , it will be recycled by the GC. The reason why the PCB dies and the thread object still exists in the code is because the life cycle of the object in java has its own rules. This life cycle is not exactly the same as the thread in the system kernel. The kernel When the thread in the thread is released, there is no guarantee that the thread object in the java code will be released immediately. Therefore, it is necessary to mark the thread object as 'invalid' through a specific state at this time, and it cannot be restarted. A thread can only be started once. .

4. The isAlive() method is used to judge whether the thread in the system is really created.

Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(1);
            }
        });
        thread.start();

            try {
                Thread.sleep(1000);
                System.out.println(thread.isAlive());
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

    }

If the run of the thread has not run yet, thread.isAlive() returns false at this time. If the run of the thread is running, it returns true at this time. If the run in the thread finishes running, it returns false at this time. The pcb in the kernel is released, and the threads in the operating system are gone.

5.public static void sleep(long millis)

Let the thread sleep, in essence, let the current sleep thread temporarily not participate in the scheduling execution of the CPU (put the thread PCB in a queue indicating the blocking state) and wait until the sleep time is up, the operating system will take the PCB back in the ready queue.

public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                for (i = 0; i < 3; i++) {


                    System.out.println("hello");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }



        });
        thread.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("world");
    }

4. Thread termination:

We usually use the interrupt() method to terminate the thread, but the thread has the final say whether to terminate or not. Thread provides a built-in flag, you can use the isInterruptted method to judge the flag, use the interrupt method to set the flag (it can also wake the thread from sleep)

public static void main4(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                while(!Thread.currentThread().isInterrupted())
                {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(1);
                }

            }
        });
        thread.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        thread.interrupt();
        //System.out.println(2);
    }

 In this code, Thread.currentThread() means to obtain a reference to the current thread, and isInterrupted() means whether it has been notified to terminate.

At the beginning, isInterrupted() is false and enters the while loop. When the thread executing the main function reaches thread.interrupt(), the thread thread is notified to terminate at this time. At this time, isInterrupted() is set to true , According to the normal situation, the while loop cannot enter at this time, but if the thread is sleeping, an exception will be triggered, and the just isInterrupted() is set back to false, and the loop can still be performed at this time. Do you want to do it at this time? Terminating the thread is actually up to us. If we add a break and jump out of the loop, then the thread will be executed.

Thread waiting: Control the end order of two threads:

 Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
               int i=0;
               for(i=0;i<8;i++)
               {
                   System.out.println(1);
               }
            }
        });
        thread.start();
        System.out.println(3);
        try {
            thread.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(2);
    }

When the original main function thread finishes executing the start statement, the main function thread will execute the following code, and the tthread thread will execute the tasks in the run() method, but when the main function thread executes to hread.join() A statement means that the thread executing the main function waits here, and waits for the thread to finish executing before executing it.

Five. The kernel of the operating system:

Ready queue:

The PCBs in this linked list are all in the ''on-call state'', ready state

Blocking queue:

It means that when the thread calls sleep, the thread will enter the dormant state at this time, then it will enter the blocking queue

The PCBs of this linked list are all blocked and do not participate in the scheduling execution of the CPU. The PCB is organized using a linked list, not a simple linked list. Once the thread is awakened and is no longer in a dormant state, the PCB will return to the ready queue, but it will not be scheduled immediately. Consider the scheduling overhead, such as calling sleep(1000), and the corresponding thread PCB will Waiting in the blocking queue for 1000ms for so long, when the PCB returns to the ready queue, will it be dispatched immediately? Although it is sleep(1000), in fact, the overhead of scheduling must be considered. The corresponding thread cannot be executed immediately after waking up. The actual time interval is likely to be greater than 1000ms.

6. The state of the thread

It reflects the scheduling situation of the current thread.

1.NEW creates a Thread object, but has not yet called start (the corresponding PCB has not been created in the kernel)

public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(1);
            }
        });
        System.out.println(thread.getState());
        thread.start();

    }

2. TERMINATED means that the pcb in the kernel has been executed, but the Thread object is still there.

 public static void main(String[] args) {
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(1);
            }
        });
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(thread.getState());

    }
}

3. RUNNABLE includes two parts, which are being executed on the CPU or on the ready queue, and can be executed on the CPU at any time

public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //System.out.println(1);
                int i = 0;
                for (i = 0; i < 1000000; i++) {
                    ;
                }
            }
        });
        System.out.println("开始前"+thread.getState());
        thread.start();

            System.out.println("进行中"+thread.getState());
        try {
            thread.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("结束了"+thread.getState());

    }



}

4. Thread blocking triggered by WAITING wait method

5. Triggered by TIMED_WAITING sleep, thread blocking

public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //System.out.println(1);
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
        });
        thread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println(thread.getState());
        }
    }

6.BLOCKED

Triggered by synchronized, the thread is blocked.

4 5 6 These three states are blocking states, which are caused by blocking for different reasons.

Six. The meaning of multithreading:

We can feel the difference in execution speed between a single thread and multiple threads through the code.

For example, let's realize the operation of increasing a variable by 20 billion. single thread:

public static void main(String[] args) {
        long a=0;
        long i=0;
        long start=System.currentTimeMillis();
        for(i=0;i<200_0000_0000L;i++)
        {
            a++;
        }
        long end=System.currentTimeMillis();
        System.out.println(end-start);

    }

Its execution time is: 

And if we let two threads complete it, and one thread does the job of increasing by 10 billion, will the time be shortened?

public static void main(String[] args) {

        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                long d=0;
               long i=0;
               for(i=0;i<100_0000_0000L;i++) {
                   d++;


               }
            }
        });
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {

                long c=0;
                long k=0;
                for(c=0;c<100_0000_0000L;c++)
                {
                    k++;
                }

            }
        });
        long beg=System.currentTimeMillis();
        thread1.start();
        thread2.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            thread2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        long end=System.currentTimeMillis();
        System.out.println(end-beg);



    }

Let's take a look at the time it takes to execute:

We can see that it takes less time for two threads to complete this self-increment operation together than one thread to complete this self-increment operation. Some people may ask why the execution time of two threads is not half of the execution time of one thread? First of all, the reason why multi-threading is block is because it can make full use of the resources of multi-core CPU, but because these two threads are in the actual scheduling process, some of these schedulings are executed concurrently (on one core), and some are For parallel execution (on two CPUs), we cannot report that these schedules are all parallel. How many times are concurrent and how many times are parallel depends on the configuration of the system. It also depends on the operating environment of the current program.

Let's take a look at the actual scheduling situation when two joins appear in the code

  long beg=System.currentTimeMillis();
        thread1.start();
        thread2.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            thread2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        long end=System.currentTimeMillis();
        System.out.println(end-beg);



    }

 We see that when the thread of the main function main executes the thread1.start statement, the thread1 thread is actually created, and then it does its work. After the main thread executes thread2.start, the thread2 thread also does its work. work, and then when the main thread executes to thread1.join, the main function thread stops, waits for the thread1 thread to finish its work, and waits for the thread1 thread to finish its work, the main function thread continues to execute, and at this time it encounters thread2 again. join, you need to wait for thread2 to finish its work before continuing to execute. There will be a situation here, that is, when thread2 finishes its work before thread1, then the main thread only needs to wait for thread1 to finish its work before executing it, and there is no need to wait any longer thread2 is up. Multiple threads are executed at the same time, and the final time is the time of the slowest thread. In addition, who calls join is to let the thread wait. For example, main calls t1, and join is to make main wait for t1. If t2 calls t1 .join is to let t2 wait for t1.

Seven. Thread safety

The risk brought by multithreading, thread safety, the root cause is the randomness brought by the preemptive execution of multithreading!!!

Let's take a look at the code for a thread safety issue:

public class count {
    public static int count=0;
    public static void add()
    {
        count++;
    }
    public static void main(String[] args) {
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                int i=0;
                for(i=0;i<10000;i++)
                {
                    add();
                }

            }
        });
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {
                int i=0;
                for(i=0;i<10000;i++)
                {
                    add();
                }
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            thread2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(count);

    }
}

Like this code, thread1 and thread2 each perform 10,000 self-increment operations on count, and theoretically the final value of count should be 20,000.

But let's take a look at the running results:

We see that the result is not 20,000. Let’s introduce the reason in detail below.

First of all? Let's first understand that the ++ operation is essentially divided into three steps: 1. First read the value in the memory into the CPU register, that is, the load operation 2. Perform +1 operation on the value in the CPU register add 3. Write the obtained result into the memory. These three operations are the three operations performed on the CPU, which are regarded as machine language.

If two threads execute count++ concurrently, it is equivalent to two sets of load add save execution. At this time, different thread scheduling orders may produce some differences in results.

This is a possible scheduling order. Since the threads are scheduled randomly, the scheduling order here is full of other possibilities.

The result of self-increment twice is 2, this situation is correct, there is no thread safety problem! !

But if this is the case:

At this time, we have performed two self-increment, and the count is indeed 1, and the expectation does not match the reality. At this time, the thread is not safe. 

Causes of thread safety issues:

1. Root cause: preemptive execution, random scheduling

2. Optimized structure: multiple threads modify a variable at the same time, one thread modifies a variable, multiple threads read the same variable, it's okay, multiple threads modify multiple different variables, it's okay.

3. Atomicity:

If the modification operation is non-atomic, it is prone to thread safety problems. count++ can be split into three operations: load, add, and save. If the ++ operation is atomic, the thread safety problem will be solved at this time.

4. Memory visibility issues

5. Instruction reordering

How to start with atomicity and solve thread safety problems. Locking!!!! Through locking, the non-atomic is converted into an atomic one.

Locking is said to ensure atomicity. In fact, it does not mean that the three operations here are completed at one time, nor is it that scheduling is not performed during the three-step operation, but that other threads that also want to operate are blocked and waited.

The locked keyword is synchronized

 public static int count=0;
    public synchronized static void add()
    {
        count++;
    }
    public static void main(String[] args) {
        Thread thread1=new Thread(new Runnable() {
            @Override
            public void run() {
                int i=0;
                for(i=0;i<10000;i++)
                {
                    add();
                }

            }
        });
        Thread thread2=new Thread(new Runnable() {
            @Override
            public void run() {
                int i=0;
                for(i=0;i<10000;i++)
                {
                    add();
                }
            }
        });
        thread1.start();
        thread2.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try {
            thread2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(count);

    }
}

If we add synchronized in front of the add() method, we will run again at this time.

At this time, 20000 is printed out, which is in line with expectations. 


Guess you like

Origin blog.csdn.net/m0_70386582/article/details/127982262