Chapter 2 - Java multi-threaded high concurrency + interview questions

s1 Basics

001 Advantages and disadvantages of concurrent programming
advantage Make full use of the computing power of multi-core cpu Facilitate business splitting, improve system concurrency and performance, improve program execution efficiency, and speed up
shortcoming memory leak | context switch thread safety | deadlock

002 Three elements of concurrent programming
Three elements Reason for security issue specific cause Countermeasures
atomicity thread switch One or more operations either all succeeded or all failed Atomic classes starting with JDK Atomic, synchronized, LOCK
visibility cache The modification of shared variables by thread a can be seen immediately by thread b synchronized、volatile、LOCK
orderliness compile optimization The processor may reorder instructions Happens-Before rule

:::success
tip: Java program guarantees multi-threaded operation safety
method 1: use security classes, consider classes under the atomic package, such as classes under java.util.concurrent, use atomic class AtomicInteger
method 2: use automatic lock synchronized
method 3 : Use manual lock Lock
Method 4: Consider classes under the atomic package to ensure the visibility of operations
Method 5: Involving thread control, consider CountDownLatch/Semaphore==
:::
image.png

003 The difference between parallelism and concurrency
category concurrency parallel serial
definition Multiple tasks are executed on the same CPU core in turn (alternately) according to subdivided time slices. Logically, those tasks are executed at the same time In a unit of time, multiple processors or multi-core processors process multiple tasks at the same time, which is the true sense of "simultaneous" There are n tasks, which are executed sequentially by one thread.
There is no thread insecurity, and there is no critical section problem
example two queues, one ticket gate Two queues, 2 ticket gates 1 queue, 1 ticket gate

004 Thread process difference
category process thread
Nature The basic unit of operating system resource allocation The basic unit of processor task scheduling and execution
resource overhead There are independent code and data spaces, and the switching overhead between processes is large , the same type of thread shares code and data space, each thread has its own independent running stack and pc, and the overhead is small
containment relationship A process can contain 1-n threads A thread is a part of a process, a lightweight process
memory allocation Address spaces and resources between processes are independent of each other Threads of the same process share the address space and resources of the process
influence relationship Process crashes, no impact on others in protected mode, robust The thread crashes and the whole process hangs
Implementation process Entry to program execution, sequential execution sequence, and program exit Threads cannot be executed independently and must depend on the application

005 context switching

The process of a task from saving to reloading is a context switch
1. A CPU core can only be used by one thread at any time. In order to allow these threads to be executed effectively, the strategy adopted by the CPU is to allocate a time slice for each thread and rotate form. When a thread's time slice is used up, it will be ready again for use by other threads. This process is a context switch.
2. The current task will save its own state before switching to another task after executing the CPU time slice, so that when switching back to this task next time, the state of this task can be loaded again:::success context switching can be considered as
the
kernel (The core of the operating system) Switch processes (including threads) on the CPU, and the information during
the context switching process is stored in the process control block (PCB, process control block). PCB is also often
referred to as "switch frame" (switchframe). Information is kept in the CPU's memory until they are used again.
:::

006 daemon thread | user thread

User (User) thread: runs in the foreground and performs specific tasks, such as the main thread of the program | sub-threads connected to the Internet, etc. are user thread daemon
threads: runs in the background, serving other foreground threads, once all users The threads are all finished running, and the daemon thread will end its work together with the JVM. For example, the garbage collection thread,
:::success
tip:
1. setDaemon(true) must be executed before the start() method, otherwise an IllegalThreadStateException will be thrown. 2.
In the daemon The new thread generated in the thread is also a daemon thread
3. Not all tasks can be assigned to the daemon thread for execution, such as read and write operations or calculation logic
4. In the daemon thread, the content of the finally block cannot be relied on to ensure that the execution is closed or Logic to clean up resources. Because the finally statement block of the daemon thread may not be executed.
5. Priority level: The priority of the daemon thread is relatively low, and it is used to provide services for other objects and threads in the system.
:::

007 deadlock

