Java
Java修饰符
final关键字
修饰类——此类不能被继承;
修饰基本数据类型——数值不能修改;
修饰引用数据类型——初始化之后不能再让其指向别的对象;
String不可变
- 不可变:不可改变内部成员变量,引用类型的变量不能指向其他的对象。
- String内部的value字节数组是final的。
- 存放在方法区。
静态绑定(编译期)和动态绑定(运行期)
绑定指的是一个方法的调用与方法所在的类(方法主体)关联起来。
静态绑定:
编译期的绑定,java当中的方法只有final,static,private和构造方法是前期绑定。
重载overload
动态绑定:
运行时根据具体对象的类型进行绑定。
重写override
11.static:方法区
static表明:一个成员变量或方法能够在没有所属的类的实例变量的情况下被访问。
static和private方法不能被覆盖override,因为方法覆盖是动态绑定,而他们是静态绑定。
12. StringBuffer与StringBuilder
StringBuffer线程安全,比他多synchronized。
13.类加载的双亲委派模型
-
一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。这样的好处是基础类得到了统一,比如Object类。
-
避免了多份同样字节码的加载。内存是宝贵的,没必要保存相同的两份 Class 对象,例如 System.out.println() ,实际我们需要一个 System 的 Class 对象,并且只需要一份,如果不使用委托机制,而是自己加载自己的,那么类 A 打印的时候就会加载一份 System 字节码,类 B 打印的时候又会加载一份 System 字节码。而使用委托机制就可以有效的避免这个问题。
如何打破双亲委派模型?
具体请看56.SPI介绍
14.接口和抽象类区别
接口 | 抽象类 | |
---|---|---|
方法 | 全抽象 | 可抽象可不抽象 |
Java | 可实现多接口 | 只能单继承 |
成员变量 | 默认final | 可以非final |
成员函数 | 默认public | 可以任意 |
接口中的所有方法都是隐式public的,abstract的和非static的。
15. Comparable接口和Comparator接口
都属于比较器,Comparable接口仅compareTo()方法,Comparator接口两个方法compare()和equals()。
31. 对象在JVM堆中的存放
对象头: Mark Word、类型指针(通过这个指针确定对象是哪个类的实例)、数组长度(如果是数组就有,否则无)
MarkWord长度不是固定的。
实例数据:
实例数据是对象真正存储的有效信息,就是代码中定义的各种类型的字段内容。
对齐填充:
由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
32. String拼接的底层原理
public class Test1 {
public static void main(String[] args) {
String str1="hello";
String str3="hello word!";
String str4=str1+"word!";
System.out.println(str3==str4);//false
}
}
String类型的“+”
的底层实现:
建立临时的StringBuilder对象,通过调用append()方法,最后调用toString()方法返回,StringBuilder的toString()是new String实现的。
此题str4是堆中的地址,而str3是方法区常量池中的地址。因此不同,false。
33. 常量池
- 方法区包括:类信息+常量池+静态变量+JIT编译器编译后的代码。
-
字面量:如字符串、声明为final的常量等。
-
常量池的好处:为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
-
字符串常量池:
-
Java基本类型的包装类大部分实现了常量池(Byte,Short,Integer,Long,Character,Boolean),比如Integer默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。
-
举例子:
-
常量池的动态性:即不止是编译期间能生成常量,在运行期间也可放入常量池,比如String类的
intern()
方法。
String的intern()
方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。
36. JMM内存模型
42. Java基本数据类型和位数
一字节==8bit
基本数据类型 | 位数 |
---|---|
byte | 8bit |
char | 两字节=16bit |
short | 两字节=16bit |
int | 四字节=32bit |
float | 四字节=32bit |
double | 8字节=64bit |
long | 8字节=64bit |
boolean | 1bit |
42. 基本数据类型与包装类
45. Hash冲突如何解决?
①线性探测:一旦出现了hash冲突,去寻找下一个空的散列地址。
②双哈希法:即存在多个hash计算函数,当发生冲突时,就采取其他的hash函数计算地址
③链地址:hashmap的基本原理
④建立溢出表:hashmap = 基本表+溢出表,冲突的单独存放
补充一下hashmap的链地址法:
Java7 HashMap 查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n) 。
在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
47. static变量
- 存放于方法区
- 方法区也可能发生GC,但条件比较苛刻:
1. 所有实例被回收
2. 加载该类的ClassLoader 被回收
3. Class无法被任何途径包括反射访问
51. 序列化底层
序列化概念
将Java对象保存为二进制字节码文件。
为什么序列化?
Java的对象声明周期短,序列化就能将对象持久化。
比如在RPC调用时,相互调用时就在网络中传输序列化后的对象。
序列化操作
static和transient字段不能被序列化。
52. 注解的实现
注解能够用java反射进行提取,得到响应的标记值。
53. 反射
反射允许程序在运行的时候动态执行对象的方法、改变对象的属性。
最重要的应用是spring的IOC框架。
步骤是:
- 解析bean的xml文件,将BeanDefinition保存在hashmap中。
- 遍历hashmap中逐条取出BeanDefinition,利用Java反射实例化对象。
54. 异常
运行时异常:也叫不检查异常,比如算数异常、下标越界。
非运行时异常:也叫检查异常,比如IO异常、SQL异常。
55. Java对象实例化
56. SPI机制(Service Provider Interface)
背景:
原始的类加载机制,是双亲委派模型
比如启动类加载器bootstrap会加载如下的包:
场景:大部分开发是面向接口的,也就是实现类是很可能出现更新的,一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。于是就有了SPI这种服务发现机制。
如何使用SPI机制:JDK提供了一个服务发现查找类:java.util.ServiceLoader
,这个就可以让你来加载自己的实现类。
举例:
SPI机制的应用
JDBC
Connection connection = DriverManager.getConnection("jdbc://xxxx", "root", "xxx");
总结:在加载DriverManager类时,通过SPI机制的线程上下文加载器
加载mysql驱动包下的类。
springboot中SPI机制的应用
当你使用@SpringBootApplication
注解时,会开始自动配置,而启动配置则会去扫描META-INF/spring.factories
下的配置类。
@SpringBootApplication
包含几个注解:
->@ComponentScan
(扫描子包中被@Service,@Repository,@Component,@Controller标记的bean)
->@SpringBootConfiguration
当前类时一个配置类,当前类内声明的一个或多个以@Bean注解标记的方法的实例纳入到spring容器中,并且实例名就是方法名。
->@EnableAutoConfiguration
->AutoConfigurationImportSelector.class
其中的selectImports()
方法->SpringFactoriesLoader.loadFactoryNames()
而这个SpringFactoriesLoader属于Spring框架私有的一种扩展方案,其主要功能就是从指定的配置文件META-INF/spring.factories加载配置。
因此@EnableAutoConfiguration
意味着从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射(Java Refletion)实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总为一个并加载到IoC容器。
57.内部类
成员内部类
-
无论内部类的修饰符是什么,内部类的对象可访问外部类的一切。
通过javap的反编译看一下:
也就是内部类中默认存在一个final的外部类变量,因此能访问所有的外部类属性和方法。 -
必须使用外部类的对象来创建内部类的对象。
静态内部类
static修饰的内部类
- 静态内部类不能直接访问外部类的非静态成员,但可以通过
new 外部类().成员
的方式访问; - 创建静态内部类的对象时,不需要外部类的对象,可以直接创建;
方法内部类
- 方法内部类仅在此方法内能使用
- 由于方法内部类不能在外部类的方法以外的地方使用,因此方法内部类不能使用访问控制符和 static 修饰符。
匿名内部类
一个没有名字的方法内部类
public class Button {
public void click(){
//匿名内部类,实现的是ActionListener接口
new ActionListener(){
public void onAction(){
System.out.println("click action...");
}
}.onAction();
}
//匿名内部类必须继承或实现一个已有的接口
public interface ActionListener{
public void onAction();
}
public static void main(String[] args) {
Button button=new Button();
button.click();
}
}
- 和方法内部类一样,无访问修饰符
- 必须继承一个抽象类或者实现一个接口
- 匿名内部类中不能存在任何静态成员或方法
JVM
1. 一个对象在哪里?一个成员变量存在哪里?一个局部变量存在哪里?如果是局部变量是一个对象的引用存在哪里?
对象在堆,成员变量在堆(因为是某一个对象的成员变量),局部变量在虚拟机栈的局部变量表,局部变量的引用也在虚拟机栈的局部变量表。
2. Java创建对象流程
- 类加载检查: 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 分配内存: 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
在堆区分配内存时的并发问题:
- CAS乐观锁失败重试
- 本地线程分配缓冲TLAB:为每一个线程预先在 Eden 区分配一块内存,称为本地线程分配缓冲。哪个线程要分配内存,就在哪个线程的TLAB上分配,当对象大于TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
-
初始化零值: 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
-
设置对象头: 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
-
执行 init 方法: 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
3. 对象的访问的方法?
- 使用句柄:那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
- 直接指针:
Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。
- 两种方法对比?
-
使用句柄来访问最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据的指针,而reference本身不需要修改。
-
使用直接指针访问方式最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。
4. GC是在什么时候对什么东西做了什么?
- 什么时候?分为MinorGC和fullGC来讲
新创建对象在Eden区,当Eden满了触发minorGC,存活的放进from区,加年龄,当再次触发minorGC时,将Eden和from存活的全放进to区,加年龄,from与to交换。
再经过几次Minor GC之后,当存活对象的年龄达到一个阈值之后(-XX:MaxTenuringThreshold默认是15),就会被从青年代到老年代。
在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC,注意Full GC的对象比较稳定,采用的算法是“标记——整理”。
- 针对什么?
死亡对象。
如何确定对象死亡?
①引用计数法,给对象添加一个引用计数器,但无法解决循环应用的问题,弃用。
②可达性分析法,通过一系列的GC root节点向下走,某对象没有任何引用链到GC root时,说明对象不可用。
但是一次不可以,至少两次的标记过程,第一次标记是筛选出此对象是否需要执行finalize()方法?
,(如果此对象没有finalize方法或JVM已经执行过finalize方法就不需要),如果此对象要执行finilize方法,就将此对象放置到一个队列F-Queue中,在此队列F-Queue中再次进行一次标记过程,还留在这个队列中的就被GC。
- 做什么事情?GC
采用复制算法:无内存碎片,效率高,但是复制耗时,对于剩下的对象少的适用
采用标记整理算法:无内存碎片,效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
9. 类加载
同一个类最多只会被加载一次(要注意对JVM来说“同一个类”并不是类的全限定名相同就足够了,而是<类全限定名, 定义类加载器>一对都相同才行)。双亲委派模型!
-
Java代码通过
javac.exe
编译为字节码.class文件 -
类加载器将字节码文件送入JVM
-
类周期七步骤:加载、验证、准备、解析、初始化、使用、卸载
-
类加载五步骤:加载、验证、准备、解析、初始化
-
加载:字节码加载进JVM,在方法区中生成一个
java.lang.Class
对象,双亲委派模型。(注意这里也可以从jar中加载,也可以在运行时由动态代理生成) -
验证:校验加载进来的.class文件中的内容是否符合规范
-
准备:为静态变量(static)在方法区中分配内存 + 设置初始值(注意是初始值,比如int全为0)
-
解析:符号引用转化为直接引用。
符号引用是编译过程中A中引用了B,在A中就用一个字符串(此字符串成为符号引用)来代表B的地址,当到了“解析”步骤,就将B也加载进来,成为直接引用。
注意:存在着静态绑定(final,static,private和构造方法)和动态绑定(多态),静态绑定在此阶段完成,而动态绑定中B是一个抽象类或者接口,有多个实现类,在使用中才能确定。
静态绑定好处:在编译期就发现程序的问题。 -
初始化:真正的赋值操作;执行静态代码块;如果父类没加载和初始化,也会触发父类的加载和初始化。
-
使用:用对象
-
卸载:卸载不用的类,而类需要同时满足下面3个条件才算无用的类:
- Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
-
49. 三色标记法
https://www.jianshu.com/p/12544c0ad5c1
G1防止漏标的办法:快照
CMS防止漏标的办法:增量更新
而是针对新增的引用,将其记录下来等待遍历,即增量更新(Incremental Update)。
11. 垃圾收集器
CMS:获取最短回收停顿时间为目标的收集器
标记-清除
初始登记(标记一下GC Roots能直接关联到的对象,这个过程速度很快)、并发标记(进行GCRoots Tracing的过程)、重新标记(修正并发标记期间因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录,速度稍慢)、并发清除(清除死亡的对象)4个步骤;其中,初始标记和重新标记仍然需要“Stop The World”。
最耗费时间的并发标记和并发清楚过程收集器线程和用户线程是一起工作的,所以总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
G1:
它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。每个Region大小都是一样的,可以是1M到32M之间的数值,根据总的内存大小而定
并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:与CMS的“标记-清理”算法不同,G1从整体看来是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上看是基于“复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
可预测的停顿:这是G1相对于CMS的另外一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器特征了。
JUC
5. volatile作用?
- 保证内存可见性
正常情况下:每个线程从主内存中取数据,在自己的工作内存中进行操作,再将修改后的结果放回主内存。
加了volatile:汇编指令前缀加了lock指令,触发MESI缓存一致性协议。
讲解:A与B线程取X值,A拿到了,A手里的值状态为E;B拿到了,A与B手里的值状态都为S;这时A进行了修改值,手里的状态变为M;总线会不断监听,发现有了M状态,将B设置为I状态,故线程B需重新获取值。
当然,MESI协议也可能不生效(比如数据长度超过一个缓存行时),这时触发总线加锁机制:第一个线程拿到这个X时,其他线程都不能拿到。
- 禁止指令重排序
举例double check lock的懒汉式单例模式:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
必须有volatile修饰,因为 instance = new Singleton();
不是原子操作:(分配内存、初始化instance、将instance指向分配的内存空间),如果先执行了后两个,其他线程就能拿到一个半成品变量。
拿什么实现的禁止指令重排序?
内存屏障,即lock指令,
lock指令:①重排序时不能把后面的指令重排序到内存屏障之前的位置 ②触发MESI缓存一致性协议。
问volatile能不能保证原子性?
不可以,比如i++,这不是一个原子性操作。只能保证取值时是从主内存取的,但是修改分为三步。
21.线程池
面向对象编程中,对象的创建和销毁是耗时的,线程池就是事先创建好若干个可执行的线程并放入一个容器中,需要时就从此获取,使用完就放回池中,减少频繁创建和销毁对象的开销。
设置主要参数为:核心线程数、最大线程数、等待队列、拒绝策略、空闲线程的存活时长。
当需要的任务大于核心线程数时,就放进等待队列;
等待队列满了就增加线程数目,直到最大线程数;
如果已经最大线程数并且等待队列也满了就执行拒绝策略。
问:几种线程池?
答:
Java5以后的都有Executor接口规定了执行线程的工具,工具类Executors中提供了一些静态的工厂方法。比如:
1.newFixedThreadPool创建一个指定工作线程数量的线程池;
2.newCachedThreadPool创建一个可缓存的线程池,无限制工作线程的创建数量;
3.newSingleThreadExecutor只创建唯一的工作者线程,
4.newScheduleThreadPool周期执行任务的线程池
ThreadPoolExecutor的重要参数:
1、corePoolSize:核心线程数
* 核心线程会一直存活,及时没有任务需要执行
* 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
* 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
2、queueCapacity:任务队列容量(阻塞队列)
* 当核心线程数达到最大时,新任务会放在队列中排队等待执行
3、maxPoolSize:最大线程数
* 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
* 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
4、 keepAliveTime:线程空闲时间
* 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
* 如果allowCoreThreadTimeout=true,则会直到线程数量=0
5、allowCoreThreadTimeout:允许核心线程超时
6、rejectedExecutionHandler:任务拒绝处理器
* 两种情况会拒绝处理任务:
- 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
- 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
* 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常
* ThreadPoolExecutor类有几个内部实现类来处理这类情况:
- AbortPolicy 丢弃任务,抛运行时异常
- CallerRunsPolicy 执行任务
- DiscardPolicy 忽视,什么都不会发生
- DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
* 实现RejectedExecutionHandler接口,可自定义处理器
如何设置参数?
1、默认值
* corePoolSize=1
* queueCapacity=Integer.MAX_VALUE
* maxPoolSize=Integer.MAX_VALUE
* keepAliveTime=60s
* allowCoreThreadTimeout=false
* rejectedExecutionHandler=AbortPolicy()
2、如何来设置
* 需要根据几个值来决定
- tasks :每秒的任务数,假设为500~1000
- taskcost:每个任务花费时间,假设为0.1s
- responsetime:系统允许容忍的最大响应时间,假设为1s
* 做几个计算
- corePoolSize = 每秒需要多少个线程处理?
* threadcount = tasks/(1/taskcost) =tasks*taskcout = (500~1000)*0.1 = 50~100 个线程。corePoolSize设置应该大于50
* 根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可
- queueCapacity = (coreSizePool/taskcost)*responsetime
* 计算可得 queueCapacity = 80/0.1*1 = 80。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行
* 切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
- maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)
* 计算可得 maxPoolSize = (1000-80)/10 = 92
* (最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数
- rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
- keepAliveTime和allowCoreThreadTimeout采用默认通常能满足
3、 以上都是理想值,实际情况下要根据机器性能来决定。如果在未达到最大线程数的情况机器cpu load已经满了,则需要通过升级硬件(呵呵)和优化代码,降低taskcost来处理。
25. 为什么CAP特性不能同时满足?
C:一致性
A:可用性
P:分区容错性
假设目前满足了P,即分区容错(比如nginx做了服务器集群的负载均衡),我向服务器A插入了一个数据,那么服务器A向服务器B的同步更新会耗费一定的时间。
故:假设此时服务器A与服务器B的中间网络出现了很大延迟,我再进行查询我之前插入的数据时,两种可能:①放弃C:我拿走旧数据。②放弃A:我等一会,等到更新完成。
再想:可不可以CA呢?可以的,但是没意义,我要做的就是系统的分区容错,不可能所有数据放一台服务器上。
因此无法同时CAP!
26. Java实现多线程两种方式
①继承Tread类+重写run()
②实现Runnable接口+实现run()方法
调用均为xxxx.start()
27. 为什么不推荐使用stop()、resume()和suspend()操作线程?
stop方法用于终止一个线程的执行,resume方法用于恢复线程的执行,suspend方法用于暂停线程的执行
stop()会解除线程获取的所有锁,很容易被其他线程继续修改此对象,比如在付款的界面,你刚扣完款,就stop了,对方没收到。
suspend()是挂起,依然保持所有锁,容易造成死锁,直到resume()。
28. 线程五种状态
sleep()
由running状态转为blocked状态,不释放锁,到时间自动恢复为runnable状态。将执行机会让给其他线程。
sleep与wait都依赖于操作系统的park()函数(导致线程放弃CPU被挂起)
wait()
调用了wait方法导致本线程放弃对象锁,只有此对象发起notify()才能唤醒此线程进入锁池队列(准备获取对象锁)。
wait()内部首先释放对象锁,之后将当前线程阻塞在某个内置的condition队列中,而这个condition仅能通过notify()来改变。 注意:wait、notify、notifyAll都是通过monitor对象来实现的
notify()和notifyAll()
只要调用了他,处于wait()中的线程所需要的condition就满足了,转入AQS阻塞队列(底层,双向链表,再问:既然是链表为什么会有抢锁,不是按照链表来吗?答:比如lock可以实现公平锁和非公平锁,公平锁实现的原理就是在判断条件内加上是否此列表为空,不为空从列表中拿,为空就随意,非公平锁就没有这个判断条件,因此是竞争关系),而AQS的阻塞队列依然需要竞争到锁才可以。
stop()已弃用
直接将线程所有锁放弃并进入死亡状态。
suspend()已弃用
由running转化为blocked状态,不会释放锁,容易死锁。只有resume()了才会转为runnable状态。
yield()
暂停,从running转为runnable状态,给相同等级或高等级线程执行机会。
使当前获得CPU的线程让出CPU的资源,
join()
xx线程.join(),会立即使当前线程停止进入blocked状态并等待xx线程执行完毕才再次进入runnable状态。
29. Synchronized底层原理
管程!
synchronized加在方法上:
在class字节码文件的方法表结构中ACC_SYNCHRONIZED 访问标志位被设置。
synchronized加在代码块上:
进入synchronized在JVM 执行时对这堆代码前后分别添加monitorEnter和monitorExit指令,当前线程尝试持有此对象的monitor,结束再释放。计数器的+1和-1操作。
如果想要查看,请利用javap反编译器。
monitorEnter
:
每个对象有一个monitor,线程执行此指令就占用了此monitor的所有权。
monitorExit
:
monitor进入数-1,直到该monitor的进入数减为0,此时该线程不再是该monitor的所有者,其他被阻塞进入该monitor的线程可以尝试获取该monitor的所有权。
30. synchronized优化
JDK6以前synchronized全都是重量级锁!
Java6进行了优化:
【1】:锁升级
【2】:锁消除。编译器优化,通过逃逸分析消除不必要加锁的代码。
【3】:锁粗化。在编译期间,将相邻的同步代码块合并为一个大的同步代码块。
【4】:自适应自旋锁。sychronized CAS尝试获取锁,会进行一段时间的自旋尝试,次数自适应。
同时用户自己可以优化的方面:
- 减少不必要的同步代码
- 减少锁的粒度(比如jdk8concurrentHashMap是基于synchronized实现的分段加锁)
1. 锁升级
操作系统实现线程的切换需要从用户态转化到核心态,这个转化耗时,Java6进行了synchronized优化,synchronized升级之路为:无锁状态、偏向锁、轻量级锁、重量级锁。只能升级并无法降级。
synchronize锁的是什么?对象!无论是自己造的对象(堆)还是Class对象(方法区)。
对象在堆中存的mark word中存放了锁状态的标志位:
这个对象处于什么状态 | 此对象的Mark Word中存放着 |
---|---|
偏向锁 | 偏向的线程ID |
轻量级锁 | 指向线程私有的栈帧中的锁记录lock record指针 |
重量级锁 | 指向线程共享的堆中monitor对象的指针 |
线程在进入同步块之前,JVM会在当前线程私有的虚拟机栈中的栈帧中创建一个锁记录lock record(用于保存对象头的Mark word的初始结构复制,又称为displaced mark word)。 | |
Displaced mark word或lock record:保存未被锁定状态下的对象原本的mark word,用于锁状态的切换。 | |
偏向锁:
某段同步代码总是被一个线程所访问。比如单线程下的锁都是偏向锁。
锁都是对象,此对象的Mark Word中记录着线程ID,下次判断还是这个线程,就不会再加锁了,如果不是这个线程了,说明出现了线程并发现象,执行偏向锁的撤销,偏向锁的撤销需要等待全局安全点,然后它会首先暂停拥有偏向锁的线程,然后判断线程是否还活着,如果线程还活着,则升级为轻量级锁,否则,将锁设置为无锁状态。
偏向锁的目的是消除CAS的开销,只需将对象的markword中添加线程ID,后续发生重入时就检查一下线程ID,简单来说,偏向锁一旦被某线程获得,除非出现了线程竞争导致偏向锁撤销(等到全局安全点),否则线程不会主动释放锁,即线程ID只能被设置一次。
轻量级锁:
轻量级锁,锁标志位是00,锁对象的markword中是指向lock record的指针,而lock record中存放着“未锁定时对象的markword”用于替换回去。
主要加轻量级锁步骤是:
进入这个步骤的有两种(无锁状态–》轻量级,偏向锁–》轻量级),他们的步骤均为:
①在线程私有的栈帧(局部变量表,操作数栈,动态连接,方法地址)中开辟一块Lock Record空间,将目前的锁对象的堆中的markword复制一份放进去。
②尝试CAS试图修改锁对象的markword,尝试修改成指向Lock Record地址值,CAS成功就获取到了轻量级锁,CAS失败再进行锁升级。
优势:
避免了生成monitor对象,适用于线程交替执行同步块
重量级锁:
依赖于堆中的monitor对象,而monitor对象依赖于操作系统中的mutex互斥量,也依赖于操作系统的系统调用,存在系统调用就必须有上下文切换(核心态和用户态的切换)。
加重量级锁的步骤:
前面的CAS操作失败了,锁膨胀,此时线程进行争抢锁,争抢到了就执行,如果没争抢到锁,就自旋一段时间(为了减少“阻塞线程再唤醒”的开销),这段也称为自适应自旋。
修改此锁对象的MarkWord,让其指向monitor对象。
2. 锁消除
由JIT编译器解决,JIT编译器两种功能:逃逸分析+标量替换。
逃逸分析:检查一个对象的被引用范围是否超出了某代码块,减轻堆上分配内存的压力(GC次数),或者进行锁消除。
举个例子:
通过jmap
命令查看jvm堆的快照,开启逃逸分析能极大地减少在堆上分配的实例数。
原因:
- 逃逸分析与标量替换分不开
- 通过逃逸分析,JVM发现MyObject就是一个聚合量(标量,就是指JVM中无法再细分的数据,比如int、long、reference等),JVM发现MyObject并没有逃出allocate()的代码块。
- 因此采取标量替换,使得myobject对象全拆成标量,即
- 因此在执行代码的过程中并没有在堆上生成myobject对象,而是直接在虚拟机栈的某栈帧的局部变量表上分配了内存,降低了堆上GC的压力。
3. 锁粗化
底层原理未知,知道后补上。
JVM检测到一系列连续的操作都对同一个对象进行加锁,会把整个锁加载这一系列操作的外部。
4. 自适应自旋锁
自旋其实就是空转 CPU,执行一些无意义的指令,目的就是不让出 CPU 等待锁的释放。
正常情况下锁获取失败就应该阻塞入队,但是有时候可能刚一阻塞,别的线程就释放锁了,然后再唤醒刚刚阻塞的线程,这就没必要了。
所以在线程竞争不是很激烈的时候,稍微自旋一会儿,指不定不需要阻塞线程就能直接获取锁,这样就避免了不必要的开销,提高了锁的性能。
5. 加锁整体流程图
6. Lock(ReentrantLock实现类)与Synchronized关键字区别
- 采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象,释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
- synchronized就不是可中断锁,而Lock是可中断锁。
Lock通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的;而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。 - synchronized和Lock都具备可重入性。
- synchronized不公平,Lock可设置成公平锁。
- synchronized原本是悲观锁,而Lock的实现类ReentrantLock中是乐观锁,其获得锁的一个方法是compareAndSetState
- Lock底层是volatile和CAS实现;synchronized底层是字节码指令就是增加两个指令:monitorenter、monitorexit,当执行到monitorenter时,获取锁,并计数器加1,当它遇到一个monitoerexit时,琐计数器会-1,当计数器为0时,就释放锁。
30. 信号量和互斥量区别
互斥量mutex:强调“互斥”,比如synchronized和Lock,只是保证临界资源被一个 线程访问,互斥量的获取者与释放者必须是同一个线程。
信号量semaphore:强调“顺序”,比如semaphore,是为了线程之间的同步运行,A线程执行完自己的操作后,发送了semaphore,B线程获取到semaphore后接着执行自己的操作。
比如在我netty的rpc小项目中,如果不用zookeeper注册中心,客户端直接连接服务端,connect是耗时的,在connect之后release一个semaphore,后续的工作acquire一个semaphore,这样才能继续执行。
注意semaphore在单线程使用,也可以实现互斥的功能!
34. AQS原理
AbstractQueuedSynchronizer抽象队列同步器。比如ReentrantLock
,Semaphore
都是基于此实现的,并没直接继承,是内部维护一个静态类sync,其继承了AbstractQueuedSynchronizer。因此属于模板模式。
- 内部维护了:volatile int state一个状态变量+同步等待队列(双向链表)+条件等待队列
- 暂时获取不到锁的线程放入同步等待队列内,暂时不满足条件的线程放在某条件的条件等待队列中。
- 举例子:
1. ReentrantLock独占锁——公平、非公平
首先非公平与公平采用一个boolean变量分辨。
非公平
lock()
:我想加锁的操作。
当此前的某一线程释放锁时,此时又来了一个线程A进行lock(),很可能A在同步等待队列之前获取到了锁,此为非公平。
处于同步队列中的线程是绝对保证串行获取锁,而其它新来的却可能抢先获取锁。
公平
lock():
2. semaphore共享锁
即能够支持多个线程一起运行。
也有公平与非公平的选择:公平是根据acquire()的顺序。
即设定state的数字可以大于1,多少都可以。
3. ReentrantReadWriteLock 读写锁:组合式
读是共享锁,写是独占锁。
底层使用二进制存储信息。
35. LongAdder和AtomicLong区别
AtomicLong的原理是依靠底层的cas来保障原子性的更新数据,在要添加或者减少的时候,会使用死循环不断地cas到特定的值,从而达到更新数据的目的。
CAS机制:在一个死循环内,不断尝试修改目标值,直到修改成功。
AtomicLong在高并发场景下,假设竞争非常激烈,会存在很多不断死循环的代码,影响性能。
LongAdder是为了解决AtomicLong在高竞争条件下的不足,低并发条件时仍然是是CAS修改base值(目标值),高并发下,采取一个cell[],每个线程进行操作时映射到其中一个数字上进行计数,最终返回当前对象的实际值是sum全cell[],提高了并发度。
43. ThreadLocal
请参考博文:ThreadLocal精讲
- 面对多线程场景下共享变量,可以使用ThreadLocal。
- 我们可以在一个线程内生成很多ThreadLocal变量,对于每一个变量,内部存在着static的ThreadLocalMap,这是一个hashmap,其中的key是当前的ThreadLocal变量,value是存入的值。
- 也就是我们声明了一个ThreadLocal变量,实际上是生成了一个变量表,对于不同线程,对于ThreadLocal变量,线程之间是隔离的,单个线程内是共享的。
- InheritableThreadLocal能够提供父与子线程的变量共享
源码分析:
即每个thread都存在一个ThreadLocalMap,key是threadLocal变量,value是相应的值。
set():
44. ThreadLocal的内存泄露问题
内存泄露:程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
key是弱引用,value是强引用。
源码:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
因此Entry继承了WeakReference是弱引用,这时候GC线程运行,发现弱引用就回收,key被回收。因此出现了key是null但是value却不为空的键值对。
解决办法:
46. ThreadLocalMap如何解决hash冲突?
采取线性探测法。
为什么不像hashmap一样用链地址法?
答:ThreadLocal的对象不多,采取线性探测法效率不低,冲突概率也不高
50. 分布式锁:比如不同服务器实现同一定时任务
不同服务器之间操作共享数据
zookeeper实现分布式锁
https://blog.csdn.net/kongmin_123/article/details/82081953
- zookeeper节点类型:临时节点、永久节点、永久顺序节点、临时顺序节点
实现分布式锁的步骤:
- 在zookeeper创建永久节点A。
- 众多服务器尝试连接A,只要连接上了,就在A节点下创建临时顺序节点。
- 排在第一位的拿到锁、第二位注册watcher监控第一位是否还存在,第三位监控第二位
- 第一位操作完直接断开与zookeeper的连接就行,因为zookeeper时CP的能够保证一致性,所以没问题。
数据库实现分布式锁
- 创建一张锁表。
- 三个字段:设备序号+上一次执行任务的时间戳+任务号
- 执行任务时,首先去查询这个任务号的那一行,看上一次的执行时间离当前时间间距是否超过了阈值,如果超过了就说明没人在执行任务,保存当前设备号和当前时间进这一条数据。
- 如果发现设备号还是自己,也可以实现可重入。
redis实现分布式锁
方式一:
redisson
- 引入redisson依赖。redisson的图如下:
- 客户端面对着一堆redis cluster节点,首先根据hash算出一台机器
- 发送一段lua脚本(能够保证一堆代码的原子性)
先检查我要加锁的key是否存在;
加锁,记录了客户端ID,默认生存时间 - 此时客户端2来了执行同一段lua脚本
判断了这个锁的key是否存在?此时已经存在了,就判断一下ID是不是自己?发现不是,会得到一段这个锁的剩余时间。然后一直自旋尝试加锁。 - 包括存在watch dog机制,后台线程,每隔一段时间检查一下是否客户端1仍然持有此锁,如果仍然持有就延长生存时间。
- 重入:加锁次数+1
- 释放:删除key
- 缺陷:可能在redis主从复制的过程中,出现了多个客户端都连接到了同一个上。
方式二:
setnx = set if not exists
-
先拿 setnx 来争抢锁, 抢到之后, 再用 expire 给锁加一个过期时间防止锁忘了释放;
-
同时把 setnx 和 expire 合成一条指令;
HTTP面试题
1. get和post区别
get | post |
---|---|
get通过在URL添加参数提交数据给服务端 | post放在Hearder中提交数据 |
最大1024字节(URL长度限制) | 无限制 |
安全性差,因为参数都会显示在地址栏上 | 更安全 |
请求 | 获取 |
2. cookie和session区别
cookie | session |
---|---|
放在客户端浏览器 | 在一定时间内服务器中(当访问增加,会比较占用服务器性能) |
安全性差(可能有木马分析本地cookie并进行cookie欺骗) | 更安全 |
单个cookie保存的数据不能超过4k |
3. HTTP和HTTPs区别
4. HTTP超文本传输协议特点
- 无状态:对客户端没有状态储存。
- 没有长链接:就算统一用户短时间多次请求服务器,也需要多次TCP三次握手四次挥手。
- 明文传输不安全。
5. HTTP请求有哪些类?
6. HTTP1.1与HTTP1.0区别
HTTP1.1增加了:
- 长连接:一个tcp上多个http请求
- 流水线:可以不等待上一个请求的回复就继续发送下一个请求
- 增加host字段:http1.0假定了一个服务器一个IP地址,但是目前可能多个服务器共享一个IP地址,因此加上了主机名
- 节约带宽:http1.0不支持断点续传,http1.1允许只请求资源的某个部分
- 状态码增加
7. SSL四次握手
HTTPs进行了加密,主要是SSL操作。
- 客户端发送加密请求ClientHello,包括加密算法和加密长度。
- 服务端发送回复SeverHello,
- 客户端验证服务器证书,客户端回应服务端
- 服务端计算“回话秘钥”,通知客户端①随后的信息采用双方商定的加密方法和秘钥加密②握手结束通知
8. 304状态码
304表示自动上次请求以后,网页没变化。
计算机网络面试题
1. 网络分层?
我
应用层——传输层——网络层(路由器)——数据链路层(网桥、交换机)——物理层(网卡,网线,集线器,中继器,调制解调器)
2. TCP三次握手与四次挥手
3.TIME_WAIT状态
假设客户端主动申请关闭,当客户端收到了服务端的FIN报文时,进入TIME_WAIT状态需持续一段时间,为了确保最后一个报文能够送达,因为服务端在一段时间没能接收到ACK就会重新发送FIN的报文(TCP超时重传)。
3. 为什么关闭连接是四次握手而建立连接是三次握手?
因为服务端是LISTEN状态下,收到建立连接的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而在关闭时,当收到客户端的FIN报文时,仅仅表示客户端不再发数据了,但是服务端还可以接收和发送信息给客户端,故过一段时间,发送FIN报文给客户端,客户端回应ACK收到报文,至此完毕。
3. SYN泛洪攻击
攻击者发送大量的SYN报文但不完成三次握手,这样服务器有很多半开的链接,占用服务器资源(服务器不断发送SYN+ACK报文)。
解决 :SYN cookie,服务器在接收到客户端的SYN报文后,生成一个cookie(有关发送方接收方IP地址及port)发送给客户端,此时不产生任何半开链接,仅返回cookie,客户端收到cookie后,下次发送ACK就必须携带此cookie,服务端仅给cookie正确的建立全开的链接。
4.浏览器访问地址的流程
5. TCP .vs UDP
6. 为什么TCP三次握手?
书中解答:为了防止已失效的连接突然传到服务端。
假设两次,即服务端发送确认信息就建立连接,当客户端向服务端发送第一次请求连接包,这时这个包由于网络不好被滞留了很久,但server收到此失效的请求连接报文段就建立了连接,就一直等client给server发信息,这样server很多资源就被浪费了。
TCP三次握手的目的:通信双方相互确认序列号起始值
client:发送SYNC包,写好我的序列号x
server:ACK = x+1,我的序列号是y
client:ACK = y+1
自此双方都确认了通信序列号(表示自己发送的信息从xx号开始)
7. 为什么TCP可靠?
三次握手
上面讲了
超时重传
进行TCP连接时,存在确认应答和序列号
机制,为了解决由于网络延时原因出现的收不到报文的问题,引出超时重传机制
,发送方在发送完数据后等待一个时间,时间到达没有接收到ACK报文,那么对刚才发送的数据进行重新发送。
滑动窗口
肯定不能发一个等一个再发下一个,太耗时了。
因此采取滑动窗口,窗口是变化的。
只要按顺序收到了ACK分组,012345…
滑动窗口就向后滑动,滑到哪一个没来ACK或者乱序了。
TCP连接的两端都有可变的缓冲区。
ARQ协议实现了滑动窗口,存在:停等ARQ、连续ARQ(√)。
连续ARQ:回退N帧ARQ协议(目前)、选择性重传ARQ协议(TCP可改进的方向)
拥塞控制
包括三个算法:
①慢启动
TCP连接后,拥塞窗口大小为1,设定一个慢启动门限ssthresh。
每次收到ACK应答,拥塞窗口加 1;
因此拥塞窗口增长是:1,2,4,8,16…
只要达到慢启动门限ssthresh就采用拥塞避免算法。
②拥塞避免
此时是线性增长,每轮传输拥塞窗口增长1。
当出现超时现象了,此时慢启动门限ssthresh = 此时拥塞窗口/2
同时拥塞窗口被设定为1,并进入慢启动
③快速恢复
新技术。
可能只是部分的丢包,其实并没有出现网络堵塞,故此时将拥塞窗口设为1,再慢慢增长就会出现性能下降。
此时将慢启动门限ssthresh = 此时拥塞窗口/2,将拥塞窗口也设定为这个值,即直接开始拥塞避免的线性增长。
7. 为什么将三次冗余ACK作为阻塞的标志?
链接:https://www.zhihu.com/question/21789252
拥塞——丢包——3次冗余的ACK
因为2次冗余ACK很可能由乱序造成的,比如12345发成了12435。
但是只要是丢包了,一定出现三次ACK。
8. 数据链路层ARP协议——数据链路层
将IP地址解析为MAC地址
为什么有了IP地址还需要MAC地址?
IP地址不唯一,例如我们到了图书馆,连上图书馆Wi-Fi,此时图书馆IP管理中心会给我们分配一个动态IP地址,等我们离开图书馆断开Wi-Fi后,刚刚分给我们的IP地址会被回收,并会被分配给下一个连接图书馆Wi-Fi的同学
为什么有了Mac地址还需要IP地址?
这就需要路由器记住当前子网下的所有设备的MAC地址,而不同设备的MAC地址都不一样没规律的,每个路由器的存储空间不够!
而IP地址是与地域相关了,同一子网上设备前缀是一样的,就像邮政编码,路由器只需记住每个子网的位置即可。
ARP过程
由于封装成帧必须知道设备的MAC地址,故发送方在子网中广播“IP地址是XXXXXX的设备MAC地址是多少?”,局域网的每台主机都对比一下自己的IP,一致就回复一个响应报文“我的IP是XXXXX,MAC是xxxxxx”。
ARP攻击
由于Hacker也被广播到了消息,故也发送一条假消息,说自己的IP地址是这个,不停发送欺骗包,将正常ARP表的更新掉,所以原本发送给别人的信息,全都发给了Hacker。
也可能Hacker发送通知,自己是网关,发送到外网的信息就全给了它。
ARP防御
静态绑定IP和MAC
9. 网际控制报文协议ICMP——网络层
网络层。
查询报文+差错报文
由于IP数据报的传输可能会出错,因此ICMP为了弥补IP协议不可靠的特性而出现。
查询报文:比如我们用ping命令尝试ping通一个IP地址
差错报文:是在IP数据报不正常被接收端接收时产生的,比如ICMP不可达报文;亦或者发现了更好的路由路径,比如ICMP重定向报文。
10. 路由选择协议——网络层
目前采用的是分层的路由选择协议
AS-自治系统
自治系统内部选择:内部网关协议
自治系统外部选择:外部网关协议
为什么分层?
- 互联网的规模大,路由表太大难处理。
- 许多单位不愿向外界展示内部的网络布局。
内部网关协议——RIP(routing information protocol)路由信息协议
基于距离的路由选择协议,距离(跳数),每经过一个路由器就+1,一条路径大于16就称为不可达。
- 特点:①仅和相邻路由器交换信息
②交换自己的路由表
③按固定时间间隔交换路由信息 - 路由表记录了:目的地+距离+下一跳去哪
- 注意:RIP只与相邻路由器交换路由表,所以存在缺点:坏消息传得慢,比如:C能到B,B能到A,但是A突然断了,变为不可达,此时C路由表中是“自己能到A跳数2”,B路由表中已经无法到达A了,因此,C向B进行RIP报文更新时,让B认为C能到达A,更新路由表,二者不断更新跳数,直至跳数为16才确定了A不可达了。即“坏消息传的慢”
- 通过UDP传输
内部网关协议——OSPF(open shortest path first)开放最短路径优先协议
-
特点:①泛洪法,向本自治系统中,的所有路由器发送,与本路由器相邻的所有链路状态信息。
②只有当链路状态变化时,采用泛洪法发送
③更新收敛快,比RIP。 -
OSPF数据报比RIP短。
-
OSPF将自治系统划分为很多个区域,每个区域内都泛洪。
-
直接使用IP数据报传送,不用UDP
外部网关协议——BGP(Border Gateway Protocol)边界网关协议
不同自治系统之间交换路由信息。
- 由于互联网规模很大,BGP力求寻找一条能够到达的路由,而非最优路由。
- 每个自治系统选出一位发言人,通过他与其他设备的发言人交换路由信息。
- 通过TCP连接交换BGP报文。
操作系统面试题
1. 64位和32位区别?
操作系统是硬件和软件的中间平台,32位是针对32位CPU设计的。
2. 进程与线程的区别?
①一个程序至少有一个进程,一个进程有多个线程
②进程之间独立内存,而多个线程之间存在共享内存,因此CPU切换或者创建线程的代价小
③进程:资源分配的最小单位;线程:CPU调度的最小单位
④多线程程序的稳定性比多进程的差,因为进程有独立的地址空间。
2.1 进程之前切换的算法
先来先服务
优点:公平,不会饥饿
缺点:排在长作业后面的短作业体验差
短作业优先
优点:最小的平均等待时间
缺点:不公平,对短作业有利,对长作业不利,可能产生饥饿
时间片轮转
优点:公平,每个进程在一定的时间间隔内就会得到响应。不饥饿
缺点:各个进程执行一个时间片就切换,高频率切换就会产生一定的开销。
优先级调度
按照优先级调度
优点:可灵活调整对各种作业偏好程度
缺点:可能产生饥饿
多级反馈队列调度算法(先来先服务+短作业优先+时间片轮转+优先级调度混合法)
规则:
设置多级队列,1、2、3、4…K队列优先级依次降低,时间片依次增大。
新进程进入1队列,按照先来先服务获取时间片,如果此次没完成,就放进下层队列的尾端,只有在上层队列为空时,才能执行下层队列。
2.2 进程之间的通信方式有哪些?
-
匿名管道:用于具有亲缘关系的父子或兄弟进程之间的通信。存放在内存中
-
有名管道:克服匿名管道只能亲缘通信的特点,严格遵循FIFO,实现任意两进程的通信。存放在磁盘中
-
消息队列:消息的链表,与管道不同在于,消息队列存放在内核中,只有系统内核重启才会失效。
不一定FIFO,也可以按类型 -
信号:比如键盘输入ctrl+C就能终止进程
-
信号量:计数器,用于进程之间的同步
-
共享内存:多个进程访问同一块内存空间
-
套接字:客户端与服务端进行TCP、IP通信
2.3 线程之间的通信方式?
- 互斥量:只有拥有了互斥对象的线程才有访问公共资源的权限。Java 中的 synchronized 关键词和各种 Lock 都是这种机制
- 信号量:允许多个线程同时访问同一资源
- 事件:wait/notify,通过通知来保证线程同步,而wait/notify也是通过monitor对象操作的。
3. 页式存储
主存被分成大小相等的块,称为主存块(相当于酒店修建成几间房)。
用户程序被分为与内存块一样大小的页(相当于一人一间房)。
用户程序分配到主存,是一页对应一个块,称为页表。页放入块可以放入不相邻的块中。
页式存储为什么慢?怎么解决?
页式存储——页表放在内存中,CPU存取需要访问两次内存。
第一次:访内存中的页表,找到该页的的块号,将此块号与页内地址拼结形成物理地址;
第二次:真正访问该物理地址,存取其中的内容。
因此执行速度慢了。
采用高速缓冲寄存器存放快表。相对,内存中的称为慢表。
多级页表
目前计算机系统空间大,单级页表的太大了。
为什么要用页式存储?
总体上分为:连续分配、非连续分配
非连续分为分页、分段、端页式。
连续分配就必须将整个用户程序连续地放在一块内存上,这样导致内存碎片,内存利用率低
4. 段式存储
页式存储是先将内存分为块,再将程序分为和块一样大的页,再存放,记录于页表中。
段式存储是先将程序分为若干个段,按照逻辑意义,比如这个是Main函数。
存在段表
分页 | 分段 |
---|---|
内存空间利用率高,少量的内部碎片 | 按照逻辑模块实现共享和保护 |
不方便代码共享 | 存在内存碎片 |
5. 段页式存储
用户程序先分段再分页。
共享以上的两种优点。
三次访问:先段表,在页表,再访问物理地址。
6. 死锁四条件
①互斥条件:资源一次仅能被一个进程占用。
②请求和保持条件:等待新资源的同时,仍然占用已占用的资源
③不剥夺条件:已经被进程占用的资源不能被抢走
④循环等待条件:已经形成一种头尾连接的循环等待
解除死锁:银行家算法(通过一种类似于全局搜索的形式,找到一个资源使用的安全序列,按此序列占用资源就不会产生死锁)。
7. LRU算法
理想算法:每次调入的页面都是最迟被使用的页面,这种需要预测未来,显然无法实现。
引入LRU算法:
“如果最近这段时间此数据被访问过,后续访问的几率也很高。”
8. 常用的IO模型
IO操作包括两个阶段:①等待数据准备好②从内核缓冲区向进程缓冲区复制
阻塞IO
应用程序被阻塞,直到数据从内核缓冲区复制到应用缓冲区再返回。
非阻塞IO
进程发起IO系统调用后,不会被阻塞,应用程序可以继续执行,但不断执行系统调用来获取IO是否执行完成了。
IO复用
有一个线程轮询
因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个 socket,并且只有在真正有 socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占
用。
信号驱动IO模型
当进程发起一个IO操作,向内核注册一个信号处理函数,然后不阻塞,如果内核数据就绪了就发一个信号给进程,继续
异步IO模型
当进程发起一个IO操作,进程不阻塞,但也不能返回结果,内核把整个IO处理完后,会通知进程结果
40. IO多路复用好处?
背景:在单线程中,当我们进行IO操作时,可能会在read、write、accept进行阻塞,比如服务器read会一直等到客户端write才继续进行。如果一个服务器连接多个用户,被1号用户阻塞了就不能处理2号用户了。
因此需要IO多路复用:在单个线程中记录每一个IO流来管理IO流状态。
IO复用典型:
注意:select和poll速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区
select()
:底层维护一个文件描述符fd的数组(最大1024),从用户空间拷贝到内核空间,并轮询是否有某个fd事件触发,然后执行相关的读或写。O(n)
poll()
:与select类似,没有最大数量的限制,底层是链表。
epoll()
:已经注册进事件设置了回调函数,如果事件触发,就执行回调函数,将准备就绪的fd放到readyList中,因此不需变量整个fd数组,而是readyList,没有最大数量的限制,在高并发情况下epoll能支持更多的连接。
epoll两种触发模式
:
水平触发:当文件描述符有读写事件发生,epoll_wait()会通知处理程序去读写。如果此次没能读写完,下次epoll_wait()还会继续通知处理这个文件描述符
边沿触发: 与之前的不同,出现了一次读写事件仅会通知一次,没有第二次机会,除非出现了下一次读写事件。
问:为什么不用多线程?
答:线程的切换涉及到系统调用,也就是必须上下文切换,非常耗时与资源;而IO多路复用就不会这样,效率比较高,比如高并发的应用Nginx。
9. 消息通信机制: 同步 vs 异步
同步:调用一个功能,在没有返回之前,一直等待其返回
异步:调用一个功能,调用立即返回,但不能马上获取结果,调用者继续后续的操作,通过回调通知调用者
10. 等待调用结果时的状态:阻塞 vs 非阻塞
阻塞:调用一个函数,当调用结果返回之前,当前线程会被挂起,只有得到结果之后才会返回。
非阻塞:调用一个函数,不能立刻得到结果之前,调用不能阻塞当前线程。
注意:与上面的区别就是,这个描述的是自己目前的状态。
11. 随机IO与顺序IO
顺序IO就找到一个地址后,按顺序进行IO操作即可,不需多次寻址。
随机IO就是不同的地址,数据分散到不同的扇区,需多次重新寻址。
数据结构与算法
1. hashset有序吗?
无序的
底层是hashmap的key
2. Object做hashmap的key的要求
key不可重复,必须重写hashcode()和equals()。
为什么?
原始的hashcode(): 依赖于内存地址,每个hashcode都是唯一的。
原始的equals():判断两个内存地址是否相等。
看一下hashmap——put
的源码:
也就是说,通过hashcode的计算,让我们避免了一个一个去比较key是不是一样的,hashcode就能避免掉一大部分的冗余计算,那么分配到了一个bucket中了,再通过equals对比。
3. hashmap的扩容机制
两个变量:负载因子+扩容阈值
当数组长度*负载因子==扩容阈值时,数组长度变为2倍,将原来的放进新的数组中。
4. hashmap的大小为什么是2的几次方?
为了散列均匀
首先hashmap中为了计算速度快,不是真正的取模运算,而是运用与运算
来计算取模运算的
在计算放在那个bucket中时,计算公式是: hash(KEY) & (length - 1)
当length为偶数时,length-1是奇数,这样&运算后,结果最后一位是1或者0。
当length是奇数时,length-1是偶数,这样&运算后,最后一位一定是0,这样很容易冲突,因为浪费了一半的地方。
当length是不是2的几次方的偶数时,比如6,那么length-1 = 5 = 101了,因此与操作过后,产生的倒数第二位永远是0。
16. 为什么重写equals和hashcode须同时?
- 这两个方法数据Object类中,每个类均可重写此方法。
- 当这个类对象存储在HashTable,HashSet, HashMap等散列存储结构中,那么重写equals后一定也要重写hashCode,否则会导致存储数据的不唯一性(会存储两个equals相等的数据)。
- put时会先判断hashcode,再判断equals,都不存在才put进去
- hashcode不重写就直接返回地址值,equals不重写就返回对比地址值。
- 比如HashMap中, 如果key是自定义的类,就必须重写hashcode()与equals()。
- 记忆:hashcode就是看几号桶。
17.HashMap底层
- 数组+链表(JDK1.7及以前)。
- 元素存放的位置一般是hashcode&(2n-1),假设数组长度是2n长。为了散列均匀。
- JDK1.8时,一旦链表个数大于8就转化为红黑树(平均查找长度是logn),log8=3,链表平均查找长度是(n/2),8/2 = 4,因此转化应该!
- 一旦元素小于等于6,就转化为链表。
- 为什么有差值?为了防止频繁的红黑树与链表的转化。
- 初始容量16,负载因子为0.75
- 当put时需检查是否超过了(容量*0.75),如果超过就扩容为原来二倍
- 扩容后,JDK1.7需要重新rehash计算,JDK1.8进行了rehash优化,如下图:
比如在同一位置的两个key,扩容后就相当于多了1bit(2n-1=原来1111,现在11111),只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+旧容量”。
17. 为什么 HashMap中 String、Integer 这样的包装类适合作为 key键?
- final:保证了不可变性。
1. 修饰类:表明这个类不能被继承
2. 修饰方法:明确禁止 该方法在子类中被覆盖的情况下,早期的java版本会将final方法转化为内嵌调用,但后期的java取消了这样的因为对于庞大的方法性能提升不明显。
3. 修饰变量:如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。 - 内部也重写了
hashcode()、equals()
,计算存放的位置和比较同一位置是否内容相等。
17. HashMap在多线程情况下出现的问题?
死循环
主要产生于扩容的resize()
函数的transfer()
函数中。JDK7出现的问题,8已经解决但是仍然不推荐多线程下用hashmap。
以单线程为例
扩容在放置的过程如下,能看到是头插法。
先放next再头插e
多线程下
假设线程1刚开始就被阻塞了,线程二正常运行了
此时线程1复活了,注意先放next再头插e,线程一先放了7,在将3放进去,再3->7,。
此时已经出现了死循环,即3->7->3。
如果扩容前相邻的两个Entry在扩容后还是分配到相同的table位置上,就会出现死循环的BUG。
多线程put导致元素丢失
两个线程同时向同一个bucket放入的元素,出现了覆盖丢失。
解决办法
- ConcurrentHashMap:用分段锁的 hashMap 类
- Collections.synchronizedMap(Mao<K,V> map)工具方法,实就是对 map 中的方法进行加锁处理,保证多线程下操作安全!
JDK8的优化
JDK7会出现死循环和元素丢失问题,JDK8仅会出现元素丢失问题。
-
链表装填下采用尾插法。这样不会出现了死循环,长度超过8就会转化为红黑树,为什么是8,因为红黑树的平均搜索长度为
logn
,经计算log2(8) = 3 < (0+8)/2 = 4,也就是当链表长度为8时,链表的平均搜索长度时4,这时转化为红黑树就可以优化。长度减少为6就会退化为链表,因为数量少,链表就很方便,并且中间有个空位7,是为了如果频繁的插入删除,可能会频繁转化红黑树与链表,浪费性能。 -
扩容机制优化。
请回忆:为什么hashmap长度必须是2的几次幂的形式。
JDK7及以前都是扩容之后,依次计算每一个元素的新位置,再进行摆放。
JDK8发现,每次扩大都是2倍的扩大,计算i = (length- 1) & hash
,可以发现,这样的length仅会增加了一位1在最高位。因此如果那个最高位是0的,在原位不动,如果是1,此元素从当前位置X移动到X+length的位置。无需重新计算hashcode。 -
如果链表的长度大于等于8 ,那么链表将转化为红黑树;当长度小于等于6时,又会变成一个链表。
18.TreeMap底层
- 红黑树,自平衡排序二叉树。
- logn时间复杂度,插入删除遍历。
- 红黑树性质:
1. 每个节点红色或黑色
2. 根节点是黑色的
3. 每个叶节点是黑色null节点
4. 从根到叶子的每个路径上都不会出现两个连续红色节点,每条路的黑色节点数量相同。
19.ConcurrentHashMap底层
JDK1.7:segment和lock,而segment继承了RetrantLock,分段锁,每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。get无锁,put加锁。
- put:对key进行hash找到对应的segment,获取锁,判断是否已经存在(通过hashcode+equals)这个HashEntry了,如果原来没存过就放,存过就覆盖,释放锁。
- get:不加锁,其value通过volatile修饰(保证内存可见性)
- size计算:先不加锁,最多连续计算3次,如果连续两次不变就返回这个,如果有变化,就把所有的segment加锁,计算一次。
JDK1.8:上面的segment有点低效,改成,Node(也就是HashEntry)+CAS+synchronized,
- put:根据key计算hashcode找到对应Node,利用CAS尝试写入,失败了就利用synchronized锁写入数据,数量大于XX就转化为红黑树。
- size:使用一个volatile变量记录元素个数。
20. 基本框架接口
21. 一致性哈希算法
为了解决普通余数hash算法伸缩性差的问题。
场景:分布式缓存、分布式服务器
算法步骤:比如先构造一个长度232的整数环,将所有的服务器节点根据名字或者IP进行hash部署在这个环上,接下来再将客户端节点计算hash,按照此点顺时针到达的第一个服务器就是此节点的服务器。
原来的普通余数hash算法,每次加入一个服务器,就必须更改很多客户端的连接位置,而这个不用改变很多。
比如:服务器3宕机了,只有他的客户会改变位置,其余正在连接的不用动。
一致性hash会产生一个问题:哈希偏斜。
解决方式:虚拟节点
22. B+树和B(B-)树
B+树的中间节点不保存数据,因此磁盘页能存更多数据。
B+树必须查询到叶子节点,B树不一定,因此B+树更稳定。
对于范围查找,B+树直接遍历叶子节点组成的链表就可以,但是B树需要中序遍历。
23. TreeMap和TreeSet
TreeMap的key 和 TreeSet的对象,都必须实现Comparable接口,里面存在compareTo()方法。
24. Collections.sort()如何比较元素?
两种重载方式
①传入的待排序容器中存放的对象,实现Comparable接口重写compareTo方法
②传入两个参数,第一个是没实现比较器的原始数据,第二个是Comparator类型的外部比较器。
注意:Comparable接口属于内部比较器,直接写在待排序的类的内部;
Comparator接口属于外部比较器,单独写一个比较器类出来,能适配多种类。
25. 排序算法
26. 快速排序原理及步骤
第一轮:在一个数组中,把第一个元素当做基准点,首先j哨兵从数组尾部,向前搜寻第一个小于基准点的位置,然后i哨兵从数组头部向后搜寻第一个大于基准点的位置(注意i<=j),i与j两点交换数字,当i==j时,交换此数和基准点数字,第一轮结束。
第二轮:以此i==j的位置切割数组,分成前后两个,对于前后两个数组都进行上一轮操作。
27. 归并排序
分治法的典型应用。
将整个数组不断二分,直至每个子数组都是2个数,然后合并两个有序数组(两个指针,哪个小就排前面),不断合并就可以。
Mysql
1. 加锁机制
- 只有通过索引(主键索引、唯一索引、普通索引)检索数据的时候加的是行锁,否则上表锁
- 当然Mysql有自己的分析机制、当其认为全表扫描效率更高就不会用索引,也就是上表锁
- 通过explain可以查看执行计划,type=ALL就是使用的表锁
2. InnoDB行锁+间隙锁==》repeatable机制下能防止幻读
- 在可重复读的机制下,利用他来防止幻读。数据库防止幻读的措施是串行化性能差。
假如要更新v1=7的数据行,那么此时会在索引idx_v1对应的值,也就是v1的值上加间隙锁,锁定的区间是(5,7)和(7,9)。同时找到v1=7的数据行的主键索引和非唯一索引,对key加上锁。
因为事务2插入的v1值为6在事务 1的锁定区间(5,7)内。而事务1插入的v1值不在事务 2的锁定区间(5,7)内,故可以成功插入。不仅仅insert操作, update操作也一样会被锁住,从而锁等待超时。
3. 两阶段提交——分布式系统保证数据一致性的手段,CAP中的C
两阶段 = 准备阶段+提交阶段
背景:分布式系统出现数据不一致的问题。
准备阶段:判断+记录功能
- 协调者问所有参与者节点能否自行提交某事务,等待响应
- 参与者执行事务操作,并记录redolog
- 参与者响应询问,如上的操作成功,返回成功,失败返回失败
提交阶段:正式提交
- 当所有参与者都成功时,协调者向所有参与者发出“正式提交”请求
- 参与者正式完成操作,释放占用的资源
- 参与者返回ACK,已经完成
- 协调者收到所有反馈,完成整个事务
if 任意参与者在准备阶段“失败” || 协调者不能收到参与者的信息
return 协调者告诉所有参与者回滚
2PC的问题
- 单点故障:一旦协调者故障了,参与者就会一直阻塞等待
- 数据不一致:如果准备阶段都没事,但是在提交阶段,协调者向参与者发出的commit有一部分由于网络问题没到,出现了数据不一致现象。
3PC解决2PC单点故障问题
将协调者的commit分为preCommit
和commit
。
一旦参与者收到了preCommit,会默认执行commit(无论收不收到commit),而不会一直持有事务资源并处于阻塞状态。
但是仍然解决不了数据不一致的问题。比如preCommit都没收到。
4. 为什么mysql的offset在大数据量下有性能问题?怎么优化?
SELECT * FROM myTable ORDER BY `id` LIMIT 1000000, 30
LIMIT 1000000, 30 的意思是
:扫描满足条件的1000030行,扔掉前面的1000000行,然后返回最后的30行。
由于是多次通过主键索引访问数据块的I/O操作。
5. 2PL:两阶段锁
48. Mybatis如何将对象转化为sql?
- 使用Mybatis:我们需要写好mapper.xml文件(namespace中写好接口位置)+UserMapper.java接口。
- Mybatis会把每个SQL语句封装成SqlSource对象。
- 再将SqlSource对象+ID(类的全限定名+方法名)封装成MappedStatement对象。
- 将接口
@Mapper
注解:在运行时,通过动态代理生成实现类 - 步骤:标记注解以后,就注册到spring bean中,beanClass是MapperFactoryBean;
一旦程序中运用了@AutoWired,就会触发这个工厂bean的getObject()方法,此方法用JDK动态代理生成,注入的是生成的代理对象;
调用了代理对象的方法就相当于调用了对应的invoke方法。
问题:如果有两个XML文件和这个Dao建立关系,岂不是冲突了?
答:直接报错,namespace+id的组合需要唯一。
rabbitMQ
1. 防止消息队列丢消息
发布确认机制 + 消息队列持久化
2. 幂等性问题
消费者在返回ack的时候,消息队列由于网络问题,没接到,出现的重复执行。
解决方法1:搞一个消息表,利用主键不能重复的原理,消费一个插一个,成功了消费,不成功不执行。
解决方法2:不写入数据库,使用redis,setnx天然幂等性。
3. 为啥不使用redis的list直接实现消息队列?
每条消息仅可被一位用户处理一次。消息队列可被用于分离重量级处理、缓冲或批处理工作以及缓解高峰期工作负载。
-
list是一种线性的有序结构,按照先进先出的原则,
LPUSH
和RPOP
。而当生产者向队列中LPUSH
一个消息后不会主动通知消费者消费,需要不断轮询,占有cpu资源。 -
当然,消费者能够采取
BRPOP
命令,进行阻塞读取,消费者在在读取队列没有数据的时候自动阻塞,直到有新的消息写入队列,才会继续读取新消息执行业务逻辑。 -
redis本身基于内存的,如何实现消息可靠传输呢?
采取BRPOPLPUSH
命令,即启动一个备份的list进行记录。
redis
38. redis主从复制原理
①与master建立连接
②slave发起同步请求
③master发来RDB
④载入RDB
39. redis为什么是单线程?
来自于redis官方回答:
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。因此采用单线程。
redis是单线程 + IO多路复用的。
41. redis AOF和RDB
优 | 缺 | |
---|---|---|
RDB | 保存了redis在某时间点上数据快照,非常紧凑,适用于灾难恢复 | redis每隔一段时间,会丢失一部分数据 |
AOF | 默认每秒sync一次,记录所有写入命令,数据一致;如果出现了未写入完整的命令也可以利用redis-check-aof工具修复 | 文件体积大于RDB |
AOF:Redis可以在AOF文件体积变得过大时,自动地在后台对AOF进行rewrite
复原时先AOF后RDB
41. redis数据结构:压缩列表和跳跃表
redis五大基本类型:
string(字符串),hash(哈希),list(列表),set(集合)及sortset(zset有序集合,跳表)
压缩列表用于:
- 列表:要求:少量列表&&每个列表项是小整数&&每个列表项是长度较短的字符串
- 哈希:要求:少量键值对&&每个键值对的键和值是小整数值&&每个键值对的键和值是长度比较短的字符串
压缩列表原理:
压缩列表好处:
- 利用编码技术除了存储内容尽可能的减少不必要的内存开销
- 减少内存碎片的作用,我把这种行为叫做"合并存储",也就是将很多小的数据块存储在一个比较大的内存区域
Spring
37. Spring注解:@Autowired
和@Resource
@Autowired:Spring提供的注解,只能byType注入,如果想byName需结合@Qualifier注解
@Resource:Java提供的注解,可以byName和byType注入
创新型问题
1. 40亿个qq号怎么去重?
使用 bitmap
数据结构
比如一个unsigned char类型,共8位。
如果这7位都是1,就能表示这7个数都存在。
如果是这样,就代表1到7都存在,0这个数不存在。
一个unsigned int类型数据可以标识0~31这32个整数的存在与否。
两个unsigned int类型数据可以标识0~63这64个整数的存在与否。
因为int数据类型为4字节32bit,因此就像32个插槽一样,想象为hashmap,如果此位是1,代表这位的这个数存在。
2. private修饰的方法可以通过反射访问,那么private的意义是什么?
1.java的private修饰符并不是为了绝对安全性设计的,更多是对用户常规使用java的一种约束;
2.从外部对对象进行常规调用时,能够看到清晰的类结构。
3. Java类初始化顺序
基类静态代码块、静态成员字段(并列优先级,按照代码中出现的先后顺序执行,且只有第一次加载时执行)
——>子类静态代码块、静态成员字段(并列优先级,按照代码中出现的先后顺序执行,且只有第一次加载时执行)
——>基类普通代码块,基类普通成员字段(并列优点级,按代码中出现先后顺序执行)
——>基类构造函数
——>子类普通代码块、普通成员字段(并列优点级,按代码中出现先后顺序执行)
——>子类构造函数
4. 对方法区和永久区的理解以及它们之间的关系
方法区是jvm规范里要求的,永久区是Hotspot虚拟机对方法区的具体实现,前者是规范,后者是实现方式。
5. 一个java文件有3个类,编译后有几个class文件
3
6.局部变量使用前需要显式地赋值,否则编译通过不了,为什么这么设计?
是一种约束,尽最大程度减少使用者犯错的可能(假使局部变量可以使用默认值,可能总会无意间忘记赋值,进而导致不可预期的情况出现)
7. ReadWriteLock读写之间互斥吗
读读共享,读写互斥,写写互斥
8. Semaphore拿到执行权的线程之间是否互斥
不互斥,有执行权的线程如并发访问同一对象,会产生线程安全问题。
9. JDK5引入的并发辅助类 Semaphore、CountDownLatch与 CyclicBarrier之间的区别
CountDownLatch:计数器,只能减法,比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行。
CyclicBarrier:一组线程等待至某个状态之后再全部同时执行。
Semaphore:Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
10. 写一个你认为最好的单例模式
DCL:
public class Singleton{
public Singleton(){}
private volatile static Singleton instance;
public static Singleton getSingleton(){
if(instance==null){
synchronized(Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
11. B树和B+树是解决什么样的问题的,怎样演化过来?
二叉树会产生退化现象,引出二叉平衡树(AVL)
二叉树的高度太高了,因此产生了m叉树和m叉平衡树
B树就是m叉平衡树。
B树的每个节点都存着key和value,查询速度不稳定,并且每个节点放不了几个key(内存限制),因此,B+树出现了,每个节点只放key值,将value值放在叶子结点,在叶子结点的value值增加指向相邻节点指针。
12. 写一个生产者消费者模式
synchronized锁住一个LinkedList,一个生产者,只要队列不满,生产后往里放,一个消费者只要队列不空,向外取,两者通过wait()和notify()进行协调。
引入队列的概念:
- 持久化
- 消费确认机制
- 广播
- 业务解耦:发布做发布,消费做消费的
- 顺序处理
- 不能重复消费
13. 写一个死锁
public class DeadLock{
public static void main(String[] args){
final List<Integer> list1 = Arrays.asList(1,2,3);
final List<Integer> list2 = Arrays.asList(1,2,3);
new Thread(new Runnable(){
@Override
public void run(){
synchronized(list1){
Thread.sleep(1000);
synchronized(list2){
}
}
}
}).start();
new Thread(new Runnable(){
@Override
public void run(){
synchronized(list2){
Thread.sleep(1000);
synchronized(list1){
}
}
}
}).start();
}
}
14.cpu 100%怎样定位
方法一:top命令,先top-c查看最费cpu的进程,在找到线程的PID(类似于名字),转换线程PID至16进制
方法二:简单的Arthus工具,
①命令dashboard
,然后已经能看到了CPU高利用率的线程了,记住ID。
②输入thread ID
,就能看到是什么方法占用了CPU100%
③输入jad 全限定名
,直接能看到全部代码。
15. String a = "ab"; String b = "a" + "b";
a == b 是否相等?
相等,方法区的字符串常量池。
编译器有优化操作,自动采用StringBuilder和append()进行加法运算。
16. int a = 1;
是原子性操作吗
是
17. 可以用for循环
直接删除ArrayList的特定元素吗?可能会出现什么问题?
两种可能:
①数字for:
由于ArrayList的remove方法,是先remove此位置的元素,再将后面的元素前移一位,因此会跳过一些元素,不行。
②泛型for:
会抛出ConcurrentModificationException
异常(并发修改异常),迭代器遍历集合元素的时候,集合是不能修改元素的。
此异常是 快速失败(fail-fast) 机制产生的(安全失败是copyonwriteArrayList,修改时先在一个复制的List上修改,再改指向)
18. 怎样解决新的任务提交到线程池,线程池是怎样处理?
第一步 :线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则执行第二步。
第二步 :线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里进行等待。如果工作队列满了,则执行第三步。
第三步 :线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
饱和策略:
- 抛出异常,并拒绝
- 直接拒绝不抛异常
- 丢弃队列最前面的任务,然后重新提交被拒绝的任务
- 由调用线程(提交任务的线程)处理该任务
19. AQS和CAS原理
AQS抽象队列同步器。是Java并发包的核心。具体请看上方的解释。
CAS比较并替换,内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false,整个比较并替换的操作是一个原子操作。
20. synchronized底层实现原理
①同步代码块
涉及monitorenter、monitorexit两条指令,将整个代码块包裹起来,进入此代码块先获取对象的monitor对象,最后再释放
②同步方法:
对此方法加上了ACC_SYNCHRONIZED标识(通过反编译能看到方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志),当方法被调用时,如果设置了此标识,则在前后加上monitorenter、monitorexit指令。
21. volatile作用
①可见性
②禁止指令重排序——内存屏障
具体原理之前讲过了,记得复习。
22. AOP和IOC原理
AOP是面向切面编程,实际是通过代理模式实现(其中的动态代理),在程序运行过程中将增强代码织入原代码中。
IOC是控制翻转,将对象的控制权交给Spring框架,用户需要使用对象无需创建,直接使用即可。
23. Spring怎样解决循环依赖的问题
循环依赖:①构造器循环依赖(spring解决不了)②属性注入循环依赖(spring可以解决)。
如何检测循环依赖:
检测一个图中是否出现了环。利用一个 HashSet
依次记录这个依赖关系方向中出现的元素, 当出现重复元素时就说明产生了环, 而且这个重复元素就是环的起点。
spring如何解决循环依赖的?
三级缓存
24.dispatchServlet怎样分发任务的?
springMVC内容。
25.mysql给离散度低的字段建立索引会出现什么问题,具体说下原因
离散度低的字段不应该作为索引,比如说性别。
首先InnoDB引擎,两个文件,frm文件——mysql语句,ibd文件——数据和索引文件,也就是索引不能一直创建,要创建就创建好一点,访问速度快一点。
索引=聚集索引(仅1个,没有IO开销)+普通索引(可多个,有IO开销)
IO开销是,聚集索引的叶子节点保存着value,而普通索引叶子节点上仅保存着key,再回表查询一次得到value。
当然,可以进行联合索引处理,注意一般将时间戳等区分度大的放在性别这种的左边。