[Concurrent programming eight-part] The three major characteristics of processes, threads, and concurrent programming

What are the concepts of process and thread?

A process refers to a running program. For example, when using DingTalk or the browser, you need to start this program, and the operating system will allocate certain resources to this program (occupying memory resources). Thread is the basic unit of CPU scheduling. Each thread executes a certain fragment of the code of a certain process.

The concepts of serial, parallel and concurrency?

Serialization means queuing up one by one, and only the second one can join after the first one has finished.
Parallelism means simultaneous processing.
The concurrency here is not the high concurrency problem of the three high schools, but the concept of concurrency in multi-threading (the concept of CPU scheduling threads). The CPU repeatedly switches to execute different threads in a very short period of time. It seems to be parallel, but it is just a high-speed switching of the CPU.

Parallelism encompasses concurrency.
Parallelism means that a multi-core CPU schedules multiple threads at the same time, which is true multiple threads executing at the same time.
A single-core CPU cannot achieve parallel effects, but a single-core CPU is concurrency.

What are the concepts of synchronous, asynchronous, blocking and non-blocking?

Synchronous and asynchronous: After executing a certain function, whether the callee will actively feedback information.
Blocking and non-blocking: After executing a function, does the caller need to wait for feedback on the result?

The two concepts may seem similar, but their focus is completely different.
Synchronous blocking : For example, if you use a pot to boil water, you will not be notified when the water is boiling. After the water boiling is started, you need to wait for the water to boil.

Synchronous non-blocking : For example, if you use a pot to boil water, you will not be notified when the water is boiled. After the water boiling is started, you do not need to wait for the water to boil. You can perform other functions, but you need to check whether the water is boiling from time to time.

Asynchronous blocking : For example, if you boil water with a kettle, after the water boils, you will be notified that the water is boiled. After the water boiling is started, you need to wait for the water to boil.

Asynchronous non-blocking : For example, if you boil water with a kettle, after the water boils, you will be notified that the water is boiled. After the water boiling is started, there is no need to wait for the water to boil and you can perform other functions.

Asynchronous non-blocking has the best effect. During normal development, the best way to improve efficiency is to use asynchronous non-blocking to handle some multi-threaded tasks.

How are threads created?

Inherit the Thread class and override the run method

To start a thread, call the start method, which will create a new thread and perform the thread's tasks. If you call the run method directly, this will cause the current thread to execute the business logic in the run method.

class Thread implements Runnable {
    
    }
public class MiTest {
    
    
    public static void main(String[] args) {
    
    
        MyJob t1 = new MyJob();
        t1.start();
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("main:" + i);
        }
    }
}
class MyJob extends Thread{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("MyJob:" + i);
        }
    }
}
Implement the Runnable interface and override the run method
public class MiTest {
    
    
    public static void main(String[] args) {
    
    
        MyRunnable myRunnable = new MyRunnable();
        Thread t1 = new Thread(myRunnable);
        t1.start();
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("main:" + i);
        }
    }
}
class MyRunnable implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("MyRunnable:" + i);
        }
    }
}
Implement Callable, rewrite the call method, and cooperate with FutureTask

Callable is generally used for non-blocking execution methods that return results. Synchronous and non-blocking.

public class FutureTask<V> implements RunnableFuture<V> {
    
    }

public interface RunnableFuture<V> extends Runnable, Future<V> {
    
    }
public class MiTest {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        //1. 创建MyCallable
        MyCallable myCallable = new MyCallable();
        //2. 创建FutureTask,传入Callable
        FutureTask futureTask = new FutureTask(myCallable);
        //3. 创建Thread线程
        Thread t1 = new Thread(futureTask);
        //4. 启动线程
        t1.start();
        //5. 做一些操作
        //6. 要结果
        Object count = futureTask.get();
        System.out.println("总和为:" + count);
    }
}
class MyCallable implements Callable{
    
    
    @Override
    public Object call() throws Exception {
    
    
        int count = 0;
        for (int i = 0; i < 100; i++) {
    
    
            count += i;
        }
        return count;
    }
}
Build threads based on thread pool