Baidu Encyclopedia: Deadlock refers to a blocking phenomenon caused by two or more processes (threads) during the execution process due to competition for resources or communication with each other. If there is no external force, they will not be able to advance. . At this time, the system is said to be in a deadlock state or the system has a deadlock, and these processes (threads) that are always waiting for each other are called deadlock processes (threads).
image.png
As shown in the figure, thread A holds resource 2, and thread B holds resource 1. They both want to apply for each other's resources at the same time, so the two threads will wait for each other and enter a deadlock state.
Four elements of deadlock:
1. Mutual exclusion conditions : threads (processes) have exclusiveness to allocated resources, that is, a resource can only be occupied by one thread (process) until it is released by the thread (process) 2
Request and holding conditions : When a thread (process) is blocked due to a request for occupied resources, it will not let go of the obtained resources.
3. Non-deprivation condition : The resources that a thread (process) has acquired cannot be forcibly deprived by other threads until it is used up, and the resources are released only after they are used up.
4. Circular waiting condition : When a deadlock occurs, the waiting thread (process) will definitely form a loop (similar to an infinite loop), causing permanent blocking
to avoid thread deadlock: (you can break any of the four elements) There is no way to destroy the
condition of mutual exclusion
, because we use locks to make them mutually exclusive (critical resources require mutual exclusive access).
Destroying the request and keeping the condition
applies for all resources at once.
Destroying the non-deprivation condition
occupies part of the resources. When the thread further applies for other resources, if the application cannot be obtained, it can actively release the resources it occupies
to break the circular waiting condition
and prevent it by applying for resources in order. Apply for resources in a certain order, and release resources in reverse order. break loop wait condition

  • Try to use the tryLock(long timeout, TimeUnit unit) method (ReentrantLock, ReentrantReadWriteLock), set the timeout period, timeout can exit to prevent deadlock.
  • Try to use java.util.concurrent concurrent class instead of your own handwritten lock.
  • Try to reduce the granularity of lock usage, and try not to use the same lock for several functions.
  • Minimize synchronized code blocks

image.png
1. Fix the order of locking, for example, we can use the size of the Hash value to determine the order of locking.
2. Reduce the scope of locking as much as possible, and only lock when operating shared variables.
3. Use a releasable timed lock (if you can't apply for the lock permission for a period of time, you can release it directly

008 Four ways to create threads (pooled resources)

There are four ways to create threads:

  1. Inherit the Thread class; rewrite the run method, call the start() method of the thread object to start the thread
//Thread类本质是实现Runnablre接口的实例
public class Test extends Thread{
    
    
    public void run(){
    
    
        System.out.print("Test.run()");
    }
}
Test test = new Test();
test.start();//start方法是一个native方法,启动新线程,执行run()方法
  1. Implement the Runnable interface; rewrite the run method, and call the start() method of the thread object to start the thread
public class MyThread extends OtherClass implements Runnable {
    
    
    public void run() {
    
    
    System.out.println("MyThread.run()");
    }
}
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//事实上,当传入一个 Runnable target 参数给 Thread 后,Thread 的 run()方法就会调用
target.run()
public void run() {
    
    
    if (target != null) {
    
    
    target.run();
	}
}
  1. Implement the Callable interface; 1. Create a class myCallable that implements the Callable interface2. Create a FutureTask object with myCallable as a parameter

    3. Create a Thread object with FutureTask as a parameter 4. Call the start() method of the thread object
    (compared to runable, it has a return value, and allows exceptions to be thrown, and exception information can be obtained) (only available after 5)

//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
    
    
	Callable c = new MyCallable(i + " ");
	// 执行任务并获取 Future 对象
	Future f = pool.submit(c);
	list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
    
    
	// 从 Future 对象上获取任务的返回值,并输出到控制台
	System.out.println("res:" + f.get().toString());
}
  1. Create a thread pool using the Executors tool class

Executors provide a series of factory methods for creating thread pools, and the returned thread pools all implement the ExecutorService interface.
There are mainly newFixedThreadPool, newCachedThreadPool, newSingleThreadExecutor, newScheduledThreadPool

// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
    while(true) {
    
    
        threadPool.execute(new Runnable() {
    
     // 提交多个线程任务,并执行
            @Override
            public void run() {
    
    
                System.out.println(Thread.currentThread().getName() + " is running ..");
                try {
    
    
                	Thread.sleep(3000);
                } catch (InterruptedException e) {
    
    
                	e.printStackTrace();
                }
            }
        });
    }
}

Thread Pool

