多线程小抄集(新编二)

多线程小抄集(新编二)

ThreadLocal

ThreadLocal可以实现每个线程绑定自己的值,即每个线程有各自独立的副本而互相不受影响。一共有四个方法:get, set, remove, initialValue。可以重写initialValue()方法来为ThreadLocal赋初值。SimpleDateFormat不是线程安全的,可以通过如下的方式让每个线程单独拥有这个对象:

private static final ThreadLocal<DateFormat> DATE_FORMAT = new ThreadLocal<DateFormat>(){
@Override protected DateFormat initialValue(){
return new SimpleDateFormat("yyyy-mm-dd");
}
};
private static final ThreadLocal<StringBuilder> stringBuilder = new ThreadLocal<StringBuilder>(){
@Override protected StringBuilder initialValue(){
return new StringBuilder(256);
}
};

ThreadLocal不是用来解决对象共享访问问题的,而主要提供了线程保持对象的方法和避免参数传递的方便的对象访问方式。ThreadLocal使用场合主要解决多线程中数据因并发产生不一致的问题。ThreadLocal为每个线程中的并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步所带来的线程消耗,也减少了线程并发控制的复杂度。

通过ThreadLocal.set()将新创建的对象的引用保存到各线程的自己的一个map(Thread类中的ThreadLocal.ThreadLocalMap的变量)中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。ThreadLocalMap初始容量为16的数组table,如果个数超过threshold(capacity*2/3)之后就会按照原来容量的2倍进行扩容。每当有新的ThreadLocal添加到ThreadLocalMap的时候会通过nextHashCode()方法计算hash值:

nextHashCode.getAndAdd(HASH_INCREMENT);//HASH_INCREMENT = 0x61c88647;

然后根据key.threadLocalHashCode & (table.length - 1);计算出在table中的位置i,如果发生冲突,那就根据((i + 1 < len) ? i + 1 : 0)计算出新的位置i。只是找下一个可用空间并在其中插入元素(线性探测法)。其中0x61c88647为((根号5 -1 )/ 2)* 2的32次方,与斐波那契额和黄金分割有关,为了让哈希码能均匀地分布在2的n次方的数组内。

ThreadLocalMap中每个Entry的key(ThreadLocal实例)是弱引用,value是强引用(这点类似于WeakHashMap)。当把threadLocal实例置为null以后,没有任何强引用指向threadLocal实例,所以threadLocal将会被gc回收,但是value却不能被回收,因为其还存在于ThreadLocalMap的对象的Entry之中。只有当前Thread结束之后,所有与当前线程有关的资源才会被GC回收。所以如果在线程池中使用ThreadLocal,由于线程会复用,而又没有显示的调用remove的话的确是会有可能发生内存泄露的问题。线程复用还会产生脏数据。由于线程池会重用Thread对象,那么与Thread绑定时的类的静态属性ThreadLocal变量也会被重用。如果在实现的线程run()方法中不显式地调用remove()清理与线程相关的ThreadLocal信息,那么倘若下一个线程不调用set设置的初始值,就可能get到重用的线程信息,包括ThreadLocal所关联的线程对象的value值。

其实在ThreadLocalMap的get或者set方法中会探测其中的key是否被回收(调用expungeStaleEntry方法),然后将其value设置为null,这个功能几乎和WeakHashMap中的expungeStaleEntries()方法一样。因此value在key被gc后可能还会存活一段时间,但最终也会被回收,但是若不再调用get或者set方法时,那么这个value就在线程存活期间无法被释放。

ThreadLocal建议

  1. ThreadLocal类变量因为本身定位为要被多个线程来访问,它通常被定义为static变量。
  2. 能够通过值传递的参数,不要通过ThreadLocal存储,以免造成ThreadLocal的滥用。
  3. 在线程池的情况下,在ThreadLocal业务周期处理完成时,最好显示的调用remove()方法,清空“线程局部变量”中的值。
  4. 在正常情况下使用ThreadLocal不会造成OOM, 弱引用的知识ThreadLocal,保存值依然是强引用,如果ThreadLocal依然被其他对象应用,线程局部变量将无法回收。

InheritableThreadLocal

使用类InheritableThreadLocal可以在子线程中取得父线程继承下来的值。可以采用重写childValue(Object parentValue)方法来更改继承的值。InheritableThreadLocal是ThreadLocal的子类,代码量很少,可以看一下:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}

InheritableThreadLocal的使用示例:

private static final InheritableThreadLocal<Object> itl = new InheritableThreadLocal<Object>(){
@Override protected Object initialValue(){
return System.currentTimeMillis();
}

@Override
protected Object childValue(Object parentValue) {
return "get value from father: " + parentValue;
}
};

public static void main(String[] args) {
System.out.println(itl.get());
new Thread(){
@Override public void run(){
System.out.println(itl.get());
new Thread(){
@Override public void run(){
System.out.println(itl.get());
}
}.start();
}
}.start();
}

程序输出:

1542475428082
get value from father: 1542475428082
get value from father: get value from father: 1542475428082

ThreadLocalRandom

我们要避免Random实例被多线程使用,虽然共享该实例是线程安全的,但是会因竞争同一seed而导致性能下降。ThreadLocalRandom类是JDK7在JUC包下新增的随机数生成器,它解决了Random类在多线程下多个线程竞争内部唯一的原子性种子变量而导致大量线程自旋重试的不足。

CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N(CountDownLatch(int count))。CountDownLatch的方法有:await(), await(long timeout, TimeUnit unit), countDown(), getCount()等。计数器必须大于等于0,只是等于0的时候,计数器就是零,调用await方法时不会阻塞当前线程。CountDownLatch不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。一个线程调用countDown方法happens-before另一个线程调用的await()方法。

CyclicBarrier

让一组线程达到一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经达到了屏障,然后当前线程被阻塞。CyclicBarrier还提供了一个更高级的构造函数CyclicBarrier(int parties, Runnable barrierAction)用于在线程达到屏障时,优先执行barrierAction,方便处理更复杂的业务场景。

示例:

public class CyclicBarrierDemo {
private static final int N = 2;
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(N, () -> System.out.println("this is a barrier action"));

startThread(cyclicBarrier);
cyclicBarrier.reset();
startThread(cyclicBarrier);
}

public static void startThread(CyclicBarrier cyclicBarrier) {
for (int i=0;i<N;i++) {
new Thread(){
@Override public void run(){
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread());
}
}.start();
}
}
}

CyclicBarrier与CountDownLatch的区别

  1. CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset() 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
  2. CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。
  3. CyclicBarrier与CountDownLatch的关键区别在于,所有线程必须同时到达CyclicBarrier位置才能继续执行。CountDownLatch用于等待事件,而CyclicBarrier用于等待其他线程。

Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它协调各个线程,以保证合理的使用公共资源。Semaphore有两个构造函数:Semaphore(int permits)默认是非公平的,Semaphore(int permits, boolean fair)可以设置为公平的。

Exchanger

用于线程间协作的工具类(线程间的数据交换)。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都达到同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

Fork/Join框架

Fork/Join框架是JDK7提供的一个用于并行执行任务的框架,是一个把大任务切分为若干子任务并行的执行,最终汇总每个小任务后得到大任务结果的框架。我们再通过Fork和Join来理解下Fork/Join框架。Fork就是把一个大任务划分成为若干子任务并行的执行,Join就是合并这些子任务的执行结果,最后得到这个大任务的结果。使用Fork/Join框架时,首先需要创建一个ForkJoin任务,它提供在任务中执行fork()和join()操作的机制。通常情况下,我们不需要直接继承ForkJoinTask,只需要继承它的子类,Fork/Join框架提供了两个子类:RecursiveAction用于没有返回结果的任务;RecursiveTask用于有返回结果的任务。ForkJoinTask需要通过ForkJoinPool来执行。任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。(工作窃取算法work-stealing)。

工作窃取算法是指某个线程从其他队列里窃取任务来执行。在生产-消费者设计中,所有消费者有一个共享的工作队列,而在work-stealing设计中,每个消费者都有各自的双端队列,如果一个消费者完成了自己双端队列中的全部任务,那么它可以从其他消费者双端队列末尾秘密地获取工作。优点:充分利用线程进行并行计算,减少了线程间的竞争。 缺点:在某些情况下还是存在竞争,比如双端队列(Deque)里只有一个任务时。并且该算法会消耗了更多的系统资源,比如创建多个线程和多个双端队列。

示例:

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

public class CountTask extends RecursiveTask<Integer>
{
private static final int THRESHOLD = 10;
private int start;
private int end;

public CountTask(int start, int end)
{
super();
this.start = start;
this.end = end;
}

@Override
protected Integer compute()
{
int sum = 0;
boolean canCompute = (end-start) <= THRESHOLD;
if(canCompute)
{
for(int i=start;i<=end;i++)
{
sum += i;
}
}
else
{
int middle = (start+end)/2;
CountTask leftTask = new CountTask(start,middle);
CountTask rightTask = new CountTask(middle+1,end);
leftTask.fork();
rightTask.fork();
int leftResult = leftTask.join();
int rightResult = rightTask.join();
sum = leftResult+rightResult;
}

return sum;
}

public static void main(String[] args)
{
ForkJoinPool forkJoinPool = new ForkJoinPool();
CountTask task = new CountTask(1,100);
Future<Integer> result = forkJoinPool.submit(task);
try
{
System.out.println(result.get());
}
catch (InterruptedException | ExecutionException e)
{
e.printStackTrace();
}

if(task.isCompletedAbnormally()){
System.out.println(task.getException());
}
}
}

欢迎支持笔者的作品《深入理解Kafka: 核心设计与实践原理》和《RabbitMQ实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客(ID: hiddenkafka)。
本文作者: 朱小厮

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×