java-后端八股文

  • Post author:
  • Post category:java

文章目录

java 基础

创建对象的5种方式

  1. new
  2. Class 对象的newInstance()方法
  3. 类构造器,Demo.class.getConstructor.newInstance()
  4. 反序列化
  5. Ojbect.clone()方法

多态

分类

  1. 编译期间:方法重载,静态分派
  2. JVM运行期间:方法重写,动态分派

静态和变量看左边(父类),方法行为看右边(子类)
javap -c demo.class 命令输出代码的字节码

throwable

在这里插入图片描述

Collection

CollectionAbstractMap
AbstractCollection-AbstractList

Queue

在这里插入图片描述

  1. 内置的不阻塞队列:
    1.1 ConcurrentLinkedQueue
    1.1 ConcurrentLinkedQueue
  2. 阻塞队列
    2.1 DelayQueue 由优先级堆支持的、基于时间的调度队列
    2.2 SynchronousQueue 由BlockingQueue 接口的简单聚集机制队列
    2.3 PriorityBlockingQueue 由优先级堆支持的无界优先级队列
    2.4 LinkedBlockingQueue 由链接节点支持的可选有界队列
    2.5 ArrayBlockingQueue 数组支持的有界队列
  3. 双端队列
    3.1 ArrayDeque
    3.2 BlockingDeque
    3.2.1 LinkedBlockingDeque

HasMmap

  1. jdk1.7 扩容的transfer() 头插法扩容会出现环
  2. jdk.1.8
    2.1 通过高低位指针链表解决1的问题
    两组指针将链表分成了两部分,高位指针指向哪些扩容后下标变为(旧索引+扩容大小),低位指针指向哪些扩容后下标还保持不变的节点。分成两条链表今次那个迁移,迁移后节点的前后顺序保持不变,不会出现环的情况
    2.2 尾插法put回出现数据覆盖丢失

CurrentHashMap

  1. jdk1.7 采用segment 分段锁+hashEntry
  2. jdk1.8 采用synchronized+cas 实现,保留了segment只做序列化使用
    采用ForwardingNodes,是的currentHashmap具有分段锁特性
    2.1 sizeCtl
/**
table is being initialized or resized: -1 for initialization,
After initialization, holds the next element count value upon which to resize the table*/
private transient volatile int sizeCtl;

2.2 ConcurrentHashMap支持多线程扩容,多个线程处理不同的节点

java线程同步机制

  1. synchronized
  2. ReentrantLock
  3. Semaphore
  4. AtomicInteger

接口和抽象类

  1. 接口无构造方法,抽象类有构造方法
  2. 接口多实现,抽象类单继承
  3. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
  4. 抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;
  5. 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;

hashcode equals ==

  1. == 对于基本类型比较的值 对于引用类型比较的引用的地址
    2.equals 比较的是值
  2. hashcode

ArrayList LinkedList

  1. 如果应用程序对数据有较多的随机访问,ArrayList对象要优于LinkedList对象;
  2. 如果应用程序有更多的插入或者删除操作,较少的随机访问,LinkedList对象要优于ArrayList对象;

Synchronized使用

  1. 修饰普通方法
    Synchronized 关键字修饰的方法如果被重写默认不同步,如果同步需要加上Synchronized
  2. 修饰静态方法
    静态方法是属于类的而不属于对象的,锁定的是这个类的所有对象
  3. Synchronized(this)
    锁的是当前对象,当前有几个对象那么这个this就是有多份
  4. Synchronized(XX.class)
    作用的对象是这个类的所有对象

Happen-Before原则

  1. 前一个线程对数据的操作,对于后一个线程是可见的
  2. 类型
    2.1 程序顺序规则
    2.2 volatile 变量规则
    2.3 传递性规则
    2.4 线程启动规则
    Thread对象的start()方法happen-before此线程的每一个动作
    2.5 线程终止规则
    线程的所有操作都happen-before对此线程的终止检测,可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段检测到线程已经终止执行
    2.6 线程中断规则
    对线程interrupt()方法的调用happen-before发生于被中断线程的代码检测到中断时事件的发生
    2.7 监视器原则

Java-Spring

spring的aop实现的两种方式

  1. 一种是采用声明的方式来实现(基于XML)
  2. 一种是采用注解的方式来实现(基于AspectJ)

