Java并发常见面试题总结(下)

1、ThreadLocal 有什么用?

在Java中,ThreadLocal是一个用于创建线程本地变量的类。ThreadLocal可以让你创建一个只有在当前线程中才能访问的变量,这个变量对其他线程而言是不可见的。

ThreadLocal有以下几个用途:

  1. 线程封闭性:ThreadLocal可以实现线程封闭性,即一个变量只能由某个线程独享,而其他线程无法访问。这对于多线程编程非常重要,因为它避免了线程之间的数据竞争和线程安全问题。

  2. 上下文信息传递:在一些情况下,需要在同一个线程中传递一些上下文信息,比如用户信息、语言环境、数据库连接等。ThreadLocal可以方便地实现这个功能,每个线程都可以存储自己的上下文信息,而不会影响其他线程。

  3. 避免参数传递:有些方法需要传递一些参数,这些参数在整个方法执行过程中都需要使用,这样会让代码变得冗长而难以维护。使用ThreadLocal可以避免参数传递,方法中可以直接访问ThreadLocal变量,而不需要将参数传递给方法。

2、如何使用 ThreadLocal?


//创建一个ThreadLocal对象。可以通过ThreadLocal类的构造方法
//来创建ThreadLocal对象,
ThreadLocal<String> threadLocal = new ThreadLocal<>();
//将值存储到ThreadLocal对象中。可以通过调用ThreadLocal对象的set()方法
threadLocal.set("Hello, world!");
//获取值
String value = threadLocal.get();
//删除
threadLocal.remove();

3、ThreadLocal原理?

ThreadLocal原理其实很简单,它是通过为每个线程创建独立的副本来实现线程隔离的。

具体来说,ThreadLocal对象中保存的变量副本是被存储在每个线程的Thread对象中的一个Map中,即每个Thread都会持有一个ThreadLocalMap,这个Map的key就是ThreadLocal对象,value就是线程的变量副本。这样,在多线程环境下,每个线程都拥有自己的变量副本,彼此之间互不干扰。

在ThreadLocal对象中,通过get()方法获取变量副本时,实际上是通过Thread.currentThread()方法获取当前线程对象,然后从该线程对象中获取对应的变量副本。如果当前线程没有在ThreadLocal对象中设置过变量副本,ThreadLocal会调用initialValue()方法创建一个初始值,并将其作为当前线程的变量副本返回。

而在ThreadLocal对象中,通过set()方法设置变量副本时,实际上是通过Thread.currentThread()方法获取当前线程对象,然后将变量副本存储到该线程对象的ThreadLocalMap中,key为ThreadLocal对象,value为变量副本。

需要注意的是,由于ThreadLocalMap是存储在线程对象中的,而线程对象是由JVM管理的,因此在使用ThreadLocal时需要注意避免内存泄漏的问题。具体来说,如果不再需要使用ThreadLocal变量,需要手动调用remove()方法将变量副本从ThreadLocalMap中移除,避免线程对象被长时间持有,导致内存泄漏。

4、ThreadLocal 内存泄露问题是怎么导致的?

ThreadLocal内存泄漏问题是由于ThreadLocalMap中的Entry没有被及时清理导致的。

具体来说,当一个线程结束时,它持有的ThreadLocalMap中的Entry并不会自动被清理,因为线程结束后Thread对象不再被使用,JVM会将其回收。但是,ThreadLocalMap中的Entry却不会被回收,如果此时ThreadLocalMap的key(即ThreadLocal对象)是强引用,那么这个ThreadLocal对象将不能被回收,从而导致内存泄漏。

这个问题的解决方法是在使用ThreadLocal的时候,手动调用remove()方法清除当前线程中ThreadLocalMap中所有的Entry。如果没有手动清除,就有可能出现内存泄漏问题。

同时,如果使用ThreadLocal的场景比较复杂,可以使用WeakReference或者SoftReference来解决内存泄漏问题。WeakReference或者SoftReference可以让ThreadLocal对象的引用变得更加弱化,使得ThreadLocal对象在没有强引用指向它的情况下也可以被回收。但是,需要注意的是,这样做可能会降低ThreadLocal的性能,因为每次访问ThreadLocal对象都需要重新创建。因此,在使用WeakReference或者SoftReference时需要权衡性能和内存泄漏问题。

5、说说线程池?

线程池是一种多线程处理方式,它可以维护一定数量的线程,当有任务需要执行时,就会从线程池中取出一个线程来处理任务,任务执行完毕后,线程并不会立即被销毁,而是返回线程池中等待下一个任务的到来。通过线程池,我们可以有效地控制线程的数量,防止线程数量过多导致系统资源被耗尽,从而提高了系统的性能和稳定性。

