1.什么是字节码?字节码的好处是什么?
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的⽂件),它不⾯向任何特定的处理器,只⾯向虚拟机。Java 语⾔通过字节码的⽅式,在⼀定程度上解决了传统解释型语⾔执⾏效率低的问题,同时⼜保留了解释型语⾔可移植的特点。所以 Java 程序运⾏时比较⾼效,⽽且,由于字节码并不针对⼀种特定的机器,因此,Java 程序⽆须重新编译便可在多种不同操作系统的计算机上运⾏。
2.Java与C++的区别?
都是⾯向对象的语⾔,都⽀持封装、继承和多态;
Java 不提供指针来直接访问内存,程序内存更加安全;
Java 的类是单继承的,C++ ⽀持多重继承;虽然 Java 的类不可以多继承,但是接⼝可以多继承;
Java 有⾃动内存管理机制,不需要程序员⼿动释放⽆⽤内存;
在 C 语⾔中,字符串或字符数组最后都会有⼀个额外的字符‘0’来表示结束。但是,Java 语⾔中没有结束符这⼀概念。
3.重写和重载的区别?
重载就是同⼀个类中多个同名⽅法根据不同的传参来执⾏不同的逻辑处理。
重写就是⼦类对⽗类⽅法的重新改造,外部样⼦不能改变,内部逻辑可以改变。
返回值类型、⽅法名、参数列表必须相同,抛出的异常范围⼩于等于⽗类,访问修饰符范围 ⼤于等于⽗类。 如果⽗类⽅法访问修饰符为 private/final/static 则⼦类就不能重写该⽅法,但是被 static 修饰 的⽅法能够被再次声明。 构造⽅法⽆法被重写。
4.String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的
可变性
String类中使用字符数组保存字符串,private final char value[],所以string对象是不可变的。StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[] value,这两种对象都是可变的。
线程安全性
String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
StringBuilder>StringBuffer>String
对于三者使用的总结
如果要操作少量的数据用 = String
单线程操作字符串缓冲区下操作大量数据 = StringBuilder
多线程操作字符串缓冲区下操作大量数据 = StringBuffer
5.==与equals()区别?
== : 它的作⽤是判断两个对象的地址是不是相等。即,判断两个对象是不是同⼀个对象(基本数据 类型==⽐的是值,引⽤数据类型==⽐的是内存地址)。 equals() : 它的作⽤也是判断两个对象是否相等。但它⼀般有两种使⽤情况: 情况 1:类没有覆盖 equals() ⽅法。则通过 equals() ⽐该类的两个对象时,等价于通过 “==”⽐这两个对象。 情况 2:类覆盖了 equals() ⽅法。⼀般,我们都覆盖 equals() ⽅法来⽐两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
6.简述线程、程序、进程的基本概念。以及他们之间关系是什么?
线程与进程相似,但线程是⼀个⽐进程更⼩的执⾏单位。⼀个进程在其执⾏的过程中可以产⽣多 个线程。与进程不同的是同类的多个线程共享同⼀块内存空间和⼀组系统资源,所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程。 程序是含有指令和数据的⽂件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。 进程是程序的⼀次执⾏过程,是系统运⾏程序的基本单位,因此进程是动态的。系统运⾏⼀个程序即是⼀个进程从创建,运⾏到消亡的过程。简单来说,⼀个进程就是⼀个执⾏中的程序,它在 计算机中⼀个指令接着⼀个指令地执⾏着,同时,每个进程还占有某些系统资源如 CPU 时间, 内存空间,⽂件,输⼊输出设备的使⽤权等等。换句话说,当程序在执⾏时,将会**作系统载 ⼊内存中。 线程是进程划分成的更⼩的运⾏单位。线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各 线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。从另⼀⻆度来说,进程属于操作系 统的范畴,主要是同⼀段时间内,可以同时执⾏⼀个以上的程序,⽽线程则是在同⼀程序内⼏乎 同时执⾏⼀个以上的程序段。
区别:线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
7.深拷贝和浅拷贝的区别?
浅拷⻉:对基本数据类型进⾏值传递,对引⽤数据类型进⾏引⽤传递般的拷⻉,此为浅拷⻉。 深拷⻉:对基本数据类型进⾏值传递,对引⽤数据类型,创建⼀个新的对象,并复制其内容,此为深拷⻉。
8.Java 序列化中如果有些字段不想进⾏序列化,怎么办?
对于不想进⾏序列化的变量,使⽤ transient 关键字修饰。
transient 关键字的作⽤是:阻⽌实例中那些⽤此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和⽅法。
9.HashMap的底层实现?
JDK1.8之前:


相⽐于 JDK1.8 的 hash ⽅法 ,JDK 1.7 的 hash ⽅法的性能会稍差⼀点点,因为毕竟扰动了 4 次。 所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建⼀个链表数组,数组中每⼀格就是⼀ 个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
10.HashMap 的⻓度为什么是2的幂次⽅?
为了能让 HashMap 存取⾼效,尽量少碰撞,也就是要尽量把数据分配均匀。我们上⾯也讲到过了,Hash 值的范围值-到,前后加起来⼤概40亿的映射空间,只要哈希函数映射得⽐均匀松散,⼀般应⽤是很难出现碰撞的。但问题是⼀个40亿⻓度的数组,内存是放不下的。所以这个散列值是不能直接拿来⽤的。⽤之前还要先做对数组的⻓度取模运算,得到的余数才能⽤来要存放的位置也就是对应的数组下标。这个数组下标的计算⽅法是“ (n - 1) & hash ”。(n代表数组⻓度)。这也就解释了 HashMap 的⻓度为什么是2的幂次⽅。 这个算法应该如何设计呢?我们⾸先可能会想到采⽤%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2 的幂次则等价于与其除数减⼀的与(&)操作(也就是说 hash%length==hash&(length-1)的前提 是 length 是2的 n 次⽅;)。” 并且 采⽤⼆进制位操作 &,相对于%能够提⾼运算效率,这就解释了 HashMap 的⻓度为什么是2的幂次⽅。那为什么是两次扰动呢?
答:这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;
11.HashMap的默认加载因子为什么是0.75?
提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小,
HashMap有两个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动扩容之前可以达到多满的一种度量。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行扩容、rehash操作(即重建内部数据结构),扩容后的哈希表将具有两倍的原容量。
通常,加载因子需要在时间和空间成本上寻求一种折衷。
加载因子过高,例如为1,虽然减少了空间开销,提高了空间利用率,但同时也增加了查询时间成本;
加载因子过低,例如0.5,虽然可以减少查询时间成本,但是空间利用率很低,同时提高了rehash操作的次数。
在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少rehash操作次数,所以,一般在使用HashMap时建议根据预估值设置初始容量,减少扩容操作。
选择0.75作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择
12.ConcurrentHashMap线程安全的具体实现⽅式/底层具体实现?
JDK1.7:
⾸先将数据分为⼀段⼀段的存储,然后给每⼀段数据配⼀把锁,当⼀个线程占⽤锁访问其中⼀个段数据时,其他段的数据也能被其他线程访问。ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。 Segment 实现了ReentrantLock ,所以 Segment 是⼀种可重⼊锁,扮演锁的⻆⾊。 HashEntry ⽤于存储键值对数据。
⼀个 ConcurrentHashMap ⾥包含⼀个 Segment 数组。 Segment 的结构和 HashMap 类似,是⼀种数组和链表结构,⼀个 Segment 包含⼀个 HashEntry 数组,每个 HashEntry 是⼀个链表结构的元素,每个 Segment 守护着⼀个 HashEntry 数组⾥的元素,当对 HashEntry 数组的数据进⾏修改时,必须⾸先获得对应的 Segment 的锁。
JDK1.8:
ConcurrentHashMap 取消了 Segment 分段锁,采⽤ CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红⿊⼆叉树。Java 8 在链表⻓度超过⼀定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红⿊树(寻址时间复杂度为 O(log(N))) synchronized 只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要 hash 不冲突,就不会产⽣并发,效率⼜提升 N 倍。
13.什么是线程和进程?
何为进程?
进程是程序的⼀次执⾏过程,是系统运⾏程序的基本单位,因此进程是动态的。系统运⾏⼀个程 序即是⼀个进程从创建,运⾏到消亡的过程。 在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,⽽ main 函数所在的线 程就是这个进程中的⼀个线程,也称主线程。 如下图所示,在 windows 中通过查看任务管理器的⽅式,我们就可以清楚看到 window 当前运⾏ 的进程(.exe ⽂件的运⾏)。
何为线程?
线程与进程相似,但线程是⼀个⽐进程更⼩的执⾏单位。⼀个进程在其执⾏的过程中可以产⽣多 个线程。与进程不同的是同类的多个线程共享进程的堆和⽅法区资源,但每个线程有⾃⼰的程序 计数器、虚拟机栈和本地⽅法栈,所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作 时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程。 Java 程序天⽣就是多线程程序,我们可以通过 JMX 来看⼀下⼀个普通的 Java 程序有哪些线 程,代码如下。
上述程序输出如下(输出内容可能不同,不⽤太纠结下⾯每个线程的作⽤,只⽤知道 main 线程 执⾏ main ⽅法即可):
从上⾯的输出内容可以看出:⼀个 Java 程序的运⾏是 main 线程和多个其他线程同时运⾏。
14.一句话简单了解堆和⽅法区?
堆和⽅法区是所有线程共享的资源,其中堆是进程中最⼤的⼀块内存,主要⽤于存放新创建的对象 (所有对象都在这⾥分配内存),⽅法区主要⽤于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
15.什么是上下⽂切换?
多线程编程中⼀般线程的个数都⼤于 CPU 核⼼的个数,⽽⼀个 CPU 核⼼在任意时刻只能被⼀个线程使⽤,为了让这些线程都能得到有效执⾏,CPU 采取的策略是为每个线程分配时间⽚并轮转的形式。当⼀个线程的时间⽚⽤完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切换。 概括来说就是:当前任务在执⾏完 CPU 时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换。
16.什么是线程死锁?如何避免死锁?
线程死锁描述的是这样⼀种情况:多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释放。由于线程被⽆限期地阻塞,因此程序不可能正常终⽌。
产⽣死锁必须具备以下四个条件:
1. 互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。 2. 请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。 3. 不剥夺条件:线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后才释放资源。 4. 循环等待条件:若⼲进程之间形成⼀种头尾相接的循环等待资源关系。
17.如何避免线程死锁?
为了避免死锁,我们只要破坏产⽣死锁的四个条件中的其 中⼀个就可以了。现在我们来挨个分析⼀下: 1. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。 2. 破坏请求与保持条件 :⼀次性申请所有的资源。 3. 破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 4. 破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。 破坏循环等待条件。 我们对线程 2 的代码修改成下⾯这样就不会产⽣死锁了。
18.说说 sleep() ⽅法和 wait() ⽅法区别和共同点?
两者最主要的区别在于:
sleep() ⽅法没有释放锁,⽽ wait() ⽅法释放了锁 。
两者都可以暂停线程的执⾏。
wait() 通常被⽤于线程间交互/通信, sleep() 通常被⽤于暂停执⾏。
wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或者 notifyAll() ⽅法。 sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(long timeout) 超时后线程会⾃动苏醒。
19.为什么我们调⽤ start() ⽅法时会执⾏ run() ⽅法,为什么我 们不能直接调⽤ run() ⽅法?
new ⼀个 Thread,线程进⼊了新建状态。调⽤ start() ⽅*启动⼀个线程并使线程进⼊了就绪状态,当分配到时间⽚后就可以开始运⾏了。 start() 会执⾏线程的相应准备⼯作,然后⾃动执⾏ run() ⽅法的内容,这是真正的多线程⼯作。 但是,直接执⾏ run() ⽅*把 run() ⽅法当成⼀个 main 线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。 总结: 调⽤ start() ⽅法⽅可启动线程并使线程进⼊就绪状态,直接执⾏ run() ⽅法的话不会 以多线程的⽅式执⾏。
20.说说⾃⼰是怎么使⽤ synchronized 关键字?
synchronized 关键字最主要的三种使⽤⽅式:
1.修饰实例⽅法: 作⽤于当前对象实例加锁,进⼊同步代码前要获得 当前对象实例的锁
2.修饰静态⽅法: 也就是给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得当前 class 的锁。因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管 new 了多少个对象,只有⼀份)。所以,如果⼀个线程 A 调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized ⽅法, 是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。
3.修饰代码块 :指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进⼊同步代码库前要获得给定对象的锁。 synchronized(.class) 表示进⼊同步代码前要获得当前 class 的锁
21.单例模式了解吗?来给我⼿写⼀下!给我解释⼀下双重检验锁⽅式实现 单例模式的原理呗!”
双重校验锁实现对象单例(线程安全)
另外,需要注意 uniqueInstance 采⽤ volatile 关键字修饰也是很有必要。 uniqueInstance 采⽤ volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这 段代码其实是分为三步执⾏: 1. 为 uniqueInstance 分配内存空间 2. 初始化 uniqueInstance 3. 将 uniqueInstance 指向分配的内存地址 但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1->3->2。指令重排在单线程环境下不 会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执 ⾏了 1 和 3,此时 T2 调⽤ getUniqueInstance () 后发现 uniqueInstance 不为空,因此返回 uniqueInstance ,但此时 uniqueInstance 还未被初始化。
使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。
22.讲⼀下 synchronized 关键字的底层原理?
synchronized同步语句块的实现使⽤的是monitorenter和monitorexit指令,其中 monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置。synchronized修饰的⽅法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该⽅法是⼀个同步⽅法。不过两者的本质都是对对象监视器monitor的获取。
第一个monitorexit指令是同步代码块正常释放锁的一个标志;
如果同步代码块中出现Exception或者Error,则会调用第二个monitorexit指令来保证释放锁。
23.讲⼀下 JMM(Java 内存模型)
JMM : Java内存模型,不存在的东西,概念!约定!
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存。
2、线程加锁前,必须读取主存中的最新值到工作内存中!
3、加锁和解锁是同一把锁
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类的变量来说,load、store、read和write操作在某些平台上允许例外)
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
24.如何创建线程池?
《阿⾥巴巴 Java 开发⼿册》中强制线程池不允许使⽤ Executors 去创建,⽽是通过 ThreadPoolExecutor 的⽅式,这样的处理⽅式让写的同学更加明确线程池的运⾏规则,规避资源耗尽的⻛险。
Executors 返回线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor :允许请求的队列⻓度为 Integer.MAX_VALUE ,可能堆积⼤量的请求,从⽽导致 OOM。
CachedThreadPool和ScheduledThreadPool :允许创建的线程数量为 Integer.MAX_VALUE,可能会创建⼤量线程,从⽽导致 OOM。
⽅式⼀:通过构造⽅法实现
⽅式⼆:通过 Executor 框架的⼯具类 Executors 来实现
我们可以创建三种类型的 ThreadPoolExecutor:
FixedThreadPool : 该⽅法返回⼀个固定线程数量的线程池。该线程池中的线程数量始终 不变。当有⼀个新的任务提交时,线程池中若有空闲线程,则⽴即执⾏。若没有,则新的任 务会被暂存在⼀个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 SingleThreadExecutor: ⽅法返回⼀个只有⼀个线程的线程池。若多余⼀个任务被提交到该线程池,任务会被保存在⼀个任务队列中,待线程空闲,按先⼊先出的顺序执⾏队列中的任务。
CachedThreadPool: 该⽅法返回⼀个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复⽤,则会优先使⽤可复⽤的线程。若所有线程均在⼯作,⼜有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执⾏完毕后, 将返回线程池进⾏复⽤。
对应 Executors ⼯具类中的⽅法如图所示:
25.ThreadPoolExecutor 类分析?
ThreadPoolExecutor 构造函数重要参数分析 ThreadPoolExecutor 3 个最重要的参数: corePoolSize : 核⼼线程数线程数定义了最⼩可以同时运⾏的线程数量。 maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运⾏的线程数 量变为最⼤线程数。 workQueue : 当新任务来的时候会先判断当前运⾏的线程数量是否达到核⼼线程数,如果达到的话,新任务就会被存放在队列中。 ThreadPoolExecutor 其他常⻅参数: 1. keepAliveTime :当线程池中的线程数量⼤于 corePoolSize 的时候,如果这时没有新的任务提交,核⼼线程外的线程不会⽴即销毁,⽽是会等待,直到等待的时间超过了 keepAliveTime 才会被回收销毁; 2. unit : keepAliveTime 参数的时间单位。 3. threadFactory :executor 创建新线程的时候会⽤到。 4. handler :饱和策略。关于饱和策略下⾯单独介绍⼀下。
ThreadPoolExecutor 饱和策略 ThreadPoolExecutor 饱和策略定义: 如果当前同时运⾏的线程数量达到最⼤线程数量并且队列也已经被放满了任 时, ThreadPoolTaskExecutor 定义⼀些策略: ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException 来拒绝新任务的处理。 ThreadPoolExecutor.CallerRunsPolicy :调⽤执⾏⾃⼰的线程运⾏任务。您不会任务请求。 但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应⽤程序可以承受此延迟并且你不能任务丢弃任何⼀个任务请求的话, 你可以选择这个策略。 ThreadPoolExecutor.DiscardPolicy : 不处理新任务,直接丢弃掉。 ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。 举个例⼦: Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造 函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的 时候默认使⽤的是 ThreadPoolExecutor.AbortPolicy 。在默认情况下, ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应⽤程序,建议使⽤ ThreadPoolExecutor.CallerRunsPolicy 。当最⼤池被填满时,此策略为我们提供可伸缩队列。
26.AQS 原理了解么?
AQS 核⼼思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占⽤,那么就需要⼀套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是⽤ CLH 队列锁实现的,即将暂时获取不到锁的线程加⼊到队列中。
CLH(Craig,Landin,and Hagersten)队列是⼀个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成⼀个CLH 锁队列的⼀个结点(Node)来实现锁的分配。
看个 AQS(AbstractQueuedSynchronizer)原理图:
AQS 使⽤⼀个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队⼯作。AQS 使⽤ CAS 对该同步状态进⾏原⼦操作实现对其值的修改。
状态信息通过 protected 类型的 getState,setState,compareAndSetState 进⾏操作
27.AQS 对资源的共享⽅式?
AQS 定义两种资源共享⽅式:
Exclusive(独占):只有⼀个线程能执⾏,如 ReentrantLock 。⼜可分为公平锁和⾮公平锁: 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 ⾮公平锁:当线程要获取锁时,⽆视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执⾏,如 CountDownLatch 、 Semaphore 、 CountDownLatch 、 CyclicBarrier 、 ReadWriteLock 我们都会在后⾯讲到。
ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某⼀资源进⾏读。 不同的⾃定义同步器争⽤共享资源的⽅式也不同。⾃定义同步器在实现时只需要实现共享资源 state 的获取与释放⽅式即可,⾄于具体线程等待队列的维护(如获取资源失败⼊队/唤醒出队 等),AQS 已经在顶层实现好了。
AQS 底层使⽤了模板⽅法模式
同步器的设计是基于模板⽅法模式的,如果需要⾃定义同步器⼀般的⽅式是这样(模板⽅法模式 很经典的⼀个应⽤):
1. 使⽤者继承 AbstractQueuedSynchronizer 并重写指定的⽅法。(这些重写⽅法很简单,⽆⾮是对于共享资源 state 的获取和释放)
2. 将 AQS 组合在⾃定义同步组件的实现中,并调⽤其模板⽅法,⽽这些模板⽅*调⽤使⽤者重写的⽅法。 这和我们以往通过实现接⼝的⽅式有很⼤区别,这是模板⽅法模式很经典的⼀个运⽤。
AQS 使⽤了模板⽅法模式,⾃定义同步器时需要重写下⾯⼏个 AQS 提供的模板⽅法
默认情况下,每个⽅法都抛出 UnsupportedOperationException 。 这些⽅法的实现必须是内部线程安全的,并且通常应该简短⽽不是阻塞。AQS 类中的其他⽅法都是 final ,所以⽆法被其他类使⽤,只有这⼏个⽅法可以被其他类使⽤。 以 ReentrantLock为例,state 初始化为0,表示未锁定状态。A 线程 lock()时,会调⽤ tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为⽌,其它线程才有机会获取该锁。当然,释放锁之前,A 线程 ⾃⼰是可以重复获取此锁的(state 会累加),这就是可重⼊的概念。但要注意,获取多少次就 要释放多么次,这样才能保证 state 是能回到零态的。 再以 CountDownLatch 以例,任务分为 N 个⼦线程去执⾏,state 也初始化为 N(注意 N 要与线 程个数⼀致)。这 N 个⼦线程是并⾏执⾏的,每个⼦线程执⾏完后 countDown() ⼀次,state 会 CAS(Compare and Swap)减 1。等到所有⼦线程都执⾏完后(即 state=0),会 unpark()主调⽤线 程,然后主调⽤线程就会从 await() 函数返回,继续后余动作。 ⼀般来说,⾃定义同步器要么是独占⽅法,要么是共享⽅式,他们也只需实现 tryAcquiretryRelease 、 tryAcquireShared-tryReleaseShared 中的⼀种即可。但AQS也⽀持⾃定义同步器同时 实现独占和共享两种⽅式,如 ReentrantReadWriteLock 。
28.⽤过 CountDownLatch 么?什么场景下⽤的?
CountDownLatch 的作⽤就是 允许 count 个线程阻塞在⼀个地⽅,直⾄所有线程的任务都执⾏完毕。之前在项⽬中,有⼀个使⽤多线程读取多个⽂件处理的场景,我⽤到了 CountDownLatch 。 具体场景是下⾯这样的:
我们要读取处理 6 个⽂件,这 6 个任务都是没有执⾏顺序依赖的任务,但是我们需要返回给⽤户的时候将这⼏个⽂件的处理的结果进⾏统计整理。
为此我们定义了⼀个线程池和 count 为 6 的 CountDownLatch 对象 。使⽤线程池处理读取任务, 每⼀个线程处理完之后就将 count-1,调⽤ CountDownLatch 对象的 await() ⽅法,直到所有⽂件读取完之后,才会接着执⾏后⾯的逻辑。
29.介绍下 Java 内存区域(运⾏时数据区)?
线程私有的: 程序计数器 虚拟机栈 本地⽅法栈
线程共享的: 堆 ⽅法区 直接内存 (⾮运⾏时数据区的⼀部分)
程序计数器:程序计数器是⼀块⼩的内存空间,可以看作是当前线程所执⾏的字节码的⾏号指示器。字节码解释器⼯作时通过改变这个计数器的值来选取下⼀条需要执⾏的字节码指令,分⽀、循环、跳 转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。另外,为了线程切换后能恢复到正确的执⾏位置,每条线程都需要有⼀个独⽴的程序计数器,各 线程之间计数器互不影响,独⽴存储,我们称这类内存区域为“线程私有”的内存。 从上⾯的介绍中我们知道程序计数器主要有两个作⽤:
1. 字节码解释器通过改变程序计数器来依次读取指令,从⽽实现代码的流程控制,如:顺序执 ⾏、选择、循环、异常处理。
2. 在多线程的情况下,程序计数器⽤于记录当前线程执⾏的位置,从⽽当线程被切换回来的时 候能够知道该线程上次运⾏到哪⼉了。
注意:程序计数器是唯⼀⼀个不会出现 OutOfMemoryError 的内存区域,它的⽣命周期随着线 程的创建⽽创建,随着线程的结束⽽死亡。
Java 虚拟机栈:与程序计数器⼀样,Java 虚拟机栈也是线程私有的,它的⽣命周期和线程相同,描述的是 Java ⽅法执⾏的内存模型,每次⽅法调⽤的数据都是通过栈传递的。
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。(实际上,Java 虚拟机栈是由⼀个个栈帧组成,⽽每个 栈帧中都拥有:局部变量表、操作数栈、动态链接、⽅法出⼝信息。)
局部变量表主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、 long、double)、对象引⽤(reference 类型,它不同于对象本身,可能是⼀个指向对象起始地址 的引⽤指针,也可能是指向⼀个代表对象的句柄或其他与此对象相关的位置)。
Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError 。 StackOverFlowError : 若 Java 虚拟机栈的内存⼤⼩不允许动态扩展,那么当线程请求栈 的深度超过当前 Java 虚拟机栈的最⼤深度的时候,就抛出 StackOverFlowError 错误。 OutOfMemoryError : 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也⽆法提供更多 内存的话。就会抛出 OutOfMemoryError 错误。
Java 虚拟机栈也是线程私有的,每个线程都有各⾃的 Java 虚拟机栈,⽽且随着线程的创建⽽创建,随着线程的死亡⽽死亡。
扩展:那么⽅法/函数如何调⽤?
Java 栈可⽤类⽐数据结构中栈,Java 栈中保存的主要内容是栈帧,每⼀次函数调⽤都会有⼀个 对应的栈帧被压⼊ Java 栈,每⼀个函数调⽤结束后,都会有⼀个栈帧被弹出。 Java ⽅法有两种返回⽅式:
1. return 语句。
2. 抛出异常。
不管哪种返回⽅式都会导致栈帧被弹出。
本地⽅法栈:和虚拟机栈所发挥的作⽤⾮常相似,区别是: 虚拟机栈为虚拟机执⾏ Java ⽅法 (也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合⼆为⼀。 本地⽅法被执⾏的时候,在本地⽅法栈也会创建⼀个栈帧,⽤于存放该本地⽅法的局部变量表、 操作数栈、动态链接、出⼝信息。 ⽅法执⾏完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种错误。
堆:Java 虚拟机所管理的内存中最⼤的⼀块,Java 堆是所有线程共享的⼀块内存区域,在虚拟机启动时创建。此内存区域的唯⼀⽬的就是存放对象实例,⼏乎所有的对象实例以及数组都在这⾥分配内存。
Java世界中“⼏乎”所有的对象都在堆中分配,但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致⼀些微妙的变化,所有的对象都分配到堆上也渐渐 变得不那么“绝对”了。从jdk 1.7开始已经默认开启逃逸分析,如果某些⽅法中的对象引⽤没有被返回或者未被外⾯使⽤(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的⻆度,由于现在收集器基本都采⽤分代垃圾收集算法,所以 Java 堆还可以细分为:新⽣代和⽼年代:再细致⼀点有:Eden 空间、From Survivor、To Survivor 空间等。进⼀步划分的⽬的是更好地回收内存,或者更快地分配内存。
在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下⾯三部分:
1. 新⽣代内存(Young Generation)
2. ⽼⽣代(Old Generation)
3. 永⽣代(Permanent Generation)
JDK 8 版本之后⽅法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取⽽代之是元空间,元空间使⽤的是直接内存。
上图所示的 Eden 区、两个 Survivor 区都属于新⽣代(为了区分,这两个 Survivor 区域按照顺序被命名为 from 和 to),中间⼀层属于⽼年代。
⼤部分情况,对象都会⾸先在 Eden 区域分配,在⼀次新⽣代垃圾回收后,如果对象还存活,则会进⼊ s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到⼀定程度(默认为 15 岁),就会被晋升到⽼年代中。对象晋升到⽼年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
堆这⾥最容易出现的就是 OutOfMemoryError 错误,并且出现这种错误之后的表现形式还会有⼏种,⽐如: 1. OutOfMemoryError: GC Overhead Limit Exceeded : 当JVM花太多时间执⾏垃圾回收并且只能回收很少的堆空间时,就会发⽣此错误。 2. java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不⾜以存放新创建的对象, 就会引发 java.lang.OutOfMemoryError: Java heap space 错误。(和本机物理内存⽆关,和你配置的内存⼤⼩有关!)
30.TCP 三次握⼿和四次挥⼿?
三次握手的本质是确认通信双方收发数据的能力。
首先,我让信使运输一份信件给对方,对方收到了,那么他就知道了我的发件能力和他的收件能力是可以的。
于是他给我回信,我若收到了,我便知我的发件能力和他的收件能力是可以的,并且他的发件能力和我的收件能力是可以。
然而此时他还不知道他的发件能力和我的收件能力到底可不可以,于是我最后回馈一次,他若收到了,他便清楚了他的发件能力和我的收件能力是可以的
- 第一次握手:客户端要向服务端发起连接请求,首先客户端随机生成一个起始序列号ISN(比如是100),那客户端向服务端发送的报文段包含SYN标志位(也就是SYN=1),序列号seq=100。
- 第二次握手:服务端收到客户端发过来的报文后,发现SYN=1,知道这是一个连接请求,于是将客户端的起始序列号100存起来,并且随机生成一个服务端的起始序列号(比如是300)。然后给客户端回复一段报文,回复报文包含SYN和ACK标志(也就是SYN=1,ACK=1)、序列号seq=300、确认号ack=101(客户端发过来的序列号+1)。
- 第三次握手:客户端收到服务端的回复后发现ACK=1并且ack=101,于是知道服务端已经收到了序列号为100的那段报文;同时发现SYN=1,知道了服务端同意了这次连接,于是就将服务端的序列号300给存下来。然后客户端再回复一段报文给服务端,报文包含ACK标志位(ACK=1)、ack=301(服务端序列号+1)、seq=101(第一次握手时发送报文是占据一个序列号的,所以这次seq就从101开始,需要注意的是不携带数据的ACK报文是不占据序列号的,所以后面第一次正式发送数据时seq还是101)。当服务端收到报文后发现ACK=1并且ack=301,就知道客户端收到序列号为300的报文了,就这样客户端和服务端通过TCP建立了连接。
四次挥手
四次挥手的目的是关闭一个连接
比如客户端初始化的序列号ISA=100,服务端初始化的序列号ISA=300。TCP连接成功后客户端总共发送了1000个字节的数据,服务端在客户端发FIN报文前总共回复了2000个字节的数据。
第一次挥手:当客户端的数据都传输完成后,客户端向服务端发出连接释放报文(当然数据没发完时也可以发送连接释放报文并停止发送数据),释放连接报文包含FIN标志位(FIN=1)、序列号seq=1101(100+1+1000,其中的1是建立连接时占的一个序列号)。需要注意的是客户端发出FIN报文段后只是不能发数据了,但是还可以正常收数据;另外FIN报文段即使不携带数据也要占据一个序列号。
第二次挥手:服务端收到客户端发的FIN报文后给客户端回复确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=1102(客户端FIN报文序列号1101+1)、序列号seq=2300(300+2000)。此时服务端处于关闭等待状态,而不是立马给客户端发FIN报文,这个状态还要持续一段时间,因为服务端可能还有数据没发完。
第三次挥手:服务端将最后数据(比如50个字节)发送完毕后就向客户端发出连接释放报文,报文包含FIN和ACK标志位(FIN=1,ACK=1)、确认号和第二次挥手一样ack=1102、序列号seq=2350(2300+50)。
第四次挥手:客户端收到服务端发的FIN报文后,向服务端发出确认报文,确认报文包含ACK标志位(ACK=1)、确认号ack=2351、序列号seq=1102。注意客户端发出确认报文后不是立马释放TCP连接,而是要经过2MSL(最长报文段寿命的2倍时长)后才释放TCP连接。而服务端一旦收到客户端发出的确认报文就会立马释放TCP连接,所以服务端结束TCP连接的时间要比客户端早一些。
31.为什么TCP连接的时候是3次?2次不可以吗?
因为需要考虑连接时丢包的问题,如果只握手2次,第二次握手时如果服务端发给客户端的确认报文段丢失,此时服务端已经准备好了收发数(可以理解服务端已经连接成功)据,而客户端一直没收到服务端的确认报文,所以客户端就不知道服务端是否已经准备好了(可以理解为客户端未连接成功),这种情况下客户端不会给服务端发数据,也会忽略服务端发过来的数据。
如果是三次握手,即便发生丢包也不会有问题,比如如果第三次握手客户端发的确认ack报文丢失,服务端在一段时间内没有收到确认ack报文的话就会重新进行第二次握手,也就是服务端会重发SYN报文段,客户端收到重发的报文段后会再次给服务端发送确认ack报文。
32.为什么TCP连接的时候是3次,关闭的时候却是4次?
因为只有在客户端和服务端都没有数据要发送的时候才能断开TCP。而客户端发出FIN报文时只能保证客户端没有数据发了,服务端还有没有数据发客户端是不知道的。而服务端收到客户端的FIN报文后只能先回复客户端一个确认报文来告诉客户端我服务端已经收到你的FIN报文了,但我服务端还有一些数据没发完,等这些数据发完了服务端才能给客户端发FIN报文(所以不能一次性将确认报文和FIN报文发给客户端,就是这里多出来了一次)。
33.为什么客户端发出第四次挥手的确认报文后要等2MSL的时间才能释放TCP连接?
这里同样是要考虑丢包的问题,如果第四次挥手的报文丢失,服务端没收到确认ack报文就会重发第三次挥手的报文,这样报文一去一回最长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了。
34.如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP设有一个保活计时器,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
35.TCP,UDP 协议的区别?
UDP 在传送数据之前不需要先建⽴连接,远地主机在收到 UDP 报⽂后,不需要给出任何确认。 虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是⼀种最有效的⼯作⽅式(⼀般⽤于即时通信),⽐如:语⾳、 视频 、直播等等 TCP 提供⾯向连接的服务。在传送数据之前必须先建⽴连接,数据传送结束后要释放连接。 TCP 不提供⼴播或多播服务。由于 TCP 要提供可靠的,⾯向连接的传输服务(TCP的可靠体现在TCP在传递数据之前,会有三次握⼿来建⽴连接,⽽且在数据传递时,有确认、窗⼝、重传、 拥塞控制机制,在数据传完后,还会断开连接⽤来节约系统资源),这⼀难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的⾸部增⼤很多,还要占⽤许多处理机资源。TCP ⼀般⽤于⽂件传输、发送和接收邮件、远程登录等场景。
36.TCP 协议如何保证可靠传输?
1. 应⽤数据被分割成 TCP 认为最适合发送的数据块。
2. TCP 给发送的每⼀个包进⾏编号,接收⽅对数据包进⾏排序,把有序数据传送给应⽤层。 3. 校验和:TCP 将保持它⾸部和数据的检验和。这是⼀个端到端的检验和,⽬的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报⽂段和不确认收到此报⽂段。
4. TCP 的接收端会丢弃重复的数据。
5. 流量控制:TCP 连接的每⼀⽅都有固定⼤⼩的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收⽅来不及处理发送⽅的数据,能提示发送⽅降低发送的速率,防⽌包丢失。TCP 使⽤的流量控制协议是可变⼤⼩的滑动窗⼝协议。(TCP利⽤滑 动窗⼝实现流量控制)
6. 拥塞控制:当⽹络拥塞时,减少数据的发送。
7. ARQ协议:也是为了实现可靠传输的,它的基本原理就是每发完⼀个分组就停⽌发送,等待对⽅确认。在收到确认后再发下⼀个分组。
8. 超时重传:当 TCP 发出⼀个段后,它启动⼀个定时器,等待⽬的端确认收到这个报⽂段。 如果不能及时收到⼀个确认,将重发这个报⽂段。
37.什么是操作系统?
1. 操作系统(Operating System,简称 OS)是管理计算机硬件与软件资源的程序,是计算机的基⽯。
2. 操作系统本质上是⼀个运⾏在计算机上的软件程序 ,⽤于管理计算机硬件和软件资源。 举 例:运⾏在你电脑上的所有应⽤程序都通过操作系统来调⽤系统内存以及磁盘等等硬件。 3. 操作系统存在屏蔽了硬件层的复杂性。 操作系统就像是硬件使⽤的负责⼈,统筹着各种相关事项。
4. 操作系统的内核(Kernel)是操作系统的核⼼部分,它负责系统的内存管理,硬件设备的管理,⽂件系统的管理以及应⽤程序的管理。 内核是连接应⽤程序和硬件的桥梁,决定着系统 的性能和稳定性。
38.进程间的通信常⻅的的有哪⼏种⽅式呢?
1. 管道/匿名管道(Pipes) :⽤于具有亲缘关系的⽗⼦进程间或者兄弟进程之间的通信。
2. 有名管道(Names Pipes) : 匿名管道由于没有名字,只能⽤于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘⽂件的⽅式存在,可以实现本机任意两个进程通信。
3. 信号(Signal) :信号是⼀种⽐较复杂的通信⽅式,⽤于通知接收进程某个事件已经发⽣; 4. 消息队列(Message Queuing) :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(⽆名管 道:只存在于内存中的⽂件;命名管道:存在于实际的磁盘介质或者⽂件系统)不同的是消 息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除⼀个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不⼀定要以先进先出的次序读取,也可以按消息的类型读取,⽐ FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载⽆格式字节流以及缓冲区⼤⼩受限等缺点。
5. 信号量(Semaphores) :信号量是⼀个计数器,⽤于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信⽅式主要⽤于解决与同步相关的问题并避免竞争条件。
6. 共享内存(Shared memory) :使得多个进程可以访问同⼀块内存空间,不同进程可以及时 看到对⽅进程中对共享内存中数据的更新。这种⽅式需要依靠某种同步操作,如互斥锁和信 号量等。可以说这是最有⽤的进程间通信⽅式。
7. 套接字(Sockets) : 此⽅法主要⽤于在客户端和服务器之间通过⽹络进⾏通信。套接字是⽀持 TCP/IP 的⽹络通信的基本操作单元,可以看做是不同主机之间的进程进⾏双向通信的端点,简单的说就是通信的两⽅的⼀种约定,⽤套接字中的相关函数来完成通信过程。
39.线程间的同步的⽅式?
线程同步是两个或多个共享关键资源的线程的并发执⾏。应该同步线程以避免关键的资源使⽤冲突。操作系统⼀般有下⾯三种线程同步的⽅式:
1. 互斥量(Mutex):采⽤互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。 因为互斥对象只有⼀个,所以可以保证公共资源不会被多个线程同时访问。⽐如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
2. 信号量(Semphares) :它允许同⼀时刻多个线程访问同⼀资源,但是需要控制同⼀时刻访 问此资源的最⼤线程数量
3. 事件(Event) :Wait/Notify:通过通知操作的⽅式来保持多线程同步,还可以⽅便的实现多线程优先级的⽐操
40.进程的调度算法?
先到先服务(FCFS)调度算法 : 从就绪队列中选择⼀个最先进⼊该队列的进程为之分配资源, 使它⽴即执⾏并⼀直执⾏到完成或发⽣某事件⽽被阻塞放弃占⽤ CPU 时再重新调度。
短作业优先(SJF)的调度算法 : 从就绪队列中选出⼀个估计运⾏时间最短的进程为之分配资 源,使它⽴即执⾏并⼀直执⾏到完成或发⽣某事件⽽被阻塞放弃占⽤ CPU 时再重新调度。 时间⽚轮转调度算法 : 时间⽚轮转调度是⼀种最古⽼,最简单,最公平且使⽤最⼴的算法, ⼜称 RR(Round robin)调度。每个进程被分配⼀个时间段,称作它的时间⽚,即该进程允许 运⾏的时间。
多级反馈队列调度算法 :前⾯介绍的⼏种进程调度的算法都有⼀定的局限性。如短进程优先的调度算法,仅照顾了短进程⽽忽略了⻓进程 。多级反馈队列调度算法既能使⾼优先级的作业得到响应⼜能使短作业(进程)迅速完成。,因⽽它是⽬前被公认的⼀种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。
优先级调度 :为每个流程分配优先级,⾸先执⾏具有最⾼优先级的进程,依此类推。具有 相同优先级的进程以 FCFS ⽅式执⾏。可以根据内存要求,时间要求或任何其他资源要求来 确定优先级。
41.操作系统的内存管理机制了解吗?内存管理有哪⼏种⽅式?
简单分为连续分配管理⽅式和⾮连续分配管理⽅式这两种。连续分配管理⽅式是指为⼀个⽤户程序分配⼀个连续的内存空间,常⻅的如块式管理 。同样地,⾮连续分配管理⽅式允许⼀个程序使⽤的内存分布在离散或者说不相邻的内存中,常⻅的如⻚式管理和段式管理。
1. 块式管理 : 远古时代的计算机操系统的内存管理⽅式。将内存分为⼏个固定⼤⼩的块,每 个块中只包含⼀个进程。如果程序运⾏需要内存的话,操作系统就分配给它⼀块,如果程序 运⾏只需要很⼩的空间的话,分配的这块内存很⼤⼀部分⼏乎被浪费了。这些在每个块中未 被利⽤的空间,我们称之为碎⽚。
2. ⻚式管理 :把主存分为⼤⼩相等且固定的⼀⻚⼀⻚的形式,⻚较⼩,相对相⽐于块式管理的划分⼒度更⼤,提⾼了内存利⽤率,减少了碎⽚。⻚式管理通过⻚表对应逻辑地址和物理地址。
3. 段式管理 : ⻚式管理虽然提⾼了内存利⽤率,但是⻚式管理其中的⻚实际并⽆任何实际意义。 段式管理把主存分为⼀段段的,每⼀段的空间⼜要⽐⼀⻚的空间⼩很多 。但是,最重要的是段是有实际意义的,每个段定义了⼀组逻辑信息,例如,有主程序段MAIN、⼦程序段 X、数据段
D及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。
4. 段⻚式管理机制:段⻚式管理机制结合 了段式管理和⻚式管理的优点。简单来说段⻚式管理机制就是把主存先分成若⼲段,每个段⼜成若⼲⻚,也就是说段⻚式管理机制 中段与段之间以及段的内部的都是离散的。
42.逻辑地址与物理地址?
逻辑地址(Logical Address) 是指由程序产生的与段相关的偏移地址部分。例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址,不和绝对物理地址相干。
物理地址(Physical Address) 是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。
43.虚拟内存?
通过虚拟内存可以让程序可以拥有超过系统物理内存⼤⼩的可⽤内存空间。另外,虚拟内存为每个进程提供了⼀个⼀致的、私有的地址空间,它让每个进程产⽣了⼀种⾃⼰在独享主存的错觉(每个进程拥有⼀⽚连续完整的内存空间)。这样会更加有效地管理内存并减少出错。
虚拟内存是计算机系统内存管理的⼀种技术,我们可以⼿动设置⾃⼰电脑的虚拟内存。不要单纯认为虚拟内存只是“使⽤硬盘空间来扩展内存“的技术。虚拟内存的重要意义是它定义了⼀个连续的虚拟地址空间,并且把内存扩展到硬盘空间。
44.MyISAM和InnoDB区别?
常用的存储引擎有以下:
Innodb引擎:Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。
MyIASM引擎(原本Mysql的默认引擎):不提供事务的支持,也不支持行级锁和外键。
MEMORY引擎:所有的数据都在内存中,数据的处理速度快,但是安全性不高。
MyISAM是MySQL的默认数据库引擎(5.5版之前)。虽然性能极佳,⽽且提供了⼤量的特性,包括全⽂索引、压缩、空间函数等,但MyISAM不⽀持事务和⾏级锁,⽽且最⼤的缺陷就是崩溃后⽆法安全恢复。不过,5.5版本之后,MySQL引⼊了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。 ⼤多数时候我们使⽤的都是 InnoDB 存储引擎,但是在某些情况下使⽤ MyISAM 也是合适的⽐如读密集的情况下。(如果你不介意 MyISAM 崩溃恢复问题的话)。
1. 是否⽀持⾏级锁 : MyISAM 只有表级锁(table-level locking),⽽InnoDB ⽀持⾏级锁(rowlevel locking)和表级锁,默认为⾏级锁。
2. 是否⽀持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原⼦性,其执⾏ 速度⽐InnoDB类型更快,但是不提供事务⽀持。但是InnoDB 提供事务⽀持事务,外部键等 ⾼级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能⼒(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
3. 是否⽀持外键: MyISAM不⽀持,⽽InnoDB⽀持。
4. 是否⽀持MVCC :仅 InnoDB ⽀持。应对⾼并发事务, MVCC⽐单纯的加锁更⾼效;MVCC只 在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下⼯作;MVCC可以使⽤ 乐 观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统⼀。
45.MyISAM索引与InnoDB索引的区别?
InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。
46.索引
MySQL索引使⽤的数据结构主要有BTree索引 和 哈希索引 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝⼤多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余⼤部分场景,建议选择BTree索引。
MySQL的BTree索引使⽤的是B树中的B+Tree,但对于主要的两种存储引擎的实现⽅式是不同的。
MyISAM: B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,⾸先按照 B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“⾮聚簇索引”。
InnoDB: 其数据⽂件本身就是索引⽂件。相⽐MyISAM,索引⽂件和数据⽂件是分离的,其 表数据⽂件本身就是按B+Tree组织的⼀个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据⽂件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。⽽其余的索引都作为ᬀ助索引,ᬀ助索引的data域存储相应记录主键的值⽽不是地址,这也是和MyISAM不同的地⽅。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再⾛⼀遍主索引。 因此,在设计表的时候,不建议使⽤过⻓的字段作为主键,也不建议使⽤⾮单调的字段作为主键,这样会造成主索引频繁分裂。
47.事务隔离级别有哪些?MySQL的默认隔离级别是?
SQL 标准定义了四个隔离级别:
READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更, 可能会导致脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻⽌脏读,但 是幻读或不可重复读仍有可能发⽣。
REPEATABLE-READ(可重复读): 对同⼀字段的多次读取结果都是⼀致的,除⾮数据是被 本身事务⾃⼰所修改,可以阻⽌脏读和不可重复读,但幻读仍有可能发⽣。
SERIALIZABLE(可串⾏化): 最⾼的隔离级别,完全服从ACID的隔离级别。所有的事务依 次逐个执⾏,这样事务之间就完全不可能产⽣⼲扰,也就是说,该级别可以防⽌脏读、不可 重复读以及幻读。
MySQL InnoDB 存储引擎的默认⽀持的隔离级别是 REPEATABLE-READ(可重复读)。
48.说⼀下 Redis 和 Memcached 的区别和共同点
共同点 :
1. 都是基于内存的数据库,⼀般都⽤来当做缓存使⽤。
2. 都有过期策略。
3. 两者的性能都⾮常⾼。
区别 :
1. Redis ⽀持更丰富的数据类型(⽀持更复杂的应⽤场景)。Redis 不仅仅⽀持简单的 k/v 类 型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。Memcached 只⽀持最简 单的 k/v 数据类型。
2. Redis ⽀持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进⾏使⽤,⽽ Memecache 把数据全部存在内存之中。
3. Redis 有灾难恢复机制。 因为可以把缓存中的数据持久化到磁盘上。
4. Redis 在服务器内存使⽤完之后,可以将不⽤的数据放到磁盘上。但是,Memcached 在服务器内存使⽤完之后,就会直接报异常。
5. Memcached 没有原⽣的集群模式,需要依靠客户端来实现往集群中分⽚写⼊数据;但是 Redis ⽬前是原⽣⽀持 cluster 模式的.
6. Memcached 是多线程,⾮阻塞 IO 复⽤的⽹络模型;Redis 使⽤单线程的多路 IO 复⽤模型。(Redis 6.0 引⼊了多线程 IO )
7. Redis ⽀持发布订阅模型、Lua 脚本、事务等功能,⽽ Memcached 不⽀持。并且,Redis ⽀持更多的编程语⾔。
8. Memcached过期数据的删除策略只⽤了惰性删除,⽽ Redis 同时使⽤了惰性删除与定期删除。
49. Redis 没有使⽤多线程?为什么不使⽤多线程?
虽然说 Redis 是单线程模型,但是, 实际上,Redis 在 4.0 之后的版本中就已经加⼊了对多线程的⽀持。
不过,Redis 4.0 增加的多线程主要是针对⼀些⼤键值对的删除操作的命令,使⽤这些命令就会使⽤主处理之外的其他线程来“异步处理”。
⼤体上来说,Redis 6.0 之前主要还是单线程处理。
那,Redis6.0 之前 为什么不使⽤多线程? 我觉得主要原因有下⾯ 3 个:
1. 单线程编程容易并且更容易维护;
2. Redis 的性能瓶颈不再 CPU ,主要在内存和⽹络;
3. 多线程就会存在死锁、线程上下⽂切换等问题,甚⾄会影响性能。
50.Redis6.0 之后为何引⼊了多线程?
Redis6.0 引⼊多线程主要是为了提⾼⽹络 IO 读写性能,因为这个算是 Redis 中的⼀个性能瓶颈 (Redis 的瓶颈主要受限于内存和⽹络)。
虽然,Redis6.0 引⼊了多线程,但是 Redis 的多线程只是在⽹络数据的读写这类耗时操作上使⽤了, 执⾏命令仍然是单线程顺序执⾏。因此,你也不需要担⼼线程安全问题。 Redis6.0 的多线程默认是禁⽤的,只使⽤主线程。如需开启需要修改 redis 配置⽂件 redis.conf:io-threads-do-reads yes。开启多线程后,还需要设置线程数,否则是不⽣效的。同样需要修改 redis 配置⽂件 redis.conf:io-threads 4 #官⽹建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程
51.Redis是如何判断数据是否过期的呢?
Redis 通过⼀个叫做过期字典(可以看作是hash表)来保存数据过期的时间。过期字典的键指向Redis数据库中的某个key(键),过期字典的值是⼀个long long类型的整数,这个整数保存了key所 向的数据库键的过期时间(毫秒精度的UNIX时间戳)。
52.过期的数据的删除策略了解么?
常⽤的过期数据的删除策略就两个(重要!⾃⼰造缓存轮⼦的时候需要格外考虑的东⻄): 1. 惰性删除 :只会在取出key的时候才对数据进⾏过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。
2. 定期删除 :每隔⼀段时间抽取⼀批 key 执⾏删除过期key操作。并且,Redis 底层会通过限制删除操作执⾏的时⻓和频率来减少删除操作对CPU时间的影响。
定期删除对内存更加友好,惰性删除对CPU更加友好。两者各有千秋,所以Redis 采⽤的是定期删除+惰性/懒汉式删除 。 但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致⼤量过期 key 堆积在内存⾥,然后就Out of memory了
53.Redis 内存淘汰机制了解么?
1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires) 中挑选最近最少使⽤的数据淘汰
2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰 4. allkeys-lru(least recently used):当内存不⾜以容纳新写⼊数据时,在键空间中,移除最近最少使⽤的 key(这个是最常⽤的)
5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
6. no-eviction:禁⽌驱逐数据,也就是说当内存不⾜以容纳新写⼊数据时,新写⼊操作会报 错。这个应该没⼈使⽤吧!
4.0 版本后增加以下两种:
7. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使⽤的数据淘汰
8. allkeys-lfu(least frequently used):当内存不⾜以容纳新写⼊数据时,在键空间中,移除最不经常使⽤的 key。
54.Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进⾏恢复)
快照(snapshotting)持久化(RDB)
Redis可以通过创建快照来获得存储在内存⾥⾯的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进⾏备份,可以将快照复制到其他服务器从⽽创建具有相同数据的服务器副本(Redis 主从结构,主要⽤来提⾼ Redis 性能),还可以将快照留在原地以便重启服务器的时候使⽤。
快照持久化是 Redis 默认采⽤的持久化⽅式,在 Redis.conf 配置⽂件中默认有此下配置:
AOF(append-only file)持久化
与快照持久化相⽐,AOF 持久化 的实时性更好,因此已成为主流的持久化⽅案。默认情况下 Redis 没有开启 AOF(append only file)⽅式的持久化,可以通过 appendonly 参数开启:
开启 AOF 持久化后每执⾏⼀条会更改 Redis 中的数据的命令,Redis 就会将该命令写⼊硬盘中的 AOF ⽂件。AOF ⽂件的保存位置和 RDB ⽂件的位置相同,都是通过 dir 参数设置的,默认的⽂件名是 appendonly.aof。
在 Redis 的配置⽂件中存在三种不同的 AOF 持久化⽅式,它们分别是:
为了兼顾数据和写⼊性能,⽤户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步⼀次 AOF ⽂件,Redis 性能⼏乎没受到任何影响。⽽且这样即使出现系统崩溃,⽤户最多只会丢失⼀ 秒之内产⽣的数据。当硬盘忙于执⾏写⼊操作的时候,Redis 还会优雅的放慢⾃⼰的速度以便适应硬盘的最⼤写⼊速度。
55.Redis持久化数据和缓存怎么做扩容?
如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。
如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。
56.Redis线程模型
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
57.哨兵模式
sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:
集群监控:负责监控 redis master 和 slave 进程是否正常工作。
消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。
哨兵的核心知识
哨兵至少需要 3 个实例,来保证自己的健壮性。
哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。
对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
58.redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?
1. 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384 个槽位
2. 每份数据分片会存储在多个互为主从的多节点上
3. 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
4. 同一分片多个节点间的数据不保持一致性
5. 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
6. 扩容时时需要需要把旧节点的数据迁移一部分到新节点
59.主从复制的数据同步的过程?
1. 当从库和主库建立MS关系后,会向主数据库发送SYNC命令
2. 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来
3. 当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis
4. 从Redis接收到后,会载入快照文件并且执行收到的缓存的命令
5. 之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致
60.缓存雪崩了解吗?
缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
61.缓存穿透了解吗?
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:
接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力
62.缓存击穿了解吗?
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
设置热点数据永远不过期。
加互斥锁,互斥锁
63.缓存预热了解吗?
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决方案:
直接写个缓存刷新页面,上线时手工操作一下;
数据量不大,可以在项目启动的时候自动进行加载;
定时刷新缓存;
64.如何保证缓存与数据库双写时的数据一致性?
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
65.使用Redis做过异步队列吗,是如何实现的?
使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。
66.什么是 Linux 内核?
Linux 系统的核心是内核。内核控制着计算机系统上的所有硬件和软件,在必要时分配硬件,并根据需要执行软件。
1. 系统内存管理
2. 应用程序管理
3. 硬件设备管理
4. 文件系统管理
67.Linux系统缺省的运行级别?
· 关机。
· 单机用户模式。
· 字符界面的多用户模式(不支持网络)。
· 字符界面的多用户模式。
· 未分配使用。
· 图形界面的多用户模式。
· 重启。
68.什么是硬链接和软链接?
1)硬链接
由于 Linux 下的文件是通过索引节点(inode)来识别文件,硬链接可以认为是一个指针,指向文件索引节点的指针,系统并不为它重新分配 inode 。每添加一个一个硬链接,文件的链接数就加 1 。
不足:1)不可以在不同文件系统的文件间建立链接;2)只有超级用户才可以为目录创建硬链接。
2)软链接
软链接克服了硬链接的不足,没有任何文件系统的限制,任何用户可以创建指向目录的符号链接。因而现在更为广泛使用,它具有更大的灵活性,甚至可以跨越不同机器、不同网络对文件进行链接。
不足:因为链接文件包含有原文件的路径信息,所以当原文件从一个目录下移到其他目录中,再访问链接文件,系统就找不到了,而硬链接就没有这个缺陷,你想怎么移就怎么移;还有它要系统分配额外的空间用于建立新的索引节点和保存原文件的路径。
实际场景下,基本是使用软链接。总结区别如下:
硬链接不可以跨分区,软件链可以跨分区。
硬链接指向一个 inode 节点,而软链接则是创建一个新的 inode 节点。
删除硬链接文件,不会删除原文件,删除软链接文件,会把原文件删除。
69.什么是网站数据库注入?
由于程序员的水平及经验参差不齐,大部分程序员在编写代码的时候,没有对用户输入数据的合法性进行判断。
应用程序存在安全隐患。用户可以提交一段数据库查询代码,根据程序返回的结果,获得某些他想得知的数据,这就是所谓的 SQL 注入。
SQL注入,是从正常的 WWW 端口访问,而且表面看起来跟一般的 Web 页面访问没什么区别,如果管理员没查看日志的习惯,可能被入侵很长时间都不会发觉。
如何过滤与预防?
数据库网页端注入这种,可以考虑使用 nginx_waf 做过滤与预防。
70.请问当用户反馈网站访问慢,你会如何处理?
有哪些方面的因素会导致网站网站访问慢?
1、 服务器出口带宽不够用
2、 服务器负载过大,导致响应不过来
3、 数据库瓶颈
4、 网站开发代码没有优化好
针对网站访问慢,怎么去排查?
1、 首先要确定是用户端还是服务端的问题。当接到用户反馈访问慢,那边自己立即访问网站看看,如果自己这边访问快,基本断定是用户端问题,就需要耐心跟客户解释,协助客户解决问题。
2、 如果访问也慢,那么可以利用浏览器的调试功能,看看加载那一项数据消耗时间过多,是图片加载慢,还是某些数据加载慢。
3、 针对服务器负载情况。查看服务器硬件(网络、CPU、内存)的消耗情况。如果是购买的云主机,比如阿里云,可以登录阿里云平台提供各方面的监控,比如 CPU、内存、带宽的使用情况。
4、 如果发现硬件资源消耗都不高,那么就需要通过查日志,比如看看 MySQL慢查询的日志,看看是不是某条 SQL 语句查询慢,导致网站访问慢。
怎么去解决?
1、如果是出口带宽问题,那么久申请加大出口带宽。
2、如果慢查询比较多,那么就要开发人员或 DBA 协助进行 SQL 语句的优化。
3、如果数据库响应慢,考虑可以加一个数据库缓存,如 Redis 等等。然后也可以搭建MySQL 主从,一台 MySQL 服务器负责写,其他几台从数据库负责读。
4、申请购买 CDN 服务,加载用户的访问。
5、如果访问还比较慢,那就需要从整体架构上进行优化咯。做到专角色专用,多台服务器提供同一个服务。
71.列举⼀些重要的Spring模块?
Spring Core: 基础,可以说 Spring 其他所有的功能都需要依赖于该类库。主要提供 IoC 依 赖注⼊功能。
Spring Aspects : 该模块为与AspectJ的集成提供⽀持。
Spring AOP :提供了⾯向切⾯的编程实现。
Spring JDBC : Java数据库连接。
Spring JMS :Java消息服务。
Spring ORM : ⽤于⽀持Hibernate等ORM⼯具。
Spring Web : 为创建Web应⽤程序提供⽀持。
Spring Test : 提供了对 JUnit 和 TestNG 测试的⽀持。
72.谈谈⾃⼰对于 Spring IoC 和 AOP 的理解
IoC
IoC(Inverse of Control:控制反转)是⼀种设计思想,就是将原本在程序中⼿动创建对象的控制权,交由Spring框架来管理。 IoC 在其他语⾔中也有应⽤,并⾮ Spring 特有。 IoC 容器是Spring ⽤来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。
将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注⼊。这样可以很⼤程度上简化应⽤的开发,把应⽤从复杂的依赖关系中解放出来。 IoC 容器就像是⼀个⼯⼚⼀样,当我们需要创建⼀个对象的时候,只需要配置好配置⽂件/注解即可,完全不⽤考虑对象是如何被创建出来的。 在实际项⽬中⼀个 Service 类可能有⼏百甚⾄上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把⼈逼疯。如果利⽤ IoC 的话,你只需要配置好,然后在需要的地⽅引⽤就⾏了,这⼤⼤增加了项⽬的可维护性且降低了开发难度。
Spring 时代我们⼀般通过 XML ⽂件来配置 Bean,后来开发⼈员觉得 XML ⽂件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流⾏起来。
Spring IoC的初始化过程:
AOP
AOP(Aspect-Oriented Programming:⾯向切⾯编程)能够将那些与业务⽆关,却为业务模块所共同调⽤的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接⼝,那么Spring AOP会使⽤JDK Proxy,去创建代理对象,⽽对于没有实现接⼝的对象,就⽆法使⽤ JDK Proxy 去进⾏代理了,这时候Spring AOP会使⽤Cglib ,这时候Spring AOP会使⽤ Cglib ⽣成⼀个被代理对象的⼦类来作为代理,如下图所示:
当然你也可以使⽤ AspectJ ,Spring AOP 已经集成了AspectJ ,AspectJ 应该算的上是 Java ⽣态系统中最完整的 AOP 框架了。使⽤ AOP 之后我们可以把⼀些通⽤功能抽象出来,在需要⽤到的地⽅直接使⽤即可,这样⼤⼤简化了代码量。我们需要增加新功能时也⽅便,这样也提⾼了系统扩展性。⽇志功能、事务管理等等场景都⽤到了 AOP 。
73.Spring 中的 bean 的作⽤域有哪些?
singleton : 唯⼀ bean 实例,Spring 中的 bean 默认都是单例的。
prototype : 每次请求都会创建⼀个新的 bean 实例。
request : 每⼀次HTTP请求都会产⽣⼀个新的bean,该bean仅在当前HTTP request内有 效。
session : 每⼀次HTTP请求都会产⽣⼀个新的 bean,该bean仅在当前 HTTP session 内有 效。
global-session: 全局session作⽤域,仅仅在基于portlet的web应⽤中才有意义,Spring5已经没有了。Portlet是能够⽣成语义代码(例如:HTML)⽚段的⼩型Java Web插件。它们基于portlet容器,可以像servlet⼀样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话
74.Spring 中的单例 bean 的线程安全问题了解吗?
不是,Spring框架中的单例bean不是线程安全的。
spring 中的 bean 默认是单例模式,spring 框架并没有对单例 bean 进行多线程的封装处理。
⼤部分时候我们并没有在系统中使⽤多线程,所以很少有⼈会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同⼀个对象的时候,对这个对象的⾮静态成员变量的写操作 会存在线程安全问题。
常⻅的有两种解决办法:
1. 在Bean对象中尽量避免定义可变的成员变量(不太现实)。
2. 在类中定义⼀个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的⼀种⽅式)。
75.@Component 和 @Bean 的区别是什么?
1. 作⽤对象不同: @Component 注解作⽤于类,⽽ @Bean 注解作⽤于⽅法。
2. @Component 通常是通过类路径扫描来⾃动侦测以及⾃动装配到Spring容器中(我们可以使 ⽤ @ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类⾃动装配到 Spring 的 bean 容器中)。 @Bean 注解通常是我们在标有该注解的⽅法中定义产⽣这个 bean, @Bean 告诉了Spring这是某个类的示例,当我需要⽤它的时候还给我。
3. @Bean 注解⽐ Component 注解的⾃定义性更强,⽽且很多地⽅我们只能通过 @Bean 注 解来注册bean。⽐如当我们引⽤第三⽅库中的类需要装配到 Spring 容器时,则只能通过 @Bean 来实现。
76.Spring 中的 bean ⽣命周期?
l Bean 容器找到配置⽂件中 Spring Bean 的定义。
l Bean 容器利⽤ Java Reflection API 创建⼀个Bean的实例。
l 如果涉及到⼀些属性值 利⽤ set() ⽅法设置⼀些属性值。
l 如果 Bean 实现了 BeanNameAware 接⼝,调⽤ setBeanName() ⽅法,传⼊Bean的名字。
l 如果 Bean 实现了 BeanClassLoaderAware 接⼝,调⽤ setBeanClassLoader() ⽅法,传⼊ ClassLoader 对象的实例。
l 与上⾯的类似,如果实现了其他 *.Aware 接⼝,就调⽤相应的⽅法。
l 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执 ⾏ postProcessBeforeInitialization() ⽅法
l 如果Bean实现了 InitializingBean 接⼝,执⾏ afterPropertiesSet() ⽅法。
l 如果 Bean 在配置⽂件中的定义包含 init-method 属性,执⾏指定的⽅法。
l 如果有和加载这个 Bean的 Spring 容器相关的 BeanPostProcessor 对象,执 ⾏ postProcessAfterInitialization() ⽅法
l 当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接⼝,执⾏ destroy() ⽅法。


