Java后端八股-------并发编程

本文介绍了Java中synchronized的使用及其底层机制,包括轻量级锁、偏向锁的原理,以及JVM如何管理多线程环境下的锁性能。还讨论了工作内存、AQS公平锁/非公平锁的区别,以及线程池和ThreadLocal在数据隔离中的作用。
摘要由CSDN通过智能技术生成

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

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

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

线程的生命周期

生命周期(状态)
  • New:新建状态,线程创建后进入的状态
  • Runnable:就绪状态,就是可被执行。调用了线程的 start () 方法时,线程进入就绪状态。
  • Running:运行中状态,获得 CPU 后,线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
  • Blocked:阻塞状态,失去 CPU 时间片后。可能由于锁被其他线程占用(获取同步锁失败)、调用了 sleep 或 join 方法、执行了 wait 方法等。
  • Waiting: 等待状态,处在这个状态的线程不会被分配 CPU 时间片,需要其他线程通知或中断。可能由于调用了无参的 wait 和 join 方法。
  • Time-Waiting:限期等待状态,可以在指定时间内自行返回。导可能由于调用了带参的 wait 和 join 方法。
  • Terminated:终止状态,表示当前线程已执行完毕或异常退出。

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

线程方法

  • wait ():让当前线程处于 Waiting 状态,直到其他线程调用此对象的 notify () 方法或 notifyAll () 方法,当前线程进入就绪状态。
  • notify () 和 notifyAll ():唤醒单个或所有线程
  • sleep ():进入休眠状态,和 wait 不同的是不会立即释放锁资源,进入 Time-Waiting 状态
  • yield ():使当前线程让出 CPU 时间片给优先级相同或更高的线程,回到就绪状态,与其他线程重新竞争 CPU 时间片。
  • join (): 用于等待其他线程运行终止。如果当前线程调用了另一个线程的 join 方法,则当前线程进入阻塞状态,当另一个线程结束时当前线程才能从阻塞状态转为就绪状态,等待获取 CPU 时间片。底层使用 wait,也会释放锁。
start 和 run 的区别
  • start(): 它的作用是启动一个新线程,新线程会执行相应的 run () 方法。start () 不能被重复调用。
  • run(): run () 就和普通的成员方法一样,可以被重复调用。单独调用 run () 的话,会在当前线程中执行 run (),而并不会启动新线程!
Sleep () 和 Wait ()

每个对象都有一个锁来控制同步访问,Synchronized 关键字可以和对象的锁交互,来实现同步方法或同步块。

  • sleep () 方法正在执行的线程主动让出 CPU(然后 CPU 就可以去执行其他任务),在 sleep 指定时间后 CPU 再回到该线程继续往下执行 (注意:sleep 方法只让出了 CPU,而并不会释放同步资源锁!!!);wait () 方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了 notify () 方法,之前调用 wait () 的线程才会解除 wait 状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify 的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说 notify 只是让之前调用 wait 的线程有权利重新参与线程的调度)
  • sleep () 方法可以在任何地方使用;wait () 方法则只能在同步方法或同步块中使用;
  • sleep() 是线程类(Thread)的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait() 是 Object 的方法,调用会放弃对象锁,进入等待队列,待调用 notify ()/notifyAll () 唤醒指定的线程或者所有线程,才会进入锁池,不再次获得对象锁才会进入运行状态;
notify 和 notifyAll
  • 如果线程调用了对象的 wait () 方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
  • 当有线程调用了对象的 notifyAll () 方法(唤醒所有 wait 线程)或 notify () 方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了 notify 后只要一个线程会由等待池进入锁池,而 notifyAll 会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
  • 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait () 方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
  • notifyAll 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify 只会唤醒一个线程。

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