spring的循环依赖如何解决

  1. Spring实例化一个bean的时候,是分两步进行的,首先实例化目标bean,然后为其注入属性。
  2. Spring是通过递归的方式获取目标bean及其所依赖的bean的;
  3. Spring解决循环依赖问题是通过结合bean实例化和bean属性填装分离,singletonObjects、earlySingletonObjects 、singletonFactories 三级缓存机制和引用提前暴露机制实现的

Spring-@Transaction生效

  1. @Transactional 注解只能应用到 public 可见度的方法上。
  2. 在类内部调用将不起作用,
  3. 只有在外部调用的情况下才能引起事务,是由于Spring Aop的本质决定的

Spring事务传播行为

  1. TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  2. TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  3. TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  4. TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  5. TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  6. TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  7. TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价TransactionDefinition.PROPAGATION_REQUIRED

并发编程

volatile实现原理

  1. volatile 写操作的前面插入StoreStore屏障,保证volatile写操作不会和之前的写操作重排序
  2. 在volatile写操作后面插入一个StoreLoad屏障,保证volatile写操作之后不会和之前的读操作重排序
  3. 在volatile 读操作后面加入LoadLoad屏障,LoadStore屏障,保证volatile读操作不会和之后的读操作,写操作重排序

cpu 基本内存屏障分为四种

  1. LoadLoad: 禁止读和读的重排序
  2. StoreStore:禁止写和写的重排序
  3. LoadStore:禁止读和写的重排序
  4. StoreLoad:禁止写和读的重排序

jdk8之后,java unsafe类中提供了三个内存屏障函数,l

  1. loadFence(); = LoadLoad+LoadStore
  2. storeFence(); = StoreStore+LoadStore
  3. fullFence(); = loadFence()+storeFence()+StoreLoad

单线程也会重排序,通过as-if-serial 但是最终结果是一致的,
多线程 happen-before原则保证,JMM 内存模型

Java-线程

线程的状态

在这里插入图片描述

java线程池的数量依据

  1. 如果是CPU密集型应用,则线程池大小设置为N+1(或者是N),线程的应用场景:主要是复杂算法
  2. 如果是IO密集型应用,则线程池大小设置为2N+1(或者是2N),线程的应用场景:主要是:数据库数据的交互,文件上传下载,网络数据传输等等
    +1的原因是:即使当计算密集型的线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费
  3. 在多核多线程池情况下
    3.1 最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

java创建线程池的方式

  1. 通过Executors创建
    1.1 newFixedThreadPool
    FixedThreadPool只有核心线程,且该核心线程都不会被回收,这意味着它可以更快地响应外界的请求
    在这里插入图片描述

1.2 newCachedThreadPool 同上
不存在核心线程,只存在数量不定的非核心线程,而且其数量最大值为Integer.MAX_VALUE
1.3 newSingleThreadExecutor 同上
只有一个核心线程
1.4 newScheduledThreadPool 如下
核心线程数量是固定的,而非核心线程是没有限制的,并且当非核心线程闲置时它会被立即回收
在这里插入图片描述

  1. 通过ThreadPoolExecutor创建

线程池拒接策略

  1. AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常
  2. CallerRunsPolicy:将直接在 正在被调用的线程 的 execute() 方法中 运行被拒绝的task
  3. DiscardPolicy:丢弃任务,但是不抛出异常
  4. DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务

ThreadPoolExecutor的成员变量

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  1. corePoolSize核心池大小、maximumPoolSize最大线程数量,它们是用来控制线程的数目,总体流程为1:若小于Core则新建线程执行;2:若大于等于Core,则加入BlockingQueue;3:若BlockingQueue满了之后,若小于Maximum,则新建线程来执行任务,若大于等于Maximum,则执行拒绝策略
  2. threadFactory线程工厂,它是用来创建线程的地方,Executors中默认的线程工厂会将守护线程属性设置为false,线程的优先级设置为Normal登记;Executors还提供了权限线程工厂privilegedThreadFactory,可以对其的访问权限进行控制
  3. BlockingQueue workQueue任务队列,用来存放需要等待执行的任务,根据策略不同,Executors中提供了4种阻塞队列,不含容量的同步队列Synchronous队列,它不保存任务,一直创建新的线程执行,直到到maximum,然后执行拒绝策略;LinkedBlockingQueue无界队列,所以可以一直添加直到系统OOM;ArrayBlockingQueue它是有界的;PriorityBlockingQueue根据优先级的大小进行执行任务,Array和Linked则根据FIFO来执行任务
  4. keepAliveTime存活时间,对于超过CorePoolSize大小的线程最多存活的时间;
  5. rejectedExecutionHandler拒绝策略,对于超标,不能执行的拒绝策略;