The task is submitted to the thread pool, and the thread pool creates a worker thread to execute the task.

public class ThreadPoolExecutor extends AbstractExecutorService {
    
    
...
private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
    
    
    ...
}
Anonymous inner classes and lambda expression approach
// 匿名内部类方式:
  Thread t1 = new Thread(new Runnable() {
    
    
      @Override
      public void run() {
    
    
          for (int i = 0; i < 1000; i++) {
    
    
              System.out.println("匿名内部类:" + i);
          }
      }
  });
  
// lambda方式:
  Thread t2 = new Thread(() -> {
    
    
      for (int i = 0; i < 100; i++) {
    
    
          System.out.println("lambda:" + i);
      }
  });
Summary: There is only one way to pursue the bottom layer, to implement Runnble

What are the states of threads? What are the states of threads in Java?

Operating system level: 5 states (generally for traditional thread states)
Insert image description here
6 states in Java
Insert image description here

Insert image description here

  • NEW : The Thread object is created, but the start method has not been executed yet.

  • RUNNABLE : When the Thread object calls the start method, it is in the RUNNABLE state (CPU scheduling/no scheduling).

  • BLOCKED : synchronized does not get the synchronization lock and is blocked.

  • WAITING : Calling the wait method will put you in the WAITING state and needs to be awakened manually.

  • TIME_WAITING : Calling the sleep method or join method will automatically wake up, no need to manually wake up.

  • TERMINATED : The run method is executed and the thread life cycle is over.

BLOCKED , WAITING , TIME_WAITING : can all be understood as blocking and waiting states, because in these three states, the CPU will not schedule the current thread

Examples of 6 states in Java

//NEW:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
    });
    System.out.println(t1.getState());
}


//RUNNABLE:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while(true){
    
    
        }
    });
    t1.start();
    Thread.sleep(500);
    System.out.println(t1.getState());
}


//BLOCKED:
public static void main(String[] args) throws InterruptedException {
    
    
    Object obj = new Object();
    Thread t1 = new Thread(() -> {
    
    
        // t1线程拿不到锁资源,导致变为BLOCKED状态
        synchronized (obj){
    
    
        }
    });
    // main线程拿到obj的锁资源
    synchronized (obj) {
    
    
        t1.start();
        Thread.sleep(500);
        System.out.println(t1.getState());
    }
}


//WAITING:
public static void main(String[] args) throws InterruptedException {
    
    
    Object obj = new Object();
    Thread t1 = new Thread(() -> {
    
    
        synchronized (obj){
    
    
            try {
    
    
                obj.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    });
    t1.start();
    Thread.sleep(500);
    System.out.println(t1.getState());
}


//TIMED_WAITING:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(500);
    System.out.println(t1.getState());
}


//TERMINATED:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        try {
    
    
            Thread.sleep(500);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(1000);
    System.out.println(t1.getState());
}

What are the common methods of threads, with examples?

Get the current thread

Thread's static method obtains the current thread object

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
	// 获取当前线程的方法
    Thread main = Thread.currentThread();
    System.out.println(main);
    // "Thread[" + getName() + "," + getPriority() + "," +  group.getName() + "]";
    // Thread[main,5,main]
}
Set the name of the thread

After constructing the Thread object, be sure to set a meaningful name to troubleshoot errors later.

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        System.out.println(Thread.currentThread().getName());
    });
    t1.setName("模块-功能-计数器");
    t1.start();
}
Set thread priority

In fact, it is the priority of the CPU scheduling thread. There are 10 levels of priority set for threads in Java, and any integer is taken from 1 to 10. If it exceeds this range, parameter exception errors will be eliminated.

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("t1:" + i);
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("t2:" + i);
        }
    });
    t1.setPriority(1);
    t2.setPriority(10);
    t2.start();
    t1.start();
}
Set concessions for threads