原子性、可见性、有序性

  • 原子性: Java 中,对基本数据类型的读取和赋值操作是原子性操作,所谓原子性操作就是指这些操作是不可中断或被分割的,要做一定做完,要么就没有执行。
  • 可见性:Java 就是利用 volatile 来提供可见性的。 当一个变量被 volatile 修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。而普通变量则不能保证这一点。
  • 有序性:JMM 是允许编译器和处理器对指令重排序的,但是规定了 as-if-serial 语义,即不管怎么重排序,程序的执行结果不能改变。

线程间通信机制

以什么样的机制来交换信息?有两种,共享内存和消息传递。

  • 共享内存:
    线程之间共享程序的公共状态,通过写 - 读内存中的公共状态进行隐式通信。
    Java 并发采用共享内存模型,线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
  • 消息传递:
    线程之间没有公共状态,线程之间必须通过发送消息来显示通信。
  • volatile 告知程序任何对变量的读需要从主内存中获取,写必须同步刷新回主内存,保证所有线程对变量访问的可见性。
  • 锁机制 synchronized 确保多个线程在同一时刻只能有一个处于方法或同步块中,保证线程对变量访问的原子性、可见性和有序性
  • 等待通知机制 通过 join,wait,notify 方法实现。
    具体:指一个线程 A 调用了对象的 wait 方法进入等待状态,另一线程 B 调用了对象的 notify/notifyAll 方法,线程 A 收到通知后结束阻塞并执行后序操作。对象上的 wait 和 notify/notifyAll 完成等待方和通知方的交互。
    如果一个线程执行了某个线程的 join 方法,这个线程就会阻塞等待执行了 join 方法的线程终止,这里涉及等待 / 通知机制。join 底层通过 wait 实现,线程终止时会调用自身的 notifyAll 方法,通知所有等待在该线程对象上的线程。
  • 管道 IO 流 用于线程间数据传输,媒介为内存。
    PipedOutputStream 和 PipedWriter 是输出流,相当于生产者,PipedInputStream 和 PipedReader 是输入流,相当于消费者。管道流使用一个默认大小为 1KB 的循环缓冲数组。输入流从缓冲数组读数据,输出流往缓冲数组中写数据。当数组已满时,输出流所在线程阻塞;当数组首次为空时,输入流所在线程阻塞。
  • ThreadLocal 是线程共享变量,但它可以为每个线程创建单独的副本,副本值是线程私有的,互相之间不影响。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
图中的 synchronized方法如果没有锁,那么可能会有超卖,数据错误等情况。
加锁之后会按顺序售卖。
synchronized的底层是monitor。
synchronized锁不保证公平(不算公平锁)。

syncronized 关键字

  • synchronized 采用的是 CPU 悲观锁机制,即线程获得的是独占锁, 其他线程只能依靠阻塞来等待线程释放锁。
  • 不同线程对同步锁的访问是互斥的。也就是说,某时间点,对象的同步锁只能被一个线程获取到。通过同步锁,我们就能在多线程中,实现对 “对象 / 方法” 的互斥访问。
  • synchronized 修饰静态方法以及同步代码块的 synchronized (类.class) 用法锁的是类,线程想要执行对应同步代码,需要获得类锁。
  • synchronized 修饰成员方法,线程获取的是当前调用该方法的对象实例的对象锁
偏向锁(Biased Locking):

偏向锁是JVM为了减少无竞争环境下的同步开销而引入的一种优化机制。当一个线程第一次访问同步代码块时,JVM会将锁偏向这个线程。

实现原理:

  • 对象头标记:偏向锁首先将对象头的锁状态设置为偏向模式,并记录偏向的线程ID。
    无竞争快速路径:当偏向的线程再次访问同步代码块时,它不需要进行任何同步操作,因为JVM知道这个线程已经持有偏向锁。
  • 竞争检测:如果有其他线程尝试访问同一个同步代码块,JVM会检测到这种竞争,然后撤销偏向状态,将锁状态升级为轻量级锁或重量级锁。
轻量级锁(Lightweight Locking):

轻量级锁是当JVM检测到有锁竞争时,为了减少重量级锁的开销而引入的一种优化机制。

