[关闭]
@songhanshi 2021-05-10T08:42:23.000000Z 字数 303941 阅读 1008

Link

1234


https://www.nowcoder.com/discuss/594676?source_id=discuss_experience_nctrack&channel=-1

https://codetop.cc/#/home

https://osjobs.net/topk/

负载均衡--https://blog.csdn.net/My_Way666/article/details/91433816
zk -- https://www.jianshu.com/p/30f3c0ce2c5b

加一个

improve

名词

测试工具

性能

高可用

监控

1-base

异常

泛型

反射

序列化与反序列化

深拷贝与浅拷贝

设计模式

4.如何实现一个生产者和消费者模型。
3. 消费者重平衡(高可用性、伸缩性)
4. 那些情景下会造成消息漏消费?
5. 如何保证消息不被重复消费(幂等性)
8. 消费者与生产者的工作流程:

单例模式

基本类型分类

哈希表

哈希表_YSO

接口和抽象类

Object-equal、hashcode

Integer

String

集合概述

接口 实现类
List ArrayList、LinkedList
Set HashSet、TreeSet、LinkedHashSet
Map HashMap、TreeMap、LinkedHashMap、HashTable

在这里插入图片描述

ArrayList和LinkedList区别

ArrayList->CopyOnWriteArrayList

LinkedList

HashMap->ConcurrentHashMap

HashTable

区别 :
(1)HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;
(2)HashMap允许key和value为null,而HashTable不允许
2.底层实现:数组+链表实现
jdk8开始链表高度到8、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在计算key的hash值,二次hash然后对数组长度取模,对应到数组下标,如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组,如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表key为null,存在下标0的位置
数组扩容

HashSet

LinkedHashMap

LinkedHashSet

TreeMap

TreeSet

collections

  1. Arrays工具类怎么用?
  2. collections有哪些?

2-concurrent

|并发笔记|javaGuide|javaGuide|

线程、进程、程序

缓存一致性

缓存一致性问题?

并发安全、活跃、性能问题?

线程安全

JMM和volatile

synchronized

synchronized锁升级

final

锁机制

  1. 分布式锁
    分布式锁一般有三种实现方式:1.数据库锁;2.基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。

  2. 线程同步的方式?
    ① synchronized
    ② Condition
    ③ CountDownLatch、CyclicBarrier

  3. java锁及实现

  4. java锁机制
    1) 所熟知的Java锁机制无非就是Sychornized 锁 和 Lock锁 (对象头知识,偏向锁,轻量级锁,重量级锁)

    • Lock 同步锁是基于 Java 实现的,而 Synchronized是基于底层操作系统的 Mutex Lock实现的,每次获取和释放锁操作都会带来用户态和内核态的切换,从而增加系统性能开销。
      https://www.jianshu.com/p/e674ee68fd3f
      2) 在 Java 多线程编程当中,提供了多种实现 Java 线程安全的方式:
    • 最简单的方式,使用 Synchronization 关键字
    • 使用 java.util.concurrent.atomic 包中的原子类,例如 AtomicInteger
    • 使用 java.util.concurrent.locks 包中的锁
    • 使用线程安全的集合 ConcurrentHashMap
    • 使用 volatile 关键字,保证变量可见性(直接从内存读,而不是从线程 cache 读
      https://www.cnblogs.com/theworld/p/12056452.html
      https://www.jianshu.com/p/e674ee68fd3f
  5. 都有什么锁?说说乐观锁悲观锁是什么,怎么实现,volatile关键字,CAS,AQS原理及实现。
    1)锁的分类:

    • 公平锁、非公平锁
    • 互斥锁、读写锁
    • 乐观锁、悲观锁
      synchronized,retreenLock, ReadWriteLock
      4)CAS,AQS原理及实现

死锁

线程排查
1. 排查CPU占满的Java线程
产生CPU100%的原因:某一程序一直占用CPU是导致CPU100%的原因,大概有以下几种情况:
1)Java 内存不够或溢出导致GC overhead问题, GC overhead 导致的CPU 100%问题;
2)死循环问题. 如常见的HashMap被多个线程并发使用导致的死循环, 或者死循环;
3)某些操作一直占用CPU
步骤:
1)jps 获取Java**进程的PID。
2)top -Hp PID 查看对应进程的哪个
线程**占用CPU过高。该进程内最耗费CPU的线程
3)echo "obase=16;PID" | bc 将线程的PID转换为16进制,大写转换为小写。
4)jstack pid >> java.txt 导出CPU占用高进程的线程栈
jstack 2444 >stack.txt或者jstack 进程id | grep 16进制线程id
在Java.txt中查找转换成为16进制的线程PID。找到对应的线程栈。
辅助
命令参考
grep "99b" stack.txt -A 25
grep -C 5 foo file 显示file文件里匹配foo字串那行以及上下5行
grep -B 5 foo file 显示foo及前5行
grep -A 5 foo file 显示foo及后5行
对线程状态进行分析。
新建( new )、可运行( runnable )、运行( running )、阻塞( block )、死亡( dead )

如何解决死锁

  1. 如何预防死锁?

    • 死锁的产生
      1)互斥,共享资源 X 和 Y 只能被一个线程占用;
      2)占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
      3)不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
      4)循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等
      待。
    • 破坏死锁条件
      1)对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
      2)对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
      3)对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。
  2. 互斥、占有且等待、循环等待

    • 1)循环等待问题:
      破坏占用且等待条件的时,如果转出账本和转入账本不满足同时在文件架上这个条件,就用while死循环的方式来循环等待,
    • 2)循环等待问题的解决方案?
      方案:如果线程要求的条件(转出账本和转入账本同在文件架上)不满足,则线程阻塞自己,进入等待状态;当线程要求的条件(转出账本和转入账本同在文件架上)满足后,通知等待的线程重新执行。其中,使用线程阻塞的方式就能避免循环等待消耗CPU的问题。
    • 3)等待-通知机制
      一个完整的等待-通知机制:线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;当要求的条件满足时,通知等待的线程重新获取互斥锁。
    • 4)如何用synchronized实现等待-通知机制?
      Java语言里,等待-通知机制可以有多种实现方式,比如 Java 语言内置的 synchronized配合wait()、notify()、notifyAll()这三个方法就能轻松实现。
    • 5)如何用synchronized实现互斥锁?
      --在下面这个图里,左边有一个等待队列,同一时刻,只允许一个线程进入synchronized保护的临界区(这个临界区可以看作大夫的诊室),当有一个线程进入临界区后,其他线程就只能进入图中左边的等待队列里等待(相当于患者分诊等待)。这个等待队列和互斥锁是一对一的关系,每个互斥锁都有自己独立的等待队列
      在这里插入图片描述
      --在并发程序中,当一个线程进入临界区后,由于某些条件不满足,需要进入等待状态,Java 对象的 wait() 方法就能够满足这种需求。如上图所示,当调用 wait() 方法后,当前线程就会被阻塞,并且进入到右边的等待队列中,这个等待队列也是互斥锁的等待队列。线程在进入等待队列的同时,会释放持有的互斥锁,线程释放锁后,其他线程就有机会获得锁,并进入临界区了。
      在这里插入图片描述
    • 那线程要求的条件满足时,如何通知这个等待的线程呢?
      就是 Java 对象的 notify()和notifyAll()方法。下图大致描述了这个过程,当条件满足时调用notify(),会通知等待队列(互斥锁的等待队列)中的线程,告诉它条件曾经满足过
      在这里插入图片描述
    • 为什么说是曾经满足过呢?
      注意:notify()只能保证在通知时间点,条件是满足的。而被通知线程的执行时间点和通知的时间点基本上不会重合,所以当线程执行的时候,很可能条件已经不满足了(保不齐有其他线程插队)。
    • 还需注意:被通知的线程要想重新执行,仍然需要获取到互斥锁(因为曾经获取的锁在调用 wait() 时已经释放了)。
    • 6)使用wait()、notify()、notifyAll()方法?
      --synchronized 锁定的是this,那么对应的一定是this.wait()、this.notify()、this.notifyAll();
      --synchronized 锁定的是target,那么对应的一定是target.wait()、target.notify()、target.notifyAll() 。
      --wait()、notify()、notifyAll()这三个方法能够被调用的前提是已经获取了相应的互斥锁,所以我们会发现 wait()、notify()、notifyAll() 都是在
      synchronized{}内部被调用的。如果在 synchronized{}外部调用,或者锁定的 this,而用target.wait() 调用的话,JVM 会抛出一个运行时异常:
      java.lang.IllegalMonitorStateException。
    • 7)尽量使用 notifyAll()?
      --上述使用notifyAll()来实现通知机制,为什么不使用notify()呢?
      --这二者是有区别的,notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程。从感觉上来讲,应该是 notify() 更好一些,因为即便通知所有线程,也只有一个线程能够进入临界区。实际上使用notify()也很有风险,它的风险在于可能导致某些线程永远不会被通知到。
      --假设我们有资源 A、B、C、D,线程 1 申请到了 AB,线程 2 申请到了 CD,此时线程 3 申请AB,会进入等待队列(AB 分配给线程 1,线程 3 要求的条件不满足),线程 4 申请 CD 也会进入等待队列。我们再假设之后线程 1 归还了资源 AB,如果使用notify()来通知等待队列中的线程,有可能被通知的是线程 4,但线程 4 申请的是 CD,所以此时线程 4 还是会继续等待,而真
      正该唤醒的线程 3 就再也没有机会被唤醒了。
      --所以除非经过深思熟虑,否则尽量使用 notifyAll()。
  3. 不可抢占

    • Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。
    • Java 语言本身提供的 synchronized 也是管程的一种实现,既然 Java 从语言层面已经实现了管程了,那为什么还要在SDK里提供另外一种实现呢?
    • 为解决的问题:
      死锁问题中,破坏不可抢占条件方案,但是这个方案synchronized没有办法解决。原因是synchronized申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,也释放不了线程已经占有的资源。但我们希望的是:

      1. 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
    • Java SDK 并发包里的Lock有别于synchronized隐式锁的三个特性:能够响应中断、支持超时和非阻塞地获取锁。

    • 互斥锁lock三种方案:
      1)能够响应中断。
      synchronized 的问题是,持有锁 A 后,如果尝试获取锁B失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁A。这样就破坏了不可抢占条件了。
      2)支持超时。
      如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
      3)非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
      --这三种方案可以全面弥补synchronized的问题。这三个方案体现在API上,就是 Lock 接口的三个方法。如下:

      1. // 支持中断的 API
      2. void lockInterruptibly()
      3. throws InterruptedException;
      4. // 支持超时的 API
      5. boolean tryLock(long time, TimeUnit unit)
      6. throws InterruptedException;
      7. // 支持非阻塞获取锁的 API
      8. boolean tryLock();
    • 如何保证可见性?
      --Java SDK 里面 Lock 的使用,有一个经典的范例,就是try{}finally{}
      ,需要重点关注的是在finally里面释放锁。
      --可见性是怎么保证的?
      --- Java 里多线程的可见性是通过 Happens-Before 规则保证的,
      --- synchronized 之所以能够保证可见性,也是因为有一条 synchronized相关的规则:synchronized 的解锁Happens-Before于后续对这个锁的加锁。
      --- Java SDK 里面 Lock 靠什么保证可见性呢?例如在下面的代码中,线程 T1 对 value 进行了 +=1 操作,那后续的线程 T2 能够看到 value的正确结果吗?

      1. class X {
      2. private final Lock rtl = new ReentrantLock();
      3. int value;
      4. public void addOne() {
      5. // 获取锁
      6. rtl.lock();
      7. try {
      8. value+=1;
      9. } finally {
      10. // 保证锁能释放
      11. rtl.unlock();
      12. }
      13. }
      14. }

    答案必须是肯定的。Java SDK里面锁原理简述:利用了 volatile 相关的 Happens-Before 规则。Java SDK 里面的ReentrantLock,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state的值(简化后的代码如下面所示)。也就是说,在执行 value+=1
    之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile变量 state。根据相关的 Happens-Before 规则:
    1)顺序性规则:对于线程 T1,value+=1 Happens-Before 释放锁的操作 unlock();
    2)volatile 变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作Happens-Before 线程 T2 的 lock() 操作;
    3)传递性规则:线程 T2 的 lock() 操作 Happens-Before 线程 T1 的 value+=1 。

    1. class SampleLock {
    2. volatile int state;
    3. // 加锁
    4. lock() {
    5. // 省略代码无数
    6. state = 1;
    7. }
    8. // 解锁
    9. unlock() {
    10. // 省略代码无数
    11. state = 0;
    12. }
    13. }

    所以说,后续线程 T2 能够看到 value 的正确结果

  4. Condition

    • Java SDK 并发包里的 Condition
      --Condition实现了管程模型里面的条件变量。
      --管程中提到 Java 语言内置的管程里只有一个条件变量,而 Lock&Condition实现的管程是支持多个条件变量的,这是二者的一个重要区别。
      --很多并发场景下,支持多个条件变量能够让并发程序可读性更好,实现起来也更容易。
      --例如,实现一个阻塞队列,就需要两个条件变量。
    • 如何利用两个条件变量快速实现阻塞队列呢?
      一个阻塞队列,需要两个条件变量,一个是队列不空(空队列不允许出队),另一个是队列不满(队列已满不允许入队)。相
      关的代码,重新列出。
      1. public class BlockedQueue<T>{
      2. final Lock lock = new ReentrantLock();
      3. // 条件变量:队列不满
      4. final Condition notFull = lock.newCondition();
      5. // 条件变量:队列不空
      6. final Condition notEmpty = lock.newCondition();
      7. // 入队
      8. void enq(T x) {
      9. lock.lock();
      10. try {
      11. while (队列已满){
      12. // 等待队列不满
      13. notFull.await();
      14. }
      15. // 省略入队操作...
      16. // 入队后, 通知可出队
      17. notEmpty.signal();
      18. }finally {
      19. lock.unlock();
      20. }
      21. }
      22. // 出队
      23. void deq(){
      24. lock.lock();
      25. try {
      26. while (队列已空){
      27. // 等待队列不空
      28. notEmpty.await();
      29. }
      30. // 省略出队操作...
      31. // 出队后,通知可入队
      32. notFull.signal();
      33. }finally {
      34. lock.unlock();
      35. }
      36. }
      37. }

    注意:
    -- Lock 和 Condition实现的管程,线程等待和通知需要调用await()、signal()、signalAll(),语义和wait()、notify()、notifyAll()是相同的。
    -- 区别是,Lock&Condition实现的管程里只能使用前面的await()、signal()、signalAll(),而后面的wait()、notify()、notifyAll() 只有在 synchronized实现的管程里才能使用。
    -- 如果一不小心在Lock&Condition实现的管程里调用了wait()、notify()、notifyAll(),那程序可就彻底玩儿完了

线程生命周期

JUC

在这里插入图片描述

synchronized和lock的区别

Object 的wait()/notify()/notifyAll() 的用法
Condition的 await()、signal()、signalAll()

乐观锁悲观锁

CAS + Atomic

从 Java1.5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。

自旋锁

AQS(AbstractQueuedSynchronizer) ★

可重入锁
AQS
ReentrantLock-公平锁|非公平锁
Condition

ReadWriteLock读写锁

Semaphore

CountDownLatch|CyclicBarrier

Exchange

并发容器

CopyOnWriteArrayList
  1. ArrayList是线程不安全的,例子以及解决方案(sxt2)

    • 1)非线程安全例子
      1. psvm{
      2. List<String> list = new ArrayList<>();
      3. for(int i=1; i<30; i++){
      4. new Thread(() -> {
      5. list.aa(UUID.randomUUID().toString().substring(0,8));
      6. sout(list);
      7. },String.valueOf(i)).start();
      8. }
      9. }

    ① 故障现象:java.util.ConcurrentModificationException(并发修改异常)
    ② 导致原因:并发争抢修改导致,一个正在写,另一个线程过来抢夺,导致数据不一致异常。并发生修改异常。
    ③ 解决方案:
    ④ 优化建议(同样的错误不犯第2次)

    • eg:开启多个线程操作List集合,向ArrayList中增加元素,同时去除元素。
      会出现以下几种情况:①Null②某些线程并未打印③数组下标越界异常
    • 2)解决非线程安全

      • 方案1:使用Vertor集合
        缺点:Vertor加锁可以保证数据一致性,但并发性低

        1. new Vector<>();
      • 方案2:使用Collections.synchronizedList
        Collection:集合接口
        Collections:集合接口辅助类
        缺点:

        1. Collections.synchronizedList(new ArrayList<>());
      • 方案3:使用JUC中的CopyOnWriteArrayList类替换。

        1. new CopyOnWriteArraylist<>();
    • 3)实现-Collections.synchronizedList实现

      • 初始化

        1. ArrayList arrayList = new ArrayList();
        2. List list2 = Collections.synchronizedList(arrayList);
      • add方法:通过关键字synchronized同步

        1. public void add(int index, E element) {
        2. synchronized (mutex) {list.add(index, element);}
        3. }
      • get方法:synchronized

        1. public V get(Object key) {
        2. synchronized (mutex) {return m.get(key);}
        3. }
    • 实现-②CopyonwriteArrayList(写时复制,读写分离的思想)

      • add:使用reentrantlock
      • Arrays.copyOf,扩容长度+1

        1. /** The lock protecting all mutators */
        2. transient final ReentrantLock lock = new ReentrantLock();
        3. /** The array, accessed only via getArray/setArray. */
        4. private volatile transient Object[] array;//保证了线程的可见性
        5. public boolean add(E e) {
        6. final ReentrantLock lock = this.lock;//ReentrantLock 保证了线程的可见性和顺序性,即保证了多线程安全。// 获取独占锁
        7. lock.lock();
        8. try {
        9. Object[] elements = getArray();
        10. int len = elements.length;
        11. Object[] newElements = Arrays.copyOf(elements, len + 1);//在原先数组基础之上新建长度+1的数组,并将原先数组当中的内容拷贝到新数组当中。
        12. newElements[len] = e;//设值
        13. setArray(newElements);//对新数组进行赋值
        14. return true;
        15. } finally {
        16. lock.unlock();
        17. }
        18. }
      • get:无锁

        1. public E get(int index) {
        2. return get(getArray(), index);
        3. }
  2. CopyOnWriteArrayList,咋实现线程安全的?

    • 当向容器添加或删除元素的时候,不直接往当前容器添加删除,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加删除元素,添加删除完元素之后,再将原容器的引用指向新的容器,整个过程加锁,保证了写的线程安全
    • 而因为写操作的时候不会对当前容器做任何处理,所以我们可以对容器进行并发的读,而不需要加锁,也就是读写分离。
    • 一般来讲我们使用时,会用一个线程向容器中添加元素,一个线程来读取元素,而读取的操作往往更加频繁。写操作加锁保证了线程安全,读写分离保证了读操作的效率,简直完美。

      并不是完全意义上的线程安全,如果涉及到remove操作,还会产生数组越界

    • 补充(sxt2):
      写时复制的概念:CopyOnWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行复制,复制出一个新的容器Object[] newElements,然后向新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

  3. Collections.synchronizedList和CopyOnWriteArrayList的异同点?
    1)同: 实现线程安全的列表方式
    2)异:

    • synchronizedList的add和get都是使用同步锁。
      读写比较均匀的并发场景。
      多线程下写性能比COWAL要好很多,而读采用了synchronized,读性能不如COWAL。
    • CopyOnWriteArrayList 的add使用可重入锁,get数据无锁
      读多写少的并发场景。写性能较差,而多线程的读性能较好。发生修改时候做copy,新老版本分离,保证读的高性能,适用于以读为主,读远远大于写的场景中使用,比如缓存。比如白名单,黑名单,商品类目的访问和更新场景。
      优点:可以进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
  4. CopyOnWriteArrayList写的时候读会读到空数据吗?

    • 读取操作没有任何同步控制和锁操作,理由就是内部数组array不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。
    • 添加集合的时候加了锁,保证了同步,避免了多线程写的时候会copy出多个副本出来。
ConcurrentHashMap->Map
CopyOnWriteArraySet
  1. Set非线程安全
    1) 线程安全问题
    故障现象:java.util.ConcurrentModificationException(并发修改异常)

    1. Collections.syschronizedSet(new hashSet<>());
    2. new CopyOnWriteArraySet<>(); //底层还是CopyOnWriteArrayList()方法实现的

ThreadLocal

ThreadLocal
ThreadLocalMap底层结构

---------------ab------------
5)哈希冲突

  1. private void set(ThreadLocal<?> key, Object value) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. int i = key.threadLocalHashCode & (len-1);
  5. for (Entry e = tab[i];
  6. e != null;
  7. e = tab[i = nextIndex(i, len)]) {
  8. ThreadLocal<?> k = e.get();
  9. if (k == key) {
  10. e.value = value;
  11. return;
  12. }
  13. if (k == null) {
  14. replaceStaleEntry(key, value, i);
  15. return;
  16. }
  17. }
  18. tab[i] = new Entry(key, value);
  19. int sz = ++size;
  20. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  21. rehash();
  22. }

--ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)。
--情况1:
如果当前位置是空的,就初始化一个Entry对象放在位置i上;

  1. if (k == null) {
  2. replaceStaleEntry(key, value, i);
  3. return;
  4. }

--情况2:
如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;

  1. if (k == key) {
  2. e.value = value;
  3. return;
  4. }

--情况3:
如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
在这里插入图片描述

阻塞队列Queue

ConcurrentLinkedQueue
  1. ConcurrentLinkedQueue
    • ConcurrentLinkedQueue这个队列使用链表作为其数据结构.ConcurrentLinkedQueue 应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现。
    • ConcurrentLinkedQueue 主要使用 CAS 非阻塞算法来实现线程安全。
    • ConcurrentLinkedQueue 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代。
BlockingQueue
  1. 阻塞队列知道吗?

    • 1> 队列+阻塞队列
      • 阻塞队列,顾名思义,首先是一个队列,而一个阻塞队列在数据结构中所起的作用大致如下图所示:
      • 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
        试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从列中移除一个或者多个元素或者完全清空队列后使队列重新变得空闲起来并后序新增。
        在这里插入图片描述
      • 当阻塞队列为空时,从队列中获取元素的操作将会被阻塞。
      • 当阻塞队列为满时,从队列里添加元素的操作将会被阻塞。
    • 2> 为什么用?有什么好处?
      • 在多线程领域:所谓阻塞,在某些情况下会挂起线程(阻塞),一旦条件满足,被挂起的线程又会自动被唤醒。
      • 为什么需要BlockingQueue
        好处是我们不需要关心什么时候需要阻塞线程,什么时候唤醒线程,因为这一切BlockingQueue都给你一手包办了。
    • 3> BlockingQueue的核心方法
      ① ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则进行排序。
      ② LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO(先进新出)排序元素,吞吐量通常要高于ArrayBlockingQueue。
      在这里插入图片描述
      ③ SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。
      -- SynchronousQueue没有容量
      -- 与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue
      -- 每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
    • 4> 架构梳理+种类分析
      1) 架构介绍
      COllection-Queue-BlockingQueue
      2) 种类分析
      -- ArrayBlockingQueue: 由数组结构组成的有界阻塞队列
      -- LinkedBlockingQueue: 由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列
      -- PriorityBlockingQueue: 支持优先级排序的无界阻塞队列。
      -- DelayQueue: 使用优先级队列实现的延迟无界阻塞队列。
      -- SynchronousQueue: 不存储元素的阻塞队列,也即单个元素的队列。
      -- LinkedTransferQueue: 由链表结构组成无界阻塞队列。
      -- LinkedBlockingDque: 由链表结构组成的双向阻塞队列。
    • 5> 用在哪里
      ① 生产者消费者模式
      1)传统版
    • 2.0版生产者消费者:sync、wait、notify => lock、await、singal
    • 多线程创判断while
      2)阻塞队列版
      代码44:https://blog.csdn.net/weixin_39879073/article/details/93379162
      ② 线程池
      ③ 消息中间件
  2. 如何设计一个消息队列

  3. 消息队列的作用

  4. 使用过哪些任务队列?
    1)线程池-ArrayBlockingQueue

