知识储备

java#

- 常见Java集合类#

非线程安全

ArrayList: 底层数组
LinkedList: 底层双向链表
HashMap: (重要必考,后面单独问) 数组加链表,链表长度 > 8转化为红黑树

- ArrayList 和 LinkedList的区别#

  1. 数据结构 数组 / 双向链表
  2. 是否支持快速随机访问
  3. 随机插入删除速度, ArrayList需要整体位移

- ArrayBlockingQueue和LinkedBlockingQueue的实现原理#

ArrayBlockingQueue是object数组实现的线程安全的有界阻塞队列
1.数组实现:使用数组实现循环队列
2.有界:内部为数组实现,一旦创建完成,数组的长度不能再改变
3.线程安全:使用了 ReentrantLock 和 两个 condition(notEmpty, notFull) 来保证线程安全
4.阻塞队列:先进先出。当取出元素的时候,若队列为空,wait直到队列非空(notEmpty);当存储元素的时候,若队列满,wait直到队列有空闲(notFull)

LinkedBlockingQueue是一个单向链表实现的阻塞队列
1.链表实现: head是链表的表头, last是链表的表尾。取出数据时,都是从表头head处取出,出队; 新增数据时,都是从表尾last处插入,入队。
2.可有界可无界:可以在创建时指定容量大小,防止队列过度膨胀。如果未指定队列容量,默认容量大小为Integer.MAX_VALUE
3.线程安全:使用了 两个 ReentrantLock 和 两个 condition,putLock是插入锁,takeLock是取出锁;notEmpty是“非空条件”,notFull是“未满条件”。通过它们对链表进行并发控制
4.阻塞队列:先进先出。当取出元素的时候,若队列为空,wait直到队列非空(notEmpty);当存储元素的时候,若队列满,wait直到队列有空闲(notFull)

注:
PriorityBlockingQueue(具有优先级的无限阻塞队列). 是一个支持优先级的无界阻塞队列,内部结构是数组实现的二叉堆

SynchronousQueue (SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素),
每一个线程的入队操作必须等待另一个线程相应的出队(take)操作,相反,每一个线程的出队操作必须等待另一个线程相应的入队操作

生产者往满的队列里添加元素时会阻塞主生产者,当消息者消费了一个队列中的元素后,会通知生产者当前队列可用

白话: ArrayBlockingQueue和LinkedBlockingQueue都是线程安全的阻塞队列。 ArrayBlockingQueue底层是数组实现,是有界的队列,线程安全是使用一个 ReentrantLock 和 两个 condition实现的。 LinkedBlockingQueue底层是单向链表实现,是可有界可无界的队列,线程安全是使用两个 ReentrantLock 和 两个 condition实现的。

- 阻塞队列原理#

核心思想就是:空就不让你取,满就不让你加。
进行操作的方法,在操作前,都必须加锁。
主要就是一个 ReentrantLock,和两个由它创建的 Condition:notFull、notEmpty。
然后,在take、put、enqueue、dequeue 四个方法里面对这两个信号条件进行控制。

- 项目中用到过哪些数据结构#

queue, map, set, list, 堆(priorityQueue)

- BIO、NIO、AIO 的区別和联系#

https://mp.weixin.qq.com/s/QrE-PLCppwzfX3JtvWx5QA

BIO:同步、阻塞。服务器实现模式为一个连接一个线程,服务器端为每一个客户端的连接请求都需要启动一个线程进行处理

NIO:多路复用技术,同步非阻塞。服务器实现模式为客户端的连接请求都会注册到多路复用器上,用同一个线程接收所有连接请求

AIO:异步非阻塞IO

BIO里用户最关心“我要读”,NIO里用户最关心”我可以读了”,在AIO模型里用户更需要关注的是“读完了”

- 谈谈对Java NIO的了解#

NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经成为解决高并发与大量连接、I/O处理问题的有效方式。

传统的BIO模型严重依赖于线程。但线程是很”贵”的资源。创建和销毁成本很高,本身占用较大内存,切换成本是很高。

NIO由原来占用线程的阻塞读写变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。大大地节约了线程,为处理海量连接提供了可能

NIO的主要事件有几个:读就绪、写就绪、有新连接到来。

首先需要注册当这几个事件到来对应的处理器。然后在合适的时机告诉事件选择器
;其次,用一个死循环选择就绪的事件,会执行系统调用(Linux 2.6之前是select、poll(轮询),2.6之后是epoll(通知))

总结NIO带来了什么:

  • 事件驱动模型
  • 避免多线程
  • 单线程处理多任务
  • 非阻塞I/O,I/O读写不再阻塞,而是返回0
  • 基于block的传输,通常比基于流的传输更高效(buffer)
  • 更高级的IO函数,zero-copy
  • IO多路复用大大提高了Java网络应用的可伸缩性和实用性

个人总结:
1.定义,NIO是同步非阻塞式IO, 也叫NEW IO
2.原理,采用事件驱动模型,相比于BIO, 一连接一线程,对线程资源有很大的浪费,NIO使用单线程去做事件的轮询,轮询到事件后再交给对应的事件处理器,通常有读写连接事件,
3.优点:节省线程资源,单线程处理多任务, 提高系统并发能力和吞吐能力。
JavaNIO使用channel、buffer传输,更高效

- 谈谈对hashMap的了解#

基本:底层数据结构, 数组 + 链表,长度超过8变为红黑树,非线程安全
加分:HashMap在put时, 经过了两次hash,一个是JDK自带的对对象key的hash,一个是内部扰动函数hash,做高16位与低16位异或,加大散列的效果
加分:HashMap 的长度为什么是2的幂次方。1. 散列值用之前要对数组长度取模,求桶位(n - 1) & hash 更高效;2. 扩容的时候也可以用 hash值与扩容后的最高位 & 判断节点是在高位还是低位,更高效。
加分:HashMap 非线程安全,多线程操作导致死循环问题,1.7 并发下头插形成循环链表,1.8 用尾插修复, 但仍可能造成丢失数据。

白话:hashmap是非线程安全的集合。底层是数组+链表,链表长度超过8会转成红黑树。hashmap中table的长度总是2的幂次方,这样可以使用&运算代替取余运算(%)来提高效率,在求桶位和扩容时获取节点新位置在左边还是右边的时候。

- loadFactor和threshold的关系?#

size: 当前共有多少个KV对
capacity: 当前HashMap的容量是多少, 桶位数, table大小, 默认是16
loadFactor: 负载因子,默认0.75
threshold: loadFactor* capacity,

白话:负载因子就是控制碰撞几率的,调的小,碰撞的概率小,调的大,碰的概率就大。阈值就是扩容时机。阈值等于负载因子乘以table大小

java并发#

- Java并发容器有哪些#

