Java并发编程(四)

ThreadLocal

1.ThreadLocal是什么

ThreadLocal 类让每一个线程都拥有了自己的本地变量,这意味着每个线程都可以独立地、安全地操作这些变量,而不会影响其他线程。

ThreadLocal的常用API

  • get():获取当前线程中与ThreadLocal对象关联的变量副本。

  • set(T value):将指定的值设置为当前线程中与ThreadLocal对象关联的变量副本。

  • remove():删除当前线程中与ThreadLocal对象关联的变量副本。这样可以避免内存泄漏问题。注意,remove()方法只会删除当前线程中的变量副本,不会影响其他线程中的副本。

  • initialValue():当调用get()或set()方法时,如果当前线程没有与ThreadLocal对象关联的变量副本,则会调用initialValue()方法创建一个新的变量副本并与当前线程关联。默认情况下,initialValue()方法返回null,可以通过继承ThreadLocal类并重写initialValue()方法来自定义初始化值。

2.ThreadLocal原理了解吗?

从 Thread 类源代码入手。

public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

从上面 Thread 类中可以看出 Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set 或 get 方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap 类对应的 get、set 方法。

ThreadLocal 类的 set 方法

public void set(T value) {
    //获取当前请求的线程
    Thread t = Thread.currentThread();
    //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将需要存储的值放入到这个哈希表中
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread() 获取到当前线程对象后,直接通过 getMap(Thread t) 可以访问到该线程的ThreadLocalMap对象。

每个 Thread中 都具备一个 ThreadLocalMap,而 ThreadLocalMap 可以存储以 ThreadLocal 为 key ,Object 对象为 value 的键值对。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //......
}

比如我们在同一个线程中声明了两个 ThreadLocal 对象的话, Thread内部都是使用仅有的那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。ThreadLocal 数据结构如下图所示。

ThreadLocalMap 是 ThreadLocal 的静态内部类。

3.ThreadLocal内存泄露问题是怎么导致的?

内存泄漏和内存溢出的区别是什么

  • 内存泄漏指的是程序中分配的内存在不再需要时没有被正确释放或回收的情况。

  • 内存溢出指的是程序试图分配超过其可用内存的内存空间的情况。

ThreadLocal 对象和 ThreadLocalMap 中使用的 key 是弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉。但是,如果Thread 对象一直在被使用,比如在线程池中被重复使用,那么从Thread 对象到 value 的引用链就一直在,导致 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。

ThreadLocalMap 实现中已经考虑了这种情况,在调用 set、get、remove 方法的时候,会清理掉 key 为 null 的 Entry。因此,使用完 ThreadLocal 方法后最好手动调用一下 remove 方法,就可以在下一次 GC 的时候,把 key 为 null 的 Entry 清理掉。

线程池

1.什么是线程池?

线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。

2.为什么要用线程池?

池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

3.如何创建线程池?

方式一:通过ThreadPoolExecutor构造函数来创建(推荐)

代码示例:

ExecutorService pools = new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS,
                                               new ArrayBlockingQueue<>(6),
                                               Executors.defaultThreadFactory(),
                                               new ThreadPoolExecutor.AbortPolicy());

ExecutorService的常用方法

方式二:通过线程池的工具类 Executors 来创建。

4.为什么不推荐使用内置线程池?

因为通过 Executors 创建出来的内置线程池会让我们不够熟悉线程池的运行规则,会有资源耗尽的风险,而通过 ThreadPoolExecutor 构造函数来创建线程池能让我们更加明确线程池的运行规则,规避资源耗尽的风险。

5.线程池的参数

6.线程池的任务拒绝策略有哪些?

  • ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出 RejectedExecutionException 异常。是默认的拒绝策略。

  • ThreadPoolExecutor.DiscardPolicy:丢弃新任务,但是不抛出异常。

  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列中等待最久的任务,然后把当前任务加入队列中。

  • ThreadPoolExecutor.CallerRunsPolicy: 在调用线程池的execute方法的线程中运行被拒绝的任务,从而绕过线程池直接执行。

7.线程池常用的阻塞队列有哪些?

新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在阻塞队列中。

  • ArrayBlockingQueue:是一个由数组结构组成的有界阻塞队列。它按照先进先出的原则对元素进行排序。

  • LinkedBlockingQueue:是一个由链表结构组成的有界阻塞队列。它按照先进先出的原则对元素进行排序。因为其队列大小默认为 Integer.MAX_VALUE,所以实际上它可以看作是无界队列。

  • SynchronousQueue:是一个没有缓冲的阻塞队列,每个插入操作必须等待另一个线程的移除操作,反之亦然。因此,该队列没有任何内部容量,不能预先插入元素。

  • PriorityBlockingQueue:是一个支持优先级排序的无界阻塞队列。默认情况下元素采取自然顺序排列,也可以通过实现 Comparable 接口或者在构造时传入 Comparator 对象进行排序。

