面经

1. 技术栈
1.1 Java 基础
这种面试题基本不固定,主要考察你对 java 基础的学习程度和理解能力,比如;
1. byte 占几个字节1个字节
    short 两字节 int 4字节 long int 8字节
    char 两字节 float 32字节 double 64字节
2. for 循环与 foreach
     fro(数据类型 名字 : 集合){}
     foreach适用于只是进行集合或数组遍历,for则在较复杂的循环中效率更高。
      foreach不能对数组或集合进行修改(添加删除操作),如果想要修改就要用for循环。
      所以相比较下来for循环更为灵活。

3. java8 的新特性
    1.lambda表达式(方便了 内部类的书写)
        创建线程时候可以使用lambda表达式
    2. hashmap发生变化
    	JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,
    	在JDK1.7的时候是先扩容后插入的,这样就会导致无论这一次插入是不是发生hash冲突都需要进行扩容,(存在无效扩容) 但是在1.8的时候是先插入再扩容的,
    
4. hashmap 的实现原理
  '' 二.位运算是如何保证索引不越界

讲到这,我们也就要想想为什么HashMap的容量是2的n次幂?两者之间有着千丝万缕的联系。

当 n 是2的次幂时, n - 1 通过 二进制表示即尾端一直都是以连续1的形式表示的。当(n - 1) 与 hash 做与运算时,会保留hash中 后 x 位的 1,这样就保证了索引值 不会超出数组长度。

同时当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n。''
    1. 底层是用的是数组链表加红黑树实现
    2. 当数组大于16时候,进行扩展,然后当容量大于8时候进行红黑树
    3. 元素中key不能为空,每个元素都有个hash
    具体步骤
    1. 插入数据时候进行哈希值的扰动(哈希值右移动16位),获取一个新的哈希值
    2. 判断tab位置是否为空,来进行扩容
    3. 若当前下标无数据进行覆盖
    4.若大于条件则进行扩容
    
5. 线程池和锁的使用和原理
6. 设计模式和面向对象
1.2 Spring
1. Spring 的好处
2. AOP 与 IOC
3. Spring 注解
4. SpringBean 加载过程
5. SpringBean 生命周期
6. Spring 中事务
1.3 Mybaits
1. Mybatis 的好处
2. Mybaits 的缓存
3. 如何进行分页
4. 插件是怎么运行的
5. #{}和${}的区别是什么
    #{} 是预编译处理,${}是字符串替换。
    Mybatis在处理#{}时,会将sql中的#{}替换为?(占位符),调用PreparedStatement的set方法来赋值;
    而在使用${}时,就是把 ${}替换成变量的值。
6. Mybatis 是否支持延迟加载?
1. Java
JDK 源码
1. HashMap
 	1 HashMap 的数据结构(1.7、 1.8 的区别)
        1. JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,
        2. 在JDK1.7的时候是先扩容后插入的,这样就会导致无论这一次插入是不是发生hash冲突要进行扩容,(存在无效扩容) 但是在1.8的时候是先插入再扩容的,
        3. 1.7是数组+链表,1.8则是数组+链表+红黑树结构;
        4. jdk1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;而1.8则是直接调用resize()扩容;

 	2. HashMap 的实现原理
    	1. 底层是用的是数组链表加红黑树实现
    	2. 当数组大于16时候,进行扩展成链表,然后当容量大于8时候进行红黑树
    	3. 元素中key不能为空,每个元素都有个hash
 	3. HashMap 扩容为什么是 2^n-1
		因为Hashmap计算存储位置时,使用了(n - 1) & hash。只有当容量n为2的幂次方,n-1的二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突,所以扩容必须2倍就是为了维持容量始终为2的幂次方。   
 	4. HashMap 是线程安全的吗
    	不安全,并没有加锁
 	5. HashMap、 HashTable 是什么关系?
		HashTable加锁了,线程安全

 2. ThreadLocal
   	1. 讲讲你对 ThreadLocal 的一些理解
        ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。
        
        ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是

        Synchronized是通过线程等待,牺牲时间来解决访问冲突
        ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。


   	2. ThreadLocal 有哪些应用场景
	3. 了解过 FastThreadLocal 吗

