Java面试笔试题大汇总(最全+详细答案)

1 常用的集合类有哪些

Map接口和Collection接口是所有集合框架的父接口:

  1. Collection接口的子接口包括:Set接口和List接口

  2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及 Properties等

  3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等

  4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

2 ArrayList 和 LinkedList 的区别是什么?

  1. 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。

  2. 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。

  3. 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

  4. 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。

  5. 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全。

综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多 时,更推荐使用 LinkedList。LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

3 多线程场景下如何使用 ArrayList?

ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用,或者使用CopyOnWriteArrayList。(Vector效率低不考虑)

线程不安全示例:

 
  1. public class Demo {

  2. public static void main(String[] args) {

  3. List<String> list = new ArrayList<>();

  4. for (int i = 0; i < 200; i++) {

  5. new Thread(() -> {

  6. list.add(UUID.randomUUID().toString().substring(0, 8));

  7. list.forEach(System.out::println);

  8. }).start();

  9. }

  10. }

  11. }

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记及答案【点击此处即可】免费获取

4 HashSet是如何保证数据不重复的?

向HashSet中add()时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equals方法比较。HashSet中的add()方法会使用HashMap的put()方法。

HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key, 并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。

 
  1. private static final Object PRESENT = new Object();

  2. private transient HashMap<E,Object> map;

  3. public HashSet() {

  4.     map = new HashMap<>();

  5. }

  6. public boolean add(E e) {

  7.     // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值

  8.     return map.put(e, PRESENT)==null;

  9. }

5 HashSet与HashMap的区别

 

7 HashMap的底层实现,jdk1.7和1.8中有何区别? 

在Java中,保存数据有两种比较简单的数据结构:数组和链表。

  • 数组的特点是:寻址容易,插入和删除困难;

  • 链表的特点是:寻址困难,但插入和删除容易;

所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。

HashMap JDK1.8之前

JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

HashMap JDK1.8之后

相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)且数组长度大于等于64 时,将链表转化为红黑树,以减少搜索时间。(若红黑树节点个数小于6,则将红黑树转为链表)

 

红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质:

  • 性质1:每个节点要么是黑色,要么是红色。

  • 性质2:根节点是黑色。

  • 性质3:每个叶子节点(NIL)是黑色。

  • 性质4:每个红色结点的两个子结点一定都是黑色。

  • 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

JDK1.7 VS JDK1.8 比较

  1. resize 扩容优化

  2. 引入了红黑树,目的是避免单条链表过长而影响查询效率

  3. 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。

1.10 HashMap的put方法的具体流程?

 
  1. public V put(K key, V value) {

  2. return putVal(hash(key), key, value, false, true);

  3. }

  4. static final int hash(Object key) {

  5. int h;

  6. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

  7. }

  8. //实现Map.put和相关方法

  9. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

  10. boolean evict) {

  11. Node<K,V>[] tab; Node<K,V> p; int n, i;

  12. // 步骤①:tab为空则创建

  13. // table未初始化或者长度为0,进行扩容

  14. if ((tab = table) == null || (n = tab.length) == 0)

  15. n = (tab = resize()).length;

  16. // 步骤②:计算index,并对null做处理

  17. // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)

  18. if ((p = tab[i = (n - 1) & hash]) == null)

  19. tab[i] = newNode(hash, key, value, null);

  20. // 桶中已经存在元素

  21. else {

  22. Node<K,V> e; K k;

  23. // 步骤③:节点key存在,直接覆盖value

  24. // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等

  25. if (p.hash == hash &&

  26. ((k = p.key) == key || (key != null && key.equals(k))))

  27. // 将第一个元素赋值给e,用e来记录

  28. e = p;

  29. // 步骤④:判断该链为红黑树

  30. // hash值不相等,即key不相等;为红黑树结点

  31. // 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null

  32. else if (p instanceof TreeNode)

  33. // 放入树中

  34. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

  35. // 步骤⑤:该链为链表

  36. // 为链表结点

  37. else {

  38. // 在链表最末插入结点

  39. for (int binCount = 0; ; ++binCount) {

  40. // 到达链表的尾部

  41. //判断该链表尾部指针是不是空的

  42. if ((e = p.next) == null) {

  43. // 在尾部插入新结点

  44. p.next = newNode(hash, key, value, null);

  45. //判断链表的长度是否达到转化红黑树的临界值,临界值为8

  46. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

  47. //链表结构转树形结构

  48. treeifyBin(tab, hash);

  49. // 跳出循环

  50. break;

  51. }

  52. // 判断链表中结点的key值与插入的元素的key值是否相等

  53. if (e.hash == hash &&

  54. ((k = e.key) == key || (key != null && key.equals(k))))

  55. // 相等,跳出循环

  56. break;

  57. // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表

  58. p = e;

  59. }

  60. }

  61. //判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值

  62. if (e != null) {

  63. // 记录e的value

  64. V oldValue = e.value;

  65. // onlyIfAbsent为false或者旧值为null

  66. if (!onlyIfAbsent || oldValue == null)

  67. //用新值替换旧值

  68. e.value = value;

  69. // 访问后回调

  70. afterNodeAccess(e);

  71. // 返回旧值

  72. return oldValue;

  73. }

  74. }

  75. // 结构性修改

  76. ++modCount;

  77. // 步骤⑥:超过最大容量就扩容

  78. // 实际大小大于阈值则扩容

  79. if (++size > threshold)

  80. resize();

  81. // 插入后回调

  82. afterNodeInsertion(evict);

  83. return null;

  84. }

  1. 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向 ⑥,如果table[i]不为空,转向③;

  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

  4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5;

  5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

  6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