线程运行

  1. 线程同步运行 CountDownLatch实现
  2. 线程顺序执行 volatile和CountDownLatch实现
  3. 线程交替执行 ReentrantLock以及Condition来实现

Java-锁

AQS

lock和synchronize的区别

在这里插入图片描述

  1. Lock是一个接口,而synchronized是关键字。
  2. synchronized会自动释放锁,而Lock必须手动释放锁。
  3. Lock可以让等待锁的线程响应中断,而synchronized不会,线程会一直等待下去。
  4. 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  5. Lock能提高多个线程读操作的效率。
  6. synchronized能锁住类、方法和代码块,而Lock是块范围内的

lock如何保证执行过程的可见性和有序性

互斥锁、自旋锁、条件锁、读写锁、可重入锁、偏向锁、公平锁、非公平锁、可重入锁、共享锁、排它锁、轻量级锁、重量级锁

  1. 互斥锁
    共享资源的使用是互斥的,即一个线程获得资源的使用权后就会将该资源加锁,使用完后会将其解锁,如果在使用过程中有其他线程想要获取该资源的锁,那么它就会被阻塞陷入睡眠状态,直到该资源被解锁才会被唤醒,如果被阻塞的线程不止一个,那么它们都会被唤醒,但是获得资源使用权的只有一个线程,其它线程又陷入沉睡.
  2. 自旋锁
    自旋锁是一种特殊的互斥锁,当资源被枷锁后,其他线程想要再次加锁,此时该线程不会被阻塞睡眠而是陷入循环等待状态(不能在做其它事情),循环检查资源持有者是否已经释放了资源,这样做的好处是减少了线程从睡眠到唤醒的资源消耗,但会一直占用CPU的资源。适用于资源的锁被持有的时间短,而又不希望在线程的唤醒上花费太多资源的情况。
  3. 条件锁
    条件变量用来自动阻塞一个线程,直到某特殊情况发生,通常条件变量和互斥锁同时使用。
    条件变量使我们可以睡眠等待某条件出现。
    条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:
    1)一个线程因 “条件不成立” 而挂起等待
    2)另一个线程使 “条件成立”, 并发出信号
  4. 读写锁
    它拥有读状态加锁、写状态加锁、不加锁这三种状态。
    只有一个线程可以占有写状态的锁,但可以有多个线程同时占有读状态锁,这也是它可以实现高并发的原因。当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;如果是处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,直到所有线程的读状态锁被释放;为了避免想要尝试写操作的线程一直得不到写状态锁,当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。所以读写锁非常适合资源的读操作远多于写操作的情况
  5. 可重入锁
    同一个线程可以多次获得该资源锁,别的线程必须等该线程释放所有次数的锁才可以获得
  6. 偏向锁
    大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。JDK1.6之后 偏向锁默认开启
    偏向锁是锁状态中最乐观的一种锁:从始至终只有一个线程请求同一把锁

java-jvm

Synchronized 和 volatile 如何保证可见性和原子性

  1. 可见性
    1.1. Synchronized
    1.1.1 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值
    1.1.2 线程解锁前,必须把共享变量的最新值刷新到主内存中
    1.1.3 1)获得互斥锁2)清空工作内存 3)从主内存拷贝变量的最新副本到工存 4)执行代码 5)将更改后的共享变量的值刷新到主内存 6)释放互斥锁
    1.2 Volatile 通过内存屏障和指令重排序来实现
    1.2.1 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令。
    1.2.2 对volatile变量执行读操作时,会在读操作后加入一条load屏障指令
  2. 原子性
    2.1 Synchronized 支持
    2.2 Volatile 不支持
  3. Synchronized 和 Volatile 区别
    3.1 volatile比synchronized更轻量,不需要加锁,不会阻塞线程
    3.2 synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性,但java的内存模型保证声明为volatile的long和double变量的get和set操作是原子的