concurrentHashmap: 线程安全的HashMap, Node数组+链表 + 红黑树
copyonwriteArrayList: 线程安全的List,在读多写少的场合性能非常好
concurrentLinkedQueue: 高效的并发队列,使用链表实现。
可以看做一个线程安全的 LinkedList
BlockingQueue(接口)
1 ArrayBlockingQueue: 有界队列实现类 底层Object数组
2 LinkedBlockingQueue: 单向链表
3 PriorityBlockingQueue: 支持优先级的无界阻塞队列

- 谈谈对ConcurrentHashMap的了解#

1.7 结构: 分段的数组+链表;
并发:分段锁,锁一段数组 segment,默认有16个segment, 并发度只有默认是16
线程安全: segment上锁,继承ReentrantLock可重入锁

1.8 结构: Node数组+链表 + 红黑树;
并发: 并发控制使用 synchronized 和 CAS
线程安全: CAS和synchronized来保证并发安全, synchronized只锁定当前链表或红黑二叉树的首节点, 只要hash不冲突,就不会产生并发,效率又提升N倍

白话:线程安全的map。1.7版本是使用分段锁实现的线程安全,segment上加锁,在构建的时候可以设置大小,默认是16,就是并发度默认只有16. 1.8版本的线程安全是用synchronized+cas实现的,使用synchronized锁住链表或红黑树的头结点,使用cas来进行size++和首个节点入桶。1.8的并发度是随着桶位增加而增加的,所以并发效率会随扩容提升很多倍。

- 谈谈对Java内存模型的了解#

jmm是为了解决线程间通信问题,线程间通信通常有两种解决方法,共享内存或通知机制, jmm使用了共享内存的方式,jmm定义了一套happens-before规则来规范多线程下的执行顺序和多线程下变量的可见性问题,happens-before规则底层是通过禁止部分编译器和处理器的指令重排序实现的,happens-before有八个,分别是

程序顺序规则:一个线程中的每个操作,发生在该线程中任意后续操作之前
监视器锁规则:对一个锁的解锁,发生在随后对这个锁的加锁之前
volatile变量规则:对一个volatile域的写,发生在任意后续对这个volatile域的读之前
传递性:如果A发生在B之前,B发生在C之前,那么A一定发生在C之前
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

定义:JMM是什么,Java内存模型是要解决什么问题:
JMM通过happens-before规则简单易懂让Java程序员理解JMM提供的内存可见性保证。

Java内存模型是在讲线程间通信机制。 线程间通信有两种:共享内存和消息传递.
Java采用的是共享内存的模型实现的线程之间通信, 隐式进行, 对程序员透明。
JMM通过控制主内存与每个线程的本地内存之间的交互,决定一个线程对共享变量的写入何时对另一个线程可见。解决多线程之间可见性问题。
(本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区、寄存器以及其他的硬件和编译器优化)

怎么做,怎么实现共享内存模型的通信:
对于Java程序员来说,happens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法。
一个happens-before规则对应于一个或多个编译器和处理器重排序规则.

  1. happens-before规则
  2. 禁止指令重排序(根据happens-before规则)
    • 编译器重排序,JMM会禁止特定类型的编译器重排序。
    • 处理器重排序,JMM的处理重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。为程序员提供一致的内存可见性保证。

- volatile原理#

  1. Volatile的特征
    A、禁止指令重排(有例外)
    B、可见性
  2. Volatile的内存语义
    当写一个volatile变量时,JMM会把线程对应的本地内存中的共享变量值刷新到主内存。
    当读一个volatile变量时,JMM会把线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量
  3. Volatile的重排序
    两个volatile变量操作不能够进行重排序;

白话:volatile有两个特性,可见性和禁止指令重排序。可见性,当一个线程更新volatile变量后,这个变量会立即写入到主内存中,其他线程读取时会从主内存中读取最新的值。禁止指令重排序,两个对volatile变量的操作不能被重排序,底层是通过内存栅栏实现的。

- java的乐观锁CAS锁原理#

CAS英文全称Compare and Swap,直白翻译过来即比较并交换,是一种无锁算法,在不使用锁即没有线程阻塞下实现多线程之间的变量同步,基于处理器的读-改-写原子指令来操作数据,可以保证数据在并发操作下的一致性。

CAS包含三个操作数:内存位置V,预期值A,写入的新值B。在执行数据操作时,当且仅当V的值等于A时,CAS才会通过原子操作方式用新值B来更新V的值(无论操作是否成功都会返回)。

CAS的含义是:我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少

CAS是怎么获取到预期值的? 通过unsafe类获取到的

1
2
3
4
5
6
7
8
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}

CAS修改的值为什么要是volatile? 可见性

在compareAndSet的操作中,JNI借助CPU指令完成的,属于原子操作,保证多个线程在执行过程中看到同一个变量的修改值
unsafe:操作内存内存空间
valueOffset:value在内存中的偏移量(把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移),这里我们可以简单认为是内存地址,在类加载的时候,通过unsafe获取到value的偏移量。
value: 顾名思义代表存储值,被volatile修饰,保证在线程间的可见性

CAS问题:
1.ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
2.循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
3.只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

白话:CAS是Compare and Swap,比较并交换,是一种乐观锁实现线程安全的方式,更加轻量。底层是通过cpu的原子指令实现的比较并替换。使用的时候参数有内存位置,预期值,写入的新值,当通过内存位置拿到的值和预期值相等时,就用新值进行替换,整个操作是原子的,cpu指令保证。

- sychronized 和 valotile 的区别#

  1. volatile更轻量,性能更好,但volatile只能用于变量而synchronized关键字可以修饰方法以及代码块
  2. 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
  3. volatile关键字能保证数据的可见性,但不能保证数据的原子性 (eg: i++).synchronized关键字两者都能保证
  4. volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性

- sychronized reentranlock 的区别#

  1. synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
  2. 等待可中断 lock.lockInterruptibly(), 线程可以选择放弃等待,改为处理其他事情
  3. 可实现公平锁
  4. 可实现选择性通知(锁可以绑定多个条件)

- 弱引用#

强引用

任何被强引用指向的对象都不能被垃圾回收器回收。

软引用

如果有软引用指向这些对象,则只有在内存空间不足时才回收这些对象(回收发生在OutOfMemoryError之前)。

弱引用

如果一个对象只有弱引用指向它,垃圾回收器会立即回收该对象,这是一种急切回收方式。

虚引用

虚引等同于没有引用,拥有虚引用的对象可以在任何时候被垃圾回收器回收。

弱引用的出现就是为了垃圾回收服务的。它引用一个对象,但是并不阻止该对象被回收。
如果使用一个强引用的话,只要该引用存在,那么被引用的对象是不能被回收的。
弱引用则没有这个问题。在垃圾回收器运行的时候,如果一个对象的所有引用都是弱引用的话,该对象会被回收

- ThreadLocal原理#

用多线程多份数据,来避免线程不安全

每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key是 ThreadLocal 实例本身,value 是真正需要存储的 Object。也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。