实现原理:

  • 对象头指针:轻量级锁将对象头的锁状态设置为指向锁记录的指针,锁记录包含了锁拥有者的线程栈帧信息。
  • CAS操作:线程尝试通过CAS操作将锁记录的指针指向自己的线程栈帧,如果成功,表示该线程获得了锁。
  • 自旋锁:如果CAS操作失败,表示有其他线程正在竞争锁,当前线程可能会进入自旋状态,等待锁的释放。
  • 锁升级:如果自旋一定次数后锁仍未释放,或者检测到有多个线程竞争,轻量级锁可能会升级为重量级锁。

区别和联系:

  • 偏向锁适用于只有一个线程访问同步代码块的场景,可以减少首次获取锁的开销。
  • 轻量级锁适用于有少量线程竞争的场景,通过CAS操作和自旋来减少线程阻塞和上下文切换的开销。
  • 锁升级:偏向锁在遇到竞争时可能会升级为轻量级锁,轻量级锁在竞争加剧时可能会升级为重量级锁。
  • 性能:偏向锁和轻量级锁都旨在减少同步操作的性能开销,但它们的适用场景和实现机制不同
轻量级锁(Lightweight Lock):

轻量级锁是Java虚拟机(JVM)在没有多线程竞争的情况下,为了减少传统重量级锁带来的性能消耗而引入的一种优化机制。

实现原理:

  • CAS操作:轻量级锁使用CAS(Compare-And-Swap)操作来尝试获取锁。如果对象的锁状态可以被CAS操作成功更改为轻量级锁状态,那么线程就获得了锁。
  • 自旋锁:在轻量级锁状态下,如果其他线程尝试获取同一个锁,JVM会尝试使用自旋锁来等待锁的释放。自旋锁会消耗CPU资源,但是可以减少线程的上下文切换开销。
  • 锁升级:如果自旋等待一定次数后锁仍未释放,或者检测到有多个线程竞争同一把锁,轻量级锁可能会升级为重量级锁,以避免长时间的自旋等待。
对象锁(重量级锁):

对象锁是传统意义上的synchronized实现的锁,也称为监视器锁(Monitor Lock)。

实现原理

  • 监视器:每个Java对象内部都有一个监视器锁(Monitor),当一个线程访问同步代码块时,它必须首先获得对象的监视器锁。
  • 阻塞和唤醒:如果监视器锁被其他线程持有,请求锁的线程将被阻塞,直到锁被释放。锁的持有者执行完毕后,会唤醒在锁上等待的线程。
  • 可重入性:对象锁是可重入的,这意味着同一个线程可以多次获取同一个对象的锁,而不会阻塞自己。
  • 互斥性:对象锁确保在任意时刻,只有一个线程可以执行某个对象的同步代码块。

区别:

  • 性能开销:轻量级锁主要在没有线程竞争的情况下使用,性能开销较小。对象锁在有线程竞争时使用,可能会涉及到线程的阻塞和唤醒,性能开销较大。
    锁状态:轻量级锁是一种优化手段,当检测到线程竞争时会升级为对象锁。
  • 使用场景:轻量级锁适用于锁竞争不激烈,且线程持有锁的时间较短的场景。对象锁适用于需要严格互斥的场景,无论是否有线程竞争。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
线程没有竞争关系的时候,引入了轻量级锁,当需要处理竞争关系的时候一定要用到重量级锁(线程的对象锁)。

在这里插入图片描述
在这里插入图片描述
mark word中重量级锁时,ptr_to_heavyweight_monitor是一个指向monitor的指针。
在这里插入图片描述
cas交换是用来保证原子操作的.
在这里插入图片描述
👆cas会做交换,交换地址值,这样其他线程就无法获取锁,但是自己的线程内可以多次获取锁(没有竞争关系时)。
在这里插入图片描述

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

