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

8次阅读

共计 5713 个字符,预计需要花费 15 分钟才能阅读完成。

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

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 表现出了锁的特性。

正文完
 0