• 【Java八股文总结】之多线程


    文章目录

    Java多线程

    一、线程

    1、什么是线程?什么是进程?二者的区别
    • 进程 就是计算机正在运行的一个独立的应用程序。进程是一个动态的概念,当我们启动某个应用的时候,进程就产生了,当我们关闭该应用的时候,进程就结束了,进程的生命周期就是我们在使用该软件的整个过程。
    • 线程组成进程的基本单位,可以完成特定的功能,一个进程是由一个或多个线程组成的。
      应用程序是静态的,进程和线程是动态的,有创建有销毁,存在是暂时的,不是永久的。

    区别:

    • 进程 在运行时拥有独立的内存空间,即每个进程所占用的内存空间都是独立的,互不干扰。
    • 线程 是共享内存空间的,但是每个线程的执行都是相互独立的,单独的线程是无法执行的,由进程来控制多个线程的执行。
    1、给线程起别名的3种方式

    在这里插入图片描述

    2、this关键字

    this指代 当前类对象 。Java中this关键字的用法如下:

    1. this关键字可用来引用当前类的实例变量。
    2. this关键字可用于调用当前类方法(隐式)。
    3. this()可以用来调用当前类的构造函数。
    4. this关键字可作为调用方法中的参数传递。
    5. this关键字可作为参数在构造函数调用中传递。
    6. this关键字可用于从方法返回当前类的实例。

    参考链接:Java基础知识(超详细解析,排版清晰!):this关键字
    注意: 在使用this关键字时,可能会遇到这个报错:“Call to ‘this()’must be first statement in constructor body”,原因是:this()和super()为构造方法,作用是在JVM堆中构建出一个对象。因为避免多次创建对象,所以 一个方法只能调用一次this()或super()。this()和super()的调用只能写在第一行,避免操作对象时对象还未构建成功。而且**this()和super()不能同时出现**。

    3、守护线程和用户线程

    java线程中有两种线程,一种是用户线程,一种是守护线程。
    守护线程是一种特殊的线程,它具有陪伴的含义。当**进程中不存在非守护线程了,则守护线程自动销毁。** 典型的守护线程就是垃圾回收线程。当进程中没有用户线程了,则垃圾回收线程没有存在的必要,自动销毁。任何一个守护线程都是整个JVM中所有非守护线程的保姆。只要当前JVM中有非守护线程没有结束,守护线程就在工作。只有当最后一个非守护线程不工作的时候,守护进程才随着JVM一同结束工作。Dammon(守护进程)的作用是为非守护进程的运行提供服务 。守护进程最典型的应用是GC(垃圾回收器),它是很称职的守护者。

    4、并发和并行的区别?

    从操作系统的角度来看,线程是CPU分配的最小单位。

    • 并行就是同一时刻,两个线程都在执行。这就要求有两个CPU去分别执行两个线程。
    • 并发就是同一时刻,只有一个执行,但是一个时间段内,两个线程都执行了。并发的实现依赖于CPU切换线程,因为切换的时间特别短,所以基本对于用户是无感知的。
    5、线程间通信的方式

    在这里插入图片描述

    1. volatile和synchronized关键字
      关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
      关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性
    2. 等待/通知机制
      可以通过Java内置的等待/通知机制(wait()/notify())实现一个线程修改一个对象的值,而另一个线程感知到了变化,然后进行相应的操作。
    3. 管道输入/输出流
      管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。
      管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。
    4. 使用Thread.join()
      如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法。
    5. 使用ThreadLocal
      ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
      可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
    2、synchronized关键字
    1. 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
    synchronized void method() {
    	// 业务代码
    }
    
    • 1
    • 2
    • 3
    1. 修饰静态方法: 也就是给当前类加锁,会 作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法, 是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
    synchronized void static method() {
    	// 业务代码
    }
    
    • 1
    • 2
    • 3
    1. 修饰代码块: 指定加锁对象,对给定对象/类加锁。 synchronized(this/object) 表示进入同步代码库前要获得 给定对象的锁synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁
    synchronized(this) {
    	// 业务代码
    }
    
    • 1
    • 2
    • 3

    总结:

    1. synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是给 Class 类上锁。
    2. synchronized 关键字加到实例方法上是给 对象实例上锁。
      注意: 静态方法调用 —— 既可以使用对象调用也可以使用类调用;
      实例方法调用 —— 只可以使用对象调用;
      在静态方法中不可以使用 this 关键字。 this 指的是当前对象。
    补充:snchronized底层实现原理

    Synchronized的语义底层是通过一个 monitor(监视器锁) 的对象来完成。每个对象有一个监视器锁(monitor)。每个Synchronized修饰过的代码,当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权。
    过程:

    1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
    2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。
    3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
      synchronized是可以通过反汇编指令javap命令,查看相应的字节码文件。
    3、volatile

    volitile关键字的作用是可以 使内存中的数据对象线程可见。主内存对线程是不可见的,添加 volitile 关键字后,主内存对线程可见。(每次读取数据都是直接从主内存中读取,而不是从本地内存处读取)

    补充:Java内存模型

    在JDK1.2之前,Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的Java内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
    在这里插入图片描述
    要解决这个问题,就需要把变量声明为volatile ,这就 指示JVM,这个变量是共享且不稳定的, 每次使用它都到主存中进行读取。 所以,volatile关键字除了**①防止 JVM 的指令重排 ,还有一个重要的作用就是②保证变量的可见性。**
    在这里插入图片描述

    Q:原子性、可见性、有序性?

    原子性: 原子性指的是一个操作是不可分割、不可中断的,要么全部执行并且执行的过程不会被任何因素打断,要么就全不执行。
    可见性: 可见性指的是一个线程修改了某一个共享变量的值时,其它线程能够立即知道这个修改。
    有序性: 有序性指的是对于一个线程的执行代码,从前往后依次执行,单线程下可以认为程序是有序的,但是并发时有可能会发生指令重排。

    Q:原子性、可见性、有序性如何保证?

    原子性: JMM只能保证基本的原子性,如果要保证一个代码块的原子性,需要使用 synchronized
    可见性: Java是利用 volatile 关键字来保证可见性的,除此之外,final和synchronized也能保证可见性
    有序性: synchronized或者volatile 都可以保证多线程之间操作的有序性。

    Q:什么是指令重排?

    在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型:

    • 编译器优化的重排序: 编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
    • 指令级并行的重排序: 现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
    • 内存系统的重排序: 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
      从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序,如图:
      在这里插入图片描述
    Q:volatile关键字的原理?

    volatile有两个作用,保证 可见性有序性
    volatile怎么保证可见性的呢?
    相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,它没有上下文切换的额外开销成本。
    volatile可以确保对某个变量的更新对其他线程马上可见,一个变量被声明为volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存,当其它线程读取该共享变量,会从主内存重新获取最新值,而不是使用当前线程的本地内存中的值。
    volatile怎么保证有序性的呢?
    重排序可以分为编译器重排序和处理器重排序,valatile保证有序性,就是通过分别限制这两种类型的重排序。

    4、synchronized vs volatile的区别

    synchronized关键字和volatile关键字是两个互补的存在,而不是对立的存在!
    volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是 volatile关键字只能用于变量,而synchronized关键字可以修饰方法以及代码块
    volatile关键字 能保证数据的可见性(√),但不能保证数据的原子性(×)。synchronized关键字 两者都能保证
    volatile关键字主要用于 解决变量在多个线程之间的可见性,而synchronized 关键字解决的是 多个线程之间访问资源的同步性
    即synchronized是线程安全的,而volatile是非线程安全的。

    5、ReentrantLock与synchrnized的区别
    1. =ReentrantLock是一个,synchronized是一个关键字
    2. ReentrantLock是JDK实现,synchronized是JVM实现
    3. ReentrantLock需要手动释放,synchronized可以自动释放锁
    4. ReentrantLock是 Lock 接口的实现类。
      在这里插入图片描述
    6、synchronized和Lock的区别
    1. synchronized 自动上锁,自动释放锁;Lock 手动上锁,手动释放锁
    2. synchronized 无法判断是否获取到了锁;Lock 可以判断是否拿到了锁
    3. synchronized 拿不到锁就会一直等待;Lock 不一定会一直等待
    4. synchronized 是 java关键字;Lock 是 java接口
    5. synchronized 是非公平锁;Lock 可以设置是否为公平锁
    7、公平锁和非公平锁的区别

    公平锁: 线程同步时,多个线程排队时,顺序执行。
    非公平锁: 线程同步时,多个线程排队时,可以插队。

    8、Java中实现多线程的方式?
    1. 继承 Thread 类
    2. 实现 Runnable 接口
    3. 实现 Callable 接口
      Callable 接口和Runnable 接口的区别在于: Runnable 的 run() 方法没有返回值;Callable 的 call() 方法有返回值。
    9、Java并发工具类(JUC工具类(三种计数方法))
    1. CountDownLatch减法计数器: 允许一个或多个线程等待其他线程完成操作。
      可以用来倒计时,当两个线程同时执行时,如果要确保一个线程先执行,可以使用计数器,当计数器清零的时候,再让另一个线程执行。
      适用场景: ①协调子线程结束动作:等待所有子线程运行结束,如王者开黑得等待所有人上线才能开始打;②协调子线程开始动作:统一各线程动作开始的时机。
      CountDownLatch的核心方法有:
      · await():等待latch降为0 。
      · boolean await(long timeout, TimeUnit unit):等待latch降为0,但是可以设置超时时间。比如有玩家超时未确认,那就重新匹配,总不能为了某个玩家等到天荒地老。
      · countDown():latch数量减1 。
      · getCount():获取当前的latch数量。

    2. CyclicBarrier加法计数器: 同步屏障,可循环使用(Cyclic)的屏障(Barrier)。
      它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
      可以用来计时,当执行的次数达到CyclicBarrier设置的值时,就会执行CyclicBarrier参数中的接口中实现的代码,达到一次条件就会执行一次CyclicBarrier中的接口方法。

    3. semaphore计数信号量: 信号量。用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
      实际开发时可以使用它来完成限流操作(流量控制),限制可以访问某些资源的线程数量,如数据库连接。

    Q:CountDownLatch和CyclicBarrier的区别?

    都可以协调多线程的结束动作,在它们结束后都可以执行特定动作。
    二者的区别在于: ① CountDownLatch的执行是一次性的,无法重复利用;CyclicBarrier可以重复使用。② CountDownLatch中的各个子线程不可以等待其他线程,只能完成自己的任务;而CyclicBarrier中的各个线程可以等待其他线程。
    在这里插入图片描述

    Q:Exchanger了解么?

    Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于 进行线程间的数据交换 。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。
    假如两个线程有一个没有执行exchange()方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以使用exchange(V x, long timeOut, TimeUnit unit) 设置最大等待时长。

    10、线程池核心参数

    corePoolSize 核心池大小,初始化的线程数量。
    maximumPoolSize 线程池最大线程数,它决定了线程池容量的上限。
    corePoolSize 就是线程池的大小,maximumPoolSize是一种补救措施,任务量突然增大的时候的一种补救措施。
    keepAliveTime=线程对象的存活时间(在没有任务可执行的情况下),必须是线程池中的数量大于corePoolSize,才会生效。
    unit 线程对象存活时间单位。
    workQueue 等待队列,存储等待执行的任务。
    threadFactory 线程工厂,用来创建线程对象。
    handler 拒绝策略。
    拒绝策略有4种:

    1. AbortPolicy:直接抛出异常。
    2. DiscardPolicy:放弃任务,不抛出异常。
    3. DiscardOldestPolicy:尝试与等待队列中最前面的任务去争夺,不抛出异常。
    4. CallerRunsPolicy:谁调用谁处理。
    11、常见的几种线程池

    线程池的顶级接口是 Executors,创建线程池是通过创建 ThreadPoolExecutors 对象来完成的。

    1. 单例线程池(Executors.newSingleThreadExecutor(),阻塞队列是无界队列LinkedBlockingQueue,可能会导致OOM),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
    2. 固定数量线程池(Executors.newFixedThreadPool(),阻塞队列是无界队列LinkedBlockingQueue,可能会导致OOM)
    3. 带缓存的线程池(Executors.newCachedThreadPool(),阻塞队列是无界队列SynchronousQueue,可能会导致OOM)
    Q:使用线程池的好处?

    ① 降低资源消耗。
    通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    ② 提高响应速度。
    当任务到达时,任务可以不需要的等到线程创建就能立即执行。
    ③ 提高线程的可管理性。
    线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

    Q:线程池的队列有哪些?

    在这里插入图片描述

    • ArrayBlockingQueue: ArrayBlockingQueue(有界队列)是一个 用数组实现的有界阻塞队列,按FIFO排序
    • LinkedBlockingQueue: LinkedBlockingQueue(可设置容量队列)是基于 链表结构 的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列。
    • DelayQueue: DelayQueue(延迟队列)是一个 任务定时周期的延迟执行的队列 。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
    • PriorityBlockingQueue: PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列。
    • SynchronousQueue: SynchronousQueue(同步队列)是一个 不存储元素的阻塞队列 ,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。
    Q:线程池提交execute和submit有什么区别?

    execute用于 提交不需要返回值的任务。
    submit()方法用于 提交需要返回值的任务 。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值。

    Q:线程池怎么关闭?

    可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
    shutdown()将线程池状态置为shutdown,并不会立即停止(将队列中的任务全部执行完之后再停止):

    • 停止接收外部submit的任务。
    • 内部正在跑的任务和队列里等待的任务,会执行完。
    • 等到第二步完成后,才真正停止。
      shutdownNow()将线程池状态置为stop。一般会立即停止,事实上不一定:
    • 和shutdown()一样,先停止接收外部提交的任务。
    • 忽略队列里等待的任务。
    • 尝试将正在跑的任务interrupt中断。
    • 返回未执行的任务列表。
      shutdown和shutdownNow区别如下:
    • shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大。
    • shutdown()只是关闭了提交通道,用submit()是无效的;而内部的任务该怎么跑还是怎么跑,跑完再彻底停止线程池。
    Q:线程池异常处理方式?

    在这里插入图片描述

    Q:线程池的状态?

    线程池的状态有以下5种:

    • Running: 运行状态,线程池创建好之后就会进入此状态,如果不手动调用关闭方法,那么线程池在整个程序运行期间都是此状态。
    • Shutdown: 关闭状态,不再接受新任务提交,但是会将已保存在任务队列中的任务处理完。
    • Stop: 停止状态,不再接受新任务提交,并且会中断当前正在执行的任务、放弃任务队列中已有的任务。
    • Tidying: 整理状态,所有的任务都执行完毕后(也包括任务队列中的任务执行完),当前线程池中的活动线程数降为0时的状态。到此状态之后,会调用线程池的terminated()方法。
    • Terminated: 销毁状态,当执行完线程池的terminated()方法之后就会变为此状态。
      在这里插入图片描述
    Q:线程池如何实现参数的动态修改?

    线程池提供了几个setter方法来设置线程池的参数。
    在这里插入图片描述
    这里主要有两个思路:
    在这里插入图片描述

    • 在微服务架构下,可以利用配置中心如Nacos、Apollo等等,也可以自己开发配置中心。业务服务读取线程池配置,获取相应的线程池实例来修改线程池的参数。
    • 如果限制了配置中心的使用,也可以自己去扩展ThreadPoolExecutor,重写方法,监听线程池参数变化,来动态修改线程池参数。
    12、final关键字

    修饰类,表示类不可以被继承。
    修饰方法,表示方法不可以被子类覆盖,但是可以重载。
    修饰变量,表示变量一旦被赋值就不可以更改它的值。(不可变指的是变量的引用不可变,不是引用指向的内容的不可变。)

    1. 修饰成员变量
    如果final修饰的是类变量(静态变量),只能在静态初始块中指定初始值或者声明该类变量时指定初始值。
    如果final修饰的是成员变量,可以在非静态初始化块声明该变量或者构造器中执行初始值。
    2. 修饰局部变量
    系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此,final修饰局部变量时,可以在定义时指定默认值(后面的代码中无法再对变量赋值),也可以不指定默认值,而是在后面的代码中对final变量赋初值(仅能赋值一次)
    在这里插入图片描述
    3. 修饰基本数据类型和引用数据类型
    如果是基本数据类型的变量,则其值一旦在初始化后便不能再更改。
    如果是引用类型的变量,则在对其初始化后便不能再让其指向另一个对象,但是**引用的值是可变的**。
    在这里插入图片描述

    13、什么是线程死锁?如何避免?

    在这里插入图片描述

    1、线程死锁的4个必要条件

    1. 互斥条件: 该资源任意一个时刻只由一个线程占用。
    2. 请求与保持条件: 一个线程因请求资源而阻塞时,对已获得的资源保持不放。
    3. 不剥夺条件: 线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
    4. 循环等待条件: 若干线程之间形成一种头尾相接的循环等待资源关系。

    2、如何预防和避免线程死锁?

    - 如何预防死锁? 破坏死锁的产生的必要条件即可:
    ①破坏请求与保持条件:一次性申请所有的资源。
    ②破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
    ③破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。

    - 如何避免死锁?
    避免死锁就是在资源分配时,借助于算法(比如 银行家算法 )对资源分配进行计算评估,使其进入安全状态。
    安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3…Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 序列为安全序列。

    3、死锁怎么排查?

    可以使用jdk自带的命令行工具排查:

    1. 使用jps查找运行的Java进程:jps -l
    2. 使用jstack查看线程堆栈信息:jstack -l 进程id
      基本就可以看到死锁的信息。还可以利用图形化工具,比如JConsole。
    14、线程类的构造方法、静态块是被哪个线程调用的?

    在这里插入图片描述
    在这里插入图片描述
    线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的(线程自身只执行run方法里的代码)。
    如上面的代码所示,程序中包含两个线程,main主线程和t线程,由于t线程是在main主线程内创建,因此t线程的构造方法、静态块代码均由main线程调用执行,t线程内的run方法中的代码由t线程自身调用执行,由于是两个线程在运行,势必存在争抢CPU资源,所有两种输出结果均有可能。

    15、ThreadLocal
    1、ThreadLocal简介

    ThreadLocal叫做 线程本地变量,意思是ThreadLocal中填充的变量属于 当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量 。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
    ThreadLoal变量,线程局部变量,同一个ThreadLocal所包含的对象,在不同的Thread中有不同的副本。这里有几点需要注意:

    • 因为每个Thread内有自己的实例副本,且该副本只能由当前Thread使用。这是也是ThreadLocal命名的由来。
    • 既然每个Thread有自己的实例副本,且其它Thread不可访问,那就不存在多线程间共享的问题。
      ①ThreadLocal类用来提供线程内部的局部变量,不同的线程之间不会相互干扰;
      ②这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量;
      ③在线程的生命周期内起作用,可以减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。
      在这里插入图片描述
      参见链接: ThreadLocal 详解
    2、ThreadLocal和Synchronized的区别

    ThreadLocal 其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于 解决多线程并发访问
    但是ThreadLocal与synchronized有本质的 区别:
    ① Synchronized用于线程间的 数据共享 ,而ThreadLocal则用于线程间的 数据隔离
    ② Synchronized是利用 锁的机制 ,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都 提供了变量的副本 ,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
    而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
    一句话理解ThreadLocal,threadlocal是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocal,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的

    3、ThreadLocal的使用场景

    每个线程需要有自己单独的实例
    实例需要在多个方法中共享,但不希望被多线程共享
    对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLocal可以以非常方便的形式满足该需求。
    对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal使得代码耦合度更低,且实现更优雅。
    举例: 我们的系统应用是一个典型的MVC架构,登录后的用户每次访问接口,都会在请求头中携带一个token,在控制层可以根据这个token,解析出用户的基本信息。那么问题来了,假如在服务层和持久层都要用到用户信息,比如rpc调用、更新用户获取等等,那应该怎么办呢?
    一种办法是显式定义用户相关的参数,比如账号、用户名……这样一来,我们可能需要大面积地修改代码,多少有点瓜皮,那该怎么办呢?
    这时候我们就可以用到ThreadLocal,在控制层拦截请求把用户信息存入ThreadLocal,这样我们在任何一个地方,都可以取出ThreadLocal中存的用户数据。
    很多其它场景的 cookie、session等等数据隔离 也都可以通过ThreadLocal去实现。
    我们常用的 数据库连接池 也用到了ThreadLocal:数据库连接池的连接交给ThreadLocal进行管理,保证当前线程的操作都是同一个Connnection。

    4、ThreadLocal内存泄漏原因

    在这里插入图片描述

    Entry将 ThreadLocal作为Key,值作为value保存,它继承自WeakReference(弱引用),注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」,如下:
    在这里插入图片描述
    主要两个原因:
    ① 没有手动删除这个Entry。
    ② CurrentThread当前线程依然运行。
    第一点很好理解,只要在使用完ThreadLocal后,调用其 remove方法 删除对应的Entry,就能避免内存泄漏。
    第二点稍微复杂一点,由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟Thread一样长。如果threadlocal变量被回收,那么当前线程的threadlocal 变量副本指向的就是key=null, 也即entry(null,value),那这个entry对应的value永远无法访问到。实际使用ThreadLocal场景都是采用线程池,而线程池中的线程都是复用的,这样就可能导致非常多的entry(null,value)出现,从而导致内存泄露。
    综上,ThreadLocal内存泄漏的根源是: threadLocal中的键是弱引用,弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap的生命周期跟 Thread一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法)对应key就会导致entry(null,value)的对象越来越多,从而导致内存泄漏。
    ThreadLocalMap的key为弱引用,value为强引用,GC发生时,该key的ThreadLocal对象被回收,此时该Entry的key为null,value为强引用,也就是说此时value无法通过key访问到并且也不会被回收,从而导致内存泄漏。

    5、为什么不将key设置为强引用?

    那么为什么ThreadLocalMap的key要设计成弱引用呢? 其实很简单,如果key设计成强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期。
    1、假设在业务代码中使用完ThreadLocal,ThreadLocal引用被回收了,但是 因为threadLocalMap的Entry强引用了threadLocal(key就是threadLocal), 造成ThreadLocal无法被回收。 在没有手动删除Entry以及CurrentThread(当前线程)依然运行的前提下, 始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry, Entry就不会被回收( Entry中包括了ThreadLocal实例和value), 导致Entry内存泄漏,也就是说:ThreadLocalMap中的key即使使用了强引用, 也无法完全避免内存泄漏。

    6、为什么key使用弱引用?

    事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(即使ThreadLocal为null)进行判断,如果为null的话,那么会把value置为null的。这就意味着使用threadLocal , CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收。对应value在下一次ThreadLocaI调用get()/set()/remove()中的任一方法的时候会被清除,从而避免内存泄漏。

    7、ThreadLocal原理?
    • Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,每个线程都有一个属于自己的ThreadLocalMap。
    • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值。
    • 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
    • ThreadLocal本身不存储值,它只是作为一个key来让线程往ThreadLocalMap里存取值。
    8、如何正确使用ThreadLocal?

    将ThreadLocal变量定义成private static 的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
    ② 每次~~使用完ThreadLocal,都调用它的remove()方法~~ ,清除数据。
    参考链接:
    史上最全ThreadLocal 详解(一)史上最全ThreadLocal 详解(二)

    9、父子线程间如何使用ThreadLocal?

    父线程能用ThreadLocal来给子线程传值吗?毫无疑问,不能。那该怎么办?
    这时候可以用到另外一个类——InheritableThreadLocal
    使用起来很简单,在主线程的InheritableThreadLocal实例设置值,在子线程中就可以拿到了。
    在这里插入图片描述
    那原理是什么呢?
    原理很简单,在Thread类里还有另外一个变量:
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    在 Thread.init 的时候,如果父线程的inheritableThreadLocals不为空,就把它赋给当前线程(子线程)的inheritableThreadLocals 。

    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
           ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    
    • 1
    • 2
    • 3

    二、多线程并发

    Q:多线程有什么缺点?

    出现内存泄漏、上下文切换、线程安全、死锁。

    Q:并发编程的3要素
    • 原子性: 原子,即一个不可再被分割的粒子。原子性指的是 一个或多个操作要么全部执行成功要么全部执行失败
    • 可见性: 一个线程对共享变量的修改,另一个线程能够立刻看到。(synchronized,volatile)
    • 有序性: 程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序,volatile关键字可以避免指令重排
    Q:Java中如何保证多线程安全运行?
    1. 使用安全类,比如java.util.concurrent下的类,使用原子类AutomicInteger;
    2. 使用自动锁synchronized;
    3. 使用手动锁Lock。
    1、Java中创建线程的方式
    1. 继承 Thread
    2. 实现 Runnable() 接口(无返回值)
    3. 实现 Callable() 接口(获取一个Future对象)(有返回值)
    4. 基于 线程池
    5. 匿名内部类

    注意:

    1. 启动线程的唯一方式: 调用start()方法,start()是一个native方法,用来启动一个新线程,并执行run()方法。{ 调用run()方法并不能启动线程,仅仅是普通Java方法的调用。}
    2. 实现Runnable接口 没有返回值 ;实现Callable接口 有返回值(是个泛型,和Future,FutureTask配合获得异步执行的结果)。
    2、线程生命周期/线程状态

    线程在生命周期中会经历 新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和终止(Terminated) 5种状态。
    在这里插入图片描述

    新建状态: 使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。
    就绪状态: 当线程对象 调用了 start() 方法之后 ,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
    运行状态: 如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。
    阻塞状态: 阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu时间片,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu时间片转到运行(running)状态。阻塞的情况分3种:
    ①等待阻塞(wait -> 等待队列)
    运行(running)的线程执行wait()方法,JVM会把该线程放入等待队列中。
    ②同步阻塞(lock -> 锁池)
    运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
    ③其他阻塞(sleep/join/yield) 运行(running)的线程执行 Thread.sleep(long ms)或 join() 或 yield() 方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
    终止状态: 线程运行完毕或因异常导致线程终止运行。

    3、终止线程的4种方式

    1. 线程正常运行结束。
    2. 使用退出标志退出线程。
    3. 调用interrupt方法结束线程。

    ①线程处于阻塞状态: 如使用了sleep,同步锁的wait,socket中的receiver,accept等方法时,会使线程处于阻塞状态。当 调用线程的interrupt()方法时,会抛出InterruptException异常。 阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的,一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。(线程处于阻塞状态,只有捕获了InterruptedException 异常之后,才能正常终止线程)
    ②线程未处于阻塞状态: 使用 isInterrupted() 判断线程的中断标志来退出循环。当使用 interrupt()方法时,中断标志就会置 true,线程可以终止。
    4. 调用stop方法终止线程(线程不安全)。

    4、sleep和wait的区别

    在这里插入图片描述

    5、什么是线程上下文切换?

    线程上下文切换就是 保存当前线程的运行条件和状态(上下文),加载其他线程的上下文信息

    6、线程调度的常用方法有?

    在这里插入图片描述

    Q:interrupt、isinterrupted和interrupted的区别
    • void interrupt() : 中断线程,例如,当线程A运行时,线程B可以调用线程interrupt() 方法来设置线程的中断标志为true并立即返回。设置标志仅仅是设置标志, 线程A实际并没有被中断,会继续往下执行。
    • boolean isInterrupted() 方法: 检测当前线程是否被中断。
    • boolean interrupted() 方法: 检测当前线程是否被中断,与 isInterrupted 不同的是,该方法如果发现当前线程被中断,则会清除中断标志

    三、Java中常见的锁

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    1、死锁

    在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。
    通俗的讲就是 两个或多个进程无限期的阻塞、相互等待的一种状态

    如何处理死锁问题
    常用的处理死锁的方法有:死锁预防、死锁避免、死锁检测、死锁解除、鸵鸟策略(忽略)。
    阻塞 是由于 资源不足引起的排队等待 的现象。

    四、什么是CAS?

    1、概念及特性

    CAS(Compare And Swap/Set)比较并交换,无锁优化,乐观锁机制,锁自旋,主要用来保证操作的原子性。位于java.util.concurrent.atomic.xxx 包下,属于原子操作。
    CAS算法过程: 它包含 3 个参数 CAS(V,E,N)V(variate)表示要更新的变量(内存值),E(expect)表示预期值(旧的),N(new)表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。(看要更新的值是否等于期望值)
    CAS 操作是基于乐观锁机制,认为操作可以成功完成。当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。 基于这样的原理, CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

    2、原子包 java.util.concurrent.atomic(锁自旋)

    JDK1.5 的原子包:java.util.concurrent.atomic 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个线程进入 ,这只是一种逻辑上的理解。相对于 synchronized 这种阻塞算法,CAS 是 非阻塞算法 的一种常见实现。

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

    CAS 会导致“ABA问题”。CAS 算法实现一个重要前提需要取出内存中某时刻的数据,而在下一时刻比较并替换,那么这个时间差中可能会存在数据变化 。比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的。 部分乐观锁的实现是 通过版本号(version)的方式来解决 ABA 问题 ,乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现 ABA 问题,因为版本号只会增加不会减少。

    5、如何解决CAS带来的3个问题?

    怎么解决ABA问题?加版本号。
    每次修改变量,都在这个变量的版本号上加1,这样,刚刚A->B->A,虽然A的值没变,但是它的版本号已经变了,再判断版本号就会发现此时的A已经被改过了。Java提供了AtomicStampReference类,它的compareAndSet方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳标志的值更新为给定的更新值。

    怎么解决循环性能开销问题?
    在Java中,很多使用自旋CAS的地方,会有一个自旋次数的限制,超过一定次数,就停止自旋。

    怎么解决只能保证一个变量的原子操作问题?
    可以考虑改用 来保证操作的原子性。
    可以考虑合并多个变量,将多个变量封装成一个对象,通过AtomicReference来保证原子性。

    6、Java中保证原子性的方法?

    在这里插入图片描述

    • 使用循环原子类,例如AtomicInteger,实现i++原子操作。
    • 使用juc包下的锁,如ReentrantLock ,对i++操作加锁lock.lock()来实现原子性。
    • 使用synchronized,对i++操作加锁。

    五、什么是AQS(抽象的队列同步器)

    AbstractQueuedSynchronizer ,抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的 同步器框架 ,许多同步类实现都依赖于它,如常用的 ReentrantLock/Semaphore/CountDownLatch。
    在这里插入图片描述
    维护一个 volatile int state共享资源 和一个 FIFO线程等待队列(多线程争抢资源时会进入该阻塞队列等待资源)。state 的访问方式有三种: getState()、setState()、compareAndSetState()。

    1、AQS 定义两种资源共享方式

    1. Exclusive 独占资源-ReentrantLock
    Exclusive(独占,只有一个线程能执行,如 ReentrantLock)

    2. Share 共享资源-Semaphore/CountDownLatch
    Share(共享,多个线程可同时执行,如 Semaphore/CountDownLatch)。
    AQS 只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现 ,AQS 这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过 state 的 get/set/CAS)之所以没有定义成 abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现 tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/ 唤醒出队等),AQS 已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

    1. isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才需要去实现它。
    2. tryAcquire(int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
    3. tryRelease(int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
    4. tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余 可用资源;正数表示成功,且有剩余资源。
    5. tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true,否则返回 false。
    2、同步器的实现是ABS的核心(state资源状态计数)

    同步器的实现是 ABS 核心,以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意, 获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。 以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS 减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后面动作。ReentrantReadWriteLock 实现独占和共享两种方式。
    一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也
    只需要实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器 同时实现独占和共享两种方式,如 ReentrantReadWriteLock

    六、Fork/Join框架了解么?

    Fork/Join框架是Java7提供的一个 用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
    要想掌握Fork/Join框架,首先需要理解两个点,分而治之工作窃取算法

    • 分而治之
      Fork/Join框架体现了分治思想:将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。
      在这里插入图片描述

    • 工作窃取算法
      大任务拆成了若干个小任务,把这些小任务放到不同的队列里,各自创建单独线程来执行队列里的任务。
      那么问题来了,有的线程干活块,有的线程干活慢。干完活的线程不能让它空下来,得让它去帮没干完活的线程干活。它去其它线程的队列里窃取一个任务来执行,这就是所谓的 工作窃取
      工作窃取发生的时候,它们会访问同一个队列,为了减少窃取任务线程和被窃取任务线程之间的竞争,通常任务会使用双端队列,被窃取任务线程永远从双端队列的头部拿,而窃取任务的线程永远从双端队列的尾部拿任务执行。
      在这里插入图片描述

  • 相关阅读:
    css滚动动画网站
    ERP (SAP) Integrator Delphi Edition
    P1045 [NOIP2003 普及组] 麦森数 python 题解
    hive变更数据过程
    Node + Express 后台开发 —— 登录标识
    推荐几个程序员必逛的个人技术博客网站
    容器技术涉及linux内核关键技术
    uniapp输入框组件easyInput初始状态清除符号显示的bug
    方法与递归(JAVA基础一)
    多线程与高并发(8)—— 从CountDownLatch总结AQS共享锁(三周年打卡)
  • 原文地址:https://blog.csdn.net/qq_46111316/article/details/127934728