3. ArrayList、 LinkList
 1. 是否保证线程安全
    并不保证线程安全
    2、 变成多线程安全,有两种方案
    (1)使用容易提供的同步方法Collections.synchronizedList(new LinkedList())。每次在写入的时候,锁住临界区,保证数据的一致性 
    (2)使用线程安全的vector等对象
 2. 底层数据结构
    ArrayList是动态数组
    LinkedList是链表
 3. 插入和删除是否受元素位置的影响
    受到位置影响
	1.`头插时候,ArrayList会出现重复复制数组,时间消耗过多
    	而LinkedList只是创建了个对象,时间消耗减少
	2. 中间插入,若是数量不是很大比如百万级别,则LinkedList和Arraylist性能消耗差不多
    3. 若是尾插,ArrayList只需要扩容即可不需要复制数组,则耗时很少
  		而LinkedList则需要创建大量数据对象而消耗过多时间.
 4. 是否支持快速随机访问
    ArrayList支持快速随机访问
    LinkedList每次查找会后都需要遍历每个节点,非常耗时
 5. 内存空间占用
    ArrayList所占用内存空间是整块的,产生碎片极少
    LinkedList则需要存放指针和数据产生了大量内存碎片
 6. 如何进行扩容的,默认初始化空间是多少
	ArrayList 是10
    LinkedList是链表没有默认长度
4. String StringBuffer StringBuilder
 1. 有什么区别
    String 在存字符串时候,jvm编译是new的StringBuilder对象,还是StringBuilder
    底层都是用final数组存储的
    StringBuffer和StringBuilder相同,只是多了个锁   
 2. 是线程安全的吗
	StringBuffer线程安全
    
5. jdk1.8 的新特性(https://www.cnblogs.com/chentianxiang180/p/14846484.html)
    1. lambda 表达式	
    Lambda表达式是jdk1.8里面的一个重要的更新,这意味着java也开始承认了函数式编程,并且尝试引入其中。不是所有的接口都可以通过这种方法来调用,只有函数式接口才行.简单的来说就是,函数也是一等公民了,在java里面一等公民有变量,对象,那么函数式编程语言里面函数也可以跟变量,对象一样使用了,也就是说函数既可以作为参数,也可以作为返回值了,
        
    2. Functional Interfaces(函数式接口)
       定义:“函数式接口”是指仅仅只包含一个抽象方法的接口,每一个该类型的lambda表达式都会被匹配到这个抽象方法。jdk1.8提供了一个@FunctionalInterface注解来定义函数式接口,如果我们定义的接口不符合函数式的规范便会报错。
    3. Optionals
  // 
    4.Stream 流
     流是Java API的新成员,它允许我们以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。就现在来说,我们可以把它们看成遍历数据集的高级迭代器。此外,流还可以透明地并行处理,也就是说我们不用写多线程代码了。
          
     流的操作类型分为两种:
     Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
     Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。
    5. Parallel-Streams 并行流
        // 
    6.方法与构造函数引用
        jdk1.8提供了另外一种调用方式::,当 你 需 要使用 方 法 引用时 , 目 标引用 放 在 分隔符::前 ,方法 的 名 称放在 后 面 ,即ClassName :: methodName 。例如 ,Apple::getWeight就是引用了Apple类中定义的方法getWeight。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式(Apple a) -> a.getWeight()的快捷写法,如下示例。
            
            
            

并发编程(j.u.c)
1. volatile
     1. volatile 的作用和使用场景
        1.保证内存可见性 (在编译后有个lock指令,保证本地缓存写入内存.让有其他写入进程使其效果无效)
        2.防止指令重排, 有序性( volatile 的内存屏故障是在读写操作的前后各添加一个 Store屏障,从而防止后面进程排到上一个进程之前)
        当前进程内看到的是有序的,其他进程看到的是无序的
        3. volatile 并不能解决原子性
        使用:sychronized内部使用了volatile防止指令重排
            开销较低的“读-写锁”策略(如果读操作远远超过写操作,可以结合使用内部锁和volatile 变量来减少公共代码路径的开销。)
     2. volatile 如何保证指令重排
        volatile 的内存屏故障是在读写操作的前后各添加一个 Store屏障,从而防止后面进程排到上一个进程之前
     3. 什么情况下会发生指令重排
     	指令执行时的各种条件,指令与指令之间的相互影响,可能导致顺序放入流水线的指令,最终乱序执行完成。这就是所谓的“顺序流入,乱序流出”。

2. synchronized
     1. 一般用在什么场景
        1.修饰一个类 (StringBuilder和Stringbuffer)
        2.修饰一个方法,
     2. 实现原理
        每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
        1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
        2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
        3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
     
     3. 锁升级过程
         偏向锁、轻量级锁、重量级锁
         - 偏向锁  
          偏斜锁会延缓 JIT 预热进程,所以很多性能测试中会显式地关闭偏斜锁,偏斜锁并不适合所有应用场景,撤销操作(revoke)是比较重的行为,只有当存在较多不会真正竞争的 synchronized 块儿时,才能体现出明显改善  
        - 轻量级锁  
          当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。  
        - 自旋锁  
          自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU。
        - 锁会降级吗?  
          当 JVM 进入安全点 SafePoint 的时候,会检查是否有闲置的 Monitor,然后试图进行降级。  
     4. 这是 JVM 层面锁,还是 JDK 层面锁{ JVM 层面}
         JVM 层面
         需要使用volatile保证变量的可变性
     5. 这是一种悲观锁还是乐观锁{悲观锁是独占锁}
         悲观锁,是独占锁

3. lock
 	1. 这是 JVM 层面锁,还是 JDK 层面锁{ JDK 层面}
         jdk层面的锁
 	2. 这是一种悲观锁还是乐观锁
         Lock 只是一个顶层抽象接口,并没有实现,也没有规定是乐观锁还是悲观锁实现规则。而 ReentrantLock 作为 Lock 的一种实现,是悲观锁,
 	3. 是可重入锁吗
         ReentrantLock是实现的重入锁

4. ReentrantLock (重入锁)
 	1. 与 synchronized 相比较有什么不同
         1. ReentrantLock可以锁过程中断,而synchronize不可中断(有原子性)
         synchronized是在JVM层面上实现的,lock是通过代码实现的,JVM会自动释放锁定(代码执行完成或者出现异常),但是使用Lock则不行,要保证锁定一定会被释放,就必须将unLock()放到finally{}中。
         2. synchronize是JVM层面而ReentrantLock是JDK层面的
         	前者只需要JVM加锁和释放,而后者需要自己加锁和释放
         3. synchronize是非公平锁,ReentrantLock可以公平或者非公平
         
         
	2. ReentrantLock 与 Lock 的关系
       	 Lock是顶层抽象接口,而ReentrantLock是实现了这个接口
       	 ReentrantLock(独占锁,公平/非公平) 重入锁
         公平锁有:CHL,MCS,Ticket
	3. 锁过程中是否可中断,与之对应的 synchronized 可中断吗
		举例子:
        线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,
        如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断
        如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事
         ReentrantLock可以锁过程中断,而synchronize不可中断(有原子性)
        synchronized是在JVM层面上实现的,lock是通过代码实现的,JVM会自动释放锁定(代码执行完成或者出现异常),但是使用Lock则不行,要保证锁定一定会被释放,就必须将unLock()放到finally{}中。

5. CAS
    1. Unsafe 类的作用
      1, 内存屏障:禁止load,store重排
      2, CAS
      3, 线程调度: 线程挂起,恢复/ 获取,释放锁
    对 state 使用 unsafe本地方法,传递偏移量 stateOffset 等参数,进行值交换操作。unsafe.compareAndSwapInt(this, stateOffset, expect,update)  (常用)
            
    2. CAS 的理解(compareAndSet)
    CAS 是 compareAndSet 的缩写,它的应用场景就是对一个变量进行值变更,在变更时会传入两个参数:一个是预期值、另外一个是更新值。如果被更新的变量预期值与传入值一致,则可以变更 .
    CAS 的 具 体 操 作 使 用 到 了 unsafe 类, 底 层 用 到 了 本 地 方 法unsafe.compareAndSwapInt 比较交换方法。  
    CAS 是一种无锁算法,这种操作是 CPU 指令集操作,只有一步原子操作,速度非常快。而且 CAS 避免了请求操作系统来裁定锁问题,直接由 CPU 搞定,但也是没有开销 .
            
    3. 什么是 ABA 问题
    ABA:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化。
    4. CAS 的实现有什么(AtomicInteger)
6.AQS
    AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS(compareAndSetState)去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。`
 1. 实现类有哪些
    ReentrantLock、 Semaphore、 CountDownLatch、CyclicBarrier
 2. 实现了 AQS 的锁有哪些 
    自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是 AQS 的衍生物 内存屏障,几乎所有的处理	器至少支持一种粗粒度的屏障指令,通常被称为“栅栏(Fence) 

       
               