单线程的实现

线程池

在这里插入图片描述

  1. 为什么用线程池,优势 (sxt2)

    • 预备知识
      例子:原(一个cpu):一个小丑玩4个球;现(多个cpu):4个小丑每人一个
      cpu核数:Runtime.getRuntime().avaliableProcessors()
      省略了上下文的切换
      (创建对象,仅仅是在JVM的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的API,然后操作系统要为线程分配一系列的资源,这个成本就很高了,所以线程是一个重量级的对象,应该避免频繁创建和销毁。)
    • 为什么 & 优势
      线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后再线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
      他的主要特点为:线程复用、控制最大并发数,管理线程。
      第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
      第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
      第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  2. JVM如何查看运行的线程数量?

  3. 怎么控制两个线程交替执行
    交替执行
  4. 银行转账多线程实现方法
  5. 谈谈你对多线程的理解?

1-Executor

Excutor源码

在这里插入图片描述

  1. public interface Executor { //顶级接口Executor,定义了线程执行的方法
  2. void execute(Runnable command);
  3. }
Future源码
使用
  1. 实现最优的“烧水泡茶”程序?

    • 数学家华罗庚先生的文章《统筹方法》,这篇文章里介绍了一个烧水泡茶的例子,文中提到最优的工序应该是下面这样:
      在这里插入图片描述
    • 用程序来模拟一下这个最优工序。前面曾经提到,并发编程可以总结为三个核心问题:分工、同步和互斥。
    • 编写并发程序,首先要做的就是分工.
      所谓分工指的是如何高效地拆解任务并分配给线程。对于烧水泡茶这个程序,一种最优的分工方案可以是下图所示的这样:用两个线程T1和T2来完成烧水泡茶程序,T1负责洗水壶、烧开水、泡茶这三道工序,T2负责洗茶壶、洗茶杯、拿茶叶三道工序,其中T1
      在执行泡茶这道工序时需要等待T2完成拿茶叶的工序。对于T1的这个等待动作,可以想出很多种办法,例如Thread.join()、CountDownLatch,甚至阻塞队列都可以解决,不过今天我们用Future特性来实现。
      在这里插入图片描述
    • 下面的示例代码就是用这一章提到的Future特性来实现的。
      首先,我们创建了两个FutureTask——ft1和ft2,ft1完成洗水壶、烧开水、泡茶的任务,ft2完成洗茶壶、洗茶杯、拿茶叶的任务;这里需要注意的是ft1这个任务在执行泡茶任务前,需要等待ft2把茶叶拿来,所以ft1内部需要引用ft2,并在执行泡茶之前,调用
      ft2的get()方法实现等待。
      1. // 创建任务T2的FutureTask
      2. FutureTask<String> ft2 = new FutureTask<>(new T2Task());
      3. // 创建任务T1的FutureTask
      4. FutureTask<String> ft1 = new FutureTask<>(new T1Task(ft2));
      5. // 线程T1执行任务ft1
      6. Thread T1 = new Thread(ft1);
      7. T1.start();
      8. // 线程T2执行任务ft2
      9. Thread T2 = new Thread(ft2);
      10. T2.start();
      11. // 等待线程T1执行结果
      12. System.out.println(ft1.get());
      13. // T1Task需要执行的任务:
      14. // 洗水壶、烧开水、泡茶
      15. class T1Task implements Callable<String>{
      16. FutureTask<String> ft2;
      17. // T1任务需要T2任务的FutureTask
      18. T1Task(FutureTask<String> ft2){
      19. this.ft2 = ft2;
      20. }
      21. @Override
      22. String call() throws Exception {
      23. System.out.println("T1:洗水壶...");
      24. TimeUnit.SECONDS.sleep(1);
      25. System.out.println("T1:烧开水...");
      26. TimeUnit.SECONDS.sleep(15);
      27. String tf = ft2.get(); // ★ 获取T2线程的茶叶
      28. System.out.println("T1:拿到茶叶:"+tf);
      29. System.out.println("T1:泡茶...");
      30. return "上茶:" + tf;
      31. }
      32. }
      33. // T2Task需要执行的任务:
      34. // 洗茶壶、洗茶杯、拿茶叶
      35. class T2Task implements Callable<String>{
      36. @Override
      37. String call() throws Exception{
      38. System.out.println("T2:洗茶壶...");
      39. TimeUnit.SECONDS.sleep(1);
      40. System.out.println("T2:洗茶杯...");
      41. TimeUnit.SECONDS.sleep(2);
      42. System.out.println("T2:拿茶叶...");
      43. TimeUnit.SECONDS.sleep(1);
      44. return "龙井";
      45. }
      46. }
      47. // 一次执行结果:
      48. T1:洗水壶...
      49. T2:洗茶壶...
      50. T1:烧开水...
      51. T2:洗茶杯...
      52. T2:拿茶叶...
      53. T1:拿到茶叶:龙井
      54. T1:泡茶...
      55. 上茶:龙井

2-ThreadPoolExecutor的理解

参数
  1. 线程池的参数解释

    • ThreadPoolExecutor类中提供的四个构造方法。
      -- 其余三个如下构造方法的基础上产生(默认某些参数,如默认拒绝策略)
      1. public ThreadPoolExecutor(int corePoolSize,
      2. int maximumPoolSize,
      3. long keepAliveTime,
      4. TimeUnit unit,
      5. BlockingQueue<Runnable> workQueue,
      6. ThreadFactory threadFactory,
      7. RejectedExecutionHandler handler) {
      8. if (corePoolSize < 0 ||
      9. maximumPoolSize <= 0 ||
      10. maximumPoolSize < corePoolSize ||
      11. keepAliveTime < 0)
      12. throw new IllegalArgumentException();
      13. if (workQueue == null || threadFactory == null || handler == null)
      14. throw new NullPointerException();
      15. this.acc = System.getSecurityManager() == null ?
      16. null :
      17. AccessController.getContext();
      18. this.corePoolSize = corePoolSize;
      19. this.maximumPoolSize = maximumPoolSize;
      20. this.workQueue = workQueue;
      21. this.keepAliveTime = unit.toNanos(keepAliveTime);
      22. this.threadFactory = threadFactory;
      23. this.handler = handler;
      24. }
  2. 7大参数(sxt2)
    1)corePoolSize:线程池中的常驻核心线程池数
    2)maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
    3)keepAliveTime:多余的空闲线程的存活时间
    当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余的空闲线程会被销毁直到只剩下corePoolSize个线程为止。
    (注,只有当线程池中的线程数大于corePoolSize时才会起作用,直到线程池中的线程数不大于corePoolSize)
    4)unit:keepAliveTime的单位。
    5)workQueue:任务队列,被提交但尚未执行的任务。(相当于候客区)
    用于保存任务的阻塞队列。可以使用ArrayBlockingQueue,LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue
    6)threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的的即可。executor 创建新线程的时候会用到。
    7)handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝

  3. 线程池的拒绝策略
    1) 是什么:等待队列也已经排满了,再也塞不下新任务了,同时,线程池中的max线程也达到了,无法继续为新任务服务。这时候就需要拒绝策略机制合理的处理这个问题。
    2) 场景:线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时。
    3) JDK内置的4种策略:
    ThreadPoolExecutor.AbortPolicy(默认):
    丢弃所提交的任务并抛出RejectedExecutionException异常组织系统正常运行。
    ThreadPoolExecutor.DiscardPolicy
    丢弃任务,不做任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
    ThreadPoolExecutor.DiscardOldestPolicy:
    丢弃队列最前面的(即队列中等待最久的任务)任务,然后把当前被拒绝的任务加入队列重新提交。
    ThreadPoolExecutor.CallerRunsPolicy:
    由调用线程(提交任务的线程)处理该任务。"调用者运行"的一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,

    • 默认的拒绝策略从而降低新任务的流量。
      4) 以上内置拒绝策略均实现了RejectedExecutionHanlder接口
创建
  1. 多线程的实现方法/方式? |2

    • 方式一:ThreadPoolExecutor构造函数实现
      在这里插入图片描述
    • 方式二:Executor框架的工具类Executors实现( 可以创建三种类型的 ThreadPoolExecutor):
      FixedThreadPool
      SingleThreadExecutor
      CachedThreadPool
      在这里插入图片描述
  2. 线程池用过吗?ThreadPoolExecutor谈谈你的理解? (sxt2)
    1) 创建线程的方式
    ① 通过继承Thread类,重写run方法;
    ② 通过实现runable接口;

    1. class MyThread implements Runnable{
    2. @Override
    3. public void run(){
    4. ...
    5. }
    6. }

    ③ 通过实现Callable接口; - 现在常用

    1. class MyThread implements Callable<Integer>{
    2. @Override
    3. public Interger call() throws Exception{
    4. sout("Callable 实现。。。");
    5. return null; // 如,return 1024;
    6. }
    7. }

    Runnable、Callable区别

    • Runnable没有返回值,Callable有返回值
    • Runnable不会抛异常,Callable会抛异常
    • Runnable生成run方法,Callable生成call方法
      创建
      1. public class CallableDemo{
      2. psvm{
      3. //FutureTask(Callable<V> callbel)
      4. FutureTask<Interger> futureTask = new FutureTask<>(new Mythread());
      5. Thread t1 = new Thread(futureTask,“线程名称”);
      6. t1.start();
      7. sout(futureTask.get()); // 获得 1024返回值
      8. }
      9. }

    分支合并(forkjoin

    1. public class CallableDemo{
    2. psvm{ //两个线程,一个main主线程,一个是AAfutureTask
    3. //FutureTask(Callable<V> callbel)
    4. FutureTask<Interger> futureTask = new FutureTask<>(new Mythread());
    5. Thread t1 = new Thread(futureTask,“线程名称”);
    6. t1.start(); // 可合并为:new Thread(futureTask,“AA”).start();
    7. // new Thread(futureTask,“AA”).start(); //共用一个futureTask只计算一次,可以再new
    8. int result01 = 100;
    9. //while(!futureTask.isDone()){ //如果没计算完,折中
    10. //}
    11. int result02 = futureTask.get(); // get()方法建议放在最后
    12. // 要求获得Callable线程的计算记过,如果没有计算完成就要去强求,会导致堵塞,直到计算完成。
    13. sout(result01 + result02); // 1124
    14. }
    15. }

    ④ 线程池

  3. ThreadPoolExecutor创建线程池

    • Runnable+ThreadPoolExecutor
      代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的5个任务会被放到等待队列中去。当前的5个任务中如果有任务被执行完了,线程池就会去拿新的任务执行。
      ① 创建一个 Runnable 接口的实现类
      1. /** 一个简单的Runnable类,需要大约5秒钟来执行其任务。*/
      2. public class MyRunnable implements Runnable {
      3. private String command;
      4. public MyRunnable(String s) {
      5. this.command = s;
      6. }
      7. @Override
      8. public void run() {
      9. System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
      10. processCommand();
      11. System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
      12. }
      13. private void processCommand() {
      14. try {
      15. Thread.sleep(5000);
      16. } catch (InterruptedException e) {
      17. e.printStackTrace();
      18. }
      19. }
      20. @Override
      21. public String toString() {
      22. return this.command;
      23. }
      24. }

    ② 测试程序,ThreadPoolExecutor 构造函数自定义参数创建线程池。

    1. public class ThreadPoolExecutorDemo {
    2. private static final int CORE_POOL_SIZE = 5;
    3. private static final int MAX_POOL_SIZE = 10;
    4. private static final int QUEUE_CAPACITY = 100;
    5. private static final Long KEEP_ALIVE_TIME = 1L;
    6. public static void main(String[] args) {
    7. //通过ThreadPoolExecutor构造函数自定义参数创建
    8. ThreadPoolExecutor executor = new ThreadPoolExecutor(
    9. CORE_POOL_SIZE,
    10. MAX_POOL_SIZE,
    11. KEEP_ALIVE_TIME,
    12. TimeUnit.SECONDS,
    13. new ArrayBlockingQueue<>(QUEUE_CAPACITY),
    14. new ThreadPoolExecutor.CallerRunsPolicy());
    15. // 创建10个
    16. for (int i = 0; i < 10; i++) {
    17. //创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
    18. Runnable worker = new MyRunnable("" + i);
    19. //执行Runnable
    20. executor.execute(worker);
    21. }
    22. //终止线程池
    23. executor.shutdown();
    24. while (!executor.isTerminated()) {
    25. }
    26. System.out.println("Finished all threads");
    27. }
    28. }
    • Callable+ThreadPoolExecutor
      1. /** ① MyCallable.java */
      2. public class MyCallable implements Callable<String> {
      3. @Override
      4. public String call() throws Exception {
      5. Thread.sleep(1000);
      6. //返回执行当前 Callable 的线程名字
      7. return Thread.currentThread().getName();
      8. }
      9. }
      10. /** ② CallableDemo.java */
      11. public class CallableDemo {
      12. public static void main(String[] args) {
      13. //通过ThreadPoolExecutor构造函数自定义参数创建
      14. ThreadPoolExecutor executor = new ThreadPoolExecutor(
      15. 5,10,1L,
      16. TimeUnit.SECONDS,
      17. new ArrayBlockingQueue<>(100),
      18. new ThreadPoolExecutor.CallerRunsPolicy());
      19. List<Future<String>> futureList = new ArrayList<>();
      20. Callable<String> callable = new MyCallable();
      21. for (int i = 0; i < 10; i++) {
      22. //提交任务到线程池
      23. Future<String> future = executor.submit(callable);
      24. //将返回值 future 添加到 list,我们可以通过 future 获得执行 Callable 得到的返回值
      25. futureList.add(future);
      26. }
      27. for (Future<String> fut : futureList) {
      28. try {
      29. System.out.println(new Date() + "::" + fut.get());
      30. } catch (InterruptedException | ExecutionException e) {
      31. e.printStackTrace();
      32. }
      33. }
      34. //关闭线程池
      35. executor.shutdown();
      36. }
      37. }
  4. 区别

    • execute() vs submit()
      execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
      submit()方法用于提交需要返回值的任务。线程池会返回一个 Future类型的对象,通过这个 Future 对象可以判断任务是否执行成功 ,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
      -- submit():
      AbstractExecutorService接口中的一个submit 方法为例子来看看源代码:
      1. public Future<?> submit(Runnable task) {
      2. if (task == null) throw new NullPointerException();
      3. RunnableFuture<Void> ftask = newTaskFor(task, null);
      4. execute(ftask);
      5. return ftask;
      6. }
      7. // 上面方法调用的 newTaskFor 方法返回了一个 FutureTask 对象。
      8. protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
      9. return new FutureTask<T>(runnable, value);
      10. }

    -- execute()方法:

    1. public void execute(Runnable command) {
    2. ...
    3. }
    • isTerminated() VS isShutdown()
      isShutDown 当调用 shutdown() 方法后返回为 true。
      isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true
  5. 多线程相关:如何停止线程?

    • 关闭线程池,可以通过shutdown和shutdownNow这两个方法。它们的原理都是遍历线程池中所有的线程,然后依次中断线程。shutdown和shutdownNow还是有不一样的地方:
      -- shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程,队列里的任务会执行完毕。
      -- shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表;
    • 看出 shutdown 方法会将正在执行的任务继续执行完,而 shutdownNow 会直接中断正在执行的任务。调用了这两个方法的任意一个,isShutdown方法都会返回 true,当所有的线程都关闭成功,才表示线程池成功关闭,这时调用isTerminated方法才会返回 true。
原理

|源码|

  1. 完整的线程池执行的流程/任务提交流程?
    当一个并发任务提交给线程池,线程池分配线程去执行任务的过程:
    在这里插入图片描述
    1) 先判断线程池中核心线程池所有的线程是否都在执行任务。如果不是,则新创建一个线程执行刚提交的任务,否则,核心线程池中所有的线程都在执行任务,则进入第 2 步;
    2) 判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第 3 步;
    3) 判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则,则交给饱和策略进行处理
    https://juejin.im/post/5aeec0106fb9a07ab379574f

  2. 说说线程池的底层工作原理?(sxt2)

    • 图为ThreadPoolExecutor的execute方法的执行示意图:
      在这里插入图片描述
      1) 在创建了线程池后,等待提交过来的任务请求
      2) 当调用execute()方法添加一个请求任务时,线程池会做如下判断
      2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
      2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
      2.3 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
      2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
      3) 当一个线程完成任务时,它会从队列中取下一个任务来执行
      4) 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断
      4.1 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
      4.2 所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小
    • execute方法源码
      RunnableDemo中使用 executor.execute(worker)来提交一个任务到线程池中

      1. // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
      2. private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
      3. private static int workerCountOf(int c) {
      4. return c & CAPACITY;
      5. }
      6. //任务队列
      7. private final BlockingQueue<Runnable> workQueue;
      8. public void execute(Runnable command) {
      9. // 如果任务为null,则抛出异常。
      10. if (command == null)
      11. throw new NullPointerException();
      12. // ctl 中保存的线程池当前的一些状态信息
      13. int c = ctl.get();
      14. // 下面会涉及到 3 步 操作
      15. // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
      16. // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
      17. if (workerCountOf(c) < corePoolSize) {
      18. if (addWorker(command, true))
      19. return;
      20. c = ctl.get();
      21. }
      22. // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
      23. // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
      24. if (isRunning(c) && workQueue.offer(command)) {
      25. int recheck = ctl.get();
      26. // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
      27. if (!isRunning(recheck) && remove(command))
      28. reject(command);
      29. // 如果当前线程池为空就新创建一个线程并执行。
      30. else if (workerCountOf(recheck) == 0)
      31. addWorker(null, false);
      32. }
      33. //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
      34. //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
      35. else if (!addWorker(command, false))
      36. reject(command);
      37. }
    • addWorker方法源码 这个方法主要用来创建新的工作线程,如果返回true说明创建和启动工作线程成功,否则的话返回的就是false。

      1. // 全局锁,并发操作必备
      2. private final ReentrantLock mainLock = new ReentrantLock();
      3. // 跟踪线程池的最大大小,只有在持有全局锁mainLock的前提下才能访问此集合
      4. private int largestPoolSize;
      5. // 工作线程集合,存放线程池中所有的(活跃的)工作线程,只有在持有全局锁mainLock的前提下才能访问此集合
      6. private final HashSet<Worker> workers = new HashSet<>();
      7. //获取线程池状态
      8. private static int runStateOf(int c) { return c & ~CAPACITY; }
      9. //判断线程池的状态是否为 Running
      10. private static boolean isRunning(int c) {
      11. return c < SHUTDOWN;
      12. }
      13. /**
      14. * 添加新的工作线程到线程池
      15. * @param firstTask 要执行
      16. * @param core参数为true的话表示使用线程池的基本大小,为false使用线程池最大大小
      17. * @return 添加成功就返回true否则返回false
      18. */
      19. private boolean addWorker(Runnable firstTask, boolean core) {
      20. retry:
      21. for (;;) {
      22. //这两句用来获取线程池的状态
      23. int c = ctl.get();
      24. int rs = runStateOf(c);
      25. // Check if queue empty only if necessary.
      26. if (rs >= SHUTDOWN &&
      27. ! (rs == SHUTDOWN &&
      28. firstTask == null &&
      29. ! workQueue.isEmpty()))
      30. return false;
      31. for (;;) {
      32. //获取线程池中线程的数量
      33. int wc = workerCountOf(c);
      34. // core参数为true的话表明队列也满了,线程池大小变为 maximumPoolSize
      35. if (wc >= CAPACITY ||
      36. wc >= (core ? corePoolSize : maximumPoolSize))
      37. return false;
      38. //原子操作将workcount的数量加1
      39. if (compareAndIncrementWorkerCount(c))
      40. break retry;
      41. // 如果线程的状态改变了就再次执行上述操作
      42. c = ctl.get();
      43. if (runStateOf(c) != rs)
      44. continue retry;
      45. // else CAS failed due to workerCount change; retry inner loop
      46. }
      47. }
      48. // 标记工作线程是否启动成功
      49. boolean workerStarted = false;
      50. // 标记工作线程是否创建成功
      51. boolean workerAdded = false;
      52. Worker w = null;
      53. try {
      54. w = new Worker(firstTask);
      55. final Thread t = w.thread;
      56. if (t != null) {
      57. // 加锁
      58. final ReentrantLock mainLock = this.mainLock;
      59. mainLock.lock();
      60. try {
      61. //获取线程池状态
      62. int rs = runStateOf(ctl.get());
      63. //rs < SHUTDOWN 如果线程池状态依然为RUNNING,并且线程的状态是存活的话,就会将工作线程添加到工作线程集合中
      64. //(rs=SHUTDOWN && firstTask == null)如果线程池状态小于STOP,也就是RUNNING或者SHUTDOWN状态下,同时传入的任务实例firstTask为null,则需要添加到工作线程集合和启动新的Worker
      65. // firstTask == null证明只新建线程而不执行任务
      66. if (rs < SHUTDOWN ||
      67. (rs == SHUTDOWN && firstTask == null)) {
      68. if (t.isAlive()) // precheck that t is startable
      69. throw new IllegalThreadStateException();
      70. workers.add(w);
      71. //更新当前工作线程的最大容量
      72. int s = workers.size();
      73. if (s > largestPoolSize)
      74. largestPoolSize = s;
      75. // 工作线程是否启动成功
      76. workerAdded = true;
      77. }
      78. } finally {
      79. // 释放锁
      80. mainLock.unlock();
      81. }
      82. //// 如果成功添加工作线程,则调用Worker内部的线程实例t的Thread#start()方法启动真实的线程实例
      83. if (workerAdded) {
      84. t.start();
      85. /// 标记线程启动成功
      86. workerStarted = true;
      87. }
      88. }
      89. } finally {
      90. // 线程启动失败,需要从工作线程中移除对应的Worker
      91. if (! workerStarted)
      92. addWorkerFailed(w);
      93. }
      94. return workerStarted;
      95. }
  3. 线程池怎么保证线程一直运行的?

    • 阻塞
    • runWorker()->getTask()中:
      1. Runnable r = timed ?
      2. workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
      3. workQueue.take();
      4. if (r != null)
      5. return r;

    --从阻塞任务队列中取任务,如果设置了allowCoreThreadTimeOut(true) 或者当前运行的任务数大于设置的核心线程数,那么timed =true 。此时将使用workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)从任务队列中取任务,而如果没有设置,那么使用workQueue.take();取任务,对于阻塞队列,poll(long timeout, TimeUnit unit) 将会在规定的时间内去任务,如果没取到就返回null。take()会一直阻塞,等待任务的添加。线程池能够一直等待任务的执行而不被销毁了,其实也就是进入了阻塞状态而已。

    • 线程池当未调用 shutdown 方法时,是通过队列的 take 方法(workQueue.take();)阻塞核心线程(Worker)的 run方法从而保证核心线程不被销毁的。队列中 take 方法的含义是当队列有任务时,立即返回队首任务,没有任务时则一直阻塞当前线程,直到有新任务才返回。
      https://blog.csdn.net/smile_from_2015/article/details/105259789
