0% found this document useful (0 votes)
6 views17 pages

Java SE

Java有8种基本数据类型及其对应的包装类,基本类型存储在栈中,而包装类型存储在堆中。文档还阐述了深拷贝与浅拷贝的区别、==与equals()的使用差异、hashCode()的重要性、抽象类与接口的区别,以及面向对象的三大特征。最后,介绍了字符串的不可变性、集合的使用场景与实现,以及HashMap的底层数据结构和扩容机制。

Uploaded by

reiwaszzz
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
6 views17 pages

Java SE

Java有8种基本数据类型及其对应的包装类,基本类型存储在栈中,而包装类型存储在堆中。文档还阐述了深拷贝与浅拷贝的区别、==与equals()的使用差异、hashCode()的重要性、抽象类与接口的区别,以及面向对象的三大特征。最后,介绍了字符串的不可变性、集合的使用场景与实现,以及HashMap的底层数据结构和扩容机制。

Uploaded by

reiwaszzz
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

JavaSE

Java基础
Java的基本数据类型

Java 中有 8 种基本数据类型,分别为:

6 种数字类型:

4 种整数型: byte 、 short 、 int 、 long

2 种浮点型: float 、 double

1 种字符类型: char

1 种布尔型: boolean 。

这八种基本类型都有对应的包装类分别为: Byte 、 Short 、 Integer 、 Long 、


Float 、 Double 、 Character 、 Boolean

int占4个字节, char占2个字节, float占4个字节, doule占8个字节, byte占1个字节

java定义了boolean数据类型,在编译之后都使用Java 虚拟机中的int数据类型来代
替. 所以boolean类型占4个字节

基本类型和包装类型的区别?

包装类型不赋值就是 null ,而基本类型有默认值且不是 null 。

包装类型可用于泛型,而基本类型不可以。

基本数据类型存放在栈中。包装类型属于对象类型,几乎所有对象实例都存在于
堆中。

相比于对象类型, 基本数据类型占用的空间非常小。

自动装箱与拆箱

装箱:将基本类型用它们对应的引用类型包装起来;调用了 包装类的
valueOf() 方法

拆箱:将包装类型转换为基本数据类型;调用了 xxxValue() 方法
深拷贝和浅拷贝区别?什么是引用拷贝

浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,
如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地
址,也就是说拷贝对象和原对象共用同一个内部对象。

深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。

引用拷贝: 引用拷贝就是两个不同的引用指向同一个对象。

== 和 equals() 的区别

== 对于基本类型和引用类型的作用效果是不同的:

对于基本数据类型来说, == 比较的是值

对于引用数据类型来说, == 比较的是对象的内存地址

equals() 方法存在两种使用情况:

类没有重写 equals() 方法 :通过 equals() 比较该类的两个对象时,等价于通


过“==”比较这两个对象,使用的默认是 Object 类 equals() 方法。

类重写了 equals() 方法 :一般我们都重写 equals() 方法来比较两个对象中的


属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

String 中的 equals 方法是被重写过的,因为 Object 的 equals 方法是比较的


对象的内存地址,而 String 的 equals 方法比较的是对象的值。
hashCode() 有什么用?

hashCode() 的作用是获取哈希值。这个哈希值的作用是确定该对象在哈希表中的索
引位置 (可以快速找到所需要的对象)

Java用 hashcode() 和 equals() 判断是否为同一个对象

如果两个对象的 hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。

如果两个对象的 hashCode 值相等并且 equals() 方法也返回 true ,我们才认


为这两个对象相等

如果两个对象的 hashCode 值不相等,我们就可以直接认为这两个对象不相等。

重写equals为什么要重写hashcode?

因为java判断两个对象是否是相等的, 需要先比较hashcode是否一致, 如果hashcode


不一致那么就认为不相等.

如果没有重写hashcode, 那么两个相等的对象由于hashcode不相等, 就会被认为是不


相等的. 但是按照重写的equals规则, 他们应该是相等的.

在集合中, 如set集合去重中就会出现, 两个相等的对象放到set中都可以存在的怪象.

