Java多线程复习(三):并发容器、线程池、ThreadLocal、伪共享

九、并发容器

1、并发容器简介

在Java1.5之前所谓的线程安全的容器,主要指的是同步容器。不过同步容器最大的问题就是性能差,所有方法都用synchronized来保证互斥,串行度太高了。因此Java在1.5及之后版本提供了性能更高的容器,一般称为并发容器

并发容器关系图如下:

在这里插入图片描述

2、List

1)、CopyOnWriteArrayList简介

List里面只有一个实现类CopyOnWriteArrayList。CopyOnWrite意思就是写的时候会将共享变量重新复制一份出来,这样做的好处是读操作完全无锁

CopyOnWriteArrayList内部维护了一个数组,成员变量array就指向这个内部数组,所有的读操作都是基于array进行的,遍历器Iterator遍历的就是array数组

在这里插入图片描述

在遍历array的同时,还有一个写操作,CopyOnWriteArrayList会将array复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将array指向这个新的数组。读写是可以并行的,遍历操作一直都是基于原array执行,而写操作则是基于新array进行

在这里插入图片描述

2)、CopyOnWriteArrayList源码分析

在这里插入图片描述

扫描二维码关注公众号,回复: 8875844 查看本文章

在CopyOnWriteArrayList类中,有一个array数组对象用来存放具体元素,ReentrantLock独占锁对象用来保证同时只有一个线程对array进行修改

1)add(E e)方法

    public boolean add(E e) {
        //获取独占锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //获取array
            Object[] elements = getArray();
            //复制array到新数组,添加元素到新数组
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            //使用新数组替换添加前的数组
            setArray(newElements);
            return true;
        } finally {
            //释放独占锁
            lock.unlock();
        }
    }

2)弱一致的迭代器

    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
    static final class COWIterator<E> implements ListIterator<E> {
        //array的快照版本
        private final Object[] snapshot;
        
        //数组下标
        private int cursor;

        //构造函数
        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }

        //是否遍历结束
        public boolean hasNext() {
            return cursor < snapshot.length;
        }

        //获取元素
        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

为什么说snapshot是list的快照呢?明明是指针传递的引用,而不是副本

如果在该线程使用返回的迭代器遍历元素的过程中,其他线程没有对list进行增删改,那么snapshot本身就是list的array,因为它们是引用关系。但是如果在遍历期间其他线程对该list进行了增删改,那么snapshot就是快照了,因为增删改后list里面的数组被新数组替换了,这时候老数组被snapshot引用。这也说明获取迭代器后,使用该迭代器元素时,其他线程对该list进行的增删改不可见,因为它们操作的是两个不同的数组,这就是弱一致性

3)、小结

CopyOnWriteArrayList使用写时复制的策略来保证list的一致性,而获取—修改一写入三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个时间只有一个线程能对list数组进行修改。另外CopyOnWriteArrayList提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对list的修改是不可见的,迭代器遍历的数组是一个快照

CopyOnWriteArrayList仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致

3、Map

Map接口的两个实现是ConcurrentHashMap和ConcurrentSkipListMap。从应用的角度来看,主要区别在于ConcurrentHashMap的key是无序的,而ConcurrentSkipListMap的key是有序的

ConcurrentHashMap和ConcurrentSkipListMap的key和value都不能为空,下面这个表格总结了Map相关的实现类对key和value的要求

在这里插入图片描述

**学习ConcurrentHashMap相关知识可以参考之前的博客:**https://blog.csdn.net/qq_40378034/article/details/87256635

ConcurrentSkipListMap是使用跳表实现的,跳表插入、删除、查询操作平均的时间复杂度是 O ( l o g n ) O(log n) ,理论上和并发线程数没有关系

4、Set

Set接口的两个实现是CopyOnWriteArraySet和ConcurrentSkipListSet,使用场景可以参考前面讲的CopyOnWriteArrayList和ConcurrentSkipListMap

5、Queue