种类
  1. Execuors类实现的几种线程池类型,最后如何返回?
    ① newFixedThreadPool创建一个固定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    ② newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    ③ newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
    ④ newScheduledThreadPool 创建一个固定长度线程池,支持定时及周期性任务执行。
    在这里插入图片描述

  2. 线程池如何使用? (sxt2)
    1) 架构说明
    Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors(辅助工具类,如Arrays),ExecutorService,ThreadPoolExecutor(线程池的底层)这几个类。
    在这里插入图片描述
    2) 编码实现(共5种线程池)-第4中获得/使用Java多线程的方式,线程池
    ① 了解
    -- Executors.newScheduledThreadPool()
    池中任务每2'执行一次
    -- Java8新出 Executors.newWorkStealingPool(int)
    使用目前机器上可用的处理器作为它的并行级别(用的少,面试不怎么考)
    ② 重点

    1. public interface List<E> extends Collection<E> {
    2. public interface ExecutorService extends Executor {
    3. // 使用
    4. public class MyThreadPoolDemo{
    5. public static void main(String[] args) {
    6. ExecutorService threadPool = Executors.newFixedThreadPool(5);//一池5个处理线程
    7. // ExecutorService threadPool = Executors.newSingleThreadExecutor();//一池1个处理线程
    8. // ExecutorService threadPool = Executors.newCachedThreadPool();//一池N个处理线程
    9. // 模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
    10. try {
    11. for (int i = 0; i <10 ; i++) { //10个请求
    12. threadPool.execute(()->{ //Lambda
    13. System.out.println(Thread.currentThread().getName()+"\t 办理业务");
    14. });
    15. // TimeUnit.SECONDS.sleep(1);
    16. }
    17. }catch (Exception e){
    18. e.printStackTrace();
    19. }finally {
    20. threadPool.shutdown();//释放
    21. }
    22. }
    23. }
  3. ① Executors.newFixedThreadPool(int)
    1)创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    2)newFixedThreadPool创建线程池CorePoolSize和maximumPoolSize值是相等的,使用的LinkedBlockingQueue。
    适用:执行长期的任务,性能好很多。

    1. public static ExecutorService newFixedThreadPool(int nThreads) {
    2. return new ThreadPoolExecutor(nThreads, nThreads,
    3. 0L, TimeUnit.MILLISECONDS,
    4. new LinkedBlockingQueue<Runnable>());
    5. }
    • execute() 方法运行示意图(该图片来源:《Java 并发编程的艺术》):
      在这里插入图片描述
      1) 如果当前运行的线程数小于 corePoolSize,
      2) 如果再来新任务的话,就创建新的线程来执行任务;
      3) 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 LinkedBlockingQueue;
      4)线程池中的线程执行完手头的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行;
    • 为什么不推荐使用?
      FixedThreadPool 使用无界队列 LinkedBlockingQueue(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :
      -- 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;
      -- 由于使用无界队列时 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool的源码可以看出创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置为同一个值。
      -- 由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数;
      -- 运行中的 FixedThreadPool(未执行 shutdown()或 shutdownNow())不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。
  4. ② Executors.newSingleThreadExecutor()
    1> 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定的顺序执行。
    2> newSingleThreadExecutor将CorePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue。
    适用:一个任务一个任务执行的场景。

    1. public static ExecutorService newSingleThreadExecutor() {
    2. return new FinalizableDelegatedExecutorService
    3. (new ThreadPoolExecutor(1, 1,
    4. 0L, TimeUnit.MILLISECONDS,
    5. new LinkedBlockingQueue<Runnable>()));
    6. }
    • 运行示意图(该图片来源:《Java 并发编程的艺术》):
      在这里插入图片描述
      1) 如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务;
      2) 当前线程池中有一个运行的线程后,将任务加入 LinkedBlockingQueue
      3) 线程执行完当前的任务后,会在循环中反复从LinkedBlockingQueue 中获取任务来执行;
    • 为什么不推荐使用?
      无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Intger.MAX_VALUE)。SingleThreadExecutor 使用无界队列作为线程池的工作队列会对线程池带来的影响与FixedThreadPool相同。说简单点就是可能会导致 OOM,
  5. 单线程线程池newSingleThreadExecutor的应用场景
    适用:一个任务一个任务执行的场景

  6. ③ Executors.newCachedThreadPool()
    1、创建一个可缓存线程池,如果线程池长度超过处理需求,可灵活回收线程池,若无可回收,则新建线程池。
    2、将CorePoolSize设置为0,将maximumPoolSize设置为Interger.MAX_VALUE,即无界的,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
    如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。
    适用:执行很多短期异步的小程序或者负载较轻的服务器。

    1. public static ExecutorService newCachedThreadPool() {
    2. return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
    3. 60L, TimeUnit.SECONDS,
    4. new SynchronousQueue<Runnable>());
    5. }

    在这里插入图片描述
    1> 首先执行 SynchronousQueue.offer(Runnable task) 提交任务到任务队列。如果当前 maximumPool中有闲线程正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则执行下面的步骤 2;
    2> 当初始 maximumPool 为空,或者 maximumPool 中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1 将失败,此时 CachedThreadPool 会创建新线程执行任务,execute 方法执行完成;

    • 为什么不推荐使用?
      CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致 OOM。
  7. 线程池 newCachedThreadPool线程池的缺点?配置参数?
    如上

  8. ④ Executors.ScheduledThreadPool()

    • 主要用来在给定的延迟后运行任务,或者定期执行任务。
    • 实际项目中基本不会被用到,因为有其他方案选择比如quartz。
      备注: Quartz 是一个由 java 编写的任务调度库,由 OpenSymphony 组织开源出来。在实际项目开发中使用 Quartz 的还是居多,比较推荐使用 Quartz。因为 Quartz 理论上能够同时对上万个任务进行调度,拥有丰富的功能特性,包括任务调度、任务持久化、可集群化、插件等等。

  1. 线程池的好处
    加快响应速度
    合理利用CPU和内存
    统一管理

  2. 线程池适应应用的场合
    -- 服务器接受到大量请求时,使用线程池技术时非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率
    --实际上,在开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理。

  3. 3种常见的队列类型
    1)直接交换:SynchronousQueue
    2)无界队列:LinkedBlockingQueue
    3)有界队列:ArrayBlockingQueue

  4. 线程池里的线程数量设定为多少比较合适?
    -- CPU密集型(加密、计算hash等):最佳线程数为CPU核心数的1-2倍左右。
    -- 耗时IO型(读写数据库、文件、网络读写等):最佳线程数一般会大于cpu核心数很多倍,以JVM线程监控显示繁忙情况为依据,保证线程空闲可以衔接上,参考Brain Goetz推荐的计算方法:线程数=CPU核心数*(1+平均等待时间/平均工作时间)
    更详细可以进行压测

3-生产使用

|设计线程池|
4. java线程你是怎么使用的?
4. 线程池用的多吗?让你设计一个线程池如何设计
5. 如何构造线程池,它的参数,饱和策略?

你如何设置合理参数
  1. 你再工作中单一的/固定的/可变的三种创建线程池的方法,你用的哪个多?超级大坑(sxt2)
    1) 正确答案:一个都不用,我们生产上只使用自定义的。
    2) Executors中JDK已经给你提供了,为什么不用?

    • 阿里巴巴开发手册-并发处理
      【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
      说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
      【强制】线程池不允许使用 Executors去创建,而是通过 ThreadPoolExecutor的方式,这样的处理方式让写同学更加明确线程池运行规则,避资源耗尽风险。
      说明: Executors返回的线程池对象返回的线程池对象的弊端 如下 : 1)FixedThreadPool和 SingleThread允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2)CachedThreadPool和 ScheduledThreadPool允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
    • 无界队列,导致OOM
  2. 工作中如何使用线程池的,是否子定义过线程池的使用?(sxt2)

    1. public static void main(String[] args) {
    2. ExecutorService threadPool = new ThreadPoolExecutor(
    3. 2, //corePoolSize
    4. 5,//maximumPoolSize
    5. 1L,//keepAliveTime
    6. TimeUnit.SECONDS,
    7. new LinkedBlockingDeque<Runnable>(3),
    8. Executors.defaultThreadFactory(),
    9. new ThreadPoolExecutor.AbortPolicy());
    10. // 银行开启最大8个窗口
    11. try {
    12. for (int i = 0; i <10 ; i++) { //10个请求
    13. threadPool.execute(()->{ //Lambda
    14. System.out.println(Thread.currentThread().getName()+"\t 办理业务");
    15. });
    16. }
    17. }catch (Exception e){
    18. e.printStackTrace();
    19. }finally {
    20. threadPool.shutdown();
    21. }
    22. }
  3. 合理配置线程池你是如何考虑的(sxt2)
    1) 决定核心线程数两个方面:CUP密集型、IO密集型

    1. //第一步:先获取运行服务器是几核的
    2. System.out.println(Runtime.getRuntime().availableProcessors());

    2) CPU密集

    • CPU密集指该任务需要大量的运算,而没有阻塞,CPU一直在全速运行
    • CPU密集任务只有在真正的多核CPU上才能得到加速(通过线程)
    • 而在单核CPU上(基本没了),无论你开几个模拟多线程该任务都不可能得到加速,因为CPU总的运算能力就这些
    • CPU密集型任务配置金肯呢个少的线程数量:CPU核数+1个线程的线程池

    3) IO密集型(2种,第1种常被讲,实际应用看效果)

    • (1)由于IO密集型任务线程并不是一直在执行任务,则配置尽可能多的线程,如CPU核数*2
    • (2)IO密集型,即该任务需要大量IO,即有大量的阻塞
    • 在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
    • 索引在IO密集型任务中使用多线程可以大大的加速程序运行,及时在单核CPU上,这种加速主要就是利用了被浪费带哦的阻塞时间。
    • IO密集型时,大部分线程都阻塞,故需要多配置线程数:
    • 公式参考:CPU核数/1-阻塞系数 阻塞系数在0.8-0.9之间 ,可取0.9
    • 如8核CPU 8/1-0.9=80个线程数
  4. corepoolsize和CPU有什么关系,为什么书上推荐是N+1,线程池适合计算密集型还是IO密集型

    • 如果任务是IO密集型,一般线程数需要设置2倍CPU数以上,以此来尽量利用CPU资源。
    • 如果任务是CPU密集型,一般线程数量只需要设置CPU数加1即可,更多的线程数也只能增加上下文切换,不能增加CPU利用率。
      IO密集型的任务,因为IO操作并不占用CPU,可以加大线程池中的线程数目,让CPU处理更多的业务
      CPU密集型任务,线程池中的线程数设置得少一些,减少线程上下文的切换。
      https://www.cnblogs.com/weigy/p/12667425.html
    • N+1:N表示N个cpu处理器,当计算密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保CPU的时钟周期不会被浪费。-p141
      任务性质不同的任务可以用不同规模的线程池分开处理。
    • CPU 密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。
    • IO 密集型任务则由于需要等待 IO 操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu。
    • 混合型的任务,如果可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。
    • 可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的 CPU 个数。
      https://juejin.im/post/5aeec0106fb9a07ab379574f
      https://www.cnblogs.com/weigy/p/12667425.html

4-skynet使用

1-创建设置
  1. public class ThreadPoolUtil {
  2. private static int corePoolSize = 4;
  3. private static int maximumPoolSize = 32;
  4. private static long keepAliveTime = 60;
  5. private static TimeUnit unit = TimeUnit.SECONDS;
  6. private static int maximumTask = 5000;
  7. private static AtomicInteger tid = new AtomicInteger();
  8. private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
  9. unit, new ArrayBlockingQueue<>(maximumTask), r -> new Thread(r,"skynet-pool-" + tid.incrementAndGet()), new ThreadPoolExecutor.AbortPolicy());
  10. public static Future<?> submit(Runnable r) {
  11. return executor.submit(r);
  12. }
  13. public static <T> Future<T> submit(Callable<T> task) {
  14. return executor.submit(task);
  15. }
  16. public static void main(String[] args) throws InterruptedException {
  17. Runnable runnable = new Runnable() {
  18. @Override
  19. public void run() {
  20. System.out.println(1000);
  21. }
  22. };
  23. submit(runnable);
  24. TimeUnit.SECONDS.sleep(3);
  25. }
  26. }
2-使用

代码段2:

  1. //不阻塞返回回调结果
  2. ThreadPoolUtil.submit(() -> {
  3. //2. 解析data
  4. // 找到该任务id 更新查询状态
  5. //3.推送消息
  6. // 创建任务相关消息
  7. // 保存到消息表
  8. // 监听接口:通过套接字、监听消息异步通知(发布到redis)
  9. String topic = RedisWsMessageListener.TOPIC_SKYNET_WS_MSG;
  10. String msg = JSON.toJSONString(message);
  11. log.info("推送的消息msg:{},写入topic:{},", msg, topic);
  12. //发布到redis
  13. redisTemplate.convertAndSend(topic, msg);
  14. }
  15. });

3-jvm

笔记_S
21.运行时数据区

私有:程序计数器 Java虚拟机栈 本地方法栈
共享:堆 方法区

** 类加载
作用:
-加载类信息 放在 方法区

加载->连接(验证->准备->解析)->初始化
-加载:
类的全限定名->二进制字节流
字节流的静态存储结构->方法区的运行时数据结构
生成.Class对象->各种数据访问入口
-连接:
-验证:
符合要求,文件格式、元数据、字节码、符号引用
-准备:
类变量-方法区(除实例变量-堆)
分配内存与初始值
-解析
符号引用->直接引用
针对:类/接口、字段、类方法、接口方法、方法类型等(在常量池中)
-初始化
执行类构造器方法()过程

** 类加载器
模型:
启动类加载器/引导类
扩展类
应用程序/系统类
自定义

向父类委托
启动类加载
父类无法完成,子类加载

** 运行时数据区
方法区:
类信息(类加载的)
运行时常量池信息
字符串字面量和数字常量(class文件中常量池部分的内存映射)
(? 即时编译器编译后的代码缓存等)

程序计数器
程序控制流指示器,是个计数器
作用:存储下一条指令的地址
每个线程都有自己的
给字节码解释器提供下一条执行指令
唯一无oom
存储反编译后的指令地址(理解:类似于行号),对应操作指令

Java栈:
线程创建->栈创建,内部保存栈帧
一个方法对应一个栈帧的入栈和出栈
包含方法的局部变量(8基本数据类型,对象的引用地址)、部分结果
-Xss
-Xss1024k 最大256kb
方法两种返回/栈帧弹出 return和异常
栈帧->基本单位存储,存储方法执行的各种数据

-结构:
  -局部变量表:
  存方法参数和方法体中的局部变量
  含基本数据类型、对象引用、返回值类型
  方法执行时,jvm使用局部变量表->参数值到参数变量列表的传递
  存储单位slot(变量槽)
  32位以内的类型只占用一个slot(包括 引用类型、returnAddress类型),64位的类型(long和double)占用两个slot
  构造器、实例方法中,对象引用this 都会存放在索引为0的位置
  slot重复使用

 -操作栈:
  变量临时存储空间
  后进先出,由字节码指令(pc计数器),进出数据
 -动态连接(或指向运行时常量池的方法引用)
 栈帧->包含指向运行时常量池中该栈帧所属方法的引用
 Java源文件->编译->字节码文件,变量和方法引用->作为符号引用->存在class文件的常量池里
 符号引用=>直接引用:通过静态链接|动态链接

-方法返回地址:
存 调用该方法的pc寄存器的值
-一些附加信息
如,对程序调试提供支持的信息

本地方法栈:
native
作用:管理本地方法的调用

堆:
还可以划分线程私有的缓冲区(TLAB)
存储:对象和数组
几乎所有对象实例?有一些对象在栈上分配(逃逸分析、标量替换)
-Xms10m -Xmx10m 堆内存
堆:新生区、老年区、永久区(1.8元空间)
新生-Eden、survivor
-Xms 初始 = -XX:InitialHeapSize
-Xmx 最大 = -XX:MaxHeapSize
老年/新生 占比 -XX:NewRatio = 4
Eden/survivor -XX:SurvivorRatio

Eden 满了MinorGC ,survivor大对象进入老年代
年龄:-XX:MaxTenuringThreshold=N
养老区内存不足 MajorGC 依旧 OOM

部分收集Partial GC
-新生代 MinorGC/YoungGC
-- 会发生STW
-老年代 MajorGC/OldGC -- CMS
--STW更久,最少伴随一次Minor
-混合收集 MixedGC
-- 整个新生代和部分老年代--只有G1
整堆收集FullGC 整Java堆和方法区
--触发:① System.gc() ② 老年代空间不足③ 方法区空间不足④Minor后进入老年代的avg大于年老代可用内存⑤to过小,对象进入老年代,但老年代空间不足
优化:
避免FullGC,缩短STW
分->优化GC性能

** 内存分配策略/对象提升(promotion)规则
-不同年龄对象分配原则
-优先Eden
-大对象 老年代
-长期存活 老年代
-survivor同年纪和大于survivor一半
-空间分配担保 Minor后,survivor无法容纳,进入老年代 -XX:HandlePromotionFailure 是否允许担保

** TLAB
-堆中,每个线程独占,在Eden,线程安全,提升内存分配吞吐量
-XX:TLABWasteTargetPercent 占Eden的百分比

** 逃逸分析技术
-对象逃逸方法失败,栈上分配 无需回收
-标量-无法分解的更小数据,如Java中的原始类型
-逃逸分析,对象不会被外界分配,JIT优化,将对象拆解成若干变量过程,标量替换-好处,不需要分配内存了,减少堆内存占用
-默认打开 -XX:+ElimilnateAllocations
-Hotspot 标量替换 实现 逃逸分析

** 方法区
主要是Class

Person person = new Person();
方法区 Java栈 Java堆
.class
Person 类的 .class 信息存放在方法区中
person 变量存放在 Java 栈的局部变量表中
真正的 person 对象存放在 Java 堆中

堆的逻辑部分,独立于堆的内存空间
类只加载一次
- OOM:定义太多类,方法区溢出
eg:加载大量三方jar包|tomcat部署工程较多(30-50)|大量动态的生成反射类
- 演进:
永久代:更易导致Java程序oom(超过-XX:MaxPermsize上限)
元空间永久代区别:元空间不在虚拟机设置的内存中,使用本地内存
-大小
JDK7永久代:
-XX:Permsize 初始分配空间 mr:20.75M
-XX:MaxPermsize 最大可分配空间 32位机器64M,64位-82M
JDK8元空间:
-XX:MetaspaceSize mr:win 21M
超过 FullGC触发并卸载没用的类(这些类对应类加载器不再存活) 值重置 新界限
-XX:MaxMetaspaceSize mr:-1 无限制
查看:
jinfo -flag MetaspaceSize PID
jinfo -flag MaxMetaspaceSize PID
弊端:mr虚拟机会耗尽所有可用系统内存
- 解决oom
-通过内存映像分析工具对dump的堆转储快照进行分析
-内存中的对象是否必要?即区分内存泄漏还是内存溢出
内存泄漏:大量引用指向某些不会使用的对象,这些对象还和GCROOT关联不会被回收
- 工具查看泄漏对象到GCROOT的引用链。查看为什么不会回收,类信息,找到泄漏代码位置
内存溢出:内存中的对象还都必须存活
-检查虚拟机堆参数(-Xmx与-Xms),调整大小,检查某些对象生命周期过长?持有状态时间过长?减少程序运行期内的内存消耗

方法区内部结构:
-存储内容:
已被jvm加载的类型信息
常量
静态变量
即时编译器编译后的代码缓存等

-结构
类型信息、运行时常量池、静态变量、JIT代码缓存、域信息、方法信息
-- 类型信息:
-- 域信息
-- 方法信息
-- 运行时常量池
https://www.cnblogs.com/tiancai/p/9321338.html
方法区-运行时常量池
Class字节码文件-常量池
- 常量池:
字节码文件包含:类的版本、字段、方法、接口等描述符信息、
及常量池(各种字面量和对类型、域、方法的符号引用)
字面量:文本字符串
被声明为final的常量值
基本数据类型的值
其他
符号引用:类和结构的完全限定名
字段名称和描述符
方法名称和描述符
- 为什么用它
不使用常量池,类信息、方法信息等要记录在当前字节码文件,文件过大,需要的结构信息记录在常量池,通过引用的方式加载、调用所需结构
- 有什么?
数量值、字符串值、类引用、字段引用、方法引用
- 常量池,可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

- 运行时常量池:
方法区的一部分
常量池表-Class字节码文件的一部分,存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后放在方法区的运行时常量池中

演进:
1.7 8 6 运行时数据区的变动
1.6 永久区->方法区 静态变量
1.7 去永久区->方法区 字符串常量池、静态变量 放进了堆
1.8 元空间 ->方法区 存放类信息
字符串常量池、静态变量 还在堆
- jdk6
方法区(永久代):
类型信息、域信息、方法信息
JIT代码缓存、静态变量
运行时常量池[字符串常量池StringTable]
- jdk7:
类型信息、域信息、方法信息
JIT代码缓存
运行时常量池
堆:静态变量、StringTable
- jdk8:
无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。

为什么使用元空间?
- 虚拟机融合
- 为永久代设置空间大小很难确定
动态加载类过多,OOM
- 对永久代调优困难
方法区的回收:常量池废弃的常量、不再用的类型
调优 为降低FullGC
方法区回收效果难以满意,尤其是类型的卸载、条件苛刻

为什么移动字符串常量池?
- 永久代回收效率低,FullGC触发 老年代空间不足、永久代不足
- 开发中大量字符串被创建,回收效率低,会导致永久代内存不足,放在堆里,能及时回收内存

静态变量放在哪?
-6 7 永久代 8 堆
- 静态变量对应的对象实体使用存在堆空间(只要是对象实例必然会在Java堆中分配)
方法区类回收?
总结?
MinorGC 新生区
MajorGC 老年区
FullGC 整个堆和方法区

** 对象
创建对象?
- new
- clone()
-

步骤?
1判断对象对应的类是否加载、连接、初始化
-new指令
-检查指令参数能否在元空间的常量池中定位到一个类的符号引用,
-检查这个符号引用代表的类是否被加载、解析、初始化 即类元数据是否存在
-未加载,双亲委派模式下,类加载器以ClassLoader+包名+类名为key查找.class文件,未找到,异常,找到,类加载
2为对象分配内存
-内存规整 -> 指针碰撞
-不规整 -> 空闲列表分配
指针碰撞:
用过的一边,空闲的一边,中间指针为分界点指示器,挪动对象大小
空闲列表:
jvm维护列表,记录哪些可用,给对象分配足够空间,更新表
-堆规整?->由采用的垃圾收集器是否带有压缩功能决定,如标记清除 会有很多内存碎片
3处理并发安全问题
-cas+重试失败、区域加锁保证更新的原子性
-每个线程预先分配TLAB -XX:+/-UseTLAB参数设置(区域加锁机制)
-Eden区给给个线程分配一块区域
4初始化分配到的空间
所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
5设置对象的对象头
-对象所属类(即类的元数据信息)、对象hashCode、对象GC信息、锁信息等数据存储在对象的对象头
6执行init方法进行初始化

对象的内存布局?
1对象头
-运行时元数据(MarkWord)
哈希值(hashcode)
GC分代年龄
锁状态标志
线程持有锁
偏向线程ID
偏向时间戳
-类型指针
指向方法区中存放的类元信息 确定该对象所属类型
数组长度:对象是数组,还需记录数组长度
2实例数据
是对象真正存储的有效信息
3对齐填充
非必须 占位符作用