抽象类和接口的区别

抽象类和接口都不能直接实例化。如果要实例化,抽象类变量必须指向实现所有
抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。

抽象类要被子类继承,接口要被类实现。

接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现。

接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。

抽象方法要被实现,所以不能是静态的,也不能是私有的

抽象类是对事物的抽象,即对类抽象;接口是对行为抽象,即局部抽象。抽象类
对整体形为进行抽象,包括形为和属性。接口只对行为进行抽象。

抽象类是多个子类的父类,定义了子类大概的共性的东西,是一种模板式设计;
接口是一种形为规范,是一种辐射式设计。
面向对象的三⼤特征

1. 封装 为了提⾼代码的安全性,隐藏对象的内部细节,封装将对象的内部状态(字
段、属性)隐藏起来,并通过定义公共 的⽅法(接⼝)来操作对象,外部代码只
需要知道如何使⽤这些⽅法⽽⽆需了解内部实现。

2. 继承 允许⼀个类(⼦类)继承另⼀个类(⽗类)的属性和⽅法的机制。⼦类可以
重⽤⽗类的代码,并且可以通过添加新 的⽅法或修改(重写)已有的⽅法来扩展
或改进功能,提⾼了代码的可重⽤性和可扩展性

3. 多态 多态是指相同的操作或⽅法可以在不同的对象上产⽣不同的⾏为,通过⽅法
的重载和重写实现。多态允许以⼀致的 ⽅式处理不同类型的对象,提⾼了代码的
灵活性

说⼀说你对多态的理解

因为子类其实是⼀种特殊的⽗类,因此Java允许把⼀个⼦类对象直接赋给⼀个⽗
类引⽤变量,⽆须任何类型转换, 或者被称为向上转型,向上转型由系统⾃动完
成。

当把⼀个⼦类对象直接赋给⽗类引⽤变量时,例如 BaseClass obj = new


SubClass(); ,这个 编译时类型是 BaseClass ,⽽运⾏时类型是 obj 引⽤变量的
SubClass ,当运⾏时调⽤该引⽤变量的⽅法时,其⽅法⾏为总是表 现出⼦类⽅
法的⾏为特征,⽽不是⽗类⽅法的⾏为特征

这就可能出现:相同类型的变量、调⽤同⼀个⽅法时呈现 出多种不同的⾏为特
征,这就是多态。

方法的重载和重写有什么区别

1. 重载方法法的重载指的是在同⼀个类中,方法名相同但参数列表不同

2. 重写是在子类中重新定义⽗类中已有的方法,方法名和参数列表必须相同

String
String的不可变性

1. String类中包含一个数组, 储存数组的每一个字符: private final byte[]


value;

2. final数组, 地址不能改变, 导致长度不能改变

3. private, 数组中的内容不能改变
String直接赋值和new对象的区别

1 String s1 = "abc";
2 String s2 = new String("abc");
3
4 [Link](s1 == s2); //false
5 [Link]([Link](s2)); //true

String重写了equals, 所以它的equals是先比较对象的地址, 再比较里面的值.

String s1 = new String("abc");这句话创建了几个字符串对象?

会创建 1 或 2 个字符串。

如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建2个对象

一个是new的String对象

一个是char[]对应的常量池中的数据:"abc"

如果字符串常量池中不存在字符串对象“abc”的引用,那么会在堆中创建1个对象,
就是new的String对象.

String和StringBuffer和StringBuilder区别

String:字符串变量,private final修饰,不可变!

StringBuffer:字符串变量(线程安全,可变) 没有使用 final 和 private 关


键字修饰
StringBuilder:字符串变量(线程不安全,可变) 没有使用 final 和 private
关键字修饰

StringBuilder是StringBuffer的简易版,更快!

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指


针指向新的 String 对象。

StringBuffer 或 StringBuilder 每次都会对 StringBuffer 或 StringBuilder


对象本身进行操作,而不是生成新的对象并改变对象引用。

对于三者使用的总结:

1. 操作少量的数据: 适用 String