1.11 ConcurrentHashMap 具体实现?

JDK1.7

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段 数据时,其他段的数据也能被其他线程访问。

在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一 种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构 的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修 改时,必须首先获得对应的 Segment的锁。

Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

JDK1.8

在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保 证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲 突,就不会产生并发 , 效率得到提升

 1.12 try catch finally,try里有return,finally还执行么?

执行,并且finally的执行早于try里面的return
结论:

  1. 不管有没有出现异常,finally块中代码都会执行;
  2. 当try和catch中有return时,finally仍然会执行;
  3. finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前确定的;
  4. finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。

1.13 谈谈Java中的Exception和Error 

Java可抛出(Throwable)的结构分为三种类型:被检查的异常(CheckedException),运行时异常
(RuntimeException),错误(Error)

  • 运行时异常

定义:RuntimeException及其子类都被称为运行时异常。
特点:Java编译器不会检查它。也就是说,当程序中可能出现这类异常时,倘若既"没有通过throws声明抛出它",也"没有用try-catch语句捕获它",还是会编译通过。例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,failfast机制产生的ConcurrentModificationException异常。

  • 被检查异常

定义:Exception类本身,以及Exception的子类中除了"运行时异常"之外的其它子类都属于被检查异常。特点 : Java编译器会检查它。此类异常,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。例如,CloneNotSupportedException就属于被检查异常。当通过clone()接口去克隆一个对象,而该对象对应的类没有实现Cloneable接口,就会抛出CloneNotSupportedException异常。被检查异常通常都是可以恢复的。
如:
IOException
FileNotFoundException
SQLException

被检查的异常适用于那些不是因程序引起的错误情况,比如:读取文件时文件不存在引发的FileNotFoundException 。然而,不被检查的异常通常都是由于糟糕的编程引起的,比如:在对象引用时没有确保对象非空而引起的 NullPointerException 。

  • 错误

定义 : Error类及其子类。
特点 : 和运行时异常一样,编译器也不会对错误进行检查。当资源不足、约束失败、或是其它程序无法继续运行的条件发生时,就产生错误。程序本身无法修复这些错误的。例如,VirtualMachineError就属于错误。出现这种错误会导致程序终止运行。OutOfMemoryError、
ThreadDeath

2. IO、网络

2.1 Java中的IO流有哪些?

按照流的流向分,可以分为输入流和输出流;

按照操作单元划分,可以划分为字节流和字符流;

按照流的角色划分为节点流和处理流。

2.2 BIO,NIO,AIO 有什么区别?介绍一下NIO

BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。

NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用

AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。

NIO 主要有三大核心部分: Channel(通道) Buffer(缓冲区)Selector。传统 IO 基于字节流和字符流进行操作, 而 NIO 基于 Channel 和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。 Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。 NIO 和传统 IO 之间第一个最大的区别是, IO 是面向流的, NIO 是面向缓冲区的。

首先说一下 Channel,国内大多翻译成“通道”。 Channel 和 IO 中的 Stream(流)是差不多一个等级的。 只不过 Stream 是单向的,譬如:InputStream, OutputStream, 而 Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。NIO 中的 Channel 的主要实现有:

1. FileChannel
2. DatagramChannel
3. SocketChannel
4. ServerSocketChannel
这里看名字就可以猜出个所以然来:分别可以对应文件 IO、 UDP 和 TCP(Server 和 Client)。

Buffer,故名思意, 缓冲区,实际上是一个容器,是一个连续数组。 Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer

上面的图描述了从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送数据时,必须先将数据存入 Buffer 中,然后将Buffer 中的内容写入通道。服务端这边接收数据必须通过 Channel 将数据读入到 Buffer 中,然后再从 Buffer 中取出数据来处理 

 Selector,是 NIO 的核心类, Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。 

2.3 什么是TCP/IP和UDP,TCP与UDP区别

TCP/IP即传输控制/网络协议,是面向连接的协议,发送数据前要先建立连接(发送方和接收方的成 对的两个之间必须建立连接),TCP提供可靠的服务,也就是说,通过TCP连接传输的数据不会丢 失,没有重复,并且按顺序到达。

UDP它是属于TCP/IP协议族中的一种。是无连接的协议,发送数据前不需要建立连接,是没有可 靠性的协议。因为不需要建立连接所以可以在在网络上以任何可能的路径传输,因此能否到达目的 地,到达目的地的时间以及内容的正确性都是不能被保证的。

