前言
已失业的中年程序猿,在这个卷的不能再卷的Java赛道中,形势恶劣的大环境下,仍不想放弃,苦苦支撑,想寻求一条生路~~都说工作拧螺丝,面试造火箭,八股文还是终极武器。从各渠道获取的八股文以及自己面试遇到的问题,整理汇总起来,愿自己后面的面试能一切顺利。(持续找工作中,很焦虑。。。。。。)
1.Java基础(基础、集合、异常)
1.1 Java基本的数据类型有哪些
1.2 == 和 equals 的区别是什么
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数 据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)
equals() : 它的作用也是判断两个对象是否相等。( 类没有覆盖 equals() 方法,则通过 equals() 比较该类的两个对象时,等价于通过 “==”比较这两个对象。
类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容相等; 若它们的内容相等,则返回 true (即,认为这两个对象相等)。
String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址, 而String的equals方法比较的是对象的值。 当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。 )
1.3 hashCode 方法的作用
hashCode()的作用是获取哈希码,也称为散列码,它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置,它定义为Object类的方法,就意味着Java中的任何类都包含有hashCode()方法。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就 利用到了散列码(可以快速找到所需要的对象)
我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同 时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假 设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不 同的话,就会重新散列到其他位置。
hashCode()与equals()的相关规定 :
-
如果两个对象相等,则hashcode一定也是相同的
-
两个对象相等,对两个对象分别调用equals方法都返回true
-
两个对象有相同的hashcode值,它们也不一定是相等的
因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖。 hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
1.4 常用的集合类有哪些
Map接口和Collection接口是所有集合框架的父接口:
-
Collection接口的子接口包括:Set接口和List接口
-
Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及 Properties等
-
Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
-
List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
1.5 ArrayList 和 LinkedList 的区别是什么?
-
数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
-
随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
-
增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
-
内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
-
线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全。
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多 时,更推荐使用 LinkedList。LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
1.6 多线程场景下如何使用 ArrayList?
ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用,或者使用CopyOnWriteArrayList。(Vector效率低不考虑)
线程不安全示例:
-
public class Demo {
-
public static void main(String[] args) {
-
List<String> list = new ArrayList<>();
-
for (int i = 0; i < 200; i++) {
-
new Thread(() -> {
-
list.add(UUID.randomUUID().toString().substring(0, 8));
-
list.forEach(System.out::println);
-
}).start();
-
}
-
}
-
}
1.7 HashSet是如何保证数据不重复的?
向HashSet中add()时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equals方法比较。HashSet中的add()方法会使用HashMap的put()方法。
HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key, 并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。
-
private static final Object PRESENT = new Object();
-
private transient HashMap<E,Object> map;
-
public HashSet() {
-
map = new HashMap<>();
-
}
-
public boolean add(E e) {
-
// 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
-
return map.put(e, PRESENT)==null;
-
}
1.8 HashSet与HashMap的区别
1.9 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 比较
-
resize 扩容优化
-
引入了红黑树,目的是避免单条链表过长而影响查询效率
-
解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
1.10 HashMap的put方法的具体流程?
-
public V put(K key, V value) {
-
return putVal(hash(key), key, value, false, true);
-
}
-
static final int hash(Object key) {
-
int h;
-
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
-
}
-
//实现Map.put和相关方法
-
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
-
boolean evict) {
-
Node<K,V>[] tab; Node<K,V> p; int n, i;
-
// 步骤①:tab为空则创建
-
// table未初始化或者长度为0,进行扩容
-
if ((tab = table) == null || (n = tab.length) == 0)
-
n = (tab = resize()).length;
-
// 步骤②:计算index,并对null做处理
-
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
-
if ((p = tab[i = (n - 1) & hash]) == null)
-
tab[i] = newNode(hash, key, value, null);
-
// 桶中已经存在元素
-
else {
-
Node<K,V> e; K k;
-
// 步骤③:节点key存在,直接覆盖value
-
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
-
if (p.hash == hash &&
-
((k = p.key) == key || (key != null && key.equals(k))))
-
// 将第一个元素赋值给e,用e来记录
-
e = p;
-
// 步骤④:判断该链为红黑树
-
// hash值不相等,即key不相等;为红黑树结点
-
// 如果当前元素类型为TreeNode,表示为红黑树,putTreeVal返回待存放的node, e可能为null
-
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);
-
//判断链表的长度是否达到转化红黑树的临界值,临界值为8
-
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
-
//链表结构转树形结构
-
treeifyBin(tab, hash);
-
// 跳出循环
-
break;
-
}
-
// 判断链表中结点的key值与插入的元素的key值是否相等
-
if (e.hash == hash &&
-
((k = e.key) == key || (key != null && key.equals(k))))
-
// 相等,跳出循环
-
break;
-
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
-
p = e;
-
}
-
}
-
//判断当前的key已经存在的情况下,再来一个相同的hash值、key值时,返回新来的value这个值
-
if (e != null) {
-
// 记录e的value
-
V oldValue = e.value;
-
// onlyIfAbsent为false或者旧值为null
-
if (!onlyIfAbsent || oldValue == null)
-
//用新值替换旧值
-
e.value = value;
-
// 访问后回调
-
afterNodeAccess(e);
-
// 返回旧值
-
return oldValue;
-
}
-
}
-
// 结构性修改
-
++modCount;
-
// 步骤⑥:超过最大容量就扩容
-
// 实际大小大于阈值则扩容
-
if (++size > threshold)
-
resize();
-
// 插入后回调
-
afterNodeInsertion(evict);
-
return null;
-
}
-
判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
-
根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向 ⑥,如果table[i]不为空,转向③;
-
判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
-
判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5;
-
遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
-
插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
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
结论:
- 不管有没有出现异常,finally块中代码都会执行;
- 当try和catch中有return时,finally仍然会执行;
- finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前确定的;
- 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协议族中的一种。是无连接的协议,发送数据前不需要建立连接,是没有可 靠性的协议。因为不需要建立连接所以可以在在网络上以任何可能的路径传输,因此能否到达目的 地,到达目的地的时间以及内容的正确性都是不能被保证的。
区别:
-
TCP是面向连接的协议,发送数据前要先建立连接,TCP提供可靠的服务,也就是说,通过TCP连 接传输的数据不会丢失,没有重复,并且按顺序到达;
-
UDP是无连接的协议,发送数据前不需要建立连接,是没有可靠性;
-
TCP通信类似于要打个电话,接通了,确认身份后,才开始进行通信;
-
UDP通信类似于学校广播,靠着广播播报直接进行通信。
-
TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多;
-
TCP是面向字节流的,UDP是面向报文的; 面向字节流是指发送数据时以字节为单位,一个数据 包可以拆分成若干组进行发送,而UDP一个报文只能一次发完。
-
TCP首部开销(20字节)比UDP首部开销(8字节)要大 ,UDP 的主机不需要维持复杂的连接状态表
对某些实时性要求比较高的情况使用UDP,比如游戏,媒体通信,实时直播,即使出现传输错误也可以容忍;其它大部分情况下,HTTP都是用TCP,因为要求传输的内容可靠,不出现丢失的情况
2.4 从输入地址到获得页面的过程 ?
-
浏览器查询 DNS,获取域名对应的IP地址:具体过程包括浏览器搜索自身的DNS缓存、搜索操作系统的DNS缓存、读取本地的Host文件和向本地DNS服务器进行查询等。对于向本地DNS服务器进行查询,如果要查询的域名包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析 (此解析具有权威性);如果要查询的域名不由本地DNS服务器区域解析,但该服务器已缓存了此网 址映射关系,则调用这个IP地址映射,完成域名解析(此解析不具有权威性)。如果本地域名服务 器并未缓存该网址映射关系,那么将根据其设置发起递归查询或者迭代查询;
-
浏览器获得域名对应的IP地址以后,浏览器向服务器请求建立链接,发起三次握手;
-
TCP/IP链接建立起来后,浏览器向服务器发送HTTP请求
-
服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;
-
浏览器解析并渲染视图,若遇到对js文件、css文件及图片等静态资源的引用,则重复上述步骤并向服务器请求这些资源;
-
浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面。
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状态;完成三次握手,连接建立。
简单来说就是 :
-
客户端向服务端发送SYN
-
服务端返回SYN,ACK
-
客户端发送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安全通信线路)+认证+完整性保护
区别:
-
https需要拿到ca证书,需要钱的
-
端口不一样,http是80,https443
-
http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
-
http和https使用的是完全不同的连接方式(http的连接很简单,是无状态的;HTTPS 协议是 由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。)
2.8 一次完整的HTTP请求所经历几个步骤?
-
建立TCP连接
-
Web浏览器向Web服务器发送请求行
-
Web浏览器发送请求头
-
Web服务器应答
-
Web服务器发送应答头
-
Web服务器向浏览器发送数据
-
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()方法
-
public class MyThread extends Thread {
-
public void run() {
-
System.out.println("MyThread.run()");
-
}
-
}
-
MyThread myThread1 = new MyThread();
-
myThread1.start();
- 实现Runnable接口
如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个
Runnable 接口
-
public class MyThread extends OtherClass implements Runnable {
-
public void run() {
-
System.out.println("MyThread.run()");
-
}
-
}
-
//启动 MyThread,需要首先实例化一个 Thread,并传入自己的 MyThread 实例:
-
MyThread myThread = new MyThread();
-
Thread thread = new Thread(myThread);
-
thread.start();
- 使用Callable和Future创建线程
和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。
1.call()方法可以有返回值
2.call()方法可以声明抛出异常
-
public class Test1 {
-
public static void main(String[] args) throws ExecutionException, InterruptedException {
-
MyThread3 thread3 = new MyThread3();
-
FutureTask<Integer> task = new FutureTask<Integer>(thread3);
-
Thread thread = new Thread(task);
-
thread.start();
-
System.out.println("线程返回值:"+task.get());
-
}
-
}
-
class MyThread3 implements Callable {
-
@Override
-
public Object call() throws Exception {
-
return 100;
-
}
-
}
- 基于线程池的方式
线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销
毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
-
public class MyThread4 {
-
public static void main(String[] args) {
-
ExecutorService threadPool = Executors.newFixedThreadPool(10);
-
while (true) {
-
// 提交多个线程任务,并执行
-
threadPool.execute(new Runnable() {
-
@Override
-
public void run() {
-
System.out.println(Thread.currentThread().getName()+":is running...");
-
try {
-
Thread.sleep(3000);
-
} catch (InterruptedException e) {
-
e.printStackTrace();
-
}
-
}
-
});
-
}
-
}
-
}
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而是一个执行线程的工具,真正的线程池接口是ExecutorService。
- newSingleThreadExecutor
-
public static ExecutorService newSingleThreadExecutor() {
-
return new FinalizableDelegatedExecutorService
-
(new ThreadPoolExecutor(1, 1,
-
0L, TimeUnit.MILLISECONDS,
-
new LinkedBlockingQueue<Runnable>()));
-
}
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。适用于串行执行任务的场景,一个任务一个任务的执行,比如任务调度。
- newFixedThreadPool
-
public static ExecutorService newFixedThreadPool(int nThreads) {
-
return new ThreadPoolExecutor(nThreads, nThreads,
-
0L, TimeUnit.MILLISECONDS,
-
new LinkedBlockingQueue<Runnable>());
-
}
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大
多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,
则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何
线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之
前,池中的线程将一直存在。
- newCachedThreadPool
-
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
-
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
-
60L, TimeUnit.SECONDS,
-
new SynchronousQueue<Runnable>(),
-
threadFactory);
-
}
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行
很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造
的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并
从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资
源。
- newScheduledThreadPool
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
-
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
-
scheduledThreadPool.schedule(new Runnable(){
-
@Override
-
public void run() {
-
System.out.println("延迟三秒");
-
}
-
}, 3, TimeUnit.SECONDS);
-
scheduledThreadPool.scheduleAtFixedRate(new Runnable(){
-
@Override
-
public void run() {
-
System.out.println("延迟 1 秒后每三秒执行一次");
-
}
-
},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()方法的区别?
- sleep()属于Thread类的,而wait()属于Object类
- 最重要的区别:sleep()没有释放锁,而wait()释放了锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态
- wait(),notify()和notifyAll()只能在同步控制方法或者同步代码块里面使用,而sleep()可以在任何地方使用
- 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自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,这会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。
一个自旋锁的简单例子:
-
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
-
public void lock() {
-
Thread current = Thread.currentThread();
-
// 利用CAS
-
while (!cas.compareAndSet(null, current)) {
-
// DO nothing
-
}
-
}
-
public void unlock() {
-
Thread current = Thread.currentThread();
-
cas.compareAndSet(current, null);
-
}
自旋锁的优点:
- 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
- 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
自旋锁的缺点:
- 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
- Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
3.5.4 Synchronized同步锁
synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重
入锁。
Synchronized 作用范围
- 作用于方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久代PermGen(jdk1.8 则是 metaspace),永久代是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
- synchronized 作用于一个对象实例时,锁住的是所有以该对象为锁的代码块。它有多个队列,当多个线程一起访问某个对象监视器的时候,对象监视器会将这些线程存储在不同的容器中。
3.5.5 ReentrantLock
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完
成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法。
Lock 接口的主要方法:
- void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
- boolean tryLock():如果锁可用, 则获取锁, 并立即返回 true, 否则返回 false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而 lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行.
- void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.
- Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的 await()方法,而调用后,当前线程将释放锁。
- getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行 lock 方法的次数。
- getQueueLength():返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个线程获得锁,此时返回的是 9
- getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了condition 对象的 await 方法,那么此时执行此方法返回 10
- hasWaiters(Condition condition):查询是否有线程等待与此锁有关的给定条(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法
- hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
- hasQueuedThreads():是否有线程等待此锁
- isFair():该锁是否公平锁
- isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true
- isLock():此锁是否有任意线程占用
- lockInterruptibly():如果当前线程未被中断,获取锁
- 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 轻量级锁
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。
锁升级
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁(但是锁的升级是单向的,
也就是说只能从低到高升级,不会出现锁的降级)。
轻量级锁一般也被叫做非阻塞同步、乐观锁,因为它执行的这个过程并没有把线程阻塞挂起,反而让线程空循环等待,串行执行。
轻量级锁不是用来替代传统的重量级锁的,而是在没有多线程竞争的情况下,使用轻量级锁能够减少性能消耗,但是当多个线程同时竞争锁时,轻量级锁会膨胀为重量级锁。
轻量级锁主要有两种:自旋锁和自适应自旋锁。