Java多线程3(补档)


ThreadLocal

ThreadLocal 了解吗

ThreadLocal是线程本地变量,只属于当前线程,其他线程无法获取这个变量,是隔离的。
,让每一个线程都有自己的专属本地变量

说说ThreadLocal 的原理

每个线程Thead对象具有一个自己的ThreadLocalMap对象,把线程信息放入到ThreadLocalMap对象中,同一个线程thread在任何地方都可以拿出来。

ThreadLocalMap对象的元素的key是ThreadLocal对象,value是需要存储的数据,可以具有多个Threadlocal对象(多个不同key)及对应的value数据。

ThreadLocal 提供 get 和 set 方法,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()set()方法。

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

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。

这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。

处理方法:

ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法

线程池

什么是线程池?

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

为什么要用线程池

池化技术,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

如何创建线程池

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

方式二:通过 Executor 框架的工具类 Executors 来创建。

可以创建多种类型的 ThreadPoolExecutor

  • FixedThreadPool : 该方法返回一个固定线程数量的线程池。
  • SingleThreadExecutor 该方法返回一个只有一个线程的线程池。
  • CachedThreadPool 该方法返回一个可根据实际情况调整线程数量的线程池。
  • ScheduledThreadPool :该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。

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

《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样更加明确线程池的运行规则,规避资源耗尽的风险

Executors 返回线程池对象的弊端如下:

  • FixedThreadPoolSingleThreadExecutor : 使用的是无界的 LinkedBlockingQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
  • CachedThreadPool :使用的是同步队列 SynchronousQueue, 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
  • ScheduledThreadPoolSingleThreadScheduledExecutor : 使用的无界的延迟阻塞队列DelayedWorkQueue,任务队列最大长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

线程池常见参数有哪些

/**
 * 用给定的初始参数创建一个新的ThreadPoolExecutor。
 */
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                          int maximumPoolSize,//线程池的最大线程数
                          long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                          TimeUnit unit,//时间单位
                          BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                          ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                          RejectedExecutionHandler handler//拒绝(饱和)策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                           ) {
    if (
    ......
}

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 线程池的核心线程数量。任务队列未达到队列容量时,最大可以同时运行的线程数量。
  • maximumPoolSize : 线程池的最大线程数。任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 任务队列。新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

ThreadPoolExecutor其他常见参数 :

  • keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
  • unit : keepAliveTime 参数的时间单位。
  • threadFactory :executor 创建新线程的时候会用到。
  • handler :饱和策略。

线程池的饱和策略有哪些

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:

  1. AbortPolicy(丢弃任务并抛出异常策略):丢弃新任务,抛出异常。
  2. CallerRunsPolicy(调用者运行策略):新任务会由提交任务的线程来执行
  3. DiscardOldestPolicy(丢弃队列最旧的任务策略):丢弃队列里最早加入的一个任务,添加新任务
  4. DiscardPolicy(丢弃任务不抛异常策略):不处理,丢弃掉。

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

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

不同的线程池会选用不同的阻塞队列,我们可以结合内置线程池来分析。

1) ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

2)LinkedBlockingQueue:一个基于链表结构的无界阻塞队列,此队列按 FIFO 排序元素

3)SynchronousQueue:一个不存储元素的阻塞队列。目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。

4)PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

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

  1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
  2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
  3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
  4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,按照饱和策略处理。

线程池中的的线程数一般怎么设置?需要考虑哪些问题

主要考虑下面几个方面:

1. 线程池中线程执行任务的性质:

计算密集型的任务比较占 cpu,一般线程数设置的大小 等于或者略微大于 cpu 的核数(N+1)

IO 型任务主要时间消耗在 IO 等待上,cpu 压力并不大,所以线程数一般设置较大(2N)

2. 内存使用率:

线程数过多和队列的大小都会影响此项数据,队列的大小应该通过前期计算线程池任务的条数,来合理的设置队列的大小,不宜过小,让其不会溢出,因为溢出会走拒绝策略,多少会影响性能,也会增加复杂度。

execute() 和 submit() 的区别是什么

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

  • submit() 方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get() 方法来获取返回值