Java并发包里面的Queue可以从以下两个维度来分类。一个维度是阻塞与非阻塞,所谓阻塞指的是当队列已满时,入队操作阻塞;当队列已空时,出队操作阻塞。另一个维度是单端与双端,单端指的是只能队尾入队,队首出队;而双端指的是队首队尾皆可以入队出队。Java并发包阻塞队列都用Blocking关键字标识,单端队列使用Queue标识,双端队列使用Deque标识

这两个维度组合后,可以将Queue细分为四大类,分别是:

1)、单端阻塞队列

其实现有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue和DelayQueue。内部一般会持有一个队列,这个队列可以是数组(其实现是 ArrayBlockingQueue)也可以是链表(其实现是LinkedBlockingQueue);甚至还可以不持有队列(其实现是 SynchronousQueue),此时生产者线程的入队操作必须等待消费者线程的出队操作。而LinkedTransferQueue融合 LinkedBlockingQueue和SynchronousQueue的功能,性能比LinkedBlockingQueue更好;PriorityBlockingQueue支持按照优先级出队;DelayQueue支持延时出队

在这里插入图片描述

2)、双端阻塞队列

其实现是LinkedBlockingDeque

在这里插入图片描述

3)、单端非阻塞队列

其实现是ConcurrentLinkedQueue

4)、双端非阻塞队列

其实现是ConcurrentLinkedDeque

另外,使用队列时,需要格外注意队列是否支持有界(所谓有界指的是内部的队列是否有容量限制)。实际工作中,一般都不建议使用无界的队列,因为数据量大了之后很容易导致OOM。上面我们提到的这些Queue中,只有ArrayBlockingQueue和LinkedBlockingQueue是支持有界的,所以在使用其他无界队列时,一定要充分考虑是否存在导致OOM的隐患

十、线程池

1、使用线程池的好处

  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行
  • 提高线程的可管理性:使用线程池可以统一分配、调优和监控

2、线程池实现原理

当提交一个新任务到线程池时,线程池的处理流程如下

1)、线程池判断核心线程池里的线程是否已满且线程都在执行任务。如果不是,则创建一个新的工作线程来执行任务。否则进入下个流程

2)、线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。否则进入下个流程

3)、线程池判断线程池的线程数是否达到最大线程数且线程都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。否则进入下个流程

在这里插入图片描述

在这里插入图片描述

ThreadPoolExecutor执行execute方法分下面4种情况

1)、如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(这一步需要获取全局锁)

2)、如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue

3)、如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(这一步需要获取全局锁)

4)、如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法

ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁

3、线程池使用

1)、线程池的创建

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

1)corePoolSize(核心线程池大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程

2)maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。如果使用了无界的任务队列这个参数无效

3)keepAliveTime(线程活动保持时间)&unit(线程活动保持时间的单位):如果一个线程空闲了keepAliveTime&unit这么久,而且线程池的线程池数大于corePoolSize,那么这个空闲的线程就要被回收了

4)workQueue(任务队列):用于保存等待执行的任务的阻塞队列

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFIO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool()使用了这个队列
  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列

5)ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置名字

6)RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常

  • AbortPolicy:直接抛出异常
  • CallerRunsPolicy:只用调用者所在线程来运行任务
  • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
  • DiscardPolicy:不处理,丢弃掉

2)、向线程池提交任务

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功

        threadPool.execute(new Runnable() {
            @Override
            public void run() {

            }
        });

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout, TimeUnit unit)方法会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完

        Future<Boolean> future = threadPool.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                return true;
            }
        });
        try {
            Boolean flag = future.get();
        } catch (InterruptedException e) {
            //处理中断异常
            e.printStackTrace();
        } catch (ExecutionException e) {
            //处理无法执行任务异常
            e.printStackTrace();
        } finally {
            //关闭线程池
            threadPool.shutdown();
        }

3)、关闭线程池

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true

4)、合理地配置线程池

1)不建议使用静态工厂类Executors:Executors提供的很多方法默认使用的都是无界的LinkedBlockingQueue,高负载情境下,无界队列很容易导致OOM,而OOM会导致所有请求都无法处理,所以强烈建议使用有界队列

