【java面试】多线程

多线程

并发和并行有什么区别?

并发强调的是同一时间间隔,并行强调的是同一时间发生。并发指的是同一台处理器在同一时间间隔下处理多个程序。而并行则是多个处理器或者同一个处理器多个核同一时间处理多个程序

线程和进程有什么区别呢?

进程是运行中的一段程序。而进程中的执行的每个任务都可以作为一个线程。

  • 进程是资源分配的基本单位,线程是执行的基本单位。一个进程可以包含多个线程,它们共享进程的资源。

  • 进程之间相互独立,拥有独立的内存空间和系统资源。线程之间共享进程的内存空间和资源。

  • 进程之间通信的方式比较复杂,包括共享内存、管道、消息队列等。线程之间通信相对简单,可以通过共享内存来实现。

  • 进程的切换代价相对较高,需要保存和恢复进程的上下文信息。线程的切换代价较低,因为线程共享相同的地址空间和资源。

什么是线程的上下文切换

线程上下文切换是指在多线程环境下,CPU 从一个线程切换到另一个线程时,需要保存当前线程的上下文(包括程序计数器、寄存器等状态),并加载下一个线程的上下文,以便让下一个线程继续执行。

线程上下文切换是一项开销较高的操作,因为它需要保存和恢复大量的状态信息。过多的线程上下文切换会导致系统性能下降。因此,在设计多线程应用程序时,需要考虑减少线程上下文切换的次数,以提高系统的运行效率。常见的策略包括使用线程池、减少锁的竞争、采用非阻塞算法等。

创建线程有哪几种方式

继承 Thread 类创建线程类,调用线程对象的 start() 方法来启动该线程。
通过 Runnable 接口创建线程类。
通过 Callable 和 Future 创建线程,

线程的run()和start()有什么区别?

start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法( 可以直接调用 Thread 类的 run 方法吗)?

当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码。

但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码,

只会把 run 方法当作普通方法去执行

线程安全的解决方案?

• 数据不共享,单线程可见,比如 ThreadLocal 就是单线程可见的;
• 使用线程安全类,比如 StringBuffer 和 JUC(java.util.concurrent)下的安全类
• 使用同步代码或者锁。

守护线程是什么

守护线程(即 daemon thread),是个服务线程,准确地来说就是服务其他的线程。

线程有哪些状态???

新建,就绪,运行,阻塞,死亡。
新建:在生成线程对象,还没有调用该对象的 start 方法,这是属于创建状态
就绪:有资格分到 cpu 但还没有轮到
运行:分到 cpu,能真正执行线程内代码
阻塞:没资格分到 cpu 时间的
死亡:一个线程的 run 方法结束或者调用 stop 方法后,该线程就会死亡

sleep()和 wait()的区别?

sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用 sleep 不会释放对象锁。

