JAVA multi-threading is explained in great detail. If you don’t understand, hit me up.

1. Multi-threading basics

1.1 Threads and processes
process:

Refers to an application running in memory. Each process has an independent memory space. An application can run multiple processes at the same time. A process is also an execution process of a program and is the basic unit for the system to run programs. The system runs a A program is the process from creation, operation to death of a process.

Thread:

An independent execution unit within a process; a process can run multiple threads concurrently at the same time. It can be understood that a process is equivalent to a single-CPU operating system, and threads are multiple tasks running in this system.

2. How to create multi-threads

The first is to inherit the Thread class and override the run method (the return value cannot be set)

  • Create a java.lang.Threadsubclass that inherits from the class and override run()methods to define the tasks performed by the thread. Then, create an instance of the subclass and call start()the method to start the thread.

class MyThread extends Thread {
    public void run() {
        
    }
}

MyThread thread = new MyThread();
thread.start();

The second method implements the Runnable interface and overrides the run method (the return value cannot be set)

  • Create a class that implements java.lang.Runnablethe interface and implements its run()methods to define the tasks performed by the thread. Then, create an Threadobject, Runnablepass the object to it, and finally call start()the method to start the thread.

class MyRunnable implements Runnable {
    public void run() {
        
    }
}

Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();

The third implementation implements the Callable interface (thread return value Object can exist)

  • Create a java.util.concurrent.Callableclass that implements the interface, implements its call()methods to define the tasks performed by the thread, and can return a result. Use ExecutorServiceto submit Callabletasks and obtain execution results.

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class MyCallable implements Callable<String> {
    public String call() {
        
        return "Task completed";
    }
}

ExecutorService executor = Executors.newFixedThreadPool(2);
Callable<String> callable = new MyCallable();
Future<String> future = executor.submit(callable);

Advantages of implementing the Runnable interface over inheriting the Thread class:
  1. Suitable for multiple threads of the same program code to share the same resource.

  2. The limitations of single inheritance in Java can be avoided.

  3. Increase the robustness of the program and achieve decoupled operations. The code can be shared by multiple threads and the code and data are independent.

  4. The thread pool can only be placed into threads that implement Runable or callable classes, and cannot be directly placed into classes that inherit Thread.

Use thread pool :

  • Thread pool is a more advanced multi-thread management method that can reuse threads to perform multiple tasks. Use ExecutorServiceinterfaces to create and manage thread pools, and then submit()submit tasks through methods.

  1. import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    ExecutorService executor = Executors.newFixedThreadPool(2); 
    Runnable runnable = () -> {
        
    };
    executor.submit(runnable);
    
    

Each of these methods can be used to create multithreading, and the specific choice depends on your needs and design. Thread pools are an efficient way to reduce the overhead of thread creation and destruction and better manage the life cycle of threads. At the same time, Callablethe interface can be used to obtain the execution results of the task, and Runnableit is used to perform tasks that do not need to return results.

Thread pool workflow

A thread pool is a mechanism for managing and reusing threads, which can improve the performance and resource management efficiency of multi-threaded applications. The following is a typical thread pool workflow:

  1. Initialize the thread pool :

    • Create a thread pool and initialize its parameters, including the minimum number of threads, the maximum number of threads, task queue size, thread idle time, etc. The size of the thread pool is usually determined based on application requirements and system resources.

  2. Submit task :

    • When a task needs to be executed, the task is submitted to the thread pool. A task can be an Runnableor Callableobject that represents a unit of work that needs to be performed.

  3. Task queue :

    • The thread pool maintains a task queue, and all submitted tasks are queued in this queue waiting for execution. If there are threads available in the thread pool, they will take the task from the queue and execute it. If no threads are available, the task waits until one becomes available.

  4. Thread execution tasks :

    • Threads in the thread pool will cyclically retrieve tasks from the task queue and execute them. Once the task is completed, the thread will return to the thread pool, ready to perform the next task.

  5. Thread reuse :

    • The thread pool reuses threads instead of destroying them after each task. This reduces the overhead of thread creation and destruction and improves execution efficiency.

  6. Thread pool management :

    • The thread pool is responsible for managing the number and status of threads. It can dynamically adjust the number of threads as needed to accommodate different workloads. For example, the number of threads can be increased or decreased based on the number of tasks in the queue.

  7. Mission accomplished :

    • When the task execution is completed, the execution result of the task can be obtained (if the task is Callableof type). The results can then be processed or returned to the caller.

  8. Close the thread pool :

    • When a thread pool is no longer needed, it should be closed explicitly. Closing the thread pool will stop accepting new tasks and wait for submitted tasks to complete. Then the threads in the thread pool will be terminated. Closing the thread pool is to release resources and avoid memory leaks.

