新鲜出炉!看完这份多线程面试题,今年秋招我完全不慌

512 阅读8分钟

一、父子线程怎么共享数据

JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把任务提交给线程池时的ThreadLocal值传递到任务执行时。

核心类TransmittableThreadLocal:
public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> { ...... }

首先TransmittableThreadLocal继承自InheritableThreadLocal,这样可以在不破坏原有InheritableThreadLocal特性的情况下,还能充分使用Thread线程创建过程中执行init方法,从而达到父子线程传递数据的目的。

变量holder源码如下:

holder中存放的是InheritableThreadLocal本地变量

WeakHashMap支持存放空置

// 理解holder,需注意如下几点:
// 1、holder 是 InheritableThreadLocal 变量;
// 2、holder 是 static 变量;
// 3、value 是 WeakHashMap;
// 4、深刻理解 ThreadLocal 工作原理;
private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder =
    new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {
      @Override
      protected Map<TransmittableThreadLocal<?>, ?> initialValue() {
          return new WeakHashMap<>();
      }
 
      @Override
      protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {
          return new WeakHashMap<>(parentValue);
      }
};
主要方法源码如下:

get方法调用时,先获取父类的相关数据判断是否有数据,然后在holder中把自身也给加进去

set方法调用时,先在父类中设置,再本地判断是holder否为删除或者是新增数据

remove调用时,先删除自身,再删除父类中的数据,删除也是直接以自身this作为变量Key

// 调用 get() 方法时,同时将 this 指针放入 holder
public final T get() {
    T value = super.get();
    if (null != value) {
        addValue();
    }
    return value;
}
void addValue() {
    if (!holder.get().containsKey(this)) {
        holder.get().put(this, null); // WeakHashMap supports null value.
    }
}
// 调用 set() 方法时,同时处理 holder 中 this 指针
public final void set(T value) {
    super.set(value);
    if (null == value) { // may set null to remove value
        removeValue();
    } else {
        addValue();
    }
}
void removeValue() {
    holder.get().remove(this);
}

采用包装的形式来处理线程池中的线程不会执行初始化的问题,源码如下:

先取得holder

备份线程本地数据

run原先的方法

还原线程本地数据

public void run() {
     Object captured = this.capturedRef.get():
     if (captured != null && (!this.releaseTtlValueReferenceAfterRun ||
             this.capturedRef.compareAndSet(captured, (0bject) null))){
         Object backup = Transmitter.replay(captured);
         try {
             this.runnable.run();
         } finally {
             Transmitter.restore(backup);
         }
     }else{
         throw new IllegalStateException("TTL value reference is released after run!");
     }
}

处理线程池中的线程不会执行初始化的问题,备份方法:

先获取holder中的数据

进行迭代,数据在captured中不存在,但是holder中存在,说明是后来加进去的,进行删除

再将captured设置到当前线程中

