java面试题 --- 并发②

1. JDK1.6 开始对 synchronized 做了哪些优化?

使用了锁升级、锁粗化、锁消除等方式来优化性能。

  • 锁升级就是先尝试偏向锁,如果没获取到锁就升级为轻量级锁,还没获取到就升级为重量级锁;
  • 锁粗化就是如果连续一系列的操作都对同一段代码反复加锁和解锁,就将加锁范围扩大,减少加解锁的次数;
  • 锁消除就是如果某一段代码加了锁但是根本不会存在并发竞争资源的问题,那么虚拟机就会把锁去掉。

2. Synchronized 和 ReentrantLock 有何异同?

  • Synchronized 是 JVM 层面的关键字,ReentrantLock 是 API 层面的;
  • Synchronized 可以修饰代码块和方法,ReentrantLock 只能用于代码块;
  • Synchronized 不需要手动释放锁,ReentrantLock 需要手动释放锁;
  • Synchronized 是非公平锁,ReentrantLock 可以通过参数指定为公平或者非公平;
  • Synchronized 等待不能中断,ReentrantLock 等待可以中断,tryLock 可以设置等待时长;
  • Synchronized 和 ReentrantLock 都是可重入锁。

3. volatile 有什么作用?

  • 它可以保证可见性,禁止指令重排,但是不能保证原子性。在 JMM 内存模型中,线程操作共享资源是先将主存中的共享资源拷贝回自己的工作内存,在工作内存中完成修改后刷回到主存,在同步回主存之前,别的线程是不知道这个值已经被改了的,这便是可见性问题。用 volatile 就可以保证一个线程对共享资源的操作对别的线程可见。JVM 编译代码的时候,会对代码做优化,使其有更好的性能,这就是指令重排。用 volatile 可以禁止指令重排。

4. final 关键字有什么特性?

  • final 修饰的对象不可变,可以保证内存的可见性,不需要额外的同步手段。

5. 说说你对 as if serial 和 happen before 的理解。

  • as if serial 就是在单线程的情况下,不管怎么指令重排,运行结果都要保持不变;
  • happen before 就是正确同步的多线程程序不管怎么指令重排运行结果要保持不变。

6. ReentrantLock 的加锁和解锁过程是怎样的?

加锁过程:

  • 加锁的时候实际上调用的是 NonfairSync/FairSync 的 lock 方法;
  • lock 方法首先调用 compareAndSetState(0, 1) 方法,如果当前 state 是0,就改成1,同时调用 setExclusiveOwnerThread 方法将持有锁线程设置为当前线程,加锁成功;
  • 如果当前 state 不是0,表示锁被别的线程持有,就用 acquire(1) 方法尝试获取锁;
  • acquire(1) 首先会用 tryAcquire(1) 方法尝试获取锁,该方法会判断 state 的值,是0,那就进行步骤2的操作;不是0但是当前线程等于正持有锁的线程,那就让 state 加1,这就是可重入原理;不是以上两种情况,那就尝试获取锁失败;
  • tryAcquire(1) 失败就会调用 acquireQueued 方法将当前线程加入到队列自旋;
  • 最后会调用 LockSupport 的 park 方法获取锁。

释放锁过程:

  • 调用的实际上是 NonfairSync/FairSync 的 release 方法;
  • release 调用的又是 tryRelease(1) 方法;
  • tryRelease(1) 会将当前 state 减1,同时把当前持有锁的线程设置为 null;
  • 最后调用 unparkSuccessor 方法,该方法里面调用 lockSupport 的 unpark 方法释放锁。

7. ReentrantReadWriteLock 怎么用一个 state 来表示读锁和写锁的状态的?

  • ReentrantReadWriteLock 读锁是共享锁,写锁是独占锁,它用 AQS 中的 state 变量的高十六位来表示读锁,值就是持有读锁的线程数量,低十六位表示写锁,值为零或者一,若是大于一,那就是重入的次数。

8. 并发的时候 List 不安全,有哪些解决办法?

  • 加锁;
  • 用 Vector;
  • 用 Collections.synchronizedList 方法;
  • 用 CopyOnWriteArrayList,它写之前会拷贝一份,写完再把引用指向拷贝的副本。

9. 你还用过哪些并发工具类?

  • CountDownLatch 等待一组线程执行完,主线程再继续执行;CycliBarriar 类似,不过 CountDownLatch 是减计数,即倒数,倒数到 0 释放所有等待的线程,调用 countDown() 方法计数减一,调用 await() 方法只进行阻塞;CycliBarriar 是加计数,即顺数,计数到指定值时释放等待线程,调用 await() 方法计数加 1,若加 1后的值不等于构造方法的值,则线程阻塞;Semaphore,构造方法传入一个 int 参数,相当于限定并发数,比如传的是 3,那么并发数只能是3。

10. 有没有了解过 ThreadLocal?

  • ThreadLocal 是用来做数据隔离的,ThreadLocal 保存的数据只对当前线程可见。用 set 方法设置数据,get 方法获取数据。原理是 Thread 类有个 ThreadLocal.ThreadLocalMap 类型的变量 threadLocals, ThreadLocal set 数据的时候,会判断当前线程类的 threadLocals 是否为空,如果为空,就会创建一个 ThreadLocalMap,然后以当前的 ThreadLocal 为 key,把 value set 进去, 并且让 threadLocals 引用指向它;如果不为空,就直接拿来用。常用于保存数据库连接,做 session、cookie 的隔离。ThreadLocal 在作为 ThreadLocalMap 的 key 时候被设计成弱引用了,但是我们 new ThreadLocal 实例的时候是强引用,所以 GC 此时并不会回收它,当 ThreadLocal 实例的生命周期结束了,没有强引用指向它了,那么它作为 ThreadLocalMap 的 key 就只有弱引用,GC 发现了就会回收它,key 被回收了,那 value 永远都用不了,就存在内存泄漏问题,解决办法就是用完之后主动调用 remove 方法。

你可能感兴趣的:(java面试题 --- 并发②)