线程池通常由以下几个部分组成:

  1. 线程池管理器(ThreadPool Manager):负责创建和管理线程池,包括初始化线程池、销毁线程池、添加任务、移除任务等操作。

  2. 工作线程(Worker Thread):线程池中执行任务的线程。

  3. 任务队列(Task Queue):用于存放需要执行的任务。

线程池的优点:

  1. 节省系统资源:线程池可以重用已经创建好的线程,避免了线程的创建和销毁带来的开销,从而节省了系统资源。

  2. 提高系统响应速度:通过线程池,任务可以在已经创建好的线程中执行,避免了线程的创建和销毁所带来的延迟,从而提高了系统的响应速度。

  3. 提高系统稳定性:线程池可以控制线程的数量,防止线程数量过多导致系统资源被耗尽,从而提高了系统的稳定性。

  4. 方便管理:通过线程池管理器,可以方便地管理线程池,包括初始化线程池、销毁线程池、添加任务、移除任务等操作。

需要注意的是,如果线程池中的任务过多,会导致线程池的响应速度变慢,从而影响系统的性能。因此,在使用线程池时,需要根据具体的应用场景来设置线程池的大小

6、为什么不推荐使用内置线程池?

虽然内置线程池(例如Java中的ThreadPoolExecutor)具有线程池的优点,但是在某些情况下不推荐使用内置线程池,主要有以下几个原因:

  1. 难以控制线程池的大小:内置线程池的大小通常是固定的,如果线程池中的任务过多,就会导致线程池的响应速度变慢,从而影响系统的性能。而如果线程池的大小过大,就会占用过多的系统资源,从而导致系统崩溃。

  2. 不够灵活:内置线程池通常只提供了一些简单的参数配置,如线程池的大小、任务队列的大小等,而在某些特定的场景下,可能需要更加灵活的线程池配置。

  3. 可能会出现任务阻塞的情况:如果任务队列中的任务过多,就有可能导致任务阻塞,从而影响系统的性能。

  4. 任务可能被拒绝:如果线程池中的线程数量已经达到上限,而任务队列也已经满了,那么新提交的任务就有可能被拒绝。

因此,在某些特定的场景下,不推荐使用内置线程池,而应该根据具体的应用场景来创建自定义的线程池,从而更好地满足应用的需求。自定义线程池可以根据需要来控制线程池的大小、任务队列的大小、线程池的创建方式、任务拒绝策略等,从而提高系统的性能和稳定性。

7、线程池的饱和策略有哪些?

线程池的饱和策略是在线程池中的任务队列已满,而线程池中的线程已经达到最大数量时,新提交的任务会被饱和策略所处理的策略。常见的饱和策略有以下几种:

  1. AbortPolicy:该策略会直接抛出RejectedExecutionException异常,阻止系统继续接收新的任务。

  2. CallerRunsPolicy:该策略会让提交任务的线程自己执行任务,即使提交任务的线程已经达到了最大数量。

  3. DiscardOldestPolicy:该策略会丢弃队列中最老的任务,然后尝试再次提交任务。

  4. DiscardPolicy:该策略会默默地丢弃新的任务,不做任何处理。

除了上述常见的饱和策略,也可以通过自定义饱和策略来实现更加灵活的线程池处理方式。自定义饱和策略可以根据具体的业务需求来实现,例如将新提交的任务存储在外部存储中,等待有空闲线程时再从外部存储中读取任务进行处理等。

8、线程池常用的阻塞队列有哪些?

线程池的任务队列是用来存储等待处理的任务的,一般有两种类型的队列:有界队列和无界队列。其中有界队列是指队列大小是固定的,而无界队列则是指队列大小没有限制。常用的阻塞队列有以下几种:

  1. ArrayBlockingQueue:是一个有界队列,其内部是通过一个定长数组来实现的,可以保证先进先出的顺序,可以在创建时指定队列大小。

  2. LinkedBlockingQueue:是一个无界队列,其内部是通过一个链表来实现的,可以保证先进先出的顺序。

  3. SynchronousQueue:是一个没有容量的队列,每个插入操作必须等待另一个线程的移除操作,反之亦然,可以保证任务的直接交换,适用于高并发的情况。

  4. PriorityBlockingQueue:是一个无界的优先级队列,可以保证元素按照优先级的顺序被处理,其中可以指定比较器来控制优先级。

根据应用场景的不同,选择不同的队列类型可以更好地满足应用的需求。有界队列可以防止系统资源的耗尽,但可能会出现任务阻塞的情况;无界队列可以保证任务不会被拒绝,但可能会占用过多的系统资源。SynchronousQueue适用于高并发的情况,PriorityBlockingQueue则适用于需要按照优先级顺序处理任务的情况。