public static Object repLay(@Nonnull Object captured) (
    Map<TransmittableThreadLocal<?>,Object>capturedMap = (Map) captured;
    Map<TransmittableThreadLocal<?>,Object>backup = new HashMap() ;
    Iterator iterator=((Map) TransmittableThreadLocal.holder.get()).entrySet().iterator();
 
    while(iterator.has Next() ) (
       Entry<TransmittableThreadLocal<?>,?>next = (Entry) iterator.next()
       TransmittableThreadLocal<?>threadLocal = (TransmittableThreadLocal)next.getKey();
       backup.put(threadLocal, threadLocal.get() ) ;
       if(!capturedMap.containsKey(thread Local) ) (
           iterator.remove() ;
           threadLocal.superRemove() ;
       )
 
    setTtlValuesTo(capturedMap) ;
    TransmittableThreadLocal.doExecuteCallback(true) ;
    return backup;
)

还原方法:

先获取holder中的数据

backup中不存在,holder中存在,说明是后面加进去的,进行删除还原操作

再将backup设置到当前线程中

public static void restore(@Nonnull0bject backup) (
    Map<TransmittableThreadLocal<?>,Object> backupMap = (Map)backup;
    TransmittableThreadLocal.doExecuteCallback(false)
    Iterator iterator=((Map) TransmittableThreadLocal.holder.get()).entrySet().iterator();
 
    while(iterator.has Next() ) (
        Entry<TransmittableThreadLocal<?>,?> next = (Entry)iterator.next();
        TransmittableThreadLocal<?> threadLocaL=(TransmittableThreadLocal)next.getKey();
        if(!backupMap.containsKey(threadLocal) ) (
            iterator.remove() ;
            threadLocal.superRemove() ;
        }
    |
    setTtlValuesTo(backupMap) ;
}

二、CountDownLatch和CyclicBarrier的异同

1、相同点

都可以实现线程间的等待

2、不同点

侧重点不同

CountDownLatch:一般用于一个线程等待一组其它线程;计数器不可以重用

CyclicBarrier:一般是一组线程间的相互等待至某同步点,计数器是可以重用的

实现原理不同

CyclicBarrier:如果有三个线程thread1、thread2和thread3,假设线程执行顺序是thread1、thread2、thread3,那么thread1、thread2对应的Node节点会被加入到Condition等待队列中,当thread3执行的时候,会将thread1、thread2对应的Node节点按thread1、thread2顺序转移到AQS同步队列中,thread3执行lock.unlock()的时候,会先唤醒thread1,thread1恢复继续执行,thread1执行到lock.unlock()的时候会唤醒thread2恢复执行

CountDownLatch:使用CountDownLatch(int count)构造器创建CountDownLatch实例 ,将count参数赋值给内部计数 state,调 await() 法阻塞当前线程,并将当前线程封装加到等待队 中,直到state等于零或当前线程被中断;调 countDown() 法使state值减 ,如果state等于零则唤醒等待队中的线程

三、AQS原理

AQS是一个基于状态(state)的链表管理方式,reentracntlock这个锁是基于AQS实现的子类sync这个来完成锁。

获取锁的时候,当前线程会去更新状态state的值,如果为0才去更新,通过CAS进行更新,如果成功更新为1,那么获取到锁,将锁的拥有者改成当前线程,如果失败,那么进行tryAcquire() 这个函数进行首先还是尝试更新state状态,反正开销也小,再次去尝试一次也行,如果尝试失败,那么去看看当前拥有锁的线程是不是当前线程,如果是,那么将state状态值加1,如果不是,那么将线程入阻塞队列,addWaiter函数,进行的话首先判断当前head是不是为空,为空尝试将当前的线程关联的节点用CAS加入队列,不为空或者加入失败,那么用CAS加入到队列的下一个节点。

释放锁的时候,将状态值减1,如果状态值为0说明可以释放锁,如果结果状态为0,就将排它锁的Owner设置为null,以使得其它的线程有机会进行执行。

四、volatile(指令重排序和内存屏障)

1、什么是内存屏障

内存屏障其实就是一个CPU指令,在硬件层面上来说可以扥为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。主要有两个作用:

(1)阻止屏障两侧的指令重排序;

(2)强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

在JVM层面上来说作用与上面的一样,但是种类可以分为四种:

2、volatile如何保证有序性

首先一个变量被volatile关键字修饰之后有两个作用:

(1)对于写操作:对变量更改完之后,要立刻写回到主存中。

(2)对于读操作:对变量读取的时候,要从主内存中读,而不是缓存。

现在针对上面JVM的四种内存屏障,应用到volatile身上。因此volatile也带有了这种效果。其实上面提到的这些内存屏障应用的效果,可以用happen-before来总结归纳。

3、内存屏障分类

内存屏障有三种类型和一种伪类型:

(1)lfence:即读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据。

(2)sfence:即写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见。

(3)mfence,即全能屏障,具备ifence和sfence的能力。

(4)Lock前缀:Lock不是一种内存屏障,但是它能完成类似全能型内存屏障的功能。

为什么说Lock是一种伪类型的内存屏障,是因为内存屏障具有happen-before的效果,而Lock在一定程度上保证了先后执行的顺序,因此也叫做伪类型。比如,IO操作的指令,当指令不执行时,就具有了mfence的功能。

由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。