由 Excutors 创建出来的内置线程池选用了不同的阻塞队列,不同的阻塞队列具有不同的特性和适用场景,具体使用哪种队列需要根据实际需求来选择。例如,

  • 如果需要控制队列大小且按照先进先出的顺序处理任务,可以选择 ArrayBlockingQueue 或 LinkedBlockingQueue;

  • 如果需要无缓冲等待两个线程之间的交互,可以选择 SynchronousQueue;

  • 如果需要按照优先级排序执行任务,可以选择 PriorityBlockingQueue。

8.线程池处理任务的流程了解吗?

  1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。

  2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。

  3. 如果任务队列已经满了导致任务投放任务失败,但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。

  4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用RejectedExecutionHandler.rejectedExecution() 方法

9.如何设定线程池的大小?

问题:很多人可能会觉得把线程池配置过大一点比较好。但是,线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了上下文切换成本。到底设置多少合适可以根据具体场景分析。

什么是上下文切换

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。Linux 相比与其他操作系统有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

CPU的8核16线程是什么意思

8核16线程指的是CPU能并行运行16线程,传统中,一个核心只能运行一个线程,但由英特尔公司开发的超线程技术硬件技术使得一个核心能并行运行多个线程。

10.如何设计一个能够根据任务的优先级来执行的线程池?

这是一个常见的面试问题,本质其实还是在考察求职者对于线程池以及阻塞队列的掌握。不同的线程池会选用不同的阻塞队列作为任务队列,比如 FixedThreadPool 使用的是LinkedBlockingQueue(无界队列),由于队列永远不会被放满,因此 FixedThreadPool 最多只能创建核心线程数的线程。ThreadPoolExecutor 的构造函数有一个 workQueue 参数可以传入任务队列。

设计方法:如果要实现一个优先级任务线程池的话,那可以考虑使用 PriorityBlockingQueue (优先级阻塞队列)作为任务队列。PriorityBlockingQueue 是一个支持优先级的无界阻塞队列,可以看作是线程安全的 PriorityQueue,两者底层都是使用小顶堆形式的二叉堆,即值最小的元素优先出队。不过,PriorityQueue 不支持阻塞操作。要想让 PriorityBlockingQueue 实现对任务的排序,传入其中的任务必须是具备排序能力的,方式有两种:

  • 提交到线程池的任务实现 Comparable 接口,并重写 compareTo 方法来指定任务之间的优先级比较规则。

  • 创建 PriorityBlockingQueue 时传入一个 Comparator 对象来指定任务之间的排序规则(推荐)。

存在的问题

  • PriorityBlockingQueue 是无界的,可能堆积大量的请求,从而导致 OOM。

  • 可能会导致饥饿问题,即低优先级的任务长时间得不到执行。

  • 由于需要对队列中的元素进行排序操作以及保证线程安全(并发控制采用的是可重入锁 ReentrantLock),因此会降低性能。

解决方法

  • 对于 OOM 这个问题的解决比较简单粗暴,就是继承PriorityBlockingQueue 并重写一下 offer 方法(入队)的逻辑,当插入的元素数量超过指定值就返回 false 。

  • 饥饿问题这个可以通过优化设计来解决(比较麻烦),比如等待时间过长的任务会被移除并重新添加到队列中,但是优先级会被提升。

  • 对于性能方面的影响,是没办法避免的,毕竟需要对任务进行排序操作。并且,对于大部分业务场景来说,这点性能影响是可以接受的。

PriorityQueue是queue系列中的一个集合

AQS

一.AQS 是什么

AQS(AbstractQueuedSynchronizer )就是一个抽象类,它定义了一套多线程访问共享资源的同步器框架,许多同步类的实现都依赖于它,如常用的 ReentrantLock。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
}

二.AQS 的原理

2.1.基本框架

AQS 的核心思想是对于共享资源,维护一个双端队列来管理线程,队列中的线程依次获取资源,获取不到的线程进入队列等待,直到资源释放,队列中的线程依次获取资源。 AQS的基本框架如图所示:

1.资源state

state变量表示共享资源,通常是int类型。

