Java最新面试200题(全网最全、最细、附答案)
1、Oracle JDK 和 OpenJDK 的对比
- Oracle JDK版本将每三年发布一次,而OpenJDK版本每三个月发布一 次;
- OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是 OpenJDK的一个实现,并不是完全开源的;
- Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎 相同,但Oracle JDK有更多的类和一些错误修复。因此,如果您想开发企 业/商业软件,我建议您选择Oracle JDK,因为它经过了彻底的测试和稳 定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程 序崩溃的问题,但是,只需切换到Oracle JDK就可以解决问题;
- 在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的 性能;
- Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过 更新到最新版本获得支持来获取最新版本;
- Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。
2、 用最有效率的方法计算 2 乘以 8
2 << 3(左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次 方)。
3、 loat f=3.4;是否正确
不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于 下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转 换float f =(float)3.4; 或者写成 float f =3.4F;。
4、Java语言采用何种编码方案?有何特点?
Java语言采用Unicode编码标准,Unicode(标准码),它为每个字符制订了一 个唯一的数值,因此在任何的语言,平台,程序都可以放心的使用。
5、 final、finally、finalize区别
- final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
- finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
- finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。
6、this关键字的用法
this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this的用法在java中大体可以分为3种:
- 普通的直接引用,this相当于是指向当前对象本身。
- 形参与成员名字重名,用this来区分:
- 引用本类的构造函数
7、 super关键字的用法
super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
super也有三种用法:
1.普通的直接引用与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。
2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分
3.引用父类构造函数
-
super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。
-
this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。
8、break ,continue ,return 的区别及作用
- break 跳出总上一层循环,不再执行循环(结束当前的循环体)
- continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)
- return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、场景题、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
9、在 Java 中,如何跳出当前的多重嵌套循环
在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。例如:
1 public static void main(String[] args) {
2 ok:
3 for (int i = 0; i < 10; i++) {
4 for (int j = 0; j < 10; j++) {
5 System.out.println("i=" + i + ",j=" + j);
6 if (j == 5) {
7 break ok;
8 }
10 }
11 }
12 }
运行项目并下载源码java运行
10、面向对象三大特性
1、 面向对象的特征有哪些方面
面向对象的特征主要有以下几个方面:
-
抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行 为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是 什么。
-
封装 : 封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如 果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没 有提供给外界访问的方法,那么这个类也没有什么意义了。
-
继承 : 继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新 的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用 继承我们能够非常方便地复用以前的代码。
关于继承如下 3 点请记住:
1.子类拥有父类非 private 的属性和方法。
2.子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
3.子类可以用自己的方式实现父类的方法。
多态 : 所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出 的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到 底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的 方法,必须在由程序运行期间才能决定。
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口 (实现接口并覆盖接口中同一方法)。
其中Java 面向对象编程三大特性:封装 继承 多态
封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便 于使用,提高复用性和安全性。
继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以 增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通 过使用继承可以提高代码复用性。继承是多态的前提。
多态性:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提 高了程序的拓展性。
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口 (实现接口并覆盖接口中同一方法)。
方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重 写(override)实现的是运行时的多态性(也称为后绑定)。
一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是 哪个类中实现的方法,必须在由程序运行期间才能决定。运行时的多态是面向对 象精髓的东西,要实现多态需要做两件事:
- 方法重写(子类继承父类并重写父类中已有的或抽象的方法);
- 对象造型(用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据 子类对象的不同而表现出不同的行为)。
2、 什么是多态机制?Java语言是如何实现多态的?
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出 的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒 底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的 方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这 样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而 导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时 所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的 重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不 同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来 实现的,也就是我们所说的多态性
3、多态的实现
Java实现多态有三个必要条件:继承、重写、向上转型。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的 方法。
向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具 备技能调用父类的方法和子类的方法。
只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现 代码处理不同的对象,从而达到执行不同的行为。
对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类 对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但 是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
4、 面向对象五大基本原则是什么(可选)
-
单一职责原则SRP(Single Responsibility Principle)类的功能要单一,不能包罗万象,跟杂货铺似的。
-
开放封闭原则OCP(Open-Close Principle)一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。
-
里式替换原则LSP(the Liskov Substitution Principle LSP)子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~
-
依赖倒置原则DIP(the Dependency Inversion Principle DIP)高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的抽象是中国人,而不是你是xx村的。
-
接口分离原则ISP(the Interface Segregation Principle ISP)
设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有
打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的
多。
27、静态变量和实例变量区别
静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会 有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。
实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量 是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。
28、 什么是内部类?
在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内部类本身就是类的一个属性,与其他属性定义方式一致。
29、 内部类的分类有哪些
在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内 部类本身就是类的一个属性,与其他属性定义方式一致。
内部类可以分为四种:成员内部类、局部内部类、匿名内部类和静态内部类。
30、IO流
1、 java 中 IO 流分为几种?
按照流的流向分,可以分为输入流和输出流; 按照操作单元划分,可以划分为字节流和字符流; 按照流的角色划分为节点流和处理流。 Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼 此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类 中派生出来的。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符 输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输 出流。
2、 BIO,NIO,AIO 有什么区别?
简答
-
BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
-
NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过
Channel(通道)通讯,实现了多路复用。 -
AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO
,异步 IO 的操作基于事件和回调机制。
详细回答
-
BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
-
NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的 N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
-
AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。
31、 Files的常用方法都有哪些?
- Files. exists():检测文件路径是否存在。
- Files. createFile():创建文件。
- Files. createDirectory():创建文件夹。
- Files. delete():删除一个文件或目录。
- Files. copy():复制文件。
- Files. move():移动文件。
- Files. size():查看文件个数。
- Files. read():读取文件。
- Files. write():写入文件。
32、 在使用 HashMap 的时候,用 String 做 key 有什么好处?
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
二、集合
1、 什么是集合 ?
集合框架: 用于存储数据的容器。
集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。 任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算 法。
接口:表示集合的抽象数据类型。接口允许我们操作集合时不必关注具体实现, 从而达到“多态”。在面向对象编程语言中,接口通常用来形成规范。
实现:集合接口的具体实现,是重用性很高的数据结构。
算法:在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方 法,例如查找、排序等。这些算法通常是多态的,因为相同的方法可以在同一个 接口被多个类实现时有不同的表现。事实上,算法是可复用的函数。 它减少了程序设计的辛劳。
集合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要部 分上,而不是为了让程序能正常运转而将注意力于底层设计上。
通过这些在无关API之间的简易的互用性,使你免除了为改编对象或转换代码以 便联合这些API而去写大量的代码。 它提高了程序速度和质量。
集合的特点
集合的特点主要有如下两点:
- 对象封装数据,对象多了也需要存储。集合用于存储对象。
- 对象的个数确定可以使用数组,对象的个数不确定的可以用集合。因 为集合是可变长度的。
集合和数组的区别
- 数组是固定长度的;集合可变长度的。
- 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存 储引用数据类型。
- 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同 数据类型。
数据结构:就是容器中存储数据的方式。
对于集合容器,有很多种。因为每一个容器的自身特点不同,其实原理在于每个 容器的内部数据结构不同。
集合容器在不断向上抽取过程中,出现了集合体系。在使用一个体系的原则:参 阅顶层内容。建立底层对象。
使用集合框架的好处
- 容量自增长;
- 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质 量; 3
- 允许不同 API 之间的互操作,API之间可以来回传递集合;
- 可以方便地扩展或改写集合,提高代码复用性和可操作性。
- 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。
2、常用的集合类有哪些?
Map接口和Collection接口是所有集合框架的父接口:
- Collection接口的子接口包括:Set接口和List接口
- Map接口的实现类主要有:HashMap、TreeMap、Hashtable、 ConcurrentHashMap以及Properties等
- Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
- List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
3、List,Set,Map三者的区别?List、Set、Map 是否继 承自 Collection 接口?List、Map、Set 三个接口存取 元素时,各有什么特点?
Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、 List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是 collection的子接口。
Collection集合主要有List和Set两大接口
- List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重 复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
- Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素, 只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、 LinkedHashSet 以及 TreeSet。
Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不 要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元 素时,只要给出键对象,就会返回对应的值对象。
Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、 ConcurrentHashMap
4、集合框架底层数据结构
List
- Arraylist: Object数组
- Vector: Object数组
- LinkedList: 双向循环链表
Set
- HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
- LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基 于 Hashmap 实现一样,不过还是有一点点区别的。
- TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。) Map
- HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主 体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转 化为红黑树,以减少搜索时间 - LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是 基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面 结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。 同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
- HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为 了解决哈希冲突而存在的
- TreeMap: 红黑树(自平衡的排序二叉树)
5、哪些集合类是线程安全的?
- vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已 经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优 先考虑的。
- statck:堆栈类,先进后出。
- hashtable:就比hashmap多了个线程安全。
- enumeration:枚举,相当于迭代器。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、场景题、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
6、Java集合的快速失败机制 “fail-fast”?
是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作 时,有可能会产生 fail-fast 机制。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中 的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简 单的修改集合元素的内容),那么这个时候程序就会抛出ConcurrentModificationException 异常,从而产生fail-fast机制。
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount 的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测 modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出 异常,终止遍历。
解决办法:
- 在遍历过程中,所有涉及到改变modCount值得地方全部加上 synchronized。
- 使用CopyOnWriteArrayList来替换ArrayList
7、 怎么确保一个集合不能被修改?
可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。 示例代码如下:
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());
运行项目并下载源码java运行
- 1
- 2
- 3
- 4
- 5
8、 说一下 ArrayList 的优缺点
ArrayList的优点如下:
- ArrayList 底层以数组实现,是一种随机访问模式。
- ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
- ArrayList 在顺序添加一个元素的时候非常方便。
ArrayList 的缺点如下:
- 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
- 插入元素的时候,也需要做一次元素复制操作,缺点同上。
ArrayList 比较适合顺序添加、随机访问的场景。
9、 ArrayList 和 LinkedList 的区别是什么?
- 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
- 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
- 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
- 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
- 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。
补充:数据结构基础之双向链表
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
10、 ArrayList 和 Vector 的区别是什么?
- 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合
- 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而ArrayList 是非线程安全的。
- 性能:ArrayList 在性能方面要优于 Vector
- 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
- Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
- Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。
11、 插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述 ArrayList、Vector、LinkedList 的存储性能和特性?
- ArrayList、LinkedList、Vector底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢。
- Vector 中的方法由于加了 synchronized 修饰,因此 Vector是线程安全容器,但性能上较ArrayList差。
- LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以LinkedList插入速度较快。
12、 多线程场景下如何使用 ArrayList?
ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:
1List<String> synchronizedList = Collections.synchronizedList(list);
2synchronizedList.add("aaa");
3synchronizedList.add("bbb");
4
5for (int i = 0; i < synchronizedList.size(); i++) {
6System.out.println(synchronizedList.get(i));
7}
运行项目并下载源码java运行
- 1
- 2
- 3
- 4
- 5
- 6
- 7
13、 List 和 Set 的区别
- List , Set 都是继承自Collection 接口
- List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有ArrayList、LinkedList 和 Vector。
- Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是HashSet、LinkedHashSet 以及 TreeSet。
另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
Set和List对比
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变
14、说一下 HashSet 的实现原理?
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成。
15、 HashSet如何检查重复?HashSet是如何保证数据不可重复的?
- 向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。HashSet 中的add ()方法会使用HashMap 的put()方法。
- HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为 HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较 hashcode 再比较equals )。
16、 BlockingQueue是什么?
Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。
BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。
Java提供了集中 BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。
在 Queue 中 poll()和 remove()有什么区别?
- 相同点:都是返回第一个元素,并在队列中删除返回的对象。
- 不同点:如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。
17、 Map接口
1、说一下 HashMap 的实现原理?
HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap的数据结构: 在Java编程语言中, 基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
HashMap 基于 Hash 算法实现的:
-
当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
-
存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value 放入链表中
-
获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
-
理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)
2、HashMap在JDK1.7和JDK1.8中有哪些不同? HashMap的底层实现
在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。
JDK1.8之前
JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8之后
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
JDK1.7 VS JDK1.8 比较
JDK1.8主要解决或优化了一下问题:
-
resize 扩容优化
-
引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
-
解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。