9、线程池处理任务的流程了解吗?

  1. 当有新的任务到达时,线程池会先判断当前线程池中的线程数是否已达到最大线程数,如果没有达到最大线程数,线程池会创建一个新的线程来处理这个任务,否则线程池会将任务提交到阻塞队列中等待处理。

  2. 当线程池中的线程处理完一个任务时,它会尝试从阻塞队列中取出一个任务进行处理。

  3. 如果阻塞队列中没有任务,线程池会进入等待状态,直到有新的任务到达或者线程池被关闭。

  4. 如果阻塞队列中有任务,线程池会从队列中取出一个任务,然后将其分配给一个空闲的线程进行处理。

  5. 当线程池被关闭时,它会停止接收新的任务,并等待线程池中的所有任务都被处理完毕,然后释放线程池中的资源。

需要注意的是,线程池在处理任务时,会根据任务队列的类型和饱和策略来决定如何处理任务。例如,当任务队列是有界队列时,如果队列已满,线程池会根据饱和策略来决定如何处理新的任务,例如直接抛出异常、丢弃任务等。因此,在使用线程池时,需要根据具体的业务需求来选择合适的任务队列类型和饱和策略。

10、说说AQS?

AQS是AbstractQueuedSynchronizer的缩写,是Java中用于实现同步机制的基础类。它提供了一种灵活的同步机制,可以被用于实现许多并发数据结构和算法,例如ReentrantLock和Semaphore等。

AQS的核心思想是使用一个FIFO队列来管理等待锁的线程,以及一个状态变量来表示当前锁的状态。当线程请求获取锁时,AQS会首先尝试使用CAS操作来修改状态变量,如果修改成功则表示获取锁成功,否则就需要将线程加入到等待队列中。当持有锁的线程释放锁时,AQS会将队列中的第一个等待线程唤醒并尝试让其获取锁。

AQS可以被子类化,从而实现不同的同步机制。子类需要实现两个方法:tryAcquire和tryRelease,这两个方法分别用于实现获取和释放锁的逻辑。另外,子类还可以重写其他方法,例如tryAcquireShared和tryReleaseShared,从而支持共享锁。

AQS的优点是它提供了一个通用的框架,可以被用于实现不同类型的同步机制。此外,AQS还支持条件变量,这使得在锁保护的临界区中等待某些条件成立的线程可以更加灵活和高效。不过,AQS的缺点是它对于子类的实现要求比较高,需要仔细考虑线程安全和可靠性等问题。

11、AQS 的原理是什么?

AQS(AbstractQueuedSynchronizer)的原理是使用一个双向队列(一般采用FIFO的方式)来存储需要获取同步状态的线程,同时使用一个整型变量表示当前同步状态。AQS允许线程在获取同步状态时被阻塞,并在同步状态被释放时通知被阻塞的线程。

AQS的核心是一个名为"state"的整型变量,表示当前同步状态。通过该变量,AQS可以支持独占锁和共享锁两种同步机制。在独占锁的情况下,state通常被设置为0或1,表示锁未被占用或已被占用;在共享锁的情况下,state通常被设置为一个大于等于0的数值,表示锁被占用的次数。

AQS同时维护了一个双向队列,用于存储等待获取同步状态的线程。当线程请求获取同步状态时,AQS会根据实现的逻辑将该线程加入到队列的尾部,同时尝试去获取同步状态。如果同步状态已被占用,则线程会被阻塞在队列的某个位置,并且进入等待状态,直到被唤醒。

当持有同步状态的线程释放同步状态时,AQS会将队列中的第一个线程唤醒,并尝试让其获取同步状态。如果该线程成功获取了同步状态,则AQS会将其从队列中移除,并将其从等待状态唤醒;否则,该线程将仍然保持等待状态,直到下次被唤醒并尝试获取同步状态。

总的来说,AQS的原理是通过使用一个状态变量和一个等待队列来实现同步机制。它提供了一个通用的框架,可以被用于实现不同类型的同步机制,例如独占锁、共享锁、读写锁等。

12、Semaphore 有什么用?

Semaphore是一种经典的同步工具,它可以用于限制并发访问资源的数量,从而避免线程间的竞争和冲突。Semaphore通过维护一个许可证(permit)的计数器来实现这种限制。在使用Semaphore时,线程需要先获取许可证,然后才能访问被Semaphore保护的资源,访问完成后需要释放许可证,以便其他线程可以获取它。

Semaphore的主要用途包括以下几个方面:

  1. 控制并发访问:Semaphore可以控制同时访问某个资源的线程数量。例如,当需要限制同时访问某个文件或数据库连接池的线程数量时,就可以使用Semaphore来实现限制。

  2. 实现线程间协作:Semaphore可以被用于实现线程间的协作。例如,在一些生产者-消费者场景中,可以使用Semaphore来限制缓冲区中元素的数量,当缓冲区满时生产者线程需要等待,直到消费者线程取走元素释放了许可证;当缓冲区为空时,消费者线程需要等待,直到生产者线程添加了元素释放了许可证。

  3. 控制流量:Semaphore还可以被用于控制流量。例如,在某些需要限制网络请求或数据传输速率的场景中,可以使用Semaphore来限制同时进行的请求或传输数量。