You can use Thread's static method yield to change the current thread from the running state to the ready state.

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            if(i == 50){
    
    
                Thread.yield();
            }
            System.out.println("t1:" + i);
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("t2:" + i);
        }
    });
    t2.start();
    t1.start();
}
Set thread sleep

The static method sleep of Thread allows the thread to change from running state to waiting state. sleep has two method overloads:

  • The first one is modified by native, which has the effect of turning the thread into a waiting state.
  • The second is a method that can pass in milliseconds and a nanosecond (if the nanosecond value is greater than or equal to 0.5 milliseconds, add 1 to the dormant millisecond value. If the passed in millisecond value is 0 and the nanosecond value is not 0, then Sleep for 1 millisecond)
// sleep 会抛出一个 InterruptedException
public static void main(String[] args) throws InterruptedException {
    
    
    System.out.println(System.currentTimeMillis());
    Thread.sleep(1000);
    System.out.println(System.currentTimeMillis());
}
Set thread preemption

Thread's non-static method join method needs to be called on a certain thread.

If t1.join() is called in the main thread, the main thread will enter the waiting state and needs to wait for all t1 threads to complete execution, and then return to the ready state to wait for CPU scheduling.

If t1.join(2000) is called in the main thread, the main thread will enter the waiting state and need to wait for t1 to execute for 2 seconds before returning to the ready state and waiting for CPU scheduling. If t1 has ended during the waiting period, the main thread automatically becomes ready and waits for CPU scheduling.

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            System.out.println("t1:" + i);
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    });
    t1.start();
    for (int i = 0; i < 10; i++) {
    
    
        System.out.println("main:" + i);
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        if (i == 1){
    
    
            try {
    
    
                t1.join(2000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}
Set up daemon thread

By default, threads are all non-daemon threads, and the JVM will end the current JVM when there are no non-daemon threads in the program.
The main thread is a non-daemon thread by default. If the execution of the main thread ends, you need to check whether there are any non-daemon threads in the current JVM. If there is no JVM, stop it directly.

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            System.out.println("t1:" + i);
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    });
    t1.setDaemon(true);
    t1.start();
}
Set thread waiting and wake-up

The thread that acquires the synchronized lock resource can enter the lock waiting pool through the wait method , and the lock resource will be released.
The thread that acquires the synchronized lock resource can wake up the thread in the waiting pool and add it to the lock pool through the notify or notifyAll method.

notify randomly wakes up a thread in the waiting pool to the lock pool.
notifyAll will wake up all threads in the waiting pool and add them to the lock pool.

When calling the wait method, notify and norifyAll methods, it must be done inside a synchronized modified code block or method, because information maintenance based on a certain object's lock needs to be operated.

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        sync();
    },"t1");

    Thread t2 = new Thread(() -> {
    
    
        sync();
    },"t2");
    t1.start();
    t2.start();
    Thread.sleep(12000);
    synchronized (MiTest.class) {
    
    
        MiTest.class.notifyAll();
    }
}

public static synchronized void sync()  {
    
    
    try {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            if(i == 5) {
    
    
                MiTest.class.wait();
            }
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName());
        }
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
}

What are the ways to end a thread?

There are many ways to end a thread. The most commonly used method is to end the thread's run method, whether it is ended by return or by throwing an exception.

stop method (not used)

Force the thread to end, no matter what you are doing, it is not recommended

    @Deprecated
    public final void stop() {
    
    ...}
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        try {
    
    
            Thread.sleep(5000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(500);
    t1.stop();
    System.out.println(t1.getState());
}
Use shared variables (rarely used)

This method is not used much, and some threads may keep running through infinite loops.
You can break the infinite loop by modifying the shared variables, let the thread exit the loop, and end the run method.

static volatile boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while(flag){
    
    
            // 处理任务
        }
        System.out.println("任务结束");
    });
    t1.start();
    Thread.sleep(500);
    flag = false;
}
interrupt mode

By default, there is an interrupt flag bit inside the thread interrupt flag bit: false

Shared variable mode