在这里插入图片描述
在Java中,轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)是Java虚拟机(JVM)内部实现的锁优化技术,它们旨在提高多线程环境下锁的性能。这些锁策略是JVM自动管理的,通常不需要开发者手动开启或关闭。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传程里面的工作内存不存在线程安全的问题,共享变量副本是主内存中共享变量的副本,需要通过JMM控制进行save和load

互斥量(mutex)

  • 互斥量本质上是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。
  • 对互斥量进行加锁以后,任何其它试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。

读写锁

  • 读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁。
  • 而读写锁可以有三种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。

volatile 关键字

volatile关键字的实现依赖于Java内存模型(JMM)和底层硬件的内存屏障(Memory Barrier)指令。
JVM 提供的最轻量级的同步机制。被 volatile 修饰的变量有两种特性:

保证此变量对所有线程的可见性
可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是立即可以得知的。

禁止指令重排序优化

  • 使用 volatile 变量进行写操作,生成的汇编指令操作是带有 lock 前缀的,相当于一个内存屏障,后面的指令不能重排到内存屏障之前的位置。

  • 使用 lock 前缀的指令在多核处理器有两个功能:
    ① 将当前处理器缓存行的数据写回到系统内存。
    ② 这个写回内存的操作会使其他在 CPU 里缓存了该内存地址的数据无效。这种操作相当于对缓存中的变量做了一次 store 和 write 操作,可以让 volatile 变量的修改对其他处理器立即可见。

  • 使用:状态量标记,对变量的读写操作,标记状态量保证修改对线程立即可见,效率比同步锁好。单例模式的实现,典型的双重检查锁定(DCL)。

重排序

就是编译器和处理器将指令的执行顺序进行先后调整。但必须符合有序性,即无论指令怎么排序,程序执行结果不能变。多线程程序重排序会造成结果不一致,所以使用 volatile 关键字禁止重排序,确保 “有序性”。

syncronized 关键字

  • synchronized 采用的是 CPU 悲观锁机制,即线程获得的是独占锁, 其他线程只能依靠阻塞来等待线程释放锁。
  • 不同线程对同步锁的访问是互斥的。也就是说,某时间点,对象的同步锁只能被一个线程获取到。通过同步锁,我们就能在多线程中,实现对 “对象 / 方法” 的互斥访问。
    synchronized 修饰静态方法以及同步代码块的 synchronized (类.class) 用法锁的是类,线程想要执行对应同步代码,需要获得类锁。
  • synchronized 修饰成员方法,线程获取的是当前调用该方法的对象实例的对象锁。

synchronized 和 volatile 区别

  • volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比 synchronized 关键字要好。
  • 多线程访问 volatile 关键字不会发生阻塞,而 synchronized 关键字可能会发生阻塞
    volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
atomicLong在做++操作的时候就是CAS,但是Longadder可以通过空间实现多个线程的并发++

++ 线程安全吗?

假设有 1000 个线程,每个线程都对共享遍历 count 进行 1000 次 ++ 操作,最终结果始终不会为 1000000。
因为一个线程对共享变量进行操作时会将共享变量从主内存加载到自己的工作内存,完成操作后才会将结果保存到主内存。
如果一个线程运算完成后还没有加载到主内存,此时共享变量的值就被另一个线程从主内存进行读取,此时读到的就是脏数据,会覆盖其他数据计算完的值。

解决:

  • volatile 虽然能保证共享变量的可见性,但是无法保证它的原子性,也就是说其他线程还是会将当前线程的值覆盖。
  • 对 ++ 操作加同步锁,同一时间只允许一个线程进行操作。
  • 使用支持原子操作的类 AtomicInteger, 使用的 CAS 算法,效率比第一种高

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