Pooling technology is not uncommon compared to everyone. Thread pools, database connection pools, Http connection pools, etc. are all applications of this idea. The idea of ​​pooling technology is mainly to reduce the consumption of resources each time, improve the response speed, improve the utilization of resources, and reuse threads . Improve the manageability of threads . Provide functions such as timing execution, periodic execution, single thread, and concurrency control

(1) newSingleThreadExecutor : Create a single-threaded thread pool. This thread pool has only one thread working, which is equivalent to executing all tasks serially with a single thread. If the only thread ends abnormally, a new thread will take its place. This thread pool guarantees that the execution order of all tasks is executed in the order in which the tasks are submitted.
(2) newFixedThreadPool: Create a fixed-size thread pool. A thread is created each time a task is submitted, until the thread reaches the maximum size of the thread pool. Once the size of the thread pool reaches the maximum value, it will remain unchanged. If a thread ends due to an abnormal execution, the thread pool will be supplemented with a new thread. If you want to use a thread pool on the server, it is recommended to use the newFixedThreadPool method to create a thread pool, which can achieve better performance.
(3)  newCachedThreadPool : Create a cacheable thread pool. If the size of the thread pool exceeds the number of threads required to process tasks, some idle threads (not executing tasks for 60 seconds) will be recovered. When the number of tasks increases, the thread pool can intelligently add new threads to process tasks. This thread pool does not limit the size of the thread pool. The size of the thread pool depends entirely on the maximum thread size that the operating system (or JVM) can create.
(4) newScheduledThreadPool : Create a thread pool with unlimited size. This thread pool supports timing and periodic task execution requirements.

  • RUNNING: This is the most normal state, accepting new tasks and processing tasks in the waiting queue.
  • SHUTDOWN: Do not accept new task submissions, but will continue to process tasks in the waiting queue.
  • STOP: Do not accept new task submissions, no longer process tasks in the waiting queue, and interrupt threads that are executing tasks.
  • TIDYING: All tasks are destroyed, workCount is 0, and the hook method terminated() will be executed when the state of the thread pool is converted to TIDYING state.
  • TERMINATED: After the terminated() method ends, the state of the thread pool will become this.

ThreadPoolExecutor creates the thread pool
ThreadPoolExecutor 3 most important parameters:

  • corePoolSize : The number of core threads, the number of threads defines the minimum number of threads that can run at the same time.
  • maximumPoolSize : The maximum number of worker threads allowed to exist in the thread pool
  • workQueue: When a new task comes, it will first judge whether the number of currently running threads reaches the number of core threads. If so, the task will be stored in the queue.image.png

image.png

009 The difference between run() and start()
run start
The thread body, the runtime code that executes the thread start thread
can be called repeatedly can only be adjusted once
The thread enters the ready state and runs only when the time slice is allocated

The state and basic operations of the s2 thread

010 Thread life cycle and five basic states

image.png
There are three types of blocking:
(1). Waiting for blocking: the thread in the running state executes the wait () method, and the JVM will put the thread into the waiting queue (waitting queue), so that the thread enters the waiting blocking state;
( 2). Synchronous blocking: If the thread fails to acquire the synchronized synchronization lock (because the lock is occupied by other threads), the JVM will put the thread into the lock pool (lock pool), and the thread will enter the synchronous blocking state; (3
) . Other blocking: When calling the thread's sleep() or join() or issuing an I/O request, the thread will enter the blocking state. When the sleep () state times out, the join () waits for the thread to terminate or time out, or when the I/O processing is completed, the thread is transferred to the ready state again.

011 Thread scheduling algorithm used in Java

The time-sharing scheduling model refers to allowing all threads to obtain the right to use the cpu in turn, and equally allocates the time slice of the CPU occupied by each thread.
The Java virtual machine adopts a preemptive scheduling model , which means that threads with higher priority in the runnable pool occupy the CPU first. If the threads in the runnable pool have the same priority, a thread is randomly selected to occupy the CPU. A thread in the running state runs until it has to relinquish the CPU.

012 Thread Scheduler (Thread Scheduler) and Time Slicing (Time Slicing)