区别:

  1. TCP是面向连接的协议,发送数据前要先建立连接,TCP提供可靠的服务,也就是说,通过TCP连 接传输的数据不会丢失,没有重复,并且按顺序到达;

  2. UDP是无连接的协议,发送数据前不需要建立连接,是没有可靠性;

  3. TCP通信类似于要打个电话,接通了,确认身份后,才开始进行通信;

  4. UDP通信类似于学校广播,靠着广播播报直接进行通信。

  5. TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多;

  6. TCP是面向字节流的,UDP是面向报文的; 面向字节流是指发送数据时以字节为单位,一个数据 包可以拆分成若干组进行发送,而UDP一个报文只能一次发完。

  7. TCP首部开销(20字节)比UDP首部开销(8字节)要大 ,UDP 的主机不需要维持复杂的连接状态表

对某些实时性要求比较高的情况使用UDP,比如游戏,媒体通信,实时直播,即使出现传输错误也可以容忍;其它大部分情况下,HTTP都是用TCP,因为要求传输的内容可靠,不出现丢失的情况

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记及答案【点击此处即可】免费获取

2.4  从输入地址到获得页面的过程 ?

  1. 浏览器查询 DNS,获取域名对应的IP地址:具体过程包括浏览器搜索自身的DNS缓存、搜索操作系统的DNS缓存、读取本地的Host文件和向本地DNS服务器进行查询等。对于向本地DNS服务器进行查询,如果要查询的域名包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析 (此解析具有权威性);如果要查询的域名不由本地DNS服务器区域解析,但该服务器已缓存了此网 址映射关系,则调用这个IP地址映射,完成域名解析(此解析不具有权威性)。如果本地域名服务 器并未缓存该网址映射关系,那么将根据其设置发起递归查询或者迭代查询;

  2. 浏览器获得域名对应的IP地址以后,浏览器向服务器请求建立链接,发起三次握手;

  3. TCP/IP链接建立起来后,浏览器向服务器发送HTTP请求

  4. 服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;

  5. 浏览器解析并渲染视图,若遇到对js文件、css文件及图片等静态资源的引用,则重复上述步骤并向服务器请求这些资源;

  6. 浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面。

2.5  什么是TCP的三次握手

在网络数据传输中,传输层协议TCP是要建立连接的可靠传输,TCP建立连接的过程,我们称为三 次握手。

 第一次握手:Client将SYN置1,随机产生一个初始序列号seq发送给Server,进入SYN_SENT状 态;

第二次握手:Server收到Client的SYN=1之后,知道客户端请求建立连接,将自己的SYN置1,ACK 置1,产生一个acknowledge number=sequence number+1,并随机产生一个自己的初始序列 号,发送给客户端;进入SYN_RCVD状态;

第三次握手:客户端检查acknowledge number是否为序列号+1,ACK是否为1,检查正确之后将 自己的ACK置为1,产生一个acknowledge number=服务器发的序列号+1,发送给服务器;进入 ESTABLISHED状态;服务器检查ACK为1和acknowledge number为序列号+1之后,也进入 ESTABLISHED状态;完成三次握手,连接建立。

简单来说就是 :

  1. 客户端向服务端发送SYN

  2. 服务端返回SYN,ACK

  3. 客户端发送ACK

2.6 什么是TCP的四次挥手

在网络数据传输中,传输层协议断开连接的过程我们称为四次挥手

第一次挥手:Client将FIN置为1,发送一个序列号seq给Server;进入FIN_WAIT_1状态;

第二次挥手:Server收到FIN之后,发送一个ACK=1,acknowledge number=收到的序列号+1; 进入CLOSE_WAIT状态。此时客户端已经没有要发送的数据了,但仍可以接受服务器发来的数据。

第三次挥手:Server将FIN置1,发送一个序列号给Client;进入LAST_ACK状态;

第四次挥手:Client收到服务器的FIN后,进入TIME_WAIT状态;接着将ACK置1,发送一个 acknowledge number=序列号+1给服务器;服务器收到后,确认acknowledge number后,变为 CLOSED状态,不再向客户端发送数

2.7 http和https的区别?

其实HTTPS就是从HTTP加上加密处理(一般是SSL安全通信线路)+认证+完整性保护

区别:

  1. https需要拿到ca证书,需要钱的

  2. 端口不一样,http是80,https443

  3. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。

  4. http和https使用的是完全不同的连接方式(http的连接很简单,是无状态的;HTTPS 协议是 由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。)

 2.8 一次完整的HTTP请求所经历几个步骤?

  1. 建立TCP连接

  2. Web浏览器向Web服务器发送请求行

  3. Web浏览器发送请求头

  4. Web服务器应答

  5. Web服务器发送应答头

  6. Web服务器向浏览器发送数据

  7. Web服务器关闭TCP连接

2.9 常用HTTP状态码是怎么分类的,有哪些常见的状态码?

HTTP状态码表示客户端HTTP请求的返回结果、标识服务器处理是否正常、表明请求出现的错误等

常见状态码 :

 2.10 HTTP请求中GET与POST方式的区别?

1、GET请求:请求的数据会附加在URL之后,以?分割URL和传输数据,多个参数用&连接。URL的编码格式采用的是ASCII编码,而不是uniclde,即是说所有的非ASCII字符都要编码之后再传输。