线程池的生命周期

  1. 创建(Creation):线程池被创建,但还未开始执行任务。可以使用 Executors 类的静态方法或者 ThreadPoolExecutor 类的构造方法来创建线程池。
  2. 运行(Running):线程池接受任务并开始执行。
  3. 关闭(Shutting Down):线程池停止接受新的任务,并且继续执行已提交的任务。
  4. 已终止(Terminated):线程池中的所有任务都执行完毕,所有线程都已关闭。线程池进入最终的终止状态。

生命周期转换:

  • 创建 -> 运行:调用 execute()submit() 方法提交任务,线程池开始执行任务。
  • 运行 -> 关闭:调用 shutdown() 方法关闭线程池。线程池不再接受新的任务,但会继续执行已提交的任务,直到所有任务执行完毕。
  • 关闭 -> 已终止:当所有任务执行完毕后,线程池进入已终止状态。

线程池关闭后无法重新启动或添加新的任务,如果需要再次使用线程池,则需要重新创建一个新的线程池。

Future

Future 类有什么用

Future 类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。

在 Java 中,Future 类只是一个泛型接口,位于 java.util.concurrent 包下,主要包括下面这 4 个功能:

  • 取消任务;
  • 判断任务是否被取消;
  • 判断任务是否已经执行完成;
  • 获取任务执行结果。

简单理解:我有一个任务,提交给了 Future 来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后,我就可以 Future 那里直接取出任务执行结果。

CompletableFuture 类有什么用

Future 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 get() 方法为阻塞调用。

Java 8 引入CompletableFuture 类可以解决Future 的这些缺陷。还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。

AQS

AQS是什么

AQS 是抽象队列同步器。AbstractQueuedSynchronizer这个类在 java.util.concurrent.locks 包下面。

AQS 就是一个抽象类,主要用来构建锁和同步器。

说说AQS 的原理

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中。

CLH队列是一个虚拟的双向队列。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

AQS 使用 int 成员变量 state 表示同步状态,通过内置的 线程等待队列 来完成获取资源线程的排队工作。

state 变量由 volatile 修饰,用于展示当前临界资源的获锁情况。

// 共享变量,使用volatile修饰保证线程可见性
private volatile int state;

状态信息 state 可以通过getState()setState()和CAS进行操作。

说说Semaphore原理和作用

Semaphore (信号量)是共享锁的一种实现,可以用来控制同时访问特定资源的线程数量。

它默认构造 AQS 的 state 值为 permits,可以将 permits 的值理解为许可证的数量,只有拿到许可证的线程才能执行。

调用semaphore.acquire() ,线程尝试获取许可证,如果 state >= 0 的话,则表示可以获取成功。如果获取成功的话,使用 CAS 操作去修改 state 的值 state=state-1。如果 state<0 的话,则表示许可证数量不足。此时会创建一个 Node 节点加入阻塞队列,挂起当前线程。

调用semaphore.release(); ,线程尝试释放许可证,并使用 CAS 操作去修改 state 的值 state=state+1。释放许可证成功之后,同时会唤醒同步队列中的一个线程。被唤醒的线程会重新尝试去修改 state 的值 state=state-1 ,如果 state>=0 则获取令牌成功,否则重新进入阻塞队列,挂起线程。

说说CountDownLatch原理和作用

CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。

CountDownLatch 是共享锁的一种实现,它默认构造 AQS 的 state 值为 count

当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。

使用场景:

我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。

调用CountDownLatch对象的 await()方法,直到所有文件读取完之后,才会接着执行后面的逻辑。

说说CyclicBarrier原理和作用

CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。

CyclicBarrier可以让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

CyclicBarrier 内部通过一个 count 变量作为计数器,count 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减 1。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。

使用场景:

CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。

JMM

说说对 Java 内存模型的理解

Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。

比如说:所有的变量都存储在主内存中,每个线程有自己的工作内存,保存主内存副本拷贝和自己私有变量,不同线程不能访问工作内存中的变量。线程间变量值的传递需要通过主内存来完成。

不同的操作系统内存模型不同。如果直接复用操作系统层面的内存模型,就可能会导致同样一套代码换了一个操作系统就无法执行了。Java 语言是跨平台的,它需要自己提供一套内存模型以屏蔽系统差异。

Java 内存模型屏蔽掉了各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致性的内存访问效果

对于 Java 开发者说,不需要了解底层原理,直接使用并发相关的一些关键字和类即可开发出并发安全的程序。

JMM主要是为了简化多线程编程增强程序可移植性的


文章作者: Aiaa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Aiaa !
  目录