CAS 是怎么保证原子性的

  1. 当虚拟机遇到一条new指令时候,首先去检查这个指令的参数是否能在常量池中能否定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
  2. 在类加载检查通过后,接下来虚拟机将为新生的对象分配内存。对象所需的内存的大小在类加载完成后便可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来
    2.1 指针碰撞(Bump the Pointer):假设Java堆的内存是绝对规整的,所有用过的内存都放一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅把那个指针向空闲空间那边挪动一段与对象大小相等的距离
    2.2 空闲列表(Free List):如果Java堆中的内存并不是规整的,已使用的内存和空间的内存是相互交错的,虚拟机必须维护一个空闲列表,记录上哪些内存块是可用的,在分配时候从列表中找到一块足够大的空间划分给对象使用。
  3. 在并发情况下划分不一定是线程安全的,有可能出现正在给A对象分配内存,指针还没有来得及修改,对象B又同时使用了原来的指针分配内存的情况,解决这个问题两种方案:
    3.1 分配内存空间的动作进行同步处理:实际上虚拟机采用CAS配上失败重试的方式保证了更新操作的原子性
    3.2 内存分配的动作按照线程划分在不同的空间中进行:为每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)
  4. 内存分配完成后,进行初始化

new出来的对象是一创建出来就进入堆中的吗

逃逸分析(Escape Analysis) – 分析对象的动态作用域。假如我们在一个方法内定义了一个对象,如果它被作为参数传递到其他地方,被本方法外的方法引用,这就就叫做方法逃逸

如果我们经过逃逸分析发现,某个对象并没有发生方法逃逸,那么它的生命周期则始于方法调用,卒于方法结束,那么此时它就是方法内的局部变量,而堆内存是线程间共享的,如果将它分配到堆中,方法结束后,它将不在被任何对象所引用,还需要GC进行回收,很不划算,于是 JIT就会将其分配到方法的栈帧中,这就是栈上分配。实际上在HotSpot中,栈上分配并不是直接在方法的栈帧中放入一个对象,它是通过标量替换的方式存储的,即将对象分解成组成对象的若干个成员变量,这些变量是无法再分解的更小的数据,叫做标量,然后用这些标量来代替之前的对象,这就叫标量替换。通过标量替换,原本的一个对象,被替换成多个成员变量。而原本需要在堆上分配的内存,也就不再需要了,完全可以在本地方法栈中完成对成员变量的内存分配。

TLAB

TLAB (Thread Local Allocation Buffer,线程本地分配缓冲区)是 Java 中内存分配的一个概念,它是在 Java 堆中划分出来的针对每个线程的内存区域,专门在该区域为该线程创建的对象分配内存。它的主要目的是在多线程并发环境下需要进行内存分配的时候,减少线程之间对于内存分配区域的竞争,加速内存分配的速度。TLAB 本质上还是在 Java 堆中的,因此在 TLAB 区域的对象,也可以被其他线程访问

如果没有启用 TLAB,多个并发执行的线程需要创建对象、申请分配内存的时候,有可能在 Java 堆的同一个位置申请,这时就需要对拟分配的内存区域进行加锁或者采用 CAS 等操作,保证这个区域只能分配给一个线程

启用了 TLAB 之后(-XX:+UseTLAB, 默认是开启的),JVM 会针对每一个线程在 Java 堆中预留一个内存区域,在预留这个动作发生的时候,需要进行加锁或者采用 CAS 等操作进行保护,避免多个线程预留同一个区域。一旦某个区域确定划分给某个线程,之后该线程需要分配内存的时候,会优先在这片区域中申请。这个区域针对分配内存这个动作而言是该线程私有的,因此在分配的时候不用进行加锁等保护性的操作

jvm-如何判断对象需要回收

  1. 引用计数法
    缺点 循环引用 造成内存泄露
  2. 可达性分析
    可作为root的
    静态属性(被static修饰的属性)
    常量(被static final修饰的属性)
    虚拟机栈(本地变量表)中引用的对象
    本地方法栈中引用的对象

jvm-垃圾回收算法

  1. 标记-清除
  2. 标记-复制
  3. 标记-整理
  4. 分代回收

jvm-典型的垃圾收集器

1.Serial/Serial Old收集器 是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。
2.ParNew收集器 是Serial收集器的多线程版本,使用多个线程进行垃圾收集。
3.Parallel Scavenge收集器 是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。
4.Parallel Old收集器 是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。
5.CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS 收集器仅作用于老年代的收集。
6.G1收集器 是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型