由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。

ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key的,弱引用的对象在 GC 时会被回收。
Entry 继承 WeekReference<ThreadLocal<?>>,也就是说,一个Entry对象是由ThreadLocal对象和一个Object(ThreadLocal关联的对象)组成。

为什么选择弱引用?
为了应对非常大和长时间的用途,哈希表使用弱引用的 key
由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal key不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除

内存泄漏问题
在ThreadLocalMap中,只有key是弱引用,value仍然是一个强引用。当某一条线程中的ThreadLocal使用完毕,没有强引用指向它的时候,这个key指向的对象就会被垃圾收集器回收,从而这个key就变成了null;然而,此时value和value指向的对象之间仍然是强引用关系,只要这种关系不解除,value指向的对象永远不会被垃圾收集器回收,从而导致内存泄漏!

解决办法:
ThreadLocal提供了这个问题的解决方案,
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
但是这些被动的预防措施并不能保证不会内存泄漏:
使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

ThreadLocal最佳实践
综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

- AQS同步器原理?tryAcquire的过程?#

**AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作。AQS通过CAS完成对state值的修改

核心思想是,如果被请求的共享资源空闲,将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,将暂时获取不到锁的线程加入到队列中, 需要一定的阻塞等待唤醒机制机制来保证锁分配。这个机制主要用的是CLH队列实现的**

AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

通过简单的几行代码就能实现同步功能,这就是AQS的强大之处。

自定义同步器实现的相关方法也只是为了通过修改State字段来实现多线程的独占模式或者共享模式。自定义同步器需要实现以下方法(ReentrantLock需要实现的方法如下,并不是全部):

ReentrantLock这类自定义同步器自己实现了获取锁和释放锁的方式,而其余的等待队列的处理、线程中断等功能,异常与性能处理,还有并发优化等细节工作,都是由AQS统一提供,这也是AQS的强大所在。对同步器这类应用层来说,AQS屏蔽了底层的,同步器只需要设计自己的加锁和解锁逻辑即可

一般来说,自定义同步器要么是独占方式,要么是共享方式,它们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。ReentrantLock是独占锁,所以实现了tryAcquire-tryRelease。

独占与共享最大不同就在各自的tryacquire里,对于独占来说只有true或false,只有一个线程得以执行任务;而对于共享锁的tryAcquireShared来说,线程数没达到限制都可以直接执行。
但本质上都是对AQS同步状态的修改,一个是0与1之间,另一个允许更多而已

应用:
1.ReentrantLock
使用AQS保存锁重复持有的次数。当一个线程获取锁时,ReentrantLock记录当前获得锁的线程标识,用于检测是否重复获取,以及错误线程试图解锁操作时异常情况的处理。
2.Semaphore
使用AQS同步状态来保存信号量的当前计数。tryRelease会增加计数,acquireShared会减少计数。
3.CountDownLatch
使用AQS同步状态来表示计数。计数为0时,所有的Acquire操作(CountDownLatch的await方法)才可以通过

白话:AQS是jdk提供的一个同步器,可以很方便的生成自定义同步器。AQS内部使用一个volatile的state来表示同步状态,通过一个FIFO队列来做多线程获取资源的排队操作,AQS通过CAS来做state变量的修改。实现AQS只要实现其中判断获取锁和释放锁的方法即可,AQS内部会去做队列入队出队等复杂逻辑处理。使用AQS实现的同步器有ReentrantLock,Semaphore,CountDownLatch。

- CountDownLatch、Semaphore、CyclicBarrier含义及实现原理#

CountDownLatch
一个或多个线程等待其他线程完成一些列操作
CountDownLatch是一个同步辅助类,当CountDownLatch类中的计数器减少为0之前所有调用await方法的线程都会被阻塞,如果计数器减少为0,则所有线程被唤醒继续运行。

典型应用场景

开始执行前等待n个线程完成各自任务:例如有一个任务想要往下执行,但必须要等到其他任务执行完毕后才可以继续往下执行。假如这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。

CyclicBarrier
多个线程相互等待,直到到达同一个同步点,再继续一起执行。CyclicBarrier适用于多个线程有固定的多步需要执行,线程间互相等待,当都执行完了,在一起执行下一步。

CyclicBarrier是全部线程一起去执行下一步,CountDownLatch是只有当时等待的线程去执行下一步.

CyclicBarrier和CountDownLatch的异同

1.CyclicBarrier和CountDownLatch都可以通过一个条件去控制多个线程,然后在条件满足时做一些操作。CyclicBarrier在条件满足时可以让所有线程都继续运行,而CountDownLatch只能让一开始就在等待的线程去运行。

2.CyclicBarrier可以复用,开启屏障之后count会回归到初始值,可以进行下一次屏障拦截线程。而CountDownLatch只能使用一次,倒计时完毕后count的值不会变化。

java异步#

- Callable、Future、FutureTask、CompletableFuture分别是什么#

Callable是一个接口,提供一个回调方法,可以放到executorService中

Future接口提供了三种功能:
1)判断任务是否完成;
2)能够中断任务;
3)能够获取任务执行结果。

FutureTask
可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口,那就可以得出FutureTask即可以作为一个Runnable线程执行,又可以作为Future得到Callable返回值。
FutureTask是Future的唯一实现类

listenableFuture
ListenableFuture和JDK原生Future最大的区别是前者做到了一个可以监听结果的Future
void addListener(Runnable listener, Executor executor);

CompletableFuture
在JDK8中开始引入的,这个在一定程度上与ListenableFuture非常类似。比如说ListenableFuture的listener监听回调,在这个类中,相当于thenRun或者whneComplete操作原语

https://blog.csdn.net/Androidlushangderen/article/details/80372711

- java异步编程,获取线程返回结果的方法#

Thread的join()方法实现

CountDownLatch实现

ExecutorService.submit方法实现
future.get()

FutureTask
futureTask.get()

listenableFuture
ListenableFuture和JDK原生Future最大的区别是前者做到了一个可以监听结果的Future
void addListener(Runnable listener, Executor executor);

CompletableFuture
CompletableFuture.supplyAsync可以用来异步执行一个带返回值的任务,调用completableFuture.get()
会阻塞当前线程,直到任务执行完毕,get方法才会返回。

线程池#

- 线程池的创建使用、有哪些参数#

为什么要用线程池?
1. 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗 (重复利用)
2. 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行 (提前开始任务)
3. 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控 (控制线程数量)

如何创建线程池
1. Executors - 不允许
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM
2. ThreadPoolExcutor
参数: 核心线程数, 最大线程数,非核心线程空闲存活时间,阻塞队列,拒绝策略,有必要的话还有ThreadFactory线程工厂.
好处:
- corePoolSize, maximumPoolSize 弹性控制线程数量,可伸缩,可扩容可释放
- keepAliveTime, TimeUnit.SECONDS 设置后在回收前可让其他任务使用,减少重新创建线程的开销
- BlockingQueue, 设置队列大小,起到缓冲的作用
- rejectHandler