The main advantage of the thread pool is that it can effectively manage and reuse threads, reduce the overhead of thread creation and destruction, and improve the performance and response speed of the application. It can also control the number of concurrent threads to avoid resource exhaustion issues. Therefore, using a thread pool is generally a good practice in multi-threaded applications.

import java.util.concurrent.*;

public class ThreadPoolExample {

    public static void main(String[] args) {
        
        ThreadFactory threadFactory = Executors.defaultThreadFactory();

        
        RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();

        
        int corePoolSize = 5;
        int maxPoolSize = 10;
        long keepAliveTime = 60; 
        TimeUnit unit = TimeUnit.SECONDS; 
        int queueCapacity = 100; 

        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                keepAliveTime,
                unit,
                new LinkedBlockingQueue<>(queueCapacity),
                threadFactory,
                rejectedExecutionHandler
        );

        
        for (int i = 0; i < 10; i++) {
            final int taskId = i; 
            executor.execute(new Runnable() {
                public void run() {
                    System.out.println("Task " + taskId + " is executing by " +
                            Thread.currentThread().getName());
                    
                    
                }
            });
        }

        
        executor.shutdown();
    }
}

In this example, we first Executors.defaultThreadFactory()create a default thread factory for creating threads in the thread pool.

Then, we created a rejection policy ThreadPoolExecutor.AbortPolicy(), which means that when the thread pool is saturated (both the thread pool and the task queue are full), we refuse to accept new tasks and throw RejectedExecutionExceptionan exception.

Finally, we ThreadPoolExecutorpass the thread factory and rejection policy as additional parameters when creating.

By customizing the thread factory and rejection policy, we can more flexibly control the creation process of threads in the thread pool and the rejection processing of tasks.

3. Daemon thread

There are two types of threads in Java, one is the user thread and the other is the daemon thread. User threads refer to threads created by users. When the main thread stops, the user thread will not stop. Daemon thread When the process does not exist or the main thread is stopped, the daemon thread will also be stopped.

A daemon thread is a special thread running in a computer program. Its main feature is that when all non-daemon threads end, the daemon thread will automatically exit without waiting for the completion of the task.

Daemon threads are usually used to perform some background tasks, such as garbage collection, logging, etc. They perform tasks silently while the program is running, without blocking the execution of the main thread or other non-daemon threads.

Unlike ordinary threads, the life cycle of the daemon thread does not affect the life cycle of the entire program. When all non-daemon threads end, the daemon thread will be forced to exit, regardless of whether its task is completed.

It should be noted that daemon threads cannot be used to perform some important tasks because they may be forced to exit at any time. Additionally, daemon threads cannot catch or handle exceptions.

In summary, a daemon thread is a thread that performs tasks in the background and will automatically exit when all non-daemon threads end. They are usually used to perform some unimportant or periodic tasks.

thread1.setDaemon(true); 

4. Issues related to thread safety

Thread safety issues are caused by global variables and static variables. If there are only read operations for global variables and static variables in each thread, but no write operations, generally speaking, this global variable is thread-safe; if multiple threads perform write operations at the same time, thread synchronization generally needs to be considered, otherwise This may affect thread safety.