The thread scheduler is an operating system service that is responsible for allocating CPU time to threads in the Runnable state. Once we create a thread and start it, its execution depends on the thread scheduler implementation.
Time slicing is the process of allocating available CPU time to available Runnable threads. Allocation of CPU time can be based on thread priority or thread waiting time.
Thread scheduling is not controlled by the Java virtual machine, so it is better for the application to control it (don't make your program depend on thread priority)

013 What is the difference between sleep() and wait()
category sleep() wait()
same: can suspend the execution of the thread
kind Static method of Thread thread class Methods of the Object class
Lock hold on release lock
use suspend execution Inter-thread interaction/communication
usage Automatically wake up after time notify() or notifyAll() method to wake up

014 sleep() method and yield() method
category sleep() yield()
Same point: can suspend the execution of the current thread
difference The priority of the thread is not considered, so the lower priority thread is given a chance to run Only threads of the same priority or higher will be given a chance to run

Go to blocked state Go to ready state

throws InterruptedException did not declare any exceptions
usage with better portability It is not recommended to use the yield() method to control concurrent thread execution

015 How does Java implement communication and collaboration between multiple threads?

可以通过 中断 和 共享变量 的方式实现线程间的通讯和协作
比如说最经典的生产者-消费者模型:当队列满时,生产者需要等待队列有空间才能继续往里面放入商品,而在等待的期间内,生产者必须释放对临界资源(即队列)的占用权。因为生产者如果不释放对临界资源的占用权,那么消费者就无法消费队列中的商品,就不会让队列有空间,那么生产者就会一直无限等待下去。因此,一般情况下,当队列满时,会让生产者交出对临界资源的占用权,并进入挂起状态。然后等待消费者消费了商品,然后消费者通知生产者队列有空间了。同样地,当队列空时,消费者也必须等待,等待生产者通知它队列中有商品了。这种互相通信的过程就是线程间的协作。
Java中线程通信协作的最常见的两种方式:
一.syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
二.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()线程间直接的数据交换:
三.通过管道进行线程间通信:1)字节流;2)字符流

016 实现线程同步的方法
  • 同步代码方法:sychronized 关键字修饰的方法
  • 同步代码块:sychronized 关键字修饰的代码块
  • 使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
  • 使用重入锁实现线程同步:reentrantlock类是可重入、互斥、实现了lock接口的锁, 他与sychronized方法具有相同的基本行为和语义

017 线程安全(Servlet | Struts2 | SpringMVC)

线程安全是编程中的术语,指某个方法在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new一个新的 action 分配给这个请求,请求完成后销毁。
SpringMVC 的 Controller 是线程安全的吗?不是的,和 Servlet 类似的处理流程。
Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题

并发理论(重要⭐⭐⭐)

s3. Java内存模型

01 Java中垃圾回收

垃圾回收是在内存中存在没有引用的对象或超过作用域的对象时进行的.
垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源

02 finalize()方法

垃圾回收器GC(garbage colector)决定回收某对象时,就会运行该对象的finalize()方法;
finalize是Object类的一个方法,该方法在Object类中的声明protected void finalize() throws Throwable { }
在垃圾回收器执行时会调用被回收对象的finalize()方法,可以覆盖此方法来实现对其资源的回收。
注意:一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间
析构函数(finalization): 特殊的情况下,比如调用了一些native的方法(一般是C写的),可以要在finaliztion里去调用C的释放函数

03 as-if-serial规则和happens-before规则的区别
as-if-serial语义 happens-before
保证单线程内程序的执行结果不被改变 保证正确同步的多线程程序的执行结果不被改变
给编写单线程程序的程序员创造了一个幻境:单线程
程序是按程序的顺序来执行的
给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。
目的: 都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度

04 并发关键字 synchronized

在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。synchronized 可以修饰类、方法、变量.
image.png
JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

public class Test{
    
    
    //私有化该类的引用
    private static volatile Test instance; //volatile修饰,保证可见和禁止指令重排
    //构造器私有化
    pvivate Test(){
    
    }
    //提供公共的访问方式
    public static Test getInstance(){
    
    
        if(instance == null){
    
    
            //类对象加锁
            synchronized (Test.class){
    
    
                if(instance == null){
    
    
                    instance = new Test();
                }
            }
        }
        return instance;
    }
}