- 线程池的原理#

  1. 先讲构造参数
    corePoolSize: 线程池核心线程数最大值
    maximumPoolSize: 线程池最大线程数大小
    keepAliveTime: 线程池中非核心线程空 闲的存活时间大小
    unit: 线程空闲存活时间单位
    workQueue: 存放任务的阻塞队列
    threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
    handler: 线城池的饱和策略事件,主要有四种类型。

  2. 再描述提交任务后的执行过程

    • 提交一个任务,线程池里存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。
    • 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
    • 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。
    • 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。
  3. 四种拒绝策略

AbortPolicy(抛出一个异常,默认的)
DiscardPolicy(直接丢弃任务)
DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
CallerRunsPolicy(交给线程池调用所在的线程进行处理)

https://mp.weixin.qq.com/s?__biz=MzA5MTkxMDQ4MQ==&mid=2648933151&idx=1&sn=2020066b974b5f4c0823abd419e8adae&chksm=88621b21bf159237bdacfb47bd1a344f7123aabc25e3607e78d936dd554412edce5dd825003d&token=995072421&lang=zh_CN#rd

如何自定义拒绝策略:实现RejectedExecutionHandler接口,实现rejectedExecution方法
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r,
ThreadPoolExecutor executor);
}

- 怎么配置参数#

线程数:

如果是CPU密集型应用,则线程池大小设置为N+1

如果是IO密集型应用,则线程池大小设置为2N+1

系统负载: 一个进程或线程正在被cpu执行或等待被cpu执行,则系统负载+1, 单核cpu负载小于1表示cpu可以在线程不等待的情况下处理完

IO密集:通常指网络IO

- 阻塞队列原理#

如果队列为空条件满足时,消费者一直等待,如果队列满条件满足时,生产者会一直等待,当条件不满足的时候,通过通知机制实现生产者和消费者间的通信。当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

JDK通过condition实现的通知机制,condition底层通过unsafe类的park、unpark方法实现的线程的阻塞和解除阻塞

- 有哪些阻塞队列#

ArrayBlockingQueue
ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。

LinkedBlockingQueue
LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列

DelayQueue
DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。

PriorityBlockingQueue
PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列;

SynchronousQueue
SynchronousQueue(同步队列)一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。

spring#

- 谈谈对springIoc的了解#

如何完美阐述你对IOC的理解?

// 最底层map: Map<String, Object> singletonObjects = new ConcurrentHashMap(256);

个人理解:
1.控制反转是一种设计思想,将对象生命周期和依赖的控制由对象转给了ioc容器,对象由主动创建其它对象变为被动由ioc容器注入
2.springioc容器控制了对象的创建、销毁及整个生命周期,,可以动态注入依赖的对象
3.将对象与对象间解耦,对象不再直接创建依赖其它对象,都依赖springioc容器

扩展:springIoc的初始化过程

它负责业务对象的构建管理和业务对象间的依赖绑定。

- 谈谈对springAop的了解#

AOP为面向切面编程,底层是通过动态代理实现,实质上就是将相同逻辑的重复代码横向抽取出来, 拦截对象方法,对方法进行改造、增强!比如在 方法执行前方法返回后方法前后方法抛出异常后 等地方进行一定的增强处理,应用场景: 事务、日志、权限、监控打点

基于动态代理来实现,在容器启动的时候生成代理实例。默认地,如果使用接口的,用 JDK 提供的动态代理实现,如果没有接口,使用 CGLIB 实现

优点:每个关注点现在都集中于一处,而不是分散到多处代码中;服务模块更简洁,服务模块只需关注核心代码

@Pointcut(切点):用于定义哪些方法需要被增强或者说需要被拦截
@Before、@After、@Around、@AfterReturning、@AfterThrowing(增强): 添加到切点指定位置的一段逻辑代码

注:@AfterReturning 比 @After 的方法参数多被代理方法的返回值

参考:
Spring AOP 使用介绍,从前世到今生
Spring AOP就是这么简单啦

- 动态代理和静态代理的区别#

静态代理在运行之前就已经存在代理类的字节码文件了(.class文件),而动态代理是在运行时通过反射技术来实现的

静态代理,如果不同接口的类想使用代理模式来实现相同的功能,将要实现多个代理类,但在动态代理中,只需要一个代理类就好了

只有实现了某个接口的类可以使用Java动态代理机制,对于没有实现接口的类,就不能使用该机制。使用cglib。

静态代理:代理类与委托类实现同一接口,并且在代理类中需要硬编码接口

JDK动态代理:代理类与委托类实现同一接口,主要是通过代理类实现InvocationHandler并重写invoke方法来进行动态代理的,在invoke方法中将对方法进行增强处理

CGLIB动态代理:代理类将委托类作为自己的父类并为其中的非final委托方法创建两个方法,一个是与委托方法签名相同的方法,它在方法中会通过super调用委托方法;另一个是代理类独有的方法。在代理方法中,它会判断是否存在实现了MethodInterceptor接口的对象,若存在则将调用intercept方法对委托方法进行代理

- Java反射的原理#

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能就称为反射机制。

通俗一点反射就是把Java类中的各种成分映射成一个个的Java对象。

操作步骤:

  1. 获取类的 Class 对象实例
  2. 根据 Class 对象实例获取 Constructor 对象
  3. 使用 Constructor 对象的 newInstance 方法获取反射类对象
  4. 获取方法的 Method 对象
  5. 利用 invoke 方法调用方法

为什么类可以动态的生成?
这就涉及到Java虚拟机的类加载机制了,推荐翻看《深入理解Java虚拟机》7.3节 类加载的过程。
Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:

通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口

关于第1点,获取类的二进制字节流(class字节码)就有很多途径
动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案

jvm#

- Java内存区域#

线程私有的

程序计数器
存放当前线程所执行的字节码的行号指示器。如果线程执行的是一个Java程序,计数器记录正在执行的虚拟机字节码指令地址;正在执行的是native方法,则计数器的值为空

Java虚拟机栈

描述Java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧来存储局部变量表(编译期可知的各种基本数据类型,对象引用【reference类型】,内存空间的分配是在编译期完成的,方法运行期间不会改变局部变量表的大小)、操作数栈、动态链接、方法出口等。

本地方法栈

为虚拟机使用到的native方法服务

线程公有的

Java堆(GC堆)

存放对象实例,所有的对象实例和数组都在堆上分配。程序运行时分支可能不一样,只有运行期间才能知道创建哪些对象,这部分内存的分配和回收是动态的

metaspace

存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码

直接内存

不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存。NIO使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免了Java堆和Native堆来回复制数据。直接内存的回收是在虚拟机进行full gc的时候顺带进行的,并不会自己触发垃圾回收