5.How to solve

When we use multiple threads to access the same resource, and there are write operations on the resource in multiple threads, thread safety issues are prone to occur. To solve the above security problem of concurrent access of a resource by multiple threads, Java provides a synchronization mechanism (synchronized) to solve it.

1. Synchronized code block (automatic lock) (weight lock)

2. Synchronization method

3. Lock synchronization (manual lock)

ReentrantLock lock = new ReentrantLock()
 lock.lock()
 sell(name)
 lock.unlock()

Interview question: JVM instruction set

Which lock has better performance, lock or syn? Before 1.8, lock was stronger. In 1.8 (including), there was no difference between syn and lock.

  1. What is the difference between synchronized code blocks and synchronized methods? The lock object of different synchronization methods is this. The lock object of the synchronization code block is any object (must be unique)

  2. Synchronized implementation principle? monitorenter and monitorexit bytecode instructions

  3. What is the difference between lock and synchronized?

  4. Is lock an optimistic lock or a pessimistic lock? Depends on the implementation class ReentrantLock pessimistic lock read-write lock optimistic lock

  5. Is ReentrantLock a fair lock or an unfair lock? No participation is not fair, participation on behalf of others is fair lock

Using locks will cause ---- deadlock: threads waiting for each other.

Multi-thread deadlock: synchronization is nested within synchronization, causing the lock to be unable to be released

How to avoid: Try to nest locks within locks

6. Thread status

Status description: NEW (new) : The thread has just been created, but has not been started.

RUNNABLE : A state in which a thread can run in the Java virtual machine. It may or may not be running its own code, depending on the operating system processor.

BLOCKED : When a thread attempts to acquire an object lock and the object lock is held by another thread, the thread enters the Blocked state; when the thread holds the lock, the thread will become Runnable state.

WAITING (infinite waiting) : When a thread is waiting for another thread to perform a (wake-up) action, the thread enters the Waiting state. After entering this state, it cannot be woken up automatically. You must wait for another thread to call the notify or notifyAll method before waking up.

TIMED_WAITING (timed waiting) : Same as the waiting state, several methods have timeout parameters, and calling them will enter the Timed Waiting state. This state will remain until the timeout expires or a wake-up notification is received. Commonly used methods with timeout parameters include Thread.sleep and Object.wait.

TERMINATED (terminated) : died because the run method exited normally, or died because an uncaught exception terminated the run method.

wait() puts the thread in a waiting state and requires manual wakeup to release the current lock resource.
sleep() will not release the lock, allowing the thread to wake up naturally in a waiting state
  • For the sleep() method, you must first know that this method belongs to the Thread class. The wait() method belongs to the Object class.

  • The sleep() method causes the program to suspend execution for the specified time and give up the CPU to other threads, but its monitoring status is still maintained. When the specified time is up, it will automatically resume running status.

    wait() hands over control, and then enters the waiting lock pool waiting for this object in a waiting state. Only after calling the notify() method for this object does this thread enter the object lock pool to prepare to acquire the object lock and enter the running state.

  • During the call to sleep() method, the thread will not release the object lock. When the wait() method is called, the thread will give up the object lock.

7. Thread ends:

There are three ways to end a thread: (1) Set the exit flag so that the thread exits normally. (2) Use the interrupt() method to interrupt the thread. (3) Use the stop method to forcibly terminate the thread (using Thread.stop is not recommended, this method of terminating thread running has been abandoned, and using them is extremely unsafe!)

Thread.sleep(1000l);

t.interrupt();
t.stop(); 

8. Thread priority

Today's operating systems basically use time-sharing to schedule running threads. The number of time slices allocated to a thread determines how much processor resources the thread uses, and also corresponds to the concept of thread priority.

In the JAVA thread, the priority is controlled through an int priority, ranging from 1-10, with 10 being the highest and the default value being 5.

Thread priority does not reflect the execution order of threads, it just allows the current thread to obtain more CPU resources.