POST请求:POST请求会把请求的数据放置在HTTP请求包的包体中。

因此,GET请求的数据会暴露在地址栏中,而POST请求则不会。

2、传输数据的大小

在HTTP规范中,没有对URL的长度和传输的数据大小进行限制。但是在实际开发过程中,对于GET,特定的浏览器和服务器对URL的长度有限制。因此,在使用GET请求时,传输数据会受到URL长度的限制。

对于POST,由于不是URL传值,理论上是不会受限制的,但是实际上各个服务器会规定对POST提交数据大小进行限制,Apache、IIS都有各自的配置。

3、安全性

POST的安全性比GET的高。比如,在进行登录操作,通过GET请求,用户名和密码都会暴露再URL上,因为登录页面有可能被浏览器缓存以及其他人查看浏览器的历史记录的原因,此时的用户名和密码就很容易被他人拿到了。除此之外,GET请求提交的数据还可能会造成Cross-site request frogery攻击

4、HTTP中的GET,POST都是在HTTP上运行的

3. 多线程

3.1 Java线程实现/创建的方式有哪几种?

  • 继承Thread类

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方
法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线
程,并执行 run()方法

 
  1. public class MyThread extends Thread {

  2. public void run() {

  3. System.out.println("MyThread.run()");

  4. }

  5. }

  6. MyThread myThread1 = new MyThread();

  7. myThread1.start();

  • 实现Runnable接口

如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个
Runnable 接口

 
  1. public class MyThread extends OtherClass implements Runnable {

  2. public void run() {

  3. System.out.println("MyThread.run()");

  4. }

  5. }

  6. //启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:

  7. MyThread myThread = new MyThread();

  8. Thread thread = new Thread(myThread);

  9. thread.start();

  • 使用Callable和Future创建线程

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。

1.call()方法可以有返回值

2.call()方法可以声明抛出异常

 

 
  1. public class Test1 {

  2. public static void main(String[] args) throws ExecutionException, InterruptedException {

  3. MyThread3 thread3 = new MyThread3();

  4. FutureTask<Integer> task = new FutureTask<Integer>(thread3);

  5. Thread thread = new Thread(task);

  6. thread.start();

  7. System.out.println("线程返回值:"+task.get());

  8. }

  9. }

  10. class MyThread3 implements Callable {

  11. @Override

  12. public Object call() throws Exception {

  13. return 100;

  14. }

  15. }

  • 基于线程池的方式 

线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销
毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

 
  1. public class MyThread4 {

  2. public static void main(String[] args) {

  3. ExecutorService threadPool = Executors.newFixedThreadPool(10);

  4. while (true) {

  5. // 提交多个线程任务,并执行

  6. threadPool.execute(new Runnable() {

  7. @Override

  8. public void run() {

  9. System.out.println(Thread.currentThread().getName()+":is running...");

  10. try {

  11. Thread.sleep(3000);

  12. } catch (InterruptedException e) {

  13. e.printStackTrace();

  14. }

  15. }

  16. });

  17. }

  18. }

  19. }

3.2 常见的4种线程池是哪些?

 Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而是一个执行线程的工具,真正的线程池接口是ExecutorService。

  • newSingleThreadExecutor

 
  1. public static ExecutorService newSingleThreadExecutor() {

  2.         return new FinalizableDelegatedExecutorService

  3.             (new ThreadPoolExecutor(1, 1,

  4.                                     0L, TimeUnit.MILLISECONDS,

  5.                                     new LinkedBlockingQueue<Runnable>()));

  6.     }

Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。适用于串行执行任务的场景,一个任务一个任务的执行,比如任务调度。

  • newFixedThreadPool
 
  1. public static ExecutorService newFixedThreadPool(int nThreads) {

  2.         return new ThreadPoolExecutor(nThreads, nThreads,

  3.                                       0L, TimeUnit.MILLISECONDS,

  4.                                       new LinkedBlockingQueue<Runnable>());

  5.     }

创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大
多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,
则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何
线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之
前,池中的线程将一直存在。

  • newCachedThreadPool
 
  1. public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {

  2.         return new ThreadPoolExecutor(0, Integer.MAX_VALUE,

  3.                                       60L, TimeUnit.SECONDS,

  4.                                       new SynchronousQueue<Runnable>(),

  5.                                       threadFactory);

  6.     }

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行
很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造
的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并
从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资
源。

  • newScheduledThreadPool

创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

 
  1. ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);

  2. scheduledThreadPool.schedule(new Runnable(){

  3. @Override

  4. public void run() {

  5. System.out.println("延迟三秒");

  6. }

  7. }, 3, TimeUnit.SECONDS);

  8. scheduledThreadPool.scheduleAtFixedRate(new Runnable(){

  9. @Override

  10. public void run() {

  11. System.out.println("延迟 1 秒后每三秒执行一次");

  12. }

  13. },1,3,TimeUnit.SECONDS);

注:

项目中创建多线程时,使用常见的三种线程池创建方式,单一、可变、定长都有一定问题,原因是 FixedThreadPool 和 SingleThreadExecutor 底层都是用LinkedBlockingQueue 实现的,这个队列最大长度为 Integer.MAX_VALUE,容易导致 OOM。所以实际生产一般自己通过 ThreadPoolExecutor 的 7 个参数,自定义线程池。