https://blog.csdn.net/qq_40378034/article/details/87344164

2)使用有界队列,当任务过多时,线程池会触发执行拒绝策略,线程池默认的拒绝策略会throw RejectedExecutionException这是个运行时异常,对于运行时异常编译器并不强制catch它,所以开发人员很容易忽略。因此默认拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略

3)使用线程池,还需要注意异常处理的问题,例如通过ThreadPoolExecutor对象的execute()方法提交任务时,如果任务在执行的过程中出现运行时异常,会导致执行任务的线程终止;不过,最致命的是任务虽然异常了,但是却获取不到任何通知,这会让人误以为任务都执行得很正常。虽然线程池提供了很多用于异常处理的方法,但是最稳妥和简单的方案还是捕获所有异常并按需处理,可以参考下面的示例代码:

        try {
            // 业务逻辑
        } catch (RuntimeException runtimeException) {
            // 按需处理
        } catch (Throwable throwable) {
            // 按需处理
        }

4)性质不同的任务(CPU密集型任务、IO密集型任务和混合型任务)可以用不同规模的线程池分开处理

创建多少线程合适可以参考之前的博客:https://blog.csdn.net/qq_40378034/article/details/100556161

5)、其他API

1)prestartAllCoreThreads()和prestartCoreThread()方法:线程池预热,提前创建等于核心线程数的线程数量

2)allowsCoreThreadTimeOut()方法:线程池包括核心线程在内,没有任务分配的所有线程,在等待keepAliveTime&unit时间后全部回收掉

4、ThreadPoolExecutor源码分析

ThreadPoolExecutor中的成员变量ctl是一个32位二进制的Integer原子变量,用来记录线程池状态和线程池中线程个数,其中高3位用来表示线程池状态,后面29位用来记录线程池线程个数

    //(高3位)用来标识线程池状态,(低29位)用来表示线程个数
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    //线程个数掩码位数
    private static final int COUNT_BITS = Integer.SIZE - 3;
    //线程最大个数(低29位) 00011111111111111111111111111111
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    //(高3位)11100000000000000000000000000000
    private static final int RUNNING    = -1 << COUNT_BITS;
    //(高3位)00000000000000000000000000000000
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    //(高3位)00100000000000000000000000000000
    private static final int STOP       =  1 << COUNT_BITS;
    //(高3位)01000000000000000000000000000000
    private static final int TIDYING    =  2 << COUNT_BITS;
    //(高3位)01100000000000000000000000000000
    private static final int TERMINATED =  3 << COUNT_BITS;

    //取高3位(运行状态)
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    //获取低29位(线程个数)
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    //计算ctl新值(线程状态与线程个数)
    private static int ctlOf(int rs, int wc) { return rs | wc; }

线程池状态含义如下:

  • RUNNING:接受新任务并且处理阻塞队列里的任务
  • SHUTDOWN:拒绝新任务但是处理阻塞队列里的任务
  • STOP:拒绝新任务并且抛弃阻塞队列里的任务,同时会中断正在处理的任务
  • TIDYING:所有任务都执行完后当前线程池活动线程数为0,将要调用terminated方法
  • TERMINATED:终止状态。terminated方法调用完以后的状态

线程池状态转换图:

在这里插入图片描述

execute()方法:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        
        //获取当前线程池的状态+线程个数变啦的组合值
        int c = ctl.get();
        //当前线程池中线程个数小于corePoolSize则开启新线程(core线程)运行
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //如果线程池处于Running状态,则添加任务到阻塞队列
        if (isRunning(c) && workQueue.offer(command)) {
            //二次检查,因为添加任务到任务队列后,可能线程池的状态已经变化了
            int recheck = ctl.get();
            //如果当前线程池状态不是Running则从队列中删除任务,并执行拒绝策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
            //否则如果当前线程池为空,则添加一个线程
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //如果队列满,则新增线程,新增失败则执行拒绝策略
        else if (!addWorker(command, false))
            reject(command);
    }