对象访问?
如何通过栈帧中的对象引用访问到其内部的对象实例?
-定位,通过栈上的reference访问
-句柄访问
优点:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改
缺点:在堆空间中开辟了一块空间作为句柄池,句柄池本身也会占用空间;通过两次指针访问才能访问到堆中的对象,效率低
-直接访问(Hotspot采用)
优点:直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据
缺点:对象被移动(垃圾收集时移动对象很普遍)时需要修改 reference 的值

** 垃圾回收器
HotSpot回收器?(连线可搭配)
Serial ParNew Parallel Scaveage
G1
CMS Serial Old(MSC) Parallel Old
-jdk8:mr:Parallel Scavenge、Parallel Old

Serial:
-分为Serial、SerialOld
-单线程 垃圾回收线程开始时,业务线程必须暂停
Serial-复制、SerialOld-标记压缩

ParNew:
-多线程
-多条垃圾回收线程并行工作,业务线程处于等待状态
-复制算法

ParallelScnvenge:
-多线程并行
-多条垃圾回收线程并行工作,业务线程处于等待状态
-复制算法

ParallelOld:
-ParallelScnvenge的老年代版本
-多线程 等待
-标记压缩

CMS:
-以获取最短回收停顿时间为目标的收集器
-多线程
垃圾线程和业务线程可以一起执行
-标记清除
-步骤:
初始标记-GCRoot能直接关联到的对象
并发标记-和业务并发
重新标记-修正并发标记期间的变动部分-不能和业务并发
并发清除
-并发标记问题:
1漏标-非垃圾对象后面引用消失,浮动垃圾 重新标记
2错标-垃圾对象后面又被引用
-解决:三色标记算法
漏标:CMS重新标记 A(黑)变成灰色
-CMS大bug
没有jdk版本默认CMS
并发标记漏标:remark阶段,必须从头扫描一遍
G1:
-面向服务端
-步骤:
初始标记
并发标记
最终标记
筛选回收
-优点:并行与并发、分代收集、空间整合、可预测停顿

4种引用

安全点、安全区域

参数配置

垃圾回收器

※ 3-JVM(Java Virtual Machine)※

-- jvm学习:https://www.zybuluo.com/songhanshi/note/1733752

  1. 配置过java启动设置吗
    没有,我只用过-xms等指令改过JVM参数,和jinfo看参数
    -XMX -XSS -XMN

  2. 说说对象创建到消亡的过程
    https://blog.csdn.net/u012312373/article/details/46718911
    https://blog.csdn.net/qq_25005909/article/details/78981512

JMM规定了内存主要划分为主内存和工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
1. 线程的工作内存指的是什么,在内存的哪个地方
* JVM将内存组织为主内存和工作内存两个部分。
* 主内存主要包括本地方法区和堆。每个线程都有一个工作内存,主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)。
① 所有的变量都存储在主内存中,对于所有线程都是共享的。
② 每条线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
③ 线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成。
待完善:https://aalion.github.io/2019/12/08/concurrency12/
https://www.jianshu.com/p/679ad52eca05

一、概述

  1. JRE和JDK的区别?

    • JDK(Java Development Kit)
      -- Java程序设计语言、Java虚拟机、Java类库这三部分统称为JDK,
      广义上JDK常来代指整个Java技术体系;
      -- Java的开发工具,提供了编译和运行Java程序所需的各种资源和工具;
      -- 不仅可以开发Java程序,也同时拥有了运行Java程序的平台;
    • JRE(Java Runtime Enviroment)
      -- Java运行环境,包括:虚拟机+java的核心类库;
      -- 只能运行Java程序,不包含开发工具(编译器、调试器等)。
  2. JVM了解么?

    • 是什么
      虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java虚拟机屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行
    • 系统
      一个Java虚拟机实例在运行过程中有三个子系统来保障它的正常运行,分别是类加载器子系统, 执行引擎子系统和垃圾收集子系统。
  3. 主流:HotSpot VM

    • Sun/OracleJDK和OpenJDK中的默认Java虚拟机,也是目前使用范围最广的Java虚拟机。
    • HotSpot虚拟机中含有两个即时编译器
      -- 编译耗时短但输出代码优化程度较低的客户端编译器(简称为C1)
      -- 编译耗时长但输出代码优化质量也更高的服务端编译器(简称为C2)
      -- 在分层编译机制下与解释器互相配合来共同构成HotSpot虚拟机的执行子系统

二、自动内存管理

2. Java内存区域与内存溢出异常

1- Java内存区域

  1. JVM的内存模型可以说下吗?
    (说一下Java虚拟机内存区域划分、各区域的介绍、1.8&1.7版本迭代)

  2. Java虚拟机内存的各个区域?
    Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。(运行时数据区域,强调对内存空间的划分

  3. Java虚拟机内存的各个区域-分
    【1】程序计数器?

    • 空间较小
    • 线程私有,生命周期与线程相同;
    • 当前线程所执行的字节码的行号指示器;
    • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,是程序控制流的指示器;
    • 辅助完成分支、循环、跳转、异常处理、线程恢复等基础功能;
    • 线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器;
    • 线程Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,
      执行的是本地(Native)方法,这个计数器值则应为空(Undefined)
    • 唯一无OutOfMemoryError情况的
      【2】 Java虚拟机栈?
    • 线程私有,生命周期与线程相同,同程序计数器
    • 作用:
      描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧用于存储数据。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    • 存储:
      栈帧(Stack Frame)存储:局部变量表、操作数栈、动态连接、方法出口等信息;
      局部变量表存储:
      ① 编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、float、long、double);
      ② 对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置);
      ③ returnAddress类型(指向了一条字节码指令的地址).
      局部变量表存储空间:
      局部变量表中的存储空间以局部变量槽(Slot)来表示,其中64位长度的long和double类型的数据会占用两个变量槽,其余的数据类型只占用一个
    • 异常:
      StackOverflowError异常:如果线程请求的栈深度大于虚拟机所允许的深度
      OutOfMemoryError异常:如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存时
      【3】本地方法栈
    • 与虚拟机栈作用相似,
    • 与虚拟机栈区别:
      -- 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务
      -- 本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
    • 异常:
      与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
      【4】Java堆(Java Heap)
    • 虚拟机所管理的内存中空间最大的,所有线程共享的一块内存区域,在虚拟机启动时创建;
    • 存储:对象实例(几乎所有;
    • 垃圾收集器管理的内存区域;
    • 分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区 (TLAB),以提升对象分配时的效率;
    • Java堆可被实现成固定大小的或可扩展的,当前主流的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定);
    • 异常:
      OutOfMemoryError异常:如果在Java堆中没有内存完成实例分配,且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
      【5】方法区(Method Area)
    • 堆的一个逻辑部分
    • 存储:已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。(字符串常量池?
    • 永久代概念:用永久代来实现方法区
    • JDK6,逐步改为采用本地内存(Native Memory)来实现方法区
    • JDK8,完全废弃永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta- space)来代替。
    • OutOfMemoryError异常:方法区无法满足新的内存分配需求时
      【5.1】方法区-运行时常量池(Runtime Constant Pool)
    • 方法区的一部分
    • 存储:编译期生成的各种字面量与符号引用、由符号引用翻译出来的直接引用
      -- Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
      -- 除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来 的直接引用也存储在运行时常量池中
    • 异常:
      和方法区一样受方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
      【6】直接内存
    • 非虚拟机运行时数据区的一部分以及非内存区域
    • 放这里的原因:这部分内存也被频繁地使用,也可能导致OutOfMemoryError异常
    • OutOfMemoryError异常:各个内存区域总和大于物理内存限制
  4. 常量池、运行时常量池、字符串常量池中都存储的什么

    • ans:常量池 .class文件的一部分,字面量和符号引用 |运行时常量池 方法区 加载后的常量池数据 |字符串常量池 方法区 是一组指针指向堆中的String对象的内存地址
    • 常量池、运行时常量池字符串常量池
    • 字符串常量池(一组指针指向Heap中的String对象的内存地址)
      :为避免每次都创建相同的字符串对象及内存分配,JVM内部对字符串对象的创建的优化
  5. 内存模型,堆和栈都有什么?
    (问法不够准确,此处只问内存模型,应该是JMM,后面又问到堆栈应该是想问JVM内存,先按照JVM的角度回答,持续关注...)

    • 经常有人把Java内存区域笼统地划分为堆内存(Heap)和栈内存(Stack),这种划分方式直接继承自传统的C、C++程序的内存布局结构,在Java语言里就显得有些粗糙了,实际的内存区域划分要比这更复杂。不过这种划分方式的流行也间接说明了程序员最关注的、与对象内存分配关系最密切的区域是“堆”和“栈”两块。其中,“堆”在稍后笔者会专门讲述,而“栈”通常就是指这里讲的虚拟机栈,或者更多的情况下只是指虚拟机栈中局部变量表部分。
    • 堆:对象实例;
    • 栈:局部变量表、操作数栈、动态连接、方法出口等信息(z-详细见上)
  6. JVM堆内存划分
    (Java垃圾回收:

    • 永久代(方法区
    • 老年代(堆
    • 新生代(堆
      -Eden区
      -From Survivor
      -To Survivor
    • 无论是哪个区域,存储的都只能是对象的实例,将Java 堆细分的目的只是为了更好地回收内存,或者更快地分配内存。
      http://www.shaoqun.com/a/99944.html

2- 对象创建过程

  1. new一个对象? -jvm3

    • Java虚拟机遇到一条字节码new指令
    • 首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程;
    • 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
      对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从Java堆中划分出来。
    • 内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。
    • 接下来,Java虚拟机还要对对象进行必要的设置
      -- 例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。
      -- 根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
    • 虚拟机的视角,新的对象已经产生。
    • Java程序的视角,对象创建才刚刚开始。
      -- 构造函数,即Class文件中的()方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。
      -- 一般来说(由字节码流中new指令后面是否跟随invokespecial指令所决定,Java编译器会在遇到new关键字的地方同时生成这两条字节码指令,但如果直接通过其他方式产生的则不一定如此),new指令之后会接着执行()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。
  2. jvm怎么知道对象属于哪个类?

    • jvm3:对象头信息:用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,即“Mark Word”。、
      以及另外一部分是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。
    • 推断:由对象头的类型指针获取。

3- OOM

|OutOfMemoryError异常-内存溢出异常|

  1. 堆溢出?

    • 概念:
      Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达到路径来避免垃圾回收机制清除这些对象,那么在对象数量达到最大堆的容量限制后就会产生内存溢出异
    • 原因:
      大量对象占据了堆空间,而这些对象都持有强引用,导致无法回收,当对象大小之和大于由-Xmx参数指定的堆空间大小时,溢出错误就自然而然地发生了。(jvms)
    • 例子:
      ① 内存中加载的数据量过于庞大,如一次从数据库取出过多数
      ② 集合类中有对对象的引用,使用完后未清空,使得JVM不能回
      ③ 代码中存在死循环或循环产生过多重复的对象实体;
  2. 栈溢出? (HotSpot-虚拟机栈和本地方法栈)

    • 概念:在《Java虚拟机规范》中描述了两种异常:
      1)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
      2)如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError异常。
    • 原因:
      HotSpot虚拟机不支持扩展支持栈的动态扩展,只会在创建线程申请内存时就因无法获得足够内存而出现OutOfMemoryError异常,也只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。(jvm3)
    • 当线程请求的栈深度超过虚拟机允许的栈深度时,便会抛出StackOverFlowError
      -- -Xss设置的参数是针对每一个栈的,而非JVM所有线程栈内存总大小。
      -- 每个方法的调用将创建一个栈帧。每一个方法调用时,都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
      -- SUM(每个栈帧大小)>栈大小发生栈溢出
  3. 调整栈内存jvm参数知道吗?常用的jvm参数有那些?

    • 栈的参数配置
    指令 作用
    -Xss 指定线程的栈大小。栈是每个线程私有的内存空间。
  4. 递归10w次会出现什么?(OOM)

    • 问题:栈溢出
    • 原因:
      -- 栈先进后出,方法压栈运行,递归过程先入的不能出栈,会存在栈空间中,这样就容易导致栈满而溢出。
      -- 线程内部的每个方法调用会创建一个栈帧,所以如果“栈帧的数量*每个栈帧的大小>栈大小”时便会发生“栈溢出”。
      -- 每当你调用一个方法,在这个方法执行前都会将之前的内存地址(也就是调用点)入栈,等被调用的方法执行完将地址出栈,程序根据这个数据返回调用点。
      若递归调用次数太多,就会只入栈不出栈,于是堆栈就被压爆了,此为栈溢出。
      递归函数调用的太深,需要太多的内存,递归里用到的局部变量存储在堆栈中,堆栈的访问效率高,速度快,但空间有限,递归太多变量需要一直入栈而不出栈,导致需要的内存空间大于堆栈的空间。
    • 解决:可以考虑采取循环的方式来解决,将需要的数据在关键的调用点保存下来使用。即用自己的数据保存方法来代替系统递归调用产生的栈数据。
    • 注:操作系统分配给一个进程的栈空间是2M,堆空间在32位机器上是4G。如果进程的栈空间使用超过了2M就会栈溢出,堆使用超过4G就会堆溢出。
  5. 栈溢出异常,通过什么方式来解决?

    • HotSpot虚拟机不支持扩展支持栈的动态扩展
    • 解决
      -- 1)代码层面
      将递归改为循环或保存数据(降低层次,或变量设为全局变量,这样它会被存在堆里(或其它地方))
      -- 2)线上临时解决办法或者1)无法解决
      重新调整JVM参数-Xss,重启应用
      如-Xss将thread stack size变为2m
    • 如何设置
      -- 首先,操作系统分配给每个进程的内存是有限制的。那么:
      可用的栈内存=进程最大内存-堆内存-方法区内存-程序计数器内存-虚拟机本身耗费内存
      -- 而栈是线程私有的,那么可以认为:
      程序可建立的线程数量=可用栈内存/栈大小
      -- 这样当栈大小设置太大时,就会导致创建的线程数量太少。这样在多线程的情况下便可能发生“内存溢出”情况。
      -- 在x64位Linux操作系统上,JVM默认的栈大小为1024kb。
      由于我们线上的程序要支持高并发场景,所以栈的大小设置为256kb,这里仅供参考。
  6. 怎么让方法区溢出?

    • “永久代(Perm)”(jdk1.6/1.7),“元空间(meta-space)”(jdk1.8)用来实现方法区
    • jvms:一个系统不断产生新的类,而没有回收,最终可能导致永久区溢出。

      1. // jdk1.6 -XX:MaxPermSize=5m
      2. public class PermOOM{
      3. public static void main(String[] args) {
      4. try {
      5. for (int i = 0; i <100000 ; i++) {
      6. // 每次循环都生成一个新的类(是类,而非对象实例)
      7. CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean"+i,new HashMap());
      8. }
      9. }catch (Error e){
      10. e.printStackTrace();
      11. }
      12. }
      13. }
      14. // 结果
      15. // Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    • 解决永久区溢出,从以下几个方面考虑(jvms)
      -- 增加MaxPermSize的值
      -- 减少系统需要的类的数量
      -- 使用ClassLoader合理地装载各个类,并定期进行回收

  7. 遇到过的OOM?

    • 不断创建对象可以导致堆溢出 - 堆
    • 递归调用可以导致栈溢出 - 栈

    • jvms

    • 递归|单线程|多线程
  8. OOM 如何排查以及优化/OOM问题怎么定位(线上?) -P50
    常规的处理方法(jvm3)

    • 首先通过内存映像分析工具对Dump出来的堆转储快照进行分析。
      第一步首先应确认内存中导致OOM的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)
    • 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
    • 如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
    • 排查流程
      在这里插入图片描述
    • 流程:https://www.cnblogs.com/c-xiaohai/p/12489336.html
      https://blog.csdn.net/ywlmsm1224811/article/details/91866707
      https://blog.csdn.net/wx1528159409/article/details/93530352#%E6%8E%92%E6%9F%A5%EF%BC%9A
      2)优化:
    • 使用更小的图片
    • StringBuilder来替代频繁的“+”
      https://blog.csdn.net/weixin_41101173/article/details/79716332
  9. Java会不会内存泄露?怎样会泄露?

  10. Java 内存泄漏问题,解释一下什么情况下会出现?

    • Java使用的内存种类包含三种,这三种类型的内存都可能发生内存泄漏。
      • 堆内存泄漏,如果JVM 不能在java 堆中获得更多内存来分配更多java 对象,将会抛出java堆内存不足(java OOM) 错误。如果java 堆充满了活动对象,并且JVM 无法再扩展java 堆,那么它将不能分配更多java 对象。更多情况是程序设计有问题,生成的对象占用过多的堆内存造成堆内存泄漏。
      • 本地内存泄漏, 如果JVM 无法获得更多本地内存,它将抛出本地OOM错误。当进程用到的内存到达操作系统的最大限值,或者当计算机用完RAM 和交换空间时,通常会发生这种情况。当发生这种情况时,JVM处于本地内存OOM状态,此时虚拟机会打印相关信息并退出。本地内存泄漏根本原因是Java调用本地库或方法,这些本地库中的API有内存泄漏。
      • 加载类(字节码)的Perm内存不足.即指定的Permsize不足以加载系统运行使用的.class字节码文件,就发发生Perm内存不足的错误。

3. 垃圾回收

  1. 垃圾回收,堆区为什么那么分

    • 不过无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java堆细分的目的只是为了更好地回收内存,或者更快地分配内存。(jvm3)
  2. Java垃圾回收简单讲一下,里面的算法?

    • 判断对象?标记算法?回收算法?收集器?

1-判断对象死亡

  1. JVM 垃圾回收的是如何确定垃圾?

    • 什么是垃圾
      简单的说就是内存中已经不再被使用到的空间就是垃圾。
      1. Person p = null;
  2. java垃圾回收,如何判断一个对象需要回收

  3. 引用计数算法 和 可达性分析算法

    • 引用计数算法
      在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;
      当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的
    • 优点
      会占用了一些额外的内存空间来进行计数,原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。
    • 缺点
      必须要配合大量额外处理才能保证正确地工作,如单纯的引用计数就很难解决对象之间相互循环引用的问题。

      1. Object a = new Object();
      2. Object b = new Object();
      3. a=b;
      4. b=a;
      5. a=b=null; //这样就导致gc无法回收他们。
    • 可达性分析算法
      通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。
      缺点:实现比较复杂、需要分析大量数据,消耗大量时间、分析过程需要GC停顿(引用关系不能发生变化),即停顿所有Java执行线程(称为"Stop The World",是垃圾回收重点关注的问题)。

  4. 是否知道什么是GC Roots?(jvm3)

    • 固定可作为GC Roots的对象:
      -- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
      -- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
      -- 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
      -- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
      -- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
      -- 所有被同步锁(synchronized关键字)持有的对象。
      -- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
    • 临时GC Roots
      -- 根据用户所选用的垃圾收集器以及当前回收的内存区域不 同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。
      -- 如后文将会提到的分代收集和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生代的垃圾收集),必须考虑到内存区域是虚拟机自己的实现细节(在用户视角里任何内存区域都是不 可见的),更不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GCRoots集合中去,才能保证可达性分析的正确性。
  5. 哪些对象可以作为gcroot(jvm2)

    • 1)虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 2)方法区中类静态属性引用的对象
    • 3)方法区中常量引用的对象
    • 4)本地方法栈中JNI(即Native方法)引用的对象
      1. public class GCRootDemo{
      2. private static GCRootDemo2 t2;//第2种,static静态,一份全部实例变量共用?被加载进方法区,
      3. // Java7方法区为永久代;GCRootDemo2其他对象
      4. private static final GCRootDemo3 t3 = new GCRootDemo3(8);//static final常量引用
      5. public static void m1(){
      6. GCRootDemo t1 = new GCRootDemo();//第1种:m1方法在栈中,t1为方法中的局部变量
      7. System.gc();
      8. System.out.println("第一次GC完成");
      9. }
      10. public static void main(String[] args) {
      11. m1();
      12. }
      13. }
  6. 引用?

    • ~
      强引用:只有强引用还存在,GC就永远不会收集被引用的对象。
      软引用:不占空间,gc不回收
      弱引用:WeakReference 调用gc直接回收 ★
      虚引用:PhantomReference 与队列结合使用,get不到
    • 强引用:最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
    • 软引用:用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2版之后提供了SoftReference类来实现软引用。
    • 弱引用:也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2版之后提供了WeakReference类来实现弱引用。
    • 虚引用:也称为“幽灵引用”或者“幻影引用”,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK1.2版之后提供 了PhantomReference类来实现虚引用。
  7. 判断一个对象生存还是死亡?

    • 第一次标记
        如果对象进行可达性分析算法之后没发现与GC Roots相连的引用链,那它将会第一次标记并且进行一次筛选。
        筛选条件:判断此对象是否有必要执行finalize()方法。
        筛选结果:当对象没有覆盖finalize()方法、或者finalize()方法已经被JVM执行过,则判定为可回收对象。如果对象有必要执行finalize()方法,则被放入F-Queue队列中。稍后在JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发这个方法;
    • 第二次标记
        GC对F-Queue队列中的对象进行二次标记。
        如果对象在finalize()方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出“即将回收”集合。如果此时对象还没成功逃脱,那么只能被回收了。
    • finalize() 方法
        finalize()是Object类的一个方法、一个对象的finalize()方法只会被系统自动调用一次,经过finalize()方法逃脱死亡的对象,第二次不会再调用;
      https://www.cnblogs.com/chenpt/p/9797126.html

2-垃圾收集算法

4.jvm卡表(Card Table)?
安全区域(Safe Region),记忆集

  1. 垃圾回收算法有哪些

    • 标记-清除
    • 复制
    • 标记-整理
    • 分代收集算法
  2. 垃圾回收算法,为什么老年代和新生代不同

    • 存活周期不同
      1)弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。 2)强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
  3. 垃圾收集算法新生代和老年代分别用什么算法

    • 回收新生代:大多使用复制算法
    • 回收老年代:使用“标记-清理”或“标记-整理”算法
  4. 如果对象大部分都是存活的,少部分需要清除,用什么算法

    • 新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
    • 老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
  5. 名词概念

    • 部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,又分为:
      ■ 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
      ■ 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
      ■ 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
    • 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
  6. 说说GC的流程

  7. 什么时候对象会到老年代,老年代的更新机制
    复制算法

    • 分配
      默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 )
    • 过程
      当对象在 Eden 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。
      但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。
    • JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。 如此往复
      http://www.shaoqun.com/a/99944.html
      https://blog.csdn.net/yangyang12345555/article/details/79257171