为什么不允许适用不允许 Executors.的方式手动创建线程池,如下图

 

 3.3 线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直“霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。

3.3.1 新建状态(New)

当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配
内存,并初始化其成员变量的值

3.3.2 就绪状态(Runnable)

当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和
程序计数器,等待调度运行。

3.3.3 运行状态(Running)

如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状

3.3.4 阻塞状态(Blocked)

阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。
直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状
态。阻塞的情况分三种:

  • 等待阻塞(o.wait->等待对列):

运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)中。

  • 同步阻塞(lock->锁池)

运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线
程放入锁池(lock pool)中。

  • 其他阻塞(sleep/join)

运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,
JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O
处理完毕时,线程重新转入可运行(runnable)状态。

3.3.5 线程死亡(Dead)

线程会以下面三种方式结束,结束后就是死亡状态。

  • 正常结束

run()或 call()方法执行完成,线程正常结束。

  • 异常结束

线程抛出一个未捕获的 Exception 或 Error。

  • 调用 stop

直接调用该线程的 stop()方法来结束该线程。该方法通常容易导致死锁,不推荐使用

3.4 sleep()和wait()方法的区别?

  1. sleep()属于Thread类的,而wait()属于Object类
  2. 最重要的区别:sleep()没有释放锁,而wait()释放了锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态
  3. wait(),notify()和notifyAll()只能在同步控制方法或者同步代码块里面使用,而sleep()可以在任何地方使用
  4. wait()是实例方法,sleep()是静态方法

3.5 Java中的锁有哪些?

3.5.1 乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为
别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数
据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),
如果失败则要重复读-比较-写的操作。

Java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。

3.5.2 悲观锁

悲观锁就是悲观思想,认为写多,遇到并发的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人读写数据就会阻塞直到拿到锁。

Java中的悲观锁就是synchronized,AQS框架下的锁则是先尝试CAS乐观锁去获取锁,获取不到才会转为悲观锁,如ReentrantLock。

3.5.3 自旋锁

自旋锁是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

自旋锁原理很简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁资源的线程就不需要做用户态和内核态之间的切换进入阻塞挂起状态,他们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免了用户线程和内核的切换的消耗。

线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,如果一直获取不到锁,那么线程也不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,这会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

一个自旋锁的简单例子:

 
  1. private AtomicReference<Thread> cas = new AtomicReference<Thread>();

  2. public void lock() {

  3. Thread current = Thread.currentThread();

  4. // 利用CAS

  5. while (!cas.compareAndSet(null, current)) {

  6. // DO nothing

  7. }

  8. }

  9. public void unlock() {

  10. Thread current = Thread.currentThread();

  11. cas.compareAndSet(current, null);

  12. }

自旋锁的优点:

  1. 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
  2. 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

自旋锁的缺点:

  1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
  2. Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
3.5.4 Synchronized同步锁

synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重
入锁。

Synchronized 作用范围

  • 作用于方法时,锁住的是对象的实例(this);
  • 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久代PermGen(jdk1.8 则是 metaspace),永久代是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
  • synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
 3.5.5 ReentrantLock

ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完
成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法。

Lock 接口的主要方法:

  1.   void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
  2.   boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行. 
  3.   void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.
  4.   Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将释放锁。
  5.   getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行 lock 方法的次数。
  6.   getQueueLength():返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个线程获得锁,此时返回的是 9
  7.   getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10
  8.   hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
  9.   hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
  10.    hasQueuedThreads():是否有线程等待此锁
  11.   isFair():该锁是否公平锁
  12.   isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true
  13.   isLock():此锁是否有任意线程占用
  14.   lockInterruptibly():如果当前线程未被中断,获取锁
  15.   tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,则获取该锁

Condition 类和 Object 类锁方法区别:
1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效
2. Condition 类的 signal 方法和 Object 类的 notify 方法等效
3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的

3.5.6 可重入锁(递归锁)

可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。

3.5.7 公平锁与非公平锁

公平锁(Fair)
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得

非公平锁(Nonfair)

JVM 按随机、就近原则分配锁的机制则称为非公平锁,ReentrantLock 在构造函数中提供了
是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非
程序有特殊需要,否则最常用非公平锁的分配机制。

加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待
1. 非公平锁性能比公平锁高 5~10 倍,因为公平锁需要在多核的情况下维护一个队列
2. Java 中的 synchronized 是非公平锁,ReentrantLock 默认的 lock()方法采用的是非公平锁。

3.5.8 ReadWriteLock(读写锁)

为了提高性能,Java 提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如
果没有写锁的情况下,读是无阻塞的,在一定程度上提高了程序的执行效率。读写锁分为读锁和写
锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。

  • 读锁

如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁

  • 写锁

如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上
读锁,写的时候上写锁!Java 中读写锁有个接口 java.util.concurrent.locks.ReadWriteLock ,也有具体的实现ReentrantReadWriteLock

3.5.9 共享锁和独占锁
  • 独占锁

  独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。