2. 单线程做大量字符串拼接操作: 适用 StringBuilder

3. 多线程做大量字符串拼接操作: 适用 StringBuffer

字符串拼接用“+” 还是 StringBuilder?

对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append()


方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。

不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会
创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。
StringBuilder 对象是在循环内部被创建的,这意味着每循环一次就会创建一个
StringBuilder 对象。

所以需要把new StringBuilder() 放在循环外部.

字符串常量池的作用了解吗?

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开


辟的一块区域,主要目的是为了避免字符串的重复创建

开发中使用的字符串很可能有大量的重复, 字符串常量池就可以将重复的字符串只保
存一份, 极大地节省了内存.
集合
集合的使用场景与实现
存取有序用List

ArrayList: 基于动态数组实现。底层使⽤数组作为存储结构。

LinkedList: 基于双向链表实现。

Vector: 线程安全版ArrayList, 底层用数组实现, 但是大量使用synchronized加


锁, 性能差, 已经不会使用了

希望自动去掉重复元素用Set

HashSet: 基于HashMap实现。底层使⽤⼀个数组和链表/红⿊树的结构来存储
元素, 无序无重复

TreeSet: 基于红⿊树实现。底层使⽤⾃平衡的⼆叉搜索树存储元素,可以自动
排序

通过唯一的key找value用map

HashMap: 基于哈希表实现。底层使⽤⼀个数组+链表/红⿊树的结构来存储键
值对。key无序无重复

TreeMap: 基于红⿊树实现, 红黑树是排序树, 可以对key自动排序

Hashtable: HashMap的线程安全版, 但是大量使用synchronized加锁, 性能差,


已经不会使用了

队列先进先出用Queue

LinkedList: 他也实现了Queue接口, 可以先进先出

ArrayDeque: 基于动态数组的双端队列。底层使⽤循环数组实现

ArrayList
ArrayList 与 LinkedList 的区别?

内部数据结构

ArrayList: 基于动态数组实现

LinkedList: 基于双向链表实现

遍历性能

ArrayList: ⽀持快速的随机访问和遍历,因为可以直接通过索引访问元素
LinkedList: 随机访问性能较差,因为必须从链表的头部或尾部开始遍历,直到
达到⽬标索引

插⼊和删除

ArrayList: 在末尾进⾏插⼊和删除操作是⾼效的,但在中间或开头插⼊和删除需
要移动元素,性能较差

LinkedList: 插⼊和删除元素的性能相对较好,特别是在链表中间或头尾插⼊和
删除元素时

内存空间占用:

ArrayList的空间浪费主要体现在底层数组需要为新元素预留一定的容量空间

LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的


空间(因为链表要存放后继和前驱节点)

使⽤场景

ArrayList: 适⽤于需要频繁随机访问元素,⽽对插⼊和删除操作要求不那么严格
的场景

LinkedList: 适⽤于需要频繁插⼊和删除操作,⽽对随机访问的需求较少的场景

(注: java的LinkedList有严重缺陷, 插入删除的性能其实很多场景比ArrayList要差)

ArrayList的扩容机制

1. 当调用ArrayList的无参构造new对象时, ArrayList对象的初始容量为0.

2. 当插入一个元素时, ArrayList会进行首次扩容(无参构造才会用首次扩容). 首次扩


容, ArrayList会创建一个长度为10的新数组, 替换掉原来的旧数组. 此时ArrayList
的长度就为10.

3. 当插入10个数据后, 要插入第11个数据时, 容量不足, 会触发第2次扩容. 第二次扩


容会扩容原容量的1.5倍, 之后扩容都是原容量的1.5倍.

ArrayList每次扩容都会创建新数组, 然后把数据转移到新数组中.
HashMap (常考)
HashMap和Hashtabe的区别

只关注一点就行了, 就是是否线程安全. 其他不用记!

HashMap线程不安全, Hashtabe线程安全

Hashtabe的线程安全是大量使用synchronized加锁, 性能差, 已经不会使用了

并发环境下建议使用ConcurrentHashMap, 它的性能更好.