public static void main(String[] args) throws InterruptedException {
    
    
    // 线程默认情况下,    interrupt标记位:false
    System.out.println(Thread.currentThread().isInterrupted());
    // 执行interrupt之后,再次查看打断信息
    Thread.currentThread().interrupt();
    // interrupt标记位:ture
    System.out.println(Thread.currentThread().isInterrupted());
    // 返回当前线程,并归位为 false,interrupt标记位:ture
    System.out.println(Thread.interrupted());
    // 已经归位了
    System.out.println(Thread.interrupted());

    // =====================================================
    Thread t1 = new Thread(() -> {
    
    
        while(!Thread.currentThread().isInterrupted()){
    
    
            // 处理业务
        }
        System.out.println("t1结束");
    });
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
}

By interrupting the thread in WAITING or TIMED_WAITING state, throwing an exception and handling it yourself.
This method of stopping threads is the most commonly used one, and is also the most common in frameworks and JUCs.

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while(true){
    
    
            // 获取任务
            // 拿到任务,执行任务
            // 没有任务了,让线程休眠
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
                System.out.println("基于打断形式结束当前线程");
                return;
            }
        }
    });
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
}

What is the difference between wait and sleep?

  • sleep is a static method in the Thread class, and wait is a method in the Object class.
  • sleep belongs to TIMED_WAITING and is automatically awakened. wait belongs to WAITING and needs to be awakened manually.
  • The sleep method is executed while holding the lock and will not release the lock resources. After the wait method is executed, the lock resources will be released.
  • sleep can be executed while holding the lock or not holding the lock. The wait method must be executed only when there is a lock.

The wait method will throw the thread holding the lock from _owner to the _WaitSet collection. This operation is modifying the ObjectMonitor object. If the synchronized lock is not held, the ObjectMonitor object cannot be operated.

What are the three major characteristics of concurrent programming?

  • atomicity
  • visibility
  • Orderliness

atomicity

concept of atomicity

JMM (Java Memory Model). Different hardware and different operating systems have certain differences in memory operations. In order to solve various problems that occur with the same code on different operating systems, Java uses JMM to shield the differences caused by various hardware and operating systems.

Make Java's concurrent programming cross-platform.

JMM stipulates that all variables will be stored in the main memory . During operation, a copy needs to be copied from the main memory to the thread memory (CPU memory) to perform calculations within the thread. Then write it back to main memory (not necessarily!).

Definition of atomicity: Atomicity means that an operation is indivisible and uninterruptible. When one thread is executing, another thread will not affect it.

The atomicity of concurrent programming is illustrated in code:

private static int count;