- 谈谈java的类加载机制#

  1. 过程

    • 加载
      通过全类名获取定义此类的二进制字节流
      将字节流所代表的静态存储结构转换为方法区的运行时数据结构
      在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
    • 连接
      验证
      文件格式、元数据、字节码、符号引用验证
    • 准备
      正式为类变量分配内存并设置类变量初始值的阶段,设置数据类型默认的零值
    • 解析
      解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
    • 初始化
      真正执行类中定义的 Java 程序代码
      初始化阶段是执行类构造器 ()方法的过程
  2. 双亲委派模式
    Java的类加载使用双亲委派模式,即一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。这种双亲委派模式的好处,一个可以避免类的重复加载,另外也避免了java的核心API被篡改。

    BootstrapClassLoader
    ExtensionClassLoader
    AppClassLoader(应用程序类加载器)

- gc对象存活判定算法#

1.1引用计数算法

给对象添加一个引用计数器,当有地方引用它时加1,当引用失效时计数减1,任何时刻计数器为0时的对象都不会再被引用。

缺点:存在两个对象不会被继续访问,但是两者循环调用的情况,由于存在循环调用导致两个对象都不会被回收。

1.2可达性分析算法

通过一系列被称作gc roots对象作为起点,从这些起点向下搜索,搜索所走过的路径成为引用链,当一个对象到gc roots没有任何引用链时证明此对象不可用。

Java中gc roots的对象包括:

虚拟机栈中引用的对象
native方法中引用的对象
类静态属性、常量中引用的对象

对于一个 Java 程序而言,对象都位于堆内存块中,存活的那些对象都被根节点引用着,即根节点 GC Roots 是一些引用类型,自然不在堆里,那它们位于哪呢?它们能放在哪呢?
答案是放在栈里,包括:
Local variables 本地变量
Static variables 静态变量
JNI References JNI引用等

- gc垃圾收集器及其优缺点#

垃圾回收器种类:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS(ConCurrent Mark Sweep)、G1(Garbage First)

- 标记算法和复制算法的区別,用在什么场合#

CMS 标记清除、Serial Old,Parallel old 标记整理:适用于存活对象比较多的场景
Serial、ParNew、PS 收集器 复制算法:用在那种不可达对象比较多的场合

- cms垃圾收集器的回收步骤及其优缺点?#

cms回收器的标记过程:

CMS以获取最短回收停顿时间为目标的收集器,使用“标记-清除”算法,分为以下6个步骤

1.STW initial mark:第一次暂停,初始化标记,从root标记old space存活对象(the set of objects reachable from roots (application code))

2.Concurrent marking:运行时标记,从上一步得到的集合出发,遍历old space,标记存活对象 (all live objects that are transitively reachable from previous set)

3.Concurrent precleaning:并发的标记前一阶段被修改的对象(card table)

4.STW remark:第二次暂停,检查,标记,检查脏页的对象,标记前一阶段被修改的对象 (revisiting any objects that were modified during the concurrent marking phase)

5.Concurrent sweeping:运行过程中清理,扫描old space,释放不可到达对象占用的空间

6.Concurrent reset:此次CMS结束后,重设CMS状态等待下次CMS的触发

或者4个大步骤:
1,initial mark 2,concurrent mark 3,remark 4,concurrent sweep

CMS缺点:cpu敏感,浮动垃圾,空间碎片

1.CMS收集器对cpu资源非常敏感,在并发阶段对染不会导致用户线程停顿,但是会因为占用一部分线程导致应用程序变慢,总吞吐量会降低。CMS默认启动的收集线程数=(CPU数量+3)/4,在cpu数比较少的情况下,对性能影响较大。

2.CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure”失败而导致另一次Full GC,原因是CMS的并发清除阶段用户线程还是在运行,所以还会有新的垃圾不断产生,这些垃圾CMS只能在下次GC时再清理掉,这部分垃圾被称为“浮动垃圾”。所以CMS不能像其他收集器那样在老年代几乎完全被填满了在开始收集,需要预留一部分空间。JDK1.6中CMS将这个阈值提高到了92%,要是CMS运行期间预留的内存不足,会出现一次“Concurrent Mode Failure”,这是虚拟机启动备用方案,临时启用Serial Old收集器充满新进行老年代垃圾收集,所以这个阈值不宜设置的过高

3.CMS基于标记-清除算法,这意味着垃圾收集结束后会有大量的空间碎片,空间碎片过多会造成老年代有很大空间空余但是无法存放大对象的情况。通过参数UseCMSCompactAtFullCollection(默认开启)开关参数来开启内存碎片的合并整理。

- 内存泄漏与内存溢出的区别#

  • 溢出: OOM,除了 PC 剩下的区域都会发生 OOM,是由于内存不够,或者是代码中错误的分配太多对象导致的。
  • 泄漏:是指 内存分配后没有回收,导致内存占有一直增加,最后会导致溢出。

- 有几种gc fail?#

Allocation Failure 新生代没有足够的空间分配对象 Young GC

GCLocker Initiated GC 如果线程执行在JNI临界区时,刚好需要进行GC,此时GC locker将会阻止GC的发生,同时阻止其他线程进入JNI临界区,直到最后一个线程退出临界区时触发一次GC。 GCLocker Initiated GC

Promotion Failure 老年代没有足够的连续空间分配给晋升的对象(即使总可用内存足够大)

Concurrent Mode Failure CMS GC运行期间,老年代预留的空间不足以分配给新的对象

- 什么情况会触发fullgc?#

1.metaspace空间不足

2.Promotion Failure 老年代没有足够的连续空间分配给晋升的对象(即使总可用内存足够大)

3.Concurrent Mode Failure CMS GC运行期间,老年代预留的空间不足以分配给新的对象

4.System.gc

5.jmap -histo:live [pid]

- 栈内存溢出#

对虚拟机栈这个区域规定了两种异常状况:

(1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常;
(2)如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出 OutOfMemoryError 异常。
(3)与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和OutOfMemoryError 异常。

- 系统内存多大,留给操作系统2G,够吗?#

16G内存 虚拟机 推荐配置12G堆, 更大的堆容易引起SWAP,SWAP使用过多会造成宕机

各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点。

活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小

例如,根据GC日志获得老年代的活跃数据大小为300M,

总堆:1200MB = 300MB × 4 新生代:450MB = 300MB × 1.5 老年代: 750MB = 1200MB - 450MB

https://tech.meituan.com/2017/12/29/jvm-optimize.html

mysql索引#

- 什么情况下索引失效#

  1. 不符合联合最左匹配
  2. like前导模糊查询 (可以通过 REVERSE()函数来创建一个函数索引解决)
  3. 字段类型不匹配
  4. 索引字段施加函数
  5. or的多个字段都要索引(只要有一个没有索引,就无法使用索引)
  6. != , <> is null, is not null都会使索引失效

- InnoDB 支持的索引类型#

  1. B+树索引:
  2. 全文索引:就是倒排索引
    仅能再char、varchar、text类型的列上面创建全文索引
    但是面对高级的搜索还是略显简陋,且性能问题也是担忧。
  3. 哈希索引:
    哈希索引在InnoDB中只是一种系统自动优化的功能
    Hash 索引仅仅能满足”=”,”IN”和”<>”查询,不能使用范围查询。
    Hash 索引无法被用来避免数据的排序操作。
    在MySQL运行的过程中,如果InnoDB发现,有很多SQL存在这类很长的寻路,并且有很多SQL会命中相同的页面(page),InnoDB会在自己的内存缓冲区(Buffer)里,开辟一块区域,建立自适应哈希所有AHI,以加速查询。InnoDB的自使用哈希索引,更像“索引的索引”

- 平时怎么创建索引的#

  1. 选择区分度高的列做索引

  2. 根据查询场景、查询语句的查询条件设计索引

  3. 如果要为多列去创建索引,遵循最左匹配原则使用联合索引去创建

  4. 在长字符类型的字段上使用前缀索引,减少空间占用

  5. 扩展索引的时候尽量做追加,不是新建

- B+树和 B 树的区别,和红黑树的区别#

为什么不用B树?:
因为B树的所有节点都是包含键和值的,这就导致了每个几点可以存储的内容就变少了,出度就少了,树的高度会增高,查询的 时候磁盘I/O会增多,影响性能。由于B+Tree内节点去掉了data域,因此可以拥有更大的出度,拥有更好的性能。

和红黑树:
B+树跟红黑树不用比,B+树的高很低,红黑树比不了。

- 如何优化sql#

  1. 让sql使用索引,如果查看sql使用索引情况,explain查看执行计划
    通过explain命令可以得到下面这些信息: 表的读取顺序,数据读取操作的操作类型哪些索引可以使用哪些索引被实际使用,表之间的引用,每张表有多少行被优化器查询等信息。 rows是核心指标,绝大部分rows小的语句执行一定很快。
    Extra字段几种需要优化的情况
    Using filesort 需要优化,MYSQL需要进行额外的步骤来对返回的行排序。
    Using temporary需要优化,发生这种情况一般都是需要进行优化的。mysql需要创建一张临时表用来处理此类查询
    Using where 表示 MySQL 服务器从存储引擎收到行后再进行“后过滤”
  2. 如果让sql使用到索引(符合建索引的几个原则)
    • 最左前缀匹配原则
    • 选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*)
    • 索引列不能参与计算,保持列“干净”, 原因很简单,b+树中存的都是数据表中的字段值
    • 尽量的扩展索引,不要新建索引
    • =和in可以乱序

- 什么是覆盖索引#

覆盖索引就是把要查询出的列和索引是对应的,不做回表操作

- 为什么尽量不要用select *?#

  1. 返回了太多不需要的数据
  2. 无法覆盖索引(select * 走的是聚簇索引),需要回表
  3. 一个好的应用程序设计应当能够在 sql 中有准确的定义,从而减少歧义或者不必要的更改

mysql隔离级别#

- MVCC, 什么是快照读,什么是当前读#

快照读: 即普通SELECT语句,既然是快照读,故 SELECT 的时候,会生成一个快照。

生成快照的时机:事务中第一次调用SELECT语句的时候才会生成快照,在此之前事务中执行的update、insert、delete操作都不会生成快照。

不同事务隔离级别下,快照读的区别:

READ COMMITTED 隔离级别下,每次读取都会重新生成一个快照,所以每次快照都是最新的,也因此事务中每次SELECT也可以看到其它已commit事务所作的更改;

REPEATED READ 隔离级别下,快照会在事务中第一次SELECT语句执行时生成,只有在本事务中对数据进行更改才会更新快照,因此,只有第一次SELECT之前其它已提交事务所作的更改你可以看到,但是如果已执行了SELECT,那么其它事务commit数据,你SELECT是看不到的。

- 什么是当前读#

https://mp.weixin.qq.com/s/w1DwsDDSxKfmFxcLOgG3Dw

- mysql数据库事务隔离级别,分別解決什么问题,next-key锁原理、如何解决幻读?#

READ-UNCOMMITTED(未提交读): 可能会导致脏读、幻读或不可重复读
READ-COMMITTED(提交读): 可以阻止脏读,但是幻读或不可重复读仍有可能发生
REPEATABLE-READ(可重复读,mysql默认隔离级别): 可以阻止脏读和不可重复读,幻读通过mvcc解决了快照读,next-key锁解决了当前读
SERIALIZABLE(串行化读): 该级别可以防止脏读、不可重复读以及幻读

- RR RC区别#

区别就在于rr解决了不可重复读和幻读,怎么解决的,通过MVCC和next-key锁.

不可重复读:rc级别下的mvcc总是读取数据行的最新快照,而rr级别下的mvcc,会在事务第一次select的时候,为数据行生成一个快照,后面每次都读这个快照,除非自己更新,所以rr下是可重复读,别的事务提交也无法影响你的事务。

幻读:快照读下rr级别不会出现幻读,因为rr级别的mvcc读的是事务第一次读取时的快照;在当前读下rr级别使用了next-key锁(临键锁),临键锁包括行锁+间隙锁, 来避免两个当前读时有其它事务插入数据,所以当前读使用next-key锁解决的幻读。 最后备注下:如果是先快照读再当前读,影响行数不一致是否属于幻读,是有争议的但大多认为并不是幻读。

mysql其他#

- 分库分表#

根据userid取模做的分库分表

两个场景,业务解耦垂直拆分,读写性能瓶颈做水平拆分。

  1. 垂直切分

业务维度切分,解耦

  1. 水平切分

读写性能遇到瓶颈,分库分表

- mysql的乐观锁、悲观锁实现#

MySQL乐观锁的实现完全是逻辑的,也就是自己去实现。 比如给每条数据附带版本号或者timestamp。更新引起数据的版本号改变,两次select判断版本号是否一致可以判断是否发生改变

MySQL悲观锁的实现需要借助于MySQL的锁机制。
行锁
常见的增删改(INSERT、DELETE、UPDATE)语句会自动对操作的数据行加写锁,查询的时候也可以明确指定锁的类型,SELECT … LOCK IN SHARE MODE 语句加的是读锁,SELECT … FOR UPDATE 语句加的是写锁
行锁的实现方式
1) Record lock 锁记录
2) Gap lock 锁两个记录之间的 GAP,防止记录插入
3) Next-key lock 锁一条记录及其之前的间隙

redis#

- redis支持的数据结构#

String

最常规的get/set操作,value可以是数字和string
一般做一些复杂的计数功能的缓存
hash

value存放的是结构化的对象
比较方便的就是操作其中的某个字段
在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果

list

使用List的数据结构,可以做简单的消息队列的功能
还有一个是,利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好
一个场景,很合适—取行情信息。就也是个生产者和消费者的场景。LIST可以很好的完成排队,先进先出的原则。

set

set堆放的是一堆不重复值的集合。所以可以做全局去重的功能
为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了
另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能

sorted set

sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。

- redis的过期删除策略#

定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.

定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除

于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。

- redis的内存淘汰机制#