3-回收器

  1. 垃圾回收器了解吗?

    • 是什么?有哪些?做什么?
  2. 为何需要垃圾回收?

    • Java垃圾收集机制为避免出现内存溢出异常。
  3. 有哪些gc收集器?

  4. 垃圾回收器在哪块?

    • Java堆
    • 收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储。
    • 在JVM体系结构中,与垃圾回收相关的两个主要组件是堆内存和垃圾回收器。堆内存是内存数据区,用来保存运行时的对象实例。垃圾回收器也会在这里操作。
  5. 垃圾回收器(CMS)详细过程。哪个阶段出现STW?

    • 运作过程的四个步骤?
      1)初始标记(CMS initial mark)
      2)并发标记(CMS concurrent mark)
      3)重新标记(CMS remark)
      4)并发清除(CMS concurrent sweep)
    • 初始标记、重新标记:这两个步骤仍然需要“Stop The World”
    • 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;
    • 并发标记阶段:就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
    • 重新标记阶段:为修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;
    • 并发清除阶段:清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
      在这里插入图片描述
  6. 垃圾收集器CMS出现问题了怎么办?

    • 1)promotion failed – concurrent mode failure
      Minor GC后, 救助空间容纳不了剩余对象,将要放入老年带,老年带有碎片或者不能容纳这些对象,就产生了concurrent mode failure, 然后进行stop-the-world的Serial Old收集器。
      -- 解决办法:-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5 或者调大新生代或者救助空间
    • 2)concurrent mode failure
      CMS是和业务线程并发运行的,在执行CMS的过程中有业务对象需要在老年带直接分配,例如大对象,但是老年带没有足够的空间来分配,所以导致concurrent mode failure, 然后需要进行stop-the-world的Serial Old收集器。
      -- 解决办法:+XX:CMSInitiatingOccupancyFraction,调大老年带的空间,+XX:CMSMaxAbortablePrecleanTime
    • 总结一句话:使用标记整理清除碎片和提早进行CMS操作。
    • 两个问题:promotion failed和concurrent mode failure
    • 解决:
      第一个,可以让CMS在进行一定次数的Full GC的时候进行一次标记整理算法。
      第二个,调低触发CMS GC执行的阀值。
      https://my.oschina.net/hosee/blog/674181

4-内存分配与回收策略

  1. java内存管理?

    • Java技术体系的自动内存管理,最根本的目标是自动化地解决两个问题
      自动给对象分配内存以及自动回收分配给对象的内存。
  2. 什么时候对象会到老年代,老年代的更新机制?

    • 大对象直接进入老年代
      -- 大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组
      -- 避免大对象的原因:
      在分配空间时,容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复 制对象时,大对象就意味着高额的内存复制开销。
      -- HotSpot中-XX:PretenureSizeThreshold
      指定大于该设置值的对象直接在老年代分配,避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。
    • 长期存活的对象将进入老年代
      -- 虚拟机给每个对象定义了一个对 象年龄(Age)计数器,存储在对象头中。
      -- 对象通常在Eden区里诞生,如果经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。
      -- -XX: MaxTenuringThreshold设置:对象晋升老年代的年龄阈值。
    • 动态对象年龄判定
      -- HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold中要求的年龄。
      -- -XX:MaxTenuringThreshold=15
      -- 同年龄的,满足同年对象达到Survivor空间一半的规则
  3. 操作系统层面是怎么分配内存的 91
    https://blog.csdn.net/qq_32635069/article/details/74838187

4. 监控、故障处理工具

1-jstack((Stack Trace for Java))

  1. jstack原理
    1)jstack定义-P111
    • jstack用于生成java虚拟机当前时刻的线程快照
    • 线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,
    • 生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。
    • 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
      2)实现 -P110 !!!
      JDK 1.5中,java.lang.Thread类新增了一个getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码就完成jstack的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈。
      3)使用
      jstack [ option ] pid 如,jstack -l 3500
      -l 长列表. 打印关于锁的附加信息。
      4)jsp(JVM Process Status Tool)
      可以列出正在进行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机进程唯一ID(LVMID)---对应3)的pid
      如,jsp -l

三、虚拟机执行子系统

6. .class文件

7. 虚拟机类加载机制

  1. java虚拟机类加载机制?
    • jvm3:Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。

1-时机

  1. 类加载的顺序?

    • 一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段;
    • 其中,验证、准备、解析三个部分统称为连接(Linking)。
    • 加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始;
    • 解析阶段:在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。
    • “开始”强调这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。
    • 七个阶段的发生顺序如图:
      在这里插入图片描述
  2. 有哪些操作会触发类加载?

    • 《Java虚拟机规范》中并没有对在什么情况下需要开始类加载过程的第一个阶段“加载”进行强制约束。但对于初始化阶段,则严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
    • 对一个类型进行主动引用,“有且只有”这六种场景中的行为称为。除此之外的所有引用类型的方式都不会触发初始化,称为被动引用;
    • 【主动引用】
      1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
      -- 使用new关键字实例化对象的时候。
      -- 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候。
      -- 调用一个类型的静态方法的时候。
      2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
      3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
      4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
      5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
      6)当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
    • 【被动引用】
      1)通过子类引用父类的静态字段,不会导致子类初始化
      2)通过数组定义来引用类,不会触发此类的初始化
      3)常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的 类的初始化
    • 图示:https://blog.csdn.net/L_Mr_l/article/details/81909995

2-过程

  1. 类加载过程?

    • 概念:
      Java虚拟机中类加载的全过程,即加载、验证、准备、解析和初始化这五 个阶段所执行的具体动作。
    • 1)加载
      加载阶段,Java虚拟机需要完成以下三件事情:
      ① 通过一个类的全限定名来获取定义此类的二进制字节流。
      ② 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
      ③ 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
    • 2)验证
      验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
      ① 文件格式验证
      ② 元数据验证
      ③ 字节码验证
      ④ 符号引用验证
    • 3)准备
      准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
      这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区 本身是一个逻辑上的区域,在JDK7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候“类变量在方法区”就完全是一种对逻辑概念的表述了。
    • 4)解析
      Java虚拟机将常量池内的符号引用替换为直接引用的过程。
      -- 符号引用(Symbolic References):
      符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
      -- 直接引用(Direct References):
      直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
      ① 类或接口的解析
      ② 字段解析
      ② 方法解析
      ③ 接口方法解析
    • 5)初始化
      初始化阶段就是执行类构造器()方法的过程。
  2. 详细说说类加载的过程,静态代码块执行在哪个阶段?
    静态代码块在初始化阶段执行
    https://blog.csdn.net/qq_36839438/article/details/106738514
    https://blog.csdn.net/qq_38159458/article/details/105865964

3-类加载器

  1. 类加载器的4个种类
    1)启动类加载器:这个类加载器负责放在目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库。用户无法直接使用。
    2)扩展类加载器:这个类加载器由AppClassLoader\lib\ext使由sun.misc.Launcher实现。是ClassLoader中getSystemClassLoader()方法的返回值。它负责用户路径(ClassPath)所指定的类库。用户可以直接使用。如果用户没有自己定义类加载器,默认使用这个。
    4)自定义加载器:用户自己定义的类加载器。

  2. 双亲委派模型
    双亲委派模型:

    • 定义:上述4种展示的类加载之间的层次关系称为xxx。
    • 优点:Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
    • 双亲委托模型的工作过程是:如果一个类加载器(ClassLoader)收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要加载的类)时,子加载器才会尝试自己去加载。
      https://blog.csdn.net/qq_35758236/article/details/81115320
  3. 为啥要双亲加载

    • 使用双亲委托机制的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。
  4. 双亲委派机制,怎么打破
    tomcat

四、程序编译与代码优化

  1. 说一说为什么要有JIT
    JIT是经过一系列的分析和热点代码探测技术,对一部分class字节码编译成机器语言,以此提高性能,而解释器就是执行一句class字节码,就翻译成一句机器语言。JIT的存在,减少对热点代码的重复翻译。
    https://www.jianshu.com/p/ae0d47e770f0
    https://www.cnblogs.com/xuyatao/p/6914769.html

  1. JVM堆上会不会产生线程安全问题 pP48

    • 对象的内存分配过程中,主要是对象的引用指向这个内存区域,然后进行初始化操作。Java堆确定出一块内存区域,用于给新建对象分配内存。
    • 在并发场景中,如果两个线程先后把对象引用指向了同一个内存区域,如何内存分配过程的线程安全性?
    • 一般有两种解决方案:
      1、对分配内存空间的动作做同步处理,采用CAS机制,配合失败重试的方式保证更新操作的原子性。
      2、每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块"私有"内存中分配,当这部分区域用完之后,再分配新的"私有"内存。
      方案1在每次分配时都需要进行同步控制,这种是比较低效的。
      方案2是HotSpot虚拟机中采用的,这种方案被称之为TLAB分配,即Thread Local Allocation Buffer。这部分Buffer是从堆中划分出来的,但是是本地线程独享的。
      TLAB时线程独享的,但是只是在“分配”这个动作上是线程独占的,至于在读取、垃圾回收等动作上都是线程共享的。
      https://juejin.im/post/5d4250def265da03ab422c79
  2. 那比如你在项目里写了一个Class A,然后在某一个jar包里也有一个Class A,比如com.a.A,那么这两个class你觉得哪个先被加载,会出现什么问题(不会,求了答案,告诉我说他也不清楚,就是考考我对这块有没有自己的理解😑)

  3. 字节码是什么?
    字节码:Java程序无须重新编译便可在多种不同的计算机上运行。
    字节码(Byte-code)是一种包含执行程序,由一序列 op 代码/数据对组成的二进制文件,是一种中间码。 javap -c

4-mysql

mysql

查询逻辑架构

一条查询语句执行流程?

redolog(重做日志)|binlog(归档日志)

redobinlog补充

innodb存储架构

存储引擎

日志有哪些?

事务

在这里插入图片描述

比较读提交,重复读性能
https://www.cnblogs.com/hainange/p/6153632.html

undolog(回滚日志)和MVCC

RC和RR级别事务的实现:一致性视图、MVCC

如何解决脏读

如何解决不可重复读

如何解决幻读

innodb锁

全局锁|表锁|行锁

死锁

索引_树

为什么使用索引/b+

索引_explain

索引_分类

索引_ab

  1. MyISAM索引和InnoDB索引?
    • Mysql的索引实现
      介绍完了索引数据结构,那肯定是要带入到Mysql里面看看真实的使用场景的,所以这里分析Mysql的两种存储引擎的索引实现:MyISAM索引和InnoDB索引
    • MyIsam索引
      -- 以一个简单的user表为例。user表存在两个索引,id列为主键索引,age列为普通索引
      -- MyISAM的数据文件和索引文件是分开存储的。MyISAM使用B+树构建索引树时,叶子节点中存储的键值为索引列的值,数据为索引所在行的磁盘地址。
      在这里插入图片描述
      1)主键索引
      注:以下分析仅供参考,MyISAM在查询时,会将索引节点缓存在MySQL缓存中,而数据缓存依赖于操作系统自身的缓存,所以并不是每次都是走的磁盘,这里只是为了分析索引的使用过程。
      在这里插入图片描述
      ◆ 表user的索引存储在索引文件user.MYI中,数据文件存储在数据文件 user.MYD中。
      ◆ 简单分析下查询时的磁盘IO情况:根据主键等值查询数据:
      select * from user where id = 28;
      ① 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO)
      ② 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO)
      ③ 检索到叶节点,将节点加载到内存中遍历,比较16<28,18<28,28=28。查找到值等于30的索引项。(1次磁盘IO)
      ④ 从索引项中获取磁盘地址,然后到数据文件user.MYD中获取对应整行记录。(1次磁盘IO)
      ⑤ 将记录返给客户端。
      ◆ 磁盘IO次数:3次索引检索+记录数据检索。
      在这里插入图片描述
      ◆ 根据主键范围查询数据:
      select * from user where id between 28 and 47;
      ① 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO)
      ② 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO)
      ③ 检索到叶节点,将节点加载到内存中遍历比较16<28,18<28,28=28<47。查找到值等于28的索引项。
      根据磁盘地址从数据文件中获取行记录缓存到结果集中。(1次磁盘IO)
      我们的查询语句时范围查找,需要向后遍历底层叶子链表,直至到达最后一个不满足筛选条件。
      ④ 向后遍历底层叶子链表,将下一个节点加载到内存中,遍历比较,28<47=47,根据磁盘地址从数据文件中获取行记录缓存到结果集中。(1次磁盘IO)
      ⑤ 最后得到两条符合筛选条件,将查询结果集返给客户端。
      ◆ 磁盘IO次数:4次索引检索+记录数据检索。
      在这里插入图片描述
      2)辅助索引
      ◆ 在 MyISAM 中,辅助索引和主键索引的结构是一样的,没有任何区别,叶子节点的数据存储的都是行记录的磁盘地址。只是主键索引的键值是唯一的,而辅助索引的键值可以重复。
      ◆ 查询数据时,由于辅助索引的键值不唯一,可能存在多个拥有相同的记录,所以即使是等值查询,也需要按照范围查询的方式在辅助索引树中检索数据。
    • InnoDB索引
      1)主键索引(聚簇索引)
      ◆ 每个InnoDB表都有一个聚簇索引,聚簇索引使用B+树构建,叶子节点存储的数据是整行记录。一般情况下,聚簇索引等同于主键索引,当一个表没有创建主键索引时,InnoDB会自动创建一个ROWID字段来构建聚簇索引。InnoDB创建索引的具体规则如下:
      ① 在表上定义主键PRIMARY KEY,InnoDB将主键索引用作聚簇索引。
      ② 如果表没有定义主键,InnoDB会选择第一个不为NULL的唯一索引列用作聚簇索引。
      ③ 如果以上两个都没有,InnoDB 会使用一个6 字节长整型的隐式字段 ROWID字段构建聚簇索引。该ROWID字段会在插入新行时自动递增。
      ◆ 除聚簇索引之外的所有索引都称为辅助索引。在中InnoDB,辅助索引中的叶子节点存储的数据是该行的主键值都。在检索时,InnoDB使用此主键值在聚簇索引中搜索行记录。
      ◆ 这里以user_innodb为例,user_innodb的id列为主键,age列为普通索引。
      在这里插入图片描述
      ◆ InnoDB的数据和索引存储在一个文件t_user_innodb.ibd中。InnoDB的数据组织方式,是聚簇索引。
      ◆ 主键索引的叶子节点会存储数据行,辅助索引只会存储主键值。
      InnoDB主键索引,如图:
      在这里插入图片描述
      ◆ 等值查询数据:
      ** select * from user_innodb where id = 28;**
      ① 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO)
      ② 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO)
      ③ 检索到叶节点,将节点加载到内存中遍历,比较16<28,18<28,28=28。查找到值等于28的索引项,直接可以获取整行数据。将改记录返回给客户端。(1次磁盘IO)
      ◆ 磁盘IO数量:3次。
      在这里插入图片描述
      2)辅助索引
      ◆ 除聚簇索引之外的所有索引都称为辅助索引,InnoDB的辅助索引只会存储主键值而非磁盘地址。
      ◆ 以表user_innodb的age列为例,age索引的索引结果如下图(InnoDB辅助索引)。
      ◆ 底层叶子节点的按照(age,id)的顺序排序,先按照age列从小到大排序,age列相同时按照id列从小到大排序。
      ◆ 使用辅助索引需要检索两遍索引:首先检索辅助索引获得主键,然后使用主键到主索引中检索获得记录。
      在这里插入图片描述
      ◆ 画图分析等值查询的情况:
      ** select * from t_user_innodb where age=19; **
      ◆ 根据在辅助索引树中获取的主键id,到主键索引树检索数据的过程称为回表查询
      ◆ 磁盘IO数:辅助索引3次+获取记录回表3次
      在这里插入图片描述
      3)组合索引
      ◆ 还是以自己创建的一个表为例:表 abc_innodb,id为主键索引,创建了一个联合索引idx_abc(a,b,c)。
      在这里插入图片描述
      ◆ 组合索引的数据结构:
      在这里插入图片描述
      ◆ 组合索引的查询过程:
      select * from abc_innodb where a = 13 and b = 16 and c = 4;
      在这里插入图片描述
      4)最左匹配原则:
      ◆ 最左前缀匹配原则和联合索引的索引存储结构和检索方式是有关系的。
      ◆ 在组合索引树中,最底层的叶子节点按照第一列a列从左到右递增排列,但是b列和c列是无序的,b列只有在a列值相等的情况下小范围内递增有序,而c列只能在a,b两列相等的情况下小范围内递增有序。
      ◆ 就像上面的查询,B+树会先比较a列来确定下一步应该搜索的方向,往左还是往右。如果a列相同再比较b列。但是如果查询条件没有a列,B+树就不知道第一步应该从哪个节点查起。
      ◆ 可以说创建的idx_abc(a,b,c)索引,相当于创建了(a)、(a,b)(a,b,c)三个索引。◆ 组合索引的最左前缀匹配原则:使用组合索引查询时,mysql会一直向右匹配直至遇到范围查询(>、<、between、like)就停止匹配
      5)覆盖索引
      ◆ 覆盖索引并不是说是索引结构,覆盖索引是一种很常用的优化手段。因为在使用辅助索引的时候,我们只可以拿到主键值,相当于获取数据还需要再根据主键查询主键索引再获取到数据。但是试想下这么一种情况,在上面abc_innodb表中的组合索引查询时,如果我只需要abc字段的,那是不是意味着我们查询到组合索引的叶子节点就可以直接返回了,而不需要回表。这种情况就是覆盖索引。
      可以看一下执行计划:
      在这里插入图片描述
    • 总结:对sql语句里面的索引的优化
      1)避免回表
      ◆ 在InnoDB的存储引擎中,使用辅助索引查询的时候,因为辅助索引叶子节点保存的数据不是当前记录的数据而是当前记录的主键索引,索引如果需要获取当前记录完整数据就必然需要根据主键值从主键索引继续查询。这个过程我们成位回表。想想回表必然是会消耗性能影响性能。那如何避免呢?
      ◆ 使用索引覆盖,举个例子:现有User表(id(PK),name(key),sex,address,hobby...)
      ◆ 如果在一个场景下,select id,name,sex from user where name ='zhangsan';这个语句在业务上频繁使用到,而user表的其他字段使用频率远低于它,在这种情况下,如果我们在建立 name 字段的索引的时候,不是使用单一索引,而是使用联合索引(name,sex)这样的话再执行这个查询语句是不是根据辅助索引查询到的结果就可以获取当前语句的完整数据。
      ◆ 这样就可以有效地避免了回表再获取sex的数据。
      ◆ 这里就是一个典型的使用覆盖索引的优化策略减少回表的情况
      2)联合索引的使用
      ◆ 联合索引,在建立索引的时候,尽量在多个单列索引上判断下是否可以使用联合索引。联合索引的使用不仅可以节省空间,还可以更容易的使用到索引覆盖。
      ◆ 试想一下,索引的字段越多,是不是更容易满足查询需要返回的数据呢。比如联合索引(a_b_c),是不是等于有了索引:a,a_b,a_b_c三个索引,这样是不是节省了空间,当然节省的空间并不是三倍于(a,a_b,a_b_c)三个索引,因为索引树的数据没变,但是索引data字段的数据确实真实的节省了。
      ◆ 联合索引的创建原则,在创建联合索引的时候因该把频繁使用的列、区分度高的列放在前面,频繁使用代表索引利用率高,区分度高代表筛选粒度大,这些都是在索引创建的需要考虑到的优化场景,也可以在常需要作为查询返回的字段上增加到联合索引中,如果在联合索引上增加一个字段而使用到了覆盖索引,那我建议这种情况下使用联合索引。
      ◆ 联合索引的使用
      ① 考虑当前是否已经存在多个可以合并的单列索引,如果有,那么将当前多个单列索引创建为一个联合索引。
      ② 当前索引存在频繁使用作为返回字段的列,这个时候就可以考虑当前列是否可以加入到当前已经存在索引上,使其查询语句可以使用到覆盖索引。

哪些需要创建索引

生产:如何设计索引

你都是如何设计索引的?
https://mp.weixin.qq.com/s/-gmAPfiKMNJgHhIZqR2C4A|用对了这些场景下的索引

• InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。

  1. 索引使用场景(重点)[-]

    • 1)where
      在这里插入图片描述
      上图中,根据id查询记录,因为id字段仅建立了主键索引,因此此SQL执行可选的索引只有主键索引,如果有多个,最终会选一个较优的作为检索的依据。
      1. -- 增加一个没有建立索引的字段
      2. -- alter table 表名 add index(字段名))
      3. alter table innodb1 add sex char(1);
      4. -- sex检索时可选的索引为null
      5. EXPLAIN SELECT * from innodb1 where sex='男';

    在这里插入图片描述

    • 2) order by
      -- 当我们使用order by将查询结果按照某个字段排序时,如果该字段没有建立索引,那么执行计划会将查询出的所有数据使用外部排序(将数据从硬盘分批读取到内存使用内部排序,最后合并排序结果),这个操作是很影响性能的,因为需要将查询涉及到的所有数据从磁盘中读到内存(如果单条数据过大或者数据量过多都会降低效率),更无论读到内存之后的排序了。
      -- 但是如果我们对该字段建立索引alter table 表名 add index(字段名),那么由于索引本身是有序的,因此直接按照索引的顺序和映射关系逐条取出数据即可。而且如果分页的,那么只用取出索引表某个范围内的索引对应的数据,而不用像上述那取出所有数据进行排序再返回某个范围内的数据。(从磁盘取数据是最影响性能的)
    • 3) join
      对join语句匹配关系(on)涉及的字段建立索引能够提高效率
    • 4) 索引覆盖
      如果要查询的字段都建立过索引,那么引擎会直接在索引表中查询而不会访问原始数据(否则只要有一个字段没有建立索引就会做全表扫描),这叫索引覆盖。因此我们需要尽可能的在select后只写必要的查询字段,以增加索引覆盖的几率。
    • 这里值得注意的是不要想着为每个字段建立索引,因为优先使用索引的优势就在于其体积小。

索引失效


  1. 索引为什么用在很多值重复的字段上会失效
    是优化器的选择,如果比例太高,mysql会认为这种方式很低效,因为涉及到回表,所以默认全表扫描

  2. 使用索引查询一定能提高查询的性能吗?为什么 [-]

    • 通常,通过索引查询数据比全表扫描要快。但是我们也必须注意到它的代价。
      • 索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改。 这意味着每条记录的INSERT,DELETE,UPDATE将为此多付出4,5 次的磁盘I/O。 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况:
      • 基于一个范围的检索,一般查询返回结果集小于表中记录数的30%
      • 基于非唯一性索引的检索

mysql调优

主从复制

判断主库故障

一主多从,主库故障,主备切换

读写分离

分库分表

1. 基础查询

1-写sql

  1. sql查询1?
    • 课程名中包含‘计算机’的课程且成绩小于60分学生的学号、姓名
      select number,name from
  2. sql查询2?

    • 成绩表中按照科目取最大成绩
  3. sql查询2?

    • 主要考察成绩查询的sql,考察到的知识点主要包括 order by,sum,limit,group by ... having ...

2-关键字

  1. 常用的函数?

  2. Mysql如何拼接字符串?
    1)CONCAT(string1,string2,…)
    说明 : string1,string2代表字符串,concat函数在连接字符串的时候,只要其中一个是NULL,那么将返回NULL
    2)CONCAT_WS(separator,str1,str2,...)
    说明 : string1,string2代表字符串,concat_ws 代表 concat with separator,第一个参数是其它参数的分隔符。分隔符的位置放在要连接的两个字符串之间。分隔符可以是一个字符串,也可以是其它参数。如果分隔符为 NULL,则结果为 NULL。函数会忽略任何分隔符参数后的 NULL 值。
    3)group_concat函数
    完整的语法如下:
    group_concat([DISTINCT] 要连接的字段 [Order BY ASC/DESC 排序字段] [Separator '分隔符'])

  3. Mysql去重关键字?

    • distinct
  4. in如何实现的?

  5. 数据库中JOIN是怎么实现的?

  6. mysql的几种连接?
    左连接、内连接、右连接

  7. right join原理

  8. 左外连接和内连接的区别? |2
    1)内连接,显示两个表中有联系的所有数据;
    2)左链接,以左表为参照,显示所有数据;
    3)右链接,以右表为参照显示数据;
    https://www.cnblogs.com/cs071122/p/6753681.html