public static void increment(){
    
    
    try {
    
    
        Thread.sleep(10);
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
    count++;
}

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
           increment();
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            increment();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

Current program: When multi-threaded operations share data, the expected results are inconsistent with the final results.

Atomicity: Multi-threaded operations on critical resources, the expected results are consistent with the final results.

Through the analysis of this program, it can be seen that the operation of ++ is divided into three parts. First, the thread gets the data from the main memory and saves it to the CPU register, then performs a +1 operation in the register, and finally the result Write back to main memory.

Ensure atomicity of concurrent programming
synchronized

Because the ++ operation can be viewed from the instruction

image.png

You can add the synchronized keyword to the method or use a synchronized code block to ensure atomicity

synchronized can prevent multiple threads from operating critical resources at the same time. At the same time, only one thread will be operating critical resources.

image.png

CAS

What exactly is CAS?

compare and swap is comparison and swap, which is a CPU concurrency primitive.

When replacing the value at a certain location in the memory, he first checks whether the value in the memory is consistent with the expected value, and if so, performs the replacement operation. This operation is an atomic operation.

Unsafe-based classes in Java provide methods for operating CAS, and the JVM will help us implement the methods into CAS assembly instructions.

But be aware that CAS is just comparison and exchange. You need to implement the operation of obtaining the original value yourself.

private static AtomicInteger count = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            count.incrementAndGet();
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            count.incrementAndGet();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

Doug Lea helped us implement some atomic classes based on CAS, including the AtomicInteger we see now, and many other atomic classes...

Disadvantages of CAS : CAS can only guarantee that the operation of a variable is atomic, and cannot achieve atomicity for multiple lines of code.

CAS questions :

  • ABA problem : The problem is as follows. The version number can be introduced to solve the ABA problem. Java provides an operation for appending version numbers to each version when a class is in CAS. AtomicStampeReferenceimage.png
  • When AtomicStampedReference is in CAS, it will not only judge the original value, but also compare the version information.
  • public static void main(String[] args) {
          
          
        AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA",1);
    
        String oldValue = reference.getReference();
        int oldVersion = reference.getStamp();
    
        boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
        System.out.println("修改1版本的:" + b);
    
        boolean c = reference.compareAndSet("B", "C", 1, 1 + 1);
        System.out.println("修改2版本的:" + c);
    }
    
  • The problem of too long spin time :
    • You can specify how many times CAS will loop in total. If it exceeds this number, it will directly fail/or hang the thread. (Spin lock, adaptive spin lock)
    • After CAS fails once, this operation can be temporarily stored. When the results need to be obtained later, all the temporary operations can be executed and the final results can be returned.
LockLock

Lock was developed by Doug Lea in JDK1.5. Its performance is much better than that of synchronized in JDK1.5. However, after JDK1.6 optimized synchronized, the performance is not much different, but if it involves When there is a lot of concurrency, ReentrantLock is recommended, and the performance will be better.

Method to realize:

private static int count;

private static ReentrantLock lock = new ReentrantLock();

public static void increment()  {
    
    
    lock.lock();
    try {
    
    
        count++;
        try {
    
    
            Thread.sleep(10);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    } finally {
    
    
        lock.unlock();
    }


}

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            increment();
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            increment();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

ReentrantLock can be directly compared to synchronized. Functionally, they are both locks.

But ReentrantLock has richer functionality than synchronized.

The bottom layer of ReentrantLock is implemented based on AQS, and there is a state variable maintained based on CAS to implement lock operations.

ThreadLocal

Four reference types in Java

The reference types used in Java are strong, soft, weak, and virtual .

User user = new User();

The most common thing in Java is strong reference. When an object is assigned to a reference variable, the reference variable is a strong reference. When an object is referenced by a strong reference variable, it is always in a reachable state, and it is impossible to be recycled by the garbage collection mechanism. Even if the object will never be used in the future, the JVM will not collect it. Therefore, strong references are one of the main causes of Java memory leaks.

SoftReference

Secondly, there are soft references. For objects with only soft references, they will not be recycled when the system memory is sufficient, and they will be recycled when the system memory space is insufficient. Soft references are often used in memory-sensitive programs as caches.

Then there are weak references, which have a shorter lifetime than soft references. For objects with only weak references, as soon as the garbage collection mechanism runs, regardless of whether the JVM's memory space is sufficient, the memory occupied by the object will always be reclaimed. It can solve the problem of memory leaks. ThreadLocal solves the problem of memory leaks based on weak references.

Finally, there is the virtual reference, which cannot be used alone and must be used in conjunction with a reference queue. The main function of virtual references is to track the status of objects being garbage collected. However, in development, we still use strong references more often.

The way ThreadLocal ensures atomicity is to prevent multiple threads from operating critical resources and allow each thread to operate its own data.

Code

static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();

public static void main(String[] args) {
    
    
    tl1.set("123");
    tl2.set("456");
    Thread t1 = new Thread(() -> {
    
    
        System.out.println("t1:" + tl1.get());
        System.out.println("t1:" + tl2.get());
    });
    t1.start();

    System.out.println("main:" + tl1.get());
    System.out.println("main:" + tl2.get());
}

ThreadLocal implementation principle:

  • Each Thread stores a member variable, ThreadLocalMap
  • ThreadLocal itself does not store data. It is like a tool class that operates ThreadLocalMap based on ThreadLocal.
  • ThreadLocalMap itself is implemented based on Entry[], because one thread can bind multiple ThreadLocals, so multiple data may need to be stored, so it is implemented in the form of Entry[].
  • Each existing ThreadLocalMap has its own independent ThreadLocalMap, and then uses the ThreadLocal object itself as the key to access the value.
  • The key of ThreadLocalMap is a weak reference. The characteristic of weak reference is that even if there is a weak reference, it must be recycled during GC. This is to prevent the ThreadLocal object from being recycled if the reference to the key is a strong reference after the ThreadLocal object loses its reference.

ThreadLocal memory leak problem:

  • If the ThreadLocal reference is lost, the key will be recycled by the GC because of the weak reference. If the thread has not been recycled at the same time, it will cause a memory leak, and the value in the memory cannot be recycled and cannot be obtained.
  • You only need to call the remove method in time to remove the Entry after using the ThreadLocal object.

image.png

visibility

visibility concept

The visibility problem occurs based on the location of the CPU. The CPU processing speed is very fast. Compared with the CPU, it is too slow to obtain data from the main memory. The CPU provides three-level caches of L1, L2, and L3. Every time it goes to the main memory, After the data is retrieved from the memory, it will be stored in the Level 3 cache of the CPU. Every time the data is retrieved from the Level 3 cache, the efficiency will definitely improve.

This has brought about problems. Nowadays, CPUs are multi-core, and the working memory (CPU third-level cache) of each thread is independent. When making modifications, each thread will be notified that only its own working memory will be modified, and there will be no timely response. Synchronized to main memory, causing data inconsistency issues.

image.png

Code logic for visibility issues

private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            // ....
        }
        System.out.println("t1线程结束");
    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
Ways to solve visibility
volatile

Volatile is a keyword used to modify member variables.

If an attribute is modified volatile, it is equivalent to telling the CPU that the operation of the current attribute is not allowed to use the CPU cache and must operate with the main memory.

Memory semantics of volatile:

  • The volatile attribute is written: When writing a volatile variable, JMM will promptly refresh the CPU cache corresponding to the current thread to the main memory.
  • The volatile attribute is read: When reading a volatile variable, JMM will set the corresponding memory in the CPU cache to invalid, and the shared variable must be re-read from the main memory.

In fact, adding volatile is to inform the CPU that the read and write operations of the current attribute are not allowed to use the CPU cache. The attribute modified with volatile will be appended with a lock prefix after being converted to assembly. When the CPU executes this instruction, if Prefixing it with lock will do two things:

  • Write the current processor cache line data back to main memory
  • The data written back is directly invalid in the cache of other CPU cores.

Summary: Volatile means that every time the CPU operates on this data, it must immediately synchronize to the main memory and read the data from the main memory.

private volatile static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            // ....
        }
        System.out.println("t1线程结束");
    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
