目录
sleep()、wait()、join()、yield()之间的的区别
线程池中阻塞队列的作⽤?为什么是先添加列队⽽不是先创建最⼤线程?
ReentrantLock中tryLock()和lock()⽅法的区别
CountDownLatch和Semaphore的区别和底层原理
线程的⽣命周期?线程有⼏种状态
新建,就绪,运行,阻塞,死亡
sleep()、wait()、join()、yield()之间的的区别
- sleep() 方法用于暂停当前线程的执行,不释放锁;
- wait() 方法用于使线程进入等待状态,释放对象锁;
- join() 方法用于等待目标线程执行完毕后再继续执行;
- yield() 方法用于提示调度器将CPU资源让给其他线程。
对线程安全的理解
线程安全就是内存安全,多个线程访问同一个对象时,每一个线程都能获取对的结果就是线程安全
Thread和Runable的区别
继承关系:Thread是一个类,它直接继承自java.lang.Thread类;而Runnable是一个接口,它需要通过实现Runnable接口来创建线程。
单继承 vs 多实现:由于Java中的单继承性质,如果使用Thread类创建线程,则该类就不能再继承其他类;而通过实现Runnable接口创建线程,则可以同时继承其他类或实现其他接口,具有更大的灵活性。
代码复用性:使用Runnable接口创建线程可以提供更好的代码复用性。多个线程可以共享同一个Runnable对象,从而共享数据和资源。而通过继承Thread类创建线程时,每个线程都需要独立拥有一个Thread实例,无法直接共享数据。
对象关系:使用Thread类创建的线程是Thread类的实例对象,可以通过this关键字访问线程的相关方法和属性;而通过实现Runnable接口创建的线程,需要将该Runnable对象作为参数传递给Thread类的构造方法,使得Runnable对象与Thread对象产生了隐式关联。
任务分离:Runnable接口更适合描述一个任务,将任务逻辑和线程分离开来,使得代码结构更加清晰和可读。由于Runnable是一个独立的对象,可以被多个线程共享,实现了任务和线程的解耦。
总结:
- 继承Thread类创建线程比较简单,但可扩展性较差,无法继承其他类或实现其他接口。
- 实现Runnable接口创建线程需要将Runnable对象作为参数传递给Thread类的构造方法,具有更好的代码复用性和扩展性,能够实现任务和线程的分离。
对守护线程的理解
守护线程是一种特殊类型的线程,它的存在为非守护线程提供服务和支持。守护线程的生命周期依赖于非守护线程,当所有非守护线程结束时,守护线程会自动终止。守护线程通常用于后台服务或支持功能,不应该用于关键任务或需要精确控制的业务逻辑。
ThreadLocal的底层原理
ThreadLocal本质就是一个map,可以用来缓存线程对象,key就是ThreadLocal对象,value是需要缓存的值
并发、并⾏、串⾏之间的区别
1. 串⾏在时间上不可能发⽣重叠,前⼀个任务没搞定,下⼀个任务就只能等着2. 并⾏在时间上是重叠的,两个任务在同⼀时刻互不⼲扰的同时执⾏。3. 并发允许两个任务彼此⼲扰。统⼀时间点、只有⼀个任务运⾏,交替执⾏
并发的三⼤特性
原子性
可见性
有序性
Java死锁如何避免?
首先我们需要知道怎么引发的死锁,无非就是以下几个原因:
1.线程在阻塞时不释放资源
2.资源循环依赖
如何解决呢:
注意加锁顺序
设置超时时间
如何理解volatile关键字
被volatlile修饰的共享变量总是可见的。并且保证一致性
volatile和synchronized和lock区别
volatile、synchronized和Lock是Java中用于实现多线程同步的三种不同机制,它们有以下区别:
可见性和有序性:
- volatile关键字保证了被修饰变量在多线程环境下的可见性和有序性。它可以确保一个线程对变量的修改对其他线程立即可见,并禁止指令重排序。
- synchronized和Lock机制也可以保证可见性和有序性,但更多地是通过锁的获取和释放来实现,而不是直接关注变量的可见性。
适用范围:
- volatile适用于对单个变量的操作,不能保证复合操作的原子性。
- synchronized和Lock机制可以用于对代码块、方法或对象进行加锁,适用于对多个变量或多个操作的同步。
原子性:
- volatile关键字不能保证复合操作的原子性,它只能保证单一变量的读写操作的原子性。
- synchronized关键字和Lock机制可以保证临界区的操作具有原子性,即同一时刻只有一个线程可以执行被加锁的代码。
粒度和灵活性:
- volatile关键字粒度较细,适用于对单一变量的操作,使用简单,但功能有限。
- synchronized关键字和Lock机制粒度较大,适用于对代码块、方法或对象进行加锁,可以实现更复杂的同步需求,并且提供了更多的灵活性和控制力。
可重入性:
- synchronized关键字是可重入锁,同一个线程可以多次获取同一个锁对象而不会造成死锁。
- Lock机制也可以实现可重入锁,但需要显式地调用lock()和unlock()方法。
总结: volatile关键字适用于单一变量操作,保证可见性和有序性;synchronized关键字和Lock机制适用于对代码块、方法或对象进行加锁,可以保证原子性和多个变量的同步。volatile功能简单,粒度小;synchronized和Lock功能更强大,灵活性高。同时要注意,synchronized是Java中的关键字,而Lock是接口,在使用时需要显式地获取和释放锁,相比之下灵活性更强。
为什么⽤线程池?解释下线程池参数?
创建线程很耗资源
参数:
核心线程数
最大线程数量
空闲线程存活时间
时间单位
线程工厂
拒绝策略
线程池的底层⼯作原理
线程池底层是队列+线程实现的
- 线程池中会维护一组可重用的线程,这些线程可以被多个任务调度使用。
- 线程池中会有一个任务队列,所有提交到线程池中的任务都会被存储在这个队列中。
- 当有新任务提交到线程池时,线程池会从任务队列中取出一个空闲的线程来执行该任务。
- 如果当前没有可用的空闲线程,则新任务将会被暂存到任务队列中等待调度执行。
- 当某个线程执行完任务之后,它会返回线程池,变为可重用状态,然后继续从任务队列中获取新任务执行。
- 线程池会根据需要动态调整线程数量,加入或移除线程来适应任务量的不同。
线程池中阻塞队列的作⽤?为什么是先添加列队⽽不是先创建最⼤线程?
阻塞队列在线程池中起到了缓冲和控制线程并发数量的作用,它可以暂存待执行的任务,并根据实际情况动态调整线程数量。通过先添加到队列而不是先创建最大线程,可以更好地利用已有的线程资源,减少线程的创建和销毁开销,提高线程的复用率和系统的性能。
线程池中线程复⽤原理
线程池中线程复用的原理是通过维护一组可重入的线程,让它们在执行完任务之后返回线程池进行下一次任务的执行
ReentrantLock中的公平锁和⾮公平锁的底层实现
ReentrantLock是Java中可重入的独占锁,它提供了公平锁(Fair)和非公平锁(Nonfair)两种模式。下面是它们的底层实现原理:
公平锁(Fair Lock):
- 公平锁是指多个线程按照申请的顺序获取锁,遵循"先到先得"的原则。
- 在ReentrantLock的公平锁模式下,锁的获取是按照线程的申请顺序进行的。
- 当一个线程请求获取锁时,如果锁已经被其他线程持有,该线程会被放入等待队列中,处于等待状态。
- 当锁被释放时,等待队列中的线程按照先后顺序将逐个获得锁。
非公平锁(Nonfair Lock):
- 非公平锁是指多个线程争夺锁时,不考虑等待队列中的顺序,允许新抢占锁的线程优先于等待的线程获取锁。
- 在ReentrantLock的非公平锁模式下,一个线程可以直接尝试获取锁,不管其他线程是否在等待获取锁。
- 如果尝试获取锁失败,则会将当前线程放入等待队列中,处于等待状态。
- 当锁被释放时,如果存在等待的线程,会从等待队列中选择一个线程获得锁。
底层的实现机制主要依赖于AbstractQueuedSynchronizer(AQS)这个同步器,它是ReentrantLock内部实现的关键组件。AQS使用一个FIFO的等待队列来管理线程的获取和释放锁的顺序。当一个线程请求获取锁时,如果无法成功,它会进入等待队列,然后被阻塞挂起。而当锁被释放时,AQS会根据公平性或非公平性的原则选择下一个应该获得锁的线程。
总结: ReentrantLock中的公平锁和非公平锁的底层实现都依赖于AbstractQueuedSynchronizer(AQS)这个同步器。公平锁按照申请的顺序获取锁,等待的线程按照先后顺序获得锁;而非公平锁允许新抢占锁的线程优先于等待的线程获取锁。AQS使用一个FIFO的等待队列来管理线程的获取和释放锁的顺序,并通过选择合适的线程来获得锁。
ReentrantLock中tryLock()和lock()⽅法的区别
1. tryLock()表示尝试加锁,可能加到,也可能加不到,该⽅法不会阻塞线程,如果加到锁则返回 true,没有加到则返回false2. lock()表示阻塞加锁,线程会阻塞直到加到锁,⽅法也没有返回值
CountDownLatch和Semaphore的区别和底层原理
CountDownLatch和Semaphore是Java中用于并发控制的两个类,它们有以下区别和底层原理:
功能区别:
- CountDownLatch(倒计时门闩)用于等待其他线程完成一组操作,它通过设定一个初始计数值,每个线程完成任务时会将计数器减1,主线程调用
await()
方法等待计数器变为0。- Semaphore(信号量)用于控制同时访问某个资源的线程数量,它通过许可证的数量进行管理,当线程需要访问资源时,需要先申请许可证,如果许可证数量足够,则线程可以获得许可证并继续执行,否则线程就要等待其他线程释放许可证。
计数机制:
- CountDownLatch使用一个整数计数器来实现。计数器的初始值由构造方法指定,每当一个线程完成任务时,计数器减1。主线程通过
await()
方法等待计数器变为0,当计数器为0时,表示所有线程已经完成任务,主线程继续执行。- Semaphore使用一个许可证数量来实现。每个线程在访问资源之前都需要获取一个许可证,如果还有许可证可用,则线程获得许可证并继续执行,否则线程等待其他线程释放许可证。当线程释放许可证时,Semaphore会增加可用的许可证数量。
底层原理:
- CountDownLatch的底层原理主要依赖于Unsafe类提供的CAS操作和一个volatile变量来保证线程间的可见性。每个线程通过CAS操作将计数器减1,并且在每次更新计数器时都会进行内存屏障操作,保证线程能够读取到最新的计数值。
- Semaphore的底层原理也使用了Unsafe类提供的CAS操作、AQS同步器以及一个FIFO的等待队列。线程申请许可证时,会尝试获取同步状态的许可证数量,如果成功获取则继续执行,否则线程将被阻塞并进入等待队列,直到有其他线程释放许可证。
总结: CountDownLatch用于等待其他线程完成任务,使用整数计数器实现等待机制;Semaphore用于控制同时访问资源的线程数量,使用许可证数量进行管理。它们的底层原理分别依赖于CAS操作、volatile变量、内存屏障、AQS同步器以及等待队列来实现并发控制。
Sychronized的偏向锁、轻量级锁、重量级锁
在Java中,synchronized关键字是用于实现线程同步的一种机制。它可以通过不同的锁状态来提供不同级别的性能和并发性。在具体实现上,synchronized关键字使用了三种不同的锁,即偏向锁、轻量级锁和重量级锁。
偏向锁(Biased Locking):
- 偏向锁是为了解决无竞争情况下的锁性能问题而引入的机制。
- 当一个线程获得了偏向锁之后,如果没有其他线程尝试竞争该锁,该线程再次进入临界区时无需再次获取锁,从而减少了额外的开销。
- 偏向锁的核心原理是在对象头中设置一个标志位,表示该对象是否被偏向锁定,并存储拥有偏向锁的线程ID。
- 当有其他线程尝试竞争该锁时,偏向锁升级为轻量级锁。
轻量级锁(Lightweight Locking):
- 轻量级锁是在短时间内,多个线程交替访问同一个锁时使用的一种优化手段。
- 当线程请求轻量级锁时,JVM会将对象的Mark Word复制到线程的锁记录中,并尝试使用CAS(Compare and Swap)操作将对象的Mark Word替换为指向线程锁记录的指针。
- 如果CAS操作成功,线程获得了轻量级锁,并继续执行临界区代码。
- 如果CAS操作失败,表示存在竞争,线程自旋等待一段时间,期望其他线程释放锁,如果自旋等待超过一定次数或时间,锁就会升级为重量级锁。
重量级锁(Heavyweight Locking):
- 重量级锁是在竞争激烈或自旋等待超过一定阈值时使用的一种锁机制。
- 当线程请求重量级锁时,JVM会将线程阻塞,并将线程置于等待状态,直到其他线程释放锁。
- 重量级锁采用操作系统提供的互斥量来实现,在切换线程和阻塞唤醒线程时涉及较大的开销。
总结: synchronized关键字在Java中提供了偏向锁、轻量级锁和重量级锁三种不同级别的锁机制。偏向锁适用于无竞争的场景,轻量级锁适用于多个线程交替访问同一锁的场景,而重量级锁适用于竞争激烈或自旋等待超过一定阈值的场景。使用不同级别的锁可以根据具体情况提高并发性和性能。
Sychronized和ReentrantLock的区别
1. sychronized是⼀个关键字,ReentrantLock是⼀个类2. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁3. sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁4. sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识 来标识锁的状态6. sychronized底层有⼀个锁升级的过程
谈谈你对AQS的理解,AQS如何实现可重⼊锁?
AQS(AbstractQueuedSynchronizer)是Java并发包中的一个抽象基类,用于构建各种同步器。它提供了一种底层的同步机制和状态管理,同时也支持可重入锁的实现。
在AQS中,可重入锁的核心实现是内部维护了一个表示当前占用次数的int值,即ReentrantLock实例的holdCount。线程首先会尝试获取锁,如果当前锁被其他线程持有,那么线程就会进入AQS的等待队列中。当线程成功获取到锁时,它会将holdCount加1;释放锁时,则会将holdCount减1。这样,在同一线程需要获取锁时,可以通过判断当前线程是否为锁的持有者,来决定是否直接获取锁。
具体地,AQS实现可重入锁的关键在于以下两个方面:
重写tryAcquire()和tryRelease()方法
- 在AQS的子类中重写tryAcquire()和tryRelease()方法,其中tryAcquire()方法用于尝试获取锁,如果当前锁已被获取,则返回false,否则返回true。
- 如果tryAcquire()方法返回true,则表示当前线程获取到了锁,可以继续执行临界区代码,并将holdCount+1。
- 当同一线程再次请求获取锁时,需要判断当前线程是否是锁的持有者,如果是,则直接将holdCount+1并获取锁;否则,需要通过AQS的等待队列来协调线程的执行顺序。
状态判断和更新
- 在tryAcquire()方法中,需要对锁状态进行判断,并进行更新操作,保证多个线程间的同步和协作。
- 如果当前锁状态为0(未被占用),则表示当前线程可以获取到锁,此时需要使用compareAndSetState()方法将锁状态设置为1。
- 如果锁状态已经为1了,则表示锁已经被其他线程占用,那么当前线程需要进入等待状态。
- 在tryRelease()方法中,需要将锁状态更新为0,并且释放当前线程对锁的占有。如果当前线程还持有该锁,则将holdCount减1。
通过重写tryAcquire()和tryRelease()方法,并实现状态的判断和更新,AQS可以很方便地实现可重入锁的功能。在具体应用时,可以使用ReentrantLock类来创建可重入锁。它内部利用了AQS的机制,提供了更为简单和易用的接口。