CAS

  • CAS 是比较并替换。它当中使用了 3 个基本操作数:内存地址 V,旧的预期值 A,要修改的新值 B。如果内存位置 V 的值等于预期的 A 值,则将该位置更新为新值 B,否则不进行任何操作。许多 CAS 的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。
  • 采用的是一种乐观锁的机制,它不会阻塞任何线程,所以在效率上,它会比 synchronized 要高。
  • 所以,在并发量非常高的情况下,我们尽量的用同步锁,而在其他情况下,我们可以灵活的采用 CAS 机制。
  • 乐观锁是一种更高效的机制,它的原理就是每次不加锁去执行某项操作,如果发生冲突则失败并重试,直到成功为止,其实本质上不算锁,所以很多地方也称之为自旋,乐观锁用到的主要机制就是 CAS

乐观锁和悲观锁

乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。

  • 乐观锁:乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
  • 悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。

实现

  • 悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如 Java 的 synchronized 关键字),也可以是对数据加锁(如 MySQL 中的排它锁)。
  • 乐观锁的实现方式主要有两种:CAS 机制和版本号机制

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

在这里插入图片描述
在这里插入图片描述
👆在上面这段代码中,线程1修改的stop为true,线程2是能读到的,但是线程3while循环却不停止。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
JVM优化导致的重排序。
实验数据
在这里插入图片描述
在这里插入图片描述
👆这个可以解决问题
在这里插入图片描述
👆这个不能组织x在y后执行,不能解决问题。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
AQS的实现类当中,公平锁和非公平锁都有实现。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Synchronized 和 Lock 区别

  • synchronized 当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。

  • Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;

  • synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unlock () 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁

  • Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断;

  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

  • 选择:
    在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于 synchronized。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
一把锁,获取效率很低
在这里插入图片描述
每个hash值都有一把锁。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

线程池

好处及作用
  • (重要)一个线程池中可以有好几个线程,通过复用其中已创建的线程,可以降低资源消耗,提高程序的响应速度;
  • (重要)而且可以控制线程最大并发数,提高了线程的可管理性。
  • 实现某些与时间相关的功能,如定时执行、周期执行等。
  • 隔离线程环境,可以配置独立线程池,将较慢的线程与较快的隔离开,避免相互影响。
  • 实现任务线程队列缓冲策略和拒绝机制。
ThreadExecutor 线程池的参数