synchronized

Synchronized can also solve visibility problems, synchronized memory semantics.

If a synchronized synchronization code block or synchronization method is involved, after acquiring the lock resource, the internal variables involved will be removed from the CPU cache, and the data must be retrieved from the main memory, and after the lock is released, the CPU will immediately Data in cache is synchronized to main memory.

private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            synchronized (MiTest.class){
    
    
                //...
            }
            System.out.println(111);
        }
        System.out.println("t1线程结束");

    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
Lock

The way Lock ensures visibility is completely different from synchronized. Based on its memory semantics, synchronized synchronizes the CPU cache to main memory when acquiring and releasing locks.

Lock is implemented based on volatile. When locking and releasing the lock inside the Lock lock, a state attribute modified by volatile will be added or subtracted.

If a write operation is performed on a volatile-modified attribute, the CPU will execute the instruction with the lock prefix, and the CPU will immediately synchronize the modified data from the CPU cache to the main memory, and will also immediately synchronize other attributes to the main memory. . This data in other CPU cache lines will also be set to invalid and must be pulled from main memory again.

private static boolean flag = true;
private static Lock lock = new ReentrantLock();

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            lock.lock();
            try{
    
    
                //...
            }finally {
    
    
                lock.unlock();
            }
        }
        System.out.println("t1线程结束");

    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