3-其他

  1. 为什么要使用数据库? [-]

    • 数据保存在内存
      优点: 存取速度快
      缺点: 数据不能永久保存
    • 数据保存在文件
      优点: 数据永久保存
      缺点:1)速度比内存操作慢,频繁的IO操作。2)查询数据不方便
    • 数据保存在数据库
      1)数据永久保存
      2)使用SQL语句,查询方便效率高。
      3)管理数据方便
  2. 为什么在技术选型时选择MySQL,而不是选择Oracle?

    • mysql是免费的,oracle是收钱的。
    • 阿里去IOE;√
    • MySQL 允许数据丢包,而且可以大量部署在PC server上。符合互联网的特点。Oracle是严谨的企业数据库,讲究就是数据一致性,所以传统行业比较适合。
    • 主要是免费,其次它是开源的,高级一点的你可以修改它的源码使其符合你的要求,方便扩展,代价是需要会修改的人来做这种工作,Linux也一样,免费开源,可以修改到适合公司的修改
  3. 数据库三范式?

    • 第一范式:每个列都不可以再拆分。确保每列的原子性.
    • 第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
    • 第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
    • 在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。
  4. mysql的三种驱动类型?
    1)Class.forName("com.mysql.jdbc.Driver");//加载数据库驱动
    2)new com.mysql.jdbc.Driver() ;//创建driver对象,加载数据库驱动
    https://www.iteye.com/blog/862123204-qq-com-1566581

  5. 数据库怎么看耗时?
    https://www.cnblogs.com/ymdphp/p/10904690.html

  6. 写SQL的注意事项?

7. 查询优化

https://thinkwon.blog.csdn.net/article/details/104778621
1. 慢查询如何分析排查和优化? |3

  1. mysql如何优化(回答索引、拆分等) |5

  2. mysql查询优化?
    索引、关联子查询等,最常见的就是给表加上合适的索引

  3. mysql在项目中的优化场景?

  4. 分库分表的理解,好处
    https://blog.csdn.net/u010817136/article/details/51037845

  5. 数据库垂直与水平拆分怎么做?
    分库分表数据切分

    • 水平切分
      ◆ 水平切分又称为 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。
      ◆ 当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。
      在这里插入图片描述
    • 垂直切分
      ◆ 垂直切分是将一张表按列分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直气氛将经常被使用的列喝不经常被使用的列切分到不同的表中。
      ◆ 在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不通的库中,例如将原来电商数据部署库垂直切分称商品数据库、用户数据库等。
      在这里插入图片描述
    • Sharding 策略
      ◆ 哈希取模:hash(key)%N
      ◆ 范围:可以是 ID 范围也可以是时间范围
      ◆ 映射表:使用单独的一个数据库来存储映射关系
    • Sharding 存在的问题
      1)事务问题
      使用分布式事务来解决,比如 XA 接口
      2)连接
      可以将原来的连接分解成多个单表查询,然后在用户程序中进行连接。
      3)唯一性
      ◆ 使用全局唯一 ID (GUID)
      ◆ 为每个分片指定一个 ID 范围
      ◆ 分布式 ID 生成器(如 Twitter 的 Snowflake 算法)
  6. sql注入原理及解决方案?
    https://www.cnblogs.com/jiaoxiaohui/p/10760763.html

  7. 资料:
    为什么大家都说SELECT * 效率低 - 老刘的文章 - 知乎
    https://zhuanlan.zhihu.com/p/149981715
    资料:★
    https://thinkwon.blog.csdn.net/article/details/104778621
    https://thinkwon.blog.csdn.net/article/details/104778621?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-7.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-7.control
    https://mp.weixin.qq.com/s/J3kCOJwyv2nzvI0_X0tlnA

临时

  1. 数据库mysql索引? |6

    • 索引概念
      -- 官方介绍索引是帮助MySQL高效获取数据的数据结构。索引的功能相当于字典前面的拼音目录一样,能加快数据库的查询速度。(在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。)
      -- 索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。
      -- 索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。
    • 存储:
      一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往是存储在磁盘上的文件中的(可能存储在单独的索引文件中,也可能和数据一起存储在数据文件中)。
    • 分类:
      我们通常所说的索引,包括聚集索引、覆盖索引、组合索引、前缀索引、唯一索引等,没有特别说明,默认都是使用B+树结构组织(多路搜索树,并不一定是二叉的)的索引。
  2. 索引作用?

    • 索引:对数据库中一列或多列的值进行排序的一种结构
    • 作用:使用索引可以快速访问数据库表中特定信息(加速检索表中的数据)
    • 索引的作用?
      1)快速读取数据
      2)保证数据记录的唯一性
      3)实现表与表之间的参照完整性
      4)在使用orderby ,group by子句进行检索时,索引可以减少排序和分组的时间。
  3. 数据库索引的优缺点?

  4. 索引的优势和劣势?

    • 优势:
      1)可以提高数据检索的效率,降低数据库的IO成本,类似于书的目录。
      2)通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗。
      -- 被索引的列会自动进行排序,包括【单列索引】和【组合索引】,只是组合索引的排序要复杂一些。
      -- 如果按照索引列的顺序进行排序,对应order by语句来说,效率就会提高很多。
    • 劣势:
      1)创建索引和维护索引要耗费时间,并且随着数据量的增加所耗费的时间也会增加
      2)索引会占据磁盘空间,数据表中的数据也会有最大上线设置的,如果我们有大量的索引,索引文件可能会比数据文件更快达到上线值
      3)索引虽然会提高查询效率,但是会降低更新表的效率。比如每次对表进行增删改操作,MySQL不仅要保存数据,还有保存或者更新对应的索引文件。
  5. 索引的优点

    • 大大减少了服务器需要扫描的数据行数。
    • 帮助服务器避免进行排序和分组,以及避免创建临时表(B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,不需要排序和分组,也就不需要创建临时表)。
    • 将随机 I/O 变为顺序 I/O(B+Tree索引是有序的,会将相邻的数据都存储在一起)。
  6. 索引优点

    • 1)大大加快数据的检索速度
      2)创建唯一性索引,保证数据库中的每一行数据的唯一性。
      3)加速表与表之间的连接
      4)在使用分组和排序进行检索时,可以显著的减少查询的时间。
  7. Mysql索引的坏处是什么?
    1)创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
    2)索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
    3)当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
    https://blog.csdn.net/kennyrose/article/details/7532032

  8. InnoDB引擎的4大特性[-]
    插入缓冲(insert buffer)
    二次写(double write)
    自适应哈希索引(ahi)
    预读(read ahead)

  9. 索引,为什么选择自增?

    • InnoDB使用聚集索引,数据记录本身被存于主索引(一颗B+Tree)的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)。
    • 如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。
      这样就会形成一个紧凑的索引结构,近似顺序填满。由于每次插入时也不需要移动已有数据,因此效率很高,也不会增加很多开销在维护索引上。
    • 如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置,此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZETABLE来重建表并优化填充页面。
2-原理
  1. 索引模型是什么?

  2. 索引的底层/mysql索引数据结构? |3

    • 准确说,mysql默认的存储引擎 InnoDB使用的是B+树
  3. 索引原理?

    • 索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。
    • 索引的原理很简单,就是把无序的数据变成有序的查询
      1)把创建了索引的列的内容进行排序
      2)对排序结果生成倒排表
      3)在倒排表内容上拼上数据地址链
      4)在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据
      https://www.cnblogs.com/klb561/p/10666296.html
      https://blog.csdn.net/weixin_42181824/article/details/82261988
  4. 数据库的索引原理 ???
    通常是「平衡树」(非二叉),也就是b tree及其变种B+树。
    https://blog.csdn.net/kennyrose/article/details/7532032
    https://blog.csdn.net/z_ryan/article/details/79685072
    https://www.cnblogs.com/harderman-mapleleaves/p/4528212.html
    https://www.cnblogs.com/makai/p/10861296.html
    https://www.cnblogs.com/aspwebchh/p/6652855.html

  5. MySQL索引数据结构? |4

    • (b树,hash)
      -- 索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引,B+树索引等;常用的InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
      -- 索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。
    • B+ Tree 索引
      • 是大多数 MySQL 存储引擎的默认索引类型。
      -- 因为不再需要进行全表扫描,只需要对树进行搜索即可,所以查找速度快很多。
      -- 因为 B+ Tree 的有序性,所以除了用于查找,还可以用于排序和分组。
      -- 可以指定多个列作为索引列,多个索引列共同组成键。
      -- 适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。
      • InnoDB 的 B+Tree 索引分为主索引和辅助索引。主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
      在这里插入图片描述
      • 辅助索引的叶子节点的data域记录着主键的值,因此在使用辅助索引进行查找时,需要先查找到主键值,然后再到主索引中进行查找,这个过程也被称作回表
      在这里插入图片描述
    • 哈希索引
      • 哈希索引能以 O(1) 时间进行查找,但是失去了有序性:
      -- 无法用于排序与分组;
      -- 只支持精确查找,无法用于部分查找和范围查找。
      • InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
      • 类似于数据结构中简单实现的HASH表(散列表)一样,当我们在mysql中用哈希索引时,主要就是通过Hash算法(常见的Hash算法有直接定址法、平方取中法、折叠法、除数取余法、随机数法),将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash碰撞(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储。当然这只是简略模拟图。
      在这里插入图片描述
  6. 索引算法有哪些? [-]

    • 索引算法有 BTree算法和Hash算法
    • BTree算法
      BTree是最常用的mysql数据库索引算法,也是mysql默认的算法。因为它不仅可以被用在=,>,>=,<,<=和between这些比较操作符上,而且还可以用于like操作符,只要它的查询条件是一个不以通配符开头的常量, 例如:

      1. -- 只要它的查询条件是一个不以通配符开头的常量
      2. select * from user where name like 'jack%';
      3. -- 如果一通配符开头,或者没有使用常量,则不会使用索引,例如:
      4. select * from user where name like '%jack';
    • Hash算法
      Hash Hash索引只能用于对等比较,例如=,<=>(相当于=)操作符。由于是一次定位数据,不像BTree索引需要从根节点到枝节点,最后才能访问到页节点这样多次IO访问,所以检索效率远高于BTree索引。

  7. ??
    索引是在存储引擎中实现的,而不是在服务器层中实现的。所以,每种存储引擎的索引都不一定完全相同,并不是所有的存储引擎都支持所有的索引类型。

    • 1 B-Tree索引
    • 2 Hash索引
      如果多个值有相同的hash code,索引把它们的行指针用链表保存到同一个hash表项中。
    • 3 空间(R-Tree)索引
      MyISAM支持空间索引,主要用于地理空间数据类型。
    • 4 全文(Full-text)索引
      全文索引是MyISAM的一个特殊索引类型,主要用于全文检索。
    • 全文索引
      -- MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。
      -- 查找条件使用 MATCH AGAINST,而不是普通的 WHERE。
      -- 全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。
      -- InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。
    • 空间数据索引
      -- MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。
      -- 必须使用 GIS 相关的函数来维护数据。
  8. InnoDB和MyISAM
    1)InnoDB

    • InnoDB也使用B+Tree作为索引结构
    • InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。
    • InnoDB的辅助索引:InnoDB的所有辅助索引都引用主键作为data域。
    • InnoDB 表是基于聚簇索引建立的。
      2)MyISAM
    • MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址
    • MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。
    • 在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复
    • 同样也是一颗B+Tree,data域保存数据记录的地址。
    • MyISM使用的是非聚簇索引,
      3)问题:主键索引是聚集索引还是非聚集索引?
      在Innodb下主键索引是聚集索引,在Myisam下主键索引是非聚集索引
      https://www.cnblogs.com/jiawen010/p/11805241.html
  9. 主键索引与非主键索引有什么区别 ?

    • 主键索引和非主键索引的区别是:非主键索引的叶子节点存放的是主键的值,而主键索引的叶子节点存放的是整行数据,其中非主键索引也被称为二级索引,而主键索引也被称为聚簇索引。
  10. 什么是聚簇索引?何时使用聚簇索引与非聚簇索引 [-]

    • 聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
    • 非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行,myisam通过key_buffer把索引先缓存到内存中,当需要访问数据时(通过索引访问数据),在内存中直接搜索索引,然后通过索引找到磁盘相应数据,这也就是为什么索引不在key buffer命中时,速度慢的原因
    • 澄清一个概念:innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引,辅助索引叶子节点存储的不再是行的物理位置,而是主键值何时使用聚簇索引与非聚簇索引
      在这里插入图片描述
  11. 聚簇索引和非聚簇索引?
    高性能的索引策略
    1 聚簇索引(Clustered Indexes)
    https://www.cnblogs.com/whgk/p/6179612.html
    https://www.cnblogs.com/likeju/p/5409102.html
    2 聚簇索引和非聚簇索引

    • 聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。都是b+树
    • 场景
      平时,我们使用Mysql数据库,会为主键建立一个B+树索引,当我们基于主键搜索的时候,比如“where id = 666”,这时候,就会用到索引,将文件最终的存放地址找出来并加载,而当我们使用没有建立索引的字段进行搜索的时候,比如“where parm = ’笑笑笑笑‘”,这样子的,就不会用到索引
    • 聚簇索引
      就是为这个不是主键的字段建立了索引,并且B+树的叶子节点最终保存了数据的行信息,可以通过这个索引直接获取行数据,而不必再通过主键索引查找数据
    • 非聚簇索引
      就是当我们为不是主键的字段建立索引的时候,在这个索引B+树结构的叶子结点中,并不会想主键索引那样存储行数据,而是存储了主键的信息,找到主键之后,在通过主键索引查找出数据来
    • Mysql5.7之后默认的存储引擎是InnoDb,InnoDb的索引是聚簇索引
    • 优缺点
      聚簇索引的优点就是数据发生变化的时候,不用再去维护非主键索引了,因为存储的知识主键的信息,由于行数据和叶子节点存储在一起,主键和行数据是一起被载入内存的,找到叶子节点就可以立刻返回数据。https://blog.csdn.net/weixin_37641413/article/details/97823120
    • 区别 在《数据库原理》一书中是这么解释聚簇索引和非聚簇索引的区别的:
      聚簇索引的叶子节点就是数据节点,而非聚簇索引的叶子节点仍然是索引节点,只不过有指向对应数据块的指针。
      https://www.cnblogs.com/jiawen010/p/11805241.html
      https://www.cnblogs.com/qlqwjy/p/8592684.html
  12. 聚簇索引和非聚簇索引的区别?
    答了聚簇索引:结构、建立(主键上建立、无主键则选择第一个唯一索引,若都没有主键和唯一索引则隐藏有一个字段实现聚簇索引)
    非聚簇结构、

  13. 非聚簇索引一定会回表查询吗?[-]

    • 不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。
    • 举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行select age from employee where age<20的查询时,在索引的叶子节点上,已经包含了age信息,不会再次进行回表查询。
  14. B+树在满足聚簇索引和覆盖索引的时候不需要回表查询数据,[-]
    -- 在B+树的索引中,叶子节点可能存储了当前的key值,也可能存储了当前的key值以及整行的数据,这就是聚簇索引和非聚簇索引。 在InnoDB中,只有主键索引是聚簇索引,如果没有主键,则挑选一个唯一键建立聚簇索引。如果没有唯一键,则隐式的生成一个键来建立聚簇索引。
    -- 当查询使用聚簇索引时,在对应的叶子节点,可以获取到整行数据,因此不用再次进行回表查询。

  15. Mysql回表?回表问题?

    • 回表概念:
      -- 回表就是先通过数据库索引扫描出数据所在的行,再通过行主键id取出索引中未提供的数据,即基于非主键索引的查询需要多扫描一棵索引树。
      -- 因此,可以通过索引先查询出id字段,再通过主键id字段,查询行中的字段数据,即通过再次查询提供MySQL查询速度。
      在这里插入图片描述
  16. 回表的过程,磁盘读几次,跟数据在内存中比哪个快?

    • 非聚簇索引所要求的字段如果全部命中了索引,不需要会表
    • 回表:再重新遍历索引树,双倍io
  17. 联合索引是什么?为什么需要注意联合索引中的顺序?[-]

    • MySQL可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。
    • 具体原因为:
    • MySQL使用索引时需要索引有序,假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。
    • 当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。
  18. Mysql对联合索引有优化么?会自动调整顺序么?哪个版本开始优化?

  19. 前缀索引 [-]

    • 语法:index(field(10)),使用字段值的前10个字符建立索引,默认是使用字段的全部内容建立索引。
    • 前提:前缀的标识度高。比如密码就适合建立前缀索引,因为密码几乎各不相同。
    • 实操的难度:在于前缀截取的长度。
    • 可以利用select count(*)/count(distinct left(password,prefixLen));,通过从调整prefixLen的值(从1自增)查看不同前缀长度的一个平均匹配度,接近1时就可以了(表示一个密码的前prefixLen个字符几乎能确定唯一一条记录)
  20. 什么是最左前缀原则?什么是最左匹配原则 [-]

    • 顾名思义,就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。
    • 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
    • =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式
  21. 联合索引的最左匹配原则? |2
    (答了:建立多列索引、多列索引顺序性和索引下推)

  22. 从底层解释最左匹配原则?

  23. mysql存储引擎索引优化?

    • 避免回表
    • 使用联合索引
  24. mysql索引有哪些,都有什么特点?

  25. 数据库索引类型有哪些? |5
  26. 索引的分类/索引有哪几种类型?

    • 主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。
    • 唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。
      • 可以通过 ALTER TABLE table_name ADD UNIQUE (column); 创建唯一索引
      • 可以通过 ALTER TABLE table_name ADD UNIQUE (column1,column2); 创建唯一组合索引
    • 普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。
      • 可以通过ALTER TABLE table_name ADD INDEX index_name (column);创建普通索引
      • 可以通过ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);创建组合索引
    • 全文索引: 是目前搜索引擎使用的一种关键技术。
      • 可以通过ALTER TABLE table_name ADD FULLTEXT (column);创建全文索引
  27. 索引的分类?

    • mytable表:

      1. CREATE TABLE mytable(
      2. ID INT NOT NULL,
      3. username VARCHAR(16) NOT NULL,
      4. city VARCHAR(50) NOT NULL,
      5. age INT NOT NULL
      6. );
    • 1)单例索引
      一个索引只包含单个列,但一个表中可以有多个单列索引。
      ① 普通索引
      没有什么限制,允许在定义索引的列中插入重复值和空值。

      1. 创建1:创建索引
      2. CREATE INDEX indexName ON mytable(username(length));
      3. -- 如果是CHARVARCHAR类型,length可以小于字段实际长度;如果是BLOBTEXT类型,必须指定 length,下同。
      4. 创建2:修改表结构
      5. ALTER mytable ADD INDEX [indexName] ON (username(length))
      6. 创建3:创建表的时候直接指定
      7. CREATE TABLE mytable(:
      8. ID INT NOT NULL,
      9. username VARCHAR(16) NOT NULL,
      10. INDEX [indexName] (username(length))
      11. );
      12. 删除索引的语法:
      13. DROP INDEX [indexName] ON mytable;

    ② 唯一索引
    索引列中的值必须是唯一的,但是允许为空值。

    1. 创建索引
    2. CREATE UNIQUE INDEX indexName ON mytable(username(length))
    3. 修改表结构
    4. ALTER mytable ADD UNIQUE [indexName] ON (username(length))
    5. 创建表的时候直接指定
    6. CREATE TABLE mytable(
    7. ID INT NOT NULL,
    8. username VARCHAR(16) NOT NULL,
    9. UNIQUE [indexName] (username(length))
    10. );

    ③ 主键索引
    是一种特殊的唯一索引,不允许有空值。

    1. 创建:一般是在建表的时候同时创建主键索引:
    2. CREATE TABLE mytable(
    3. ID INT NOT NULL,
    4. username VARCHAR(16) NOT NULL,
    5. PRIMARY KEY(ID)
    6. );

    当然也可以用 ALTER 命令。记住:一个表只能有一个主键。

    • 2) 组合索引
      在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时遵循最左前缀集合。

      1. 创建:将 name, city, age建到一个索引里:
      2. ALTER TABLE mytable ADD INDEX name_city_age (name(10),city,age);
      3. 使用:“最左前缀”都会用到
      4. usernname,city,age | usernname,city | usernname
      5. SELECT * FROM mytable WHREE username="admin" AND city="郑州"
      6. SELECT * FROM mytable WHREE username="admin"
    • 3) 全文索引
      在一堆文字中,通过其中的某个关键字等,就能找到该字段所属的记录行,比如有"你是个大煞笔,二货 ..." 通过大煞笔,可能就可以找到该条记录。
      只有在MyISAM引擎上才能使用,只能在CHAR,VARCHAR,TEXT类型字段上使用全文索引。
      FULLTEXT

    • 4) 空间索引
      空间索引是对空间数据类型的字段建立的索引,MySQL中的空间数据类型有四种,GEOMETRY、POINT、LINESTRING、POLYGON。
      在创建空间索引时,使用SPATIAL关键字。
      要求,引擎为MyISAM,创建空间索引的列,必须将其声明为NOT NULL。
      SPATIAL
  28. mysql索引类型?
    单列索引(普通索引,唯一索引,主键索引)、组合索引、全文索引、空间索引

  29. 索引之间的区别
    1) 单列索引:一个索引只包含单个列,但一个表中可以有多个单列索引。

    • 普通索引:没有什么限制,允许在定义索引的列中插入重复值和空值;
    • 唯一索引:索引列中的值必须是唯一的,允许为null
    • 主键索引:一种特殊的唯一索引,不允许有null。
      2)组合索引
      在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时遵循最左前缀集合;
      3)全文索引:只有在MyISAM引擎上才能使用,只能在CHAR,VARCHAR,TEXT类型字段上使用全文索引,在一堆文字中,通过其中的某个关键字等,就能找到该字段所属的记录行;
      4)空间索引:空间索引是对空间数据类型的字段建立的索引,MySQL中的空间数据类型有四种,GEOMETRY、POINT、LINESTRING、POLYGON。
      在创建空间索引时,使用SPATIAL关键字。
      要求,引擎为MyISAM,创建空间索引的列,必须将其声明为NOT NULL。