instance = new Test();这段代码其实是分为三步执行:
1. 为 instance 分配内存空间
2. 调用构造器初始化 instance
3. 将 instance 指向分配的内存地址
but 由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2;
指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例;
例如,线程 T1 执行了 1 和 3,此时 T2 调用getInstance() 后发现 Instance 不为空,因此返回
Instance,但此时 Instance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
底层:
image.png
可以看出在执行同步代码块之前之后都有一个monitor字样,其中前面的是monitorenter,后面的是离开monitorexit,不难想象一个线程也执行同步代码块,首先要获取锁,而获取锁的过程就是monitorenter ,在执行完代码块之后,要释放锁,释放锁就是执行monitorexit指令。
为什么会有两个monitorexit呢?
这个主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。
仅有ACC_SYNCHRONIZED这么一个标志,该标记表明线程进入该方法时,需要monitorenter,退出该方法时需要monitorexit。
synchronized可重入的原理
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁。底层原理维护
一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放
锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线
程可以竞争获取锁。
自旋锁
很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时
等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内
核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁
的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果
做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略

05 可见性(线程之间修改变量)

(1)volatile 修饰变量
(2)synchronized 修饰修改变量的方法
(3)wait/notify
(4)while 轮询

06 synchronized、volatile、CAS 比较

(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
(3)CAS 是基于冲突检测的乐观锁(非阻塞)

07 synchronized 和 Lock 有什么区别?
  • 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
  • synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
  • synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到
08 synchronized 和 ReentrantLock 区别

两者都是可重入锁。自己可以再次获取自己的内部锁。

synchronized ReentrantLock
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础: 使用起来比较灵活,但是必须有释放锁的配合动作
不需要手动释放和开启锁 必须手动获取与释放锁
可以修饰类、方法、变量== 只适用于代码块锁
操作的应该是对象头中 mark word 底层调用的是 Unsafe 的park 方法加锁

09 volatile 关键字的作用

volatile 关键字来保证可见性和禁止指令重排。
volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的
volatile 的一个重要作用就是和 CAS 结合,保证了原子性
volatile 常用于多线程环境下的单次操作(单次读或者单次写)

10 synchronized 和 volatile 的区别是什么?

synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程。
volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。
区别

  • volatile 是变量修饰符;synchronized 可以修饰类、方法、变量。
  • volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 可以保证变量的修改可见性和原子性。
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些

##### **11 Java memory model (important ⭐JMM)** The specific website:
The memory model mainly uses two methods to solve concurrency problems: limiting processor optimization and using memory barriers. ###### The Java memory model has the same set of memory model specifications, and different languages ​​may have some differences in implementation. Next, we will focus on the implementation principle of the Java memory model.
**Relationship between Java runtime memory area and hardware memory**
JVM runtime memory area is fragmented (stack, heap, etc.), in fact, these are logical concepts defined by JVM. These concepts do not exist in traditional hardware memory architectures.
![](https://img-blog.csdnimg.cn/img_convert/159817f13f858edb5a2153b80f643685.jpeg)
It can be seen from the above figure that the stack heap exists in both the main memory and the high-speed cache, so there is no direct relationship between the two
**Java The relationship between threads and main memory**
  • All variables are stored in main memory (Main Memory)
  • Each thread has a private local memory/working memory (Local Memory), which stores a copy of the thread to read/write shared variables
  • All operations on variables by threads must be performed in local memory, and cannot directly read and write main memory
  • Variables in each other's local memory cannot be accessed between different threads

tip: working memory means local memory

image.png

summary

由于CPU 和主内存间存在数量级的速率差,想到了引入了多级高速缓存的传统硬件内存架构来解决,多级高速缓存作为 CPU 和主内间的缓冲提升了整体性能。解决了速率差的问题,却又带来了缓存一致性问题。
数据同时存在于高速缓存和主内存中,如果不加以规范势必造成灾难,因此在传统机器上又抽象出了内存模型。
Java 语言在遵循内存模型的基础上推出了 JMM 规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题
为了更精准控制工作内存和主内存间的交互,JMM 还定义了八种操作:lock, unlock, read,load,use,assign, store, write。见下图:
image.png

s4 Lock体系

01 Lock简介

它的优势有:
(1)可以使锁更公平
(2)可以使线程在等待锁的时候响应中断
(3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
(4)可以在不同的范围,以不同的顺序获取和释放锁
整体上来说 Lock 是 synchronized 的扩展版,Lock 提供了无条件的、可轮询
的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、
可多条件队列的(newCondition 方法)锁操作。另外 Lock 的实现类基本都支持
非公平锁(默认)和公平锁,synchronized 只支持非公平锁,当然,在大部分情
况下,非公平锁是高效的选择

02 乐观锁和悲观锁的理解及实现

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步 synchronized关键字的实现也是悲观锁
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁
乐观锁的实现方式:
1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。
2、java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。

03 CAS

CAS 是 compare and swap 的缩写,即我们所说的比较交换。
cas 是一种基于锁的操作,而且是乐观锁. 乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如
通过给记录加 version 来获取数据,性能较悲观锁有很大的提高
CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的
(AtomicInteger,AtomicBoolean,AtomicLong)

Q A
1、ABA 问题: 1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference
2、循环时间长开销大: 对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized
3、只能保证一个共享变量的原子操作 当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子
但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时
候就可以用锁。

image.png

04 AQS(AbstractQueuedSynchronizer)详解与源码分析

这个类在java.util.concurrent.locks包下面
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器
AQS 原理概览
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

image.png
AQS(AbstractQueuedSynchronizer)原理图:
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改;

private volatile int state; //共享变量,使用volatile修饰保证线程可见性

状态信息通过protected类型的getState,setState,compareAndSetState进行操作

//返回同步状态的当前值
protected final int getState() {
    
    
  return state;
 }
 // 设置同步状态的值
 protected final void setState(int newState) {
    
    
  state = newState;
 }
 //原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
 protected final boolean compareAndSetState(int expect, int update) {
    
    
  return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
 }

AQS 对资源的共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁;
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、CyclicBarrier、ReadWriteLock

AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样
1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:
1 isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
2 tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
3 tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
4 tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
5 tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞
①重入性的实现原理
要想支持重入性,就要解决两个问题:1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
②ReentrantLock支持两种锁:公平锁和非公平锁。何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFOimage.png
image.png

05 多线程锁的升级原理

Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
1)只有⼀个线程进⼊临界区,偏向锁
2)多个线程交替进⼊临界区,轻量级锁
3)多线程同时进⼊临界区,重量级锁
image.png
image.png

