目录
String/StringBuffer/StringBulider之间的关系
ArrayList,Vactor,LinkedList的相同点和不同点
HashTable与concurrentHashMap的区别
有了Synchronized为什么还需要ReentrantLock
基础部分
Java语言有什么特点
1.java是纯面向对象的语言,能够直接反映生活当中的对象。
2.java是具有平台无关性的解释性语言,java利用JVM来运行字节码,javac将.java文件编译为与平台无关的.class中间文件,在经由JVM编译为机器能够识别的文件,能够很好的进行移植。
3.java具有健壮性和安全性,提供了异常处理和垃圾回收机制,并移除了c语言中的难以理解的指针。
JDK与JRE的区别
jdk:java开发工具包,包含有jre,为java提供了开发环境和运行环境
jre:java运行环境,为java提供运行环境。
Java的基本数据类型
byte:占有1个字节
short:占有2个字节
int:占有4个字节
long:占有8个字节
float:占有4个字节
double:占有8个字节
boolean:根据虚拟机的不同
char:占有2个字节
装箱和拆箱过程
装箱:将基本的数据类型转换为包装类
拆箱:将包装类转换为基本数据类型
Java访问修饰符
private:同一个类中可见,不可修饰类
default:同一个包内可见
protected:同一个包下的类和所有子类可见,不能修饰类
public:对所有类都可见
构造方法,成员变量和静态成员变量的初始化顺序
父类静态成员变量,父类静态代码块,子类静态成员变量,子类静态代码块,父类非静态成员变量,父类非静态代码块,父类构造器,子类非静态成员变量,子类非静态代码块,子类构造器。
面向对象的三大特征
封装:利用private访问修饰符限制其他的类对内部变量的访问,可以提供public修饰的set/get方法来进行数据操作与访问,降低数据的访问代价。
继承:
1.从父类中派生出新的类称为子类,子类可以从他的父类继承方法和实例变量,实现代码的复用,也可针对自己的需求进行代码的重写。
2.java中是不允许多继承的,接口除外(因为接口只有方法定义,没有方法实现,只有实现接口的类对方法进行实现才能够使用),假如A继承B,C,当A调用B和C都拥有的方法时就会发生二义性。
多态:
1.允许不同的类的对象对同一信息作出反应,即使调用的方法和参数相同,最终的表现形式也是不同的。
2.java为多态提供了重载和重写两种机制:重载是在同一个类中,对相同的方法名提供不同的参数列表,在编译时就能决定使用哪个方法。重写是发生在父类和子类之间的,使用父类指向子类的实例对象,或者接口类指向实现类的实例对象。
3.多态分为编译时多态和运行时多态,编译时多态主要是指方法的重载,即根据不同的参数列表调用不同的方法。而运行时多态指的是父类的继承和接口的实现,父类的引用可以指向子类对象。
重载和重写的区别
重载发生在同一个类中,而重写发生在父类子类之间
重载是多个方法之间的关系,而重写是一对方法的关系
重载的参数列表必须不同,而重写的参数列表必须相同
调用方法时,重载之间根据参数列表确定调用的方法,编译时即可确定;重写之间根据对象的类型来确定调用的方法,运行时才能确定。
重载可以改变返回值而重写不能
接口类和抽象类的相同点和不同点
相同:
都不可以被实例化,只有通过抽象类的子类和接口类的实现类对方法进行重写才可以实例化。
不同:
接口只能有方法的定义,而抽象类可以有方法的定义和实现。
一个类可以实现多个接口,但是只能继承一个抽象类
当子类和父类有层次上的关系时,推荐是由抽象类,抽象类以便于方法功能的积累;当功能差别比较大,推荐使用接口类,能够降低软件的耦合度,方便日后的删除和修改。
内部类及其作用
成员内部类:作为成员对象内部的类,可以访问private以及以上的外部类的属性和方法,外部类想要访问内部类的属性和方法时需要先创建一个内部类对象,通过对象去访问内部类的属性和方法,外部类也可以访问内部类的private属性,不可以存在static修饰的方法和属性。
局部内部类:存在与方法中的内部类,不允许使用任何访问修饰符和static,除了创建该内部类的方法,其他均不可访问;只能访问外部类的final变量。
匿名内部类:只能使用一次,只能访问外部类的final变量。
静态内部类:不需要依赖外部类,可以直接创建;不可以使用外部任何非静态的方法和变量。
static关键字的作用
1.static能够为某种数据类型或者对象划分与对象个数无关的单一的存储空间
2.使得某个方法或者属性与类关联起来而不是对象,即在不创建对象的情况下可以直接使用方法或者使用类的属性。
3.修饰成员变量,用static修饰的变量在内存中只会存储一份副本,在加载当前类时就会给静态成员变量划分空间,可以通过“类.静态变量”,“对象.静态变量”进行使用。
4.修饰成员方法, static修饰的成员方法无需创建对象就可直接使用,static方法中不可使用this和super,static方法只可使用所属类的静态成员变量和静态方法。
5.修饰代码块,JVM加载类时就会调用static代码块,static代码块只会被执行一次,通常用来初始化变量。
6.修饰内部类,静态内部类可以不依赖与外部类实例对象而被实例化,只可访问外部类的静态变量与方法。
为什么将String设计为不可变
1.节省空间:String存储于字符串常量池当中,可以被多个用户读取
2.提高效率:因为String的不可变,属于线程安全,在多线程的操作下可以不进行同步操作
3.安全问题:String常常被用来存储用户名密码,由于String的不可变,可以避免黑客的恶意篡改。
String/StringBuffer/StringBulider之间的关系
String采用fianl来修饰,是不可变得,对字符串进行操作只能新建对象。
StringBuilder不采用final,可以进行字符串的拼接,但通过分析源码可知,是线程不安全的。
StringBuffer不采用final,通过分析源码可见被synchronized修饰,是线程安全的。
==与equals的区别
==比较的是引用,而equals比较的是内容。
当==比较的是基本数据类型则是判断内容是否相等,如果是比较引用类型,则判断引用是否指向同一块内存空间。
equals属于Object中的方法,因此每个类都具有这个方法,Object中的equals直接采用==来进行比较,通过重写可以实现比较内容的功能。
Object中常用的方法
hashCode:通过对象计算出散列值,用于map型和equals方法,需要保证同一个对象多次调用该方法返回相同的值。
equals:判断两个对象是否一致,需保证equals方法相同,对应的对象hashCode也相同。
toString:通过字符串输出该对象
clone:深度拷贝一个对象
Java中的异常
异常分为Error(程序无法处理的错误)和Execption(程序可以处理的异常),均继承与Throwable。
Error常见的有StackOverFlowError(栈溢出)和OutOfMemoryError(内存溢出)
Execption分为编译时异常和运行时异常,运行时异常可以通过try/catch处理,编译时异常必须处理,否则无法进行编译。
throw出现在方法体内部,有程序员自定义程序发生异常时抛出的类型
throws出现在方法声明上面,代表该方法可能出现的异常
finally代码块是否一定执行
当进入try代码块之前发生异常,或者在try代码块中通过System.exit(0)来强制退出就不会执行finally代码块。
正常执行时,先执行try代码块,发生异常后执行catch代码块之后再执行finally代码块,若try当中有return,则跳过return,先执行finally在执行return,若finally中有return则会覆盖掉try中的return。
简述final,finally和finalize的区别
final可以用于修饰变量,方法和类,分别表示变量不可修改,方法不可重写和类不可继承
finally用于try/catch中表示一定被执行的部分,通常用于释放内存
finalize是Object的一个方法,在垃圾回收器准备好回收对象资源时,就会调用该对象的finalize方法,并且在下次垃圾回收器进行动作时真正的释放内存
简述泛型
泛型用于不确定传入参数的类型的问题,Java编译生成的字节码是不包含泛型信息的,在编译期间被擦除,称为泛型擦除。
Java的反射机制
java的反射机制值得是可以再程序运行期间构造任意一个类的对象,获取任意类的成员变量和成员方法,获取任意一个对象的类信息,调用任意一个对象的属性和方法。java的反射机制使得可以动态获取对象信息和动态调用对象方法的能力。
Class类:可获得类属性方法
Field类:可获得类的成员变量
Method类:可获得类的方法信息
Construct类:可获得类的构造方法信息
Java中的List
List是一个有序队列,在java中有两种实现方式:
ArrayList基于数组实现,属于线程不安全结构,随机访问元素比较快但增删元素较慢,对ArrayList扩容时需要新建一个数组,将原有元素放入其中。
LinkedList基于双向链表实现,属于线程不安全结构,增删元素很快但随机访问比较慢。
Java中的Set
Set即集合,该结构中不允许有重复的元素并且元素无序,java中有三种方法实现Set:
1.HashSet基于HashMap实现,HashMap中的key值就是Set的元素值,而Value则被系统定义为PRESENT的Object变量,比较是否相同时先通过hashCode比较,相同后在通过equals比较。
2.LinkedHashSet,继承自HashSet,基于LinkedHashMap实现,通过双向链表维护元素插入其中的顺序。
3.TreeSet基于TreeMap实现的,底层是红黑树,通过一定的规则比较将元素插入其中使其集合依然有序。
Java中的HashMap
HashMap在JDK7以前基于数组+链表实现,JDK8之后基于数组+链表/红黑树实现,主要成员变量头table数组用于存储数据,size元素数量以及加载因子loadFactor,数据是以键值对的形式存储的,key对应的hash值用来计算数组下标,如果hash值相同就会发生哈希冲突,存放到同一个链表当中。
table数组存放的是一个链表,hash值相同的元素都会存放在同一个链表当中,Node/Entry节点当中存放有四个数据:key,value,next指针和hash值。JDK8之后若链表超过8则会转换为红黑树。
当前数据量/总容积>负载因子就需要对HashMap进行扩容,默认初始大小为16,每次扩容为2的幂次方。
HashMap是线程不安全的,因为JDK7以前采用的是头插法,因此在并发编程状态下容易形成环路,进而死循环。JDK8值都采用尾插发改善了这一状况,但是并发下的put操作会使前一个key值被后一个key值覆盖。HashMap也存在扩容机制,可能会导致线程一进行扩容之后可能导致线程二的get操作失败。
Java中的TreeMap
TreeMap是基于红黑树实现的Map结构,底层是一颗平衡的排序二叉树,他的插入删除和遍历所需要的时间复杂度为O(longn)因此效率低于哈希表,但是哈希表是无序的,而TreeMap可以实现数据的有序输出。
ArrayList,Vactor,LinkedList的相同点和不同点
1.ArrayList,Vactor,LinkedList都可以动态改变长度
2.ArrauList和Vactor都是基于数组来实现的,在内存当中开辟一块连续的空间存储,对于数据的增加,删除的功能比较低效,当超过最大长度时均可进行扩容。LinkedList是基于双向链表的结构,访问元素效率很低,但是增删元素比较高效。
3.ArrayList和LinkedList是线程不安全的,而Vactor是线程安全的,其大部分方法是直接或者间接同步的
HashMap和HashTable的区别
HashMap是HashTable的轻量级实现,HashMap中允许key,value为null,但是HashTable不允许
HashMap是线程不安全的,在多线程中使用需要额外的同步机制,HashTable是线程安全的。
HashMap通过Iterator进行遍历,HashTable通过Enumeration进行遍历。
HashMap和TreeMap的选择
如果经常进行数据的增删改查推荐使用HashMap,但如果需要对数据进行一个有序的遍历推荐使用TreeMap。
hashCode和equals的关系
hashCode和equals都是从Object中继承过来的方法,equals比较的是引用指向同一块内存空间,而hashCode则是将对象的内存地址通过哈希规则转换为一个哈希码。
HashSet中首先通过比较hashCode的值,如果不相同则对象不同,若相同则继续通过equals进行比较,若相同则为同一对象否则不同。
Collection和Collections的区别
Collection是集合框架的父接口,他提供了很多集合对象进行基本操作的通用接口方法,所有的集合都是他的子类
Collections是一个包装类,它包含有很多集合类的静态多态方法,不能被实例化,类似于一个工具类服务于Collection
并发编程
JMM模型
所有的变量都存储在主存当中,每个线程都有自己的工作内存;
工作内存中存储了被该线程操作的变量的主存副本,所有的操作只能在自己的工作内存中进行,不能对主存数据直接进行读写操作;
操作完毕后通过缓存一致性协议将数据写进主存中
什么是原子性
一系列的操作要么全部都执行并且执行过程中不被任何因素打断,要么就都不执行。
什么是内存可见性
当一个线程修改完共享数据之后,所有的线程都能够得知,通过Volatile,synchronized,final关键字可以保护内存可见性
什么是有序性
多线程存在并发和指令重排序等操作,但在本线程内观察指令依然是有序的。
Java线程的创建方式
1.继承Thread:建立一个类继承自Thread,重写run方法,在run函数中写入执行流在主函数中创建实例并通过start函数执行 为了方便观察引入sleep睡眠函数,但线程可能会提前苏醒,通过异常包裹。睡眠时间过后线程将重新抢占CPU时间片,若没抢占到则延后执行直至抢占到,所以等待线程的执行可能多于睡眠时间。
public class Text {
public static void main(String[] args) {
class MyThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("新线程");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
MyThread myThread = new MyThread();
myThread.start();
while(true){
System.out.println("主线程");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.实现Runnable接口:
public class ThreadDemo2 {
public static void main(String[] args) {
class MyTask implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Runnable MyTask = new MyTask();
Thread thread = new Thread(MyTask);
thread.start();
}
}
3.lambda表达式:
public class ThreadDemo3 {
public static void main(String[] args) {
Thread thread = new Thread(()-> System.out.println("lambda表达式建立线程"));
thread.start();
}
}
4.匿名实现的两种方法
public class ThreadDemo3 {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
while(true){
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("hello");
}
}
});
thread.start();
}
}
线程的状态
NEW:此时线程被创建出来,但是还未通过start()启动
RUNNABLE:线程处于运行状态,但是可能没有在运行,处于排队等待CPU时间片的过程中
BLOCKED:线程未能获取到锁,等待有锁的释放,进入阻塞状态
WAITTING:等待状态,线程执行Object.wait()/Thread.join()方法后进入的状态
TIMED_WAITTING:进入一段限期时间的等待,线程执行完Object.wait(long)/Thread.join(long)/Thread.sleep(long)后进入的状态
TERMINATED:结束状态,线程执行完run()方法后进入的状态
多线程运行产生线程安全问题的原因分析
1.线程之间是抢占式的执行(系统调度问题)
2.多个线程操作一个数据(需求决定)
3.不是原子操作,一个线程读取数据之后到将数据存储到主存之前,有另一个线程读取了还未操作的数据,因此出现了bug
4.内存的可见性问题,因为从主存中读取数据比较耗时间,JVM自己进行了优化,将多次读取执行存储操作合并为一次读取多次执行一次存储这样的结构,导致了一个线程操作数据之后其他线程未能及时得知此次操作而导致的bug
5.指令的重排序问题,也是JVM擅自进行优化导致的bug,单线程模式下并未改变执行的逻辑,但多线程的情况下可能将逻辑改变了。
Volatile关键字的作用
保证了内存可见性,禁止了指令重排序。
Synchronized关键字的作用
保证了原子性和内存可见性,禁止了指令重排序
如何线程安全的使用顺序表
1.自己手动加锁
2.Collections.synchronizedList,相当于在ArrayList等集合类加了一层壳,壳里面试用synchronized来加锁。
3.CopyOnWriteArrayList,让不同的线程使用不同的变量,并没有加锁。属于写时拷贝,多个线程来读取一份数据,某个线程进行了修改,立刻就给这个线程拷贝一份新的数据。
如何线程安全的使用队列(阻塞队列)
Java提供了多种阻塞队列:
1.ArrayBlockingQueue:底层是由数组实现的有界阻塞队列
2.LinkedBlockingQueue:底层是由链表实现的有界阻塞队列
3.PriorityBlockingQueue:优先级阻塞队列
4.TransferQueue:只包含一个元素的阻塞队列,生产一个元素加入队列时会一直阻塞直到其中的元素被消费
5.DelayQueue:创建元素时可以指定多久之后才能从队列中获取到当前元素
6.SynchronizedQueue:不存储元素,每一个存储必须等待一个取出操作
HashTable与concurrentHashMap的区别
concurrentHashCode:
1.并不是针对整个对象进行加锁,而是分成很多把锁,每个链表/红黑树进行加锁,只有当多个线程修改到同一个链表/红黑树才会发生锁竞争。
2.针对读操作直接不加锁,虽说是一个十分大胆的操作不过还好,大部分的场景状态下对读操作的线程安全并没有太高的要求,如果有则更加推荐读写锁的使用。
3.内部采用大量的CAS操作,提高效率
4.针对扩容进行了优化,HashTable的扩容特别麻烦,需要将整个表进行一次拷贝,如果轮到了哪个倒霉线程去执行,就需要负责整个扩容的过程,相对比来说,ConcurrentHashMap将扩容的任务分散开了,一次只扩容一点,能够更加平滑的过度。
HashTable:并不推荐使用,单纯的一个synchronized对整个哈希表进了加锁,坏处就是锁冲突会特别容易发生。
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
线程池
简述线程池
没有线程池的情况下,对于现成的创建和销毁将耗费大量的资源,如果能过对线程进行复用就能节省大量的资源,降低开销;线程池创建线程时会将线程封装为工作线程worker,worker执行完任务后会循环继续从工作队列中获取任务执行。
线程池常用类型
newCachedThreadPool:可缓存线程池,可设置最小线程数和最大线程数,线程空闲1分钟销毁
newFixedThreadPool:可指定工作线程的数量的线程池
线程池的状态
RUNNING:能够接受新的任务,并且也能处理阻塞队列中的任务
SHUTDOWN:不能够接受新的任务,但是能继续处理阻塞队列中的任务
STOP:不接受新任务也不处理阻塞队列中的任务,处理中的任务将被中断
TIDYING:所有任务都已终止,工作线程数为0
TERMINATED:执行terminated()方法后进入的状态
线程池参数
corePoolSize:核心线程数
maximumPoolSize:最大线程数(核心线程数+临时线程数)
keepAliveTime:允许临时线程空闲的时间(不累计)
unit:空闲的时间的基本单位
workQueue:线程池的任务队列,可以由程序员进行指定,指定的意义在于明确队列的长度以及是否需要带有优先级
handler:拒绝策略,如果线程池满了之后如何处理:
AbortPolicy:直接异常
CallerRunsPolicy:重新尝试提交任务
DiscardOldestPolicy:丢弃最老的任务
DiscardPolicy:丢弃当前的任务
如何选用则是具体情况具体分析。
threadFactor:线程工厂,创建线程用的辅助类,用于生产一组相同的任务
发放一个任务,线程池的几种工作场景
先检查线程池是否为RUNNING状态,不是的话拒绝任务
如果工作线程小于核心线程数则创建线程并执行新提交的任务
如果工作线程大于等于核心线程数并且阻塞队列没有满,则将任务放入阻塞队列中等待执行
如果工作线程大于等于核心线程数并且小于最大线程数,切阻塞队列满了,则创建新的线程执行该任务
如果工作线程数大于最大线程数并且阻塞队列已经满了,则执行拒绝策略处理该任务
多种锁策略
偏向锁
大多数情况下,锁的竞争状态并不存在,而加锁过程有浪费了大量的资源;每次进行加锁时就会判断偏向锁是否偏向自己,如果是则进入同步状态。
偏向锁的实现:
1.首先通过对象的Mark Word判断是不是偏向状态,如果不是则进入轻量型锁的判断。
2.判断请求锁的线程ID是否与偏向锁记录的ID是否一致,一致的话判断是否需要重偏向,如果不需要则直接获得偏向锁
3.利用CAS算法将记录的ID替换为当前线程的ID,如果成功则重偏向成功,获得偏向锁;如果失败则说明有多个线程竞争,升级为轻量型锁
乐观锁与悲观锁
乐观锁:这个锁认为出现锁竞争大概率比较小(当前场景中,线程数目比较少,不太涉及锁竞争),工作量比较小,付出的代价也比较少。读操作前不上锁,执行写操作才会进行判断,如果数据发生改变则返回false,如果数据没有改变则进行操作并返回true;通常乐观锁是基于CAS实现的。
悲观锁:这个锁认为出现锁竞争大概率比较大(当前场景中,线程数目比较多,非常可能涉及锁竞争),工作量比较大,付出的代价也比较多。
操作系统中的Mutex就是一个典型的悲观锁。Java中的synchronize既属于乐观锁也属于悲观锁,会根据当前锁冲突的状况来切换模式。
读写锁
这是一种特殊的锁,将读操作和写操作分别加锁,能够进一步减少锁冲突。一般具有以下三种情况:
读加锁和读加锁之间不发生互斥。
读加锁和写加锁之间发生互斥。
写加锁和写加锁之间发生互斥。
根据这三种情况不难判断出该锁更加适用于少写多读的场景当中。
重量型锁和轻量型锁
这两种锁非常类似于乐观锁和悲观锁,重量型锁大概率也是悲观锁,轻量型锁大概率也是乐观锁。乐观锁和悲观锁根据锁冲突来划分的,而重量型锁和轻量型锁根据工作量来划分的。
轻量型锁的实现:
1.如果同步对象没有被锁定,则在虚拟机在当前线程的栈帧中划分一个记录锁空间,存储锁对象目前Mark Word的拷贝。
2.虚拟机尝试使用CAS将Mark Word更新为指向锁的记录指针。
3.如果更新成功则表示这该线程拥有锁,锁标记为00;如果更新失败,就会检查Mark Word是否指向当前线程的栈帧,如果是则说明当前线程已经获取到锁,直接进入同步状态,如果不是则说明锁被另一个线程锁获取到了,这时就不再是轻量型锁了,而是膨胀为重量型锁。
自旋锁和自适应自旋锁
自旋锁:线程在竞争失败之后并未放弃CPU,而是进行一段时间的自旋,在自旋的过程中不断的尝试重新获取到锁
自适应自旋锁:自旋的时间不再是由人为设定的了,而是通过上一任锁拥有者的自旋时间和自旋状态来决定的了。
可重入锁和不可重入锁
一个线程对同一把锁进行两次加锁,若没有问题则成为可重入锁,若存在问题则成为不可重入锁。synchronized就属于一种可重入锁,通过对synchronized我们来了解一下可重入锁的机制。
在synchronized当中持有哪个线程获取到锁并且将会维护一个计数器,当该线程再次遇到加锁状况时并没有真正的加锁,而是计数器自增,同理可的当遇到解锁时并不会真正的解锁,而是计数器自减,直到计数器为0时才会真正地解锁。
死锁
死锁的出现往往伴随着线程挂掉等严重的Bug的出现:
1.一个线程一把锁之间通过可重入锁可完美的解决这个问题。
2.两个线程两把锁,线程一获取到锁A,在获取锁B,线程二获取到锁B在获取锁A,那么这个时候,线程一想要获取锁B则需要线程二解开锁B,而线程二想要获取锁A则需要线程一解开锁A,这样就形成了环路等待。
3.N个线程M把锁,与2情况相同原因,形成了环路等待。
解决方法:
1.一般的在锁当中不要轻易的加锁,不过说起来容易做起来难,很大的概率根据需求分析不可避免的锁上加锁。
2.约定一个固定的加锁顺序,例如要先加锁1,再加锁2,再加锁3。这样不会形成环路等待。
CAS
1.该算法认为竞争比较少
2.该算法的核心在于比较线程读取的值和内存中的值是否一致,如果一致则说明中间过程数据没有被操作,将该变量替换为新的值;如果不一致说明该数据被其他线程更改,则不进行任何操作。
ABA问题
即在CAS判断的过程中,一个线程读取值后,另一个线程将数据A操作为B,再将B操作为A,那么CAS很容易误认为该数据未进行其他操作。我们通过转账来分析一下:
JUC提供了一个AtomicStampedReference,即在原先的版本下加入一个版本戳,解决ABA问题
CountLatch
是指一个线程或多个线程等待其他线程完成任务之后才能执行。通过一个计数器来实现的,计数器的初始值为工作线程的个数,每有一个线程完成工作,就会调用countDown()方法来使计数器-1,如果计数器不为0,其他线程调用await()方法时就会进入阻塞状态,直到计数器为0才会执行其他等待的线程。只能使用一次,不能reset。
CyclicBarrier
与ConutLatch功能类似,也是通过计数器使一个或多个线程一组现成的完成。但是可以重复使用
Semaphore
semaphore信号量,是并发编程的一个重要概念,表示可用资源的数量。信号量涉及的核心操作:P操作:申请一个资源(可用资源-1),V操作:释放一个资源(可用资源+1)与此同时,可用资源变化的操作均属于原子操作。
semaphore持有acquire()方法和release()方法,分别对应P操作和V操作。当可用资源为0时执行acquire方法时线程就会进入阻塞状态,执行release方法时就会释放资源使可用资源+1,这时被阻塞的线程将会竞争这份资源,获取到之后就会继续执行。
锁升级
我们以synchronized来说一下什么是锁升级:
当一个线程进行操作时首先需要进行加锁,但此时并不是真的加锁,而是在对象头里面通过一个标志位进行标记。此时synchronized是偏向锁,在赌没有其他锁来竞争,往往赌还能赌成功。
此时又有一个线程来进行操作时,第二个线程就会尝试加锁,与此同时第一个线程将会立即获取锁,而后面的线程也将尝试真正的加锁,此时涉及到锁竞争,synchronized此时为轻量型锁。
越来越多的线程参与进锁竞争,竞争越来越激烈,自旋锁也慢慢的不容易获取到锁,并且还占用大量的CPU资源,此时synchronized就会继续膨胀为重量型锁,线程争取不到锁之后就会进入阻塞状态,争取到的就会继续工作。
总而言之,synchronized的锁膨胀/锁升级过程是为了更好地适应不同的环境,提高执行的效率。
锁粗化
锁的粒度代表synchronized所影响的范围。思想就是增加锁的粒度,扩大synchronized的影响范围,避免重复加锁解锁
锁清除
初学程序员写代码过程中可能会多次无意义的加锁而导致程序效率变低,JVM如果将该程序判定为不涉及线程安全问题,就会自动将锁去掉。例如单线程变成的情况下,使用StringBuffer将会涉及到无意义的加锁,编译器判定在一个线程内完成,就不会再加锁,而是从编译生成的字节码当中直接去除加锁过程。
有了Synchronized为什么还需要ReentrantLock
ReentrantLock是JUC(java.util.concurrent)的一个组件,并且也是一个可重入锁。ReentrantLock有两个方法,一个是lock()用于加锁,一个是unlock()用于解锁,这样的做法有优点也有缺点:
缺点:程序员容易忘记解锁从而导致Bug的产生。
优点:更加的灵活,可以将lock()放在一个方法里,unlock()放进另一个方法里。
ReentrantLock还提供了另一个方法tryLock(),对于synchronized来说,如果锁被占用了,将会进入阻塞状态,直到对方释放锁,不管这个时间多久都会一直等待;而对于tryLock()来说,此处不留爷自有留爷处,tryLock()将会立即返回或者等待一段时间后返回。这样就会导致节省了很多的资源以及更多的操作空间。
区别:
synchronized 竞争锁时会一直等待;ReentrantLock 可以尝试获取锁,并得到获取结果
synchronized 获取锁无法设置超时;ReentrantLock 可以设置获取锁的超时时间
synchronized 无法实现公平锁;ReentrantLock 可以满足公平锁,即先等待先获取到锁
synchronized 控制等待和唤醒需要结合加锁对象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和唤醒需要结合 Condition 的 await() 和 signal()、signalAll() 方法
synchronized 是 JVM 层面实现的;ReentrantLock 是 JDK 代码层面实现
synchronized 在加锁代码块执行完或者出现异常,自动释放锁;ReentrantLock 不会自动释放锁,需要在 finally{} 代码块显示释放
JVM
JVM模型
堆:堆的主要作用在于存储对象的实例以及数组,占有JVM最大的空间,垃圾回收主要针对的就是这个区域,抛出的异常类型OutOfMemoryError
方法区:用于存储类信息,静态变量和常量,抛出的异常类型OutOfMemoryError
程序计数器:只占有很小的一片空间,用来存储当前线程执行字节码的地址
本地方法栈:和栈很相似,带有native关键字,是虚拟机栈为虚拟机执行java代码提供服务的,是JVM内部的c++代码的调用栈。
JVM栈:java代码的调用栈,生命周期与线程相等。每个方法的执行都会划分一个栈帧空间,存储的有局部变量,操作数栈,动态链接,出口等信息。JVM栈存储了一个个的栈帧
局部变量:存储8个基本数据类型,引用类型和returnAddress(存储return之后的字节码地址)
操作数栈:用于对数据进行操作的空间
动态链接:此方法若调用了其他方法,则需要链接到其他方法的地址,这个过程就称为动态链接。
出口方法:存储return和异常处理
JVM内存参数
-Xms:设置初始堆大小
-Xmx:设置最大堆大小
-Xmn:设置堆中年轻代大小
-XX:NewSize=n:设置年轻代初始大小
-XX:MaxNewSize=n:设置年轻代最大值
-XX:NewRatio=n:设置年轻代和年老代的比值
-Xss:设置每个线程的堆栈大小
-XX:ThreadStackSize=n:线程堆栈大小
-XX:PermSize=n:设置持久代大小
-XX:MaxTenuringThreshold=n:设置年轻代垃圾对象最大年龄
垃圾回收机制
内存管理
1.申请内存:在创建实例对象时申请
2.释放内存(时机不确定):当对象不被使用的时候就可以被回收
3.内存泄漏:长期的不回收内存就会导致可用的内存越来越少,直到程序崩溃
判断垃圾
1.引用计数法(Java不使用):创建一个对象时就会分配一个计数器,每有一个引用指向对象则计数器+1,引用失效时计数器-1,计数器为0时就可以被回收
优点:回收迅速
缺点:计数器改变时涉及线程安全,需要加锁,意味着可能占用更多资源
循环引用时可能会导致死循环,进而无法回收对象
2.可达性算法:从GCRoot出发,不断地扫描对象间的引用关系,如果能够到达该对象则为“可达”,否则为“不可达”,可以被回收
GCRoot的选取:栈上的局部变量表的引用的对象;常量池中的引用的对象;类静态引用的对象
四种引用类型
强引用:被强引用的对象不会被回收
软引用:在内存不够的情况下被回收
弱引用:碰到垃圾回收器就会被回收
虚引用:无法引用,只能在被回收时获得系统的通知
垃圾回收算法
1.标记清除法:标记出垃圾之后直接清除
优点:回收及时
缺点:形成内存碎片,使得内存利用率降低
2.复制算法:将内存分为两半,标记垃圾,之后将未标记部分复制到另一边,再将之前部分清除
优点:消除了内存碎片
缺点:每次只能使用一般的内存,内存利用率低;回收垃圾越少越低效
3.标记整理:类似于顺序表中的删除元素
优点:消除了内存碎片提高内存利用率
缺点:内存频繁被移动,效率降低
4.分代回收:将内存分为不同的区域,每个区域采用不同的算法
新生代:复制算法
老年代:标记清除或者标记整理
在伊甸区中存储新创建的对象
采用可达性算法不断地扫描新生代,一般的大多数对象活不过伊甸区,伊甸区的对象撑过一轮后就会进入到幸存区当中
两个幸存区相互配合使用复制算法进行垃圾清除
每撑过一轮GC年龄就会+1,到达一定的年龄就会被移动至老年代,一般的老年代的对象被回收的概率比较低,因此老年的的扫描频率降低很多
几种垃圾收集器
Serial收集器:单线程串行收集器,在垃圾回收时需要停止其他线程的工作,在新生代当中使用复制算法,在老年代中使用标记整理,整体简单高效。
ParNew收集器:可以理解为Serial的多线程版本
Parallel Scavenge垃圾收集器:注重吞吐量即 CPU代码运行时间/(CPU代码运行时间+垃圾回收时间)
CMS垃圾收集器:CMS注重最短停顿时间,是最早提出的并发式垃圾收集器,即垃圾收集线程与用户线程一并工作。该收集器分为以下几个步骤:
1.初始标记:暂停其他线程,标记直接与GCRoot有关的对象
2.并发标记:可达性算法过程
3.并发预清理:查找 执行并发标记阶段 中从新生代晋升为老年代的线程,重新标记,暂停虚拟机,对CMS堆中剩余的对象
4.并发清除:清除垃圾对象
5.并发重置:将收集器进行重置
G1垃圾回收器:将堆空间划分为多个大小相等的独立区域,新生代和老年代不再物理隔离,对每个小区域单独进行垃圾回收。该收集器分为以下几个步骤:
1.初始标记:标记与GCRoot直接关联的对象
2.并发标记:可达性算法
3.最终标记:将并发标记过程中,用户线程操作的对象再次标记
4.筛选回收:将每个独立区域的回收价值和回收成本进行排序,根据用户所期望的GC停顿时间指定计划并回收
几种专业术语
Minor GC:新生代的垃圾回收器,因为新生代的存活时间普遍偏短,所以Minor GC回收速度很快
Full GC:清理整个堆空间包括年轻代和永久代,调用System.gc()方法实现
Particlial GC:进行部分区域的垃圾回收
Major GC:针对新生代+老年代的垃圾回收
类加载机制
类加载的过程
1.加载:找到执行的.class文件,解析.class文件格式存储到内存当中
2.链接:类与类之间相互配合,将加载类依赖的类一并加载
3.初始化:对类对象进行初始化(初始化静态成员变量,执行静态代码块)
几种类加载器
BootstrapClassLoader:负责加载Java标准库中的类
ExtensionClassLoader:负责加载JVM扩展的类
ApplicationClassLoader:负责加载用户自定义的类
双亲委派模型
一个类加载起受到类加载请求后会先判断这个类是否已经被加载,如果被加载那么直接返回;如果没有被加载,那么类加载器就会询问他的父类加载器,直到询问到启动类加载器,如果父类加载器无法完成才会尝试自己加载。
优点:避免了相同类名的不同加载,保证了Java程序能够稳定执行;保证核心API不被修改
自定义类加载器
新建自定义类继承自java.lang.ClassLoader,重写findClass,loadClass,defineClass方法
计算机网络原理
TCP/IP5层模型
应用层:使用户能够访问网络,并为各类应用提供相应服务
传输层:实现源端到目的端的传输,即一个进程到另一个进程的传输
网络层:将数据报(数据段封装成报)从源地址传输到目的地址,即从一台主机传输到另一台主机
数据链路层:将数据帧(数据报封装成帧)从一个节点发送到另一个节点
物理层:在物理信道里面使用比特流传输
OIS7层模型
应用层,表示层,会话层,传输层,网络层,数据链路层,物理层
TCP/IP对应的每个层次常用的协议
应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet
传输层:TCP,UDP
网络层:IP,RIP,OSPF
传输层:ARP
物理层:ISO211.0
UDP
1.协议格式:
其中2字节校验和若出错则直接丢弃,2字节数据包长度包含UDP数据+UDP首部
2.特点:无连接:知道端地址和端口号之后直接传输,不需要建立连接
不可靠:无重传机制和确认机制,一旦丢包无从可知
面向数据报:应用层传输过来的报文原样传出
3.优点:传输速度快,可以广播传输
缺点:不可靠,无重传机制,无确认机制,即如果传输过程中发生丢包,我们无法得知;数据长度64kb不适合大文件的传输
4.如何保证UDP的可靠性传输:我们可以根据TCP的可靠传输来进行分析,可以为UDP添加超时重传和确认应答机制以及有序接受的机制,比如可以在应用层实现可靠传输机制传输,使用UDP数据报+序列号,UDP数据报+时间戳等方法(加上序列号确保有序,加上确认应答机制保证数据被成功发送,加上超时重传机制)
TCP
协议格式:
源端口号/目的端口号:从哪个进程来,到哪个进程
4位首部长度:表示TCP头部有多少个32bit
URG:紧急指针是否有效
ACK:确认序号
PSH:提示接收端应用程序立即将缓冲区中的数据取走
RST:对方请求重新连接,称为复位报文段
SYN:请求连接,称为同步报文段
FIN:通知即将关闭连接,称为结束报文段
检验和:发送端填充,CRC检验和,接收端进行校验,如果不通过则认为数据有问题
紧急指针:表示哪些是紧急数据
确认应答机制
发送方发送数据,将数据每个字节编上号码即序列号,接收方接收数据后返回最后一位序列号+1充当ACK,表示前多少个数据顺利接收,你从ACK开始发送数据。
超时重传机制
当发送方发送数据一段时间后没有收到ACK确认,那么就会再次发送数据,此时有两种情况,一种是数据丢失,另一种是ACK丢失,若为ACK丢失,那么再次重传后接收方就会接收到两次数据,但是得益于接收方的缓冲区,缓冲区会判断发送的序列号是否需要去重。等待的时间是不确定的切逐渐变久的,时间到达一定程度就会认为是网络问题从而断开连接。
连接管理
TCP三次握手:本质:确认通信双方接受能力发送能力正常
TCP四次挥手:
ACK与FIN合并/不合并的理由:
发送ACK是操作系统内核的行为。在收到FIN时发送ACK。发送FIN是应用程序的行为,在执行代码中的“close()”才会发送FIN。因此不合并。
如果close()能够迅速触发,即发送ACK与发送FIN的时间间隔很短,就能触发“捎带应答+延迟应答”的机制一起发送。因此可以合并。
TCP过程的各种状态:
ESTABLISHED :连接成功,可以进行后续通信。
LISTIN:服务器端的状态:允许客户端随时建立连接。
CLOSE_WAIT:断开连接时服务器端的一个中间状态。存在于接收FIN,发送ACK,发送FIN的时间点。
TIME_WAIT:断开连接时主动发起的客户端的一个状态,防止最后一个ACK丢包。发送ACK后等待一段时间,若服务器端发送FIN则ACK丢包,继续发送ACK。TIME_WAIT=2MSL,MSL为端与端传输数据的最大时间间隔。
滑动窗口
目的是为了在保证可靠性的情况下提高效率。
窗口内的数据就是发送的一系列的数据并且等待一系列ACK的响应,当收到ACK后,等待响应的数据就会发生改变,与此同时发送新的数据,就类似于一个窗口在滑动
若ACK丢失,则接收到后续的ACK就能确定前面的数据发送成功,比如说发送1-1000的数据后ACK=1001丢失,而1001-2000的数据返回的ACK为2001成功到达,那么就会认为1-2000数据成功发送。
若数据包丢丢失,则服务器端后续发送给客户端的数据报中ACK 为成功传输数据包的ACK,如1-1000成功发送,1001-2000丢包则接收2001-3000的数据包返回的依然是ACK=1001,多次返回1001后,客户端重新发送1001-2000数据包。以此类推。
流量控制
目的:为了使发送的速率和接收方的速率可能一致,提高数据传输的效率
方法:发送方发送数据后,接收方返回一个ACK以及一个“缓冲区空余空间”。根据这个大小来决定窗口的大小。若无空余空间,则每隔一段时间发送方发送试探报文来获取一个ACK和缓冲区空余空间。
缓冲区空余空间存储在TCP报头的16位窗口大小的字段中来确定窗口大小。
拥塞控制
发送方发送的初始窗口为一个较小的窗口,若无丢包行为则逐渐增大,若丢包则减小窗口大小,循环此过程。窗口先指数增加,到达阈值后线性增加,少量丢包后执行超时重传,大量丢包后就认为网络拥塞,窗口大小重新定为1,阈值为拥塞时窗口大小的一半。
延迟应答机制
窗口越大吞吐量就会越大,传输效率也会越高,我们的目的是在不导致网络拥塞的情况下提高传输效率。如果接收方接收到数据立即返回ACK,那么此时窗口可能会很小,此时接收方会选择等待一段时见之后再将相应数据一起返回。
捎带应答机制
建立在延迟应答的基础上,将需要发送的数据并在一起发送,减少了传输数据包的个数,降低传输成本,提高传输效率。
面向字节流
得益于内核的发送缓冲区和接收缓冲区,在发送数据的时候,会先将数据以字节的形式读入发送缓冲区
如果发送的字节太多就会将TCP报文拆分成好几份来发送
如果发送的字节太少就会在缓冲区等待,等字节数差不多了或者合适的时机到了再发送
发送的数据将被存储到接收缓冲区当中,接收方接受数据就从缓冲区中读取
由于缓冲区的缘故,可以不必将读和写操作进行一一对应
粘包问题
存储在缓冲区的数据对于应用层来说就是一长串数据,并不能分清从哪一步分到哪一部分属于一个完整的TCP报文。
对于定长的包来说只要按照固定的长度读取就可以
对于不定长的包来说可以在包头部分添加一个总包长度字段,这样就可以知道结尾处了;也可以使用一个显式的分隔符进行划分
UDP传输是不存在粘包问题的,对于应用层来说,UDP是将数据一个一个传输给应用层的,应用层不存在接收到半个数据报的情况
TCP与UDP的对比
TCP:有连接,可靠,面向字节流
UDP:无连接,不可靠,面向数据报
TCP应用于可靠传输的重要场景:比如重要文件的传输,重要状态的更新
UDP应用于对于可靠性要求不高但是对高速传输和实时性要求高的场景,比如视频传输等
IP协议
-
版本:常用IPV4,IPV6
-
4位首部长度能表示4*15
-
TOS:切换IP协议模式,只有四位能用,分别表示:最小延时,最大吞吐量,最高可靠性,最小成本,其中四者是相互冲突的。
-
16位总长度:不同于UDP只支持64kb,IP协议支持拆包和组包.
-
16位标识:当一个IP数据报触发分包机制,分成多个包时,拥有同一个标识。
-
3位标志:识别这个包是不是最后一个包。
-
13位片偏移:区分出若干个包,谁在前,谁在后。
-
8位生存时间:有一个初始生存时间,每经过一个设备转发就会-1,到0时就认为不会到达目的端,将其丢弃。
-
8位协议:明确指出是传输层的哪个协议。
-
16位首部检验和:验证首部是否正确。
-
32位源IP地址:发送方地址
-
32位目的IP地址:接收方地址
网段划分
1.传统划分:
2.CIDR:通过子网掩码来划分:子网掩码与IP地址相与操作得到网络号,剩余位为主机号
3.特殊IP地址:主机号全为0,为网络号;主机号全为1,为广播地址,127.*为本机回环地址
4.局域网可用的IP地址范围为:
A类地址:10.0.0.0 - 10.255.255.255
B类地址:172.16.0.0 - 172.31.255.255
C类地址:192.168.0.0 -192.168.255.255
IPV4地址枯竭
动态划分IP地址:当一台设备关闭时回收IP地址,请求时在重新分配。
NAT机制:将原IP地址替换,使用一个外网IP替代多个内网IP,多个内网IP通过端口号来进行区分。
IPV6:IPV4的升级版,保证地址绝对够用,但与IPV4不兼容。
以太网协议
源地址/目的地址:指的是MAC地址,长度为48位,出厂时固定
帧协议字段类型有三个值:IP,ARP,RARP
帧末尾是校验和
MAC
即物理地址,和主机的网卡绑定,唯一的,不能修改的
IP地址立足于全局,用于网络规划
MAC地址立足于局域网,专注于相邻节点的通信
将IP数据报加入帧头和帧尾形成以太网数据帧
ARP协议
建立IP地址与MAC地址之间的映射关系。
ARP每隔一段时间就会对当前局域网进行广播,把ARP请求发送到局域网内每个设备,设备会发送一个ARP响应,其中包括设备的IP地址和MAC地址,路由器会对其进行一一对应并存储。
DNS应用层协议(域名解析系统)
查询网页时,IP地址难以记忆,用域名来进行查询方便很多。DNS将域名转换为IP地址。
用户通过域名查询网站时都要访问DNS服务器,此时服务器承担很大的压力,一般有两种处理方法:
1.缓存:访问后存储到主机
2.分布式:每个主机访问DNS时候,就近访问DNS服务器,DNS服务器根据域名分级查询,每个DNS服务器不比装载过多的域名
HTTP(超文本传输)
URL
urlencode和urldecode
像 / ? : 等这样的字符, 已经被url当做特殊意义理解了. 因此这些字符不能随意出现. 比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义. 转义的规则如下:将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%, 编码成%XY格式,这就是urlencode的过程,urldecode是这个过程的逆过程。
HTTP协议格式
1.HTTP请求:
首行: [方法] + [url] + [版本]
Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分 结束
Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;
2.HTTP响应:
首行: [版本号] + [状态码] + [状态码解释]
Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分 结束
Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有 一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页 面内容就是在body中.
状态码
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向),504(Bad Gateway)
HTTP常见的Header
Content-Type: 数据类型(text/html等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息;
HTTP VS HTTPS
HTTP1.0,HTTP1.1,HTTP2.0的区别
http 1.0
短连接
每一个请求建立一个TCP连接,请求完成后立马断开连接。这将会导致2个问题:连接无法复用,head of line blocking
连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大。head of line blocking会导致带宽无法被充分利用,以及后续健康请求被阻塞。
http 1.1
长连接:
通过http pipelining实现。多个http 请求可以复用一个TCP连接,服务器端按照FIFO原则来处理不同的Request
增加connection header
该header用来说明客户端与服务器端TCP的连接方式,若connection为close则使用短连接,若connection为keep-alive则使用长连接
身份认证
状态管理
Cache缓存等机制相关的请求头和响应头
增加Host header
http 2.0
多路复用 (Multiplexing)
多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。在 HTTP/1.1 协议中浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞。 HTTP/2 的多路复用(Multiplexing) 则允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。因此 HTTP/2 可以很容易的去实现多流并行而不用依赖建立多个 TCP 连接,HTTP/2 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。并行地在同一个 TCP 连接上双向交换消息。
二进制分帧
HTTP/2在 应用层(HTTP/2)和传输层(TCP or UDP)之间增加一个二进制分帧层。在不改动HTTP/1.x 的语义、方法、状态码、URI 以及首部字段的情况下, 解决了HTTP1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。在二进制分帧层中, HTTP/2 会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码 ,其中 HTTP1.x 的首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面。
HTTP/2 通信都在一个连接上完成,这个连接可以承载任意数量的双向数据流。在过去, HTTP 性能优化的关键并不在于高带宽,而是低延迟。TCP 连接会随着时间进行自我调谐,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度。这种调谐则被称为 TCP 慢启动。由于这种原因,让原本就具有突发性和短时性的 HTTP 连接变的十分低效。HTTP/2 通过让所有数据流共用同一个连接,可以更有效地使用 TCP 连接,让高带宽也能真正的服务于 HTTP 的性能提升。
这种单连接多资源的方式,减少服务端的链接压力,内存占用更少,连接吞吐量更大;而且由于 TCP 连接的减少而使网络拥塞状况得以改善,同时慢启动时间的减少,使拥塞和丢包恢复速度更快。
首部压缩(Header Compression)
HTTP/1.1并不支持 HTTP 首部压缩,为此 SPDY 和 HTTP/2 应运而生, SPDY 使用的是通用的DEFLATE 算法,而 HTTP/2 则使用了专门为首部压缩而设计的 HPACK 算法。