Priority can increase the amount of CPU resources a thread obtains, but it cannot determine the execution order of threads.

t.setPriority(1);  

join() method (let threads execute sequentially)

The function of join is to make other threads wait. thread.Join adds the specified thread to the current thread, and can merge two alternately executing threads into sequential execution threads. For example, if the Join() method of thread A is called in thread B, thread B will not continue to execute until thread A completes execution.

yield method

The role of the Thread.yield() method: pause the currently executing thread and execute other threads. (May have no effect) yield() returns the currently running thread to a runnable state to allow other threads with the same priority to get a chance to run. Therefore, the purpose of using yield() is to allow appropriate rotation of execution between threads with the same priority. However, in practice, there is no guarantee that yield() will achieve the purpose of yielding, because the yielding thread may be selected again by the thread scheduler.

9. Three characteristics of multi-thread concurrency (key points)

Atomicity: that is, an operation or multiple operations are either fully executed and the execution process is not interrupted by any factors, or they are not executed at all.

Visibility: When multiple threads access the same variable, and one thread modifies the value of the variable, other threads can immediately see the modified value (volitale)

Orderliness: The order of program execution is based on the order of code execution.

Solution to visibility issues:

1. Solve the visibility problem in a synchronous manner

while (flag) {
            synchronized (this) {
            }
        }

Before the thread is unlocked (when exiting the synchronized code block): the latest value of the shared variable in its own working memory must be refreshed to the main memory.

When the thread locks (when entering the synchronized code block): the value of the shared variable in the local memory will be cleared, so when using the shared variable, you need to re-read the latest value from the main memory (locking and unlocking are the same lock)

spin lock

The so-called spin lock is to let the thread wait for a period of time without being suspended immediately to see whether the thread holding the lock will release the lock soon. How to wait? Just execute a meaningless loop (spin).

Adapt to spin lock

That is, adaptive spin lock. The so-called adaptive means that the number of spins is no longer fixed. It is determined by the previous spin time on the same lock and the status of the lock owner.

Lock elimination (implementation of JDK object Syn optimization)

In order to ensure the integrity of the data, we need to perform synchronization control on this part of the operation when performing operations. However, in some cases, the JVM detects that there is no shared data competition, which is why the JVM will eliminate these synchronization locks. The basis for lock elimination is the data support of escape analysis.

The JVM can clearly detect that the variable vector has not escaped from the method vectorTest(), so the JVM can boldly eliminate the locking operation inside the vector.

About the definition of Java escape analysis:

Escape Analysis (Escape Analysis) is simply a technology that allows the Java Hotspot virtual machine to analyze the usage range of newly created objects and decide whether to allocate memory on the Java heap.

Lock down foul language

However, a series of continuous locking and unlocking operations may cause unnecessary performance loss, so the concept of lock coarsening is introduced.

It is to connect multiple consecutive locking and unlocking operations together and expand them into a larger lock.

Weight lock (SYN)

The operating system needs to implement switching between threads from
The switching cost from user mode to kernel mode is very high.

10.Volatile introduction (interview point)

Interview question: Can volatile guarantee thread safety? Why?

No, volatile can only guarantee visibility and order, not atomicity.

Function: Solve the problem of memory visibility
public volatile boolean flag = true;

The process of implementing memory visibility with Volatile

The process of thread writing Volatile variables:
  1. Change the value of a copy of a Volatile variable in thread local memory;

  2. Flushes the changed copy value from local memory to main memory

The process of thread reading Volatile variables:
  1. Read the latest value of the Volatile variable from main memory to the thread's local memory

  2. Read a copy of a Volatile variable from local memory

Volatile implements memory visibility principle:

During a write operation, a store barrier instruction is added after the write operation instruction so that the value of the variable in the local memory can be refreshed to the main memory.

During the read operation, by adding a load barrier instruction before the read operation, the value of the variable in the main memory can be read in time.

Volatile cannot guarantee atomicity