06 ReadWriteLock

ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
而读写锁有以下三个重要的特性:
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

07 ConcurrentHashMap

ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现.利用了锁分段的思想提高了并发度
JDK 1.6版本关键要素:

  • segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障;
  • segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表。

JDK1.8后,ConcurrentHashMap抛弃了原有的Segment 分段锁(锁段),而采用了CAS + synchronized 来保证并发安全性。
同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 Vector,Hashtable等方法返回的容器,需要同步的方法上加上关键字 synchronized;
并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。
ConcurrentHashMap 使用分段锁来保证在多线程下的性能。
ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。
这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。

08 ThreadLocal内存泄漏分析

ThreadLocal造成内存泄漏的原因?
ThreadLocalMap  中使用的 key 为  ThreadLocal  的弱引用,而 value 是强引用。所以,如果  ThreadLocal  没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来, ThreadLocalMap  中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个
时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用  set() 、 get() 、 remove()  方法的时候,会清理掉 key 为 null 的记录。使用完ThreadLocal 方法后 最好手动调用 remove() 方法
image.png
ThreadLocal内存泄漏解决方案?

  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
  • 在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理

image.png

image.png

面试题部分

引用地址 :https://www.bilibili.com/video/BV1yT411H7YK?p=95&spm_id_from=pageDriver&vd_source=08ac522c6603c56e243d5e129a309a60

线程的基础知识

线程进程区别

类别 进程 线程
本质 操作系统资源分配的基本单位 处理器任务调度和执行的基本单位
资源开销 有独立代码和数据空间,进程间切换开销大 ,同一类线程共享代码和数据空间,每个线程有自己独立的运行栈和pc,开销小
包含关系 一个进程可以包含1-n个线程 线程是进程的一部分,轻量级的进程
内存分配 进程之间的地址空间和资源是相互独立 同一进程的线程共享本进程的地址空间和资源
影响关系 进程崩溃,保护模式下对其他无影响,健壮 线程崩溃,整个进程挂
执行过程 程序运行的入口、顺序执行序列和程序出口 线程不能独立执行,必须依存在应用程序中
  1. 进程是正在运行程序的实例,进程里包括线程,每个线程执行不同的任务,每个进程包括至少一个线程;
  2. 不同进程有不同的内存空间,当前进程的所有线程可以共享内存空间,但线程都有自己的栈和PC
  3. 线程更轻量,上下文切换的成本一般而言更低;

