面试----知识点
一、Java-基础
1、Java的8种基本数据类型
- 字符类型:byte、char;
- 基本整型:short、int、long;
- 浮点型:float、double;
- 布尔型:boolean;
2、Java中 == 和 equels的区别
- ==:基本类型比较的是值是否相同;
- equels:引用类型比较的是引用是否相同;
String x = "string";
String y = "string";
String z = new String("string");
System.out.println(x==y); // true
System.out.println(x==z); // false
System.out.println(x.equals(y)); // true
System.out.println(x.equals(z)); // true
3、普通类和抽象类有哪些区别
- 普通类不能包含抽象方法,抽象类可以有抽象方法也可不有抽象方法;
- 抽象类不能被实例化,普通类可以直接实例化;
- 抽象类不能用final修饰,抽象类就是让其他类继承的,如果定义为final就不能被继承了,矛盾;
4、接口和抽象类的区别
- 抽象类使用extend继承,接口使用implements实现接口;
- 抽象类可以有构造函数,接口不能有构造函数;
- 类可以实现很多个接口,但是只能继承一个抽象类;
- 接口默认修饰符是public,抽象类可使用任何的修饰符;
二、Java-集合
1、ArrayList
- ArrayList 是 java 集合框架中比较常用的数据结构,继承自 AbstractList,实现了 List 接口。底层基于数组实现容量大小动态变化。允许 null 的存在。同时还实现了 RandomAccess、Cloneable、Serializable 接口,所以ArrayList 是支持快速访问、复制、序列化的。
2、LinkedList
- LinkedList是java一种常用的数据容器,与ArrayList相比,LinkedList的增删操作效率更高,而查改操作效率较低。
- LinkedList实现了List接口 ,所以LinkedList是具备List的存储特征的(有序、元素有重复);
- LinkedList 实现了Deque 接口,即能将LinkedList当作双端队列使用;
- LinkedList 实现了Cloneable、Serializable 接口,所以LinkedList 是支持复制、序列化的。
3、Vector
- Vector用数组存储元素,容量能够动态的增长,它的很多实现方法都加入了同步语句,因此是线程相对安全的。
4、HashSet
- HashSet继承于AbstractSet,并且实现了Set接口。
- 它是由HashMap实现的,不保证元素的顺序,而且HashSet允许使用 null 元素
5、LinkedHashSet
- LinkedHashSet是Set集合的一个实现,具有set集合不重复的特点,同时具有可预测的迭代顺序,也就是我们插入的顺序。但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
- LinkedHashSet插入性能略低于 HashSet,但在迭代访问Set里的全部元素时有很好的性能。
6、TreeSet
- 向Treeset中添加的数据,要求是相同类的对象。
- 两种排序方式:自然排序和定制排序。
- 自然排序中,比较两个对象是否相同的标准为: compareTo()返回e.不再是equals()。
7、HashMap
- 链接: link
7.1、HashMap1.7和1.8的区别
1.7 | 1.8 | |
---|---|---|
数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
节点 | Entry<K,V> | Node<K,V> |
hash | hash是可变的,因为有rehash的操作 | hash是final修饰,也就是说hash值一旦确定,就不会再重新计算hash值了 |
hash算法 | 先判断这Object是否是String,如果是,则不采用String复写的hashcode方法,处于一个Hash碰撞安全问题的考虑 | (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16) |
对Null处理 | 单独处理 putForNullKey(value); | null为键和其他非null是一样的,也有hash值,也能别替换。只是计算结果为0而已 |
初始化 | 赋值空数组,put时初始化 | 懒加载,没有赋值,数组为null, put操作的时候进行resize()操作,进行初始化 |
节点插入 | 头插 | 尾插 |
7.1、HashMap1.7链表头插法死循环问题
例链表:A – B
// 扩容转换方法 将数据放到一个新的Entry数组中
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
// 获取entry在原链表(未扩容前)的next
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
// 将当前entrydenetx指向新数组对应index的值,即头插法(新来的在最上面)
e.next = newTable[i];
// 将新数组对应位置占据
newTable[i] = e;
// 进入下一个节点的循环
e = next;
}
}
}
- 线程1执行此方法,遍历table,此时的Entry<K,V> e为A,然后e.next为B,线程1挂起;
- 线程2执行此方法,e为A,然后e.next为B,遍历的顺序为A然后B,先将A放到新的数组中,然后在轮到B,因为是头插,此时B.next = A,B的next指向了A;
- 线程1继续执行遍历B,此时的B.next = A了,e = next; 此时的e就为A,然后进入下一次循环,然后A继续进行头插入,此时A.next = B,A的next指向了B;
- 此时为A.next = B,B.next = A,在get的时候,死循环;
7.2、1.8中HashMap红黑树与链表之前的转换条件
- 链表转红黑树, 主要方法 treeifyBin
// 调用转换红黑树方法的条件 TREEIFY_THRESHOLD = 8
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
------------------------------------------------------
// 转为红黑树的方法
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
1). 数组为null或者数组的长度 < 64,会走resize()扩容的方法,扩容的方式避免红黑树结构化,否则会走replacementTreeNode转为红黑树的方法(tab长度足够,hash冲突过多,转为红黑树)。
- 红黑树转链表,主要方法 untreeify
1). map调用remove方法删除元素的时候,根据红黑树根节点和子叶节点是否为空判断转换为链表;
if (root == null || (movable && (root.right == null || (rl = root.left) == null || rl.left == null))) {
tab[index] = first.untreeify(map); // too small
return;
}
2). resize()的时候,对红黑树进行拆分,红黑树的长度小于等于6的时候进行转换为链表;
// UNTREEIFY_THRESHOLD = 6
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
7.3、1.8中HashMap的容量为什么是2的N次幂
容量最终都会转为2的n次幂,以下对应的方法
// 都会转为最近的一个2的n次幂的数
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
1). (n - 1) & hash 计算的是tab数组索引的位置,n为2的次幂,n - 1得到的十进制数字对应二进制永远是末尾以连续1的形式表示,(n - 1) & hash 等价于 hash % n;
- &位运算比取模运算快;
- 保证索引的值一定在2的次幂范围内;
- 在扩容时不需要重新进行hash,只需要在&位运算去高位的值,为0时新索引值 = 原索引值,为1时新的索引值 = 索引值 + 原长度(扩容机制);
8、LinkedHashMap
-
大多数情况下,只要不涉及线程安全问题,Map基本都可以使用HashMap,不过HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序。HashMap的这一缺点往往会带来困扰,因为有些场景,我们期待一个有序的Map。
-
这个时候,LinkedHashMap就闪亮登场了,它虽然增加了时间和空间上的开销,但是通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序。该迭代顺序可以是插入顺序或者是访问顺序。
9、HashTable
- HashTable是较为远古的使用Hash算法的容器结构了,现在基本已被淘汰,单线程转为使用HashMap,多线程使用ConcurrentHashMap。
- HashTable<K,V>也是一种key-value结构,它继承自Dictionary<K,V>,实现了Map<K,V>和Cloneable以及Serializable接口。
- HashTable的操作几乎和HashMap一致,主要的区别在于HashTable为了实现多线程安全,在几乎所有的方法上都加上了synchronized锁,而加锁的结果就是HashTable操作的效率十分低下。
10、TreeMap
-
与HashMap相比,TreeMap是一个能比较元素大小的Map集合,会对传入的key进行了大小排序。其中,可以使用元素的自然顺序,也可以使用集合中自定义的比较器来进行排序;
-
不同于HashMap的哈希映射,TreeMap底层实现了树形结构,至于具体形态,你可以简单的理解为一颗倒过来的树—根在上–叶在下。如果用计算机术语来说的话,TreeMap实现了红黑树的结构,形成了一颗二叉树。
11、ConcurrentHashMap
- 链接: link
三、Java-线程
1、并行和并发的区别
- 并行:多人多个事件同一时刻发生(一个人看电视,一个人洗衣服);
- 并发:多人同时抢着干一件事;
2、线程和进程的区别
- 进程:是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
- 线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
3、创建线程的几种方式
- extend继承Thread类重写run方法;
- implements实现runnable接口重写run方法;
- implements实现callable接口重写call方法;
4、runnable 和 callable 有什么区别
- runnable 没有返回值,callable 可以拿到有返回值,callable 可以看作是 runnable 的补充。
5、线程有哪些状态
- 新建状态(NEW):在程序中用构造方法创建一个新线程时,如new Thread(),该线程就是创建状态,此时它已经有了相应的内存空间和其它资源,但是还没有开始执行。
- 就绪状态(READ):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
- 运行状态(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
- 阻塞状态(BLOCKED):一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入输出操作时(调用sleep()、wait()方法),将让出CPU并暂时中止自己的执行,进入堵塞状态。
- 死亡状态(TERMINATED):线程调用stop(), destory()或run()执行结束后,线程即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
6、sleep() 和 wait() 有什么区别
- 类不同:sleep() 来自 Thread,wait() 来自 Object。
- 释放锁:sleep() 不释放锁;wait() 释放锁。
- 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。
7、notify()和 notifyAll()有什么区别
- notifyAll()会唤醒所有的线程,notify()之后唤醒一个线程。notifyAll() 调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而 notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
8、线程的 run() 和 start() 有什么区别
- start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
9、创建线程池有哪几种方式
/**
* 一个线程池,一个线程,一个任务
*/
private static ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
/**
* 固定线程数的线程池,执行长期任务
*/
private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
/**
* 缓存线程池,一个线程池N个线程,执行短期任务
*/
private static ExecutorService cacheThreadPool = Executors.newCachedThreadPool();
/**
* 周期性线程池
*/
private static ExecutorService scheduleThreadPool = Executors.newScheduledThreadPool(5);
10、线程池创建的几个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
}
- corePoolSize:核心线程数,正常工作的线程数量;
- maximumPoolSize:容纳同时执行的最大线程数量;
- keepAliveTime:空闲线程存活时间;
- unit:空闲线程存活时间单位;
- workQueue:存放任务的阻塞队列;
- threadFactory:线程工厂;
- handler:拒绝策略,有四种实现:
1.AbortPolicy:拒绝任务抛弃处理,并且抛出异常;
2.CallerRunsPolicy:拒绝任务直接抛弃;
3.DiscardOldestPolicy:重试添加当前的任务,直到成功;
4.DiscardPolicy:抛弃队列里面等待最久的一个线程,将当前任务加入队列;
四、Java-锁
1、volatile关键字的作用
- java提供的 轻量级同步机制;
- 保证可见性;
- 不保证原子性(java.util.concurrent.atomic包下的类可保证原子性);
- 禁止指令重排。
下面代码:reader方法依赖flag的值,我们希望程序是顺序执行的正常单线程程序没问题,但是在多线程环境下,writer方法可能先执行flag=true,这样reader方法获取的值就不确定了,volatile的一个特点就是禁止指令重排,固定必须先执行a=1然后flag=true
public class OrderSortDemo {
private int a = 0;
private boolean flag = false;
/**
* 写方法
*/
public void writer(){
a = 1;
flag = true;
}
/**
* 读方法,执行依赖flag的值
*/
public void reader(){
if(flag){
int i = a + 1;
}
}
}
双重检索DCL单例实现使用volatile代码
:
public class SingleInstance {
private static volatile SingleInstance instance = null;
private SingleInstance() {
}
public static SingleInstance getInstance() {
if (instance == null) {
synchronized (SingleInstance.class) {
if (instance == null) {
instance = new SingleInstance();
}
}
}
return instance;
}
}
2、公平锁
- 加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得;
>>示例如下:
Lock lock = new ReentrantLock(true);
3、非公平锁
- 直接获取锁,获取不到然后用公平锁的方式有序获取锁,非公平锁闭公平锁吞吐量大;ReentrantLock默认非公平锁、synchronized也是一种非公平锁;
4、非公平可重入锁(递归锁)
- 同步方法调用另一个同步方法或者同步代码块调用另一个同步代码块,同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁;
>>示例代码:
public class ReEnterLockDemo {
private static Object obj = new Object();
private static Lock lock = new ReentrantLock();
public static void syncMethod() {
new Thread(() -> {
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "111");
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "222");
}
}
}, "threadA").start();
}
public static void lockMethod() {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "111");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "222");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}, "threadB").start();
}
public static void main(String[] args) {
syncMethod();
lockMethod();
}
}
5、自旋锁
- 线程不会立即阻塞,而是挂起采用循环的方式去尝试获取锁,好处是减少上下文切换,缺点是循环消耗CPU;
>>示例代码:
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}
6、独占锁(写锁)
- ReentrantReadWriteLock.WriteLock一次只能被一个线程所持有,ReentrantLock和synchronized都是独占锁;
7、共享锁(读锁)
- ReentrantReadWriteLock.ReadLock可被多个线程同时持有;
8、synchronized有什么作用
- 确保线程互斥的访问同步代码;
- 保证共享变量的修改能够及时可见;
- 有效解决重排序问题。
9、synchronized同步方法的原理
- 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
10、synchronized同步代码块的原理
- 当执行monitorenter时,如果目标对象的计数器为0,那么说明它没有被任何线程所持有,java虚拟机会将该锁对象的持有线程设置为当前线程,并将计数器+1;在目标对象的计数器不为0时,如果锁对象的持有线程是当前线程,那么java虚拟机可以将其计数器+1,否则需要等待,直至持有线程释放该锁;当执行monitorexit时,java虚拟机将其计数器-1,计数器为0表示该锁已被释放。
11、ReentrantLock和synchronized有什么区别
- synchronized是java的关键字,ReentrantLock是API层面的;
- synchronized底层是通过minitor对象完成,wait和notify方法也依赖monitor对象所在的同步方法或者同步代码块,自动释放,不可中断,默认为非公平锁;
- ReentrantLock为显示锁必须使用lock加锁和unlock释放锁,可以中断,默认也是非公平锁,可以设置为公平锁,通过
ReentrantLock(boolean fair)
设置; - synchronized的await和notify相当于ReentrantLock的Condition(条件)的await和signal,ReentrantLock可精确唤醒某个线程;
12、什么是CAS
- 方法名为compareAndSet(比较并赋值),主要就是自旋锁与Unsafe类,内存值V,期望值A,待修改值B,如果V与A的值相同,将内存值V更新为B,否则一直比较下去,直到成功(do…while…)
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}
13、CAS有什么缺点
- 循环时间过长,开销大;
- 只能保证一个共享变量的原子操作,也就是compareAndSet方法this参数;
- ABA问题(
解决:new AtomicReference<V>(V initialValue):参数为一个初始值、new AtomicStampedReference<V>(V initialRef, int initialStamp):参数为一个初始值和一个时间戳版本号(类似于数据库的乐观锁)
);
14、LockSupport的作用
- 线程等待唤醒机制,两个方法阻塞线程park()和解除阻塞线程unpark(),有一个permit许可证,只能有0和1两个值,park为0,unpark为1,有凭证消耗掉凭证然后正常退出,没有凭证则阻塞,凭证只能为一个累加无效,park和unpark成对出现;
15、阻塞线程和解除阻塞线程的方式
- Object的wait和notify方法;
- LockSupport的park和unpark;
- Lock的Condition的await和singnal;
16、什么是AQS
-
AbstractQueuedSynchronizer(抽象队列同步器):是构建锁和其他同步器组件的整个JUC体系的基石,主要由一个变量state状态值和变种的CLH双向队列(头节点Node head和尾节点Node tail)组成;
-
获取锁时通过CAS去更新state状态值操作,state初始值为0,说明没有被占用,当前线程可以获取锁,如果不为0说明被占用会尝试获取锁,仍获取不到会添加到等待队列,如队列不存在时初始一个队列(Node的thread值为null,waitStatus为0的哨兵节点作为头节点),如队列存在会在其后增加当前线程的一个Node节点,然后队列里的线程仍会自旋去尝试获取锁,如果仍获取不到,将当前线程的前节点的waitStatus改为-1,并通过LockSupport的park阻塞到等待队列,如果state的值等于0为空闲说明能够获取到锁然后出队,将该线程对应的Node节点设置为哨兵节点,以前的哨兵的节点将被回收掉;
五、JVM(java虚拟机)
1、jvm内存模型
2、jvm - 类加载子系统
-
1、加载:将.class二进制字节流文件从磁盘读取到内存中(通过文件的全限定名);
-
1). 启动类加载器(Bootstrap ClassLoader):JRE的核心类库,例如rt.jar包中java和sun包下的类,都将使用此类加载器进行加载;
-
2). 扩展类加载器(Extension ClassLoader):负责加载JRE扩展目录ext中的jar包;
-
3). 系统类加载(Application ClassLoader):负责加载ClassPath路径下的类包,就是平时我们自己开发时编写的类文件;
-
4). 自定义类加载器:因为系统的ClassLoader只会加载指定目录下的class文件,如果你想加载自己的class文件,那么就可以自定义一个ClassLoader,而且我们可以根据自己的需求,对class文件进行加密和解密,
4.1:新建一个类继承自java.lang.ClassLoader,重写它的findClass方法;4.2:将class字节码数组转换为Class类的实例;4.3:调用loadClass方法即可
; -
2、 链接
-
1). 验证:验证字节码文件的正确性,主要包括四种验证;
文件格式验证,源数据验证,字节码验证,符号引用验证;
-
2). 准备:给类的静态变量分配内存,并赋予默认值;
-
3). 解析:将常量池内的符号引用转换为直接引用的过程。
-
3、初始化:为静态变量赋予正确的初始值,此阶段才是程序员编写的程序变量赋予真正的初始值,执行静态代码块。
3、双亲委派机制
- 双亲委派机制:避免类的重复加载,类加载器收到加载请求时,不会立即加载而是先将加载请求委托给上一级的类加载器,直到启动类加载器,能加载启动类加载器直接加载,不能加载到扩展类加载器,扩展类加载器能加载直接加载,以此向下类推;
4、jvm - 运行时数据区
- 堆:线程之间共享区域,主要用来存放类的对象实例信息,堆分为老年代(Old Space)和年轻代(Young Space),老年代主要存放应用程序中生命周期长的存活对象或者Young Space存放不下的大对象,年轻代又分为Eden、S0和S1,Eden主要存放新生的对象;S0和S1是两个大小相同的内存区域,存放每次垃圾回收后Eden存活的对象,作为对象从Eden过渡到Old Space的缓冲地带;
- 方法区:线程之间共享区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据;
- 虚拟机栈:线程私有,描述的是 Java 方法执行的内存模型,每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程;
- 本地方法栈:区别于 Java 虚拟机栈的是,本地方法栈服务于Native本地方法;
- 程序计数器:线程私有,节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成;
5、jvm - 执行引擎
- 解释器;
- 即时编译器;
- 垃圾回收器;
6、GC垃圾回收 - 算法
- 引用计数:用于年轻代,给每个对象一个计数器,计数减到0的时候,回收,无法处理循环引用问题;
- 复制清除:用于年轻代,把空间分成两块,每次只对其中一块进行 GC,当这块内存使用完时,就将还存活的对象复制到另一块上面,例:Eden–>S0、S0–>S1采用了复制的算法,不适合用于存活对象多的情况,因为那样需要复制的对象很多;
- 标记清除:用于老年代,遍历所有的GC Roots,并将从GC Roots可达的对象标记设置为存活对象,然后遍历堆中的所有对象,将没有被标记可达的对象清除,大量的内存遍历工作,所以执行性能较低;
- 标记整理:用于老年代,在进行完标记清除之后,对内存空间进行压缩整理,节省内存空间,解决了标记清除算法内存不连续的问题,效率不高,移动对象需要耗费更多时间;
7、GC垃圾回收 - 垃圾回收器
- 串行垃圾回收器(Serial):单线程环境只使用一个线程进行垃圾回收,会暂停所有的用户线程,所以不适用服务器环境;
- 并行垃圾回收器(Parallel):多个垃圾回收线程并行工作,此时的用户线程也是暂停的,适用于科学计算/大数据处理;
- 并发垃圾回收器(CMS):用户线程和垃圾回收线程同时执行,低停顿用户线程,优点:并发收集低停顿;缺点:并发执行,对CPU资源压力大;采用标记清除会导致大量碎片;因为垃圾碎片占用内存,可以配置-XX:CMSFullGCsBeForeCompaction多少次垃圾回收之后进行Full GC(默认0,每次垃圾回收之后都进行内存整理);
- G1垃圾回收器(G1):将堆内存分割成不同的区域然后并发的对其进行垃圾回收,特点:与CMS一样与应用程序并发执行;整理空间更快,不会产生很多内存碎片;更短的停顿时间,停顿时间添加了预测机制,用户可以指定期望的停顿时间,region区域化垃圾回收器,避免全内存扫描,只需要按照区域进行扫描;
8、GC垃圾回收 - 可达性分析法
- 通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。finalize()方法最终判定对象是否存活。
- 在Java语言中,可作为GC Roots的对象包含以下几种:
1). 虚拟机栈(栈帧中的本地变量表)中引用的对象;
2). 方法区中静态属性引用的对象;
3). 方法区中常量引用的对象;
4). 本地方法栈中(Native方法)引用的对象;
9、JVM调优常用参数
- -Xms:初始内存大小,一般为物理内存的1/64;
- Xmx:最大分配内存,一般为物理内存的1/4;
- -Xss:设置单个线程栈的大小,一般默认;
- -XX:MetaspceSize :设置元空间大小;
- -XX:+PrintGCDetails:开启打印GC和FullGC垃圾回收信息;
- -XX:SuivivorRatio:新生代与Suivivor比例,默认值为8,则Eden:S0:S1=8:1:1;
- -XX:NewRatio:年轻代与老年的比例,默认值为2,年轻代站1/3,老年代占2/3;
- -XX:MaxTenuringThreshold :设置垃圾回收的年龄,也就是从年轻到经过多少次GC,才到老年代;
10、几种常见的OOM
- StackOverflowError:栈内存溢出,递归方法的调用可出现此错误;
- OutOfMemoryError:java heap space:堆内存溢出,new了很多的对象或者new了大的对象可出现此错误;
- OutOfMemoryError:GC overhead limit exceeded:GC时间太长引发的异常超过98%的时间用来做GC并且回收不到2%的堆内存,不停的往常量池里添加可出现此错误;
- OutOfMemoryError:Direct buffer memory:直接内存溢出,对象没有分配到JVM的堆内存,而是分配到了本地内存中,NIO的buffer操作分配到堆外内存;
代码示例
:
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
- OutOfMemoryError:Metaspace:元空间内存溢出;
11、JMM(java内存模型)的特点
- 可见性:一个线程更新内存的值,立即通知其他线程,更新后的值对其他线程可见;
- 原子性:所有操作要么全部成功,要么全部失败,这些操作是不可拆分,操作过程中不可被中断;
- 有序性:对于单线程代码执行时,我们认为代码是从上到下执行的,但是在多线程环境下,程序的执行可能是乱序的;
六、Spring
1、IOC(控制反转)
- IOC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。
2、AOP(面向切面编程)
- AOP (Aspect Orient Programming),直译过来就是面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。
3、SpringBean的生命周期
4、Spring的循环依赖
说明链接:link.
5、Spring的事务隔离级别
说明链接: link.
6、Spring的事务的失效场景
说明链接: link.
7、Spring的事务的回滚
七、SpringBoot
1、SpringBoot自动装配原理
- 主要是启动类上@SpringBootApplication注解
- @SpringBootConfiguration:代表是一个配置类。
- @ComponentScan:功能其实就是自动扫描并加载符合条件的组件或bean定义,最终将这些bean定义加载到容器中。
- @EnableAutoConfiguration:其原理是借助@Import的帮助,将所有符合自动配置条件的bean定义加载到Ioc容器;@Import({AutoConfigurationImportSelector.class}) 扫描META-INF/spring.factories下所有的jar包。
2、自己写个SpringBoot的stater
- 1、创建SpringBoot项目,添加maven依赖,官方建议 artifactId 命名应遵循{name}-spring-boot-starter 的格式。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
- 2、编写服务类
public class SayService {
private String fromName;
private String toName;
public SayService(String fromName, String toName) {
this.fromName = fromName;
this.toName = toName;
}
public String sayLove() {
return fromName + "love" + toName;
}
}
- 3、编写配置类,读取配置文件的配置信息。
@ConfigurationProperties(prefix = "say.service") // 读yml文件里的该路径
public class SayServiceProperties {
private String prefix;
private String suffix;
}
- 4、编写一个自动装配类
@Configuration // 标注为配置类
@ConditionalOnClass(SayService.class) // 注入SayService服务类才生效
@EnableConfigurationProperties(SayServiceProperties.class) // 开启配置
public class SayAutoConfiguration {
@Autowired
private SayServiceProperties properties; // 注入配置文件
@Bean
@ConditionalOnMissingBean() // 该注解的意思是如果没有发现service的bean就执行新建一个bean
public SayService sayService() {
return new SayService(properties.getPrefix(), properties.getSuffix());
}
}
- 5、在项目的resources下新建META-INFO/spring.factories,声明自己配置的类。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.exmple.config.SayAutoConfiguration
七、Redis
1、Redis的数据结构
- string:Redis最基本的数据类型,一个键对应一个值,一个键值最大存储512MB。
- set:是String字符串类型的无序集合,也不可重复。
- zset:是String类型的有序集合,也不可重复。有序集合中的每个元素都需要指定一个分数,根据分数对元素进行升序排序。
- hash:hash是一个键值对的集合,是一个String类型的field和value的映射表,适合用于存储对象。
- list:是redis的简单的字符串列表,按插入顺序排序。
2、Redis的应用场景
- 会话缓存(最常用);
- 消息队列(支付);
- 活动排行榜或计数;
- 发布,订阅消息(消息通知);
- 商品列表,评论列表。
3、Redis的持久化机制(RDB和AOF)
- 1、RDB:是指用数据集快照的方式半持久化模式,记录 redis 数据库的所有键值对,,在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。
-
- 优点:
1、只有一个文件 dump.rdb,方便持久化;
2、容灾性好,一个文件可以保存到安全的磁盘;
3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是IO最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis的高性能) ;
4、相对于数据集大时,比 AOF 的启动效率更高。
- 优点:
-
- 缺点:
数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候。
- 缺点:
- 2、AOF:是指所有的命令行记录以 redis 命令请求协议的格式完全持久化存储保存为 aof 文件。
-
- 优点:
1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。
2、通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof工具解决数据一致性问题。
3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall)。
- 优点:
-
- 缺点:
1、AOF 文件比 RDB 文件大,且恢复速度慢。
2、数据集大的时候,比 rdb 启动效率低。。
- 缺点:
4、redis 过期键的删除策略
- 1、定时删除:在设置键的过期时间的同时,创建一个定时器 timer). 让定时器在键的过期时间来临时,立即执行对键的删除操作。
- 2、惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
- 3、定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
5、Redis 的回收策略(淘汰策略)
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
- allkeys-lru:从数据集中挑选最近最少使用的数据淘汰。
- allkeys-random:从数据集中任意选择数据淘汰。
- no-enviction:禁止驱逐数据。
6、Redis 的同步机制
- Redis 可以使用主从同步,从从同步。第一次同步时,主节点做一次 bgsave,并同时将后续修改操作记录到内存 buffer,待完成后将 rdb 文件全量同步到复制节点,复制节点接受完成后将 rdb 镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。
7、Redis延时双删
作用:保证redis和数据数据同步的一种方法;
- 1、删除redis数据;
- 2、更新数据库;
- 3、延时500毫秒(保证数据库更新操作的完成);
- 4、删除redis数据(再有请求会去请求数据库,将数据存储到redis中,保证数据一致性);
8、Redis的雪崩
- 如果缓在某一个时刻出现大规模的key失效,那么就会导致大量的请求打在了数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。这时候如果运维马上又重启数据库,马上又会有新的流量把数据库打死。这就是缓存雪崩。
- 解决:过期时间设置分散一些,不要都是一样的。
9、Redis的击穿
- 缓存击穿跟缓存雪崩有点类似,缓存雪崩是大规模的key失效,而缓存击穿是某个热点的key失效,大并发集中对其进行请求,就会造成大量请求读缓存没读到数据,从而导致高并发访问数据库,引起数据库压力剧增。这种现象就叫做缓存击穿。
- 解决:一些热点key不设置过期时间。
10、Redis的穿透
- 缓存穿透是指用户请求的数据在缓存中不存在即没有命中,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍。如果有恶意攻击者不断请求系统中不存在的数据,会导致短时间大量请求落在数据库上,造成数据库压力过大,甚至导致数据库承受不住而宕机崩溃。
- 解决:1、设置null的key,短时间缓存到redis;2、布隆过滤器;
11、布隆过滤器
链接: link.
12、Redission分布式锁 - 死锁
- 死锁:一个线程持有锁,然后服务挂掉了,导致锁一直没有释放,其他线程无法获取锁,就会造成死锁。
13、Redission分布式锁特性
- 1、互斥性;在任意时刻,只能有一个客户端持有锁。
- 2、分布式锁要避免死锁。
- 3、加锁和解锁必须是同一个客户端,谁加的锁就谁去解锁。
- 4、具有容错性,只要大多数的redis节点正常运行,就可以正常的加锁和解锁。
- 5、不能只是加锁和解锁,需要实现一些其他功能,比如:锁判断,锁超时,锁的可重入等。
14、Redission分布式锁分类
- 可重入锁。
RLock lock = redisson.getLock("myTestLock");
// 最常见的使用方法 需unlock方法手动解锁
lock.lock();
...
lock.unlock();
// 加锁以后10秒钟自动解锁 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
- 公平锁:所有请求线程会在一个队列中排队,它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。
RLock fairLock = redisson.getFairLock("myTestLock");
// 最常见的使用方法 需unlock方法手动解锁
fairLock.lock();
...
fairLock.unlock();
// 10秒钟以后自动解锁 无需调用unlock方法手动解锁
fairLock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
- 联锁:基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。联锁是所有节点的锁加锁成功才算成功。
RLock lock1 = redissonInstance1.getLock(lockKey);
RLock lock2 = redissonInstance2.getLock(lockKey);
RLock lock3 = redissonInstance3.getLock(lockKey);
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3 所有的锁都上锁成功才算成功。需unlock方法手动解锁
lock.lock();
...
lock.unlock();
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);
// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
- 红锁:基于Redis的Redisson红锁RedissonRedLock对象可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。区别在与联锁是所有节点的锁加锁成功才算成功,但红锁是大部分节点加锁成功即为成功。
RLock lock1 = redissonInstance1.getLock(lockKey);
RLock lock2 = redissonInstance2.getLock(lockKey);
RLock lock3 = redissonInstance3.getLock(lockKey);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3 大多数的锁(N/2 + 1)都上锁成功才算成功。需unlock方法手动解锁
lock.lock();
...
lock.unlock();
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);
// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
- 读写锁:基于Redis的Redisson分布式可重入读写锁RReadWriteLock允许同时有多个读锁和一个写锁处于加锁状态。
- 信号量:在上面的分布式可重入锁中,只有自己持有的锁才可以解锁,也就是说其他线程是没有办法解不属于他们的锁的,但是如果有业务需要的话可以使用基于Redis的Redisson的分布式信号量(Semaphore)来实现。
- 可过期性信号量:基于Redis的Redisson可过期性信号量(PermitExpirableSemaphore)是在RSemaphore对象的基础上,为每个信号增加了一个过期时间。
- 闭锁:基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch。
15、Redission分布式锁 - 续锁(看门狗机制)
- 如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间。这样也存在一个问题,加入一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题,Redisson给出了自己的答案,就是 watch dog 自动延期机制。
Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。
默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。
七、消息中间件MQ
1、MQ的优点
- 异步处理:相比于传统的串行、并行方式,提高了系统吞吐量。
- 应用解耦:系统间通过消息通信,不用关心其他系统的处理。
- 流量削锋:可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求。
- 日志处理:解决大量日志传输。
- 消息通讯:消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。
2、MQ的缺点
- 系统可用性降低:本来系统运行好好的,现在你非要加入个消息队列进去,那消息队列挂了,你的系统业务受影响。因此,系统可用性会降低;
- 系统复杂度提高:加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。
- 一致性问题:A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,数据不一致;
3、MQ常见的问题
- 消息的顺序问题(消息有序指的是可以按照消息的发送顺序来消费)。
1)保证生产者–MQServer–消费者 为一一对应的关系; - 消息的重复问题(消息的幂等)。
1)利用一张日志表记录已经处理成功的消息的ID,如果存在就跳过处理这条消息;
2)消息体中存业务唯一编号,业务中判断处理;
4、RabbitMQ的基本概念
链接: link
- Message:消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
- Publisher:消息的生产者,也是一个向交换器发布消息的客户端应用程序。
- Exchange:交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
- Binding:绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
- Queue:消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
- Connection:网络连接,比如一个TCP连接。
- Channel:信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
- Consumer:消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
- Virtual Host:虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
- Broker:表示消息队列服务器实体。
5、RabbitMQ的Exchange类型
- Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键,此外 headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接看另外三种类型。
1)direct:消息中的路由键(routing key)如果和 Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中。它是完全匹配、单播的模式。
2)fanout:每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。
3)topic:topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“”。#匹配0个或多个单词,匹配不多不少一个单词。
八、MySQL
1、MySQL的事务隔离级别
- 读未提交(READ UNCOMMITTED)。
- 读提交 (READ COMMITTED)。
- 可重复读 (REPEATABLE READ)。
- 串行化 (SERIALIZABLE)。
事务隔离其实就是为了解决上面提到的脏读、不可重复读、幻读这几个问题,下面展示了 4 种隔离级别对这三个问题的解决程度: