Java并发指南18:JUC常见⾯试题及答案
什么是 CAS 吗?
CAS(Compare And Swap)指⽐较并交换。CAS算法CAS(V, E, N)包含 3 个参数,V 表⽰要更新的变量,E 表⽰预期的值,N 表⽰新值。在且仅在 V 值等于 E值时,才会将 V 值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,当前线程什么都不做。最后,CAS 返回当前 V 的真实值。Concurrent包下所有类底层都是依靠CAS操作来实现,⽽sun.misc.Unsafe为我们提供了⼀系列的CAS操作。
CAS 有什么缺点?
ABA问题
⾃旋问题
范围不能灵活控制
对 CAS 中的 ABA 产⽣有解决⽅案吗?
什么是 ABA 问题呢?多线程环境下。线程 1 从内存的V位置取出 A ,线程 2 也从内存中取出 A,并将 V 位置的数据⾸先修改为 B,接着⼜将 V 位置的数据修改为 A,线程 1 在进⾏CAS操作时会发现在内存中仍然是 A,线程 1 操作成功。尽管从线程 1 的⾓度来说,CAS操作是成功的,但在该过程中其实 V 位置的数据发⽣了变化,线程 1 没有感知到罢了,这在某些应⽤场景下可能出现过程数据不⼀致的问题。
可以版本号(version)来解决 ABA 问题的,在 atomic 包中提供了AtomicStampedReference 这个类,它是专门⽤来解决 ABA 问题的。
直达链接:
CAS ⾃旋导致的问题?
由于单次 CAS 不⼀定能执⾏成功,所以 CAS往往是配合着循环来实现的,有的时候甚⾄是死循环,不停地进⾏重试,直到线程竞争不激烈的时候,才能修改成功。
CPU 资源也是⼀直在被消耗的,这会对性能产⽣很⼤的影响。所以这就要求我们,要根据实际情况来选择是否使⽤ CAS,在⾼并发的场景下,通常 CAS 的效率是不⾼的。
CAS 范围不能灵活控制
不能灵活控制线程安全的范围。只能针对某⼀个,⽽不是多个共享变量的,不能针对多个共享变量同时进⾏ CAS操作,因为这多个变量之间是独⽴的,简单的把原⼦操作组合到⼀起,并不具备原⼦性。
什么是 AQS 吗?
AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是使⽤AQS实现的。AQS定义了⼀套多线程访问共享资源的同步框架,许多同步类的实现都依赖于它,例如常⽤
的Synchronized、ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch等。该框架下的锁会先尝试以CAS乐观锁去获取锁,如果获取不到,则会转为悲观锁(如RetreenLock)。
了解 AQS 共享资源的⽅式吗?
独占式:只有⼀个线程能执⾏,具体的Java实现有ReentrantLock。
共享式:多个线程可同时执⾏,具体的Java实现有Semaphore和CountDownLatch。
Atomic 原⼦更新
Java 从 JDK1.5 开始提供了 urrent.atomic 包,⽅便程序员在多线程环 境下,⽆锁的进⾏原⼦操作。在 Atomic 包⾥⼀共有 12个类,四种原⼦更新⽅式,分别是原⼦更新基本类型,原⼦更新数组,原⼦更新引⽤和原⼦更新字段。在 JDK 1.8 之后⼜新增⼏个原⼦类。如下如:
列举⼏个AtomicLong 的常⽤⽅法
long getAndIncrement() :以原⼦⽅式将当前值加1,注意,返回的是旧值。(i++)
long incrementAndGet() :以原⼦⽅式将当前值加1,注意,返回的是新值。(++i)
long getAndDecrement() :以原⼦⽅式将当前值减 1,注意,返回的是旧值 。(i--)
long decrementAndGet() :以原⼦⽅式将当前值减 1,注意,返回的是新值 。(--i)
long addAndGet(int delta) :以原⼦⽅式将输⼊的数值与实例中的值(AtomicLong⾥的value)相加,并返回结果
说说 AtomicInteger 和 synchronized 的异同点?
相同点
都是线程安全
不同点
1、背后原理
synchronized 背后的 monitor 锁。在执⾏同步代码之前,需要⾸先获取到 monitor 锁,执⾏完毕后,再释放锁。原⼦类,线程安全的原理是利⽤了 CAS 操作。
2、使⽤范围
原⼦类使⽤范围是⽐较局限的,⼀个原⼦类仅仅是⼀个对象,不够灵活。⽽ synchronized 的使⽤范围要⼴泛得多。⽐如说
synchronized 既可以修饰⼀个⽅法,⼜可以修饰⼀段代码,相当于可以根据我们的需要,⾮常灵活地去控制它的应⽤范围
3、粒度
原⼦变量的粒度是⽐较⼩的,它可以把竞争范围缩⼩到变量级别。通常情况下,synchronized锁的粒度都要⼤于原⼦变量的粒度。
4、性能
synchronized是⼀种典型的悲观锁,⽽原⼦类恰恰相反,它利⽤的是乐观锁。
原⼦类和 volatile 有什么异同?
volatile 可见性问题
解决原⼦性问题
AtomicLong 可否被 LongAdder 替代?
有了更⾼效的 LongAdder,那AtomicLong 可否不使⽤了呢?是否凡是⽤到 AtomicLong的地⽅,都可以⽤LongAdder替换掉呢?答案是不是的,这需要区分场景。
LongAdder 只提供了 add、increment 等简单的⽅法,适合的是统计求和计数的场景,场景⽐较单⼀,⽽ AtomicLong 还具有compareAndSet 等⾼级⽅法,可以应对除了加减之外的更复杂的需要CAS 的场景。
结论:如果我们的场景仅仅是需要⽤到加和减操作的话,那么可以直接使⽤更⾼效的 LongAdder,但如果我们需要利⽤ CAS ⽐
如compareAndSet 等操作的话,就需要使⽤ AtomicLong 来完成。
直达链接:
并发⼯具
CountDownLatch
CountDownLatch基于线程计数器来实现并发访问控制,主要⽤于主线程等待其他⼦线程都执⾏完毕后执⾏相关操作。其使⽤过程为:在主线程中定义CountDownLatch,并将线程计数器的初始值设置为⼦线程的个数,多个⼦线程并发执⾏,每个⼦线程在执⾏完毕后都会调
⽤countDown函数将计数器的值减1,直到线程计数器为0,表⽰所有的⼦线程任务都已执⾏完毕,此时在CountDownLatch上等待的主线程将被唤醒并继续执⾏。
CyclicBarrier
CyclicBarrier(循环屏障)是⼀个同步⼯具,可以实现让⼀组线程等待⾄某个状态之后再全部同时执⾏。在所有等待线程都被释放之
后,CyclicBarrier可以被重⽤。CyclicBarrier的运⾏状态叫作Barrier状态,在调⽤await⽅法后,线程就处于Barrier状态。
CyclicBarrier中最重要的⽅法是await⽅法,它有两种实现。
public int await():挂起当前线程直到所有线程都为Barrier状态再同时执⾏后续的任务。
public int await(long timeout, TimeUnit unit):设置⼀个超时时间,在超时时间过后,如果还有线程未达到Barrier状态,则不再等待,让达到Barrier状态的线程继续执⾏后续的任务。
Semaphore
Semaphore指信号量,⽤于控制同时访问某些资源的线程个数,具体做法为通过调⽤acquire()获取⼀个许可,如果没有许可,则等待,在许可使⽤完毕后通过release()释放该许可,以便其他线程使⽤。
CyclicBarrier 和 CountdownLatch 有什么异同?
相同点:都能阻塞⼀个或⼀组线程,直到某个预设的条件达成发⽣,再统⼀出发。
但是它们也有很多不同点,具体如下。
作⽤对象不同:CyclicBarrier 要等固定数量的线程都到达了栅栏位置才能继续执⾏,⽽ CountDownLatch 只需等待数字倒数到 0,也就是说 CountDownLatch 作⽤于事件,但 CyclicBarrier 作⽤于线程;CountDownLatch 是在调⽤了 countDown ⽅法之后把数字倒数减 1,⽽ CyclicBarrier 是在某线程开始等待后把计数减 1。
可重⽤性不同:CountDownLatch 在倒数到 0 并且触发门闩打开后,就不能再次使⽤了,除⾮新建⼀个
新的实例;⽽ CyclicBarrier 可以重复使⽤。CyclicBarrier还可以随时调⽤ reset ⽅法进⾏重置,如果重置时有线程已经调⽤了 await ⽅法并开始等待,那么这些线程则会抛出 BrokenBarrierException异常。
执⾏动作不同:CyclicBarrier 有执⾏动作 barrierAction,⽽ CountDownLatch没这个功能。
CountDownLatch、CyclicBarrier、Semaphore的区别如下。
CountDownLatch和CyclicBarrier都⽤于实现多线程之间的相互等待,但⼆者的关注点不同。CountDownLatch主要⽤于主线程等待其他⼦线程任务均执⾏完毕后再执⾏接下来的业务逻辑单元,⽽CyclicBarrier主要⽤于⼀组线程互相等待⼤家都达到某个状态后,再同时执⾏接下来的业务逻辑单元。此外,CountDownLatch是不可以重⽤的,⽽CyclicBarrier是可以重⽤的。
Semaphore和Java中的锁功能类似,主要⽤于控制资源的并发访问。
locks
公平锁与⾮公平锁
ReentrantLock⽀持公平锁和⾮公平锁两种⽅式。公平锁指锁的分配和竞争机制是公平的,即遵循先到先得原则。⾮公平锁指JVM遵循随机、就近原则分配锁的机制。ReentrantLock通过在构造函数Reentra
ntLock(boolean fair)中传递不同的参数来定义不同类型的锁,默认的实现是⾮公平锁。这是因为,⾮公平锁虽然放弃了锁的公平性,但是执⾏效率明显⾼于公平锁。如果系统没有特殊的要求,⼀般情况下建议使⽤⾮公平锁。
synchronized 和 lock 有什么区别?
synchronized 可以给类,⽅法,代码块加锁,⽽lock 只能给代码块加锁。
jvm面试题总结及答案synchronized 不需要⼿动获取锁和释放锁,使⽤简单,发⽣异常会⾃动释放锁,不会造成死锁,⽽ lock 需要⼿动⾃⼰加锁和释放锁,如果使⽤不当没有 unLock 去释放锁,就会造成死锁。
通过 lock 可以知道有没有成功获取锁,⽽ synchronized ⽆法办到。
synchronized 和 Lock 如何选择?
synchronized 和Lock 都是⽤来保护资源线程安全的。
都保证了可见性和互斥性。
synchronized 和 ReentrantLock都拥有可重⼊的特点。
不同点:
⽤法(lock 需要配合finally )
ReentrantLock可响应中断、可轮回,为处理锁提供了更多的灵活性
ReentrantLock通过Condition可以绑定多个条件
加解锁顺序()
synchronized 锁不够灵活
是否可以设置公平/⾮公平
⼆者的底层实现不⼀样:synchronized是同步阻塞,采⽤的是悲观并发策略;Lock是同步⾮阻塞,采⽤的是乐观并发策略。
使⽤
如果能不⽤最好既不使⽤ Lock 也不使⽤ synchronized。
如果 synchronized关键字适合你的程序,这样可以减少编写代码的数量,减少出错的概率
如果特别需要 Lock 的特殊功能,⽐如尝试获取锁、可中断、超时功能等,才使⽤ Lock。
Lock接⼝的主要⽅法
void lock():获取锁,调⽤该⽅法当前线程将会获取锁,当锁获得后,从该⽅法返回
void lockInterruptibly() throws InterruptedException:可中断地获取锁,和lock⽅法地不同之处在于该⽅法会响应中断,即在锁的获取中可以中断当前线程
boolean tryLock(): 尝试⾮阻塞地获取锁,调⽤该⽅法后⽴刻返回,如果能够获取则返回 true 否则 返回false
boolean tryLock(long time, TimeUnit unit):超时地获取锁,当前线程在以下 3 种情况下会返回:
当前线程在超时时间内获得了锁
当前线程在超时时间被中断
超时时间结束后,返回 false
void unlock(): 释放锁
Condition newCondition():获取锁等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调⽤该组件的 wait() ⽅法,⽽调⽤后,当前线程将释放锁。
tryLock、lock和lockInterruptibly的区别
tryLock、lock和lockInterruptibly的区别如下。
tryLock若有可⽤锁,则获取该锁并返回true,否则返回false,不会有延迟或等待;tryLock(long timeout, TimeUnit unit)可以增加时间限制,如果超过了指定的时间还没获得锁,则返回 false。
lock若有可⽤锁,则获取该锁并返回true,否则会⼀直等待直到获取可⽤锁。
在锁中断时lockInterruptibly会抛出异常,lock不会。
ReentrantReadWriteLock 读写锁的获取规则
要么是⼀个或多个线程同时有读锁,要么是⼀个线程有写锁,但是两者不会同时出现。也可以总结为:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)
ReentrantLock 适⽤于⼀般场合,ReadWriteLock 适⽤于读多写少的情况,合理使⽤可以进⼀步提⾼并发效率。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。