1、线程有哪些状态?
在Java中,线程有以下几种状态:
- 新建(New):当我们创建一个线程实例但尚未调用其start()方法时,线程处于新建状态。此时线程并不会执行任何代码。
- 运行(Runnable):当调用线程实例的start()方法后,线程进入运行状态。在运行状态中,线程可以被CPU调度执行任务。
- 阻塞(Blocked):当线程在等待某个特定条件(如获取锁或等待I/O操作完成)时,它可能会进入阻塞状态。在这个状态下,线程暂时停止执行,并且不消耗CPU资源。
- 等待(Waiting):线程进入等待状态是因为调用了wait()方法、join()方法或 LockSupport.park()方法。在等待状态下,线程会一直等待直到其他线程唤醒它。
- 超时等待(Timed Waiting):线程进入超时等待状态是因为调用了Thread.sleep()方法、Object.wait()方法的指定时间版本,或者使用LockSupport.parkNanos()、LockSupport.parkUntil()方法。在指定的时间内,线程将会处于超时等待状态。
- 终止(Terminated):线程执行完其run()方法的代码或者出现异常而终止后,进入终止状态。已终止的线程不会再被调度执行。
2、sleep() 方法和 wait() 方法区别?
sleep()方法和wait()方法是用于线程控制的两个不同的方法,它们之间有以下几个区别:
- 调用位置:sleep()方法是Thread类的静态方法,可以在任何地方直接调用;而wait()方法是Object类的方法,只能在同步代码块或同步方法中调用。
- 使用对象:sleep()方法不会释放锁,它仅仅是让当前线程休眠指定的时间,然后继续执行。而wait()方法会释放对象的锁,并使当前线程进入等待状态,直到其他线程调用相同对象的notify()或notifyAll()方法来唤醒等待的线程。
- 唤醒方式:通过调用notify()方法可以随机唤醒处于等待状态的一个线程(如果有多个等待线程,则按照线程优先级来选择);而notifyAll()方法则会唤醒所有等待的线程。对于sleep()方法,线程会在指定的时间后自动苏醒。
- 对象锁:当线程调用wait()方法时,它会释放持有的对象锁,从而允许其他线程进入同步块或同步方法。而sleep()方法并不会释放对象锁。
- 唤醒条件:线程调用wait()方法进入等待状态后,只有其他线程调用相同对象的notify()或notifyAll()方法才能唤醒等待的线程。而调用sleep()方法的线程会在指定时间内自动苏醒,不需要其他线程的干预。
综上所述,sleep()方法主要用于控制线程的休眠时间,不涉及同步和锁的问题;而wait()方法则用于线程之间的协作,当一个线程需要等待某个条件满足时,可以调用wait()方法使其进入等待状态,并释放对象锁,以便其他线程可以执行相关操作。
3、偏向锁是什么?轻量级锁是什么?
- 偏向锁(Biased Locking):偏向锁是为了解决无竞争情况下的同步操作性能问题而引入的锁优化机制。当一个线程访问一个同步块时,如果该同步块没有被其他线程访问过,那么该线程会自动获取对象的偏向锁,并标记为已偏向。这样,在后续的操作中,该线程再次进入同步块时就不需要再进行加锁的操作,因为它已经持有了对象的偏向锁。偏向锁可以减少无竞争情况下的同步操作,提升程序性能。然而,如果有其他线程尝试竞争同一个锁,则偏向锁会被撤销,线程会转而使用轻量级锁或重量级锁。
- 轻量级锁(Lightweight Locking):轻量级锁是针对低竞争情况下的锁优化技术。当一个线程访问一个同步块时,如果该同步块没有被其他线程占用,那么该线程会尝试使用CAS(Compare and Swap)操作将对象头部的标志位设置为 “锁定” 状态,来获取锁。这个过程是非阻塞的,所以称之为轻量级锁。如果CAS操作成功,表示该线程成功获取了锁,并且可以直接执行同步操作。如果CAS操作失败,则表示其他线程已经占用了锁,当前线程需要进行自旋等待,直到获取到锁或者自旋次数达到一定阈值后,转为使用重量级锁进行锁的升级。
总结起来,偏向锁和轻量级锁都是为了优化多线程同步的性能。偏向锁适用于无竞争的情况,可以减少同步操作的开销;轻量级锁适用于低竞争的情况,可以减少线程间的竞争和上下文切换。而在竞争激烈的情况下,这两种锁机制可能会退化为重量级锁,以保证线程安全。
4、讲一讲synchronized锁升级过程?
synchronized 锁的升级过程如下:
- 偏向锁(Biased Locking)阶段:当一个线程访问一个同步块时,如果该同步块没有被其他线程竞争,那么该线程会自动获取对象的偏向锁,并标记为已偏向。这个过程是非常快速的,不涉及任何同步操作。在偏向锁状态下,线程可以重入同步块而无需重新获取锁。
- 轻量级锁(Lightweight Locking)阶段:如果有另一个线程尝试竞争同一个锁,则偏向锁会被撤销,线程会尝试使用CAS(Compare and Swap)操作将对象头部的标志位设置为 “轻量级锁” 状态来获取锁。这个过程是非阻塞的,仍然尝试保持低开销。如果CAS操作成功,即当前线程成功获取了锁,可以直接执行同步操作。如果CAS操作失败,表示有其他线程已经占用了锁,则当前线程需要进行自旋等待,直到获取到锁或自旋次数达到一定阈值。
- 重量级锁(Heavyweight Locking)阶段:当自旋等待次数超过一定阈值时,轻量级锁会升级为重量级锁。此时,当前线程会进入阻塞状态,锁会被转化为互斥量(Mutex)实现,即通过操作系统提供的互斥机制来实现同步。
需要注意的是,锁的升级过程是逐级升级的,即从偏向锁到轻量级锁,再到重量级锁。而且,锁的降级是不可行的,一旦锁升级为重量级锁,就无法再降级为轻量级锁或偏向锁。
这个升级过程是为了在不同竞争情况下提供更高效的同步操作,根据实际的场景和并发情况来选择合适的锁机制,以优化程序的性能。
5、CAS了解多少?
CAS(Compare And Swap,比较并交换)是一种原子操作,常用于并发编程中实现线程安全的同步操作。CAS 操作包含三个参数:内存地址 V、旧的预期值 A 和新的值 B。
CAS 的工作原理如下:
- 首先,它会读取内存地址 V 的当前值。
- 接着,它会将读取到的值与预期值 A 进行比较。如果相等,则说明现在的内存值与预期值一致,可以执行接下来的操作。
- 如果相等,CAS 会原子地将新的值 B 写入内存地址 V,也就是把旧的内存值替换为新值。
- 如果不相等,说明其他线程已经修改了内存地址 V 的值,当前 CAS 操作失败,需要重新尝试。
CAS 操作是基于硬件指令的支持,在底层实现了原子性的读-改-写操作。由于 CAS 是无锁操作,避免了传统锁机制中的竞争与上下文切换,因此具有较高的性能和可伸缩性。
CAS 在并发编程中广泛应用于无锁算法、乐观锁机制和并发数据结构的实现,例如 Java 中的 Atomic 原子类就是基于 CAS 实现的。然而,使用 CAS 时需要考虑内存一致性的问题,以及 CAS 操作可能导致的ABA问题(即值在操作过程中经历变化、又回到原值的情况),需要采取相应的处理措施来解决这些问题。
6、CAS底层实现原理?
- 首先,它会读取内存地址 V 的当前值,并将其与预期值 A 进行比较。
- 如果比较结果相等,说明当前内存值与预期值一致,此时它会使用原子指令将新的值 B 写入内存地址 V,并返回操作成功。
- 如果比较结果不相等,说明其他线程已经修改了内存地址 V 的值,CAS 操作失败,需要重新尝试。
7、AQS了解多少?
AQS(AbstractQueuedSynchronizer)是 Java 并发包中的一个抽象基类,用于构建同步器。它提供了一种框架,使开发者可以自定义各种并发组件,如锁、信号量、栅栏等。
AQS 的核心思想是使用一个基于先进先出原则的等待队列来管理线程的竞争和阻塞状态。AQS 内部维护了一个表示状态的变量,并通过对状态的操作来实现线程的排队和唤醒。通过继承 AQS 类并实现其中的方法,开发者可以定制化自己的同步器。
AQS 提供了一些基本的操作方法,如获取锁(acquire)和释放锁(release),同时还提供了一些辅助方法供子类使用。开发者可以通过继承 AQS 并重写这些方法来实现各种同步器,满足特定的需求。
在 Java 并发包中,很多重要的同步组件都是基于 AQS 实现的,比如 ReentrantLock、CountDownLatch、Semaphore 等。AQS 的设计提供了强大而灵活的支持,使得开发者能够更容易地构建高效且可靠的并发组件。
总之,AQS 是 Java 中用于构建同步器的抽象基类。通过继承 AQS 并实现其中的方法,开发者可以创建自定义的同步组件,实现并发控制。AQS 提供了强大的基础框架,使得并发编程变得更加简单和灵活。
8、ReentrantLock公平锁实现原理?
ReentrantLock(可重入锁)是Java中提供的一种同步机制,它有两种模式:非公平模式和公平模式。在这里,我将重点解释公平锁的实现原理。
公平锁的实现原理基于AbstractQueuedSynchronizer(AQS)的队列机制。当多个线程争夺锁时,公平锁会按照线程的先后顺序来获取锁,即先到先得的原则。
具体实现原理如下:
- 当一个线程请求锁时,如果锁当前没有被其他线程持有,则该线程可以立即获得锁,并将锁的占有数加一。
- 如果锁已经被其他线程持有,或者有其他线程在等待队列中排队,则当前线程会被加入到等待队列中,并处于阻塞状态。
- 当锁被释放时,AQS 会按照队列的顺序唤醒等待队列中的第一个线程,使其获得锁。被唤醒的线程会再次尝试获取锁,只有当它位于等待队列头部并且成功获取锁时,才算真正获得了锁。
总结来说,ReentrantLock 公平锁的实现原理是基于 AbstractQueuedSynchronizer 的等待队列机制。它按照线程的先后顺序来获取锁,并在释放锁时按照队列的顺序唤醒等待线程,从而实现了公平性。
9、ReentrantLock非公平锁实现原理?
- 当一个线程请求锁时,如果锁当前没有被其他线程持有,则该线程可以立即获得锁,并将锁的占有数加一。
- 如果锁已经被其他线程持有,当前线程会尝试使用 CAS(Compare and Swap)操作来尝试获取锁。
- 如果失败,说明锁被其他线程持有,当前线程会将自己插入到等待队列的尾部,并进入阻塞状态。
- 当锁被释放时,AQS 会唤醒等待队列中的头部线程,使其能够再次尝试获取锁。
- 被唤醒的线程再次尝试获取锁,只有当它成功获取锁时,才算真正获得了锁。如果获取失败,被唤醒的线程会继续留在等待队列中,等待下一次重新竞争锁。
总结来说,ReentrantLock 的非公平模式允许线程在没有遵循先来先服务原则的情况下获取锁,并且是通过使用等待队列和 CAS 操作来实现的。这种模式可以提高性能,可以提高整体的吞吐量,但不保证公平性,可能会导致某些线程饥饿的情况。
10、线程池有哪些核心参数?
线程池的核心参数包括以下几个:
- 核心线程数(Core Pool Size):线程池中同时运行的核心线程数。当有任务提交给线程池时,如果核心线程数尚未达到上限,线程池会创建新的核心线程来处理任务。
- 最大线程数(Maximum Pool Size):线程池中允许存在的最大线程数。当核心线程数已满且工作队列也已满时,线程池会创建新的非核心线程来处理任务,但不会超过最大线程数。
- 线程存活时间(Keep Alive Time):当线程池中的线程数量超过核心线程数时,空闲线程的存活时间。如果一个线程在空闲时间超过设定的存活时间后仍未被使用,那么它将被终止并从线程池中移除。
- 工作队列(Work Queue):用于保存等待执行的任务的队列。当线程池中的线程已满时,新的任务会被放入工作队列等待执行。
- 拒绝策略(Rejected Execution Handler):当线程池和工作队列已经达到最大容量,并且无法接受新的任务时,拒绝策略决定如何处理这些被拒绝的任务。
除了这些核心参数外,线程池的实现还可能包含其他非必需的参数,如线程名称前缀、任务超时时间等,它们可以根据具体的使用场景进行配置。
11、讲讲线程池的工作方式?
线程池的工作方式如下所述:
- 初始化线程池:线程池在创建时会初始化一定数量的核心线程,并将它们置于等待就绪状态。
- 提交任务:当有任务被提交给线程池时,线程池会按照一定的策略进行任务处理。通常情况下,线程池会先尝试使用空闲的核心线程来执行任务,如果所有核心线程都在执行任务,则将任务放入工作队列中。
- 任务处理:空闲的核心线程或者从工作队列中获取到任务的线程会执行任务。线程执行完一个任务后,会从工作队列中取下一个任务继续执行。
- 动态调整线程数量:如果工作队列中的任务数量持续增加,线程池可以根据设定的最大线程数动态地创建新的线程,以加快任务处理速度。相反,如果任务数量减少,线程池会逐渐回收多余的线程,以节省资源消耗。
- 异常处理:线程池需要对任务执行过程中可能出现的异常进行处理。一般情况下,线程池会捕获任务执行时抛出的异常,并将其记录下来,以便后续分析和处理。
- 关闭线程池:当任务处理完成或者不再需要线程池时,需要及时关闭线程池。关闭线程池的操作包括两个步骤:首先,停止接收新的任务,并等待已提交的任务执行完毕;然后,终止所有线程,释放资源。
通过使用线程池,可以提高系统的并发性能,避免反复创建和销毁线程的开销,并有效地管理线程资源。
12、如果线程到达 maximumPoolSize 仍然有新任务来临,并且该任务的优先级比较高,不允许直接丢弃,希望该任务立即执行,如何处理?
如果线程池的线程数已达到最大值并且有高优先级的任务到达时,可以使用CallerRunsPolicy
策略来处理。该策略会将任务交给提交任务的线程自己来执行,而不会在新建线程。
以下是一个示例代码,展示了如何使用CallerRunsPolicy
策略(让调用者运行任务):
1 | import java.util.concurrent.*; |
在上述示例中,我们创建了一个 ThreadPoolExecutor
对象,并设置了核心线程数为2,最大线程数为4,工作队列容量为10。然后,我们使用 setRejectedExecutionHandler()
方法设置了拒绝策略为 CallerRunsPolicy
。
当线程池的线程数达到最大值并且工作队列已满时,如果有新的任务到达,该任务将由提交任务的线程自己来执行,而不会创建新的线程。这样可以确保新任务能够得到立即执行,但也需要注意潜在的性能影响。
请注意,使用CallerRunsPolicy
策略时,任务的执行会发生在提交任务的线程上,而不是在线程池的线程上。
补充,线程池的拒绝策略,JDK提供了四种拒绝策略
AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
CallerRunsPolicy 让调用者运行任务
DiscardPolicy 放弃本次任务
DiscardOldestPolicy 放弃队列中最旧的任务,本任务取而代之,以便立即执行新到达的高优先级任务
1 | /* Predefined RejectedExecutionHandlers */ |