美文网首页
java杂谈

java杂谈

作者: 小王ovo | 来源:发表于2021-01-05 10:05 被阅读0次

    其实就是偶尔复习的时候随手写的东西,以后还会继续更新

    1.关于jvm内存的模型的复习

    1.栈
    2.堆(新生代(eden和幸存代)和老年代)
    3.程序计数器
    4.方法区(就是永久代,jdk8移除了,)
    5.字符串常量池
    6.本地方法栈
    7.堆外(直接内存)

    2.oom发生的几种可能

    1.分配一个超大对象,类似一个超大数组超过堆的最大值会oom
    2.堆内存不足导致oom,xmx调节堆大小
    3.程序不断递归调用,不停压栈会抛出stackoverflow,如果jvm这时候去扩展栈空间且失败就会抛出oom
    4.老版本永久带因为大小有限也会经常oom比如intern字符串缓存过多。
    5.直接内存不足也会oom

    3.初始化一个类的步骤

    1. 分配对象内存空间
    2. 初始化对象
    3. 设置instance指向刚刚分配的内存地址, 此时instance != null (重点)

    由于第二步和第三步没有依赖关系所以可能会被重排优化

    1. 分配对象内存空间
    2. 设置instance指向刚刚分配的内存地址, 此时instance != null (重点)
    3. 初始化对象

    原子性一般是说读写等操作是不可分割的

    可见性是多线程情况下对工作内存的变量修改会同步到主内存,多线程可见

    有序性就是代码按照顺序执行不会被指令重排

    4.happens-before原则

    1.一个线程内代码循序执行

    2.unlock操作现行发生于同一个锁的lock,也就是说第二次上锁,需要先解锁

    3.对于volatile写操作先行于读操作

    4.对与线程来说start()优先于其他动作

    5.线程的其他动作又优先于线程终止规则

    6.线程的中断调用interrupt()优先于中断检测interrupted()

    7.一个对象的构造函数执行优先于finalize()方法

    8.a操作先行于b操作,b操作先行于c操作,所以a先行于c

    无锁都是基于硬件的原子操作实现的

    cas会有aba问题就是要被赋值的变量在检测的时候,由a变成了b又变回了a这样就会通过检测,java是靠版本号(对变量维护一个版本号)来优化解决这个问题的

    自旋避免了线程切换的开销,但是占用了处理器时间

    5.关于锁

    1.关于Synchronized

    1.使用
    修饰实例方法,对当前实例对象this加锁
    修饰静态方法,对当前类的class对象加锁
    修饰代码块,制定一个加锁的对象,给对象加锁
    
    2.对象组成
    
        对象头
        mark word字段:存储对象的hashcode,存储分代年龄,存储锁标志位信息
        (00轻量级锁01无锁或者偏向锁10重锁11垃圾回收标记)
    
        klass point: 对象指向它的类元数据的指针,虚拟机通过这指针判断这对象是哪个类的实例
    
        实例数据
        存放类的数据信息,父类的信息
    
        填充数据
        虚拟机要求对象的起始地址必须是8字节的整数倍,为了字节对齐
        空对象的大小就是8字节,因为会自动对齐
    
    3.可重入性
    
        Synchronized锁对象的时候有个计数器,会记录下线程获取锁的次数,在执行完对应的代码块之后,计数器就会-1
        直到计数器清零,就释放锁了。
    
    4.不可中断性
    
        就是说,一个线程获取锁之后,另一个线程处于阻塞或者等待状态,前一个不释放,后一个也已知会阻塞或者等待,不可以被中断
    
    5.javap -c xxx.class可以查看反编译文件
    首先关联到一个monitor对象。
    当我们进入一个方法的时候,执行monitorenter,就会获取当前对象的一个所有权,这个时候monitor进入数为1,当前的这个线程就是这个monitor的owner。
    如果你已经是这个monitor的owner了,你再次进入,就会把进入数+1.
    同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。
    不知道大家注意到方法那的一个特殊标志位没,ACC_SYNCHRONIZED。
    同步方法的时候,一旦执行到这个方法,就会先判断是否有标志位,然后,ACC_SYNCHRONIZED会去隐式调用刚才的两个指令:monitorenter和monitorexit。
    所以归根究底,还是monitor对象的争夺。
    
    6.无锁->偏向锁->轻锁->重锁(锁升级过程)
    
    为什么需要有锁升级这种优化?
    多线程在同一时刻请求同一把锁,没拿到锁的线程需要被阻塞,所以需要用重量级锁(重锁有自旋,取消,粗化等优化)
    多线程在不同时间段请求同一把锁,也就是说没有锁竞争需要轻量级锁
    锁一直被同一个线程所持有,使用偏向锁即可
    

    wait()和sleep()的区别
    wait()来自Object类,sleep()来自Thread类
    调用 sleep()方法,线程不会释放对象锁。而调用 wait() 方法线程会释放对象锁;
    sleep()睡眠后不出让系统资源,wait()让其他线程可以占用 CPU;
    sleep(millionseconds)需要指定一个睡眠时间,时间一到会自然唤醒。而wait()需要配合notify()或者notifyAll()使用

    6.类加载的过程

    1.加载 2.链接 3.初始化

    1.字节码被读取到jvm中映射为class对象
    2.链接分为三个过程:
    1.验证:验证字节信息是否符合jvm规范
    2.准备:申请静态变量需要的内存,但不执行赋值操作
    3.解析:将常量池中的符号引用变成直接引用
    3.真正执行类的初始化代码,包括静态变量的赋值,静态初始化块内的逻辑

    什么叫做双亲委派模型
    1.就是一个类被加载的时候先找父类加载器,再找当前加载器
    2.类加载器分为bootstrap(.jar),ext(/jre/lib),app(classpath),自定义加载器

    7.逃逸分析的判断

    一是对象是否被存入堆中(静态字段或者堆中对象的实例字段),二是对象是否被传入未知代码中。

    一旦对象被存入堆中,其他线程便能获得该对象的引用。即时编译器也因此无法追踪所有使用该对象的代码位置。

    由于 Java 虚拟机的即时编译器是以方法为单位的,对于方法中未被内联的方法调用,即时编译器会将其当成未知代码,毕竟它无法确认该方法调用会不会将调用者或所传入的参数存储至堆中。因此,我们可以认为方法调用的调用者以及参数是逃逸的

    如果即时编译器能够证明锁对象不逃逸,那么对该锁对象的加锁、解锁操作没有意义。这是因为其他线程并不能获得该锁对象,因此也不可能对其进行加锁。在这种情况下,即时编译器可以消除对该不逃逸锁对象的加锁、解锁操作。就是你说的锁消除

    如果逃逸分析能够证明某些新建的对象不逃逸,那么 Java 虚拟机完全可以将其分配至栈上,并且在 new 语句所在的方法退出时,通过弹出当前方法的栈桢来自动回收所分配的内存空间。这样一来,我们便无须借助垃圾回收器来处理不再被引用的对象

    但是java没有使用栈上分配

    而是使用了标量替换

    那什么是标量替换呢?

    就是将原本对对象的字段的访问,替换为一个个局部变量的访问

    这些字段既可以存储在栈上,也可以直接存储在寄存器中。而该对象的对象头信息则直接消失了,不再被保存至内存之中。

    由于该对象没有被实际分配,因此和栈上分配一样,它同样可以减轻垃圾回收的压力。与栈上分配相比,它对字段的内存连续性不做要求,而且,这些字段甚至可以直接在寄存器中维护,无须浪费任何内存空间。

    8.关于wait()和notify()用法

    notify()一定要写在wait()的前面,这样才能及时唤醒

    wait 必须放在同步块,或者同步方法中。而sleep可以任意位置

    9.关于数据库的水平拆分和垂直拆分

    举个栗子
    user{
    id
    name
    age
    }

    我们把user存在不同的user表里面,每个user具体去那个表可以做个hash,实际就是我们一个user表拆分成了多个user表。这就是水平拆分

    特点

    每个表结构的都一样
    每个表数据都不一样,没有交集
    每个表都不是全量数据

    2.什么是垂直拆分?

    一行的数据如果太大,那就分成,多张表,表与表之间用某一列数据做连接(一般是主键)

    特点

    每个表数据都不一样,但有交集
    每个表结构都不一样
    每个表都不是全量数据

    什么是mysql的预读
    其实这不只是mysql,而是说一次最少读一个内存页(一页4k),这样也许会多读,但是把下一次可能用到的的数据也读到了,减少了磁盘io

    mysql的锁结构其实就是个内存结构

    重要的两个字段

    trx:代表当前锁是哪个事务生成的

    is_waiting:代表当前事务是否在等待

    10.关于2pc

    1.第一阶段投票阶段

    参与者通知协调者,协调者反馈结果

    2.第二个阶段

    收到参与者的反馈后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否提交还是回滚

    11.关于二叉树

    1.高度和深度都是从0开始的,层数从1开始

    2.满二叉可以利用数组存储,为了方便计算节点一般从下标为1开始存储

    3.树的前中后遍历其实是对于根节点的前中后
    前:根左右
    中:左根右
    后:左右根

    4.迭代写法无非是起一个栈把节点存进去while循环不断迭代按照根左右的顺序输出

    5.dfs深度遍历分为前中后序三种
    bfs宽度优先搜索

    12.二叉查找树

    1.先说要求
    二叉查找树的要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点,右子树的每个节点都要大于这个节点。

    2.查找,遍历,删除相关的操作

    13.copy-on-write

    本质是一种延时策略,只有在真正需要的时候才复制,而不是提前复制好。

    适合场景:读多写少,弱一致性。

    没有提供CopyOnWriteLinkedList是因为linkedlist的数据结构关系分散到每一个节点里面,对每一个节点的修改都存在竟态条件,需要同步才能保证一致性。arraylist就不一样,数组天然的拥有前驱后继的结构关系,对列表的增删,因为是copy on wirte,所以只需要cas操作数组对象就能够保证线程安全,效率上也能接受,更重要的是避免锁竞争带来的上下文切换消耗。有一点需要注意的是CopyOnWriteArrayList在使用上有数据不完整的时间窗口,要不要考虑需要根据具体场景定夺

    设计一个rpc的路由表

    思考

    1.每次 RPC 调用都需要访问路由表,所以访问路由表这个操作的性能要求是很高的。

    2.不过路由表对数据的一致性要求并不高,一个服务提供方从上线到反馈到客户端的路由表里,即便有 5 秒钟,很多时候也都是能接受的(这个时间参考了注册中心nacos)

    3.路由表是典型的读多写少类问题

    综合:对读的性能要求很高,读多写少,弱一致性。

    设计

    服务提供方的每一次上线、下线都会更新路由信息。
    1.一种是通过更新 Router 的一个状态位来标识,如果这样做,那么所有访问该状态位的地方都需要同步访问,这样很影响性能。

    2.另外一种就是采用 Immutability 模式,每次上线、下线都创建新的 Router 对象或者删除对应的 Router 对象。由于上线、下线的频率很低,所以后者是最好的选择。

    结果

    1.选择第二种方式来实现 使用copy-on-write

    大概结构ConcurrentHashMap<String, CopyOnWriteArraySet<Router>>

    路由表就是一个namenode

    14.关于一次加载和懒加载

    1.一次加载顾名思义就是一次把需要的数据在初始化的时候一次性全查出来加入缓存。

    2.懒加载需要哪一个数据就加载那个数据,不会多加载

    15.关于代码级别的锁升级与降级

    1.获取读锁,在没有释放读锁的情况下获取写锁,典型的锁升级场景,java不允许这样。
    此时读锁还没有释放。

    2.获取写锁,获取读锁,释放读锁,释放写锁。典型的锁降级场景

    16关于wait()和await()的关系

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

    17.关于线程池

    1.初始化
    ThreadPoolExecutor(
    int corePoolSize,//保持的最小线程数
    int maximumPoolSize,//最大线程数
    long keepAliveTime,//当线程空闲这么长时间后,且线程数大于corePoolSize,就要回收多余的线程资源
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,//工作队列
    ThreadFactory threadFactory,//线程工厂,自定义如何创建线程
    RejectedExecutionHandler handler//拒绝策略
    如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。
    )
    2.四种拒绝策略
    1.CallerRunsPolicy:提交任务的线程自己去执行该任务。
    2.AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
    3.DiscardPolicy:直接丢弃任务,没有任何异常抛出。
    4.DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。

    18.mysql方案的天花板

    1.执行计划是单机的
    2.如果一张表分布在不同的mysql server中那么读取下一行可能会很大
    3.数据复制问题,mysql是半同步或者异步的

    19.关于ioc

    1.什么是控制反转
    控制反转就是把创建bean的过程移交给第三方,这个第三方就是容器container

    容器负责创建、配置和管理 bean,也就是它管理着 bean 的生命,控制着 bean 的依赖注入。
    
    通俗点讲,因为项目中每次创建对象是很麻烦的,所以我们使用 Spring IoC 容器来管理这些对象,需要的时候你就直接用,不用管它是怎么来的、什么时候要销毁,只管用就好了。
    

    2.什么是bean?

    bean就是包装好的object
    

    3,spring是如何设计容器的?

    使用 ApplicationContext,它是 BeanFactory 的子类,更好的补充并实现了 BeanFactory 的。
    
    BeanFactory 简单粗暴,可以理解为 HashMap:
    k为全类名 v是对象
    Key - bean name
    Value - bean object
    

    4.ApplicationContext 的里面有两个具体的实现子类,用来读取配置配件的:

    ClassPathXmlApplicationContext - 从 class path 中加载配置文件,更常用一些;
    FileSystemXmlApplicationContext - 从本地文件中加载配置文件,不是很常用,如果再到 Linux 环境中,还要改路径,不是很方便。
    

    其实这就是 IoC 给属性赋值的实现方法,我们把「创建对象的过程」转移给了 set() 方法,而不是靠自己去 new,就不是自己创建的了。

    这里我所说的“自己创建”,指的是直接在对象内部来 new,是程序主动创建对象的正向的过程;这里使用 set() 方法,是别人(test)给我的;而 IoC 是用它的容器来创建、管理这些对象的,其实也是用的这个 set() 方法,不信,你把这个这个方法去掉或者改个名字试试?

    何为控制,控制的是什么?

    答:是 bean 的创建、管理的权利,控制 bean 的整个生命周期。

    何为反转,反转了什么?

    答:把这个权利交给了 Spring 容器,而不是自己去控制,就是反转。由之前的自己主动创建对象,变成现在被动接收别人给我们的对象的过程,这就是反转。

    何为依赖,依赖什么?

    程序运行需要依赖外部的资源,提供程序内对象的所需要的数据、资源。

    何为注入,注入什么?

    配置文件把资源从外部注入到内部,容器加载了外部的文件、对象、数据,然后把这些资源注入给程序内的对象,维护了程序内外对象之间的依赖关系。

    所以说,控制反转是通过依赖注入实现的。但是你品,你细品,它们是有差别的,像是「从不同角度描述的同一件事」:

    IoC 是设计思想,DI 是具体的实现方式;
    IoC 是理论,DI 是实践;
    从而实现对象之间的解藕(这样你们不管怎么修改外部的对象,都对我们内部的对象没有影响)。

    当然,IoC 也可以通过其他的方式来实现,而 DI 只是 Spring 的选择。

    IoC 和 DI 也并非 Spring 框架提出来的,Spring 只是应用了这个设计思想和理念到自己的框架里去。

    20.关于网络的七层模型

    1.物理层面
    2.链路层
    3.网络层
    4.传输层(tcp/udp)
    5.会话层
    6.表示层
    7.应用层

    什么是tcp?

    TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。

    什么是一个tcp连接呢?

    用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。

    Socket:由 IP 地址和端口号组成

    序列号(sequence numbers):用来解决乱序问题等

    窗口大小(window size):用来做流量控制

    如何确定一个唯一的连接呢?

    源地址 源端口 目标地址 目标端口(从哪来到哪去)从哪个主机发送到那个主机,从哪个进程发送到那个进程

    拥塞控制、流量控制

    TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。

    UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。

    前两次握手不能携带数据,第三次握手可以携带数据

    为什么一定要三次握手

    1.三次握手才可以阻止历史重复连接的初始化(主要原因)
    比如旧的syn比新的先到

    2.三次握手才可以同步双方的初始序列号
    这样一来一回,才能确保双方的初始序列号能被可靠的同步。

    3.三次握手才可以避免资源浪费
    如果不回复ack,每次syn就要主动建立连接,如果客户端的syn在网络中拥堵了,且收不到ack。就会多次发送syn。这样的话就会建立多个连接,造成连接资源的浪费。

    何为syn攻击?

    在三次握手阶段客户端发送syn服务端发送ack+syn后,客户端不回复ack这样服务端就会一直等待客户端ack直到超时,服务端的syn队列会很快排满

    1.修改内核参数
    修改队列大小,以及队列满后的处理策略

    四次挥手(断开连接)

    主动关闭连接的,才有 TIME_WAIT 状态。

    21.关于cas

    1.本质
    是cpu提供了compare and swap指令,该指令提供了三个参数
    1.共享变量的内存地址A。
    2.用于比较的值B。
    3.用于共享变量的新值C。
    当A=B时,更新A=C

    ABA

    解决方式版本号or时间戳

    22.关于hashmap的一切

    1.组成

    是有数组和链表组成的,根据hash求出数组索引
    。当索引重复的时候新的键值对就会在链表上增加一个节点

    2.当hash重复的时候是如何插入的呢?

    1.8之前是头插法(就是插在链表的头部)
    1.8之后是尾插法

    为什么会修改呢?
    因为头插法会在多线程插入且rehash的时候形成循环链表
    尾插法不会修改链表的顺序所以不会引发这个问题

    3.为何resize何时resize

    1.当容量不够的时候需要扩容,就需要resize了
    2.当存储的键值对达到容量*0.75(负载因子)就需要resize

    如何扩容

    1.创建一个长度是原数组两倍的新数组
    2.遍历原数组,把所有键值对重新hash到新数组里面

    为什么rehash

    1.求index的公式index = HashCode(Key) & (Length - 1)
    当数组长度不一致时计算出来的值也变了。

    为什么是默认初始化容量是16

    因为16-1是15,15的二进制是1111
    可以的hashcode&1111得出的十进制数就是4,只要length-1不是2的幂数,那么转二进制之后,
    就都是1,那么hash算法算出的数字就和hashcode的后几位有关。那么只要hashcode均匀那么hash
    算法就是均匀的

    为什么重写equals需要重写hashcode?

    因为equals是比较两个内存的地址,如果是值对象就比较两个对象的值
    如果是引用对象,就比较两个引用的地址。
    如果我们的两个对象算出的index都是2那么他们就存在同一个链表里面了
    如果他们hashcode一致我没就没法区分到底我们想要get的是哪个。
    所以我们需要重写hashcode算法.保证每一个key的hashcode都不一样

    插入过程

    1.链表法:先key hash func获取存在那个桶里(数组)然后用index公式获取插在数组的那个位置,如果当前数组的位置已经有内容了
    就使用尾插法插入链表。
    2.开放地址法:当前桶被占了,就用一定的方式去找下一个桶,直到找到空的

    什么时候转红黑树

    当链表大小超过8个的时候。

    与hashtable的对比

    1.hashtable是并发安全的,hashmap不是,所以在多线程写入的时候会有数据覆盖问题。hashtable不允许键值为null,hashmap允许
    hashmap:
    初始化容量是16
    扩容直接乘2

    hashtable:
    初始化容量是11
    乘2+1

    fail-fast机制是怎么回事?

    快速失败是java集合中的一种机制,在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容做了修改,就会抛出异常。集合会维护一个modcount变量,执行一个hashNext()/Next()的时候会去判断modcount是否为expectedmodCount,如果不是就抛出异常

    fail-safe

    安全失败是并发包里面的机制,即查询的时候先看数据存在不存在

    23.ConcurrentHashMap

    使用了分段锁,锁的是数组的每一个元素。

    默认容量是16

    插入

    1.先是尝试获取锁
    2.自旋获取锁(scanAndLockForPut(key,hash,value))
    3.自旋到一定次数,改为阻塞获取锁
    4.put的时候会判断value==null如果等于会抛出异常

    也就是说一定是获取锁再去操作

    步骤和之前的hashmap一样,先算出index
    然后遍历链表,如果有hashcode相等的就说明
    当前节点存在,那么需要更新节点,如果当前节点不存在,
    那么就new一个新节点,然后将新节点利用尾插法插入链表
    修改成之后释放锁。

    jdk 1.7使用的是segment分段锁

    jdk 1.8使用cas+synchronize

    之前的方式是获取锁然后写入,现在是先cas写入,如果容量不足则需要扩容。
    如果都不满足怎利用synchronized锁写入数据
    如果链表大于8个节点了,需要转成红黑树

    24.回顾缓存行

    1.为什么要有缓存行?
    因为cpu比内存快太多了,为了更快的从内存中读取数据,于是诞生了多级缓存

    2.查询过程
    首先会去L1缓存查找所需数据,如果没找到,再去L2缓存查找,一次类推,知道从内存中获取数据,这也就意味着,越长的调用链,所耗费的时就越长,如果每次去主存的时候多拿一些,那么是不是就避免了频繁您访问主存了呢?一般来说每个缓存行大小为64字节,并且每个缓存行有效的引用主内存的一块儿地址,cpu每次从主内存中获取数据时,会将相邻的数据也一同拉取到缓存行中,这样当cpu执行运算时,就大大减少了于主内存的交互。

    3.当多线程同时修改cache会怎么样?
    core1上的线程修改了l1里面的a,同时告诉其他cpul1里面的缓存引用已经没用了,这时core2上的线程发起了修改同一个缓存中的变量b,为了可见性core会将数据回写回主存。此时core2将内存重新读取到l1缓存然后在修改。所以说一个cache line的数据被多线程访问,就会相互竞争,并且频繁回写。

    4.那么如何解决未共享问题?
    数据填充,填满一个缓存行。即当前数据和填充数据的大小为64字节,下一个数据放在下一个64字节里面即可。

    25.随便复习一下快速排序

    1.第一步选择一个基准值,一般选择头部元素作为基准值
    2.将小于基准值的元素放在基准值的前面,将大于基准值的元素放在基准值后面。

    26.jdk动态代理和cglib的区别

    JDK 动态代理只能只能代理实现了接口的类,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。

    27.关于ThreadLocal

    28缓存更新的套路

    1.Cache Aside Pattern

    失效:应用程序先从cache取数据,没有得到,则从数据库读取,成功后放到缓存中。

    命中:应用程序从cache中取数据,取到后返回

    更新:先把数据存到数据库中,成功后,再让缓存失效。

    一个读操作没有命中缓存,然后就去数据库拉数据了,同时有一个并发写操作,写完数据库之后让缓存失效,然后之前的读操作会把老数据放入缓存,所以,会造成脏数据。

    2.Read/Write Through Pattern

    更新数据库的操作由缓存代理,从应用来说后端就是一个单一的存储。

    Read Through
    如果读的时候缓存失效,缓存服务将数据从数据库读出并加入缓存。

    Write Through
    当有数据更新且没有命中缓存,就直接更新数据库。命中了就先更新缓存,再更新数据库

    3.Write Behind Caching Pattern

    更新数据的时候只更新缓存,不更新数据库,由缓存异步批量的更新数据库

    操作系统的write back会在仅当这个cache需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫lazy write。

    29.关于LRU

    最近最少使用。假设最少使用的信息,将来被使用的概率也不大,所以在内存不够的情况下,就可以吧这些不常用的信息踢出去,腾地方。

    FIFO先进先出

    LFU 对每个访问信息记数,踢走访问次数最的那个,如果访问次数一样,就踢走好久没用过的那个。

    30.堆和堆排序

    1.堆是一个完全二叉树

    2.完全二叉树就是除最后一层节点,其他层的节点都是满的,最后一层的节点都靠左排序

    3.堆中的每个节点的值必须大于等于(或者小于等于)其子树中每个节点的值。实际上,我们还可以换一种说法,堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。这两种表述是等价的。

    4.对于每个节点的值都大于等于子树中每个节点值的堆,我们叫做“大顶堆”。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫做“小顶堆”。

    5.因为是完全二叉树,所以适合存储在数组里。如果一个节点为下标i,那么左节点为i2,右节点为
    i
    2+1,如果有父节点那父节点为i/2\x32

    1.添加一个节点

    直接将数据放在堆尾,然后开始判断有没有父节点即i/2>0。

    然后判断父节点于子节点的大小

    大顶堆a[i]>a[i/2]

    小顶堆a[i]<a[i/2]

    以上这两个条件符合说明需要堆化,即交换元素位置,并继续判断

    2.删除一个节点

    为了让删除后的节点也是一个完全二叉树

    例子从根节点开始删除

    首先将根节点个尾节点作交换,然后对交换后树进行堆化即可。

    31.关于redis分布式锁的问题

    1.上锁

    set命令要用set key value px milliseconds nx
    jedis.set(String key, String value, String nxxx, String expx, int time)
    需要5个参数为了解决四个问题

    1.互斥性。在任意时刻,只有一个客户端能持有锁。

    2.不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

    3.具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。d

    4.解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

    第一个为key,我们使用key来当锁,因为key是唯一的。

    第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?
    原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,
    我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

    第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

    第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

    第五个为time,与第四个参数相呼应,代表key的过期时间。

    2.解锁

    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
    if (RELEASE_SUCCESS.equals(result)) {
    return true;
    }

    首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)

    使用eval()可以保证原子性

    3.缺陷

    如果我们在master节点获取了锁,且锁还没有没有被同步到slave节点,此时如果master节点出现错误,slave节点升级为master节点就会导致锁丢失

    4.redlock(3,5,7奇数节点)

    1.获取当前Unix时间,以毫秒为单位。

    2.依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。
    当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。
    例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。
    如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。

    3.客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。
    当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。

    4.如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
    如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间)
    ,客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,
    防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

    32.nio

    客户端 到 buffer 到channel 到selector 到线程 开始执行
    selector.open获取一个选择器
    1.当客户端连接时,会通过serversocketchannel得到对应的socketchannel
    2.selector进行监听 select()方法(可使用阻塞或者非阻塞方法),返回有事件发生的通道的个数。
    3.将得到的channel注册到selector
    4.注册后得到一个selectionkey,会被selector管理,以集合的方式
    5.四种事件 读事件 写事件 连接成功事件 新连接事件
    6.获取selectionkey
    7.通过key获取channel
    7.通过channel完成业务的处理

    33.索引问题

    索引解决的问题
    1.索引极大的减少了扫描行数
    2.避免重排序和临时表
    3.将随机io变成顺序io

    select * from user order by age desc;
    这条sql会将所有的行加载到内存之后,再按age排序生成一张临时表表
    再将这张表排序后返回给客户端如果临时表大于tmp_table_size的值(默认16M),
    那么这张内存临时表会变成磁盘临时表,如果加了索引的话,
    索引本身是有顺序的,所以从磁盘读取行数本身就是按照age排序好的
    不用生成临时表和额外排序,提升了性能

    什么时候sql的索引失效呢
    1.当sql语句中索引列是函数或者表示式的一部分
    即where条件后面跟的是一个计算过程或者函数调用

    2.隐式类型转换
    比如说表里存的是个string但是sql写的是个int就会触发隐式类型转换
    就会调用cast函数,于是就触发了上一条规约

    3.隐式编码转换
    如果两张表编码不一致那么当一句sql涉及到这两张表时就会触发隐式的函数调用
    转换编码集

    因为使用order by导致的全表扫描,加了索引还是全表扫描了,因为select *导致了回表查询

    什么是回表查询呢

    我们都知道普通索引叶子节点存主键id,聚簇索引叶子节点存具体的行的值

    我们如果使用聚簇索引查到索引就找相对的行的值

    如果使用普通索引就会先找到主键id,然后再走一遍聚簇索引找到具体的记录

    这样就走了两次索引查询,故而叫做回表查询

    34.mysql事务

    事物的四大特性
    1.原子性:是一个不可分割的整体,要么全都执行,要么全不执行。执行出错事务回滚。
    2.隔离性:同一时间,只允许一个事务请求同一组数据。不同事物彼此之间没有干扰。
    3.一致性:事务开始前和开始后。数据库的完整性约束没有被破坏。
    4.持久性:事务完成后落盘,不能回滚。

    事务的并发问题
    脏读:事务A读取了事务B的数据,事务B回滚,A读到了脏数据。
    幻读:事务A修改表A,事务B向表A插入了一条数据,事务A修改完发现表A有一条记录还没有被修改。
    不可重读:事务A不断的读表A,事务B不断的修改表A。导致事务A读取到的数据不一致。

    四大隔离级别

    1.读未提交。就是读到脏数据
    2.读已提交。就是要等另一个事务提交完了,才能够读取。解决了脏读问题
    3.可重读。保证了每次的读到的数据一致,不管其他事务是否已经提交。解决了不可重读问题
    4.序列化。开启一个序列化事务,其他事务的对数据表的写操作都会挂起。

    mysql
    有行锁表锁页锁三种innob只有行锁表锁两种。
    表锁
    行锁
    页锁

    35.mybatis的缓存

    1.一级缓存(一级缓存sqlsession)

    1.同一个 SqlSession 对象, 在参数和 SQL 完全一样的情况先, 只执行一次 SQL 语句

    2.在同一个 SqlSession 中, Mybatis 会把执行的方法和参数通过算法生成缓存的键值, 将键值和结果存放在一个 Map 中, 如果后续的键值一样, 则直接从 Map 中获取数据;

    3.不同的 SqlSession 之间的缓存是相互隔离的;

    4.用一个 SqlSession, 可以通过配置使得在查询前清空缓存;

    5.任何的 UPDATE, INSERT, DELETE 语句都会清空缓存。

    2.二级缓存(二级缓存mapper)

    二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace 级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。缓存的清除策略也是lru。增删改都会刷新缓存,而且是namespace级别的。所以比较好的实践是将一个表的相关的sql放入同一个mapper里面,这样既方便管理,同时也不会让二级缓存影响其他表。

    3.关于mapper和repository

    1.相同点
    两个都是注解在Dao上

    2.不同
    @Repository需要在Spring中配置扫描地址,然后生成Dao层的Bean才能被注入到Service层中。

    @Mapper不需要配置扫描地址,通过xml里面的namespace里面的接口地址,生成了Bean后注入到Service层中。

    36.mysql相关

    1.mysql的执行流程

    1.连接器:请求接收和权限验证
    2.查询缓存:命中则直接返回结果
    3.分析器:词法分析,语法分析
    4.优化器:执行计划生成,索引选择
    5.执行器:操作引擎,返回结果

    相关问题

    1.使用长连接过多会导致mysql内存暴涨,因为在mysql执行时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。

    2.如果create table的时候不指定存储引擎会默认使用innodb

    3.一个连接除了建立以外如果没有后续动作,那么就是空闲连接,mysql的空闲连接是8小时断开

    解决方案:
    1.定期断开连接,或者程序里面判断执行过一个占用内存的大查询后。断开连接。之后要查询再重连。

    2.如果你用的mysql 5.7之后的版本,可以在每次执行一个比较大的操作后初始化连接资源
    

    查询缓存

    当连接建立以后就可执行语句了,mysql拿到一个请求会先去查询缓存看看,之前是不是执行过这条语句。之前执行过的语句可能会以key-value的形式,被直接缓存在内存里。key的相应的查询语句value是查询的结果。

    建议不要使用查询缓存
    1.查询缓存失效非常的频繁,只要有对一个的更新,这个表上所有的缓存都会被清空。因此你可能费劲的把查询结果缓存了起来,还没使用就被一个更新全部清空了。

    2.由上一条推出,那些更新很少,或者基本不怎么更新的表比较适合使用查询缓存。
    你可以将参数 query_cache_type 设置成 DEMAND这样对于默认的 SQL 语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定,像下面这个语句一样。
    
    当然
    

    37.jwt

    第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
    1.header:
    {
    'typ': 'JWT',
    'alg': 'HS256'
    }
    加密方式base64
    2.payload:
    自己添加的一些信息
    {

    }
    加密方式还是base64
    3.signature
    使用用户选定的加密算法加盐的方式进行生成

    三段用.组合就是jwt.

    38.关于自动装箱类型的测试

    使用自动装箱类型:6500
    使用基本类型:702

    计算还是不要使用对象类型,性能差了9倍还多.恐怖

    相关文章

      网友评论

          本文标题:java杂谈

          本文链接:https://www.haomeiwen.com/subject/wgjioktx.html