addWorker()方法:

    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            //检查队列是否只在必要时为空
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            //循环CAS增加线程个数
            for (;;) {
                int wc = workerCountOf(c);
                //如果线程个数超限则返回false
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //CAS增加线程个数,同时只有一个线程成功
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get(); 
                //CAS失败了,则看线程状态是否变化了,变化则调到外层(retry处)循环重新尝试获取线程池状态,否则内层重新CAS
                if (runStateOf(c) != rs)
                    continue retry;
            }
        }

        //此时说明CAS成功
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            //创建worker
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                //加独占锁,为了实现workers同步,因为可能多个线程调用了线程池的execute方法
                mainLock.lock();
                try {
                    //重新检查线程池状态,以避免在获取锁前调用了shutdown接口
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        //添加任务
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                //添加成功后则启动任务
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

break retry:跳到retry处,且不再进入循环
continue retry:跳到retry处,且再次进入循环

十一、创建多少线程才合适

对于CPU密集型的计算场景,理论上线程的数量=CPU核数就是最合适的。不过在工程上,线程的数量一般会设置为CPU核数+1,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证CPU的利用率

对于I/O密集型的计算场景,最佳线程数=1+(I/O耗时/CPU耗时),针对多核CPU,最佳线程数=CPU核数×[1+(I/O耗时/CPU耗时)]

十二、ThreadLocal

1、ThreadLocal简介

ThreadLocal是一个以ThreadLocal对象为键、任意对象为值的存储结构,提供了线程本地变量,也就是如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存

在这里插入图片描述

ThreadLocal的内部结构图如下

在这里插入图片描述

2、ThreadLocal使用示例

public class ThreadLocalTest {
    static void print(String str) {
        System.out.println(str + ":" + localVariable.get());
        localVariable.remove();
    }

    static ThreadLocal<String> localVariable = new ThreadLocal<String>();

    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                localVariable.set("threadOne local variable");
                print("threadOne");
                System.out.println("threadOne remove after :" + localVariable.get());
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                localVariable.set("threadTwo local variable");
                print("threadTwo");
                System.out.println("threadTwo remove after :" + localVariable.get());
            }
        }).start();
    }
}

运行结果:

threadOne:threadOne local variable
threadOne remove after :null
threadTwo:threadTwo local variable
threadTwo remove after :null

3、ThreadLocal的实现原理

在这里插入图片描述

Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的HashMap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的set()或者get()方法时才会创建它们。其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面。也就是说,ThreadLocal类型的本地变量存放在具体的线程内存空间中。ThreadLocal就是一个工具壳,它通过set()方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get()方法时,再从当前线程的threadLocals变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove()方法,从当前线程的threadLocals里面删除该本地变量

1)、void set(T value)

    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //找到当前线程对应的threadLocals变量
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            //第一次调用就创建当前线程对应的threadLocals变量
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        //获取线程自己的变量threadLocals,threadLocals变量被绑定到了线程的成员变量上
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        //创建当前线程的threadLocals变量
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

2)、T get()

    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的threadLocals变量
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //如果threadLocals不为null,则返回对应本地变量的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //threadLocals为空则初始化当前线程的threadLocals成员变量
        return setInitialValue();
    }

    private T setInitialValue() {
        //初始化为null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //如果当前线程的threadLocals变量不为null
        if (map != null)
            map.set(this, value);
        //如果当前线程的threadLocals变量为null
        else
            createMap(t, value);
        return value;
    }

    protected T initialValue() {
        return null;
    }

3)、void remove()

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

如果当前线程的threadLocals变量不为空,则删除当前线程中指定ThreadLocal实例的本地变量

4)、ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现的

在这里插入图片描述

在ThreadLocalMap中,也是用Entry来保存K-V结构数据的,但是Entry中key只能是ThreadLocal对象

        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有key是弱引用类型的,value并非弱引用

        private static final int INITIAL_CAPACITY = 16;

		ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