并行并发区别

并行 : 真正意义上的同步执行,多核CPU同时执行多个线程的任务,同一时间动手做多件事情
并发 ; 逻辑意义的同时执行,多线程等待时间片资源,交替执行任务,只不过切换时间快,给人同时执行的假象,同一时间应对多件事情的能力,多线程轮流使用一个或者多个CPU

线程创建方式

一, 继承 Thread 类,重写run方法, start开启
二, 实现 Runnable 接口,重写run方法, start开启
三, 实现 Callable 接口,
四, Executor (线程池创建线程)

线程状态

new runnable blocked waiting time_waiting terminated

  • 创建线程对象是新建状态new
  • 调用start()方法变为可执行状态
  • 线程拿到 CPU 执行权,执行结束是终止状态
  • 可执行状态过程里,如果没有拿到 CPU 执行权,可能切换其他状态
    • 没有锁进入阻塞状态,获得锁再换为可执行状态
    • wait()方法进行等待状态,释放锁和资源,调用唤醒方法切换为可执行状态
    • sleep() 方法,进入计时等待状态,到时间后切换为 可执行状态;

runable & callable

r 没有返回值, c 的call 方法有返回值,需要 FutureTask 获取结果
r 异常只能内部消化,不能向上抛, c 的call方法允许抛出异常

wait() & sleep()

共同点 ; wait() wait(long)和sleep(long)都能让当前线程暂时放弃 CPU 执行权,进入阻塞状态
不同点 :
方法归属 : 前者属于 Thread的静态方法,后者属于 Object 的成员方法
唤醒时机 :

  • 如果有参数,都会在等待相应时间后醒来,
  • wait()和wait(long)还可以被 notify唤醒,wait()如果不唤醒就一直等,
  • 都可以被打断唤醒

锁特性不同(重要)

  • wait方法调用必须获取wait对象锁,sleep没有此限制
  • wait方法执行后会释放锁,允许其他线程获得该对象锁
  • 但是sleep人如果在 synchronized 代码块执行,并不释放对象锁 (放弃CPU,但你们也用不了)

线程之间如何保证顺序执行

t1 -> t2 ->t3
新建t1线程
t1.join(); 在t2线程代码块的首行
t2.join(); 在t3线程代码块的首行

notify() 和 notifyAll() 的区别

唤醒一个,唤醒所有

run() 和 start() 区别

start() 开启线程,只调用一次,调用 run方法,所定义的逻辑代码
run() 封装了要被线程执行的代码,可调用多次

如何停止一个正在运行的线程

  • 退出标志,线程正常退出
  • stop方法强行终止
  • interrupt方法中断线程
    • 打断阻塞线程 sleep wait join 线程,抛异常InterruptedEXception
    • 打断正常线程,可根据打断状态标记是否退出线程

线程并发安全

synchronized底层

  • Synchronized(对象锁) 采用互斥的方式让同一时刻最多只能有一个线程能够持有对象锁
  • 底层是 monitor 实现, monitor 是JVM级别的对象(C++实现),线程获得锁需要使用对象锁关联 monitor
  • monitor 内部有三个属性,分别是 owner entrylist waitset
  • 其中 owner 关联获得锁的线程,并且只能关联一个线程, entrylist关联阻塞状态的线程, waitset是处于等待

在JDK1.6版本以后,有偏向锁、轻量级锁、重量级锁,分别对应锁只被一个线程持有,不同线程交替持有锁,多线程竞争锁的三种情况;

分类 desc
偏向锁 当很长时间只有一个线程使用锁,可用偏向锁,第一次获得时,CAS操作,之后该线程再获取锁,需要判断 mark word 里是否是自己的线程 id 即可,而不是开销相对较大的CAS命令
轻量级锁 当线程加锁时间错开(无竞争),可用轻量级锁优化,修改了对象头的锁标志,相比重量级锁性能提升很多,每次修改都是CAS操作,保证原子性
重量级锁 底层 Monitor 实现,涉及到用户态内核态切换,进程上下文切换,成本较高,性能比较低