在Redis内存使用达到设定上限时,触发满容淘汰策略释放内存

Redis 目前提供8种策略:

volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰

volatile-random:从已设置过期时间的数据集中任意选择数据淘汰

volatile-lfu:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰

volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰

allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)

allkeys-random:从数据集中任意选择数据淘汰

allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key

no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

- 如果redis有热点key怎么解决#

本地缓存

- redis缓存击穿、缓存穿透、缓存雪崩怎么解决#

缓存穿透

定义: 访问一个不存在的key,缓存不起作用,请求会穿透到DB,流量大时DB会挂掉。

解决方案:

  1. 缓存空值 (值少时)
  2. 布隆过滤器, 特性: 没有的肯定没有,有的不一定有

缓存击穿

定义:并发性,一个存在的key,在缓存过期的一刻,同时有大量的并发请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。

解决方案:

  1. 分布式锁,在访问key之前,采用分布式锁SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key
  2. 双重缓存,设置不同过期,其实同时解决了key过热 和 缓存击穿问题,如果更新的操作确实很耗时,返回的有损请求比较多,那确实需要双重缓存了,再放一份到本地、分布式缓存的另一个key

缓存雪崩

定义:大量的key设置了相同的过期时间,或者某台服务器宕机,导致大量缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。

解决方案:

  1. 提前预防,主从加集群,主从可以一个实例挂了,另一个可以顶上。集群数据分片,即使一个分片上的主从都挂了,打到db的量也不会是全部
  2. 将key的过期时间设置时添加一个随机时间,分散过期时间
  3. 如果是热点key,可以加分布式锁,减少并发量
  4. 二级缓存(本地缓存),减少db压力

击穿是单个,必定是热点key,雪崩是很多,不一定是热点key,对应的解决方案也有不一样的地方,雪崩有一个设置过期时间加随机数,雪崩用二级缓存比较好使,但击穿就没太大必要

mq#

- mq使用场景#

  • 解耦
    • 多个下游相同依赖
    • 数据驱动的任务依赖(下游任务强依赖上游数据到达)
  • 异步
    • 上游不关心执行结果
    • 上游关注执行结果,但执行时间很长(改为异步操作,再尝试获取结果)
  • 削峰
    • 请求高峰期

- kafka如何保证消息顺序性#

  1. 一个partition内保证顺序性
  2. 可以通过message key来定义,同一个key的message可以保证只发送到同一个partition

- kafka如何保证消息可靠性#

  1. 多副本写入
  2. 数据持久化,通过后台线程将消息持久化到磁盘
  3. 生产者确认,分片所有副本写入成功才返回。发送失败重试
  4. 消费组回复消费成功ack,确保消费一次

网络#

- http1.X 和 http2.0 的区別#

HTTP/2 的多路复用(Multiplexing) 则允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息

- Http https 的区別#

HTTPS和HTTP的区别主要如下:
1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

- https过程#

- https优缺点#

优点:
(1)使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
(2)HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
(3)HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。
(4)谷歌曾在2014年8月份调整搜索引擎算法,并称“比起同等HTTP网站,采用HTTPS加密的网站在搜索结果中的排名将会更高”。
缺点:
(1)HTTPS协议握手阶段比较费时,会使页面的加载时间延长近50%,增加10%到20%的耗电;
(2)HTTPS连接缓存不如HTTP高效,会增加数据开销和功耗,甚至已有的安全措施也会因此而受到影响;
(3)SSL证书需要钱,功能越强大的证书费用越高,个人网站、小网站没有必要一般不会用。
(4)SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名,IPv4资源不可能支撑这个消耗

- tcp/ip模型分层及各层协议举例#

分层
应用层
任务是通过应用进程间的交互来完成特定网络应用, dns, http, smtp
传输层
负责向两台主机进程之间的通信提供通用的数据传输服务, tcp, udp
网络层
选择合适的网间路由和交换结点, 确保数据及时传送, ip,ICMP
数据链路层
两台主机之间的数据传输,这就需要使用专门的链路层的协议, ARP

- tcp如何保证可靠传输,tcp、udp区别#

tcp与udp区别

  1. 是否建立连接, udp不建立连接,tcp三次握手
  2. 是否可靠,udp不需要确认, tcp会有确认、重传、窗口、拥塞等机制
  3. 应用场景, udp一般用于即时通信,qq,直播; tcp用于文件传输,收发邮件、登录等

- 为什么要三次握手,四次挥手#

为什么要三次握手
确认双发收发功能都正常

为什么要四次挥手
确认双方都没有数据再发送

- 四次挥手的closewait, timewait分别在哪,为什么timewait要等待2msl#

2MSL是两倍的MSL(Maximum Segment Lifetime),MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间

如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接

TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK

设计#

- 秒杀系统 、高并发读、写#

前端-动静分离,CDN
后端-1.隔离(业务、数据库、系统)、限流、降级、服务分层(bc端分离)
2.高并发读 - 分布式缓存、本地缓存
3.高并发写
- redis可扛,先写redis再写DB或通过MQ做落地操作, 分布式限流
- redis扛不住, 本地限流、分库存
4.其它 验证码、答题、排队

- 秒杀系统设计#

前端-动静分离-静态数据缓存到离用户最近的地方

页面内容静态化

静态资源缓存到CDN和用户Cache

后端-独立部署、热点隔离、分层过滤

业务隔离:秒杀系统是独立的系统,专门为秒杀开发,其他什么事情都不干。

系统隔离:秒杀系统部署在独立的服务器上。

数据隔离:秒杀系统关联的数据库单独部署,防止读写操作影响常规业务数据库。

后端-削峰
排队、答题、二维码

商品卖光后,后来的请求直接返回活动结束页。

直接只处理前面一部分请求,后面没有任何机会的请求可以直接返回活动结束页。