solution:

  1. Use synchronized (not recommended)

    public synchronized void addCount() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }
    
    
  2. Use ReentrantLock (reentrant lock)

    private Lock lock = new ReentrantLock();
    public void addCount() {
        for (int i = 0; i < 10000; i++) {
            lock.lock();
            count++;
            lock.unlock();
        }
    }
    
    
  3. Using AtomicInteger (atomic operations)

public static AtomicInteger count = new AtomicInteger(0);
public void addCount() {
    for (int i = 0; i < 10000; i++) {
        
        count.incrementAndGet();
    }
}

Introduction to CAS

What is CAS?

CAS: Compare and Swap, that is, compare and exchange.

jdk5 adds the concurrent package java.util.concurrent.*, and the following classes use the CAS algorithm to implement an optimistic lock that is different from the synchronouse synchronization lock. Before JDK 5, the Java language relied on the synchronized keyword to ensure synchronization. This is an exclusive lock and a pessimistic lock.

CAS algorithm understanding

Understanding CAS, CAS is a lock-free algorithm (optimistic locking). CAS has 3 operands, the memory value V, the old expected value A, and the new value to be modified B. If and only if the expected value A and the memory value V are the same, modify the memory value V to B, otherwise do nothing.

If there are three threads that want to modify the value of an AtomicInteger concurrently, their underlying mechanisms are as follows:

1. First, each thread will first obtain the current value. Then perform an atomic CAS operation. Atomic means that this CAS operation must be completely executed by yourself and will not be interrupted by others.

2. Then in the CAS operation, it will be compared to see if your value now is the value I just obtained. If it is, it means no one has changed this value, then you can set it to a value after accumulating 1.

3. In the same way, if someone is executing CAS and finds that the value he obtained before is different from the current value, it will cause CAS to fail. After the failure, it will enter an infinite loop, obtain the value again, and then perform the CAS operation.

CAS defects

Although CAS efficiently solves atomic operations, it still has some flaws, mainly manifested in three methods: the cycle time is too long, only one shared variable atomic operation can be guaranteed, and the ABA problem

There is a problem:

1. Maybe cas will always fail and then spin

2. If a value was originally A, changed to B, and then changed to A again, then during the CAS check it will be found that there has been no change, but in fact it has changed. This is the so-called ABA problem. The solution to the ABA problem is to add a version number, that is, add a version number to each variable, and add 1 every time it changes, that is, A —> B —> A, becomes 1A —> 2B —> 3A.

AQS of JAVA

What is AQS? (lock acquisition and lock release)

It is just an abstract class, but many components in JUC are based on this abstract class. It can also be said that this AQS is the basis of most JUC components.

Used under the JUC package, the core component AQS (AbstractQueuedSynchronizer), that is, the queue synchronizer.

JAVA Lock

ReentrantLock Reentrant lock (pessimistic lock)

Get the lock sync.lock();

Release the lock sync.release(1);

The difference between ReentrantLock and synchronized

1. It has more functions than synchronized and is more scalable.

2. Treat thread waiting and wake-up operations in more detail and flexibility.

3.ReentrantLock provides pollable lock requests. It will try to acquire the lock, and if successful, continue, otherwise it can wait until the next run, and synchronized will either succeed or block once the lock request is entered, so compared to synchronized, ReentrantLock will be less prone to deadlock.

4.ReentrantLock supports more flexible synchronized code blocks, but when using synchronized, it can only be obtained and released in the same synchronized block structure.

5.RentrantLock supports interrupt processing, and its performance is better than synchronized.

Read-write lock ReentrantReadWriteLock (implementation of optimistic lock)

A read-write lock maintains a pair of locks, a read lock and a write lock. By separating the read lock and the write lock, the concurrency is greatly improved compared to the general mutex lock: multiple read threads can be allowed to access at the same time, but when the write thread accesses, all read threads and write threads will blocked.

Guess you like

Origin blog.csdn.net/weixin_54542328/article/details/133352056