从ThreadLocalMap的构造函数可以得知,ThreadLocalMap初始化容量为16,负载因子为2/3

和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式。所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置

        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

		private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下

  • 如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上
  • 位置i已有对象,如果这个Entry对象的key正好是即将设置的key,那么覆盖value
  • 位置i的对象,和即将设置的key没关系,那么只能找下一个空位置

5)、ThreadLocalMap内存泄露问题

在这里插入图片描述

上图中,实线代表强引用,虚线代表的是弱引用,如果threadLocal外部强引用被置为null(threadLocalInstance=null)的话,threadLocal实例就没有一条引用链路可达,很显然在GC(垃圾回收)的时候势必会被回收,因此entry就存在key为null的情况,无法通过一个Key为null去访问到该entry的value。同时,就存在了这样一条引用链:threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory,导致在垃圾回收的时候进行可达性分析的时候,value可达从而不会被回收掉,但是该value永远不能被访问到,这样就存在了内存泄漏。当然,如果线程执行结束后,threadLocal和threadRef会断掉,因此threadLocal、threadLocalMap、entry都会被回收掉。可是,在实际使用中我们都是会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们关注

ThreadLocalMap的设计中已经做出了哪些改进?

ThreadLocalMap中的get()和set()方法都针对内存泄露问题做了相应的处理,下文为了叙述,针对key为null的entry,源码注释为stale entry,就称之为“脏entry”

ThreadLocalMap的set()方法

        private void set(ThreadLocal<?> key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

在该方法中针对脏entry做了这样的处理:

  • 如果当前table[i]不为空的话说明hash冲突就需要向后环形查找,若在查找过程中遇到脏entry就通过replaceStaleEntry()进行处理;
  • 如果当前table[i]为空的话说明新的entry可以直接插入,但是插入后会调用cleanSomeSlots()方法检测并清除脏entry

当我们调用threadLocal的get()方法时,当table[i]不是和所要找的key相同的话,会继续通过threadLocalMap的getEntryAfterMiss()方法向后环形去找

        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

当key为null的时候,即遇到脏entry也会调用expungeStleEntry()对脏entry进行清理

当我们调用threadLocal.remove()方法时候,实际上会调用threadLocalMap的remove方法

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

同样的可以看出,当遇到了key为null的脏entry的时候,也会调用expungeStaleEntry()清理掉脏entry

从以上set()get()remove()方法看出,在ThreadLocal的生命周期里,针对ThreadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry()cleanSomeSlots()replaceStaleEntry()这三个方法清理掉key为null的脏entry

想要更加深入学习ThreadLocal内存泄漏问题可以查看这篇文章

小结

在这里插入图片描述

在每个线程内部都有一个名为threadLocals的成员变量,该变量的类型为HashMap,其中key为我们定义的ThreadLocal变量的this引用,value则为我们使用set方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢岀,因 此使用完毕后要记得调用ThreadLocal的remove()方法删除对应线程的threadLocals中的本地变量

4、InheritableThreadLocal类

同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。而子类InheritableThreadLocal提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    //(1)
    protected T childValue(T parentValue) {
        return parentValue;
    }

    //(2)
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    
	//(3)
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

从上面InheritableThreadLocal的源码中可知,InheritableThreadLocal继承了ThreadLocal,并重写了三个方 法。由代码(3)可知,InheritableThreadLocal重写了createMap()方法,那么现在当第一次调用set()方法时,创建的是当前线程的inheritableThreadLocals变量的实例而不再是threadLocals。由代码(2)可知,当调用get()方法获取当前线程内部的map变量时,获取的是inheritableThreadLocals而不再是threadLocals

综上可知,在InheritableThreadLocal的世界里,变量inheritableThreadLocals替代了threadLocals

下面我们看一下重写的代码(1)何时执行,以及如何让子线程可以访问父线程的本地变量。这要从创建Thread的代码说起,打开Thread类的默认构造函数

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        //获取当前线程,也就是父线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            
            if (security != null) {
                g = security.getThreadGroup();
            }

            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        g.checkAccess();

        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        //如果父线程的inheritableThreadLocals变量不为null
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
			//设置子线程中的inheritableThreadLocals变量
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        
        this.stackSize = stackSize;

        tid = nextThreadID();
    }

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