3、 HashMap的put方法的具体流程?
- 计算哈希值:首先调用键(key)的hashCode()方法获取原始哈希值,接着对该值进行二次哈希处理(即(h = key.hashCode()) ^ (h >>> 16)),以此降低哈希冲突的出现几率。
- 存储位置:利用处理后的哈希值和哈希表的长度((n - 1) & hash)来确定键值对在数组中的具体位置。
- 处理空桶情况:要是指定位置为空,就会创建一个新的节点并将其放入该位置。
- 处理哈希冲突:若该位置已存在节点,就需要进一步处理:
- 链表节点:会遍历链表,若发现有相同的键,就更新其对应的值;若没有,则在链表尾部添加新节点。当链表长度达到 8 时,链表会转化为红黑树。
- 树节点:会在红黑树中查找是否存在相同的键,若有则更新值,没有则插入新节点。插入后若树的节点数量减少到 6,树会转回链表。
- 扩容操作:在插入元素之后,若哈希表的大小超过了阈值(负载因子乘以容量),哈希表的容量就会翻倍,并且所有元素会重新进行哈希计算并分布到新的位置。
下面是put方法的简化代码实现,能帮助你更好地理解整个流程:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 若哈希表为空或长度为0,则进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 若指定位置为空,直接插入新节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 若键已存在,记录该节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 若节点为树节点,在树中插入或更新
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 遍历链表
for (int binCount = 0; ; ++binCount) {
// 若到达链表尾部,插入新节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 若链表长度达到阈值,转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 若键已存在,记录该节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 若键已存在,根据条件更新值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 结构修改次数加1
++modCount;
// 若超过阈值,进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
运行项目并下载源码java运行
4、 HashMap是怎么解决哈希冲突的?
答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行;什么是哈希?
Hash,一般翻译为“散列”,也有直接音译为“哈希”的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
所有散列函数都有如下一个基本特性**:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同**。
什么是哈希冲突?
当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。
HashMap的数据结构
在Java中,保存数据有两种比较简单的数据结构:数组和链表数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突,这样我们就可以将拥有相同哈希值的对象(img)组织成一个链表放在hash值所对应的 bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化 hash()函数。
上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让 hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:
1static final int hash(Object key) {
2int h;
3return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
4}
运行项目并下载源码java运行
- 1
- 2
- 3
- 4
这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动);
5、 HashMap 的长度为什么是2的幂次方?
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。
这个算法应该如何设计呢?我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
那为什么是两次扰动呢?
答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性, 终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的。
6、 HashMap 与 HashTable 有什么区别?
- 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;
HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
-
效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
-
对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
-
初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2 的幂次方。
-
底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
-
推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。
7、如何决定使用 HashMap 还是TreeMap?
对于在Map中插入、删除和定位元素这类操作,HashMap是 好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。
8、 HashMap 和 ConcurrentHashMap 的区别
-
ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized 锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启了一种全新的方式实现,利用CAS算法。)
-
HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。
三、并发编程
1、并发编程的优缺点为什么要使用并发编程(并发编程的优点)
- 充分利用多核CPU的计算能力:通过并发编程的形式可以将多核CPU 的计算能力发挥到极致,性能得到提升
- 方便进行业务拆分,提升系统并发能力和性能:在特殊的业务场景下,先天的就适合于并发编程。现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。
2、并发编程有什么缺点
并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,
比如:内存泄漏、上下文切换、线程安全、死锁等问题。
3、并发编程三要素是什么?在 Java 程序中怎么保证多线程的运行安全?
并发编程三要素(线程的安全性问题体现在):
- 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么 全部执行成功要么全部执行失败。
- 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。 (synchronized,volatile)
- 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行 重排序)
出现线程安全问题的原因:
- 线程切换带来的原子性问题
- 缓存导致的可见性问题
- 编译优化带来的有序性问题
解决办法:
- JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
- synchronized、volatile、LOCK,可以解决可见性问题
- Happens-Before 规则可以解决有序性问题
4、并行和并发有什么区别?
- 并发:多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,从逻辑 上来看那些任务是同时执行。
- 并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上 的“同时进行”。
- 串行:有n个任务,由一个线程按顺序执行。由于任务、方法都在一个线程执行 所以不存在线程不安全情况,也就不存在临界区的问题。
做一个形象的比喻:
并发 = 两个队列和一台咖啡机。
并行 = 两个队列和两台咖啡机。
串行 = 一个队列和一台咖啡机。
5、什么是多线程,多线程的优劣?
多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个 不同的线程来执行不同的任务。
多线程的好处: 可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可 以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单 个程序创建多个并行执行的线程来完成各自的任务。
多线程的劣势:
- 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
- 多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
- 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
6、线程和进程区别 什么是线程和进程?
进程 :
一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进 程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进 程。
线程:
进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至 少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、场景题、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
进程与线程的区别 :
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process), 它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有 若干个线程,至少包含一个线程。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执 行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切 换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空 间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开 销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线 (线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻 量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空 间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个 线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控 制,两者均可并发执行
7、什么是上下文切换?
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任 意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的 策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就 会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存 自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在 每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换 对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一 项就是,其上下文切换和模式切换的时间消耗非常少。
8、守护线程和用户线程有什么区别呢?
守护线程和用户线程
- 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网 络的子线程等都是用户线程
- 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护 线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程 会随 JVM 一起结束工作
main 函数所在的线程就是一个用户线程,main 函数启动的同时在 JVM 内部 同时还启动了好多守护线程,比如垃圾回收线程。 比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线 程运行。而守护线程不会影响 JVM 的退出。
注意事项:
- setDaemon(true)必须在start()方法前执行,否则会抛出 IllegalThreadStateException 异常
- 在守护线程中产生的新线程也是守护线程
- 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算 逻辑
- 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清 理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守 护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语 句块可能无法被执行。
9、如何在 Windows 和 Linux 上查找哪个线程cpu利用率最高?
windows上面用任务管理器看,linux下可以用 top 这个工具看。
- 找出cpu耗用厉害的进程pid, 终端执行top命令,然后按下shift+p 查 找出cpu利用厉害的pid号
- 根据上面第一步拿到的pid号,top -H -p pid 。然后按下shift+p,查 找出cpu利用率厉害的线程号,比如top -H -p 1328
- 将获取到的线程号转换成16进制,去百度转换一下就行
- 使用jstack工具将进程信息打印输出,jstack pid号 > /tmp/t.dat,比 如jstack 31365 > /tmp/t.dat
- 编辑/tmp/t.dat文件,查找线程号对应的信息
10、 什么是线程死锁 ?
百度百科:死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资 源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推 进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进 程(线程)称为死锁进程(线程)。
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线 程被无限期地阻塞,因此程序不可能正常终止。
线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方 的资源,所以这两个线程就会互相等待而进入死锁状态。
形成死锁的四个必要条件是什么
- 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只 能被一个线程(进程)占用,直到被该线程(进程)释放
- 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对 已获得的资源保持不放。
- 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程 强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环 路(类似于死循环),造成永久阻塞
如何避免线程死锁
我们只要破坏产生死锁的四个条件中的其中一个就可以了。
破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源 需要互斥访问)。
破坏请求与保持条件
一次性申请所有的资源。
破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占 有的资源。
破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环 等待条件。
11、 创建线程有哪几种方式?
创建线程有四种方式:
- 继承 Thread 类;
- 实现 Runnable 接口;
- 实现 Callable 接口;
- 使用 Executors 工具类创建线程池继承 Thread 类
12、 线程的 run()和 start()有什么区别?
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的, run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。 start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。 start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待 run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
13、 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
14、 什么是 Callable 和 Future?
Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说Callable用于产生结果,Future 用于获取结果。
15、什么是 FutureTask?
FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。
16、说说线程的生命周期及五种基本状态?
网上对线程状态的描述很多,有5种,6种,7种,都可以接受。
5中状态一般是针对传统的线程状态来说(操作系统层面):