多线程
0. 创建线程的几种方式
    1.继承Thread类创建线程类

    (1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
    (2)创建Thread子类的实例,即创建了线程对象。
    (3)调用线程对象的start()方法来启动该线程。
          
    二、通过Runnable接口创建线程类
          ]
    (1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
    (2)创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread[.对象,该Thread对象才是真正的线程对象
    (3)调用线程对象的run()方法来启动该线程。      
          -
      三、通过Callable和Future创建线程

    (1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
    (2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
    (3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
    (4)调用FutureTask对象的get()方法来获h得子线程执行结束后的返回值  
          
1. 线程池的种类
    1. newCachedThreadPool(线程池大小固定)  新缓存线程池
          -  介绍:创建一个固定大小可重复使用的线程池,以 LinkedBlockingQueue 无界阻塞队列存放等待线程。  
          -  风险:随着线程任务不能被执行的的无限堆积,可能会导致 OOM。  
          
    2. newFixedThreadPool(线程池里就一个线程干活)   新整理线程池
          - 介绍:只创建一个执行线程任务的线程池,如果出现意外终止则再创建一个。  
          - 风险:同样这也是一个无界队列存放待执行线程,无限堆积下会出现 OOM。 
          
    3. newScheduledThreadPool (有多少任务,线程池就有多少线程生产者与消费者)  新安排线程池
          -介绍:首先 SynchronousQueue 是一个生产消费模式的阻塞任务队列,只要有任务就需要有线程执行,线程池中的线程可以重复使用  
          -风险: 如果线程任务比较耗时,又大量创建,会导致 OOM  
          
    4. newSingleThreadExecutor (同样是来多少任务就创造多少线程,可以延迟定时执行) 新单例线程池
          - 介绍:这就是一个比较有意思的线程池了,它可以延迟定时执行,有点像我们的定时任务。同样它也是一个无限大小的线程池 Integer.MAX_VALUE。它提供的  调用方法比较多,包括: scheduleAtFixedRate、scheduleWithFixedDelay,可以按需选择延迟执行方式  
          - 风险:同样由于这是一组无限容量的线程池,所以依旧有 OOM 风险。  

          
2. 线程的生命周期
     
	1. 新建、就绪、运行、阻塞(等待阻塞、同步阻塞、其他阻塞)、死亡
          - New:新创建的一个线程,处于等待状态  
          - Runnable:可运行状态,并不是已经运行,具体的线程调度各操作系统决定。在Runnable 中包含了 Ready、 Running 两个状态,当线程调用了 start() 方法后,线程则处于就绪 Ready 状态,等待操作系统分配 CPU 时间片,分配后则进入 Running 运行状态。此外当调用 yield() 方法后,只是谦让的允许当前线程让出 CPU,但具体让不让不一定,由操作系统决定。如果让了,那么当前线程则会处于 Ready 状态继续竞争 CPU,直至执行  
          - Timed_waiting:指定时间内让出 CPU 资源, 此时线程不会被执行,也不会被系统调度,直到等待时间到期后才会被执行。下列方法都可以触发:Thread.sleep、 Object.wait、 Thread.join、LockSupport.parkNanos、 LockSupport.parkUntil  
          - Wating:可被唤醒的等待状态,此时线程不会被执行也不会被系统调度。此状态可以通过 synchronized 获得锁,调用 wait 方法进入等待状态。最后通过notify、 notifyall 唤醒。下列方法都可以触发: Object.wait、Thread.join、 LockSupport.park  
          - Blocked:当发生锁竞争状态下,没有获得锁的线程会处于挂起状态。例如synchronized 锁,先获得的先执行,没有获得的进入阻塞状态。  
          - Terminated:这个是终止状态,从 New 到 Terminated 是不可逆的。一般是程序流程正常结束或者发生了异常。  

3. 线程启动过程
		基本核心过程包括:Java 创建线程和启动、 调用本地方法 start0()、JVM 中JVM_StartThread 的创建和启动、设置线程状态等待被唤醒、根据不同的 OS 启动线程并唤醒、最后回调 run() 方法启动 Java 线程。
       	开始:new Thread(()->{}).start();   -- start0 ()---> JVM  ---os:create_thread/os:: start_thread---> OS
       	回调:os  -- javaThread:: run(); --> JVM  ---- run()---> new Thread(()->{}).start();



JVM  笔记
1. GC 优化 
2. 垃圾回收
    一.如何确定某个对象是“垃圾”?
	1.引用计数算法(Refesrence Counting)
    2. 根搜索算法( Root Tracing)
               在实际生产语言中(java,C#等)都使用跟搜索算法判断对象是否存活。算法的基本思路就是通过一系列被称作“GC ROOTS “ 的点作为起始进行向下搜索,当一个对象到GC ROOTS 没有任何引用链相连,则证明此对象是不可用的其中的GC ROOTS 包括:
               - 在VM栈中(帧中的本地变量)中的引用
               - 方法区中的静态引用
               - JNI(即一般所说的Native方法)中的引用
    二.典型的垃圾收集算法
       Mark-Sweep(标记-清除)算法:谁没用标记谁
       Copying(复制)算法: 谁有用复制 (新生代)
       Mark-Compact(标记-整理)算法: 标记没用的,把有用的整理一端,然后清除没用的
       分代算法(Generational) 
               
    三.典型的垃圾收集器        
       垃圾回收器的并行(Parallel)和并发(Concurrent)        
        serial 收集器
        是最早的收集器,单线程收集器,Hotspot Client模式缺省的收集器,收集时会暂停所有工作线程(Stop The World,简称STW),因为是单线程GC,没有多线程切换的额外开销,简单实用
               
        Serial Old 收集器
        Serial Old是单线程收集器,使用标记一整理算法, 是老年代的收集器
          
        parnew收集器
        parnew收集器就是Serial收集器在新生代的多线程版本,是Server模式下新生代的缺省收集器,除了使用多个收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与 Serial收集器一模一样。
        Parallel Scavenge收集器
        Parallel Scavenge 收集器也是一个多线程收集器(Parallel就是并行的意思),也是使用复制算法,但它的对象分配规则与回收策略都与 Parnew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化,jvm1.8默认在新生代使用Parallel Scavenge ,老年代使用Parallel Old收集
               
        Parallel Old 收集器
        JVM1.6提供,在此之前,新生代使用PS收集器的话,老年代除了使用Serial Old外别无选择,因为PS无法和CMS配合工作。jvm1.8默认在新生代使用Parallel Scavenge ,老年代使用Parallel Old收集       
               
3. 类的对象头都包括什么
福哥口诀法:
    标类长(对象头:markword标记字、klass类型指针、数组长度(仅限于数组))
    无偏轻重G(锁状态:无锁、偏向锁、轻量级锁、重量级锁、GC标记)
    未哈未年标,25 31 1 (64位无锁情况:未使用25、hashcode31、未使用1、年龄4、偏向标志1)
    线时未年标,54和2 1(64位偏向锁情况:线程id54、偏向时间戳2、未使用1、年龄4、偏向标志1)
    哈年标25 (32位无锁情况:hashcode25、年龄4、偏向标志1)
    线时年标23 2 (32位偏向锁情况:线程id23、偏向时间戳2、年龄4、偏向标志1)

4. new Object() 初始化都做了什么
    1.Java关键字new是一个运算符。与+、-、*、/等运算符具有相同或类似的优先级。
    2.创建一个Java对象需要三部分:声明引用变量、实例化、初始化对象实例。
    3.实例化:就是“创建一个Java对象”-----分配内存并返回指向该内存的引用。
    4.初始化:就是调用构造方法,对类的实例数据赋初值。              
               
5. 类加载机制
    1.类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
    2. 类加载周期:
    加载->连接(验证->准备->解析)->初始化->使用->卸载
    3. 类加载器
       1. 启动类加载器
       2. 扩展类加载器
       3. 应用程序类加载器
    4. JVM类加载机制
    •全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
    •父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
    •缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效   
     5.类的加载
       类加载有三种方式:
        1、命令行启动应用时候由JVM初始化加载
        2、通过Class.forName()方法动态加载
        3、通过ClassLoader.loadClass()方法动态加载         
               
6. Java 的内存模型 JMM  (类比:主内存对应硬件系统中的内存部分,工作空间对应高速缓存)
       为了控制线程之间的通信,(完成底层封装), 用来屏蔽掉各种硬件和操作系统之间的内存访问差异,以实现让Java程序在各平台下都能达到一致的内存访问效果。
      JMM目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节(这里变量指代的是实例字段、静态字段和构成数组对象的元素)
 
     主内存和工作内存
    (1)所有变量均存储在主内存(虚拟机内存的一部分)
    (2)每个线程都对应着一个工作线程,主内存中的变量都会复制一份到每个线程的自己的工作空间,线程对变量的操作都在自己的工作内存中,操作完成后再将变量更新至主内存;
    (3)其他线程再通过主内存来获取更新后的变量信息,即线程之间的交流通过主内存来传递      
        
     Note:JMM的空间划分和JVM的内存划分不一样,非要对应的话,关系如下:
    (1)JMM的主内存对应JVM中的堆内存对象实例数据部分
    (2)JMM的工作内存对应JVM中栈中部分区域

2、内存间交互操作
    JMM定义了8种操作(原子操作),虚拟实现时保证这8中操作均为原子操作,以下为8中操作的介绍以及执行顺序:
    (1)lock(锁定):作用于主内存的变量,把一个变量标志为一个线程占有状态(锁定)
    (2)unlock(解锁):作用于主内存的变量,把一个变量从一个线程的锁定状态解除,以便其他线程锁定
    (3)read(读取):作用于主内存的变量,将变量从主内存读取到线程的工作空间,以便后续load操作使用
    (4)load(载入):作用于工作空间的变量,将load操作从主内存得到的变量放入工作内存变量副本中
    (5)use(使用):作用于工作空间的变量,将工作空间中的变量值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的字节码指令时将执行这个操作。
    (6)assign(赋值):作用于工作内存的变量,把一个从执行引擎接收的值赋给工作空间的变量
    (7)store(存储):作用于工作内存的变量,把工作空间的一个变量传到主内存,以便后续write操作使用
    (8)write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量值放入主内存的变量中
 java内存模型对并发提供的保障:原子性、可见性。有序性(当前自己线程是有序,其他的看的是无序的) 和 Synchronized一样 

        
        
        
        
设计模式
0 单例最重要,看自己的总结 
1. 设计模式 6 大原则
    单一职责(一个类和方法只做一件事)、里氏替换(多态,子类可扩展父类)、依赖倒置(细节依赖抽象,下层依赖上层)、接口隔离(建立单一接口)、迪米特原则(最少知道,降低耦合)、开闭原则(抽象架构,扩展实现)
2. 创建型模式 
   这类模式提供创建对象的机制, 能够提升已有代码的灵活性和可复用性。
3. 结构型模式
   这类模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。
4. 行为模式 
   这类模式负责对象间的高效沟通和职责委派。

        
        
反射、代理 (书签 2021-06-03 23:41:49)
               // https://www.cnblogs.com/chentianxiang180/p/14850452.html
1. 怎么实现反射调用方法
    所谓反射,就是根据一个已经实例化了的对象来还原类的完整信息
        1.通过对象取得包名和类名  t.getClass().getName()
        2.Class类的实例化
               对象.getClass( )
                类.Class
                Class.forName( "XXXX")
        3.取得类的构造方法  c = Class.forName("java.lang.Boolean");   
               			 Constructor<?>[] cons = c.getConstructors();
        4.取得本类的全部属性 c = Class.forName("Person");
               			  Field[] f = c.getDeclaredFields();
        5.修改属性值
                Class<?> c = p.getClass();
     
                //定义要修改的属性
                Field f = c.getDeclaredField("name");
                f.setAccessible(true);
                //修改属性,传入要设置的对象和值
                f.set(p, "张二蛋");
                System.out.println(p);
              
               
2. 怎么代理一个类,有什么场景使用 
     辅助被代理类,或增加额外的功能,这样对用户端(就是上面例子里的 Consumer 类)的代码没有任何影响,耦合很低。
3. 类代理的原理是什么
     静态/动态代理 反射
4. 有什么框架可以做类代理
      Spring AOP             
   如果加入容器的目标对象有实现接口,用JDK代理
  如果目标对象没有实现接口,用Cglib代理   
  如果目标对象实现了接口,且强制使用cglib代理,则会使用cglib代理
5. 代理
   静态代理在使用时,需要定义接口或者父类,被代理对象与代理对象一起实现相同的接口或者是继承相同父类.
                                                       
  JDK中生成代理对象的API
    代理类所在包:java.lang.reflect.Proxy
    JDK实现代理只需要使用newProxyInstance方法,但是该方法需要接收三个参数,完整的写法是:

    static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h )
        注意该方法是在Proxy类中是静态方法,且接收的三个参数依次为:

    ClassLoader loader,:指定当前目标对象使用类加载器,获取加载器的方法是固定的
    Class<?>[] interfaces,:目标对象实现的接口的类型,使用泛型方式确认类型
    InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入                                                     
      
    
    
               
4.1.3 Mysql
锁
    1. 全局锁
    全局锁就是对整个数据库实例加锁。MySQL 提供了一个加全局读锁的方法,命令是Flush tables with read lock (FTWRL)。
	当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
    使用场景:做全库逻辑备份(mysqldump)
    2. 表锁
	表锁的语法是 lock tables。。。read/write,与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。
    3. 行锁

    4. 乐观锁、悲观锁
        1. 悲观锁:
           含义: 具有强烈的独占和非他性,对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改保守态度,因此,在整个数据处理的过程中,将数据处于锁定状态
           应用: 关系型数据库, 行锁,表锁,读/写锁
        2. 乐观锁:
           含义: 乐观,每次拿数据的时候都任务别人不会修改,所以不会上锁,但在更新的时候,会判断一下在此期间别人有么有去更新这个数据,可以使用版本号机制,
           使用场景: 适用于多读的应用类型,这样可以提高吞吐量
           实现: 版本号,发现冲突,上层应用会不断的进行retry.     
        5. 排他锁
            他锁(X锁):用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。
            如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
            们在操作数据库的时候,可能会由于并发问题而引起的数据的不一致性(数据冲突)
	


		6. 锁优化
        	1. 锁优化的思路和方法
			锁优化的思路和方法有以下几种:
             1.减少锁持有时间
             2.减小锁粒度
             将大对象(这个对象可能会被很多线程访问),拆成小对象,大大增加并行度,降低锁竞争。降低了锁的竞争,偏向锁,轻量级锁成功率才会提高。
             3.锁分离
             最常见的锁分离就是读写锁ReadWriteLock,根据功能进行分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥。即保证了线程安全,又提高了性能。
             4.锁粗化
             通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。
             5.锁消除
             锁消除是在编译器级别的事情。在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作。
            
               
事务
    1. 事物特征
   事务是必须满足4个条件(ACID) :
   ■原子性Atomicity: -一个事务中的所有操作,要么全部完成,要么全部不完成,最小的执行单位。
   ■一致性 Consistency: 事务执行前后,都处于-致性状态 。
   ■隔离性Isolation: 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止
   多个事务并发执行时由于交叉执行而导致数据的不一一致。
   ■持久性Durability: 事务执行完成后,对数据的修改就是永久的,即便系统故障也不会丢失。
      
    2. 脏读:事务A读到了事务B未提交的数据。
    3. 幻读:事务A第一次查询得到一行记录row1,事务B提交修改后,事务A第二次查询得到两行记录row1和row2。
    4. 不可重复读:事务A第一次查询得到一行记录row1,事务B提交修改后,事务A第二次查询得到row1,但列内容发生了变化。
        
    5. 事物隔离
    ■READ;_UNCOMMITTED
    这是事务最低的隔离级别,它充许另外-一个事务可以看到这个事务未提交的数据。解决第一类丢失更
    新的问题,但是会出现脏读、不可重复读、第二类丢失更新的问题,幻读。
    ■READ_ COMMITTED
    保证一个事务修改的数据提交后才能被另外-一个事务读取,即另外一个事务不能读取该事务未提交的
    数据。解决第一类丢失更新和脏读的问题,但会出现不可重复读、第二类丢失更新的问题,幻读问题
    ■REPEATABLE _READ  repeatable_read (礼貌)
    保证一个事务相同条件下前后两次获取的数据是一致的(注意是 一个事务,可以理解为事务间的数
    据互不影响)解决第一类丢失更新, 脏读、不可重复读、第二类丢失更新的问题,但会出幻读。
    ■SERIALIZABLE   serializablecv  序列化
    事务串行执行,解决了脏读、不可重复读、幻读。但效率很差,所以实际中一般不用。
        
    6. 并发事物
      1.更新丢失 2.脏读 3.不可重复读 4.幻读
        
    7. 事物实现原理
        事务的原子性是通过 undo log 来实现的
        事务的持久性性是通过 redo log 来实现的
        事务的隔离性是通过 (读写锁+MVCC)来实现的
        而事务的终极大 boss 一致性是通过原子性,持久性,隔离性来实现的!!!
       如果面试官这样问你,你可以从Spring框架来回答比较好,但是还是得带着数据库的方面作为比较
        数据库事务的实现原理,MySQL为例:
        应该从原子性、一致性、隔离性、持久型、隔离级别这些方面来回答		(https://zhuanlan.zhihu.com/p/272601254)	
        Spring为例:
应该从事务的基本原理、Spring 事务的传播属性、数据库隔离级别、Spring中的隔离级别、事务的嵌套这些方面来回答(https://zhuanlan.zhihu.com/p/272604674)
           
                               
        
    
4.2 框架
4.2.1 Spring   
    1. Bean 的注册过程   https://javadoop.com/post/spring-ioc
    2. Bean 的定义都包括什么信息
    3. Spring 事务中的隔离级别有哪几种
	https://zhuanlan.zhihu.com/p/272604674                                                           
    TransactionDefinition 接口中定义了五个表示隔离级别的常量:( isolation 隔离)            
    TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.
    TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
    TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
    TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
    TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
                                                            
    4.Spring 事务中哪几种事务传播行为? propagation 传播
                                                            
    支持当前事务的情况:
    TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
    TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
    TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)

不支持当前事务的情况:
    TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
    TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。

其他情况:
    TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。                                                        
                                                            
4.2.2  https://www.cnblogs.com/huigelaile/p/10996537.html
	1. mybatis 在 spring 的使用中,只需要定义接口,就可以和 xml 中的配置的 sql 语句,进行关联,执行数据库增删改查操作。怎么实现的
    Dao接口即Mapper接口。接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中,每一个 <select>、<insert>、<update>、<delete> 标签,都会被解析为一个MapperStatement对象。
       Mapper 接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,转而执行MapperStatement所代表的sql,然后将sql执行结果返回。                                                     
2. session 是怎么管理的

4.3.3 SpringBoot  博客
1. SpringBoot 怎么开发一个自己的 Stater https://www.cnblogs.com/chentianxiang180/p/14820072.html

6. 框架篇

Spring bean

1. 获取Bean

图片

这里的流程图的入口在AbstractBeanFactory类的doGetBean方法,这里可以配合前面的getBean方法分析文章进行阅读。主要流程就是

  1. 先处理Bean 的名称,因为如果以“&”开头的Bean名称表示获取的是对应的FactoryBean对象;
  2. 从缓存中获取单例Bean,有则进一步判断这个Bean是不是在创建中,如果是的就等待创建完毕,否则直接返回这个Bean对象
  3. 如果不存在单例Bean缓存,则先进行循环依赖的解析
  4. 解析完毕之后先获取父类BeanFactory,获取到了则调用父类的getBean方法,不存在则先合并然后创建Bean

bean实例化过程

接下来对照上图,一步一步对 singleton 类型 bean 的生命周期进行解析:

  1. 实例化 bean 对象,类似于 new XXObject()
  2. 将配置文件中配置的属性填充到刚刚创建的 bean 对象中。
  3. 检查 bean 对象是否实现了 Aware 一类的接口,如果实现了则把相应的依赖设置到 bean 对象中。比如如果 bean 实现了 BeanFactoryAware 接口,Spring 容器在实例化bean的过程中,会将 BeanFactory 容器注入到 bean 中。
  4. 调用 BeanPostProcessor 前置处理方法,即 postProcessBeforeInitialization(Object bean, String beanName)。
  5. 检查 bean 对象是否实现了 InitializingBean 接口,如果实现,则调用 afterPropertiesSet 方法。或者检查配置文件中是否配置了 init-method 属性,如果配置了,则去调用 init-method 属性配置的方法。
  6. 调用 BeanPostProcessor 前置处理方法,即 postProcessAfterInitialization(Object bean, String beanName)。我们所熟知的 AOP 就是在这里将 Adivce 逻辑织入到 bean 中的。
  7. 注册 Destruction 相关回调方法。
  8. bean 对象处于就绪状态,可以使用了。
  9. 应用上下文被销毁,调用注册的 Destruction 相关方法。如果 bean 实现了 DispostbleBean 接口,Spring 容器会调用 destroy 方法。如果在配置文件中配置了 destroy 属性,Spring 容器则会调用 destroy 属性对应的方法。

bean流程解析

其实就是 实例化bean ->把配置文件中的属性填充到bean中->看是否实现Aware一类接口(有一个是BeanFactory)有就调用->开始AOP的adivce逻辑的织入bean. ->调用前置处理方法,beanPostProcess,看是否实现 初始化bean的接口(initzingBean)有就检测配置文件是否配置init_method方法,->最后继续调用后置处理方法,然后注册 Destruction 相关回调方法然后就可以使用了!

实例化bean ->填充bean->检测是否实现Aware接口(包含BeanFactory)->调用后置方法beanPostProcess->看是否实现初始化bean的接口->beanPostProcess ->注册Destruction相关回调方法->bean对象处于就绪状态.

bean属性

Bean 标签的常用属性

1、id 属性:Bean 的唯一标识名,必须以字母开头,且不能

包含特殊字符

2、class 属性:用来定义类的全限定名(包名+类名)

3、name 属性:用来为 Bean 指定一个 或 多个别名,且能

包含特殊字符。如果 Bean 没有 id,name 可当做 id 使用

「多个别名之间可通过 逗号、分号、空格 隔开(可混用)」

4、scope 属性,其属性值如下:

(1)singleton:默认值,单例

(2)prototype:多例

(3)request:Web 项目中,把创建的对象放到 request 域中

(4)session:Web 项目中,把创建的对象放到 session 域中

(5)globalSession:Web 项目中,应用在 Portlet 环境,如

果没有 Portlet 环境,则 globalSession 相当于 session

SpringMVC

img

Servlet 生命周期

img

(1)加载和实例化
  当Servlet容器启动或客户端发送一个请求时,Servlet容器会查找内存中是否存在该Servlet实例,若存在,则直接读取该实例响应请求;如果不存在,就创建一个Servlet实例。
(2) 初始化
  实例化后,Servlet容器将调用Servlet的init()方法进行初始化(一些准备工作或资源预加载工作)。
(3)服务
  初始化后,Servlet处于能响应请求的就绪状态。当接收到客户端请求时,调用service()的方法处理客户端请求,HttpServlet的service()方法会根据不同的请求 转调不同的doXxx()方法,比如 doGet, doPost
(4)销毁 
  当Servlet容器关闭时,Servlet实例也随时销毁。其间,Servlet容器会调用Servlet 的destroy()方法去判断该Servlet是否应当被释放(或回收资源)。

其中实例化,初始化,销毁只会执行一次,service方法执行多次,默认情况下servlet是在第一次接受到用户请求的情况下才会实例化,可以在web.xml中的<servlet><servlet>标签内添加一个<load-on-startup>1<load-on-startup>配置,此时在启动tomcat时会创建servlet实例。

7. 软件

Nginx Nginx

Nginx 实现负载均衡的策略:

  • 轮询策略:默认情况下采用的策略,将所有客户端请求轮询分配给服务端。这种策略是可以正常工作的,但是如果其中某一台服务器压力太大,出现延迟,会影响所有分配在这台服务器下的用户。
  • 最小连接数策略:将请求优先分配给压力较小的服务器,它可以平衡每个队列的长度,并避免向压力大的服务器添加更多的请求。
  • 最快响应时间策略:优先分配给响应时间最短的服务器。
  • 客户端 ip 绑定策略:来自同一个 ip 的请求永远只分配一台服务器,有效解决了动态网页存在的 session 共享问题。

反向代理

这里为了演示更加接近实际,作者准备了两台云服务器,它们的公网 IP 分别是:121.42.11.34121.5.180.193

我们把 121.42.11.34 服务器作为上游服务器,做如下配置:

# /etc/nginx/conf.d/proxy.conf
server{
  listen 8080;
  server_name localhost;
  
  location /proxy/ {
    root /usr/share/nginx/html/proxy;
    index index.html;
  }
}

# /usr/share/nginx/html/proxy/index.html
<h1> 121.42.11.34 proxy html </h1>

配置完成后重启 Nginx 服务器 nginx \-s reload

121.5.180.193 服务器作为代理服务器,做如下配置:

# /etc/nginx/conf.d/proxy.conf
upstream back_end {
  server 121.42.11.34:8080 weight=2 max_conns=1000 fail_timeout=10s max_fails=3;
  keepalive 32;
  keepalive_requests 80;
  keepalive_timeout 20s;
}

server {
  listen 80;
  server_name proxy.lion.club;
  location /proxy {
   proxy_pass http://back_end/proxy;
  }
}

本地机器要访问 proxy.lion.club 域名,因此需要配置本地 hosts ,通过命令:vim /etc/hosts进入配置文件,添加如下内容:

121.5.180.193 proxy.lion.club

图片

分析:

当访问 proxy.lion.club/proxy 时通过 upstream 的配置找到 121.42.11.34:8080

因此访问地址变为 http://121.42.11.34:8080/proxy

连接到 121.42.11.34 服务器,找到 8080 端口提供的 server

通过 server 找到 /usr/share/nginx/html/proxy/index.html 资源,最终展示出来。

Nginx 解决跨域的原理

例如:

  • 前端 server 的域名为:fe.server.com
  • 后端服务的域名为:dev.server.com

现在我在 fe.server.comdev.server.com 发起请求一定会出现跨域。

现在我们只需要启动一个 Nginx 服务器,将 server_name 设置为 fe.server.com 然后设置相应的 location 以拦截前端需要跨域的请求,最后将请求代理回 dev.server.com 。如下面的配置:

server {
 listen      80;
 server_name  fe.server.com;
 location / {
  proxy_pass dev.server.com;
 }
}

这样可以完美绕过浏览器的同源策略:fe.server.com 访问 Nginxfe.server.com 属于同源访问,而 Nginx 对服务端转发的请求不会触发浏览器的同源策略。

redis redis

3、使用redis有哪些好处?

3.1 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)

3.2 支持丰富数据类型,支持string,list,set,sorted set,hash

String

常用命令 :set/get/decr/incr/mget等;

应用场景 :String是最常用的一种数据类型,普通的key/value存储都可以归为此类;

实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr、decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。

Hash

常用命令 :hget/hset/hgetall等

应用场景 :我们要存储一个用户信息对象数据,其中包括用户ID、用户姓名、年龄和生日,通过用户ID我们希望获取该用户的姓名或者年龄或者生日;

实现方式:Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口。如图所示,Key是用户ID, value是一个Map。这个Map的key是成员的属性名,value是属性值。这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field),也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据。当前HashMap的实现有两种方式:当HashMap的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,这时对应的value的redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时redisObject的encoding字段为int。

List

常用命令 :lpush/rpush/lpop/rpop/lrange等;

应用场景 :Redis list的应用场景 非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现;

实现方式:Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。

Set

常用命令 :sadd/spop/smembers/sunion等;

应用场景 :Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的;

实现方式:set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。

Sorted Set

常用命令 :zadd/zrange/zrem/zcard等;

应用场景 :Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。

实现方式:Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。

posted @ 2021-06-15 14:49  我想喝杨枝甘露~  阅读(127)  评论(0)    收藏  举报