在createlnheritedMap内部使用父线程的inheritableThreadLocals变量作为构造函数创建了一个新的ThreadLocalMap变量,然后赋值给了子线程的inheritableThreadLocals变量

小结

InheritableThreadLocal类通过重写代码(2)和(3)让本地变量保存到了具体线程的inheritableThreadLocals变量里面,那么线程在通过InheritableThreadLocal类实例的set()或者get()方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。当父线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量里面

十三、伪共享

1、CPU Cache概述

随着CPU的频率不断提升,而内存的访问速度却没有质的突破,为了弥补访问内存的速度慢,充分发挥CPU的计算资源,提高CPU整体吞吐量,在CPU与内存之间引入了一级Cache。随着热点数据体积越来越大,一级Cache
L1已经不满足发展的要求,引入了二级Cache L2,三级Cache L3。(若无特别说明,本文的Cache指CPU
Cache,高速缓存)CPU Cache在存储器层次结构中的示意如下图:

在这里插入图片描述

计算机早已进入多核时代,软件也越来越多的支持多核运行。一个处理器对应一个物理插槽,多处理器间通过QPI总线相连。一个处理器包含多个核,一个处理器间的多核共享L3 Cache。一个核包含寄存器、L1 Cache、L2 Cache,下图是Intel Sandy Bridge CPU架构,一个典型的NUMA多处理器结构:

在这里插入图片描述

以常见的X86芯片为例,Cache的结构下图所示:整个Cache被分为S个组,每个组是又由E行个最小的存储单元——Cache Line所组成,而一个Cache Line中有B(B=64)个字节用来存储数据,即每个Cache Line能存储64个字节的数据,每个Cache Line又额外包含一个有效位(valid bit)、t个标记位(tag bit),其中valid bit用来表示该缓存行是否有效,tag bit用来协助寻址,唯一标识存储在Cache Line中的块,Cache Line里的64个字节其实是对应内存地址中的数据拷贝。根据Cache的结构体,可以推算出每一级Cache的大小为B×E×S

在这里插入图片描述

2、什么是伪共享

在这里插入图片描述

数据X、Y、Z被加载到同一Cache Line中,线程A在Core1上修改X,而修改X会导致其所在的所有核上的缓存行均失效;假设此时线程B在Core2上读取Y,由于X所在的缓存行已经失效,所有Core2必须从内存中重新读取。线程A的操作不会修改Y,但是由于X和Y共享的是一个缓存行,就导致线程B不能很好地利用Cache,这其实就是伪共享。简单来说,伪共享指的是由于共享缓存行导致缓存无效的场景

3、如何避免伪共享

举个例子:

public class Data {
    long modifyTime;
    boolean flag;
    long createTime;
    char key;
    int value;
}

假如业务场景中,上述的类满足以下几个特点:

1)当value变量改变时,modifyTime肯定会改变

2)createTime变量和key变量在创建后,就不会再变化

3)flag也会经常变化,不过与modifyTime和value变量毫无关联

当上面的对象需要由多个线程同时的访问时,从Cache角度来说,就会有一些有趣的问题。当我们没有加任何措施时,Data对象所有的变量极有可能被加载在L1缓存的一行Cache Line中。在高并发访问下,会出现这种问题:

在这里插入图片描述

如上图所示,每次value变更时,根据MESI协议,对象其他CPU上相关的Cache Line全部被设置为失效。其他的处理器想要访问未变化的数据(key和createTime)时,必须从内存中重新拉取数据,增大了数据访问的开销

1)、Padding方式