① corePoolSize:常驻核心线程数,设置过大会浪费资源,过小会导致线程的频繁创建与销毁。
② maximumPoolSize:线程池能够容纳同时执行的线程最大数,必须大于 0。
③ keepAliveTime:当线程池中线程数量超过 corePoolSize 时,允许等待多长时间从 workQueue 中拿任务,超过这个时间会被销毁,避免浪费内存资源。
④ unit:keepAliveTime 的时间单位。
⑤ workQueue:工作队列 / 阻塞队列,当线程请求数大于等于 corePoolSize 时线程会进入队列。
⑥ threadFactory:线程工厂,用来创建的线程。可以给线程命名,有利于分析错误。
⑦ rejectHandler:线程池中线程超过 maximumPoolSize 时采用

  • 拒绝处理任务时的策略
    默认使用 AbortPolicy 丢弃任务并抛出 RejectedExecutionException 异常;
    CallerRunsPolicy 重新尝试提交任务(由调用线程(提交任务的线程)处理该任务);
    DiscardOldestPolicy 丢弃队列最前面的任务,然后重新提交被拒绝的任务;
    DiscardPolicy 丢弃任务但不抛出异常。
    线程池处理任务的流程

  • 简述:

    • 首先判断核心线程池 (corePoolSize) 里是否满了,如果没满则直接从核心线程池中创建一个线程来执行
    • 如果核心线程满了就判断工作队列 (workQueue) 是否满了,没满的话提交任务到工作队列等待执行
    • 如果工作队列满了就判断整个线程池是否满了 (maximumPoolSize),如果满了就执行决绝策略,否则就创建一个新线程用于执行任务。
  • 原理:

    • ① 创建一个线程池,在还没有任务提交的时候,默认线程池里面是没有线程的。也可以调用 prestartCoreThread 方法,来预先创建一个核心线程。
    • ② 线程池里还没有线程或者线程池里存活的线程数小于核心线程数 (workCount < corePoolSize) 时,这时对于一个新提交的任务,线程池会创建一个线程去处理提交的任务。此时线程池里面的线程会一直存活着,就算空闲时间超过了 keepAliveTime,线程也不会被销毁,而是一直阻塞在那里一直等待任务队列的任务来执行。
    • ③当线程池里面存活的线程数已 >=corePoolSize 了,对于一个新提交的任务,会被放进阻塞队列排队等待执行。而之前创建的线程并不会被销毁,而是不断的去拿阻塞队列里面的任务。
      当任务队列为空时,线程会阻塞,直到有任务被放进任务队列,线程拿到任务后继续执行,执行完了过后会继续去拿任务。这也是为什么线程池队列要是用阻塞队列。
    • ④ 当线程池里面存活的线程数已经等于 corePoolSize 了,并且任务队列也满了,(这里假设 maximumPoolSize>corePoolSize)这时如果再来新的任务,线程池就会继续创建新的线程来处理新的任务,直到线程数达到 maximumPoolSize,就不会再创建了。
      这些新创建的线程执行完了当前任务过后,在任务队列里面还有任务的时候也不会销毁,而是去任务队列拿任务出来执行。在当前线程数大于 corePoolSize 过后,线程执行完当前任务,会有一个判断当前线程是否需要销毁的逻辑:如果能从任务队列中拿到任务,那么继续执行,如果拿任务时阻塞(线程处于空闲状态),那超过 keepAliveTime 时间就直接返回 null 并且销毁当前线程,直到线程池里面的线程数等于 corePoolSize 之后才不会进行线程销毁。
    • ⑤ 如果当前的线程数达到了 maximumPoolSize,并且任务队列也满了,这种情况下还有新的任务过来,那就直接采用拒绝策略进行处理。默认的处理器逻辑是使用 AbortPolicy 抛出一个 RejectedExecutionException 异常。

创建线程池
可以通过 Executors 的静态工厂方法创建四类线程池:

  • ① newFixedThreadPool,固定大小的线程池,核心线程数也是最大线程数,不存在空闲线程,keepAliveTime = 0。使用的工作队列是无界阻塞队列 LinkedBlockingQueue,适用于负载较重的服务器。
  • ② newSingleThreadExecutor,使用单线程,相当于单线程串行执行所有任务,所有任务按照指定顺序 (FIFO, LIFO, 优先级) 执行。适用于需要保证顺序执行任务的场景。
  • ③ newCachedThreadPool,创建一个可缓存线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程,若没有可回收的空闲线程,就会新建线程。如果主线程提交任务的速度高于线程处理的速度,线程池可能会不断创建新线程,极端情况下会耗尽 CPU 和内存资源。适用于执行很多短期异步任务的小程序或负载较轻的服务器。
  • ④ newScheduledThreadPool:定长线程池,支持定期及周期性任务执行,适用需要多个后台执行任务,同时限制线程数量的场景。相比 Timer 更安全,功能更强,与 newCachedThreadPool 的区别是不回收工作线程。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Linked效率高一点(两把锁),因为可以分别控制输入输出。而array只有一把锁,输入输出同时控制。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
👆可以一直创造线程。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
使用线程池子来进行多线程的复用就可以达到上图中右边的效果。在这里插入图片描述
上图为上上图中利用线程池执行的线程逻辑代码。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上图中保存搜索历史记录使用的异步调用。
taskExecutor是一个已经定义的线程池。
在这里插入图片描述
在这里插入图片描述

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

ThreadLocal