final

Final-modified attributes are not allowed to be modified during runtime. In this way, visibility is indirectly guaranteed. All multi-threads reading final attributes must have the same value.

Final does not mean that every time the data is retrieved, it is read from the main memory. It is not necessary, and final and volatile are not allowed to modify an attribute at the same time.

The content modified by final is no longer allowed to be written again, and volatile ensures that each read and write data is read from the main memory, and volatile will affect certain performance, so there is no need to modify it at the same time.

image.png

Orderliness

concept of orderliness

In Java, the content in the .java file will be compiled and needs to be converted again into instructions that the CPU can recognize before execution. When the CPU executes these instructions, in order to improve the execution efficiency without affecting the final result (satisfying some requirements), the instructions will be rearranged.

The reason why instructions are executed out of order is to maximize the performance of the CPU.

Programs in Java are executed out of order.

Java program verifies the effect of out-of-order execution:

static int a,b,x,y;

public static void main(String[] args) throws InterruptedException {
    
    
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
    
    
        a = 0;
        b = 0;
        x = 0;
        y = 0;

        Thread t1 = new Thread(() -> {
    
    
            a = 1;
            x = b;
        });
        Thread t2 = new Thread(() -> {
    
    
            b = 1;
            y = a;
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        if(x == 0 && y == 0){
    
    
            System.out.println("第" + i + "次,x = "+ x + ",y = " + y);
        }
    }
}

Singleton mode may have problems due to instruction reordering:

Threads may get uninitialized objects, which may cause some unnecessary problems when using them because the internal properties have default values.

private static volatile MiTest test;

private MiTest(){
    
    }

public static MiTest getInstance(){
    
    
    // B
    if(test  == null){
    
    
        synchronized (MiTest.class){
    
    

            if(test == null){
    
    
                // A   ,  开辟空间,test指向地址,初始化
                test = new MiTest();
            }
        }
    }
    return test;
}
as-if-serial

as-if-serial semantics:

No matter how the reordering is specified, it is necessary to ensure that the execution result of the single-threaded program remains unchanged.

And if there are dependencies, instructions cannot be rearranged.

// 这种情况肯定不能做指令重排序
int i = 0;
i++;

// 这种情况肯定不能做指令重排序
int j = 200;
j * 100;
j + 100;
// 这里即便出现了指令重排,也不可以影响最终的结果,20100
happens-before

Specific rules:
  1. Single-threaded happen-before principle: In the same thread, write the previous operation happen-before the subsequent operation.
  2. The happen-before principle of locks: The unlock operation of the same lock happens-before the lock operation of this lock.
  3. The happens-before principle of volatile: A write operation on a volatile variable happens-before any operation on this variable.
  4. The transitivity principle of happen-before: If A operates happen-before B, and B happens-before C, then A happens-before C.
  5. The happen-before principle of thread startup: the start method of the same thread happens-before other methods of this thread.
  6. The happen-before principle of thread interruption: The call to the thread interrupt method happens-before the interrupted thread detects the code sent by the interrupt.
  7. The happen-before principle of thread termination: All operations in a thread are detected happen-before the thread's termination.
  8. The happen-before principle of object creation: the initialization of an object is completed before its finalize method is called.
JMM will not trigger the instruction rearrangement effect only when the above 8 conditions do not occur.

You don't need to pay too much attention to the happens-before principle, you just need to be able to write thread-safe code.

volatile

If you need to prevent the program from re-arranging instructions when operating a certain attribute, in addition to satisfying the happens-before principle, you can also modify the attribute based on volatile, so that the problem of instruction re-arrangement will not occur when operating this attribute.

How does volatile prohibit instruction rearrangement?

Memory barrier concept. Think of a memory barrier as an instruction.

A previous instruction will be added between the two operations. This instruction can avoid the reordering of other instructions executed above and below.

Guess you like

Origin blog.csdn.net/qq_44033208/article/details/132434143