Jvm内存溢出排查

  1. jmap -heap pid
  2. 下载MemoryAnalyzer MAT工具导入第一步导出的heap.bin进行分析

操作系统

进程与线程的区别

(1)进程有自己的独立地址空间,线程没有
(2)进程是资源分配的最小单位,线程是CPU调度的最小单位
(3)进程和线程通信方式不同,同一进程下的线程共享数据(比如全局变量,静态变量)
(4)进程上下文切换开销大,线程开销小
(5)一个进程挂掉了不会影响其他进程,而线程挂掉了会影响其他线程

进程上下文切换比线程上下文切换

进程切换分两步:
1.切换页目录以使用新的地址空间
2.切换内核栈和硬件上下文
对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。
切换的性能消耗:
1、线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
2、另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

纤程

  1. 是用户态的线程,是线程中的线程,切换和调度不需要经过操作系统
    目前支持内置纤程的语言Kotlin Scala Go 等

线程同步进制

进程中线程同步的四种常用方式:

  1. 互斥量: 采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
  2. 信号量: 它允许同一时刻多个线程来访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
  3. 事件(信号):通过通知操作的方式来保持多线程同步,还可以方便实现多线程优先级的比较。
  4. 临界区:临界区对象和互斥对象非常相似,只是互斥量允许在进程间使用,而临界区只限制与同一进程的各个线程之间使用,但是更节省资源,更有效率。
    临界区: 当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止。

死锁产生的四个条件

  1. 互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
  2. 不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
  3. 请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
  4. 循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所深情地资源。

有了互斥锁,为什么还要条件锁

条件锁反复尝试上锁,但无需进行内核切换

select、poll、epoll

计算机网络

TCP 三次握手 四次挥手

在这里插入图片描述

三次握手

  1. 建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)
  2. 服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  3. 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

在这里插入图片描述

四次挥手

  1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
  2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
  3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
  4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
  5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
  6. 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

为什么连接的时候是三次握手,关闭的时候却是四次握手

因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态

虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

如果已经建立了连接,但是客户端突然出现故障了怎么办

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

数据结构

中间件

MQ

工作模式

  1. 简单模式:消息产生者产生消息,消息的消费者进行消费
    在这里插入图片描述
  1. 工作模式:消息消费产生消息,将消息发送到消息队列中,这是竞争,消费者1和消费者2都监听消息队列,一起来抢消息。谁抢到谁处理。
    在这里插入图片描述
  1. 消息发布和订阅:消息产生者产生消息,将消息发送到交换机中。多个消息队列绑定到交换机上。交换机将消息发送到多个队列中。消费者1监听自己的队列,如果有消息就进行消费。消费者2监听自己的队列,如果有消息进行消费。
    在这里插入图片描述
  1. 路由模式:比发布订阅模式多了一个路由选择,称为路由key。路由key指定一个名称。队列在绑定到交换机时,还要设置这个路由key。消息的队列中不是所有的消息了,交换机会根据消息的路由key,选择性将消息传递给消息队列。
    在这里插入图片描述
  1. 主题模式:在路由模式基础上,让路由key可以使用通配符。相当于进行分类。灵活程度更高些。隐患:容易误伤。
    在这里插入图片描述

Kafka

Kafka的架构

kafka 高性能的原因

  1. 顺序写入:顺序写入与随机写入速度相差高达6000倍
  2. 批量处理:使用消息累加器仅多个消息批量发送,既节省带宽有提高了发送速度
  3. 消息压缩:kafka支持队消息压缩,支持格式有:gzip、snapply、lz4,可以使用compression.type配置
  4. 页缓存:在消息发送后,并没有等到消息写入磁盘后才返回,而是到page Cache中就返回。page Cache与文件系统的写入由操作系统自动完成
  5. 零拷贝(zero-copy):Kafka两个重要过程都使用了零拷贝技术,且都是操作系统层面的狭义零拷贝,一是Producer生产的数据存到broker,二是 Consumer从broker读取数据

ack机制

  1. ack=0, producer根本不管写入broker的消息到底成功没有,发送一条消息出去,立马就可以发送下一条消息,这是吞吐量最高的方式
  2. 只要leader写入成功,就认为消息成功了,默认给这个其实就比较合适的,还是可能会导致数据丢失的
  3. ack=1,这个leader写入成功以后,必须等待其他ISR中的副本都写入成功,才可以返回响应说这条消息写入成功了,此时你会收到一个回调通知