ThreadLocal 是Java中的一个类,它提供了一种线程局部变量的存储方式,使得每个线程都可以独立地访问自己的变量副本,而不会与其他线程发生冲突。这种特性在多线程环境中非常有用,因为它可以避免线程间的数据共享和同步问题。

  • ThreadLocal 的工作原理
    线程局部变量存储:ThreadLocal 为每个线程提供了一个独立的变量副本。这些副本存储在 ThreadLocalMap 中,每个线程的 ThreadLocal 实例都有一个与之关联的 ThreadLocalMap。

  • 线程隔离:由于每个线程都有自己的 ThreadLocalMap,因此线程之间不会相互干扰,实现了线程局部变量的隔离。

  • 内存泄漏风险:如果不正确地使用 ThreadLocal,可能会导致内存泄漏。因为 ThreadLocalMap 的生命周期与线程相同,如果线程长时间运行,而 ThreadLocal 变量没有被及时清理(例如通过调用 remove() 方法),那么这些变量就会一直占用内存。

ThreadLocalMap:

ThreadLocalMap 是 ThreadLocal 类内部的一个静态类,它是一个线程私有的哈希表,用于存储线程局部变量。以下是 ThreadLocalMap 的一些关键特性:

  • 线程私有:每个线程都有一个独立的 ThreadLocalMap 实例,存储该线程的 ThreadLocal 变量。

  • 键值对存储:ThreadLocalMap 以 ThreadLocal 实例作为键,以线程局部变量的值作为值,存储键值对。

  • 弱引用键:ThreadLocalMap 的键(即 ThreadLocal 实例)是弱引用。这意味着如果 ThreadLocal 实例没有被其他地方引用,它可以被垃圾回收器回收,即使 ThreadLocalMap 中还存在这个键的条目。

  • 清理机制:ThreadLocalMap 提供了一种清理机制,当 ThreadLocal 被回收时,会尝试清理 ThreadLocalMap 中对应的条目。但是,如果 ThreadLocal 实例被回收后,对应的线程仍然存活,那么 ThreadLocalMap 中的条目仍然会占用内存,直到线程结束。

  • 迭代器清理:ThreadLocalMap 的 Entry 类提供了一个 set() 方法,允许在迭代过程中清理条目。这有助于在访问 ThreadLocalMap 时清理不再需要的条目。

在这里插入代码片

public class ThreadLocalExample {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                threadLocal.set(i);
                System.out.println("Thread " + Thread.currentThread().getName() + " has value " + threadLocal.get());
            }).start();
        }
    }
}

在这个示例中,每个线程都会设置和获取自己的 ThreadLocal 变量副本,而不会影响到其他线程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ThreadLocal中线程之间数据是不互通的。通过以上样例可以看出。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

当面试官要求你回答一个关于 Python 的典型问题时,下面是一个可能的八股文回答模板: 首先,我会对问题进行理解和分析,确保我准确地理解了问题的要求和背景。 然后,我会按照以下结构来回答问题: 1. **问题背景和解决思路**:我会简要介绍问题的背景和解决思路。例如,如果问题是关于列表的操作,我可能会解释什么是列表以及如何使用它们来存储和操作数据。 2. **核心代码实现**:接下来,我会给出一个基本的代码实现,以展示我对问题的理解和能力。我会尽量保持代码简洁而清晰,并结合注释来解释代码的每一步。 3. **优化和扩展**:在展示基本实现之后,我还可以讨论如何优化代码以提高性能或满足更复杂的需求。我可以提出一些常见的优化策略,例如使用生成器、使用递归等。 4. **错误处理和异常处理**:此外,我还可以谈谈在代码中处理错误和异常的重要性。我会提到一些常见的错误和异常类型,并解释如何使用 try-except 语句来捕获和处理它们。 5. **应用场景和实际例子**:最后,我会给出一些实际的应用场景和示例,以展示我对 Python 的灵活运用能力。我可以谈论一些常见的 Python 库和框架,以及它们在实际项目中的使用案例。 通过以上结构,我可以清晰地展示我对 Python 的理解、能力和经验,并给面试官留下一个积极的印象。当然,在回答问题时,我也会根据具体问题的要求进行适当调整和补充。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值