wait 是 Object 类的方法,对此对象调用 wait 方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出 notify 方法(或 notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态

为什么 wait() 方法不定义在 Thread 中?

wait() 是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入 WAITING 状态,自然是要操作对应的对象(Object)而非当前的线程(Thread)。

类似的问题: 为什么 sleep() 方法定义在 Thread 中?
因为 sleep() 是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。

什么是线程死锁,产生线程死锁的原因(必要条件)有哪些?

  • 互斥条件:线程对所需资源的访问具有排他性,即一次只能有一个线程占有资源。如果多个线程同时持有一个资源,并且互斥条件得不到满足,就可能导致死锁。
  • 请求和保持条件:线程在持有某个资源的同时,又请求其他资源,但无法获得所需的额外资源,导致线程等待。如果多个线程都在持有某个资源并等待其他线程释放资源,就可能发生死锁。
  • 不可剥夺条件:线程已经获得的资源不能被其他线程强制剥夺,只能由线程自愿释放。如果线程在持有某个资源时不愿释放,其他线程无法获得该资源,就可能导致死锁。
  • 循环等待条件:多个线程形成一个循环等待资源的链,每个线程都在等待下一个线程所持有的资源。如果没有外部干预打破循环,就会导致死锁。

以上条件同时满足时,就可能产生死锁。当发生死锁时,所有涉及到的线程都无法继续执行,程序会陷入无限等待的状态

如何预防死锁?如何避免死锁

预防:破坏死锁的产生的必要条件即可
破坏请求与保持条件:要求进程在开始执行之前一次性获取所有需要的资源,而不是逐个获取。
如果一个进程无法获取所有所需资源,它必须释放已经占有的资源,然后再次尝试获取。
破坏不剥夺条件:引入资源的抢占机制,以便在需要时可以回收已经分配的资源,然后分配给其他进程。
破坏循环等待条件:引入资源分级,确保资源的请求顺序,以消除循环等待。例如,规定所有进程只能按照资源的编号递增的顺序请求资源。

避免的几个常用方法
使用资源分配策略:通过合理的资源分配策略,减少资源竞争的可能性。例如,可以使用资源预分配或动态分配资源的方式,比如借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态,确保每个线程都能够及时获取到所需的资源。

使用定时锁和超时机制:在获取锁的过程中,设置超时时间,如果超过一定时间还未获取到锁,就放弃当前操作,避免无限等待导致死锁。 定时锁和超时机制可以确保在一定时间内,无法获取到资源的线程能够放弃等待,继续执行其他操作

请描述一下线程池?

简单来说就是创建一些线程放入一个池子中,来任务了有空闲线程就分配线程,如果没有的话任
务进入队列进行等待其它任务使用后释放线程,如果队列满了的话我们就在创建一个线程,如果
创建这个线程大于最大线程数限制那么就会触发拒绝策略!(这个过程体现复用的原理)

线程池的优点

  • 线程池可以实现少量线程复用执行大量任务,提高线程的利用率
  • 不用重复的创建销毁,提高程序的响应速度
  • 放在同一个池子中,方便统一管理
  • 可以控制最大并发数

线程池的参数

(1)corePoolSize:线程池中常驻核心线程数

(2)maximumPoolSize:线程池能够容纳同时执行的最大线程数

(3)keepAliveTime:多余的空闲线程存活时间

(4)unit:keepAliveTime的时间单位

(5)workQueue:任务队列,被提交但尚未执行的任务

(6)threadFactory:表示生成线程池中的工作线程的线程工厂

(7)handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何拒绝

JMM

什么叫做指令重排序,有几种

简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。常见的指令重排序有下面 2 种情况:
编译器优化重排:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。
指令并行重排:现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

volatile的特性

可见性:当一个线程修改了被 volatile 修饰的变量的值时,它会立即将新值刷新到主内存中,同时通知其他线程该变量的值已被更新。其他线程在读取该变量时,会从主内存中重新获取最新的值,而不是使用线程本地缓存的旧值。这样可以确保不同线程之间对该变量的修改是可见的。

禁止指令重排序:volatile 关键字可以禁止编译器和处理器对指令进行重排序优化。这样可以保证在多线程环境下,对 volatile 变量的读写操作按照程序的顺序执行,不会出现指令重排导致的意外结果

然而,volatile 无法保证原子性。当多个线程同时对同一个 volatile 变量进行修改时,虽然每个线程的修改会立即对其他线程可见,但由于没有互斥机制,可能会出现同时读取、修改和写入的情况,导致最终结果出现不一致的问题。这是因为 volatile 无法保证对变量的复合操作具有原子性。

为了保证对变量的原子操作,需要使用其他机制,如使用锁(例如 synchronized 关键字或 java.util.concurrent 中的原子类),或者使用 volatile 结合原子类(如 AtomicInteger、AtomicLong 等)来实现原子操作。这些机制可以提供更强的原子性保证,确保对变量的操作是原子的、不可分割的

谈一下悲观锁和乐观锁的区别

悲观锁的代表分别是 synchronized 和 Lock 锁
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

public void performSynchronisedTask() {
    synchronized (this) {
        // 需要同步的操作
    }
}

private Lock lock = new ReentrantLock();
lock.lock();
try {
   // 需要同步的操作
} finally {
    lock.unlock();
}

高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。乐观锁的代表时 AtomicInteger,使用 cas 来保证原子性核心思想是无需加锁,每次只有一个线程能修改共享变量,其它失败线程不需要停止,不断重试直至成功。由于线程一直运行,不需要阻塞因此不涉及到线程的上下文切换。

在 Java 中java.util.concurrent.atomic包下面的原子变量类(比如AtomicInteger、LongAdder)就是使用了乐观锁的一种实现方式 CAS 实现的。

高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。

两者用途上的不同:

  • 悲观锁通常多用于写比较多的情况下(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如LongAdder),也是可以考虑使用乐观锁的,要视实际情况而定。
  • 乐观锁通常多于写比较少的情况下(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考java.util.concurrent.atomic包下面的原子变量类)。

如何实现乐观锁

乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些。

版本号机制: 它的基本思想是为每个被并发访问的对象引入一个版本号,用于标识对象的状态。当一个线程要修改对象时,会先读取对象的版本号,然后执行操作。在写回操作时,会检查对象的版本号是否发生了变化,如果没有变化,则说明在该线程操作期间没有其他线程修改过对象,可以顺利写回。如果版本号发生了变化,则说明在该线程操作期间有其他线程修改过对象,此时需要进行冲突处理。

CAS: CAS(Compare and Swap,比较并交换)是一种并发编程中的原子操作,用于实现乐观锁机制。CAS 操作包含三个操作数:内存位置(地址)、期望的旧值和要更新的新值。CAS 操作的执行过程如下:

  • 比较内存位置的当前值与期望的旧值是否相等。
  • 如果相等,则将内存位置的值更新为新值。
  • 如果不相等,则说明有其他线程修改了内存位置的值,操作失败。

CAS 是一种无锁操作,不需要使用传统的互斥锁机制,因此在并发环境中可以提供更高的性能。CAS 的关键在于比较和更新是一个原子操作,当多个线程同时尝试使用 CAS 操作更新同一个内存位置时,只有一个线程能成功执行更新操作,其他线程会重新尝试或进行其他处理

乐观锁一定就是好的吗?

乐观锁认为对一个对象的操作不会引发冲突,所以每次操作都不进行加锁,只是在最后提交更改时验证是否发生冲突,如果冲突则再试一遍,直至成功为止,这个尝试的过程称为自旋。

乐观锁没有加锁,但乐观锁引入了ABA问题,此时一般采用版本号进行控制;
也可能产生自旋次数过多问题,此时并不能提高效率,反而不如直接加锁的效率高

CAS存在哪些问题?

  • ABA问题:ABA问题指的是当一个值从A变为B,然后再变回A时,CAS操作无法察觉到这种变化。解决ABA问题的一种方法是使用版本号或标记来跟踪变量的状态变化,确保在比较和交换时能够正确地检测到值的变化。

  • 自旋开销:CAS操作在并发冲突时采用自旋的方式,即反复尝试更新操作直到成功或达到一定次数。如果CAS操作频繁失败,会导致大量的自旋,增加了系统的开销和延迟。可以通过合理设置自旋次数或采用退避策略,如逐渐增加自旋次数或采用指数退避,以减少自旋开销

  • 只适用于单个变量:CAS操作只能针对单个变量进行原子比较和交换操作,无法直接应用于多个变量或复杂的数据结构。对于多个变量的原子操作,可以使用锁机制或其他并发控制机制来确保操作的原子性

  • 更新丢失问题:CAS操作无法解决更新丢失问题。如果多个线程基于相同的旧值进行CAS操作,可能会导致其中一个线程的更新被覆盖,从而导致数据的丢失。解决更新丢失问题的一种方法是使用版本号或时间戳等机制,以便在进行CAS操作时能够检测到变量是否已经被其他线程修改。

Synchronized

synchronized 是什么?有什么用?

synchronized是Java中的关键字,用于实现线程同步和互斥的机制。它可以用于修饰方法或代码块,用于控制对共享资源的访问。

synchronized的主要作用是确保在多线程环境下的线程安全性,即防止多个线程同时访问共享资源而导致的数据不一致或冲突。

具体来说,synchronized关键字有以下作用:

  • 互斥性:当一个线程进入被synchronized修饰的代码块或方法时,会自动获取锁,其他线程在此期间无法进入该代码块或方法,从而确保同一时间只有一个线程可以执行该代码块或方法。这样可以防止多个线程同时修改共享资源而导致的数据竞争和不一致。
  • 可见性:synchronized不仅保证了互斥性,还保证了可见性。当一个线程释放锁时,它会将对共享资源的修改刷新到主内存中,使得其他线程在获取锁后能够看到最新的共享资源的值,避免了线程之间的数据不一致性。
  • 内存屏障:synchronized关键字还会在锁的获取和释放过程中插入内存屏障,确保了指令的顺序性和可见性。这些内存屏障可以防止指令重排序导致的线程安全问题。

内存屏障可以通过以下两种方式来禁止指令重排序:
1.写-读屏障:写-读屏障用于确保在内存屏障之前的写操作不会被重排序到内存屏障之后的读操作之后。这样可以确保其他线程在读操作时能够获取到最新的值。写-读屏障可以通过在写操作之后插入一个内存屏障来实现。
2.写-写屏障:写-写屏障用于确保在内存屏障之前的写操作不会被重排序到内存屏障之后的写操作之前。这样可以确保其他线程在读取或写入共享变量时不会看到不一致的中间状态。写-写屏障可以通过在写操作之后插入一个内存屏障来实现。

synchronized用法

  • 修饰实例方法

给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。

synchronized void method() {
    //业务代码
}

  • 修饰静态方法 (锁当前类)

给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。

synchronized static void method() {
    //业务代码
}

静态 synchronized 方法和非静态 synchronized 方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

  • 修饰代码块 (锁指定对象/类)

对括号里指定的对象/类加锁:
synchronized(object) 表示进入同步代码库前要获得 给定对象的锁。
synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁

synchronized(this) {
    //业务代码
}

synchronized 和 volatile 有什么区别?

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

Synchronized 用过吗,其原理是什么?

synchronized 的作用是确保多个线程不会同时访问或修改共享资源,从而防止竞态条件和数据不一致的问题。当一个线程进入一个 synchronized 块或方法时,它会锁定指定的对象或类,其他线程将被阻塞,直到锁定被释放。
简单来说,就好像是一个资源的“独占权证书”。只有持有这个证书的线程才能进入被 synchronized 保护的代码块,其他线程必须等待。

为什么说 Synchronized 是非公平锁?

当锁被释放后,任何一个线程都有机会竞争得到锁,这样做的目的是提高效率,但缺点是可能产生线程饥饿现象。

多线程 Thread.yield 方法到底有什么用

yield 即 “谦让”,也是 Thread 类的方法。它让出当前线程 CPU 的时间片,使正在运行中的线程重新变成就绪状态,并重新竞争 CPU 的调度权。它可能会获取到,也有可能被其他线程获取到。

Reentrantlock

一种可重入的互斥锁实现,类似于synchronized关键字但比其提供更多功能,用于控制线程之间的互斥访问共享资源。

可重入性是指同一个线程在持有锁的情况下,能够再次获取同一个锁而不会发生死锁或其他异常的特性。
1.在可重入锁(如ReentrantLock)或synchronized关键字中,当一个线程已经持有了某个锁后,它可以多次进入由该锁保护的代码块,而不会被阻塞。每次进入临界区时,锁的计数器会加1,退出临界区时计数器会减1。只有当计数器归零时,锁才会被完全释放。
2.这种机制使得可重入锁可以避免线程由于重复获取同一个锁而导致的死锁。

lock 与 synchronized 的区别

ReentrantLock和synchronized都是Java中用于实现线程同步的机制,但它们有以下区别:

  1. 可重入性:ReentrantLock是可重入锁,允许同一个线程多次获取同一个锁而不会产生死锁。而synchronized也是可重入的,同一个线程可以多次进入由synchronized修饰的代码块或方法。

  2. 锁的获取和释放方式:使用ReentrantLock时,需要手动获取锁和释放锁。通过调用lock()方法获取锁,并使用unlock()方法释放锁。而synchronized关键字是隐式的,当线程进入synchronized代码块时自动获取锁,当线程退出代码块时自动释放锁。

  3. 锁的灵活性:ReentrantLock提供了更多的功能和灵活性。例如,ReentrantLock可以选择公平性或非公平性,可以实现可中断的获取锁,可以使用多个条件变量等。而synchronized关键字相对简单,提供的功能相对有限。

  4. 性能:在低竞争情况下,synchronized的性能可能比ReentrantLock更好。因为synchronized是由JVM内部优化的,而ReentrantLock是通过显式方法调用实现的,会有额外的方法调用开销。

总体而言,ReentrantLock提供了更多的功能和灵活性,但使用稍微复杂一些。synchronized则是更简单的选择,适用于大多数简单的同步需求。在性能要求不是非常高且不需要使用ReentrantLock特有的功能时,synchronized是一种更常见和方便的选择。

ReentrantLock 是如何实现可重入性的?

(1)什么是可重入性

一个线程持有锁时,当其他线程尝试获取该锁时,会被阻塞;而这个线程尝试获取自己持有锁时,如果成功说明该锁是可重入的,反之则不可重入。

(2)synchronized是如何实现可重入性

当一个线程第一次进入 synchronized 修饰的代码块时,它会获取锁并将锁的持有者设置为当前线程。此时,其他线程尝试进入相同的 synchronized 代码块会被阻塞。

如果同一个线程再次进入相同的 synchronized 代码块,它会发现自己已经是锁的持有者。这时,它不需要再次获取锁,而是会增加一个计数器(重入次数)来记录进入次数。每次成功进入 synchronized 代码块时,计数器都会递增。每次离开 synchronized 代码块时,计数器递减。

当计数器的值变为零时,表示线程完全释放了锁,其他线程可以竞争获取锁。如果计数器的值不为零,表示锁仍然由当前线程持有,其他线程无法获取

(3)ReentrantLock如何实现可重入性

ReentrantLock 实现可重入性的机制主要基于一个计数器(counter)和线程标识(owner)。

当一个线程第一次获取 ReentrantLock 时,计数器的值会被设置为 1,并且线程标识会被设置为当前获取锁的线程。在此期间,其他线程尝试获取锁时会被阻塞。

如果同一个线程再次获取锁,计数器的值会递增,表示该线程多次获取了锁。每次成功获取锁时,计数器都会递增,每次释放锁时计数器递减。只有当计数器的值变为零时,锁才会完全释放,其他线程才能获取锁。

AQS是什么?

是Java并发框架中的一个抽象基类,用于实现同步器(synchronizer)的基本功能,AQS 的核心思想是基于一个先进先出(FIFO)的等待队列(CLH队列),来管理线程的等待和唤醒。当一个线程需要获取锁时,如果发现锁已经被其他线程占用,则会被加入到等待队列中。当锁的持有者释放锁时,AQS 会从等待队列中选择一个线程唤醒,并将锁分配给该线程。

AQS 还提供了对条件变量(Condition)的支持,允许线程在某个条件满足时等待或唤醒。它通过内部维护一个条件队列,当一个线程调用条件变量的等待方法时,会被加入到对应的条件队列中等待,当条件满足时,AQS 会从条件队列中选择一个线程唤醒。

公平锁和非公平锁有什么区别?

  • 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
  • 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁

可中断锁和不可中断锁有什么区别?

  • 可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。ReentrantLock 就属于是可中断锁。
  • 不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized 就属于是不可中断锁。

Threadlocal

ThreadLocal 是什么?有哪些使用场景?

ThreadLocal 是 Java 中的一个类,用于在多线程环境下实现线程局部变量的机制,从而实现线程间数据隔离。

在使用 ThreadLocal 时,每个线程都可以通过 ThreadLocal 对象来获取和修改自己线程私有的变量副本,而不会影响其他线程的副本。每个线程都拥有自己独立的变量副本,互不干扰。

  1. 线程上下文传递:在多线程环境中,将上下文数据传递给每个线程是一种常见需求。通过使用 ThreadLocal,可以将上下文数据存储在 ThreadLocal 对象中,每个线程可以独立地访问和修改自己的上下文数据

  2. 数据库连接管理:在使用数据库连接池的情况下,为了避免在每个方法中传递数据库连接对象,可以使用 ThreadLocal 存储当前线程的数据库连接,从而实现线程私有的数据库连接管理。

  3. 会话管理:Web 应用中的会话信息通常需要在多个请求之间进行共享。通过使用 ThreadLocal,可以将会话信息存储在 ThreadLocal 中,每个线程可以独立地访问和修改自己的会话信息,避免了线程安全性问题。

ThreadLocal是怎么产生内存泄漏的

ThreadLocal 内存泄漏问题通常是由于线程池或长时间运行的线程未正确地清理 ThreadLocal 对象引用所导致的。

当使用 ThreadLocal 时,如果没有正确地清理 ThreadLocal 对象的引用,那么存储在 ThreadLocal 中的对象将无法被垃圾回收,从而造成内存泄漏。

请谈谈 ThreadLocal 是怎么解决并发安全的?

在java程序中,常用的有两种机制来解决多线程并发问题,一种是sychronized方式,通过锁机制,一个线程执行时,让另一个线程等待,是以时间换空间的方式来让多线程串行执行。而另外一种方式就是ThreadLocal方式,通过创建线程局部变量,以空间换时间的方式来让多线程并行执行。两种方式各有优劣,适用于不同的场景,要根据不同的业务场景来进行选择。

在spring的源码中,就使用了ThreadLocal来管理连接,在很多开源项目中,都经常使用ThreadLocal来控制多线程并发问题,因为它足够的简单,我们不需要关心是否有线程安全问题,因为变量是每个线程所特有的。

很多人都说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么?

  • 内存泄漏问题:ThreadLocal 使用不当可能导致内存泄漏。如果在使用完 ThreadLocal 后没有及时清理对象引用,那么存储在ThreadLocal 中的对象将无法被垃圾回收,造成内存泄漏。因此,在使用完 ThreadLocal 后应该显式地调用 remove()方法清除对象引用。
  • 并发性和线程安全性:ThreadLocal 对象本身是线程安全的,每个线程都有自己的副本,但存储在 ThreadLocal中的对象可能不是线程安全的。如果多个线程共享同一个 ThreadLocal对象,并在其中存储和获取可变状态的对象,就需要额外的同步机制来确保线程安全性。
  • 内存占用:每个线程在使用 ThreadLocal时都会创建一个副本,这可能会增加内存消耗。在使用大量线程或长时间运行的线程的情况下,需要考虑线程数和内存占用之间的平衡。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值