结合ConcurrentHashMap那一节的内容答

HashMap底层数据结构, 1.7和1.8有何不同?

1.7的HashMap是哈希表, 数组 + 链表.

1.8的HashMap是数组 + (链表 或 红黑树)

HashMap什么时候进行扩容

HashMap 默认的初始化大小为16, 默认负载因子为0.75.

当hashmap中的元素个数超过数组大小*负载因子(loadFactor)时,就会进行数
组扩容.

之后每次扩充,容量变为原来的 2 倍

HashMap为什么要使用红黑树, 为啥不用平衡二叉树

当某个位置, Hash冲突严重, 则链表的长度会很长. 那么查找的时候依次比较, 效


率会很低 O(n).

将链表转为红黑树, 因为红黑树是排序树, 查找效率一般是O(logN), 使用红黑树就


提高了查找效率.

平衡二叉树追求绝对平衡,每次插入新节点之后需要旋转的次数不能预知, 自平
衡效率低

红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差
不大的情况下,自平衡的效率高
什么时候会树化

要满足两个条件

链表长度超过树化阈值8

数组长度大于等于64

当链表长度超过8时, 若数组长度小于64, 则会对数组进行扩容, 然后二次哈希的值


就会变, 此时链表的部分值就会重新分配到数组其他位置. 二次分配后链表长度未
必大于8, 所以必须同时满足两个条件才会树化.

为何一上来不树化

刚开始链表的长度可能只有三四个, 如果此时树化, 那么树化后查询的效率和短链


表差不多. 所以, 在短链表的情况下, 树化意义不大.

而且链表的节点是Node, 红黑树的节点为TreeNode. TreeNode的内存占用大于


Node.

为何树化阈值为8

红黑树是为了防止链表超长时性能下降,树化应当是偶然情况. 长度超过8的链表
出现几率非常小, 选择8就是为了让树化几率足够小

hash表的查找, 更新的时间复杂度是O(1). 而红黑树的查找, 更新的时间复杂度是


O(log2n), TreeNode占用空间也比普通Node的大,所以如非必要,尽量还是使
用链表。

索引如何计算? hashCode有了, 为何还有hash()方法? 数组容量为


何是2^n?

索引计算方式

对任何一个对象调用其hashCode()方法会获得其原始hash值.

对原始hash值再调用HashMap的hash方法进行二次hash, 获取到二次hash
值.

二次hash值对数组容量进行取余操作获取到存放的数组下标.

为何需要二次hash?

二次hash是为了让hash值分布更加均匀, 减少hash冲突, 从而使链表更短, 因


此也就提升了查找效率.
数组容量为何是2^n?

计算索引时,如果是2的n次方可以使用位与运算代替取模运算, 效率更高

数组容量为质数会使hash值分布均匀, 但是2^n计算索引的效率更高.

HashMap的put()方法流程

1.8的put流程

1. HashMap 是懒惰创建数组的,首次使用才创建数组

2. 计算索引(桶下标)

3. 如果桶下标还没人占用,创建Node放入数据后返回

4. 如果桶下标已经有人占用

1. 已经是TreeNode走红黑树的添加或更新逻辑

2. 是普通Node,走链表的添加或更新逻辑. 如果链表长度超过树化阈值,走树化
逻辑

5. 返回前检查容量是否超过阈值, 一旦超过进行扩容.

1. 扩容时, 先将新的数据放进数组, 然后创建新的数组, 再将旧数组元素迁移到


新数组

1.8和1.7的put流程不同之处

链表插入节点时, 1.7是头插法, 1.8是尾插法

1.8有判断链表长度, 树化的逻辑. 1.7没有.

负载因子为何是0.75

0.75是在占用空间和查询时间中取得了比较好的平衡. (扩容阈值=数组大小*负载
因子)

大于0.75, 冲突增加了, 数组空间就节省了, 但是链表就会比较长, 影响性能.

若负载因子为1, 则16*1 = 16, 只有当存满16个元素之后, 才会扩容. 节省空


间, 影响性能.