一旦锁发生竞争,就会升级为重量级锁
image.png
image.png

JMM

  • JMM (Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性;
  • JMM 把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存);
  • 线程之间相互隔离,线程之间的交互需要通过主内存;

CAS

  • CAS 全称是 :** Compare And Swap (比较再交换);体现的一种乐观锁**的思想,在无锁状态下保证线程操作数据的原子性;
  • CAS 使用的地方 : AQS框架,AtomicXXX 类
  • 操作共享变量的时候用的自旋锁,效率上更高一些
  • CAS 底层调用的 Unsafe 类中的方法,都是操作系统提供的,其他语言实现

乐观锁和悲观锁的区别

  • CAS 基于乐观的思想: 最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,吃点亏再重试;
  • synchronized 基于悲观锁的思想:悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,必须等我改完了解锁,你们才有机会用;

AQS

  • 多线程的队列同步器,锁机制,像 ReentrantLock 和信号量基于AQS实现的
  • 内部维护一个先进先出的双向队列,存储的是排队的线程
  • 内部有一个属性 state ,相当于一个资源,默认是 0 (无锁状态),如果队列有一个线程修改成功 state 为1,则当前线程获取资源
  • 对state 修改时候用 CAS 操作,保证多线程修改情况下的原子性

image.png

ReentrantLock实现原理

  • 表示支持重新进入的锁,调用 Lock 方法获取锁之后,再调用 lock 不会阻塞
  • 主要利用 CAS + AQS 队列实现
  • 支持公平锁和非公平锁,提供的构造器中无参默认是非公平锁,也可以传参设置为公平锁

image.png

synchronized & Lock

image.png

死锁产生条件

image.png

死锁诊断

volatile

问题一 : JVM有一个 JIT (即时编译器)给代码做了优化

//问题一 : JVM有一个 JIT (即时编译器)给代码做了优化
while(!stop){
    
    
    i++;
}

// 优化之后

while(true){
    
    
    i++;
}

//解决方案一: 程序运行时候加上 vm 参数 -Xint表示禁用即时编译器,不推荐
//解决方案二: 在修改 stop 变量时候加上 volatile 当前告诉JIT,不要对volatile修饰的变量做优化 

:::info
写变量 让 volatile 修饰的变量在代码最后位置
读变量 让 voaltile 修饰的变量在代码最开始位置
:::

①保证线程间的可见性
用volatile修饰共享变量,能防止编译器优化等发生,让一个线程对共享变量的修改对另一个线程可见
②禁止指令重排序
指令重排:volatile修饰共享变量会在读、写共享变量时加入不同屏障,阻止其它读写操作越过屏障,从而阻止重排序的效果

ConcurrentHashMap

image.png

导致并发程序出现问题的根本原因

原子性
可见性
指令重排序

线程池

线程池核心参数(执行原理)

image.png

线程池常见的阻塞队列

image.png

核心线程数的确定

image.png

线程池种类

image.png

不建议用 Executors 创建线程池的原因

image.png

使用场景

线程池使用场景(项目中哪里用到线程池CountDownLatch、Future)

场景一(ES数据批量导入)
项目上线之前,需要把数据库的数据一次性同步到 ES 索引库,但是当时数据好像是1KW左右,一次性读取可能OOM异常,用线程池方式导入,利用闭锁(倒计时锁)来控制,避免一次性加载过多,防止内存溢出
image.png
场景二(数据汇总)
image.png
image.png
场景三 : 比如主线程进行搜索的时候,异步调用其它线程来保存搜索记录;

批量导入 : 线程池 + 闭锁 批量把数据库的数据导入到 ES 中,避免 OOM
数据汇总:调用多个接口汇总数据,如果所有接口(或者部分接口没有依赖关系),就可以使用线程池 + future 来提升性能
异步线程(线程池),避免下一级方法影响上一级方法(性能考虑),可用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法响应时间

控制某个方法允许并发访问线程的数量

工具类 信号量,并发情况下,可以控制方法访问量

  1. 创建信号量对象,可以给一个容量
  2. acquire()可以请求一个信号量,这时候信号量个数-1
  3. release() releases a semaphore, at this time the number of semaphores +1

ThreadLocal

image.png

Guess you like

Origin blog.csdn.net/Kaka_csdn14/article/details/130850750