总的来说,Semaphore是一种非常有用的同步工具,它可以帮助我们解决许多并发访问和线程间协作的问题。

13、Semaphore 的原理是什么?

Semaphore的原理是基于一个计数器和一个等待队列实现的。Semaphore可以看做是一个资源的计数器,用来保护共享资源的访问。当一个线程需要访问被Semaphore保护的共享资源时,它必须先尝试获取Semaphore的许可证(permit)。如果许可证的数量大于0,表示可以获取许可证,线程就可以继续访问共享资源,并将许可证的数量减1;如果许可证的数量等于0,表示没有许可证可用,线程就必须等待,直到有其他线程释放许可证。当线程完成对共享资源的访问后,它必须将许可证归还Semaphore,以便其他线程可以获取许可证。Semaphore的实现可以是公平或非公平的,即它可以按照先来先服务的顺序或者按照优先级的顺序分配许可证。

14、CountDownLatch 的原理是什么?

CountDownLatch的原理是基于一个计数器和一个等待队列实现的。在使用CountDownLatch时,可以通过调用CountDownLatch的构造函数来设置计数器的初始值,然后在需要等待的线程中调用CountDownLatch的await()方法来等待,直到计数器的值变为0。同时,在执行需要等待的任务的线程中,可以通过调用CountDownLatch的countDown()方法来减少计数器的值。当计数器的值变为0时,表示所有需要等待的任务都已经完成,等待线程可以继续执行后续操作。

举个例子,假设有一个主线程需要等待两个子线程都完成后才能继续执行。可以使用CountDownLatch来实现。在主线程中,首先创建一个CountDownLatch对象并将计数器初始化为2。然后,分别创建两个子线程,并在子线程中执行需要等待的任务。在任务执行完成后,子线程调用CountDownLatch的countDown()方法来减少计数器的值。最后,在主线程中调用CountDownLatch的await()方法来等待,直到计数器的值变为0,表示两个子线程都已经完成了任务。这样,主线程就可以继续执行后续操作了。

15、什么场景下用CountDownLatch 的?

CountDownLatch适用于需要等待多个线程或操作完成后再继续执行的场景。例如:

  1. 并发测试场景:可以使用CountDownLatch来控制多个线程同时开始执行某个测试用例,等待所有线程都完成后再进行结果验证。

  2. 复杂的任务调度场景:可以使用CountDownLatch来等待多个任务完成后再执行下一步操作,例如,在某些数据处理流程中,需要等待多个数据源都准备好后才能开始执行数据处理操作。

  3. 多线程协同操作场景:可以使用CountDownLatch来协调多个线程之间的操作,例如,在某些多线程并发处理场景中,需要等待多个线程都完成某个任务后才能继续执行下一步操作。

总之,当需要等待多个线程或操作都完成后才能继续执行时,可以使用CountDownLatch来实现线程同步和控制。通过CountDownLatch,可以有效地控制多个线程之间的执行顺序,保证线程之间的协同操作能够顺利完成,提高程序的并发性能和可靠性。

16、说说LockSupport 工具?

LockSupport是Java中提供的一个工具类,它用于创建基于线程的同步工具。LockSupport主要提供了三个方法:park()、unpark(Thread thread)和parkNanos(long nanos)。

park()方法用于让当前线程进入等待状态,直到被其他线程唤醒。如果当前线程已经被唤醒,则该方法会立即返回。park()方法可以用来实现线程的阻塞等待。

unpark(Thread thread)方法用于唤醒指定线程,使其从等待状态中恢复执行。如果指定线程已经被唤醒,则该方法不会产生任何影响。unpark()方法可以用来实现线程的唤醒操作。

parkNanos(long nanos)方法与park()方法类似,不同的是该方法会等待指定的时间,如果在等待期间没有被唤醒,则会自动返回。parkNanos()方法可以用来实现带有超时时间的等待操作。

LockSupport的使用通常需要和其他同步工具(如Semaphore、CountDownLatch等)结合使用,以实现更为复杂的同步和控制操作。LockSupport还可以用来实现线程的中断、唤醒等操作,具有比较灵活的应用场景。

需要注意的是,使用LockSupport需要非常小心,因为它可以使线程进入等待状态,而无法被中断或中止,因此在使用时应该遵循一定的规则和约定,以避免潜在的死锁或线程挂起问题。

公众号:大雄说技术 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值