小于0.75, 冲突减少了, 链表会比较短, 数组扩容会很频繁, 空间占用增多.

若负载因子为0.25, 则16*0.25=4, 当元素个数大于4个, 就会扩容. 浪费空间,


提高性能
多线程操作HashMap会出现什么问题

知道hashMap是线程不安全即可. 具体并发性会出现什么问题了解即可.

扩容死链 (1.7)

扩容的时候线程切换, 两个线程都要进行扩容.

因为1.7是头插法, 进行数组扩容的时候, 需要链表迁移. 在并发环境下, 会出现


循环链表, 造成扩容死链问题.

数据错乱 (1.7, 1.8)

两个线程都要放入一个新数据 (两个数据索引一致, 且该索引下无链表)

ConcurrentHashMap
对比
与HashMap的区别是什么?

ConcurrentHashMap是HashMap的升级版,HashMap是线程不安全的,而
ConcurrentHashMap是线程安全。而其他功能和实现原理和HashMap类似

与Hashtable的区别是什么?

Hashtable也是线程安全的,整个Hashtable对应一把锁,同一时刻,只能有一
个线程操作它, 并发性低

1.7的ConcurrentHashMap使用分段锁, 也就是Segment+ HashEntry数组+链表


的结构,相当于将数组切分为多个Segment. 每个Segment对应一把锁,如果多
个线程访问不同的Segment, 则不会冲突.

1.8开始 ConcurrentHashMap将链表的每个头节点或者红黑树的根节点作为锁
(锁桶),如果多个线程访问的头节点不同,则不会冲突. 也就是说, 只要没有hash
冲突, 多个线程就可以同时访问ConcurrentHashMap.
JDK8的ConcurrentHashMap和JDK7的ConcurrentHashMap有什么区别?

1. JDK8中新增了红黑树

2. JDK7中使用的是头插法,JDK8中使用的是尾插法

3. JDK7中使用了分段锁,而JDK8中没有使用分段锁, 而是锁住链表或者红黑树的头
结点. JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度
是数组的大小,并发度更大

4. JDK7中使用了ReentrantLock,JDK8中没有使用ReentrantLock了,而使用了
Synchronized

5. JDK7中的扩容是每个Segment内部进行扩容,不会影响其他Segment,而JDK8
中的扩容和HashMap的扩容类似,只不过支持了多线程扩容,并且保证了线程
安全

特性
ConcurrentHashMap是如何保证并发安全的?

JDK7中ConcurrentHashMap是通过ReentrantLock+CAS+分段思想来保证的并发安
全的,ConcurrentHashMap的put方法会通过CAS的方式,把一个Segment对象存到
Segment数组中,一个Segment内部存在一个HashEntry数组,相当于分段的
HashMap,Segment继承了ReentrantLock,每段put开始会加锁。

JDK8中的ConcurrentHashMap为什么使用synchronized来进行加锁?

在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且


synchronized 有多种锁状态,会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步
步转换。

JDK7中使用了分段锁,所以对于一个ConcurrentHashMap对象而言,分了几段
就得有几个ReentrantLock对象,表示得有对应的几把锁。ReentrantLock是一
个对象, 而synchronized是一个关键字, 当Segment段比较多时ReentrantLock对
象也比较多, 那么就会占用很多内存, 而jdk1.8使用synchronized关键字来加锁就
会更节省内存.
反射
聊聊反射

反射机制是在运行时,能够动态获取类的所有属性和方法;动态调用对象任意方法,
动态的创建对象

但是反射的性能较差

多线程
聊聊线程和进程

进程是程序的一次执行过程,是系统运行程序的基本单位,是操作系统分配资源的最
小单位. 一个进程会有一个主线程

在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程

而 main 函数所在的线程就是这个进程中的一个线程,也称主线程

线程是一个比进程更小的执行单位, 一个进程在其执行的过程中可以产生多个线程

线程共享进程的堆和方法区资源

每个线程有自己的程序计数器、虚拟机栈和本地方法栈

线程切换的代码比进程小得多,线程也被称为轻量级进程