redis

redis多并发竞争key

redis性能好的原因

  1. 放在内存里
  2. 单线程,省掉了线程切换的消耗
  3. 数据结构高效,对数据压缩,跳表
  4. I/O多路复用 reactor模式
    zk 实现
    zk 持久节点,持久节点顺序节点,临时节点,临时顺序节点
    临时顺序节点 链表结构 parentLock -> node1->node2 node1执行完毕自动注销掉

redis 定期删除

  1. 定期删除
  2. 惰性删除

redis内存的淘汰机制

  1. 存活key
    1.1 移除最近最少使用的key,这个是最常用的
    1.2 随机移除key
    1.3 移除最不经常(最少)使用的key
  2. 过期key中
    2.1 移除最近最少使用的key
    2.2 随机移除某个key
    2.3 移除最使用的key
    2.4 优先移除过期时间最早的key

数据库

三范式

三范式

  1. 列不可拆分/1NF
  2. 属性完全依赖主键/2NF
  3. 属性直接依赖主键,不存在依赖传递/3NF

Mysql

Mysql 怎么实现可重复读

  1. 通过MVCC实现,undo log 事务ID

Mysql 怎么实现事务的

  1. 事务的原子性是通过 undo log 来实现的 逻辑日志 根据sql生成逆向回滚sql
  2. 事务的持久性性是通过 redo log 来实现的 物理日志
  3. 事务的隔离性是通过 (读写锁+MVCC)来实现的
    mvcc 通过undo log +快照读+回滚指针实现
  4. 事务的一致性是通过原子性,持久性,隔离性三者组合来实现的

mysql日志种类

  1. 重做日志(redo log
    确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。
    内容:
    物理格式的日志,记录的是物理数据页面的修改的信息,其redo log是顺序写入redo log file的物理文件中去的。

  2. 回滚日志(undo log)
    保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读
    逻辑格式的日志,在执行undo的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于redo log的。

  3. 二进制日志(binlog)
    3.1 用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。
    3.2 用于数据库的基于时间点的还原
    逻辑格式的日志,可以简单认为就是执行过的事务中的sql语句。

  4. 错误日志(errorlog)

  5. 慢查询日志(slow query log)

  6. 一般查询日志(general log)

  7. 中继日志(relay log)

MySQL中binlog和redo log的一致性问题

Mysql-索引分类

  1. FULLTEXT即为全文索引,目前只有MyISAM引擎支持。其可以在CREATE TABLE ,ALTER TABLE ,CREATE INDEX 使用,不过目前只有 CHAR、VARCHAR ,TEXT 列上可以创建全文索引。
    全文索引并不是和MyISAM一起诞生的,它的出现是为了解决WHERE name LIKE “%word%”这类针对文本的模糊查询效率较低的问题。
  2. HASH
    由于HASH的唯一及类似键值对的形式,很适合作为索引。
    HASH索引可以一次定位,不需要像树形索引那样逐层查找,因此具有极高的效率。但是,这种高效是有条件的,即只在“=”和“in”条件下高效,对于范围查询、排序及组合索引仍然效率不高。
  3. BTREE
    BTREE索引就是一种将索引值按一定的算法,存入一个树形的数据结构中(二叉树),每次查询都是从树的入口root开始,依次遍历node,获取leaf。这是MySQL里默认和最常用的索引类型。
  4. RTREE
    RTREE在MySQL很少使用,仅支持geometry数据类型,支持该类型的存储引擎只有MyISAM、BDb、InnoDb、NDb、Archive几种。

mysql-解决幻读

  1. 如果是快照读mvcc
    InnoDB为每行记录添加了一个版本号(系统版本号),每当修改数据时,版本号加一。
    在读取事务开始时,系统会给事务一个当前版本号,事务会读取版本号<=当前版本号的数据,这时就算另一个事务插入一个数据,并立马提交,新插入这条数据的版本号会比读取事务的版本号高,因此读取事务读的数据还是不会变。
  2. 如果是当前读 next-key lock
    如果是当前读,那么是通过Gap Lock锁来避免幻读

Linux

linux下查看进程下的线程信息

linux权限命令

在这里插入图片描述

  1. ps -mp pid -o THREAD,tid,time
  2. printf “%x\n” tid 把十进制的tid转16进制的id
  3. jstack pid |grep tid -A 1000