访问方法:state类型用户无法直接进行修改,而需要借助于AQS提供的方法进行修改,即getState()、setState()、compareAndSetState()等。

访问类型:AQS定义了两种资源访问类型:

  • 独占(Exclusive):一个时间点资源只能由一个线程占用;

  • 共享(Share):一个时间点资源可以被多个线程共用。

2.CLH双向队列

CLH 队列是一种基于逻辑队列非线程饥饿的自旋公平锁,队列中的每个节点都表示一个线程,处于头部的线程获取资源,而队列中的其它线程都在会通过自旋等待不断检查前驱节点的状态变化,当前驱节点释放了锁并修改了状态后,即可成功获取到锁。

  • 基于逻辑队列是指使用一个数据结构来维护线程的等待顺序,从而实现对共享资源的有序访问。
  • 非线程饥饿是指在多线程并发环境下,所有的线程都有机会获取共享资源,避免出现某个线程长时间无法获得资源的情况。

节点结构的源码如下所示

static final class Node {
    // 模式,分为共享与独占
    // 共享模式
    static final Node SHARED = new Node();
    // 独占模式
    static final Node EXCLUSIVE = null;        
    // 结点状态
    // CANCELLED,值为1,表示当前的线程被取消
    // SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark
    // CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中
    // PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行
    // 值为0,表示当前节点在sync队列中,等待着获取锁
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;        

    // 结点状态
    volatile int waitStatus;        
    // 前驱结点
    volatile Node prev;    
    // 后继结点
    volatile Node next;        
    // 结点所对应的线程
    volatile Thread thread;        
    // 下一个等待者
    Node nextWaiter;
    
    // 结点是否在共享模式下等待
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    
    // 获取前驱结点,若前驱结点为空,抛出异常
    final Node predecessor() throws NullPointerException {
        // 保存前驱结点
        Node p = prev; 
        if (p == null) // 前驱结点为空,抛出异常
            throw new NullPointerException();
        else // 前驱结点不为空,返回
            return p;
    }
    
    // 无参构造方法
    Node() {    // Used to establish initial head or SHARED marker
    }
    
    // 构造方法
        Node(Thread thread, Node mode) {    // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }
    
    // 构造方法
    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

Node 的方法和属性值如下图所示。其中:

  • waitStatus表示当前节点在队列中的状态;

  • thread表示当前节点表示的线程;

  • prev和next分别表示当前节点的前驱节点和后继节点;

  • nextWaiterd当存在CONDTION队列时,表示一个condition状态的后继节点。

waitStatus 结点的等待状态是一个整数值,具体的参数值和含义如下所示: 

  • 1-CANCELLED,表示节点获取锁的请求被取消,此时节点不再请求资源;

  • 0,是节点初始化的默认值;

  • -1-SIGNAL,表示线程做好准备,等待资源释放;

  • -2-CONDITION,表示节点在condition等待队列中,等待被唤醒而进入同步队列;

  • -3-PROPAGATE,当前线程处于共享模式下的时候会使用该字段。

2.2 AQS模板

AQS 提供一系列结构作为一个完整的模板,自定义的同步器只需要实现资源的获取和释放就可以,而不需要考虑底层的队列修改、状态改变等逻辑。 使用AQS实现一个自定义同步器,需要实现的方法:

  • isHeldExclusively():该线程是否独占资源,在使用到condition的时候会实现这一方法;

  • tryAcquire(int):独占模式获取资源的方式,成功获取返回true,否则返回false;

  • tryRelease(int):独占模式释放资源的方式,成功获取返回true,否则返回false;

  • tryAcquireShared(int):共享模式获取资源的方式,成功获取返回true,否则返回false;

  • tryReleaseShared(int):共享模式释放资源的方式,成功获取返回true,否则返回false;

一般来说,一个同步器是资源独占模式或者资源共享模式的其中之一,因此 tryAcquire(int) 和tryAcquireShared(int) 只需要实现一个即可,tryRelease(int) 和 tryReleaseShared(int) 同理。 但是同步器也可以实现两种模式的资源获取和释放,从而实现独占和共享两种模式。

2.3 ReentrantLock

以 ReentrantLock 为例,state 初始值为 0,表示未锁定状态。A 线程 lock() 时,会调用 tryAcquire() 独占该锁并将 state+1 。此后,其他线程再 tryAcquire() 时就会失败,直到 A 线程 unlock() 到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。

虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系

2.4 面试问题模拟

AQS是接口吗?有哪些没有实现的方法?看过相关源码吗? 

三.源码分析

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

真滴book理喻

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值