Java创建线程有⼏种⽅式

常规回答

继承Thread类, 重写run方法

实现 Runnable 接⼝

实习Callable接口

通过线程池创建

亮眼回答

Java创建线程有且只有一种方式, 就是继承Thread类重写run方法, 调用Thread类


的start方法

实现Runnable和Callable的还是要把实现类对象传入Thread类中, 调用Thread类
的start方法. 所以本质创建线程还是依靠Thread类中的start方法.
实现Runnable和Callable实际上是创建了一个线程任务. 然后调用Thread类中的
start方法, start方法调用start0方法, start0是一个本地方法, 由C/C++编写, 用来
进行系统调用创建新线程, 然后用新线程来执行线程任务.

这也说明了为何实现Runnable或者Callable后调用run方法不能开启新线程, 是因
为开启新线程本质上需要调用Thread的start0方法.

什么是死锁

多线程争抢资源, 只有得到资源才能继续执行.

但是每个线程都持有一部分资源, 都等待对方释放资源.

多个线程互相僵持, 导致都无法运行

关于死锁几大调整, 死锁检测, 避免等在操作系统中查看

线程池
ThreadPoolExecutor构造函数

1 ThreadPoolExecutor(int corePoolSize,
2 int maximumPoolSize,
3 long keepAliveTime,
4 TimeUnit unit,
5 BlockingQueue<Runnable> workQueue,
6 ThreadFactory threadFactory,
7 RejectedExecutionHandler handler)

corePoolSize 指定线程池的核心线程数(必须大于0) , 核心线程就是一直在线程


池里的长久存活的线程

maximumPoolSize 指定线程池中的最大线程数(最大线程数>核心线程数), 临时
线程用完销毁

keepAliveTime 指定临时线程空闲时的存活时间

unit 指定临时线程存活时间的单位

workQueue 指定任务队列 (不能为null). 当提交的任务数超过核心线程数后, 再提


交的任务就存放在工作队列

threadFactory 指定哪个线程工厂创建线程 (不能为null)

handler 指定线程忙, 任务队列满的时候, 新任务来了怎么办 (不能为null)


临时线程什么时候创建啊?

新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线
程,此时才会创建临时线程。

这样是最大限度避免创建线程

什么时候会开始拒绝任务?

核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任
务拒绝。

提交一个新任务到线程池时,具体的执行流程 / 线程池的工作流程

当我们提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任

当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队

当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)
个数量的线程来 执行任务,如果任务处理完成,maximumPoolSize-
corePoolSize额外创建的线程等待 keepAliveTime之后被自动销毁

如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝
策略对应处理

线程池线程数如何设置?

CPU密集型: 任务需要大量计算, 很少阻塞, CPU一直处于忙碌状态. CPU核数 + 1

IO密集型: 任务需要频繁的IO操作(与磁盘, 网络交互), CPU经常等待IO完成. CPU


核数 * 2

[Link]().availableProcessors(); 获取CPU最大核心数
Executors创建线程池会存在什么问题

Executors有 newCachedThreadPool() newFixedThreadPool(int nThreads)


newSingleThreadExecutor() newScheduledThreadPool(int corePoolSize)
几大方法创建线程池.

Executors用起来比较方便, 但是在大型分布式系统中直接使用会产生一些问题.

newFixedThreadPool() 和 newSingleThreadExecutor() 固定线程数量的线程池


和单个线程的线程池, 线程数量不会溢出, 但是任务队列有可能溢出. 允许的任务队列
长度为Integer.MAX_VALUE, 可能堆积大量任务, 导致OOM.

newCachedThreadPool() 和 newScheduledThreadPool() 线程数量随着任务增加


而增加, 线程数量有可能溢出. 线程数最大为Integer.MAX_VALUE, 可能会创建大量线
程, 从而导致oom.

阿里巴巴开发手册中强制规定, 线程池不允许使用Executors创建, 而是通过


ThreadPoolExecutor方式创建, 这样可以使编程者更加明确线程池运行规则, 避免资
源耗尽.

You might also like