独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线
程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。

  • 共享锁

  共享锁则允许多个线程同时获取锁,并发访问共享资源,如:ReadWriteLock。共享锁则是一种
乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
1. AQS 的内部类 Node 定义了两个常量 SHARED 和 EXCLUSIVE,他们分别标识 AQS 队列中等
待线程的锁获取模式。
2. java 的并发包中提供了 ReadWriteLock,读-写锁。它允许一个资源可以被多个读操作访问,
或者被一个写操作访问,但两者不能同时进行。

3.5.10 重量级锁

  Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又
是依赖于底层的操作系统的 Mutex Lock (互斥锁)来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为“重量级锁”。JDK 中对 Synchronized 做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6 以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁"

3.5.11 轻量级锁

  锁的状态总共有四种:无锁状态偏向锁轻量级锁重量级锁
锁升级
  随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(但是锁的升级是单向的,
也就是说只能从低到高升级,不会出现锁的降级)。

轻量级锁一般也被叫做非阻塞同步、乐观锁,因为它执行的这个过程并没有把线程阻塞挂起,反而让线程空循环等待,串行执行。

轻量级锁不是用来替代传统的重量级锁的,而是在没有多线程竞争的情况下,使用轻量级锁能够减少性能消耗,但是当多个线程同时竞争锁时,轻量级锁会膨胀为重量级锁。

轻量级锁主要有两种:自旋锁自适应自旋锁

3.5.12 偏向锁

通俗的讲,偏向锁就是在运行的过程中,对象的锁偏向某个线程。即在开启偏向锁机制的情况下,某个线程获得锁,当该线程下次再想获得锁时,不需要再获得锁(即忽略synchronized关键字),直接就可以执行同步代码,比较适合竞争较少的情况。

3.5.13 分段锁

分段锁也并非一种实际的锁,而是一种思想, ConcurrentHashMap 是学习分段锁的最好实践

3.5.14 死锁 

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止,就导致了死锁

Java 死锁产生的四个必要条件:

  • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

死锁示例:

 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记及答案【点击此处即可】免费获取

  1. public class DeadLock {

  2. public static String obj1 = "obj1";

  3. public static String obj2 = "obj2";

  4. public static void main(String[] args) {

  5. LockA lockA = new LockA();

  6. LockB lockB = new LockB();

  7. new Thread(lockA).start();

  8. new Thread(lockB).start();

  9. }

  10. }

  11. class LockA implements Runnable{

  12. @Override

  13. public void run() {

  14. try {

  15. System.out.println("LockA开始执行。。。。");

  16. while (true) {

  17. synchronized (DeadLock.obj1) {

  18. System.out.println("LockA锁住obj1");

  19. Thread.sleep(3000);

  20. synchronized (DeadLock.obj2) {

  21. System.out.println("LockA锁住obj2");

  22. }

  23. }

  24. }

  25. } catch (InterruptedException e) {

  26. e.printStackTrace();

  27. }

  28. }

  29. }

  30. class LockB implements Runnable{

  31. @Override

  32. public void run() {

  33. try {

  34. System.out.println("LockB开始执行。。。。");

  35. while (true) {

  36. synchronized (DeadLock.obj2) {

  37. System.out.println("LockB锁住obj2");

  38. Thread.sleep(3000);

  39. synchronized (DeadLock.obj1) {

  40. System.out.println("LockA锁住obj1");

  41. }

  42. }

  43. }

  44. } catch (InterruptedException e) {

  45. e.printStackTrace();

  46. }

  47. }

  48. }

3.6 锁的优化方式有哪些? 

  • 减少锁持有时间

只用在有线程安全要求的程序上加锁

  • 减小锁粒度

将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。
降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。最最典型的减小锁粒度的案例就是
ConcurrentHashMap。

  • 锁分离

最常见的锁分离就是读写锁 ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,即保证了线程安全,又提高了性能。读写分离思想可以延伸,只要操作互不影响,锁就可以分离。比如 LinkedBlockingQueue 从头部取出,从尾部放数据

  • 锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完
公共资源后,应该立即释放锁。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步
和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化 。

  • 锁消除

锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这
些对象的锁操作,多数是因为程序员编码不规范引起。

3.7 谈谈对线程池的了解?

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。

一般的线程池主要分为以下4个部分组成:

  1.  线程池管理器:用于创建并管理线程池
  2. 工作线程:线程池中的线程
  3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
  4.  任务队列:用于存放待处理的任务,提供一种缓冲机制

Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,
ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。

 ThreadPoolExecutor的构造方法如下:

 
  1. public ThreadPoolExecutor(int corePoolSize,

  2. int maximumPoolSize,

  3. long keepAliveTime,

  4. TimeUnit unit,

  5. BlockingQueue<Runnable> workQueue,

  6. ThreadFactory threadFactory,

  7. RejectedExecutionHandler handler) {

  8. }

 线程池7大参数:

1. corePoolSize:指定了线程池中的核心线程数量。
2. maximumPoolSize:指定了线程池中的最大线程数量。
3. keepAliveTime:空闲线程的存活时间
4. unit:keepAliveTime 的单位。
5. workQueue:存放提交但未执行任务的队列
6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务