3-创建索引|使用
  1. 怎么建立索引1?
    select * from t where b=1;
    Select * from t where a=1 and b=1;
    先说需要建两个索引,后来反应过来了,建一个联合索引。

  2. 怎么建索引2?
    select * from a=1 and b>2 or c in(1,2,3)

  3. 场景题:音乐界面和评论,如何建立表和索引

  4. Select * from t where c=1;
    C是非主键索引,问几次磁盘io,b+索引树高度3。

  5. mysql给性别建立索引 和 直接查询 有区别吗?

    • 重复性较强的字段,不适合添加索引,列的离散度太低,索引查询效率很低的
    • 建了索引数据库也不一定会用到,只会白白增加索引维护的额外开销,因为索引也是需要存储的,所以插入和更新的写入操作,同时需要插入和更新你这个字段的索引的.
      所以说,唯一性太差的字段不需要创建索引,即便用于where条件.
      https://www.cnblogs.com/mkl34367803/p/13096564.html
  6. 只有一个字段,字段值都是汉字,建立索引后是如何排序的 ?

  7. 索引:A>0 B =3 C=1 会不会走索引?

  8. 一列只有8中情况的数据,另一列不确认,哪一列适合建索引?

  9. 索引语句

    1. CREATE TABLE table_name
    2. [col_name data type]
    3. [unique|fulltext]
    4. [index|key]
    5. [index_name](col_name[length])
    6. [asc|desc]
    • col_name:需要创建索引的字段列
    • unique|fulltext:可选参数,唯一索引|全文索引
    • index和key:两者作用相同,用来指定创建索引
    • index_name:指定索引的名称,可选参数,默认col_name为索引值
    • length:可选参数,索引的长度,只有字符串类型的字段才能指定索引长度
    • asc或desc指定升序或降序的索引值存储
      https://www.cnblogs.com/luyucheng/p/6289714.html
  10. 怎么建索引? |2
    https://www.cnblogs.com/whgk/p/6179612.html
    创建索引的三种方式,删除索引? [-]

    • 第一种方式:在执行CREATE TABLE时创建索引

      1. CREATE TABLE user_index2 (
      2. id INT auto_increment PRIMARY KEY,
      3. first_name VARCHAR (16),
      4. last_name VARCHAR (16),
      5. id_card VARCHAR (18),
      6. information text,
      7. KEY name (first_name, last_name),
      8. FULLTEXT KEY (information),
      9. UNIQUE KEY (id_card)
      10. );
    • 第二种方式:使用ALTER TABLE命令去增加索引

      1. ALTER TABLE table_name ADD INDEX index_name (column_list);

    -- ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。
    -- 其中,table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。
    -- 索引名index_name可自己命名,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。

    • 第三种方式:使用CREATE INDEX命令创建
      1. CREATE INDEX index_name ON table_name (column_list);

    -- CREATE INDEX可对表增加普通索引或UNIQUE索引。(但是,不能创建PRIMARY KEY索引)

    • 删除索引
      -- 根据索引名删除普通索引、唯一索引、全文索引:alter table 表名 drop KEY 索引名
      1. alter table user_index drop KEY name;
      2. alter table user_index drop KEY id_card;
      3. alter table user_index drop KEY information;

    -- 删除主键索引:alter table 表名 drop primary key(因为主键只有一个)。这里值得注意的是,如果主键自增长,那么不能直接执行此操作(自增长依赖于主键索引):
    在这里插入图片描述
    -- 需要取消自增长再行删除:
    -- 但通常不会删除主键,因为设计主键一定与业务逻辑无关。

    1. alter table user_index
    2. -- 重新定义字段
    3. MODIFY id int,
    4. drop PRIMARY KEY
  11. 百万级别或以上的数据如何删除 [-]

    • 关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。所以,在我们删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的。
      1) 所以我们想要删除百万数据的时候可以先删除索引(此时大概耗时三分多钟)
      2) 然后删除其中无用数据(此过程需要不到两分钟)
      3) 删除完成后重新创建索引(此时数据较少了)创建索引也非常快,约十分钟左右。
      4) 与之前的直接删除绝对是要快速很多,更别说万一删除中断,一切删除会回滚。那更是坑了。
  12. 创建索引的原则(重中之重)

    • 索引虽好,但也不是无限制的使用,最好符合一下几个原则
      1) 最左前缀匹配原则,组合索引非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
      2)较频繁作为查询条件的字段才去创建索引
      3)更新频繁字段不适合创建索引
      4)若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)
      5)尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
      6)定义有外键的数据列一定要建立索引。
      7)对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。
      8)对于定义为text、image和bit的数据类型的列不要建立索引。
  13. 建索引时需要注意什么? [-]

    • 非空字段:应该指定列为NOT NULL,除非你想存储NULL。在mysql中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值;
    • 取值离散大的字段:(变量各个取值之间的差异程度)的列放到联合索引的前面,可以通过count()函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高;
    • 索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次IO操作获取的数据越大效率越高。
  14. MySQL建立索引有什么规则 ?

  15. 索引的使用注意事项
    1)哪些情况下不需要使用索引
    2)索引不可用的情况
    3)索引不会被使用的几种情况
    https://www.cnblogs.com/xyhero/p/b0ad525c6a6a5ed2bd7f40918c5dbd98.html

  16. 使用原则:
    1、对经常更新的表就避免对其进行过多的索引,对经常用于查询的字段应该创建索引,
    2、数据量小的表最好不要使用索引,因为由于数据较少,可能查询全部数据花费的时间比遍历索引的时间还要短,索引就可能不会产生优化效果。
    3、在一同值少的列上(字段上)不要建立索引,比如在学生表的"性别"字段上只有男,女两个不同值。相反的,在一个字段上不同值较多可是建立索引

  17. 索引的使用条件? [-]

    • 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效;
    • 对于中到大型的表,索引就非常有效;
    • 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。
  18. 为什么对于非常小的表,大部分情况下简单的全表扫描比建立索引更高效?

    • 如果一个表比较小,那么显然直接遍历表比走索引要快(因为需要回表)。
    • 注:首先,要注意这个答案隐含的条件是查询的数据不是索引的构成部分,否也不需要回表操作。其次,查询条件也不是主键,否则可以直接从聚簇索引中拿到数据。
  19. 索引设计的原则?[-]
    适合索引的列是出现在where子句中的列,或者连接子句中指定的列
    基数较小的类,索引效果较差,没有必要在此列建立索引
    使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空间
    不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。

  20. 哪些建立索引比较适合(比如性别建立索引合适吗)
    索引是建立在数据库表中的某些列的上面。在创建索引的时候,应该考虑在哪些列上可以创建索引,在哪些列上不能创建索引。一般来说,应该在这些列上创建索引:在经常需要搜索的列上,可以加快搜索的速度;在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构;在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度;在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。
    https://blog.csdn.net/kennyrose/article/details/7532032

5-redis

redis启动流程

一条指令的执行

redis数据结构

排行榜--微信步数

lua脚本原理

https://zhuanlan.zhihu.com/p/48337244

https://wiki.jikexueyuan.com/project/redis/lua.html

缓存

  1. 讲讲redis缓存?
    • 缓存能够有效地加速应用的读写速度,同时也可以降低后端负载。
    • 缓存的收益和成本?
      -- 左侧为客户端直接调用存储层的架构,右侧为比较典型的缓存层+存储层架构;
      在这里插入图片描述
      -- 收益
      ① 加速读写:因为缓存通常都是全内存的(例如Redis、Memcache),而存储层通常读写性能不够强悍(例如MySQL),通过缓存的使用可以有效地加速读写,优化用户体验。
      ② 降低后端负载:帮助后端减少访问量和复杂计算(例如很复杂的SQL语句),在很大程度降低了后端的负载。
      -- 成本
      ① 数据不一致性:缓存层和存储层的数据存在着一定时间窗口的不一致性,时间窗口跟更新策略有关。
      ② 代码维护成本:加入缓存后,需要同时处理缓存层和存储层的逻辑,增大了开发者维护代码的成本。
      ③ 运维成本:以Redis Cluster为例,加入后无形中增加了运维成本。
    • 缓存的使用场景基本包含如下两种?
      ① 开销大的复杂计算:以MySQL为例子,一些复杂的操作或者计算(例如大量联表操作、一些分组计算),如果不加缓存,不但无法满足高并发量,同时也会给MySQL带来巨大的负担。
      ② 加速请求响应:即使查询单条后端数据足够快(例如select*from table where id=),那么依然可以使用缓存,以Redis为例子,每秒可以完成数万次读写,并且提供的批量操作可以优化整个IO链的响应时间。
    • 缓存的缓存更新策略的选择和使用场景?
      -- 缓存中的数据通常都是有生命周期的,需要在指定时间后被删除或更新,以保证缓存空间在一个可控的范围。
      1)LRU/LFU/FIFO算法剔除
      ► 使用场景:用于缓存使用量超过了预设的最大值时候,如何对现有的数据进行剔除。例如Redis使用maxmemory-policy这个配置作为内存最大值后对于数据的剔除策略。
      ► 一致性:要清理哪些数据是由具体算法决定,开发人员只能决定使用哪种算法,所以数据的一致性是最差的。
      ► 维护成本:算法不需要开发人员自己来实现,通常只需要配置最大maxmemory和对应的策略即可。开发人员只需要知道每种算法的含义,选择适合自己的算法即可。
      2)超时剔除
      ► 使用场景:超时剔除通过给缓存数据设置过期时间,让其在过期时间后自动删除,例如Redis提供的expire命令。如果业务可以容忍一段时间内,缓存层数据和存储层数据不一致,那么可以为其设置过期时间。在数据过期后,再从真实数据源获取数据,重新放到缓存并设置过期时间。例如一个视频的描述信息,可以容忍几分钟内数据不一致,但是涉及交易方面的业务,后果可想而知。
      ► 一致性:一段时间窗口内(取决于过期时间长短)存在一致性问题,即缓存数据和真实数据源的数据不一致。
      ► 维护成本:维护成本不是很高,只需设置expire过期时间即可,当然前提是应用方允许这段时间可能发生的数据不一致。
      3)主动更新
      ► 使用场景:应用方对于数据的一致性要求高,需要在真实数据更新后,立即更新缓存数据。例如可以利用消息系统或者其他方式通知缓存更新。
      ► 一致性:一致性最高,但如果主动更新发生了问题,那么这条数据很可能很长时间不会更新,所以建议结合超时剔除一起使用效果会更好。
      ► 维护成本:维护成本会比较高,开发者需要自己来完成更新,并保证更新操作的正确性。
      -- 应用建议
      ► 低一致性业务建议配置最大内存和淘汰策略的方式使用。
      ► 高一致性业务可以结合使用超时剔除和主动更新,这样即使主动更新出了问题,也能保证数据过期时间后删除脏数据。
      在这里插入图片描述
    • 缓存粒度控制方法。
      -- 缓存全部数据和部分数据;
      在这里插入图片描述
    • 穿透问题优化。
    • 无底洞问题优化。
    • 雪崩问题优化。
    • 热点key重建优化。
1-穿透|击穿|雪崩

|aobing|

  1. redis的缓存穿透、缓存击穿、缓存雪崩原因现象和解决措施? |3

  2. redis缓存穿透与解决措施? |3 (rky

    • 是什么?
      指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层,如图整个过程分为如下3步:
      1)缓存层不命中。
      2)存储层不命中,不将空结果写回缓存。
      3)返回空结果。
      在这里插入图片描述
    • 后果:
      ► 导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
      ► 缓存穿透问题可能会使后端存储负载加大,由于很多后端存储不具备高并发性,甚至可能造成后端存储宕掉。通常可以在程序中分别统计总调用数、缓存层命中数、存储层命中数,如果发现大量存储层空命中,可能就是出现了缓存穿透问题。
    • 基本原因有两个:
      ► 第一,自身业务代码或者数据出现问题;
      ► 第二,一些恶意攻击、爬虫等造成大量空命中。
    • 解决缓存穿透问题:
      1)缓存空对象
      如图,当第2步存储层不命中后,仍然将空对象保留到缓存层中,之后再访问这个数据将会从缓存中获取,这样就保护了后端数据源。
      在这里插入图片描述
      ▷ 缓存空对象会有两个问题:
      ① 空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
      ② 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。
      例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
      ▷ 缓存空对象的实现代码:
      1. String get(String key) {
      2. // 从缓存中获取数据
      3. String cacheValue = cache.get(key);
      4. // 缓存为空
      5. if (StringUtils.isBlank(cacheValue)) {
      6. // 从存储中获取
      7. String storageValue = storage.get(key);
      8. cache.set(key, storageValue);
      9. // 如果存储数据为空,需要设置一个过期时间(300秒)
      10. if (storageValue == null) {
      11. cache.expire(key, 60 * 5);
      12. }
      13. return storageValue;
      14. } else {
      15. // 缓存非空
      16. return cacheValue;
      17. }
      18. }

    2)布隆过滤器拦截
    ► 如图,在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。
    ►
    ► 场景:
    例如:一个推荐系统有4亿个用户id,每个小时算法工程师会根据每个用户之前历史行为计算出推荐数据放到存储层中,但是最新的用户由于没有历史行为,就会发生缓存穿透的行为,为此可以将所有推荐数据的用户做成布隆过滤器。如果布隆过滤器认为该用户id不存在,那么就不会访问存储层,在一定程度保护了存储层。
    ► 实现:
    有关布隆过滤器的相关知识,可以参考:https://en.wikipedia.org/wiki/Bloom_filter可以利用Redis的Bitmaps实现布隆过滤器,GitHub上已经开源了类似的方案,读者可以进行参考:https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter
    ► 应用场景:
    适用于数据命中不高、数据相对固定、实时性低(通常是数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用少。
    ► 两种解决方法的对比(实际上这个问题是一个开放问题,有很多解决方法)
    在这里插入图片描述

  3. redis缓存雪崩与解决措施? |3 (rky

    • 是什么?
      由于缓存层承载着大量请求,有效地保护了存储层,但是如果缓存层由于某些原因不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。缓存雪崩的英文原意是stampeding herd(奔逃的野牛),指的是缓存层宕掉后,流量会像奔逃的野牛一样,打向后端存储。
      在这里插入图片描述
    • 三个方面预防和解决缓存雪崩问题:
      1)保证缓存层服务高可用性。
      和飞机都有多个引擎一样,如果缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如前面介绍过的Redis Sentinel和Redis Cluster都实现了高可用。
      2)依赖隔离组件为后端限流并降级。
      无论是缓存层还是存储层都会有出错的概率,可以将它们视同为资源。作为并发量较大的系统,假如有一个资源不可用,可能会造成线程全部阻塞(hang)在这个资源上,造成整个系统不可用。
      ▷ 降级机制在高并发系统中是非常普遍的:比如推荐服务中,如果个性化推荐服务不可用,可以降级补充热点数据,不至于造成前端页面是开天窗。在实际项目中,我们需要对重要的资源(例如Redis、MySQL、HBase、外部接口)都进行隔离,让每种资源都单独运行在自己的线程池中,即使个别资源出现了问题,对其他服务没有影响。但是线程池如何管理,比如如何关闭资源池、开启资源池、资源池阀值管理,这些做起来还是相当复杂的。
      3)提前演练。
      在项目上线前,演练缓存层宕掉后,应用以及后端的负载情况以及可能出现的问题,在此基础上做一些预案设定。
  4. 见你写了个加随机数预防缓存雪崩,解释一下?

  5. redis缓存击穿(热点数据集中失效/热点key重建优化)与解决措施? |3 (rky

    • 缓存+过期时间-策略:
      ► 优:加速数据读写、保证数据的定期更新,基本能够满足绝大部分需求。
    • 问题:
      如下两个问题如果同时出现,在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
      ① 当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。
      ② 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等。
      在这里插入图片描述
    • 解决:
      1)互斥锁(mutex key)
      ► 此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可,整个过程如图。
      在这里插入图片描述
      ► 下面代码使用Redis的setnx命令实现上述功能:
      1. String get(String key) {
      2. // 从Redis中获取数据
      3. String value = redis.get(key);
      4. // 如果value为空,则开始重构缓存
      5. if (value == null) {
      6. // 只允许一个线程重构缓存,使用nx,并设置过期时间ex
      7. String mutexKey = "mutext:key:" + key;
      8. if (redis.set(mutexKey, "1", "ex 180", "nx")) {
      9. // 从数据源获取数据
      10. value = db.get(key);
      11. // 回写Redis,并设置过期时间
      12. redis.setex(key, timeout, value);
      13. // 删除key_mutex
      14. redis.delete(mutexKey);
      15. }
      16. // 其他线程休息50毫秒后重试
      17. else {
      18. Thread.sleep(50);
      19. get(key);
      20. }
      21. }
      22. return value;
      23. }

    1)从Redis获取数据,如果值不为空,则直接返回值;否则执行下面的2.1)和2.2)步骤。
    2.1)如果set(nx和ex)结果为true,说明此时没有其他线程重建缓存,那么当前线程执行缓存构建逻辑。
    2.2)如果set(nx和ex)结果为false,说明此时已经有其他线程正在执行构建缓存的工作,那么当前线程将休息指定时间(例如这里是50毫秒,取决于构建缓存的速度)后,重新执行函数,直到获取到数据。
    2)永远不过期
    ► “永远不过期”包含两层意思:
    -- 从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期。
    -- 从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
    在这里插入图片描述
    ► 从实战看,此方法有效杜绝了热点key产生的问题,但唯一不足的就是重构缓存期间,会出现数据不一致的情况,这取决于应用方是否容忍这种不一致。
    ► 代码实现:

    1. String get(final String key) {
    2. V v = redis.get(key);
    3. String value = v.getValue();
    4. // 逻辑过期时间
    5. long logicTimeout = v.getLogicTimeout();
    6. // 如果逻辑过期时间小于当前时间,开始后台构建
    7. if (v.logicTimeout <= System.currentTimeMillis()) {
    8. String mutexKey = "mutex:key:" + key;
    9. if (redis.set(mutexKey, "1", "ex 180", "nx")) {
    10. // 重构缓存
    11. threadPool.execute(new Runnable() {
    12. public void run() {
    13. String dbValue = db.get(key);
    14. redis.set(key, (dbvalue,newLogicTimeout));
    15. redis.delete(mutexKey);
    16. }
    17. });
    18. }
    19. }
    20. return value;
    21. }
    • 缓存指标对比解决方案?
      ► 作为一个并发量较大的应用,在使用缓存时有三个目标:
      第一,加快用户访问速度,提高用户体验。
      第二,降低后端负载,减少潜在的风险,保证系统平稳。
      第三,保证数据“尽可能”及时更新。下面将按照这三个维度对上述两种解决方案进行分析。
      ► 互斥锁(mutex key):这种方案思路比较简单,但是存在一定的隐患,如果构建缓存过程出现问题或者时间较长,可能会存在死锁和线程池阻塞的风险,但是这种方法能够较好地降低后端存储负载,并在一致性上做得比较好。
    • “永远不过期”:这种方案由于没有设置真正的过期时间,实际上已经不存在热点key产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。
      在这里插入图片描述
  6. 为什么选择Redis作为缓存?
    -- 收益
    ① 加速读写:因为缓存通常都是全内存的(例如Redis、Memcache),而存储层通常读写性能不够强悍(例如MySQL),通过缓存的使用可以有效地加速读写,优化用户体验。
    ② 降低后端负载:帮助后端减少访问量和复杂计算(例如很复杂的SQL语句),在很大程度降低了后端的负载。

  7. 缓存一致性相关问题?

    • 问题:
      1)如何保证mysql与redis的双写一致性。
      (最终一致性和强一致性)
      如果对数据有强一致性要求,不能放缓存。
      2)怎么保证redis与Mysql的数据一致性(秒杀预热数据的一致性,就解释了不需要一致性,只保证Mysql库存正确即可之类的)
      3)怎么实现redis,mysql数据一致性,为什么不采取更新数据库,再更新缓存?这样做有什么不好?怎么改进呢?等等
      4)项目中的缓存不一致怎么解决的
      -- 不一致原因:先操作缓存,在写数据库成功之前,如果有读请求发生,可能导致旧数据入缓存,引发数据不一致。
      -- 解决:串行化
    • 解决:
      https://www.jianshu.com/p/c72ba33ea49e
2-项目和场景题
2.1 场景题
  1. 设计一个缓存商品的方案,什么时候保存商品到缓存,什么时候删除缓存的商品?

  2. 如何设计一个秒杀系统?
    ① 怎么测试秒杀
    ② Redis怎么库存预热,RabbitMQ怎么进行队列削峰

  3. 如何解决一个高并发场景呢?
    (答数据库主从复制读写分离,分库分表,服务器划分不通服务或者负载均衡,加消息队列和缓存)

2.2 项目
  1. Redis项目中用来做什么 ?

  2. 你项目如果用redis改进,怎么改?

缓存与数据库的双写一致性