Java中给线程准备的6种状态:

- NEW:Thread对象被创建出来了,但是还没有执行start方法。
- RUNNABLE:Thread对象调用了start方法,就为RUNNABLE状态(CPU调度/没有调度)
- BLOCKED、WAITING、TIME_WAITING:都可以理解为是阻塞、等待状态,因为处在这三种状态下,CPU不会调度当前线程
- BLOCKED:synchronized没有拿到同步锁,被阻塞的情况
- WAITING:调用wait方法就会处于WAITING状态,需要被手动唤醒
- TIME_WAITING:调用sleep方法或者join方法,会被自动唤醒,无需手动唤醒
- TERMINATED:run方法执行完毕,线程生命周期到头了
在Java代码中验证一下效果
NEW:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
});
System.out.println(t1.getState());
}
运行项目并下载源码java运行
RUNNABLE:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true){
}
});
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
运行项目并下载源码java运行
BLOCKED:
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread t1 = new Thread(() -> {
// t1线程拿不到锁资源,导致变为BLOCKED状态
synchronized (obj){
}
});
// main线程拿到obj的锁资源
synchronized (obj) {
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
}
运行项目并下载源码java运行
WAITING:
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
Thread t1 = new Thread(() -> {
synchronized (obj){
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
运行项目并下载源码java运行
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、场景题、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
TIMED_WAITING:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(500);
System.out.println(t1.getState());
}
运行项目并下载源码java运行
TERMINATED:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(1000);
System.out.println(t1.getState());
}
运行项目并下载源码java运行
17、 sleep() 和 wait() 有什么区别?
两者都可以暂停线程的执行
- 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
- 是否释放锁:sleep() 不释放锁;wait() 释放锁。
- 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
- 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
18、 如何停止一个正在运行的线程?
在java中有以下3种方法可以终止正在运行的线程:
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
- 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及 resume一样都是过期作废的方法。
- 使用interrupt方法中断线程。
19、 Java 中 interrupted 和 isInterrupted 方法的区别?
interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。
注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
isInterrupted:查看当前中断信号是true还是false
20、 notify() 和 notifyAll() 有什么区别?
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
notifyAll() 会唤醒所有的线程,notify() 只会唤醒一个线程。
notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。如何在两个线程间共享数据?在两个线程间共享变量即可实现共享。
21、 Java 线程数过多会造成什么异常?
-
线程的生命周期开销非常高消耗过多的
-
CPU资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU资源时还将产生其他性能的开销。
-
降低稳定性JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。
22、synchronized 的作用?
synchronized 可以修饰类、方法、变量。
在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。
庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
23、 synchronized、volatile、CAS 比较?
(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
(3)CAS 是基于冲突检测的乐观锁(非阻塞)
24、synchronized 和 Lock 有什么区别?
- 首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
- synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
- synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
25、 synchronized 和 ReentrantLock 区别是什么?
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
相同点:
两者都是可重入锁两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0 时才能释放锁。主要区别如下:
- ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
- ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
- ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
- 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized 操作的应该是对象头中 mark word
- Java中每一个对象都可以作为锁,这是synchronized实现同步的基础: 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的class对象
- 同步方法块,锁是括号里面的对象
26、 volatile 关键字的作用
在 Java 中,volatile 是一个类型修饰符,用于修饰变量,主要用于保证变量的可见性和禁止指令重排序。以下是其核心作用的详细解释:
1、保证可见性(Visibility)
当一个变量被声明为 volatile 时,它会保证以下两点:
- 对变量的写操作会立即刷新到主内存:普通变量的写操作可能会被缓存在 CPU 的寄存器或本地缓存中,其他线程无法立即看到最新值。
- 对变量的读操作会强制从主内存读取:普通变量的读操作可能会使用本地缓存中的旧值,而 volatile变量会跳过缓存,直接从主内存获取最新值。
示例场景:
public class VolatileExample {
private volatile boolean flag = false; // 使用volatile保证可见性
public void writer() {
flag = true; // 写操作立即刷新到主内存
}
public void reader() {
while (!flag) { // 读操作强制从主内存获取最新值
// 循环等待
}
System.out.println("Flag is now true");
}
}
运行项目并下载源码java运行
**解释:**若 flag 未被声明为 volatile,reader 线程可能永远看不到 writer 线程对 flag 的修改,导致无限循环。
2、禁止指令重排序(Happens-Before Ordering)
Java 编译器和处理器为了优化性能,可能会对指令进行重排序,但 volatile 变量会禁止特定类型的重排序。具体规则是:
- 写操作前的指令不会被重排序到写操作之后。
- 读操作后的指令不会被重排序到读操作之前。
示例场景:
public class Singleton {
private static volatile Singleton instance; // 使用volatile禁止重排序
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 禁止指令重排序
}
}
}
return instance;
}
}
运行项目并下载源码java运行
**解释:**若 instance 未被声明为 volatile,可能会出现指令重排序导致的问题:instance = new Singleton() 可能被分解为:
- 分配内存空间。
- 初始化对象(可能被重排序到后面)。
- 将引用指向内存空间(此时 instance 不为 null,但对象未完全初始化)。
若另一个线程在此时读取 instance,会得到一个未完全初始化的对象,导致程序崩溃。
3、不保证原子性(Atomicity)
volatile 不保证变量操作的原子性,即无法防止多个线程同时修改变量时的竞态条件。例如:
private volatile int count = 0;
public void increment() {
count++; // 非原子操作,即使count被声明为volatile
}
运行项目并下载源码java运行
- 1
- 2
- 3
- 4
- 5
**解释:**count++ 实际上是三个操作的组合(读取、加 1、写入),多个线程同时执行时仍可能导致数据不一致。若需要原子性,应使用 AtomicInteger 等原子类。
4、适用场景
- 状态标志:如示例中的 flag,用于线程间的简单通信。
- 双重检查锁(DCL):如单例模式中的 instance 变量。
- 需要禁止指令重排序的场景:确保变量初始化和赋值的顺序。
总结:
volatile 的核心作用是:
- 保证变量的可见性。
- 禁止指令重排序。
但它不能替代 synchronized 或原子类,仅适用于一个线程写、多个线程读的场景,或作为轻量级的同步机制。
27、什么是AQS?
在 Java 中,AQS(AbstractQueuedSynchronizer)是一个位于java.util.concurrent.locks包下的抽象类,它是构建锁和其他同步组件(如ReentrantLock、Semaphore、CountDownLatch)的基础框架。AQS 通过一个 FIFO 队列来管理线程的阻塞和唤醒,并使用一个int类型的状态变量(state)来表示同步状态,极大地简化了并发编程的实现。
核心原理
AQS 的核心思想是通过一个状态变量(state)和一个CLH 队列(FIFO 双向队列)来管理线程的同步状态:
1、 状态变量(state):
- 由子类通过getState()、setState()和compareAndSetState()方法操作。
- 例如,ReentrantLock用state表示锁的重入次数,Semaphore用state表示剩余许可数。
2、 CLH 队列:
- 当多个线程竞争同一个锁时,未获取到锁的线程会被封装成Node节点加入队列。
- 队列中的线程会按照 FIFO 顺序等待获取锁。
关键方法
AQS 提供了两类方法:
1、模板方法(由使用者调用):
- acquire(int arg):获取锁(独占模式)。
- release(int arg):释放锁(独占模式)。
- acquireShared(int arg):获取锁(共享模式)。
- releaseShared(int arg):释放锁(共享模式)。
2、钩子方法(由子类实现):
- tryAcquire(int arg):尝试获取锁(独占模式)。
- tryRelease(int arg):尝试释放锁(独占模式)。
- tryAcquireShared(int arg):尝试获取锁(共享模式)。
- tryReleaseShared(int arg):尝试释放锁(共享模式)。
- isHeldExclusively():判断当前线程是否独占锁。
独占模式 vs 共享模式
- 独占模式(如ReentrantLock):同一时间只允许一个线程持有锁。
- 共享模式(如Semaphore、CountDownLatch):允许多个线程同时持有锁。
AQS 的典型应用
1、ReentrantLock:
- 使用 AQS 的独占模式实现可重入锁。
- state表示锁的重入次数。
2、Semaphore:
- 使用 AQS 的共享模式实现信号量。
- state表示可用许可的数量。
3、CountDownLatch:
- 使用 AQS 的共享模式实现倒计时功能。
- state表示需要等待的操作数量。
4、ReentrantReadWriteLock:
- 使用 AQS 同时实现独占锁(写锁)和共享锁(读锁)。
示例:自定义同步器(基于 AQS)
下面是一个简单的自定义同步器示例,实现一个不可重入的独占锁:
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class SimpleLock {
private static class Sync extends AbstractQueuedSynchronizer {
// 锁被占用时状态为1,空闲时为0
@Override
protected boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int releases) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
private final Sync sync = new Sync();
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
}
运行项目并下载源码java运行
AQS 的优势
- 简化并发编程:开发者只需实现钩子方法,无需关心队列管理和线程阻塞 / 唤醒的底层细节。
- 高性能:基于CAS(Compare-and-Swap)操作实现无锁算法,减少线程上下文切换的开销。
- 可扩展性:支持自定义同步器,满足各种复杂的同步需求。
总结
AQS 是 Java 并发包的核心基石,通过状态变量和 FIFO 队列提供了一套通用的同步机制。理解 AQS 有助于深入掌握 Java 中的锁、信号量等并发工具的工作原理,并能灵活扩展自定义同步组件。
AQS内部结构和属性:

28、ThreadLocal 是什么?有哪些使用场景?
hreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。
核心作用:
ThreadLocal的主要作用是:
- 隔离线程间的数据:每个线程都拥有自己的独立变量副本,线程间互不干扰。
- 简化并发编程:避免在多线程环境下使用共享变量时需要进行的同步操作。
基本用法:
以下是ThreadLocal的基本使用示例:
public class ThreadLocalExample {
// 创建ThreadLocal变量,初始值为0
private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
// 创建两个线程
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
int value = threadLocal.get(); // 获取当前线程的副本值
threadLocal.set(value + 1); // 修改当前线程的副本值
System.out.println("Thread 1: " + threadLocal.get());
}
threadLocal.remove(); // 建议在finally块中调用,防止内存泄漏
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Thread 2: " + threadLocal.get());
threadLocal.set(threadLocal.get() - 1);
}
threadLocal.remove();
});
t1.start();
t2.start();
}
}
运行项目并下载源码java运行
输出示例(线程间数据隔离):
plaintext
Thread 1: 1
Thread 2: 0
Thread 1: 2
Thread 2: -1
Thread 1: 3
Thread 2: -2
运行项目并下载源码java运行
实现原理:
ThreadLocal的核心机制是:
1、每个线程维护一个ThreadLocalMap:
- Thread类中有一个ThreadLocalMap类型的成员变量threadLocals,用于存储该线程的所有局部变量。
- ThreadLocalMap的键是ThreadLocal实例本身,值是用户设置的具体值。
2、ThreadLocal作为键:
- 当调用ThreadLocal.get()时,会从当前线程的ThreadLocalMap中查找对应的 value。
- 不同线程的ThreadLocalMap是独立的,因此数据不会共享。
内存泄漏问题:
ThreadLocal可能导致内存泄漏的原因:
- 强引用链:Thread → ThreadLocalMap → Entry → value。
- 弱引用键:ThreadLocalMap的键是WeakReference,当外部没有强引用指向ThreadLocal时,键会被 GC 回收,但值(value)仍然被Entry持有。
- 线程复用:在线程池环境中,线程可能不会被销毁,导致ThreadLocalMap中的值一直存在。
解决方案:
- 使用后及时调用remove():在try-finally块中确保调用,避免残留值。
典型应用场景:
数据库连接管理、Session 管理、避免参数传递。
经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。
注意事项:
不可继承性:ThreadLocal中的值不能被子线程继承。若需要子线程继承父线程的值,可使用InheritableThreadLocal。
线程安全:ThreadLocal本身是线程安全的,但如果多个线程操作同一个ThreadLocal中的可变对象(如ArrayList),仍需同步。
总结:
ThreadLocal通过为每个线程创建独立的变量副本,实现了线程间的数据隔离,适用于需要避免共享变量的场景。使用时需注意内存泄漏问题,建议在每次使用后调用remove()。它是简化并发编程的重要工具,常见于数据库连接、事务管理、用户会话等场景。
29、 ThreadLocal造成内存泄漏的原因?
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key 为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完ThreadLocal方法后手动调用remove()方法。
ThreadLocal内存泄漏解决方案?
- 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
- 在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
30、 线程池有什么优点?
- 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
- 提高响应速度。可有效的控制 大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
- 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。
31、 线程池都有哪些状态?
- RUNNING:这是 正常的状态,接受新的任务,处理等待队列中的任务。 SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
- STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
- TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为
- TIDYING 状态时,会执行钩子方法 terminated()。
- TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
32、 Executors和ThreaPoolExecutor创建线程池的区别
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 各个方法的弊端:
- newFixedThreadPool 和 newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至 OOM。 - newCachedThreadPool 和 newScheduledThreadPool:
主要问题是线程数 大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM。
ThreaPoolExecutor创建线程池方式只有一种,就是走它的构造函数,参数自己指定。
33、ThreadPoolExecutor参数说明?
1、corePoolSize:核心线程数
* 核心线程会一直存活,及时没有任务需要执行
* 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
* 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
2、queueCapacity:任务队列容量(阻塞队列)
* 当核心线程数达到最大时,新任务会放在队列中排队等待执行
3、maxPoolSize:最大线程数
* 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
* 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
4、 keepAliveTime:线程空闲时间
* 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
* 如果allowCoreThreadTimeout=true,则会直到线程数量=0
5、allowCoreThreadTimeout:允许核心线程超时
6、rejectedExecutionHandler:任务拒绝处理器
* 两种情况会拒绝处理任务:
- 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
- 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
* 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
* ThreadPoolExecutor类有几个内部实现类来处理这类情况:
- AbortPolicy 丢弃任务,抛运行时异常
- CallerRunsPolicy 执行任务
- DiscardPolicy 忽视,什么都不会发生
- DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
* 实现RejectedExecutionHandler接口,可自定义处理器
运行项目并下载源码
34、 ThreadPoolExecutor执行顺序
线程池按以下行为执行任务
1. 当线程数小于核心线程数时,创建线程。
2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
3. 当线程数大于等于核心线程数,且任务队列已满
-1 若线程数小于最大线程数,创建线程
-2 若线程数等于最大线程数,抛出异常,拒绝任务
运行项目并下载源码
- 1
- 2
- 3
- 4
- 5
- 6
35、 并发工具
1、CycliBarriar 和 CountdownLatch 有什么区别?
CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的:
- CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。
- 调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行;
- CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,并且CyclicBarrier的构造方法可以传入 barrierAction,指定当所有线程都到达时执行的业务功能;
- CountDownLatch是不能复用的,而CyclicLatch是可以复用的。
2、 Semaphore
Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。
Semaphore有一个构造函数,可以传入一个 int 型整数 n,表示某段代码 多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个 synchronized 了。
Semaphore(信号量)-允许多个线程同时访问: synchronized 和
ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量) 可以指定多个线程同时访问某个资源。
3、 Exchanger
Exchanger是一个用于线程间协作的工具类,用于两个线程间交换数据。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。交换数据是通过 exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、场景题、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
36、常用的并发工具类有哪些?
- Semaphore(信号量)-允许多个线程同时访问: synchronized 和ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
- CountDownLatch(倒计时器): CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
- CyclicBarrier(循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到 后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉
- CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
四、Spinrg
1、什么是spring?
Spring是一个轻量级Java开发框架,最早由Rod Johnson创建,目的是为了解 决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个分层的 JavaSE/JavaEE full-stack(一站式)轻量级开源框架,为开发Java应用程序提 供全面的基础架构支持。Spring负责基础架构,因此Java开发者可以专注于应 用程序的开发。 Spring最根本的使命是解决企业级应用开发的复杂性,即简化Java开发。
Spring可以做很多事情,它为企业级开发提供给了丰富的功能,但是这些功能 的底层都依赖于它的两个核心特性,也就是依赖注入(dependency injection,DI)和面向切面编程(aspect-oriented programming, AOP)。
为了降低Java开发的复杂性,Spring采取了以下4种关键策略:
- 基于POJO的轻量级和最小侵入性编程;
- 通过依赖注入和面向接口实现松耦合;
- 基于切面和惯例进行声明式编程;
- 通过切面和模板减少样板式代码。
2、Spring框架的设计目标,设计理念,和核心是什么 ?
Spring设计目标:Spring为开发者提供一个一站式轻量级应用开发平台;
Spring设计理念:在JavaEE开发中,支持POJO和JavaBean开发方式,使应用 面向接口开发,充分支持OO(面向对象)设计方法;Spring通过IoC容器实现 对象耦合关系的管理,并实现依赖反转,将对象之间的依赖关系交给IoC容器, 实现解耦;
Spring框架的核心:IoC容器和AOP模块。通过IoC容器管理POJO对象以及他 们之间的耦合关系;通过AOP以动态非侵入的方式增强服务。 IoC让相互协作的组件保持松散的耦合,而AOP编程允许你把遍布于应用各层的 功能分离出来形成可重用的功能组件。
3、Spring的优缺点是什么?
优点
- 方便解耦,简化开发
Spring就是一个大工厂,可以将所有对象的创建和依赖关系的维护,交给 Spring管理。 - AOP编程的支持
Spring提供面向切面编程,可以方便的实现对程序进行权限拦截、运行监控等 功能。 - 声明式事务的支持
只需要通过配置就可以完成对事务的管理,而无需手动编程。 - 方便程序的测试
Spring对Junit4支持,可以通过注解方便的测试Spring程序。 - 方便集成各种优秀框架
Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架的直接支持 (如:Struts、Hibernate、MyBatis等)。 - 降低JavaEE API的使用难度
Spring对JavaEE开发中非常难用的一些API(JDBC、JavaMail、远程调用 等),都提供了封装,使这些API应用难度大大降低。
缺点
- Spring明明一个很轻量级的框架,却给人感觉大而全
- Spring依赖反射,反射影响性能
- 使用门槛升高,入门Spring需要较长时间
原文地址:https://blog.csdn.net/2401_89221867/article/details/159955638
免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!
