JAVA高频面试题目集锦(4)

CAS?CAS 有什么缺陷,如何解决?

CAS:Compare and Swap,即比较再交换。


CAS算法理解:CAS是一种无锁算法,CAS有3个操作数,内存值E,旧的预期值V,要修改的新值N。当且仅当预期值V和内存值E相同时,将内存值E修改为N,否则什么都不做。


CAS带来的问题:

1.ABA问题

因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么CAS进行检查的时候发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前的标志是否等于预期标志,如果全部相等,则以原子方式将该应用和该标志的值设置为给定的更新值。


2.循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销,如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空,从而提高CPU的实行效率。


3.只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ji=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之前的原子性,就可以把多个变量放在一个对象里来进行CAS操作。


ThreadLocal作用、原理以及适用范围

ThreadLocal:线程本地变量,它为每个使用该对象的线程创建了一个独立的变量副本。

原理:在Thread类中存在一个ThreaLocalMap变量,ThreadLocalMap中又有一个Entry类型的数组,而这个Entry对象则以ThreadLocal的弱引用为key。当我们调用ThreadLocal的get()方法时,会先获取当前线程的ThreadLocalMap对象,并将当前ThreadLocal对象作为key(实际上key为ThreadLocal的弱引用),去它的Entry数组中寻找我们需要的value。就这是我们说ThreadLocal为每个线程创建了一个变量副本的意思,线程对自己ThreadLocalMap中的值进行操作时,并不会对其它线程造成影响。


内存泄漏问题:


JAVA高频面试题目集锦(4)


ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

解决方案:线程执行前重新调用set()设置值。线程复用导致产生脏数据,如果复用线程在执行下个任务之前调用set()重新设置值,那么脏数据问题就不会出现了。


脏数据问题:脏数据问题:线程复用导致产生脏数据。由于线程池会复用Thread对象,进而Thread对象中的threalLocals也会被复用,导致Thread对象在执行其他任务时通过get()方法获取到之前任务设置的数据,从而产生脏数据。

解决方案:每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

适用范围: 数据库连接、Session管理等。


举一个在实际中应用的例子,例如,我们有一个银行的BankDAO类和一个个人账户的PeopleDAO类,现在需要个人向银行进行转账,在PeopleDAO类中有一个账户减少的方法,BankDAO类中有一个账户增加的方法,那么这两个方法在调用的时候必须使用同一个Connection数据库连接对象,如果他们使用两个Connection对象,则会开启两段事务,可能出现个人账户减少而银行账户未增加的现象。


使用同一个Connection对象的话,在应用程序中可能会设置为一个全局的数据库连接对象,从而避免在调用每个方法时都传递一个Connection对象。问题是当我们把Connection对象设置为全局变量时,你不能保证是否有其他线程会将这个Connection对象关闭,这样就会出现线程安全问题。


解决办法就是在进行转账操作这个线程中,使用ThreadLocal中获取Connection对象,这样,在调用个人账户减少和银行账户增加的线程中,就能从ThreadLocal中取到同一个Connection对象,并且这个Connection对象为转账操作这个线程独有,不会被其他线程影响,保证了线程安全性。


什么是AQS,抽象队列同步器?

AQS定义就是抽象队列同步器,定义了锁的框架,具体实现由其子类完成,在AQS内部会保存一个状态变量state,通过CAS修改该变量的值,修改成功的线程表示获取到该锁,没有修改成功,或者发现状态state已经是加锁状态,则通过一个Waiter对象封装线程,添加到一个FIFO的双向等待队列中,并挂起等待被唤醒。AQS支持两种模式:独占和共享,独占模式中锁只会被一个线程独占,共享模式中多个线程可同时执行。像ReentrantLock,ReentrantReadWriteLock、CountDownLatch、Semaphore这些常用类都是基于AQS实现的。


ReentrantLock可重入锁

,ReentrantLock通过AQS+Volitile+CAS实现,是可以重入的锁,当一个线程获取锁时,还可以接着重复获取多次。

ReentrantLock核心流程加锁和解锁,默认的是非公平锁


1.非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。

2.非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

可重入锁的实现就是Volitile关键字,获取锁的时候加1,释放锁的时候-1


Condition接口及其实现原理

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列


Condition是个接口,基本的方法就是await()和signal()方法;

Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()

调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用

为何要使用Condition?


因为有时候获得锁的线程发现其某个条件不满足导致不能继续后面的业务逻辑,此时该线程只能先释放锁,等待条件满足。那可不可以不释放锁的等待呢?比如将await方法替换为sleep方法(这也是面试经常问的await和sleep的区别)?

显然不行,因为等待的条件显然和共享的资源是有关的,在这个例子里,take方法会等待notEmpty条件,notEmpty指的是items不为空,意味着此时items是空的,那么就只有对items执行add操作,即其它线程调用put方法才有机会达到notEmpty的条件,所以如果使用sleep(不释放锁)来等待而不是await(释放锁)来等待,则会导致notEmpty这个条件永远满足不了。

总结起来,就是获得锁的线程发现某个条件不满足而不能继续执行,而且该条件需要其它线程对共享资源进行操作才能触发,所以必须释放锁。


为什么要使用多个Condition?


图中可以看到,每个Condition会有自己单独的等待队列,调用await方法,会放到对应的等待队列中。当调用某个Condition的signalAll/signal方法,则只会唤醒对应的等待队列中的线程。

唤醒的粒度变小了,且更具针对性。如果只使用一个Condition的话,有些线程即使被唤醒并取得锁,其依然有可能并不满足条件而浪费了机会,产生时间损耗,相当于notEmpty的Condition唤醒了


上一篇:Android开发之手势滑动(滑动手势监听)详解


下一篇:Dream It Possible