关键点:分层过滤
![](https://ipic-1252327316.cos.ap-beijing.myqcloud.com/image/image2016-10-18 18_10_51.png)
1.将动态请求的读数据缓存(Cache)在 Web 端,过滤掉无效的数据读;
2.对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题
3.对写数据进行基于时间的合理分片,过滤掉过期的失效请求;
4.对写请求做限流保护,将超出系统承载能力的请求过滤掉
5.对写数据进行强一致性校验,只保留最后有效的数据。

后端高可用-兜底
系统限流、降级方案

其他#

- 分布式事务#

分布式事务,主要为了实现分布式系统下一组操作的全部成功或全部失败,
主要分为数据库层的和业务层的分布式事务,

一、数据库层的主要是基于XA协议的分布式事务,实现的强一致性,基于XA模型,主要有全局的事务管理器和本地资源管理器,事务管理器会调度本地资源管理器的本地事务的提交和回滚,达到全局的强一致性

二、业务层的,基于CAP和BASE理论,实现的最终一致性,来支持的分布式事务,主要有tcc模型的补偿性事务,tcc模型主要将操作分为三个,try, confirm, cancel, try会预留该事务的资源,即使当前事务出问题,也不会影响其它事务,支持高并发, 当一组服务的try都成功,tcc会保证所有服务的confirm最终成功,如果一组服务中有try失败,tcc会保证所有的服务最终cancel成功。

- 主从延迟怎么办#

主从延迟的原因:
主库写入数据并且生成binlog文件, 从库异步读取更新

解决方案:
一、更新操作,做SQL优化,减少批量更新操作
二、查询场景,

  1. 强制读主,对主库压力大,谨慎使用
  2. 延迟读从,更新时写一个过期时间为主从延迟时间的缓存key,读的时候添加一个判断
  3. 识别重试机制,通过消息发送的版本号与数据库数据版本对比识别最新数据,如果不一致,通过等待重试来保证

- 设计模式六大原则#

其中设计模式的SOLID原则(Principles)如下:

单一职责原则(Single Responsibility)
开闭原则(Open Closed)
里氏代换原则(Liskov Substitution)
接口隔离原原则(Interface Segregation)
依赖倒置原则(Dependency Inversion)

单一职责原则
定义:不要存在多于一个导致类变更的原因

里氏替换原则:所有引用基类的地方必须能透明地使用其子类的对象

依赖倒置原则
定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。

问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。

接口隔离原则
定义:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则

迪米特法则
定义:一个对象应该对其他对象保持最少的了解。
尽量降低类与类之间的耦合

开闭原则
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

- 线上需要关注哪些机器参数#

指标 阈值

cpu.iowait 所有进程因为等待IO完成而被阻塞,导致CPU idle所花的时间的百分比

disk.io.util 如果这个指标较高,代表io遇到瓶颈

cpu.busy 60

cpu.load 1以下比较好,1.5 会引起程序响应时间变慢,应触发报警

mem.swapused 一般情况下使用swap,代表物理内存已不足,当系统没有足够物理内存来应付所有请求的时候就会用到 swap 设备。使用 swap 的代价非常大,如果系统没有物理内存可用,就会频繁 swapping,如果 swap 设备和程序正要访问的数据在同一个文件系统上,那会碰到严重的 IO 问题,最终导致整个系统迟缓。

cms gc后的老年代大小

jvm.fullgc.count 5

jvm.yonggc.count 70

jvm.yonggc.meantime(一分钟内的每次年轻代gc的平均时间) 500

- 请求幂等性#

请求幂等性总结:

1.是否需要幂等。比如查询,insert含唯一索引的表,update set数据,delete by id 等简单场景是天然幂等。不需要额外做幂等操作。无法这样的比如要做数据的累加,就要做幂等。

2.如何判断重复。

业务唯一键,查记录表或流水表,根据记录的有无和状态来判断。

3.实现。

  1. 简单的话接口直接实现, 但通常幂等逻辑是有通用性的
  2. 如果服务多接口使用写幂等工具类
  3. 如果多服务使用一套幂等逻辑,写sdk
  4. 最复杂的多服务间幂等,还无法都获取到记录结果,就要在sdk统一读写幂等表,读写相同的幂等记录做幂等

4.并发处理

  1. 单机情况下,使用java锁synchronized
  2. 使用数据库锁,悲观锁:select for update,乐观锁:update set XXX and version = 2 where version = 1
  3. 使用分布式锁:
    redis锁:1.过期时间导致并发问题,2.主从切换时锁丢失
    zookeeper锁:写能力不如redis

幂等设计

- 分布式锁#

方案1、基于数据库唯一主键
原理:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。

方案2、基于redis实现分布式锁
原理:通过redis 中的命信setNx 来实现 (当key不存在则设置值并返回true,否则返回失败)

方案3、基于Zookeeper来实现分布式锁
原因:1、先建一个目录lock 2、线程A想获取锁就在lock目录下创建一个带顺序的临时节点 3、然后获取比当前顺序号小的顺序号,获取到则获取锁失败,获取不到则获取锁成功。 4、解锁则是删除这个临时节点。

- 分布式主键生成方案选择#

分布式自增ID的实现

1.uuid
组成部分:当前日期和时间、时钟序列、机器识别码

缺点:
UUID长度128bit,32个16进制字符,占用存储空间多,且生成的ID是无序的

对于InnoDB这种聚集主键类型的引擎来说,数据会按照主键进行排序,由于UUID的无序性,InnoDB会产生巨大的IO压力,此时不适合使用UUID做物理主键。

2.号段模式,底层proxy服务+数据库分段获取id
结合数据库维护一个Sequence表,每当需要为某个表的新纪录生成ID时就从Sequence表中取出对应的nextid,将其+1后更新到数据库中以备下次使用。

缺点:由于所有的插入都要访问该表,很容易造成性能瓶颈。
在高并发场景下,无法保证高性能。

3.Snowflake
使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。

长度为64bit,占用存储空间多
存在时钟回拨问题, leaf已解决

- 有哪些处理线上问题的经验#

实际是在考雪崩,限流,降级等措施

xxx 慢查询引发的血案1
事件:下单的时候先去删除用户未支付订单并归还库存,但未支付订单表竟然没有给orderid添加索引,导致delete操作锁全表,在当天有大量未支付订单,导致下单接口越来越慢,最终把订单db中的连接数占满,db的整体不可用。

分析原因: DB的客户端在超时的时候会断开连接,但DB服务器还是在执行该操作,或阻塞着,客户端新的调用再申请新的连接,直到把DB的连接池打满,DB完全阻塞在这个查询上,导致不可用。

事前: 1. 学习数据库知识,尤其是索引和锁相关 2. 定时检查服务的索引是否覆盖全 3. 提前做好限流熔断等降级措施
事中: 1. 限流降级, 2. 问题排查,数据库压力没有增加很多,但数据库响应缓慢,应该分析出是慢查询的原因,当很多的慢sql出来时
事后: 1. 对服务整体重新排查

xxx 慢查询引发问题,慢sql导致mq积压
事件: 新表去更新库存,但索引创建的时候没覆盖全场景,后续开发有sql未走到索引,某个周六出现MQ积压告警,看监控发现是慢sql更新,有条update语句未使用到索引,导致更新时锁全表
事中: 临时增加索引
事后: 检查了新表所有设计的sql没有未使用索引的情况。
(慢查询是有可能占用过多的DB资源,把整个DB打垮)(是有可能出现卖超的)

xxx ES导致雪崩,bc端未分离,b端下游超时引发的血案
事件: 调用ES持续大量超时,服务bc端都在调用,b端超时时间设置长,占用了很多线程资源,影响C端服务响应,造成C端上游雪崩,影响二十度个服务
(调ES超时的原因?查询请求的返回响应太大了)
事中: 无熔断降级错误,现上线加熔断ES降级,
(降级措施是?返回有损服务,创建活动失败)
事后: bc端分离,对下游添加熔断降级,根本上还是要让es调用查询粒度更小一点,减少es调用的返回量

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×