线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也
塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:
1. AbortPolicy : 直接抛出异常,阻止系统正常运行。
2. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的
任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
3. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再
次提交当前任务。
4. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢
失,这是最好的一种方案。
以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际
需要,完全可以自己扩展 RejectedExecutionHandler 接口。

3.8 线程池的工作原理?

  • 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过就算队列里有任务,线程池也不会马上执行他们
  • 当调用execute()方法添加一个任务时,线程池会做如下判断:

        a) 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

        b) 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

        c) 如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还是要创建非核心线程立即运行这个任务;

        d) 如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会  启动饱和拒绝策略来执行

  • 当一个线程完成任务时,它会从队列中取下一个任务来执行
  • 当一个线程空闲超过一定时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小

总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。

3.9 阻塞队列原理?阻塞队列有哪些?

阻塞队列,关键字是阻塞,先理解阻塞的含义,在阻塞队列中,线程阻塞有这样的两种情况:

1. 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放
入队列。

2. 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有
空的位置,线程被自动唤醒。

阻塞队列的主要方法:

  •  抛出异常:抛出一个异常;
  •  特殊值:返回一个特殊值(null 或 false,视情况而定)
  • 阻塞:在成功操作之前,一直阻塞线程
  • 超时:放弃前只在最大的时间内阻塞

Java中的阻塞队列:

1. ArrayBlockingQueue :由数组结构组成的有界阻塞队列。
2. LinkedBlockingQueue :由链表结构组成的有界阻塞队列。
3. PriorityBlockingQueue :支持优先级排序的无界阻塞队列。
4. DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
5. SynchronousQueue:不存储元素的阻塞队列。
6. LinkedTransferQueue:由链表结构组成的无界阻塞队列。
7. LinkedBlockingDeque:由链表结构组成的双向阻塞队列。

 小结:

1. 在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
2. 为什么需要 BlockingQueue? 在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。使用后我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切 BlockingQueue 都给你一手包办了

 3.10 写出一个线程通信的例子(生产者消费者问题)

两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求用线程间通信

示例:

 
  1. public class MyThread7 {

  2. public static void main(String[] args) {

  3. ThreadDemo threadDemo = new ThreadDemo();

  4. new Thread(() -> {

  5. for (int i = 1; i <= 20; i++) {

  6. threadDemo.incr();

  7. }

  8. }, "线程a").start();

  9. new Thread(() -> {

  10. for (int i = 1; i <= 20; i++) {

  11. threadDemo.decr();

  12. }

  13. }, "线程b").start();

  14. }

  15. }

  16. class ThreadDemo {

  17. private int number = 0;

  18. private Lock lock = new ReentrantLock();

  19. private Condition condition = lock.newCondition();

  20. public void incr() {

  21. try {

  22. lock.lock();

  23. while (number != 0) {

  24. condition.await();

  25. }

  26. number++;

  27. System.out.println("*****" + Thread.currentThread().getName() + "加1操作成功,值为:" + number);

  28. condition.signalAll();

  29. } catch (InterruptedException e) {

  30. e.printStackTrace();

  31. } finally {

  32. lock.unlock();

  33. }

  34. }

  35. public void decr() {

  36. try {

  37. lock.lock();

  38. while (number == 0) {

  39. condition.await();

  40. }

  41. number--;

  42. System.out.println("*****" + Thread.currentThread().getName() + "减1操作成功,值为:" + number);

  43. condition.signalAll();

  44. } catch (InterruptedException e) {

  45. e.printStackTrace();

  46. } finally {

  47. lock.unlock();

  48. }

  49. }

  50. }

3.11 JUC三大辅助类 CountDownLatch(线程计数器)、 CyclicBarrier(回环栅栏) 、 Semaphore(信号量)

3.11.1 CountDownLatch

        CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句。

        CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这
些线程会阻塞。其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程
不会阻塞) 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行。

示例:6 个同学陆续离开教室后值班同学才可以关门。

 
  1. public class ThreadDemo6 {

  2. public static void main(String[] args) throws InterruptedException {

  3. // 定义一个数值为6的计数器

  4. CountDownLatch countDownLatch = new CountDownLatch(6);

  5. for (int i = 1; i <= 6; i++) {

  6. new Thread(() -> {

  7. try {

  8. if (Thread.currentThread().getName().equals("同学6")) {

  9. Thread.sleep(6000);

  10. }

  11. System.out.println(Thread.currentThread().getName() + "离开了");

  12. // 计数器减1,不会阻塞

  13. countDownLatch.countDown();

  14. } catch (InterruptedException e) {

  15. e.printStackTrace();

  16. }

  17. }, "同学" + i).start();

  18. }

  19. // 主线程await休息

  20. System.out.println("主线程睡觉");

  21. countDownLatch.await();

  22. System.out.println("全部离开了,现在的计数器为:" + countDownLatch.getCount());

  23. }

  24. }

3.11.2  CyclicBarrier

        CyclicBarrier 看英文单词可以看出大概就是循环阻塞的意思,在使用中。CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier 理解为加 1 操作

 示例: 集齐 7 颗龙珠就可以召唤神龙

 
  1. public class ThreadDemo7 {

  2. private final static int NUMBER = 7;

  3. public static void main(String[] args) {

  4. CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> {

  5. System.out.println("集齐" + NUMBER + "颗龙珠,开始召唤神龙。。。");

  6. });

  7. for (int i = 1; i <= 7; i++) {

  8. new Thread(() -> {

  9. System.out.println(Thread.currentThread().getName() + "收集到了!!!!");

  10. try {

  11. cyclicBarrier.await();

  12. } catch (InterruptedException e) {

  13. e.printStackTrace();

  14. } catch (BrokenBarrierException e) {

  15. e.printStackTrace();

  16. }

  17. }, "龙珠" + i + "号").start();

  18. }

  19. }

  20. }

        Semaphore 翻译成字面意思为 信号量,Semaphore 可以控制同时访问的线程个数,通过
acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

示例: 抢车位, 6 台汽车 3 个停车位

 
  1. public class ThreadDemo8 {

  2. public static void main(String[] args) throws Exception {

  3. Semaphore semaphore = new Semaphore(3);

  4. for (int i = 1; i <= 6; i++) {

  5. Thread.sleep(300);

  6. new Thread(() -> {

  7. System.out.println(Thread.currentThread().getName() + "找车位ing。。。。");

  8. try {

  9. semaphore.acquire();

  10. System.out.println(Thread.currentThread().getName() + "汽车停成功。。。。。");

  11. Thread.sleep(5000);

  12. } catch (InterruptedException e) {

  13. e.printStackTrace();

  14. } finally {

  15. System.out.println(Thread.currentThread().getName() + "离开了,,,,,,,");

  16. semaphore.release();

  17. }

  18. }, "汽车" + i).start();

  19. }

  20. }

  21. }

    CountDownLatch 和 CyclicBarrier 都能够实现线程之间的等待,只不过它们侧重点不
同;CountDownLatch 一般用于某个线程 A 等待若干个其他线程执行完任务之后,它才
执行(减法计数);而 CyclicBarrier 一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行(加法计数);另外,CountDownLatch 是不能够重用的,而 CyclicBarrier 是可以重用的。Semaphore 其实和锁有点类似,它一般用于控制对某组资源的访问权限。

3.12 volatile 关键字的作用

        Java 语言提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其他线程。volatile 变量具备两种特性,volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile 类型的变量时总会返回最新写入的值。

主要作用:

  • 变量可见性

这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的

  • 禁止重排序

volatile禁止了指令重排

注:在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是一种比 sychronized 关键字更轻量级的同步机制。volatile 适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到 CPU 缓存中。如果计算机有
多个 CPU,每个线程可能在不同的 CPU 上被处理,这意味着每个线程可以拷贝到不同的 CPU 
cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 
这一步。 

适用场景:

值得说明的是对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。在某些场景下可以代替synchronized。但是volatile不能完全取代synchronized,只有在一些特殊场景下,才能适用volatile。总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:

(1)对变量的写操作不依赖于当前值(比如i++),或者说是单纯的变量赋值(boolean flag =true)

(2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的volatile变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用 volatile。

3.13 ThreadLocal 作用(线程本地存储)

ThreadLocal,很多地方叫线程本地变量,也有些地方叫线程本地存储,它的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

ThreadLocalMap(线程的一个属性)

        1. 每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
        2. 将一个共用的 ThreadLocal 静态实例作为 key,将不同对象的引用保存到不同线程的
ThreadLocalMap 中,然后在线程执行的各处通过这个静态 ThreadLocal 实例的 get()方法取
得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
        3. ThreadLocalMap 其实就是线程里面的一个属性,它在 Thread 类中定义
ThreadLocal.ThreadLocalMap threadLocals = null;

使用场景:
最常见的 ThreadLocal 使用场景为用来解决数据库连接、Session 管理等。

示例:

 
  1. private static final ThreadLocal threadSession = new ThreadLocal();

  2. public static Session getSession() throws InfrastructureException {

  3. Session s = (Session) threadSession.get();

  4. try {

  5. if (s == null) {

  6. s = getSessionFactory().openSession();

  7. threadSession.set(s);

  8. }

  9. } catch (HibernateException ex) {

  10. throw new InfrastructureException(ex);

  11. }

  12. return s;

  13. }

3.14 synchronized 和 ReentrantLock 的区别

两者的共同点:

  1. 都是用来协调多线程对共享对象、变量的访问
  2. 都是可重入锁,同一线程可以多次获得同一个锁
  3. 都保证了可见性和互斥性

两者的不同点:

  1.  ReentrantLock 显示的获得、释放锁,synchronized隐式获得、释放锁
  2. ReentrantLock是API级别的,synchronized是JVM级别的
  3. ReentrantLock可以实现公平锁
  4.  ReentrantLock 通过Condition可以绑定多个条件
  5. 底层实现不一样,synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略
  6. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
  7. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生,而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁
  8. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,
    等待的线程会一直等待下去,不能够响应中断
  9. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
  10. Lock 可以提高多个线程进行读操作的效率,即就是实现读写锁等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值