内存管理

  1. 基础知识点:

    • 内存相关配置
      在这里插入图片描述
    • Redis内存管理:
      主要通过控制内存上限和回收策略实现;
      1)Redis使用maxmemory参数限制最大可用内存。限制内存的目的主要有:
      ·用于缓存场景,当超出内存上限maxmemory时使用LRU等删除策略释放空间。
      ·防止所用内存超过服务器物理内存。
      2)Redis的内存回收机制主要体现在以下两个方面:
      ·删除到达过期时间的键对象。
      ·内存使用达到maxmemory上限时触发内存溢出控制策略。
  2. redis缓存回收机制?

    • 因为C语言并不具备自动内存回收功能,所以Redis在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。
      每个对象的引用计数信息由redisObject结构的refcount属性记录:

      1. typedef struct redisObject {
      2. // ...
      3. // 引用计数
      4. int refcount;
      5. // ...
      6. } robj;
    • 对象的引用计数信息会随着对象的使用状态而不断变化:
      ·在创建一个新对象时,引用计数的值会被初始化为1;
      ·当对象被一个新程序使用时,它的引用计数值会被增一;
      ·当对象不再被一个程序使用时,它的引用计数值会被减一;
      ·当对象的引用计数值变为0时,对象所占用的内存会被释放。

    • 生命周期
      对象的整个生命周期可以划分为创建对象、操作对象、释放对象三个阶段。作为例子,以下代码展示了一个字符串对象从创建到释放的整个过程(其他不同类型的对象也会经历类似的过程):

      1. // 创建一个字符串对象s,对象的引用计数为1
      2. robj *s = createStringObject(...)
      3. //对象s执行各种操作...
      4. // 将对象s 的引用计数减一,使得对象的引用计数变为0
      5. // 导致对象s 被释放
      6. decrRefCount(s)
    • API
      修改对象引用计数的API,这些API分别用于增加、减少、重置对象的引用计数。
      在这里插入图片描述

  3. 什么是内存碎片,产生的原因?

    • 内存分配器为了更好地管理和重复利用内存,分配内存策略一般采用固定范围的内存块进行分配。
      -- 例如jemalloc在64位系统中将内存空间划分为:小、大、巨大三个范围。每个范围内又划分为多个小的内存块单位。比如当保存5KB对象时jemalloc可能会采用8KB的块存储,而剩下的3KB空间变为了内存碎片不能再分配给其他对象存储。是jemalloc针对碎片化问题专门做了优化,一般不会存在过度碎片化的问题;
    • 原因:以下场景容易出现高内存碎片问题:
      -- 频繁做更新操作,例如频繁对已存在的键执行append、setrange等更新操作。
      -- 大量过期键删除,键对象过期删除后,释放的空间无法得到充分利用,导致碎片率上升。
    • 常见的解决:
      1)数据对齐:在条件允许的情况下尽量做数据对齐,比如数据尽量采用数字类型或者固定长度字符串等,但是这要视具体的业务而定,有些场景无法做到。
      2)安全重启:重启节点可以做到内存碎片重新整理,因此可以利用高可用架构,如Sentinel或Cluster,将碎片率过高的主节点转换为从节点,进行安全重启。
  4. redis数据达到多少是阈值?

    • Redis使用maxmemory参数限制最大可用内存。
    • 注意:
      maxmemory限制的是Redis实际使用的内存量,也就是used_memory统计项对应的内存。由于内存碎片率的存在,实际消耗的内存可能会比maxmemory设置的更大,实际使用时要小心这部分内存溢出。
  5. redis最大内存设置了多少?

  6. redis为什么要设置过期时间?

  7. 过期时间是怎么设置的?

    • 通过EXPIRE key seconds命令来设置数据的过期时间
    • Redis有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除):
      ·EXPIRE命令用于将键key的生存时间设置为ttl秒。
      ·PEXPIRE命令用于将键key的生存时间设置为ttl毫秒。
      ·EXPIREAT命令用于将键key的过期时间设置为timestamp所指定的秒数时间戳。
      ·PEXPIREAT命令用于将键key的过期时间设置为timestamp所指定的毫秒数时间戳。
    • 虽然有多种不同单位和不同形式的设置命令,但实际上EXPIRE、PEXPIRE、EXPIREAT三个命令都是使用PEXPIREAT命令来实现的:无论客户端执行的是以上四个命令中的哪一个,经过转换之后,最终的执行效果都和执行PEXPIREAT命令一样。
  8. redis key 的过期键删除策略?

    • Redis所有的键都可以设置过期属性,内部保存在过期字典中。由于进程内保存大量的键,维护每个键精准的过期删除机制会导致消耗大量的CPU,对于单线程的Redis来说成本过高,实现过期键的内存回收。
    • 三种不同的删除策略:
      ·定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
      ·惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
      ·定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
    • 在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。
  9. 定期删除怎么实现的,是开启一个新进程还是停止工作去删除?

    • 定期删除策略的实现
      过期键的定期删除策略由redis.c/activeExpireCycle函数实现,每当Redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。
    • 整个过程可以用伪代码描述如下:

      1. # 默认每次检查的数据库数量
      2. DEFAULT_DB_NUMBERS = 16
      3. # 默认每个数据库检查的键数量
      4. DEFAULT_KEY_NUMBERS = 20
      5. # 全局变量,记录检查进度
      6. current_db = 0
      7. def activeExpireCycle():
      8. # 初始化要检查的数据库数量
      9. # 如果服务器的数据库数量比DEFAULT_DB_NUMBERS要小,那么以服务器的数据库数量为准
      10. if server.dbnum < DEFAULT_DB_NUMBERS:
      11. db_numbers = server.dbnum
      12. else:
      13. db_numbers = DEFAULT_DB_NUMBERS
      14. # 遍历各个数据库
      15. for i in range(db_numbers):
      16. # 如果current_db的值等于服务器的数据库数量,这表示检查程序已经遍历了服务器的所有数据库一次
      17. # 将current_db重置为0,开始新的一轮遍历
      18. if current_db == server.dbnum:
      19. current_db = 0
      20. # 获取当前要处理的数据库
      21. redisDb = server.db[current_db]
      22. # 将数据库索引增1,指向下一个要处理的数据库
      23. current_db += 1
      24. # 检查数据库键
      25. for j in range(DEFAULT_KEY_NUMBERS):
      26. # 如果数据库中没有一个键带有过期时间,那么跳过这个数据库
      27. if redisDb.expires.size() == 0: break
      28. # 随机获取一个带有过期时间的键
      29. key_with_ttl = redisDb.expires.get_random_key()
      30. # 检查键是否过期,如果过期就删除它
      31. if is_expired(key_with_ttl):
      32. delete_key(key_with_ttl)
      33. # 已达到时间上限,停止处理
      34. if reach_time_limit(): return
    • activeExpireCycle函数的工作模式可以总结如下:
      · 函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
      · 全局变量current_db会记录当前activeExpireCycle函数检查的进度,并在下一次activeExpireCycle函数调用时,接着上一次的进度进行处理。比如说,如果当前activeExpireCycle函数在遍历10号数据库时返回了,那么下次activeExpireCycle函数执行时,将从11号数据库开始查找并删除过期键。
      · 随着activeExpireCycle函数的不断执行,服务器中的所有数据库都会被检查一遍,这时函数将current_db变量重置为0,然后再次开始新一轮的检查工作。

  10. redis内存满了会怎么样

    • 当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略/淘汰策略。具体策略受maxmemory-policy参数控制,Redis支持6种策略;默认值noeviction。
  11. redis使用哪种淘汰策略?

    • noeviction
  12. Redis缓存(内存)淘汰策略 |2

    • Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。
      1)全局的键空间选择性移除
      noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
      allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
      allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
      2)设置过期时间的键空间选择性移除
      volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
      volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
      volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
    • 总结
      Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据。
    规则名称 规则说明
    volatile-lru 使用LRU算法删除一个键(只对设置了生存时间的键)
    allkeys-lru 使用LRU算法删除一个键
    volatile-random 随机删除一个键(只对设置了生存时间的键)
    allkeys-random 随机删除一个键
    volatile-ttl 删除生存时间最近的一个键
    noeviction 不删除键,只返回错误
    • LRU算法,least RecentlyUsed,最近最少使用算法。
  13. Java语言,实现一下LRU缓存?

  14. redis中lru咋实现的?

    • 实现思路:首先实现一个双向链表,每次有一个key被访问之后,就把被访问的key放到链表的头部。当缓存不够时,直接从尾部逐个摘除。
    • redis中LRU的思路:即如果一个key经常被访问,那么该key的idle time应该是最小的
    • lfu的思路:如果能够记录一个key被访问的次数,那么经常被访问的key最有可能再次被访问到。
      https://segmentfault.com/a/1190000017555834

哨兵

作者:ce、欢笙
链接:https://www.nowcoder.com/discuss/566337?source_id=discuss_experience_nctrack&channel=-1

定时任务的功能分别如下:通过向主从节点发送info命令获取最新的主从结构;通过发布订阅功能获取其他哨兵节点的信息;通过向其他节点发送ping命令进行心跳检测,判断是否下线。

在从节点中选择新的主节点:选择的原则是,首先过滤掉不健康的从节点;然后选择优先级最高的从节点(由slave-priority指定);如果优先级无法区分,则选择复制偏移量最大的从节点;如果仍无法区分,则选择runid最小的从节点。

8-redis分布式锁

应用场景:
1.互联网秒杀
2.抢优惠卷

客户端api:
Jedis
RedisTemplate:Springboot封装好的模板

例子:redis库存 stock-1操作
问题:
多线程并发

解决1:synchronized(this)
适合单体架构(1个tomcat示例运行)

集群:
集群、分布式(多个tomcat部署)
-每个tomcat jvm进程
synchronized 在jvm内部
-整体并发:
2个请求 ngnix 分发到2个tomcat 2个代码段同时操作
-更改端口号 启动程序 可创建多个tomcat实例
-Jmeter 模拟压测的工具

解决2:redis分布式锁-初级理解,问题很多
setnx k v:.setIfAbsent(k,v)
k不存在,设置v;k存在,v不变
完毕后删除k
问题1:
-异常:try finally,finally中删除k
-死锁:try finally中代码,在执行中服务挂了(重启或kill),k未释放,其他请求不到
问题2:死锁:
method:设置超时时间 expire
问题3:
setnx后,未expire成功
method:
原子操作:setIfAbsent()同时设置setnx、expire

高并发场景:
问题4:
-场景:线程A执行任务15',k过期时间10’,->执行过程中,10'后锁已过期;
线程B需8',获取锁,再进行5',锁被A释放;还有C、D...;
=>锁永久失效(k、v是一样的)
-method:
-删除对应k问题:生成唯一标识uuid:原v+“id”,释放前判断是否是自己的v
-程序没执行完,k过期问题:分线程执行定时器timer,k续命,设置过期时间的1/3,如过期时间10',则10/3=3,timer 3'执行一次(注:分布式锁,无论几个tomcat,多线程只有一个timer执行)

redisson框架:redisson.org
-上述思想的实现
-与Jedis类似,redis Java的一个客户端,更适合分布式
问题:redis主从结构
-主从复制时,主挂了,选举,从变成主,新主还没同步k(超高并发下)
m:redlock、zookeeper(推荐,内部也会保证一致性)
-性能优化:分段式

为什么选择redis而不是zookeeper?
-redis性能更高
-zk准确性高

skynet分布式锁代码

  1. /**
  2. * 不可重入分布式锁的实现 */
  3. @Slf4j
  4. @Component
  5. public class RedisDistributeLock implements DistributeLock {
  6. @Autowired
  7. private RedisTemplate<String, String> redisTemplate;
  8. /**默认key过期时间,单位秒
  9. * 5min*/
  10. private int defaultExpiration = 300;
  11. /** * 非阻塞请求锁 */
  12. @Override
  13. public boolean tryLock(String key, String req) {
  14. return tryLock(key, req, defaultExpiration);
  15. }
  16. /** * 非阻塞请求锁 -默认过期时间*/
  17. @Override
  18. public boolean tryLock(String key, String req, int expiration) {
  19. Boolean state = redisTemplate.opsForValue().setIfAbsent(key, req, expiration, TimeUnit.SECONDS);
  20. if (state != null && state) {
  21. log.info("持有分布式锁{}, req:{}:", key, req);
  22. return true;
  23. }
  24. return false;
  25. }
  26. /*** 阻塞请求锁
  27. * @param timeout 阻塞时长*/
  28. @Override
  29. public boolean tryLock(String key, String req, int expiration, int timeout) {
  30. long start = System.currentTimeMillis();
  31. //毫秒
  32. int period = 10;
  33. for (; ; ) {
  34. boolean lock = tryLock(key, req, expiration);
  35. if (lock) return true;
  36. if (System.currentTimeMillis() - start >= (timeout * 1000)) {
  37. break;
  38. }
  39. try {
  40. TimeUnit.MILLISECONDS.sleep(period);
  41. } catch (InterruptedException e) {
  42. return false;
  43. }
  44. }
  45. return false;
  46. }
  47. /** * 删除分布式锁 */
  48. @Override
  49. public boolean unlock(String key, String req) {
  50. //lua脚本
  51. String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
  52. RedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);
  53. Boolean execute = redisTemplate.execute(redisScript, Lists.newArrayList(key), req);
  54. boolean status = execute == null ? false : execute;
  55. if (status) {
  56. log.debug("删除分布式键{}成功:{}", key, req);
  57. }
  58. return status;
  59. }
  60. }
  1. @Scheduled(cron = "0 0/5 * * * ?")
  2. public void LogServiceUserAttrTrackSchedule() {
  3. String key = "ssssss";
  4. String req = UUID.randomUUID().toString();
  5. try {
  6. int expiration = 30 * 60;
  7. boolean holdLock = this.distributeLock.tryLock(key, req, expiration);
  8. if (holdLock) {
  9. // 业务逻辑
  10. // ....
  11. }
  12. } finally {
  13. distributeLock.unlock(key, req);
  14. }
  15. }

分布式锁分析

redis锁的缺陷

5-os&net

1-操作系统

内存管理

算了...
https://www.cnblogs.com/CareySon/archive/2012/04/25/2470063.html
https://www.zhihu.com/question/50796850
https://zhuanlan.zhihu.com/p/87514615
* 概述
--内存概念不存在时。程序直接访问和操作的都是物理内存。也不存在多进程。
--内存:
为了解决直接操作内存带来的各种问题,引入的地址空间(Address Space),这允许每个进程拥有自己的地址。
还需要硬件上存在两个寄存器,基址寄存器(base register)和界址寄存器(limit register),第一个寄存器保存进程的开始地址,第二个寄存器保存上界,防止内存溢出。

进程间通讯

2-网络

1.UDP|TCP
  1. UDP是什么?

    • 是什么:
      • UDP(User Datagram Protocol用户数据报协议)
      • 传输层协议
      • 无连接的数据报协议
      • 不能提供数据报分组,组装和不能对数据报进行排序
      • 主要用于不要求分组顺序到达的传输中,分组传输顺序的检查和排序有应用层完成。
      • 提供面向事务的简单不可靠传递服务。
      • UDP协议使用端口分别运行在同一台设备上的多个应用程序
      • 功能:为了在给定的主句上能识别多个目的的地址,同时允许多个应用程序在同一台主句上工作并能够独立地进行数据包的发送和接受,设计用户数据报协议UDP
    • 应用场景:
      UDP当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用UDP。比如,日常生活中,常见使用UDP协议的应用如下:
      -- QQ语音、QQ视频、TFTP……
  2. UDP怎么实现可靠传输? |2
    1)添加seq/ack机制,确保数据发送到对端
    2)添加发送和接收缓冲区,主要是用户超时重传。
    3)添加超时重传机制。
    https://www.jianshu.com/p/6c73a4585eba

  3. tcp参考:
    https://blog.csdn.net/qq_38950316/article/details/81087809
    https://blog.csdn.net/qzcsu/article/details/72861891
    https://www.cnblogs.com/jainszhang/p/10641728.html

  4. TCP是什么?应用场景,udp怎么实现tcp功能

    • 是什么?
      • 传输控制协议(TCP)是一种面向连接的,可靠的,基于字节流的传输通信协议。
      • 传输层协议
      • 原因:应用层需要可靠的连接,但是IP层没有这样的流机制
      • 面向连接,即在客户端和服务器之间发送数据之间,必须先建立连接
      • 位于应用层和IP层之间
      • 连接需要建立三次握手、四次挥手断开连接
      • 传输数据时可靠的
    • 应用场景
      TCP当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传递给对方,这往往用于一些要求可靠的应用,比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议。
      在日常生活中,常见使用TCP协议的应用如下:
      -- 浏览器,用的HTTP
      -- FlashFXP,用的FTP
      -- Outlook,用的POP、SMTP
      -- Putty,用的Telnet、SSH
      -- QQ文件传输…………
  5. tcp三次握手? |5

    • 必读:https://blog.csdn.net/qq_38950316/article/details/81087809
    • 三次握手过程理解:
      在这里插入图片描述
    • 第一次握手:
      客户端将TCP报文标志位SYN置为1,随机产生一个序号值seq=x,保存在TCP首部的序列号(Sequence Number)字段里,指明客户端打算连接的服务器的端口,并将该数据包发送给服务器端,发送完毕后,客户端进入SYN_SENT状态,等待服务器端确认。
    • 第二次握手:
      服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将TCP报文标志位SYN和ACK都置为1,ack=x+1,随机产生一个序号值seq=y,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
    • 第三次握手:
      客户端收到确认后,检查ack是否为x+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=y+1,并将该数据包发送给服务器端,服务器端检查ack是否为y+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
      在这里插入图片描述
      https://blog.csdn.net/a519640026/article/details/104448480
  6. tcp四次挥手?以及客户端/服务端分别发送消息后的状态? |4
    在这里插入图片描述

    • 四次挥手过程
      第一次挥手:客户端发出释放FIN=1,自己序列号seq=u,进入FIN-WAIT-1状态
      第二次挥手:服务器收到客户端的后,发出ACK=1确认标志和客户端的确认号ack=u+1,自己的序列号seq=v,进入CLOSE-WAIT状态
      第三次挥手:客户端收到服务器确认结果后,进入FIN-WAIT-2状态。此时服务器发送释放FIN=1信号,确认标志ACK=1,确认序号ack=u+1,自己序号seq=w,服务器进入LAST-ACK(最后确认态)
      第四次挥手:客户端收到回复后,发送确认ACK=1,ack=w+1,自己的seq=u+1,客户端进入TIME-WAIT(时间等待)。客户端经过2个最长报文段寿命后,客户端CLOSE;服务器收到确认后,立刻进入CLOSE状态。
  7. 为什么要三次握手,四次挥手,两次不行么

    • 三次握手的原因 :第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。
    • 四次挥手:客户端发送了FIN连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。。
  8. 为什么客户端最后还要等待2MSL tcp的timewait
    客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由:

    • 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。
    • 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。
      https://blog.csdn.net/TJtulong/article/details/89858678
  9. TCP 如何保证可靠传输,讲了一下拥塞控制、滑动窗口/tcp可靠性/为什么是可靠的
    1)可靠传输:通过序列号、检验和、确认应答信号、重发控制、连接管理、窗口控制、流量控制、拥塞控制实现可靠性。(重传机制?)
    2)TCP 滑动窗口
    窗口允许发送方在收到ACK之前连续发送多个分组,窗口的大小就是指无需等待确认应答而可以继续发送数据的最大值。
    https://blog.csdn.net/TJtulong/article/details/89858678

  10. TCP协议怎么保证传输可靠性,如果收到了重复数据怎么办?

  11. TCP流量控制?

  12. tcp拥塞控制? |3

  13. tcp拥塞控制怎么实现

    • 如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。
    • TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。
      https://blog.csdn.net/TJtulong/article/details/89858678
  14. TCP用的是ipoc还是什么?

  15. TCP头有什么信息?
  16. TCP往IP层的包添加了哪些东西
  17. 弱网情况下TCP性能较差,为什么。
  18. 讲一下TCP和UDP区别? |3
  19. 为什么选用TCP而不用UDP
  20. 查找出目前正在运行的TCP/UDP服务?
    netstat -atunlp
  21. 网络拥塞,最快方式下载一个视频文件的方法
TCP粘包

https://blog.csdn.net/nigar_/article/details/104237780

https加密过程
输入网址执行过程
零拷贝?
BIO、NIO、IO多路复用、AIO

3-分布式

分布式理论
分布式算法--一致性算法
分布式事务

6-Spring

Spring概念

Bean的定义方式

Spring的Ioc

Spring有哪些容器

BeanFactory和ApplicationContext的区别

BeanFactory和FactoryBean区别

区别:BeanFactory是个Factory,也就是IOC容器或对象工厂,FactoryBean是个Bean。在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的。但对FactoryBean而言,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似

spring的bean依赖注入方式

Bean的自动装配

Bean的作用域

Spring的AOP

原理

3.spring的两个特性IOC和AOP?
6.spring的IOC和Aop介绍一下?
(说了反射、工厂模式和动态代理,之前看过一点源码,说的比较详细,包括每步调了什么方法等)
4.Spring的AOP自调用问题。
7.aop 你怎么使用的aop

实现方式
应用-外数

Bean的生命周期

1、解析类得到BeanDefinition
2、如果有多个构造方法,则要推断构造方法
3、确定好构造方法后,进行实例化得到一个对象
4、对对象中的加了@Autowired注解的属性进行属性填充
5、回调Aware方法,比如BeanNameAware,BeanFactoryAware
6、调用BeanPostProcessor的初始化前的方法
7、调用初始化方法
8、调用BeanPostProcessor的初始化后的方法,在这里会进行AOP
9、如果当前创建的bean是单例的则会把bean放入单例池
10、使用bean
11、Spring容器关闭时调用DisposableBean中destory()方法

循环依赖

设计模式

https://blog.csdn.net/qq_34125999/article/details/114858004

Spring的事务

Spring事务的实现方式和原理以及隔离级别?
传播机制
事务失效

spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了!常见情况有如下几种
1、发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是UserService对象本身!
解决方法很简单,让那个this变成UserService的代理类即可!
在这里插入图片描述
2、方法不是public的
@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。
3、数据库不支持事务
4、没有被spring管理
5、异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)

应用

@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。**

多提一嘴:createAopProxy() 方法 决定了是使用 JDK 还是 Cglib 来做动态代理

Spring MVC的执行流程

springboot的自动配置

Springboot的Starter

使用spring + springmvc使用,如果需要引入mybatis等框架,需要到xml中定义mybatis需要的bean, starter就是定义一个starter的jar包,写一个@Configuration配置类、将这些bean定义在里面,然后在starter包的META-INF/spring.factories中写入该配置类,springboot会按照约定来加载该配置类开发人员只需要将相应的starter包依赖进应用,进行相应的属性配置(使用默认配置时,不需要配置),就可以直接进行代码开发,使用对应的功能了,比如mybatis-spring-boot--starter,springboot-starter-redis

Spring Boot、Spring MVC 和 Spring 有什么区别

@Controller和@RestController

8.如果Controller层想返回的数据是JSON格式的,怎么办。
RestController

mybatis

https://zhuanlan.zhihu.com/p/104941876

9-中间件

ZooKeeper

RabbitMQ

https://www.cnblogs.com/ysocean/p/9227233.html

Kafka

0-项目

版本

占数师

0-项目概述
  1. 背景:
    针对app、微信等多种渠道的用户行为数据进行采集;将各类行为数据进行整合,与公司内线下数据、公司外数据结合;开展行为数据分析,提供相应数据分析功能和工具;提供相关业务功能和可视化展示页面。

  2. 工程中用到的技术

    • 总:
      ① 数据流
      简
      详细
      ② 项目名称
      data_generator: 生成测试数据(php)
      dc_openresty:lua脚本
      log_service:数据收集(重点)
      realtime-task:flink实时写入(重点)
      skynet-backend: web工程(重点)
      das-api: 数据服务平台(重点)
      zsh_docker:一键部署
      ③ 知识点:
      lua脚本、filebeat、kafka、flink、impala、kudu、springboot、docker
    • 分:
      ① 客户端app+openresty
      客户端app会将埋点数据加密后,请求dc-skynet.rong360.com下/prod/send_data接口上传数据。
      nginx运行lua脚本获取数据,进行封装,写入log。脚本参见zsh_docker工程
      ② 日志收集+log-service
      app端或者客户服务端打点数据过来后,会在日志收集系统中进行数据的简单校验和转换,同时添加用户标识id。定义转换为放入 kafka 的消息格式。
      1> 数据收集
      后台脚本不断扫描数据文件是否有新的写入 com\example\log_service\common\ThreadPoolExecutor.java
      a.生产者不断扫描文件判断是否有新数据写入,如果有新数据写入将数据读取,并推送至阻塞队列中
      b.消费者主线程不断监听阻塞队列,如果有数据将数据取出发送给消费线程池进行消费
      c.消费线程负责数据处理、保存等
      2> 日志数据的处理
      1)日志消费分为sdk日志和nginx的log日志
      2)Nginx日志消费com\example\log_service\common\NgConsumeService.java
      a.数据的decode,unzip等操作,解析出json格式数据
      b.Sdk日志消费 com\example\log_service\common\SdkConsumeService.java
      将数据直接解析成json格式数据
      c.Json格式处理 com\example\log_service\domain\impl\LogServiceImpl.java
      3)数据存储,通过log4j日志插件将日志保存至文件中代码:
      com\example\log_service\common\writelogs\LogFileOperator.java
      在这里插入图片描述
      ③ filebeat:
      数据推送到kafka。配置参考zsh_docker工程
      ④ realtime-task:
      利用flink程序消费kafka,将数据存入kudu实时表。
      ⑤ skynet:
      元数据、漏斗分析、用户分群、事件分析、后端推送消息(websocket)
      在这里插入图片描述
      ⑥ das
      1> 背景:
      数据应用层在整个系统架构中承担了业务系统对数据层的访问逻辑。为适应各种业务的变化带来的数据应用层频繁开发新接口上线问题,数据应用层需要对现有的异构数据源(包括但不限于MySQL, Redis, Impala等)进行抽象,提供出公共数据服务接口,以应对业务应用层多变的数据查询需求。
      在这里插入图片描述
      ⑦ 大数据端
      1> 数据层交互架构图
      在这里插入图片描述
      2> 数仓架构图
      在这里插入图片描述
      3> 查询方式
      kudu表当天分区增量和hive历史数据做union all 建成视图,供外提供查询:
      在这里插入图片描述
1-skynet结构
2-元数据设计
3-留存分析

留存分析需求
1)日留存中间表schema设计

a、log_dt,skynet_user_id 作为主键

b、表命名规则?krs.tmp_用户_时间戳?
在这里插入图片描述

外数

0-技术栈

前端:Vue + highChart
后端: Spring boot + MyBatis-Plus+ Quartz+ QLExpress + Shiro
数据库:Oracle
缓存+队列:Redis
监控+日志收集:Open-Falcon+Kibana

1-系统结构
功能

在这里插入图片描述

技术栈

在这里插入图片描述

数据流

在这里插入图片描述

调用

在这里插入图片描述

2-监控

时间戳

项目量级
Hbase
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注