77.SpringMVC ⼯作原理了解吗?
流程说明(重要):
1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet 。
2. DispatcherServlet 根据请求信息调⽤ HandlerMapping,解析请求对应的Handler 。
3. 解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter适配器处理。
4. HandlerAdapter 会根据 Handler 来调⽤真正的处理器开处理请求,并处理相应的业务逻 辑。
5. 处理器处理完业务后,会返回⼀个 ModelAndView 对象, Model 是返回的数据对象, View 是个逻辑上的View 。
6. View Resolver 会根据逻辑 View 查找实际的 View 。
7. DispaterServlet 把返回的 Model 传给 View (视图渲染)。
8. 把 View 返回给请求者(浏览器)
78.Spring 框架中⽤到了哪些设计模式?
l ⼯⼚设计模式 : Spring使⽤⼯⼚模式通过 BeanFactory 、 ApplicationContext 创建 bean 对象。
l 代理设计模式 : Spring AOP 功能的实现。
l 单例设计模式 : Spring 中的 Bean 默认都是单例的。
l 包装器设计模式 : 我们的项⽬需要连接多个数据库,⽽且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
l 观察者模式: Spring 事件驱动模型就是观察者模式很经典的⼀个应⽤。
l 适配器模式 :Spring AOP 的增强或通知(Advice)使⽤到了适配器模式、spring MVC 中也是⽤到了适配器模式适配 Controller 。
79.@Transactional(rollbackFor = Exception.class)注解了解吗?
我们知道:Exception分为运⾏时异常RuntimeException和⾮运⾏时异常。事务管理对于企业应 ⽤来说是⾄关重要的,即使出现异常情况,它也可以保证数据的⼀致性。
当 @Transactional 注解作⽤于类上时,该类的所有 public ⽅法将都具有该类型的事务属性,同 时,我们也可以在⽅法级别使⽤该标注来覆盖类级别的定义。如果类或者⽅法加了这个注解,那 么这个类⾥⾯的⽅法抛出异常,就会回滚,数据库⾥⾯的数据也会回滚。
在 @Transactional 注解中如果不配置 rollbackFor 属性,那么事物只会在遇到 RuntimeException 的 时候才会回滚,加上 rollbackFor=Exception.class ,可以让事物在遇到⾮运⾏时异常时也回滚。
80.Spring 事务中哪⼏种事务传播⾏为?
⽀持当前事务的情况:
TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加⼊该事务;如果当前没有事务,则创建⼀个新的事务。
TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加⼊该事务;如果当前没有事务,则以⾮事务的⽅式继续运⾏。 TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加⼊该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
不⽀持当前事务的情况:
TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建⼀个新的事务,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以⾮事务⽅式运⾏,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER: 以⾮事务⽅式运⾏,如果当前存在事务,则抛出异常。
其他情况:
TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建⼀个事务 作为当前事务的嵌套事务来运⾏;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED。
81.Spring 应用程序有哪些不同组件?
Spring 应用一般有以下组件:
· 接口 - 定义功能。
· Bean 类 - 它包含属性,setter 和 getter 方法,函数等。
· Bean 配置文件 - 包含类的信息以及如何配置它们。
· Spring 面向切面编程(AOP) - 提供面向切面编程的功能。
· 用户程序 - 它使用接口。
82.Spring 的 IoC支持哪些功能
Spring 的 IoC 设计支持以下功能:
l 依赖注入
l 依赖检查
l 自动装配
l 支持集合
l 指定初始化方法和销毁方法
l 支持回调某些方法(但是需要实现 Spring 接口,略有侵入)
其中,最重要的就是依赖注入,从 XML 的配置上说,即 ref 标签。对应 Spring RuntimeBeanReference 对象。
对于 IoC 来说,最重要的就是容器。容器管理着 Bean 的生命周期,控制着 Bean 的依赖注入。
83.BeanFactory 和 ApplicationContext有什么区别?
BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口。
依赖关系
BeanFactory:是Spring里面最底层的接口,包含了各种Bean的定义,读取bean配置文档,管理bean的加载、实例化,控制bean的生命周期,维护bean之间的依赖关系。
ApplicationContext接口作为BeanFactory的派生,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:
l 继承MessageSource,因此支持国际化。
l 统一的资源文件访问方式。
l 提供在监听器中注册bean的事件。
l 同时加载多个配置文件。
l 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
加载方式
BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。
ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了。
相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。
创建方式
BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。
注册方式
BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。
84.什么是Spring的依赖注入?
控制反转IoC是一个很大的概念,可以用不同的方式来实现。其主要实现方式有两种:依赖注入和依赖查找
依赖注入:相对于IoC而言,依赖注入(DI)更加准确地描述了IoC的设计理念。所谓依赖注入(Dependency Injection),即组件之间的依赖关系由容器在应用系统运行期来决定,也就是由容器动态地将某种依赖关系的目标对象实例注入到应用系统中的各个关联的组件之中。组件不做定位查询,只提供普通的Java方法让容器去决定依赖关系。
85.构造器依赖注入和 Setter方法注入的区别
两种依赖方式都可以使用,构造器注入和Setter方法注入。最好的解决方案是用构造器参数实现强制依赖,setter方法实现可选依赖。
86.Spring基于xml注入bean的几种方式
1. Set方法注入;
2. 构造器注入:①通过index设置参数的位置;②通过type设置参数类型;
3. 静态工厂注入;
4. 实例工厂;
87.使用@Autowired注解自动装配的过程是怎样的?
使用@Autowired注解来自动装配指定的bean。在使用@Autowired注解之前需要在Spring配置文件进行配置,<context:annotation-config />。
在启动spring IoC时,容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器,当容器扫描到@Autowied、@Resource或@Inject时,就会在IoC容器自动查找需要的bean,并装配给该对象的属性。在使用@Autowired时,首先在容器中查询对应类型的bean:
如果查询结果刚好为一个,就将该bean装配给@Autowired指定的数据;
如果查询的结果不止一个,那么@Autowired会根据名称来查找;
如果上述查找的结果为空,那么会抛出异常。解决方法时,使用required=false。
88.@Autowired和@Resource之间的区别
@Autowired可用于:构造函数、成员变量、Setter方法
@Autowired和@Resource之间的区别
@Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。
@Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。
89.@RequestMapping 注解有什么用?
@RequestMapping 注解用于将特定 HTTP 请求方法映射到将处理相应请求的控制器中的特定类/方法。此注释可应用于两个级别:
· 类级别:映射请求的 URL
· 方法级别:映射 URL 以及 HTTP 请求方法
90.Spring通知有哪些类型?
在AOP术语中,切面的工作被称为通知,实际上是程序执行时要通过SpringAOP框架触发的代码段。
Spring切面可以应用5种类型的通知:
前置通知(Before):在目标方法被调用之前调用通知功能;
后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
返回通知(After-returning ):在目标方法成功执行之后调用通知;
异常通知(After-throwing):在目标方法抛出异常后调用通知;
环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
91.Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:Spring组件扫描。
92.Spring Boot 自动配置原理是什么?
注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,
@EnableAutoConfiguration 给容器导入META-INF/spring.factories 里定义的自动配置类。
筛选有效的自动配置类。
每一个自动配置类结合对应的 xxxProperties.java 读取配置文件进行自动配置功能
93.你如何理解 Spring Boot 配置加载顺序?
在 Spring Boot 里面,可以使用以下几种方式来加载配置。
1)properties文件;
2)YAML文件;
3)系统环境变量;
4)命令行参数;
等等……
94.spring boot 核心配置文件是什么?bootstrap.properties 和 application.properties 有何区别 ?
单纯做 Spring Boot 开发,可能不太容易遇到 bootstrap.properties 配置文件,但是在结合 Spring Cloud 时,这个配置就会经常遇到了,特别是在需要加载一些远程配置文件的时侯。
spring boot 核心的两个配置文件:
bootstrap (. yml 或者 . properties):boostrap 由父 ApplicationContext 加载的,比 applicaton 优先加载,配置在应用程序上下文的引导阶段生效。一般来说我们在 Spring Cloud Config 或者 Nacos 中会用到它。且 boostrap 里面的属性不能被覆盖;
application (. yml 或者 . properties): 由ApplicatonContext 加载,用于 spring boot 项目的自动化配置。
95.Spring Boot 中如何解决跨域问题 ?
全局配置类实现
CorsConfig类
我们使用cookie存放用户登录的信息,在spring拦截器进行权限控制,当权限不符合时,直接返回给用户固定的json结果。
当用户登录以后,正常使用;当用户退出登录状态时或者token过期时,由于拦截器和跨域的顺序有问题,出现了跨域的现象。
我们知道一个http请求,先走filter,到达servlet后才进行拦截器的处理,如果我们把cors放在filter里,就可以优先于权限拦截器执行。
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration("/", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}
96.什么是 WebSockets?
WebSocket 是一种计算机通信协议,通过单个 TCP 连接提供全双工通信信道。
1、WebSocket 是双向的 -使用 WebSocket 客户端或服务器可以发起消息发送。
2、WebSocket 是全双工的 -客户端和服务器通信是相互独立的。
3、单个 TCP 连接 -初始连接使用 HTTP,然后将此连接升级到基于套接字的连接。然后这个单一连接用于所有未来的通信
4、Light -与 http 相比,WebSocket 消息数据交换要轻得多。
97.什么是 Swagger?你用 Spring Boot 实现了它吗?
Swagger 广泛用于可视化 API,使用 Swagger UI 为前端开发人员提供在线沙箱。Swagger 是用于生成 RESTful Web 服务的可视化表示的工具,规范和完整框架实现。它使文档能够以与服务器相同的速度更新。当通过 Swagger 正确定义时,消费者可以使用最少量的实现逻辑来理解远程服务并与其进行交互。因此,Swagger消除了调用服务时的猜测。
98.简述java类加载机制?
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。
99.描述一下JVM加载Class文件的原理机制
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
类装载方式,有两种 :
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
2.显式装载, 通过class.forname()等方法,显式加载需要的类
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候
才加载。这当然就是为了节省内存开销。
100.说一下类装载的执行过程?
类装载分为以下 5 个步骤:
加载:根据查找路径找到相应的 class 文件然后导入;
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作。
101什么是Cookie ? Cookie的作⽤是什么?
Cookie 和 Session都是⽤来跟踪浏览器⽤户身份的会话⽅式,但是两者的应⽤场景不太⼀样。
维基百科是这样定义 Cookie 的:Cookies是某些⽹站为了辨别⽤户身份⽽储存在⽤户本地终端上的数据(通常经过加密)。简单来说: Cookie 存放在客户端,⼀般⽤来保存⽤户信息。
下⾯是 Cookie 的⼀些应⽤案例:
1. 我们在 Cookie 中保存已经登录过的⽤户信息,下次访问⽹站的时候⻚⾯可以⾃动帮你登录的⼀些基本信息给填了。除此之外,Cookie 还能保存⽤户⾸选项,主题和其他设置信息。
2. 使⽤Cookie 保存 session 或者 token ,向后端发送请求的时候带上 Cookie,这样后端就能取到session或者token了。这样就能记录⽤户当前的状态了,因为 HTTP 协议是⽆状态的。
3. Cookie 还可以⽤来记录和分析⽤户⾏为。举个简单的例⼦你在⽹上购物的时候,因为HTTP 协议是没有状态的,如果服务器想要获取你在某个⻚⾯的停留状态或者看了哪些商品,⼀种常⽤的实现⽅式就是将这些信息存放在Cookie。
102.Cookie 和 Session 有什么区别?如何使⽤Session进⾏身份验证?
Session 的主要作⽤就是通过服务端记录⽤户的状态。 典型的场景是购物⻋,当你要添加商品到购物⻋的时候,系统不知道是哪个⽤户操作的,因为 HTTP 协议是⽆状态的。服务端给特定的⽤户创建特定的 Session 之后就可以标识这个⽤户并且跟踪这个⽤户了。 Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。相对来说 Session 安全性更⾼。如果使⽤ Cookie 的⼀些敏感信息不要写⼊ Cookie 中,最好能将 Cookie 信息加密然后使⽤到的时候再去服务器端解密。
那么,如何使⽤Session进⾏身份验证?
很多时候我们都是通过 SessionID 来实现特定的⽤户,SessionID ⼀般会选择存放在 Redis 中。 举个例⼦:⽤户成功登陆系统,然后返回给客户端具有 SessionID 的 Cookie,当⽤户向后端发起请求的时候会把 SessionID 带上,这样后端就知道你的身份状态了。关于这种认证⽅式更详细的过程如下:
1. ⽤户向服务器发送⽤户名和密码⽤于登陆系统。
2. 服务器验证通过后,服务器为⽤户创建⼀个 Session,并将 Session信息存储起来。
3. 服务器向⽤户返回⼀个 SessionID,写⼊⽤户的 Cookie。
4. 当⽤户保持登录状态时,Cookie 将与每个后续请求⼀起被发送出去。
5. 服务器可以将存储在 Cookie 上的 Session ID 与存储在内存中或者数据库中的 Session 信息进⾏⽐,以验证⽤户的身份,返回给⽤户客户端响应信息的时候会附带⽤户当前的状态。
使⽤ Session 的时候需要注意下⾯⼏个点: 1. 依赖Session的关键业务⼀定要确保客户端开启了Cookie。 2. 注意Session的过期时间
画了个图简单总结了⼀下Session认证涉及的⼀些东⻄。
103.如果没有Cookie的话Session还能⽤吗?
⼀般是通过 Cookie 来保存 SessionID ,假如你使⽤了 Cookie 保存 SessionID的⽅案的话, 如 果客户端禁⽤了Cookie,那么Seesion就⽆法正常⼯作。 但是,并不是没有 Cookie 之后就不能⽤ Session 了,⽐如你可以将SessionID放在请求的 url ⾥ ⾯ https://javaguide.cn/?session_id=xxx 。这种⽅案的话可⾏,但是安全性和⽤户体验感降低。当然,为了你也可以对 SessionID 进⾏⼀次加密之后再传⼊后端。
104.什么是 Token?什么是 JWT?如何基于Token进⾏身份验证?
有没有⼀种不需要⾃⼰存放 Session 信息就能实现身份验证的⽅式呢?使⽤ Token 即可!JWT (JSON Web Token) 就是这种⽅式的实现,通过这种⽅式服务器端就不需要保存 Session 数据了,只⽤在客户端保存服务端返回给客户的 Token 就可以了,扩展性得到提升。
JWT 本质上就⼀段签名的 JSON 格式的数据。由于它是带有签名的,因此接收者便可以验证它的真实性。
JWT 由 3 部分构成:
1. Header :描述 JWT 的元数据。定义了⽣成签名的算法以及 Token 的类型。
2. Payload(负载):⽤来存放实际需要传递的数据
3. Signature(签名):服务器通过 Payload 、 Header 和⼀个密钥( secret )使⽤ Header ⾥⾯指定的签名算法(默认是 HMAC SHA256)⽣成。
在基于 Token 进⾏身份验证的的应⽤程序中,服务器通过 Payload 、 Header 和⼀个密钥 ( secret )创建令牌( Token )并将 Token 发送给客户端,客户端将 Token 保存在 Cookie 或 者 localStorage ⾥⾯,以后客户端发出的所有请求都会携带这个令牌。你可以把它放在 Cookie ⾥⾯⾃动发送,但是这样不能跨域,所以更好的做法是放在 HTTP Header 的 Authorization字段 中: Authorization: Bearer Token 。
1. ⽤户向服务器发送⽤户名和密码⽤于登陆系统。
2. 身份验证服务响应并返回了签名的 JWT,上⾯包含了⽤户是谁的内容。
3. ⽤户以后每次向后端发请求都在Header中带上 JWT。
4. 服务端检查 JWT 并从中获取⽤户相关信息。
105.微信登录获取token?
第一步:请求CODE(生成授权URL)
第二步:通过code获取access_token(开发回调URL)
106.BASE理论?
BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的简写,BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。接下来我们着重对BASE中的三要素进行详细讲解。
基本可用
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性——但请注意,这绝不等价于系统不可用,以下两个就是“基本可用”的典型例子。
· 响应时间上的损失:正常情况下,一个在线搜索引擎需要0.5秒内返回给用户相应的查询结果,但由于出现异常(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
· 功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者几乎能够顺利地完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
弱状态也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
最终一致性
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性
107.OSI与TCP/IP各层的结构与功能,都有哪些协议?
应⽤层(application-layer)的任务是通过应⽤进程间的交互来完成特定⽹络应⽤。应⽤层协议定 义的是应⽤进程(进程:主机中正在运⾏的程序)间的通信和交互的规则。对于不同的⽹络应⽤ 需要不同的应⽤层协议。在互联⽹中应⽤层协议很多,如域名系统DNS,⽀持万维⽹应⽤的 HTTP协议,⽀持电⼦邮件的 SMTP协议等等。我们把应⽤层交互的数据单元称为报⽂。
运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通⽤的数据传输服务。应⽤进程利⽤该服务传送应⽤层报⽂。“通⽤的”是指并不针对某⼀个特定的⽹络应⽤,⽽是多种应⽤可以使⽤同⼀个运输层服务。由于⼀台主机可同时运⾏多个线程,因此运输层有复⽤和 分⽤的功能。所谓复⽤就是指多个应⽤层进程可同时使⽤下⾯运输层的服务,分⽤和复⽤相反, 是运输层把收到的信息分别交付上⾯应⽤层中的相应进程。
在计算机⽹络中进⾏通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信⼦⽹。
⽹络层的任务就是选择合适的⽹间路由和交换结点,确保数据及时传送。在发送数据时,⽹络层把运输层产⽣的报⽂段或⽤户数据报封装成分组和包进⾏传送。在 TCP/IP 体系结构中,由于⽹络层使⽤ IP 协议,因此分组也叫 IP 数据报 ,简称数据报。
数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在⼀段⼀段的链路上传送的,这就需要使⽤专⻔的链路层的协议。 在两个相邻节点之间传送数据时,数据链路层将⽹络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。每⼀帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。 在接收数据时,控制信息使接收端能够知道⼀个帧从哪个⽐特开始和到哪个⽐特结束。这样,数据链路层在收到⼀个帧后,就可从中提出数据部分,上交给⽹络层。
物理层(physical layer)的作⽤是实现相邻计算机节点之间⽐特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。 使其上⾯的数据链路层不必考虑⽹络的具体传输介质是什么。 “透明传送⽐特流”表示经实际电路传送后的⽐特流没有发⽣变化,对传送的⽐特流来说,这个电路好像是看不⻅的
108.ARQ协议
⾃动重传请求(Automatic Repeat-reQuest,ARQ)是OSI模型中数据链路层和传输层的错误纠正协议之⼀。它通过使⽤确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。 如果发送⽅在发送后⼀段时间之内没有收到确认帧,它通常会重新发送。ARQ包括停⽌等待ARQ 协议和连续ARQ协议。
停⽌等待ARQ协议
停⽌等待协议是为了实现可靠传输的,它的基本原理就是每发完⼀个分组就停⽌发送,等待 对⽅确认(回复ACK)。如果过了⼀段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下⼀个分组;
在停⽌等待协议中,若接收⽅收到重复分组,就丢弃该分组,但同时还要发送确认;
优点: 简单
缺点: 信道利⽤率低,等待时间⻓
1) ⽆差错情况: 发送⽅发送分组,接收⽅在规定时间内收到,并且回复确认.发送⽅再次发送。 2) 出现差错情况(超时重传): 停⽌等待协议中超时重传是指只要超过⼀段时间仍然没有收到确认,就重传前⾯发送过的分组 (认为刚才发送过的分组丢失了)。因此每发送完⼀个分组需要设置⼀个超时计时器,其重传时 间应⽐数据在分组传输的平均往返时间更⻓⼀些。这种⾃动重传⽅式常称为 ⾃动重传请求 ARQ 。另外在停⽌等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续 ARQ 协议 提⾼信道利⽤率。发送维持⼀个发送窗⼝,凡位于发送窗⼝内的分组可连续发送出去,⽽不需要等待对⽅确认。接收⽅⼀般采⽤累积确认,对按序到达的最后⼀个分组发送确认,表明到这 个分组位置的所有分组都已经正确收到了。
3) 确认丢失和确认迟到 确认丢失 :确认消息在传输过程丢失。当A发送M1消息,B收到后,B向A发送了⼀个M1确 认消息,但却在传输过程中丢失。⽽A并不知道,在超时计时过后,A重传M1消息,B再次收到该消息后采取以下两点措施:1. 丢弃这个重复的M1消息,不向上层交付。 2. 向A发送确认消息。(不会认为已经发送过了,就不再发送。A能重传,就证明B的确认消息丢失)。 确认迟到 :确认消息在传输过程中迟到。A发送M1消息,B收到并发送确认。在超时时间内 没有收到确认消息,A重传M1消息,B仍然收到并继续发送确认消息(B收到了2份M1)。 此时A收到了B第⼆次发送的确认消息。接着发送其他数据。过了⼀会,A收到了B第⼀次发 送的对M1的确认消息(A也收到了2份确认消息)。处理如下:1. A收到重复的确认后,直接丢弃。2. B收到重复的M1后,也直接丢弃重复的M1。
连续ARQ协议
连续 ARQ 协议可提⾼信道利⽤率。发送⽅维持⼀个发送窗⼝,凡位于发送窗⼝内的分组可以连续发送出去,⽽不需要等待对⽅确认。接收⽅⼀般采⽤累计确认,对按序到达的最后⼀个分组发送确认,表明到这个分组为⽌的所有分组都已经正确收到了。
优点: 信道利⽤率⾼,容易实现,即使确认丢失,也不必重传。
缺点: 不能向发送⽅反映出接收⽅已经正确收到的所有分组的信息。 ⽐如:发送⽅发送了 5条消息,中间第三条丢失(3号),这时接收⽅只能对前两个发送确认。发送⽅⽆法知道后三个分 组的下落,⽽只好把后三个全部重传⼀次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。
109.滑动窗⼝和流量控制
TCP利⽤滑动窗⼝实现流量控制。流量控制是为了控制发送⽅发送速率,保证接收⽅来得及接收。接收⽅发送的确认报⽂中的窗⼝字段可以⽤来控制发送⽅窗⼝⼤⼩,从⽽影响发送⽅的发送速率。将窗⼝字段设置为 0,则发送⽅不能发送数据。
110.拥塞控制
在某段时间,若对⽹络中某⼀资源的需求超过了该资源所能提供的可⽤部分,⽹络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防⽌过多的数据注⼊到⽹络中,这样就可以使⽹络中的路由器或链路不致过载。拥塞控制所要做的都有⼀个前提,就是⽹络能够承受现有的⽹络负荷。
为了进⾏拥塞控制,TCP 发送⽅要维持⼀个拥塞窗⼝(cwnd) 的状态变量。拥塞控制窗⼝的⼤⼩取决于⽹络的拥塞程度,并且动态变化。发送⽅让⾃⼰的发送窗⼝取为拥塞窗⼝和接收⽅的接受窗⼝中⼩的⼀个。
TCP的拥塞控制采⽤了四种算法,即慢开始 、 拥塞避免 、快重传 和 快恢复。在⽹络层也可以使路由器采⽤适当的分组丢弃策略(如主动队列管理 AQM),以减少⽹络拥塞的发⽣。
111.在浏览器中输⼊url地址 ->> 显示主⻚的过程
1. DNS解析 2. TCP连接 3. 发送HTTP请求 4. 服务器处理请求并返回HTTP报⽂ 5. 浏览器解析渲染⻚⾯ 6. 连接结束
112. HTTP⻓连接,短连接?
在HTTP/1.0中默认使⽤短连接。也就是说,客户端和服务器每进⾏⼀次HTTP操作,就建⽴⼀次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的Web⻚中包含有其他的Web资源(如JavaScript⽂件、图像⽂件、CSS⽂件等),每遇到这样⼀个Web资源,浏览器就会重新建⽴⼀个HTTP会话。
⽽从HTTP/1.1起,默认使⽤⻓连接,⽤以保持连接特性。使⽤⻓连接的HTTP协议,会在响应头加⼊这⾏代码:Connection:keep-alive
在使⽤⻓连接的情况下,当⼀个⽹⻚打开完成后,客户端和服务器之间⽤于传输HTTP数据的 TCP连接不会关闭,客户端再次访问这个服务器时,会继续使⽤这⼀条已经建⽴的连接。KeepAlive不会永久保持连接,它有⼀个保持时间,可以在不同的服务器软件(如Apache)中设定这 个时间。实现⻓连接需要客户端和服务端都⽀持⻓连接。
113.HTTP 1.0和HTTP 1.1的主要区别是什么?
同时HTTP1.1也是当前使⽤最为⼴泛的HTTP协议。 主要区别主要体现在:
1. ⻓连接 : 在HTTP/1.0中,默认使⽤的是短连接,也就是说每次请求都要重新建⽴⼀次连接。 HTTP 是基于TCP/IP协议的,每⼀次建⽴或者断开连接都需要三次握⼿四次挥⼿的开销,如果每次请求都要这样的话,开销会⽐较⼤。因此最好能维持⼀个⻓连接,可以⽤个⻓连接来发多个请求。HTTP 1.1起,默认使⽤⻓连接 ,默认开启Connection: keep-alive。 HTTP/1.1 的持续连接有⾮流⽔线⽅式和流⽔线⽅式 。流⽔线⽅式是客户在收到HTTP的响应报⽂之前就能接着发送新的请求报⽂。与之相对应的⾮流⽔线⽅式是客户在收到前⼀个响应后才能发送下⼀个请求。
2. 错误状态响应码 :在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发⽣冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
4. 带宽优化及⽹络连接的使⽤ :HTTP1.0中,存在⼀些浪费带宽的现象,例如客户端只是需要某个对象的⼀部分,⽽服务器却将整个对象送过来了,并且不⽀持断点续传功能,HTTP1.1 则在请求头引⼊了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就⽅便了开发者⾃由的选择以便于充分利⽤带宽和连接。
114.什么是HTTP2?
HTTP2 可以提高了网页的性能。
在 HTTP1 中浏览器限制了同一个域名下的请求数量(Chrome 下一般是六个),当在请求很多资源的时候,由于队头阻塞当浏览器达到最大请求数量时,剩余的资源需等待当前的六个请求完成后才能发起请求。
HTTP2 中引入了多路复用的技术,这个技术可以只通过一个 TCP 连接就可以传输所有的请求数据。多路复用可以绕过浏览器限制同一个域名下的请求数量的问题,进而提高了网页的性能。
115.HTTP 和 HTTPS 的区别?
1. 端⼝ :HTTP的URL由“http://”起始且默认使⽤端⼝80,⽽HTTPS的URL由“https://”起始且默认使⽤端⼝443。
2. 安全性和资源消耗: HTTP协议运⾏在TCP之上,所有传输的内容都是明⽂,客户端和服务器端都⽆法验证对⽅的身份。HTTPS是运⾏在SSL/TLS之上的HTTP协议,SSL/TLS 运⾏在 TCP之上。所有传输的内容都经过加密,加密采⽤对称加密,但对称加密的密钥⽤服务器⽅的证书进⾏了⾮对称加密。所以说,HTTP 安全性没有 HTTPS⾼,但是 HTTPS ⽐HTTP耗费更多服务器资源。 对称加密:密钥只有⼀个,加密解密为同⼀个密码,且加解密速度快,典型的对称加密 算法有DES、AES等; ⾮对称加密:密钥成对出现(且根据公钥⽆法推知私钥,根据私钥也⽆法推知公钥), 加密解密使⽤不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度慢,典型的⾮对称加密算法有RSA、DSA等。
116.什么是临界区?
每个进程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个进程使用的共享资源)。每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问。
多个进程中涉及到同一个临界资源的临界区称为相关临界区。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.bianchenghao6.com/h6javajc/2205.html