正确的方式应该将该对象属性分组,将一起变化的放在一组,与其他属性无关的属性放到一组,将不变的属性放到一组。这样当每次对象变化时,不会带动所有的属性重新加载缓存,提升了读取效率。在JDK1.8以前,一般是在属性间增加长整型变量来分隔每一组属性。被操作的每一组属性占的字节数加上前后填充属性所占的字节数,不小于一个cache Line的字节数就可以达到要求,通过填充变量,使不相关的变量分开:

public class DataPadding {
    long a1, a2, a3, a4, a5, a6, a7, a8;//防止与前一个对象产生伪共享
    int value;
    long modifyTime;
    long b1, b2, b3, b4, b5, b6, b7, b8;//防止不相关变量伪共享
    boolean flag;
    long c1, c2, c3, c4, c5, c6, c7, c8;//防止不相关变量伪共享
    long createTime;
    char key;
    long d1, d2, d3, d4, d5, d6, d7, d8;//防止与下一个对象产生伪共享
}

2)、Contended注解方式

在JDK1.8中,新增了一种注解@sun.misc.Contended来使各个变量在Cache Line中分隔开。其原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,使用2倍于大多数硬件缓存行的大小来避免相邻扇区预取导致的伪共享冲突。可以在类前或属性前加上此注释:

//类前加上代表整个类的每个变量都会在单独的Cache Line中
@sun.misc.Contended
@SuppressWarnings("restriction")
public class ContendedData {
    int value;
    long modifyTime;
    boolean flag;
    long createTime;
    char key;
}

或者:

//属性前加上时需要加上组标签
@SuppressWarnings("restriction")
public class ContendedGroupData {
    @sun.misc.Contended("group1")
    int value;
    @sun.misc.Contended("group1")
    long modifyTime;
    @sun.misc.Contended("group2")
    boolean flag;
    @sun.misc.Contended("group3")
    long createTime;
    @sun.misc.Contended("group3")
    char key;
}

采取上述措施图示:

在这里插入图片描述

在默认情况下,@Contended注解只用于Java核心类,比如rt包下的类。 如果用户类路径下的类需要使用这个注解,则需要添加JVM参数:-XX:-RestrictContended。填充的宽度默认为128,要自定义宽度则可以设置-XX:ContendedPaddingWidth参数

如果感兴趣的话,可以通过下面的代码来试一下加padding和不加的效果:

public class FalseSharing implements Runnable {

    public static final int NUM_THREADS = 4;
    public static final long ITERATIONS = 500 * 1000 * 1000;
    private final int arrayIndex;

    private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
//    private static VolatileLong2[] longs = new VolatileLong2[NUM_THREADS];
//    private static VolatileLong3[] longs = new VolatileLong3[NUM_THREADS];

    static {
        for (int i = 0; i < longs.length; i++) {
            longs[i] = new VolatileLong();
        }
    }

    public FalseSharing(final int arrayIndex) {
        this.arrayIndex = arrayIndex;
    }

    public static void main(final String[] args) throws Exception {
        long start = System.nanoTime();
        runTest();
        System.out.println("duration = " + (System.nanoTime() - start));
    }

    private static void runTest() throws InterruptedException {
        Thread[] threads = new Thread[NUM_THREADS];

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new FalseSharing(i));
        }

        for (Thread t : threads) {
            t.start();
        }

        for (Thread t : threads) {
            t.join();
        }
    }

    public void run() {
        long i = ITERATIONS;
        while (0 != --i) {
            longs[arrayIndex].value = i;
        }
    }

    public final static class VolatileLong {
        public volatile long value = 0L;
    }

    /**
     * long padding避免false sharing
     */
    public final static class VolatileLong2 {
        volatile long p0, p1, p2, p3, p4, p5, p6;
        public volatile long value = 0L;
        volatile long q0, q1, q2, q3, q4, q5, q6;
    }

    /**
     * JDK8新特性,Contended注解避免false sharing
     */
    @sun.misc.Contended
    public final static class VolatileLong3 {
        public volatile long value = 0L;
    }
}
发布了177 篇原创文章 · 获赞 407 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/103748812