Java核心技术36讲笔记(3)线程池、CAS、堆的结构

一 线程池

1.1 常用线程池

Executors目前提供了5种不同的线程池创建配置:

  • newCachedThreadPool() ,它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过60秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue作为工作队列。(如果短时间内有大量任务进来,那么就会在短时间内创建大量线程。这点需要注意)
  • newFixedThreadPool(int nThreads) ,重用指定数目( nThreads )的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数nThreads。(看源码可得,线程数量一开始就固定了。除非有线程异常结束,此时数量会变小)
  • newSingleThreadExecutor() ,它的特点在于工作线程数目被限制为1 ,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。
  • newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize) ,创建的是个ScheduledExecutorService ,可以进行定时或周期性的工作调度,区别在于单个工作线程还是多个工作线程。
  • newWorkStealingPool(int paralelism) ,这是一个经常被 人忽略的线程池, Java 8才加入这个创建方法,其内部会构建ForkJoinPool ,利用Work-Stealing算法,并行地处理任务,不保证处理顺序

1.2 线程池工资原理

Executor框架

在这里插入图片描述

  • Executor是一个基础的接口,其初衷是将任务提交和任务执行细节解耦,只有一个方法
  • ExecutorService 则更加完善,不仅提供service的管理功能,比如shutdown等方法,也提供了更加全面的提交任务机制,如返回Future而不是void的submit方法。

ThreadPoolExecutor

在这里插入图片描述
部分概念在我之前写的文章中以及提及,这里就不记录了。https://blog.csdn.net/m0_38060977/article/details/104075577

  • 线程池的工作线程被抽象为静态内部类Worker ,基于AQS实现。
  • ThreadFactory提供上面所需要的创建线程逻辑。
  • 如果任务提交时被拒绝,比如线程池已经处于SHUTDOWN状态或者是线程池的工作队列和线程都不可用(满了),需要为其提供处理逻辑,Java标准库提供了类似ThreadPoolExecutor .AbortPolicy等默认实现,也可以按照实际需求自定义。

1.3 线程池的状态

在这里插入图片描述

1.4 实践中需要注意的问题

  1. 避免任务堆积。前面我说过newFixedThreadPool是创建指定数目的线程,但是其工作队列是无界的,如果工作线程数目太少,导致处理跟不上入队的速度,这就很有可能占用大量系统内存,甚至是出现OOM。诊断时,你可以使用jmap之类的工具,查看是否有大量的任务对象入队。
  2. 避免过度扩展线程。我们通常在处理大量短时任务时,使用缓存的线程池,比如在最新的HTTP/2 dlient API中,目前的默认实现就是如此。我们在创建线程池的时候,并不能准确预计任务压力有多大、数据特征是什么样子(大部分请求是1K、100K 还是1M以上? ) ,所以很难明确设定一个线程数目。
  3. 如果线程数目不断增长(可以使用jstack等工具检查) ,也需要警惕另外一种可能性,就是·线程泄漏,·这种情况往往是因为任务逻辑有问题,导致工作线程迟迟不能被释放。建议你排查下线程栈,很有可能多个线程都是卡在近似的代码处。
  4. **避免死锁等同步问题,**对于死锁的场景和排查,
  5. 尽量避免在使用线程池时操作ThreadLocal ,可能会导致用完的对象不被回收。

二 java中的部分CAS类

2.1 原子类型字段更新器AtomicXxxxFieldUpdater

可以用来更新对象的某个字段
参考:原子类型字段更新器AtomicXxxxFieldUpdater

2.2 LongAdder和AtomicLong

以空间换时间,在高并发时比AtomicLong的效率更高

2.3 AtomicStampedReference

AtomicReference存在ABA问题
参考:https://blog.csdn.net/zhaozhirongfree1111/article/details/72781758

三 堆结构

在这里插入图片描述

3.1 新生代

新生代是大部分对象创建和销毁的区域,在通常的 Java 应用中,绝大部分对象生命周期都是很短暂的。其内部又分为 Eden 区域,作为对象初始分配的区域;两个 Survivor,有时候也叫from、to 区域,被用来放置从 Minor GC 中保留下来的对象。(因为新生代所有的gc算法都是标记复制算法

TLAB

从内存模型而不是垃圾收集的角度,对 Eden 区域继续进行划分,Hotspot JVM 还有一个概念叫做 Thread Local Allocation Buffer(TLAB),据我所知所有 OpenJDK 衍生出来的JVM 都提供了 TLAB 的设计。这是 JVM 为每个线程分配的一个私有缓存区域,否则,多线程同时分配内存时,为避免操作同一地址,可能需要使用加锁等机制,进而影响分配速度,你可以参考下面的示意图。从图中可以看出,TLAB 仍然在堆上,它是分配在 Eden 区域内的。其内部结构比较直观易懂,start、end 就是起始地址,top(指针)则表示已经分配到哪里了。所以我们分配新对象,JVM 就会移动 top,当 top 和 end 相遇时,即表示该缓存已满,JVM 会试图`再从 Eden 里分配一块儿``
在这里插入图片描述

3.2 老年代

放置长生命周期的对象,通常都是从 Survivor 区域拷贝过来的对象。当然,也有特殊情况,我们知道普通的对象会被分配在 TLAB 上;如果对象较大,JVM 会试图直接分配在 Eden其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM 就会直接分配到老年代

3.3 永久代

这部分就是早期 Hotspot JVM 的方法区实现方式了,储存 Java 类元数据、常量池、Intern 字
符串缓存,在 JDK 8 之后就不存在永久代这块儿了。

JDK1.8中,永久代已经从java堆中移除,String直接存放在堆中,类的元数据存储在meta space中,meta space占用外部内存,不占用堆内存。可以说,在java8的新版本中,持久代已经更名为了元空间(meta space)

参考:https://www.cnblogs.com/yonyong/p/9334279.html

3.4 virtual区域

在 JVM 内部,如果Xms 小于 Xmx,堆的大小并不会直接扩展到其上限,也就是说保留的空间
(reserved)大于实际能够使用的空间(committed)。当内存需求不断增长的时候,JVM 会逐渐扩展新生代等区域的大小,所以 Virtual 区域代表的就是暂时不可用(uncommitted)的空间。

发布了107 篇原创文章 · 获赞 1 · 访问量 3937

猜你喜欢

转载自blog.csdn.net/m0_38060977/article/details/104936740