一、java基础
1、java有哪几种数据类型?
- 基本数据类型:byte(1),char(2),short(2),int(4),long(8),double(8),float(4),boolean(1)
- 引用数据类型:各种类和接口,枚举,数组
2、 面向对象和面向过程的区别?
面向对象和面向过程都是一种开发思想。
- 面向过程就是根据解决问题所需要的步骤,具体化的一步一步的去实现。
- 面向对象就是把数据及对数据的操作方法放在一起,作为一个整体,也就是对象,若干个这样的整体组成一个系统去解决实际问题。
- 面向过程只用函数实现,性能比较快因为不需要进行实例化,但是不容易扩展、维护,复用
- 面向对象通过类去实现功能模块,代码安全性高,容易扩展和复用,比较灵活且便于维护
3、JDK/JRE/JVM三者的关系
JVM指的是Java的虚拟机,Java程序运行需要在JVM虚拟机上,不同平台都有自己的JVM虚拟机,所以说java语言实现了跨平台
JRE指的是Java的运行时环境,包含了java核心内库和JVM虚拟机
JDK就是 Java 开发工具包,里面包含了工java运行时需要的工具类和运行时环境,比如javac.exe和其他的一系列编译调试打包工具
4、什么是字节码文件
我们使用java编译命令就能将java源文件编译对应成字节码文件(.class),字节码文件是一种八位数据的二进制流文件,可以被JVM快速加载到内存中运行
5、面向对象有哪些特性?
面向对象有三大特性:封装,继承和多态
- 封装就是将类信息隐藏在类内部,不允许外部程序直接访问,而是通过调用类的方法去访问,良好的封装能减少耦合,增加安全性。
- 继承就是从已有的类派生出新的类,新的类继承父类的属性和方法,并扩展新的能力,极大提高了代码的复用行和易维护性。
- 多态是指同一个行为具有多种形式的表现,具体体现在继承,重写,重载,父类引用指向子类对象等,多态体现出面向对象的灵活性。
- 另外面向对象还有抽象的特性,指把客观事物用代码抽象出来。
6、StringBuilder和StringBuffer的区别
它们都继承于abstractStringBuilder,相对于String来说都是可变,StringBuffer添加了syncronized所以是线程安全的,而StringBuilder是线程不安全的
线程安全指,多个线程访问一个对象时,每个线程调用该对象的行为都能获得正确的结果
7、为什么静态方法不能调用非静态方法和变量?
类在加载的时候,会将所有被static修饰的内容(包括方法和属性)初始化,但此时非静态方法和变量还没有经过初始化,也就是没有内存空间,调用的时候就会出错。
8、异常,error和exception的区别,运行时异常和非运行时异常(checked)的区别?
JAVA异常的层次结构如图:
Throwable类是java所有异常和错误的顶类,有两个重要的子类
Error:处理JVM的内部严重问题,如资源不足,无法恢复
Exception: 可以恢复的异常,分为RuntimeException(unchecked Exception)和其他Exception(checked Exception)
RuntimeException:运行时异常,处理或者不处理都可以(try/catch throw操作)
其他Exception:必须捕获或者声明抛出的异常
9、java内存模型JMM
JMM是一个抽象的规范,它定义了共享内存中多线程程序读写规范,即变量如何存储在内存中又如何从内存中读取(这里的变量指实例字段,静态变量,构成数组的元素)
JMM将内存分为主内存和工作内存
- 主内存是线程共享的公共区域,里面存储了共享变量
- 工作内存是线程私有的工作区域,里面存储了共享变量的副本
- 线程通常是对工作内存中的共享变量进行修改,然后同步到主内存,然后其他线程从主内存中同步更新自己的变量,也即,线程之间的交互是通过主内存的。
JMM对Java编程提供了原子性,可见性,有序性的保障
原子性:原子性就是说一个操作是不可分割的,不可中断的,要么执行完成,要么不执行,在JMM中通过lock操作实现,它保证了一个变量在同一时刻只能被一个线程访问,不会被其他线程影响而中断操作,对应关键字Syncronized
可见性:可见性指线程对一个变量的操作对其他线程来说是立即可见的,在JMM中通过在修改变量后变量同步到主内存,其他线程读取时刷新变量值来实现,对应关键字volatile,Syncronized
volatile
关键字!作用:它可以用来修饰成员变量和静态变量(放在主存线程共享的变量),它可以避免线程从自己的工作内存中查找读取变量的值,而是每次都从主内存中去读取,线程操作
volatile
修饰的变量都是直接操作主存的。
有序性:指本线程的所有操作都是有序的
在多线程中会有指令重排序的问题, 通过volatile关键字解决
volatile
的底层原理是内存屏障,Memory Barrier(Memory Fence)
写屏障:对 volatile 变量的写指令后会加入写屏障,
写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障:对 volatile 变量的读指令前会加入读屏障
- 读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
10、常见的集合
Java中的集合主要由Collection(单列集合)和Map(双列集合)这两个接口派生而来
Collection下有子接口List,Queue,Set(无序)
List接口的实现类有ArrayList,vector(线程安全),stack,LinkedList(双向链表)
Set接口的实现类有hashset(底层是hashMap),sortedSet(接口)、treeSet(底层是treeMap,遍历有序),LinkedHashSet
线程安全的集合:Vector、HashTable、ConcurrentHashMap、Stack(继承Vector)、ArrayBlockingQueue、ConcurrentLinkedQueue
11、重写与重载的区别
1.定义不同---重载是定义相同的方法名,参数不同;重写是子类重写父类的方法。
2.范围不同---重载是在一个类中,重写是子类与父类之间的。
3.多态不同---重载是编译时的多态性,重写是运行时的多态性。
4.返回不同---重载对返回类型没有要求,而重写要求返回类型只能与父类一致或者是父类方法返回类型的子类。
5.参数不同---重载的参数个数、参数类型、参数顺序可以不同,而重写要求参数列表一致
6.修饰不同---重载对访问修饰没有特殊要求,重写访问修饰符的访问权限一定要大于被重写方法的访问修饰符。
12、HashMap底层,插入,扩容
前置知识:
二叉树:每个节点至多只有两个子节点的树
搜索二叉树:满足当前节点的左子树上的节点不能大于当前节点,右子树上的节点不能小于当前节点的二叉树
红黑树:一种自平衡的搜索二叉树,能保证遍历,插入,删除的时间复杂度永远是O(logn)
红黑树规则:
红黑:节点只有红黑两种颜色,根节点和叶节点一定是黑色,红节点的子节点一定是黑色
树:叶节点一定是null,任一节点到叶节点的所有路径包含相同数量的黑节点
散列表:又称Hash表,是一种kv存储结构,存储时先计算k经过哈希函数运算后的值,然后根据这个值将v存放在对应的位置
HashMap底层实现是数组+链表/红黑树
添加数据时,根据key的hash值来确定value所在的数组下标
如果数组下标对应的链表/红黑树中有相同的key,就会替换value,否则加入到链表/红黑树中
jdk1.8之前采用的拉链法 数组+链表
jdk1.8之后采用的数组+链表+红黑树法,链表长度大于8时,先尝试扩容数组,如果数组长度此时大于64则会从链表转化为红黑树。
插入操作(简化):
1、判断数组是否为空,如果为空调用resize方法,初始化一个容量16的数组
2、对key进行hash运算,得到数组中的索引,table下标i=(table.length- 1) & ((h = key.hashCode()) ^ (h >>> 16)),如果索引指向的位置为空,则直接插入一个新的node
3、如果不为空,则判断当前key是否存在,如果存在直接进行替换value操作
4、如果不存在,判断当前Node节点是否是红黑树结构,如果是树类型,则按照树的方式去追加节点,否则在链表尾部插入数据
5、最后判断链表长度是否大于8,如果大于8,会先尝试扩容,判断数组长度是否小于64,是就扩容,否则链表转换为红黑树。
6、插入操作结束后,判断如果hashmap的元素个数超过了负载因子*容量的阈值,则需要进行扩容操作
7、扩容操作会将原来数组的元素分配到新的数组中,重新每个元素在数组中的下标
8、扩容操作完成后,再将元素插入到新的数组中
扩容操作:
1、在添加元素或初始化时扩容,执行resize方法,第一次初始化长度为16,以后每次在元素达到容量*负载因子(0.75)时,进行扩容
2、每次扩容操作后,容量都是之前的2倍
3、扩容之后,会创建一个新的数组,然后把老的数组中的元素移动到新的数组中
4、如果是链表,则计算每个链表元素的下标,然后移动到新的数组
5、如果是红黑树,则走红黑树的添加。
13、对象的创建方式
1、new
2、将类继承Cloneable接口,然后实现clone方法,调用clone方法即可复制对象
3、反射(类派发),Student.class.newInstance()
4、反射(动态加载),Class.forName("entity.Student").newInstance()
5、反射(构造方法),Student.class.getConstructor().newInstance()
6、Spring容器启动时将Bean注入
14、四种访问修饰符
1)public :公共权限,可以被任意类访问,不同包不同类依然可以访问,
可修饰:类、成员变量、方法,内部类
2)protected:受保护的权限,可以被同包类访问,如果不是同包类,必须是该类的子类才可以访问
可修饰:成员变量、方法、内部类
3)default:默认的(无),同包权限,只能被同包的类访问
可修饰:类、成员变量、方法,内部类
4)private:私有权限,只能在本类中访问,同包其他类也不能访问
二、多线程
1、进程与线程的区别
概念:
-
进程是程序的一次执行结果,是系统运行的基本单位,每个进程都有自己的内存空间和系统资源,进程直接相互独立,但线程之间的运行是可以相互影响的。
-
线程是比进程更小的单位,一个进程中包含的多个线程,这些多个线程共享进程的内存空间和系统资源,所以线程之间的切换开销比较小。//线程运行在用户态,但是线程切换由操作系统内核完成,需要切换到核心态
-
协程是一种轻量级的线程,协程在用户态就可以控制,协程的上下文切换更加节省资源。
上下文的切换指的是CPU从当前正在运行的进程或线程中保存状态,然后切换到另一个线程或进程
区别:
-
进程是正在运行的程序的实例,线程是操作系统运算调度的最小单位,包含在进程当中,一个进程可以有多个线程
-
进程有自己的内存空间和系统资源,线程之间共享一个进程的内存空间和系统资源
-
进程之间相互独立,而线程之间是可以相互影响的
-
线程更加轻量,上下文切换开销比进程低
2、并行与并发的区别
并发是同一时间处理多件事的能力,比如多个线程轮流使用一个CPU
并行是同一时间做多件事的能力,比如4核CPU同时执行4个线程
关键区别在于是否同时执行
3、创建线程的方式有哪几种?Runnable与Callable有什么区别?run方法与start方法有什么区别
-
继承Tread类 ——直接调用start执行线程
-
实现Runnable接口——先创建MyRunnable对象,再创建Thread对象(将MyRunnable作为构造器参数),再执行start方法
-
实现Callable接口——先创建MyCallable对象,再创建futureTask对象,再创建Tread对象,再执行start方法
-
线程池创建(项目中一般使用方式)——对象要实现Runnable,然后创建线程池对象,调用线程池对象的submit方法
Runnable与Callable的区别:
-
Runnable接口的run方法没有返回值,而Callable接口的call方法有返回值,可以配合FutureTask获取返回结果
-
Runnable接口的run方法的异常只能在内部消化,而Callable接口的call方法可以将异常抛出
run方法与start方法区别:
-
start方法是开启一个线程,然后线程去调用run方法,start方法只能被调用一次
-
run方法封装了线程要执行的方法,可以调用多次
4、线程包含了几种状态,状态之间是如何转换的
线程包含了六种状态:
-
新建(New):创建线程对象时
-
可执行(Runnable):调用start方法后,有执行资格
-
阻塞(Blocked):进入可执行状态后,无法获得锁
-
等待(Waiting):执行时调用Wait方法进入(wait方法会释放锁)
-
计时等待(Timed_Waiting):执行时调用Time.sleep进入
-
死亡(Terminated):运行结束,线程死亡,变成垃圾
5、假设有t1,t2,t3三个线程,如何保证他们按顺序执行?
第一种方法:调用join方法:它使当前线程进入time_waiting状态,直到调用join方法的线程结束,比如t1.join(),就是在t1结束后才会继续运行。
具体:在t2线程的run方法中调用t1.join,在t3线程的run方法中调用t2.join
第二种方法:使用计数器countdownLatch:它需要先设定一个起始值,然后在线程中调用countdown方法让值-1,当值为0时,会解除wait状态
具体:
countDownLatch1.await(); //第一个计数器值为0时结束等待 System.out.println("拿出一瓶牛奶!"); //执行线程方法 countDownLatch2.countDown(); //让控制另一个线程的计数器值为0
6、wait和sleep方法有哪些不同?
wait方法是Object类方法,每个对象都有,而sleep方法是Tread类静态方法
wait方法需要在同步块(synchronized)中执行,并且需要先获取锁,而sleep方法没有这个限制
wait方法执行后会释放锁,而sleep方法在拿到锁的情况下执行不会释放锁
7、为什么wait方法和notify方法要在synchronized关键字中使用?
从程序层面来说:不这样做会报IllegalMonitorStateException错误
从线程安全层面来说:如果不加同步锁,wait() 方法还没有执行完,notify() / notifyAll() 方法已经执行完,这样 notify() / notifyAll() 就进行了一次空唤醒操作,而 wait() 执行完后由于再没有notify() / notifyAll()的唤醒,会导致wait() 所在线程一直阻塞。
从底层代码层面来说:
首先,每个Java对象底层都会关联一个 Monitor 对象,在 Monitor 中维护了 两个队列 WaitSet 和 EntryList ,owner 属性。
synchronized( lock ) 被调用后会将当前线程 赋值给lock对象所关联的 monitor 对象的 owner 属性,其他线程 再想获取 lock 锁对象的话如果发现 lock对象所关联的 monitor 对象的 owner 属性不为空,就会进入 EntryList 进行阻塞,而调用了wait() 后就会将 owner 指向的线程对象放入 WaitSet 中进行等待,并将 owner 置为 null (释放掉锁),直到其他线程获取到 lock 这个对象锁以后通过 notify() / notifyAll() 方法 唤醒 WaitSet 中 的线程,这时 WaitSet中等待的线程才会进入 EntryList 参与lock 锁的竞争。( notify() / notifyAll() 并不会释放锁,只有等待 synchronized 执行完才会释放锁 )
8、Synchronized关键字底层原理
synchronized是采用互斥的方式,让同一时刻只能有一个线程能够获取对象锁
java中的Synchronized有偏向锁,轻量级锁,重量级锁三种形式,分别对应了只被一个线程所有,被多个线程交替持有,被多个线程竞争三种情况
在对象锁被多个线程竞争的情况下(重量级锁):
-
底层由Monitor实现,Monitor是JVM级别的对象,Monitor分为三个部分,owner,entryset,waitset
-
当一个线程要获取对象锁时,会先将对象与Monitor关联,然后将owner与当前线程绑定
-
如果owner已经被其他线程绑定了,当前线程就会加入到entryset中进入阻塞状态
-
另外,当对象调用wait方法时,当前线程会进入到waitset队列中进入阻塞状态
在对象锁被多个进程无竞争交替持有的情况下(轻量级锁):
这个时候不需要monitor
当线程尝试获取锁时,会创建一个栈帧,栈帧里包含一个锁记录的结构,主要内容有锁记录地址和对象指针
执行到加锁时,让锁记录中的对象指针指向对象,并尝试使用cas替换锁记录地址和对象头中的mark word,如果标记位为无锁则进行交换,如果非无锁,则有两种情况
1、其他线程已经获取了这个对象的锁,当前线程获取锁失败,线程会尝试自旋获取锁,如果自旋次数超过一定阈值,或者存在多个线程在等待同一个锁,轻量级锁会升级为重量级锁
2、当前线程已经获取了这个对象的锁(锁重入),也就是对象头的mark word指向当前线程的栈帧,此时会在栈帧中再添加一条锁记录,并设置锁记录的锁地址为null,最后在退出synchronized代码块时,如果有锁记录地址为null的锁记录,会清除锁记录,表示重入次数-1
当对象锁被一个线程持有的情况下(偏向锁):
在上文中可知,轻量级锁在没有竞争时(还是这个线程来获取锁),将会发生锁重入,要执行CAS操作,并在栈桢创建新的锁记录。这样耗费了资源。
优化方式:只有第一次使用CAS将线程ID设置到被锁对象的MarkWord中,以后再来的线程只用验证这个线程ID是自己则没有竞争,无需CAS。
加锁过程:在当前线程的栈帧中创建一个锁记录对象,然后将锁记录的Lock record地址与对象的mark word进行CAS操作,
9、synchronized与lock有什么区别?
从语法层面来说:synchronized关键字是在JVM中实现的,通过c++实现
从功能层面来说:
-
两者都属于悲观锁,都具备基本的互斥,同步,锁重入功能,
-
而lock支持更多synchronized不支持的功能:如可中断等待(lock.lockInterruptibly()),公平锁,可设置等待超时(tryLcok(time,unit)),可选择性通知(condition)
-
lock还提供了多种不同场景的实现,如ReentrantReadWriteLcok(读写锁)
从性能层面来说:synchronized提供了偏向锁,轻量级锁,在竞争不激烈的情况下性能很好,而lock在竞争激烈的时候,通常会有更好的表现,因为提供更多的功能。
10、CAS你知道吗?
CAS(compireAndSwap)比较并交换,它体现了乐观锁的一种思想,在无锁的情况下保证线程操作数据的原子性,它的内部存在3个操作数
1、变量内存值V
2、旧的预期值A
3、准备设置的新值B
当执行CAS指令时,只有当V=A时,才会去执行B更新V的值,否则不会更新
当多个线程同时使用CAS去操作一个变量时,只有一个线程会执行成功,其他线程均会失败,然后会重新尝试或将线程挂起(阻塞)
另外,CAS是一种系统原语,它的执行一定是连续不被中断的,也就不存在并发问题,这样就保证了原子性
CAS虽然能很高效的解决原子操作,但是仍然存在问题
-
ABA问题 因为CAS只是判断获取值和在操作时这个值之间的时间该没改变来进行操作,当在这个时间内如果有一个操作修改了这个内存变量的值,由A改为B再改为A,这时CAS会认为这个值从来没有变过,但是值其实已经发生了一次改变
-
循环时间长时开销大 因为底层是自旋锁,当操作迟迟无法完成的时候,会对CPU带来非常大的开销
-
只能保证一个共享变量的原子操作 当对多个共享变量进行原子操作时,循环CAS就无法保证操作的原子性
11、什么是AQS?
AQS全称abstractQueueSynchronizer,即抽象队列同步器,是一种锁机制,它是作为一个基础框架使用的,像Reentrantlock,countdownlatch都是基于AQS实现的
AQS内部维护了一个先进先出的双向队列,队列中存储了排队的线程
AQS还维护了一个state,表示锁的状态,0为无锁状态,1为有锁状态,如果一个线程将state修改为1,就相当于当前线程获得了资源
对state的修改使用cas操作,保证多线程下的原子性
12、ReentrantLock底层原理是什么
ReentrantLcok意为可重入锁,和synchronized一样,当一个线程已经获得锁,再去尝试获得锁时,不会阻塞而是直接获得
ReentrantLock的底层是基于CAS+AQS实现,AQS的底层维护了一个state和一个双向队列,当有线程来抢锁时,会使用cas的方法来对state进行修改,如果修改成功,就将ReentrantLock中的ownerTread属性指向当前线程,如果修改失败,就会插入到双向队列的队尾
ReentrantLock支持公平锁和非公平锁两种实现,如果使用无参构造器,就是非公平锁,也可以传参true设置为公平锁。
13、悲观锁、乐观锁和分布式锁的实现和细节
悲观锁:认为线程安全问题一定会发生,所以在操作数据之前先获取锁,保证线程串行执行,例如synchronized,lock
细节:
悲观锁适合插入数据
锁的粒度要尽量小,只锁住需要串行执行的代码
配合事务使用时,要先提交事务再释放锁
乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在操作数据前判断是否有其他线程对数据做了修改,如果没有被修改则说明线程安全,更新数据,反正说明出现了线程安全问题,可以重试或者返回异常,例如给表加字段version,cas操作
细节:
乐观锁适合更新数据
更新前要先查询version,更新时比较version是否相同
分布式锁:满足分布式系统或集群模式下多进程可见且互斥的锁,常见的实现有redis和zookeeper,redis通常利用setnx方法
细节:
锁的误删,比如线程1拿到锁但是出现了阻塞导致锁自动释放,在线程2拿到锁后执行业务逻辑时,线程1反应过来,继续执行,最后将本已经不属于他的锁误删了
锁的误删解决:设置锁的唯一标识,每个线程在获取锁时,设置锁的value为线程唯一标识(可以用uuid实现),释放锁时判断锁的value是否跟自身线程唯一标识一致,一致才能释放
14、什么是死锁,死锁产生的条件?如何避免死锁?
死锁就是一组互相竞争资源的线程,因为互相等待又互不相让资源,导致永久阻塞无法进行下去的情况
死锁产生的条件有四个:
-
互斥条件:资源x和y只能分别被一个线程占用
-
占有且等待:线程t1占有资源x后,等待资源y被释放,同时自己不释放资源x
-
不可抢占:其他线程不能强行抢占线程t1占有的资源
-
循环等待:线程t1等待线程t2占有的资源,线程t2等待线程t1占有的资源
避免死锁的方法(破坏对应的条件):
-
一次性申请所有资源(破坏互斥条件)
-
占有资源的线程,在申请其他资源时,如果申请失败,可以主动释放自己的资源(破坏占有且等待条件)
-
按照顺序去申请资源,然后反序释放资源,破坏循环等待条件
15、ConcurrentHashMap底层原理
ConcurrentHashMap是在HashMap的数据结构上,增加了CAS操作和Synchronized互斥锁来保证线程安全,并且使用volatile关键字修饰了node中的next和val字段来保证多线程环境下某个线程新增或修改节点对于其他线程是立即可见的。
在进行添加操作时:
-
计算hash值,定位该元素应该添加到的位置
-
如果不存在hash冲突,即该位置为null,则使用CAS操作进行添加
-
如果存在hash冲突,即该位置不为null,则使用synchronized关键字锁住该位置的头节点,然后进行添加操作
16、线程池的核心参数和执行原理?
线程池的核心参数有七个:
corePoolSize:核心线程数
maximumPoolSize:最大线程数量,核心线程+救急线程的最大数量
keepAliveTime:救急线程的存活时间,存活时间内没有新任务,该线程资源会释放
unit:救济线程的存活时间的单位
workQueue:工作队列,当没有空闲核心线程时,新来的任务会在此队列排队,当该队列已满时,会创建应急线程来处理该队列的任务
treadFactory:线程工厂,可以定制线程的创建,线程名称,是否是守护线程等
handler:拒绝策略,在线程数量达到最大线程数量时,实行拒绝策略 拒绝策略:
线程池执行原理:
17、线程池常见的阻塞队列?
常见的阻塞队列有:
ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO
LinkedBlockingQueue:基于链表的有界阻塞队列,FIFO
两者区别(链表与数组的区别)
ArrayBlockingQueue | LinkedBlockingQueue |
底层是数组 | 底层是链表 |
强制有界 | 默认无界,传递容量参数后有界 |
初始化时即创建好node对象数组 | 插入时才创建node对象 |
两把锁(头尾) | 一把锁 |
LinkedBlockingQueue的优点是锁分离,很适合生产和消费频率差不多的场景,这样生产和消费互不干涉的执行,能达到不错的效率
18、线程池核心线程数如何确定?
没有固定答案,先设定预期,比如我期望的CPU利用率在多少,负载在多少,GC频率多少之类的指标后,再通过测试不断的调整到一个合理的线程数
公式:
分三种情况
高并发,任务处理时间短 ——》CPU核数+1,减少线程上下文切换
并发不高,任务处理时间长 IO密集型任务——》CPU核数*2+1
读写多,DB操作多,CPU占用少,并且IO数据传输时,是不占用CPU的,所以就可以多释放CPU资源,给其他线程运行,
CPU密集型任务——》CPU核数+1
CPU占用高,如计算任务,视频解码任务,这些任务线程上下文切换开销大,所以要尽量减小开销,提高CPU效率
19、线程池的种类有哪些
在JUC的Executor类中,提供了多种创建线程池的方法,主要有四种
1、固定线程数的线程池
核心线程数与最大线程数一样,没有救急线程
阻塞队列是LinkedBlockingQueue,最大容量是Integer.MaxValue
适合任务量已知,相对耗时的任务
2、单例线程的线程池
核心线程数和最大线程数都是1,没有救急线程
阻塞队列是LinkedBlockingQueue,最大容量是Integer.MaxValue
适合按顺序执行的任务,与单线程的区别是,单线程运行完了就会销毁,而线程池创建的线程运行结束不会销毁,而是等待下一个任务,可以重复使用,减少了创建线程和销毁线程的时间,提高资源利用率。
3、可缓存的线程池
核心线程数为0,最大线程数为Integer.MaxValue,救急线程存活时间为1分钟
阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每一个插入操作必须等待一个移除操作
适合任务比较密集,且任务执行时间短的情况,因为使用的是救急线程,在一定时间没有新任务后就会销毁,节省资源,同时能应付任务密集的时间段。
4、可以定时执行的线程池
核心线程数自定义,最大线程数为Integer.MaxValue
适合需要定时执行任务的场景
20、线程池使用场景
1、批量将pgsql数据导入ES
计算数据总条数——》固定每页200条,计算总页数N——》将页数N设置为CountdownLatch的count——》创建N个线程批量导入,每次导入完调用countdown方法——》主线程中调用await方法,线程都执行完后,主线程结束
2、异步调用
用户搜索需要保存搜索记录,但保存功能不能影响正常搜索——》搜索时通过线程异步调用保存记录功能
关键注释:@EnableAsync加在SpringBoot启动类上,@Bean将自定义线程池注入到容器中(核心8,最大8), @Async("线程池名称")加在需要异步调用的方法上,
21、对ThreadLocal的理解,ThreadLocal内存泄漏问题
ThreadLocal是java.lang包中的一个类,它实现了线程之间的资源隔离,让每个线程都有自己的独立资源
ThreadLocal的底层实现的关键是它的静态内部类ThreadLocalMap
-
每个Thread对象都有一个成员变量threadlocals,它指向一个ThreadLocalMap,该Map以ThreadLocal为key,需要存储的线程变量为value
-
当调用ThreadLocal的set()方法时,会先拿到当前线程对象的ThreadLocalMap,然后以当前ThreadLocal为key将变量存储到map中,调用get方法和remove方法时也是通过这个key去操作。
ThreadLocal内存泄漏问题:
回答这个问题,我们首先要知道java中的四种引用
强引用:强引用是最常见的,只要把一个对象赋值给一个引用变量,那么这个对象就被强引用了,强引用的对象只要不为null,就不会被回收
软引用:软引用相对弱化一些,需要用softReference对象构造方法去创建软引用,当内存充足时,软引用的对象不会被回收,当不足时就会被回收
弱引用:弱引用又更弱了一些,需要用weakReference的构造方法创建弱引用,当发送GC时,只要是弱引用的对象就会被回收
虚引用:虚引用要配合引用队列使用,它的主要作用是跟踪对象垃圾回收的状态,当对象被回收时,通过引用队列做一些通知类工作
在ThreadLocalMap中,Key ThreadLocal是一个弱引用,但是值value是一个强引用,当垃圾回收时,弱引用ThreadLocal会被回收,而value不会,这就导致value成了一个无法被访问也无法回收的变量,造成内存泄漏
解决办法:当使用完ThreadLocal变量后,及时使用remove方法进行清除。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
三、JVM虚拟机
1、JVM的主要组成部分及其作用
类加载器:将java字节码文件加载到内存中
运行时数据区:就是我们常说的JVM的内存,我们的所有程序都被加载到这运行
执行引擎:负责将字节码翻译成底层系统指令,然后交由系统执行
本地方法接口:与本地库交互实现一些基础功能
2、运行时数据区的组成部分及其作用
JVM运行时数据区由方法区(元空间),堆,程序计数器,虚拟机栈,本地方法栈组成
-
方法区/元空间:方法区是一个线程共享的区域,里面存储了类信息,常量,静态变量等待信息,虚拟机启动时创建,虚拟机关闭时释放,内存无法满足分配请求时,会报OOMError:Metaspace
-
堆:Java堆是一个线程共享的区域,里面存储了实例对象、数组等等,内存不够时抛出OOM异常
-
堆分为年轻代和老年代
-
年轻代被分为三块,eden区和两个严格相同的Survivor区
-
老年代主要用来保存生命周期长的对象,主要是老的对象
-
-
-
程序计数器:用来记录当前线程正在执行的字节码指令地址,是线程私有的
-
虚拟机栈:虚拟机栈是java方法执行时的内存结构,线程私有,虚拟机会在每个java方法执行时开启一个栈帧,用于存储方法参数,局部变量,返回地址,操作数栈(中间计算结果,比如i+j的值)等信息,当方法执行完毕时,该方法会从虚拟机栈中出栈。
-
本地方法栈:本地方法栈是线程私有的,为虚拟机使用的native方法提高服务
如果栈的深度超过了虚拟机允许的最大深度,就会抛出StackOverflowError异常; 如果在扩展栈时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
3、垃圾回收是否涉及栈?
垃圾回收主要指的是堆内存,栈帧内存在栈帧出栈后就释放
4、栈内存越大越好吗?
每个虚拟机栈内存默认为1M
不一定,机器内存是固定的,栈内存越大,可以同时活动的栈帧就越少,效率反而降低
5、方法内部的局部变量是否线程安全?
只要局部变量没有离开方法的作用范围,就是线程安全的
如果局部变量引用了一个对象,并且逃离了方法的作用范围,就有可能线程不安全
6、什么情况下会栈溢出?
栈帧过多:比如太多层级的递归调用
栈帧过大:出现少
7、堆栈的区别是什么?
1、存放内容
堆中存放的是对象实例和数组,该区域更关注的是数据的存储,
(静态变量放在方法区,静态对象仍然放在堆中)
栈中存放的是局部变量,栈帧,操作数栈,返回结果等。该区更关注的是程序方法的执行。
然而实际上,对象并不总是在堆中进行分配的,这里就需要介绍一下JVM的逃逸分析技术了。JVM会通过逃逸分析技术,对于逃不出方法的对象,会让其在栈空间上进行分配。
2、程序的可见度
堆是线程共有的,栈是线程私有的。
3、异常错误
如果栈内存没有可用的空间存储方法调用和局部变量,JVM会抛出java.lang.StackOverFlowError。而如果是堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError
4、物理地址
堆的物理地址是不连续的,性能相对较慢,是垃圾回收区工作的区域。在GC时,会考虑物理地址不连续,而使用不同的算法,比如复制算法,标记-整理算法,标记-清除算法等。
栈中的物理地址是连续的,LIFO原则,性能较快。
5、内存分别
堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定,一般堆大小远远大于栈。
栈是固定大小的,所以在编译期就确认了。
8、运行时常量池是什么?和字符串常量池的区别
运行时常量池是方法区的一部分,它可以看做是一张表,用于存放编译器生成的各种字面量和符号引用,在类被加载时,它的常量池信息就会被放入运行时常量池,并且还会保存符号引用对应的直接引用。
而字符串常量池是存放在堆里的,在HotSpot虚拟机中,它的底层是一个c++的hashtable,它将字符串的字面量作为key,实际堆中创建的String对象的引用作为value。
当创建一个String对象时,会拿着字面量尝试在字符串常量池中获取对应String对象引用,如果是首次执行,就会在堆中创建一个String对象,并保存到字符串常量池中,然后返回。
9、什么是直接内存?
直接内存不属于JVM内存,是操作系统的内存,常见于NIO操作,用于数据缓冲区,拥有较高的读写性能,且不受JVM内存回收影响
BIO(同步阻塞IO)
发送请求后线程一直阻塞,直到数据处理完并返回
NIO(同步非阻塞IO)
通过一个线程轮询大量socket,当有socket准备就绪时通知客户端,客户端调用函数接收。
AIO(异步非阻塞IO)
-
每个请求都会绑定一个Buffer;
-
通知操作系统去完成异步的读(这个时间你就可以去做其他的事情)
-
读完之后会通知客户端来读取数据
10、对象什么时候可以被垃圾回收?如何标记垃圾?
当没有任何一个强引用指向对象时,对象就可以被垃圾回收
主流的虚拟机一般通过可达性分析算法分析一个对象是否能被回收,也有系统采用引用计数法判断
可达性分析算法:
1、算法主要思想是先确定一些肯定不能被回收的对象作为GCRoot, GCRoot对象可以是:
虚拟机栈中引用的对象
本地方法栈中Native方法引用的对象
方法区中静态属性,常量引用的对象
2、然后以GCRoot为根节点,去向下搜索,找到它们直接引用或间接引用的对象
3、在遍历完之后,如果发现有一些对象不可达,那么就认为这些对象已经没用用了,需要被回收
引用计数法:
就是为每一个对象添加一个引用计数器,用来统计指向当前对象的引用次数,一旦这个引用计数器变为0,就意味着它可以被回收了。
11、垃圾回收算法有哪些?
1、标记清除算法:标记清除算法分为两个步骤,标记-清除
标记:遍历内存空间,对需要被回收的对象打上标记,通常使用可达性分析算法
清除:再次遍历空间,将被标记的内存进行回收
缺点:
遍历两次,效率低
因为清除后没有进行整理,容易形成碎片化不连续的的内存空间
2、标记整理算法:标记整理算法分为三个步骤,标记-整理-清除
标记同上
整理:将所有存活的对象压到内存的一端,并按顺序排列
清除掉其他空间
缺点:
效率低,速度慢
3、复制算法:将内存分为等大的两块,每次只使用其中一块,当触发GC时,将存活的对象全部移到另一块内存的一端,然后将当前内存空间一次性回收,如此循环往复
缺点:内存利用率低
12、什么是分代收集算法
在java8中,内存被分为两份:新生代和老年代
新生代内存使用复制算法,老年代内存使用标记整理算法
当对象在新生代内存中经历了一定的垃圾回收后,它将被晋升到老年代。
分代收集算法重复利用了对象生命周期的特点,提高了回收效率
大对象直接进入老年代
什么是大对象呢,这个是由jvm定义的参数值决定的,但是这个参数只在Serial和ParNew垃圾收集器中生效 :-XX:PretenureSizeThreshold
当我们新分配的对象大小大于等于这个值,就会直接在老年代中分配
长期存活的对象将进入老年代
在每个对象的头信息中,都包括一个年龄计数器
对象在经过一次minor gc之后,如果仍然存活,并且能够被 survior所容纳 ,那么这个年龄计数器就会加一,当计数器的值达到了默认值大小(一般默认值为15),就会进入到老年代。
对象动态年龄判断后决定是否进入老年代
当survior区域的存活对象的总大小占用了survior区域大小的50%(可以通过参数指定),那么此时将按照这些对象的存活年龄从从到大排序,然后依次累加,当累加到对象大小超过50%,则将大于等于当前对象年龄的存活对象全部挪到老年代。
详细解释:
首先要知道Minor GC 和 Full GC的区别
普通GC(minor GC):只针对新生代区域的GC,指发生在新生代的垃圾回收动作,因为大多数Java对象存活率都不高,所以Minor GC非常频繁,一般回收速度也比较快。
全局GC(major GC or FullGC) :指发生在老年代的垃圾收集动作,出现了Major GC,经常会伴随至少一次的MinorGC(不是绝对的)。Major GC的速度一般要比Minor GC慢10倍以上。
该算法将内存分为新生代、老年代、新生代中又分伊甸园、幸存区from、幸存区to,对象创建之初,会存在于伊甸园中,当伊甸园满了之后,会触发一次Minor GC,伊甸园中还存活的对象会被转入幸存区from,当伊甸园再次触发Minor GC时,GC会扫描伊甸园和幸存区from,对这两个区进行垃圾回收,垃圾回收后,幸存区from中还存活的对象利用复制清除算法复制到幸存区to(如果有对象的年龄达到了老年代区,则复制到老年代区),复制完之后把幸存区from清除掉,之后把from区和to区交换位置(from变to,to变from,保证幸存区to为空),最后把伊甸园中存活下来的对象放入幸存区from,(如果有对象的年龄达到了老年代区,则复制到老年代区),同时把这些对象的年龄+1即可。
13、垃圾收集器有哪些?
串行收集器:GC时只会有一个线程在工作,Java应用中的线程都要STW,等待垃圾回收结束
并行收集器:GC时会有多个线程参与垃圾收集,Java应用中的线程都要STW,等待垃圾回收结束,JVM默认
CMS(Concurrent Mark Sweep)收集器:并发收集器,进行垃圾回收时不会暂停Java应用线程,STW时间短,CMS收集器采用的是标记清除算法,所以不需要移动存活对象的位置,GC可以和Java应用程序同时运行
G1(Garbage-First)收集器:基于区域划分的收集器,适用于大内存应用。
14、什么是G1垃圾收集器
G1是JDK9默认的垃圾收集器,代替了CMS收集器。它的目标是达到更高的吞吐量和更短的GC停顿时间。
G1垃圾回收器将堆内存划分为多个区域,每个区域都可以充当Eden,Survivor,old,humongous(专为大对象准备)
G1垃圾回收过程可以分为几个主要阶段:
1、初始标记:标记GCRoot直接关联的对象,需要STW
2、并发标记:对GCRoot开始对堆中对象进行并发标记,需要STW
3、重新标记:解决一些漏标错标问题,需要STW
4、混合收集:将不需要回收的eden,Survivor对象放入新的Survivor区,old对象放入新的old区,如果Survior对象达到老年年龄后也会放入新old区。然后将区域内存回收
要注意的是,混合收集不会处理所有区域,而是根据停顿时间目标去筛选出回收价值高(存活对象少)的区域。
15、什么是类加载器?类加载器有哪些?
类加载器是一个负责加载类的对象,因为JVM虚拟机只能运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中运行。
JVM中有三个内置的类加载器
BootStrapClassLoader(启动类加载器):最顶层的加载器,由C++实现,主要用来加载Java的核心类裤(JAVA_HOME/lib)
ExtensionClassLoader(扩展类加载器) :主要负责加载JAVA_HOME/lib/ext下的扩展类
AppClassLoader(应用程序类加载器) :主要负责加载ClassPath下的类,也就是开发者自己写的Java类
另外,还有自定义ClassLoader,通过继承ClassLoader实现。
16、什么是双亲委派模型?为什么类加载采用双亲委派?
当一个类加载器尝试加载一个类时,会先委托上一级加载器去加载,如果上一级还有上级,就会委托上一级的加载器的上级去加载,如果所有上级加载器都不能加载该类,则类加载器尝试自己加载类。
作用:
1、防止重复加载:防止已经在上级加载器中加载的类重复地被子加载器再次加载
2、为了安全,防止核心类库API被篡改
3、保证类的一致性
17、类装载的执行过程是怎样?
1、加载
加载过程需要完成三步
-
通过类的全限定名来获取类的二进制流
-
解析类的二进制流为方法区的类数据结构(Java类模型)
-
在堆中创建一个该类的Class对象,作为方法区中该类的数据访问入口。
2、验证
验证类是否符合JVM规范
①文件格式验证
②元数据验证
③字节码验证
④符号引用验证:检查符号引用对应的类或方法是否在常量池中存在
3、准备
该阶段为类的静态变量分配内存,并设置初始值,这里所说的初始值“通常情况”下是数据类型的零值
4、解析
将常量池的符号引用替换为直接引用
符号引用:以一组符号来描述所引用的目标,有时候JVM不知道引用的类或者方法内存地址在哪,于是就先用符号引用代替直接引用
直接引用:直接指向目标的指针
5、初始化
对类的静态变量,静态代码块进行初始化操作
//6、使用:使用类执行代码
//7、卸载:用户代码执行完毕后,销毁Class对象
18、用于JVM调优的参数有哪些?
1、设置堆空间大小
-Xms 512m 设置初始化大小,默认是物理内存的1/64
-Xmx 1g 设置最大大小,默认是物理内存的1/4
2、虚拟机栈大小
-Xss 256k 默认1M
3、年轻代eden区和survivor区大小比例
-XXSurvivorRatio 8 表示eden区:survivor区=8:2
4、年轻代晋升到老年代阈值
-XX:MaxTenuringThreshold=10 默认为15
5、设置垃圾回收器
-XX:+Use[垃圾回收器名]
19、JVM调优的工具有哪些?
命令:
jsp 进程状态信息
Jstack 线程内的堆栈信息
Jmap(使用较多) 查看堆的信息,堆的大小配置,堆内存各部分的使用情况
jstat 垃圾回收信息
工具:
jconsole:用于对JVM内存,线程,类的监控
visualVM:用于对JVM内存,线程,类的监控
20、Java内存泄漏的排查思路?
首先通过jmap命令或者设置JVM参数区获取dump文件
然后将dump文件放入VisualVM中去分析
通过查看堆的信息,定位到哪行代码出了问题,然后再去修改代码
21、CPU彪高排查方案与思路
1、使用top命令,查看哪个进程CPU使用率较高
2、使用ps H -eo pid,tid,%cpu | grep 进程id 命令查看进程中的线程状态
3、找到关键线程,然后使用jstack命令去查看进程中哪个线程出了问题,然后找到对应代码并修改
四、Mysql数据库
1、InnoDB和MyISAM的区别和使用场景
事务:InnoDB支持事务,而MyISAM不支持
外键:InnoDB支持外键,而MyISAM不支持
锁:InnoDB支持行级锁(并发量高)和表锁,MyISAM只支持表锁
索引:InnoDB不支持全文索引,MyISAM支持全文索引(全文索引是基于分词的索引,能更快速地在文本中检索一段信息内容)
使用场景:
InnoDB适合需要高并发或事务的场景,
MyISAM适合读操作远远大于写操作且不需要事务的场景
属性 | InnoDB | MyISAM |
事务 | 支持事务 | 不支持事务 |
---|---|---|
外键 | 支持外键 | 不支持外键 |
锁 | 支持行锁(并发更高)和表锁 | 只支持表锁(不会死锁) |
索引 | 不支持全文索引 | 支持全文索引(查询更快) |
适用场景 | 并发量高或者需要事务 | 读远多于写且不需要事务 |
2、如何防止sql注入?
在编写sql语句时,使用参数传递的方式去给变量赋值而不是直接嵌入到sql语句中
在mybatis的mapper中,我们可以用#{value}的方式去传递参数防止sql注入。
3、怎样实现幂等
幂等:也就是相同条件下对一个业务的操作,不管操作多少次,结果都是一样
实现方案:唯一索引(通常是主键),乐观锁(通常是version字段),token+redis(令牌-缓存)
token:token是服务端生成的一串字符串,当客户端登录后,服务端将token作为令牌发送给客户端,之后客户端访问服务端时,只需要校验token即可,不需要再验证用户和密码,redis作为缓存保存令牌到服务端。
4、一条sql语句的执行流程
Mysql分为server层和存储引擎层
- 建立连接:server层的连接器验证客户端发来的用户名和密码是否正确,如果错误就返回错误提示,如果正确就校验用户的权限
- 查询缓存:当执行sql查询语句时,mysql会先去缓存中查询是否有记录,如果有记录就直接返回,没有的话就去数据库中查询,然后将查询记录记录到缓存中
- 分析器:分析器的作用是对sql语句进行词法分析和语法分析,词法分析就是对sql语句中的关键词进行分析,比如查询到select关键词就知道这是一条查询语句,此外还会分析被操作的表,条件语句的字段等,语法分析就是判断这条语法是否正确。
- 优化器:对sql语句进行优化
- 执行器:调用存储引擎的接口,执行sql语句,执行之前会判断一下有没有权限
5、Mysql的事务特性(ACID)
- 原子性:单个事务是不可分割的最小工作单元,事务中的所有操作,要么全都执行成功,要么全都执行失败。通过undolog日志解决
- 一致性:数据库总是从一个一致性的状态转换到另外一个一致性的状态。如果事务成功完成,那么所有变化都将成功应用,如果失败,就会回滚到原始状态。通过undolog日志解决
- 持久性:一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。通过redolog日志解决
- 隔离性:事务与事务之间互不影响。通过加锁实现
6、 Mysql事务的隔离级别,脏读、不可重复度,幻读。
脏读:事务读取到了未提交的数据,比如A查询到了B修改了但未提交的数据
不可重复读:事务对同一内容的两次查询结果不一致,比如A查询到数据不存在,B插入并提交,A再次查询时发现存在
幻读:事务在插入前检查到数据不存在,插入时却发现数据已经存在无法插入,比如A发现数据不存在准备插入,但B此时插入数据并提交
不可重复读重点在于update,而幻读的重点在于insert或delete。
Mysql事务的隔离级别从低到高为:
- 读未提交RC:所有的事务都可以读取其他事务未提交的结果,会发生脏读,不可重复读,幻读
- 读已提交RU:事务开始时,只能读取到已提交的修改,会发生不可重复读,幻读
- 可重复读RR:事务开始时,在同一条件的查询返回的内容是一致的,会发生幻读,mysql默认隔离级别Mysql-可重复读的隔离级别在什么情况下会出现幻读_可重复读为什么会出现幻读_rsjssc的博客-CSDN博客
- 可串行化:所有事务依次执行,事务之间不能互相干扰,能防止脏读,不可重复读,幻读
7、 锁的类型,行锁和表锁,共享锁和独占锁
锁的类型 | 是否会死锁 | 并发量 | 适用场景 |
表锁 | 不会死锁 | 并发量低 | 1、读多写少(上一次锁连续读) 2、写特别多(如果用行锁,会导致其他事务长时间锁等待,锁冲突频率变高) |
行锁 | 会死锁 | 并发量高 | 并发量高的场合 |
页面锁 | 会死锁 | 并发量中 |
行锁:行锁就是加在单行上的锁,只存在于InnoDB引擎中,分为共享锁(读锁)和独占锁(写锁)
类型 | 作用 |
共享锁(读锁) | 获得共享锁的事务被允许读一行,并阻止其他事务获得该行的独占锁 |
独占锁(写锁) | 获得独占锁的事务被允许写一行,并阻止其他事务获得该行的共享锁和独占锁 |
读锁和写锁的
- 共同点: 自动加锁
- 不同点:共享锁可能会死锁,独占锁不会 / 读锁读取数据后释放,写锁要事务结束后才释放
- 语法:SELECT...+LOCK IN SHARE MODE / INSERT...+FOR UPDATE
此外还有意向共享锁和意向独占锁,这是InnoDB自动添加的,事务在获取读锁和写锁前必须先获取该表的对应意向锁。
表锁:表锁是加在整个表的锁
表锁也有两种锁,共享锁和独占锁,作用与行锁的基本相同
语法:LOCK TABLE {TABLENAME} READ / LOCK TABLE {TABLENAME} WRITE
8、内关联,全关联,左关联,右关联的区别
内关联(INNER JOIN):所有的查询结果在关联的两张表中都有记录
全关联:联结了那些在相关表中没有关联的行,分为左关联和右关联
左关联(LEFT JOIN):从From子句的左边表中选择所有行,可以没有右边的表的行
左关联(RIGHT JOIN):从From子句的右边表中选择所有行,可以没有左边的表的行
9、索引,索引的分类
索引的作用相当于是图书的目录,它是对数据表中一列或多列值进行排序的一种存储结构
索引的分类:普通索引,主键索引,复合索引,唯一索引,外键索引,全文索引
普通索引:
mysql表中最基本的索引
语法:CREAT INDEX index_name ON TABLE table_name(column)
主键索引:
在Mysql创建表指定主键的时候,会自动在主键上建立一个主键索引,标志着主键具有唯一性且不为空
复合索引:
复合索引也叫组合索引,在建立索引的时候使用表中的多个字段,就比如说使用身份证号和手机号建立索引
语法:CREAT INDEX index_name ON TABLE table_name(column1,column2)
唯一索引:
唯一索引标志着该字段具有唯一性
语法:CREAT UNIQUE INDEX index_name ON TABLE table_name(column)
如果创建表的时候加索引语法为:
CREATE TABLE tablename(
propname1 type1,
……
propnamen type..n,
UNIQUE INDEX|KEY index_name (column [(length)] [ ASC | DESC ] ) );//简化为:UNIQUE INDEX index_name (column);
10、索引的优缺点和使用规则
索引的优点主要体现在:
- 提高了用户查询的速度,提高了性能
- 降低了查询中分组和排序的时间
- 加速表与表之间的连接
- 通过索引的唯一性,可以确保表中数据的唯一性
索引的缺点主要体现在:
- 空间:索引占用了磁盘空间
- 时间:当数据量较大时,索引的创建和维护也是非常耗费时间的
- 每次对数据进行增删改查时,索引也要进行动态维护,降低了数据的维护速度
总结,以下条件不适合添加索引:
- 在查询中很少使用的列
- 数据值很少的列
- 写操作远多于读操作的列(修改频率大于查询频率的列)
使用索引的规则:
- 对查询频率高的字段创建索引
- 对经常需要分组、排序和联合操作的字段创建索引
- 尽量使用唯一索引
- 多使用短索引
- 索引的数目不要不多(不然会降低增删改的效率)
11、索引的原理,B+树的优点
首先我们了解一下B树(Balance Tree),即平衡树的意思,下图即是一颗B树
B树的每一个节点代表一个磁盘块,每一个节点上面存储了多个键值对和指针
B树相对于平衡二叉树,每个节点存储了更多的键(Key)和数据(Data),并且每个节点拥有更多的子节点,子节点的个数就是B树的阶数,比如上图就是的B树就是三阶B树
基于这个性质,B树查询数据读取磁盘的次数会减少,查找的效率会比平衡二叉树高。
B+树是对B树的进一步优化,下图是一颗B+树
在B+树中,非叶子节点不会存储数据(Data),只会存储键值(Key),而所有的数据都存储在叶子节点上,并且所有数据都是按照顺序排列的
在数据库中,页(节点)的大小是固定的,InnoDB默认为16kb,页如果不存储数据,就可以存储更多的键值,树的阶数会提升,
这样一来我们查询数据访问磁盘的次数会进一步减少,查询的效率更高,根节点默认存储在内存,所以我们访问一次三阶B+树索引的数据只需要2次磁盘IO
因为B+树索引中所有的数据都存储在叶子节点上,并且按顺序排列,所以对数据的范围查找,分组,排序都会更加简单
MyISAM中的B+树索引实现与innodb中的略有不同。在MyISAM中,B+树索引的叶子节点并不存储数据,而是存储数据的文件地址。
B+树相对于B树的特点:
- 只有叶子节点才会存放数据,非叶子节点只存放索引
- B+树中每个节点都通过双向链表连接,叶子节点中的数据通过单向链表连接
- 非叶子节点中的索引会出现在子节点中,而且是子节点中索引的最大(最小)值
- 非叶子节点的索引树=子节点数
B+树相对于B树的优点:
- 普通查询,范围查询,分组查询,排序的速度更快
- 插入和删除的速度更快
12、聚集索引和非聚集索引
以主键为B+树索引的键值构建的B+树索引就是聚集索引
以主键以外的字段作为B+树索引的键值构建的B+树索引就是非聚集索引
非聚集索引与聚集索引的区别在于非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键,想要查找数据我们还需要根据主键再去聚集索引中进行查找,这个再根据聚集索引查找数据的过程,我们称为回表。
13、联合索引的原理
联合索引是对多个列进行索引
联合索引也是通过B+树实现的,特点如下:
- B+树的键值数量不是1,而是2或2以上
- B+树在对第一个索引排序的基础上,再去对第二个索引排序,再去对第三个,依次排序
- 联合索引遵循最左匹配原则
最左匹配原则:在联合索引中查询时,从左边的字段开始匹配,若条件中的字段顺序与联合索引中字段顺序一致,就可以走索引查询,否则不走
14、索引什么时候会失效
- 使用LIKE模糊查询时%或者_放在最前面(会变为全表查询)
- 使用or语句or的两边没有同时使用索引
- 联合索引查询时没有遵守最左匹配原则
- 索引列数据类型隐式转化(varchar不加单引号的话可能会自动转换为int型)
- 对索引列进行计算或使用函数(改变了列值,在索引中找不到)
- order by使用错误(排序与索引中的排序不一致)
- 全盘扫描速度比索引快
- Where 子句中使用参数
15、高性能数据库优化实战经验:
冷门又重要的6个技巧,高并发数据库优化,架构师一定要收藏_哔哩哔哩_bilibili
1.打破范式设计,冗余少量字段方便查询,需要注意源表和冗余表保证同一事务写。
2.关联关系在业务层面约束,不依赖数据库外键
3.字段拓展性,如模板信息这种结构不清晰的字段使用json类型,json检索的问题我的想法是少量key使用虚拟列并建立索引,多条件检索直接异构es
4.冷热分离,源表拆分成多张表,可以把频繁变更的字段放在主表,使用率较低的放在副表,判断依据可以是创建时间、业务域
5.服务拆分在分片字段选择上尽量考虑使用本地事务,让同业务的不同sql命中同一个分表,以避免使用分布式事务
6.尽量使用单表维度sql,原因:join性能差,后期分库分表更方便,前瞻性设计要考虑使用哪种ID主键策略
16、sql语句中group by的使用
group by 根据字面意思就是对数据进行分组,所谓分组就是将数据集划分为若干个不同的小区域,针对每个小区域单独数据处理
注意:group by语句中select 查询的字段要么就是分组依据,要么在聚合函数中
select 类别,sun(分数) from 成绩 group by 类别 order by sum(分数) desc
17、sql书写顺序
SELECT <字段名>
FROM <表名>
JOIN <表名>
ON <连接条件>
WHERE <筛选条件>
GROUP BY <字段名>
HAVING <筛选条件>
UNION
ORDER BY <字段名>
LIMIT <限制行数>;
18、关联语句join与合并语句union
Join是关联语句,可以把两个表通过连接条件合并一个数据集
inner join:内连接,只有在两个表中都符合连接条件的记录才会被收集
left join:左连接的结果包括左表中的所有记录和右表中满足连接条件的记录
right join:右连接的结果包括右表中的所有记录和左表中满足连接条件的记录
full join:全连接的结果是左右表的并集
cross join:左右表做笛卡尔积
union (all):union能将多个select的记录进行合并,这多个记录必须列数一致,列名一致,列数据类型对应一致,如果是union all则返回的记录集包括重复记录
五、Redis
1、redis缓存穿透,缓存击穿以及缓存雪崩问题和对应的解决方案
缓存穿透:当客户端访问一个不存在的数据,这个数据在缓存和数据库中都不能命中,如果有大量的这种穿过缓存直接访问数据库的请求,就会给数据库带来很大压力。
解决思路:
- 缓存空对象:当发现数据库中也没有该数据时,我们把这个数据存在redis中,但值设置为空,这样下次访问时就会直接从redis中获取空对象。实现简单,维护方便,但内存消耗更大
- 布隆过滤器:布隆过滤器实际上是一串很长的二进制数组,它通过哈希思想去判断key是否存在,如果不存在则拒绝请求,存在则放行右redis和数据库处理。内存占用少,但实现复杂,有误判可能(存在也可能被判为不存在)
实例解决方案(缓存空对象)
1、判断缓存中是否有数据,如果有直接返回,没有则去数据库查数据
2、判断从数据库查出的数据是否为空,如果为空,则将空对象存在redis中
缓存击穿:当一个被高并发访问并且重建业务比较复杂的热点key失效时,会有大量的请求直接访问到数据库,给数据库带来巨大冲击。
解决思路:
- 分布式互斥锁:当一个线程获得锁然后执行重建缓存的逻辑时,其他线程只能等待缓存重建完成后再去获取缓存(使用redis的setnx方法来表示获取锁 ,set if not exist)。有死锁和线程池阻塞的风险,降低吞吐量,但能有效保持数据一致
- 设置永不过期:不直接设置key的过期时间,而是将过期时间放在value中,当有线程去查询key并发现已经到过期时间后,就异步地去更新缓存。解决了热点key的一系列危机,但在缓存更新时不能保证数据一致性
实例解决方案(分布式互斥锁)
缓存雪崩:当缓存中大量的key同时失效或redis服务崩溃时,会有大量请求直接访问到数据库,带来巨大压力
解决思路:
- 给不同的key的TTL增加随机值
- 设置key永不过期,加分布式锁
- 利用redis集群提高服务的可用性(比如使用redis哨兵)
- 给缓存业务添加降级限流策略
- 添加多级缓存
实例解决方案(设置key永不过期)
与分布式互斥锁相比,其实就是在缓存命中之后加一层缓存是否过期的校验,过期后就执行获取锁更新缓存的操作,没过期就直接返回
值得注意的点:
- 该方案中可以封装一个实体类,里面存有逻辑缓存过期时间和真实value,redis中的value就引用该对象实例
- 判断缓存是否过期通过拿逻辑缓存过期时间与LocalDateTime做对比
- 更新缓存时,获得锁后,应该开启独立线程去更新缓存,然后直接返回旧的缓存数据,目的是减少响应时间,因为设置key永不过期本身就不能保证数据完全同步
- 开启独立线程使用的是Executor,一套线程池管理框架,可以处理多线程操作,具体实现如下
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); if (isLock){ CACHE_REBUILD_EXECUTOR.submit( ()->{ try{ //重建缓存 this.saveShop2Redis(id,20L); }catch (Exception e){ throw new RuntimeException(e); }finally { unlock(lockKey); } }); }
2、redis的五种数据类型
3、redis的最佳实践
参考文章:Redis最佳实践_行万里路,读万卷书的博客-CSDN博客
3.1、redis键值设计
①优雅的key结构 业务名称:数据名称:id 例:login:user:1
优点:可读性强,避免key冲突,方便管理,节省内存空间
3.2、bigkey和bigkey的危害
bigkey以key的成员数量和key的大小来综合评估,比如key本身数据量过大,key的成员数量过多
bigkey的危害有:
- 网络阻塞:读取bigkey时,少量的QPS就可能宽带使用率拉满,造成网络阻塞,导致物理机和redis执行速度变慢
- 数据倾斜:bigkey的内存使用率大,导致数据分片的内存分配不均衡
- redis阻塞:对bigkey的数据进行一些运算操作时,耗时会比较久,就有可能导致redis进程阻塞
- CPU压力:对bigkey进行序列化和反序列化将导致CPU的使用率飙升,给予CPU巨大压力
3.3 总结
对于key:
- 固定格式:业务名称:数据名称:id
- 足够简短:尽量不超过44字节,因为key是string类型,底层编码包含int、embstr和raw三种。embstr在小于44字节时使用,采用连续内存空间,内存占用更小
- 不要包含特殊字符
对于value:
- 合理地 拆分数据,拒绝bigkey
- 选择合适的数据结构
- hash的entry数量尽量不要大于500,因为hash的entry数量大于500时,会使用哈希表结构而不是ziplist,增大内存占用
- 设置合理地过期时间
3.4 redis的更新策略
更新缓存还是删除缓存?
一般选择删除缓存,有以下好处:
没有无效更新操作,线程安全问题小
先操作数据库还是先操作缓存
一般选择先操作数据库,理由如下:
在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来,他查询缓存数据并不存在,此时他写入缓存,当他写入缓存后,线程1再执行更新动作时,实际上写入的就是旧的数据,新的数据被旧数据覆盖了。
而先操作数据库就不会有这种问题,安全系数更高
4、redis为什么快?
1、基于内存实现
Redis是基于内存存储的,读取数据时不需要进行磁盘IO,要比数据存储在磁盘的数据库快
2、高效的数据结构
redis中采用了许多高效的数据结构,比如
哈希表:可以保证查找操作空间复杂度是O(1)
跳表:保证查找/添加/插入/删除操作都能够在O(LogN)的复杂度内完成。
【精选】Redis-跳跃表(skip List)_redis跳表-CSDN博客
3、单线程操作,避免了不必要的线程上下文切换和竞争锁消耗,而且也不用去考虑线程安全问题,提高性能
4、高性能的多路复用IO模型
redis网络模型使用IO多路复用结合事件处理器
IO多路复用让redis在单线程的情况下也能处理多个连接请求,并且由于IO多路复用是非阻塞的,让redis能够高效网络通信
事件处理器通过事件监听和通知机制,避免了redis一直轮询关注事件进度,避免CPU浪费
并且在redis6.0中,为了提升性能,在命令回复和命令转换中使用了多线程
5、虚拟内存机制
Redis直接自己构建了VM机制 ,虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。
RedisIO多路复用模型:
IO多路复用模型通过调用select/poll/epoll等函数,开启一个监听线程(selector)去监听多个socket客户端,此时监听线程会阻塞,其他非监听线程仍可以继续工作,一旦某个socket客户端有IO操作就绪,selector会解除阻塞并通知redis,Redis就会建立连接,接收数据,然后selector继续阻塞等待客户端就绪。
一条命令在Redis是如何完成执行的?
Redis Server一旦和某一客户端建立连接后,就会在事件驱动框架中注册可读事件,对应客户端的命令请求。整个命令处理的过程可分为如下阶段:
-
命令解析,对应processInputBufferAndReplicate
-
命令执行,对应processCommand
-
结果返回,对应addReply
5、Redis为什么是单线程的?
首先要知道,Redis的性能瓶颈在内存和网络IO,而不是CPU
使用多线程对Redis的性能提升并不大,而多线程本身也有线程安全和上下文切换问题,所以Redis开发者选择使用单线程。
但是在Redis6.0之后,在发送响应数据中引用了多线程操作,这主要是为了提升网络IO性能
6、Redis双写一致性如何保证
Redis跟数据库双写方案需要根据业务对数据一致性的需求来决定
1、需要强一致性
采用Redission的读写锁,读数据采用读锁,写业务采用写锁
比如在抢券业务中,券的剩余量是需要强一致性的
2、允许延迟一致
采用异步方案同步,即写操作先写入数据库,然后异步地去修改缓存
异步方案:MQ(基于消息队列),Canal(基于mysql binlog),
7、Redis持久化方式
Redis进行持久化时,在主进程中会fork一个子进程,子进程会共享主进程的内存,并且通过内存中的页表对物理内存进行读取,然后将数据写入磁盘文件中
Redis持久化有两种方式
1、RDB(Redis DataBase Backup file)
RDB是Redis默认持久化方式,它按照配置好的时间周期策略将内存数据以快照的方式存入磁盘,生成rbg后缀文件,可以通过修改配置文件中的save参数来配置策略
2、AOF(Append only file)
AOF会将每一个写命令通过wirte函数追加到aof文件最后,类似于Mysql的binlog,当redis重启后,会执行文件中保存的写命令恢复数据
当两种方式都开启时,默认执行AOF
8、Redis过期删除策略
Redis采用惰性删除+定期删除策略
惰性删除:当访问key时判断是否过期,如果过期,就将key删除
定期删除:定期检查一定量key是否过期,如果过期就删除
惰性删除的问题是,如果key过期了,但却一直没有被访问,就无法删除,造成内存资源浪费
定期删除的问题是,会有很多key过期了但无法删除,不仅占用内存,访问时还会访问到过期数据
为什么不用到期自动删除?
因为这样就需要一个定时器去监视每个key是否过期,十分浪费CPU资源
9、Redis数据淘汰策略
Redis数据淘汰一共有8种策略
比较关键的是后面四种:
如果业务中数据访问频率差距大,有冷热数据之分,建议使用lru
如果要保留置顶数据,建议使用volatile,然后将置顶数据不设置过期时间
如果业务中有很多短时间内高频访问的数据,建议使用lfu,保留这些高频数据
场景题、数据库有1000w数据,但redis只能保存20w,如何让Redis存的数据更有价值?
首先建议采用lru算法,它能有效保存经常被访问的热点数据,然后如果有需要固定一些数据,则可以使用volatile-lru,然后不给固定数据设置TTL
10、在项目中Redis的使用场景
秒杀抢购逻辑:
在项目中,我使用了Redis保存了优惠券库存信息(string)和优惠券订单信息(set)
-
用户抢购时,首先要从Redis中获取优惠券信息,是否库存大于0,是才能继续,否则直接返回
-
接下来从Redis中获取优惠券订单信息,判断set集合中是否有用户id,如果是说明用户已经有订单了,就直接返回。否则将库存-1,然后在订单集合中增加该用户id。
以上均在lua脚本中完成
优惠券基本信息的缓存在添加优惠券时写入,TTL设置为优惠券过期时间减去当前时间。
判断有下单资格后,通过kafka消息队列异步生成订单,然后直接返回订单Id。
在项目中,我还使用了Redission分布式锁在生成订单操作前上锁,锁的key为order:user:{userId}
这里加锁主要是一个兜底,防止一人多单,实际上lua脚本中都判断过了
11、Redission分布式锁原理
Redission是Redis官方推荐的客户端,继承了juc中的lock接口,实现了可中断,可重入,超时释放等操作
底层结构:Redis中的hash结构,key存储锁名,field存储客户端id(uuid+ThreadId),value用于存储线程重入次数
加锁实现:
Redission加锁采用的是lua脚本来保证原子性,它首先会判断该key是否存在,如果不存在就创建一个,并设置线程重入次数为1,然后返回null
如果key存在就判断客户端id是否存在,如果存在就将重入次数+1,然后返回null
如果以上都不满足,就返回TTL
每次在加锁时都会判断,如果返回的TTL为null,说明加锁成功,否则失败,然后循环尝试获取锁
lease续约实现:
Redission中为了业务的安全性和可靠性引入watchdog机制,这个机制就是说在任务获取锁时,会同步开启一个守护线程,它会每隔leaseTime/3时间续约锁的超时时间,默认续约30s,可以配置,守护线程会一直续约直到锁释放,这样就保证了执行业务期间不会因超时释放锁
解锁实现:
首先判断锁是否是存在的,如果不存在直接返回nil; 如果该线程持有锁,则对当前的重入值-1,如果计算完后大于0,重新设置超时持有时间返回0; 如果算完不大于0,删除这个Hash,并且进行广播,通知watch dog停止进行刷新,并且 返回1.
RedissionRedLock红锁实现:
Redission锁没有解决主从节点切换导致的多个线程持有锁的问题,所以提供了,RedissionRedLock来实现,它要求对多个Redis实例都进行加锁,只有当超过一半的锁加锁成功时,才认为成功加锁。
12、Redis主从同步原理
单节点的Redis并发能力是有上限的,往往需要搭建Redis主从集群来提升性能,一般是一主多从,读写分离,主负责写,从负责读
主从同步的流程:
1、从节点发送同步请求
2、主节点判断是否是第一次请求,如果是,则进行全量同步,否则进行增量同步
全量同步:
1、首先与从节点同步版本信息(replication id和offset)
2、然后主节点执行一次bgsave,生成rgb文件,然后将rgb文件传给从节点去执行
3、在bgsave期间,主节点会将这期间的命令写入到一个日志文件中(repl_back.log)
4、在发送rgb文件之后,主节点会将日志文件发送给从节点同步
增量同步:
1、主节点获取从节点的offset信息
2、从命令日志文件中读取offset之后的数据,发送给从节点同步
13、Redis高可用,哨兵模式,脑裂问题
哨兵模式:哨兵模式可以实现主从集群的故障恢复
它主要实现了三个功能:
1、监控:每隔一秒钟向集群实例发送ping命令,检查集群实例是否在工作,如果有超过一定数量(可配置)的sentinel都认为某实例已经下线,则该实例被认为客观下线。
2、自动故障修复:当master节点被认为客观下线后,哨兵会将一个从节点提升为主节点,故障实例恢复后也以新的主节点为主
3、通知:Sentinel用来充当Redis的服务发现源,当Redis集群发生故障转移后,Sentinel会及时的将新的Redis节点信息推送到客户端。
脑裂问题:
发生脑裂问题一般是这种情况:
1、主节点因某些原因出现暂时性失联,但仍在正常工作,Sentinel此时联系不到主节点,于是将一个从节点升级为主节点。
2、然而Redis客户端会在Sentinel通知还未到时,继续向旧主节点写入数据
3、当旧主节点恢复与哨兵的联系时,会清除自己的数据与新主节点同步,造成了一段时间的数据丢失。
脑裂解决办法:
1、设置Redis中的min-slave-to-write参数:这个参数确保了主节点要至少有n个从节点才能执行写命令
2、设置Redis中的min-slave-max-lag参数:这个参数确保在主从复制时,如果ACK没在规定时间内,就拒绝写命令
这样一来,即使主节点失联了,也会因为联系不上从节点而拒绝写命令,防止数据丢失。
14、Redis分片集群的作用和原理
说到Redis分片集群,我们可以从特点说起:
Redis分片集群有以下特点:
-
有多个master节点,每个master节点中存储不同的数据——支持高并发写
-
每个master节点都有多个slave节点——支持高并发读
-
每个master节点会互相ping监测彼此健康状态——主节点监听
-
每个请求最终都会被转发到正确的节点中——请求路由转发
Redis分片集群存储和读取原理
-
Redis分片集群引入了哈希槽的概念,哈希槽可以看做若干哈希值的集合,Redis中共有16384个哈希槽。
-
Redis将哈希槽分配在各个master实例上
-
当读写请求到达集群时,会使用CRC16算法计算key的hash值,然后对16384取模来决定访问哪个节点
四种主流MQ的比较:
六、Spring
1、对Spring的理解
Spring 是一个开源的轻量级的Java开发框架,以 Ioc(Inverse Of control:反转控制)和AOP(Aspect Oriented Programming: 面向切面编程) 为核心,提供了展现层Spring MVC和持久层Spring JDBC以及业务层事务管理等众多企业级应用技术,其还整合众多开源的第三方框架,逐渐成为最受欢迎的Java EE企业应用框架
2、Spring框架特征
-
轻量高效:Spring框架本身体量小
-
IOC控制翻转:见下
-
AOP面向切面编程:见下
-
声明式事务:Spring可以使用注解的方式声明事务,省去了很多繁琐的代码,提高开发效率
-
容器:Spring管理每个应用对象的的配置和生命周期,在这个层面上它是一个容器,配置每个bean如何被创建
-
框架:Spring集成了各种框架,并降低了很多框架的使用复杂度,提供了对各种框架的直接支持
3、Spring IOC理解
IOC控制翻转是一种设计思想,它将原本是在程序运行时才创建并管理的对象交给Spring框架创建和管理,控制权由应用代码转移到了 Spring 容器,控制权发生了反转。
让开发者能够专注于业务逻辑层的开发,大大降低了模块之间的耦合度,提高开发效率和代码可维护性
4、依赖注入(DI)理解,依赖注入的方式
依赖注入是一种实现控制反转的设计模式,它指将对象的创建和依赖关系的分析从业务逻辑中分离开来,并将它们集中管理。核心思想是将依赖关系从代码中剔除出来,使得代码更加灵活,提高系统可扩展性和可维护性
依赖指的是一个对象需要使用的外部资源,比如其他对象、服务、数据库连接等
依赖注入主要有三种方式,set方法注入,构造器注入,自动注入 详情见:Spring积累(3):依赖注入3种方式_依赖注入的三种实现方式-CSDN博客
5、什么是AOP,AOP的应用场景有哪些?
AOP就是面向切面编程,它是一种编程思想和方法,它将影响多个对象的公共行为抽离并封装,实现了与业务逻辑的解耦,减少重复代码,增强代码可维护性,提高开发效率
AOP中的概念:
-
目标:指被代理对象
-
切面:相当于是切入点和通知的集合,定义了切面的全部内涵
-
连接点:指被增强的方法
-
切入点:连接点的集合
-
通知:指拦截到连接点后要做的增强
-
织入:指将切入点增强的过程
AOP的应用场景:
-
记录操作日志
-
缓存处理
-
Spring内置事务处理
-
异常处理
6、AOP的原理是什么,静态代理和动态代理的区别
AOP是通过JDK动态代理和CGLIB动态代理实现的
如果被代理类实现了接口,就会通过JDK动态代理,JDK动态代理是通过Java的反射机制实现的,代理类必须实现InvocationHandler接口,然后通过Proxy类的new ProxyInstance方法创建代理对象,然后通过代理对象调用被代理类的方法,并在此实现方法增强
如果代理类没有实现接口,就会通过CGLIB动态代理,CGLIB是通过创建当前类子类的代理对象,然后通过这个代理对象去继承父类方法,并在此做增强
静态代理去动态代理的区别:
静态代理在编译期间就已经确定了代理类和代理方法,而动态代理在运行时才确定
静态代理需要为每一个被代理类写一个具体的代理类,而动态代理只需要写一个代理类
静态代理是通过实现接口或继承父类的方式,而动态代理是通过反射
7、Spring事务失效的场景
-
在同一个类中,一个方法调用一个事务方法(没有用到代理类)
-
@Transactional标记的方法不是public(对外包不可见,无法使用代理)
-
@Transactional标记的方法被final或static修饰(无法重写,不能动态代理)
-
数据库表不支持事务,如MyIsam
-
异常捕获处理而不是抛出
-
抛出异常与rollbackfor指定的异常不一致,rollbackfor默认抛出RuntimeException
-
Spring的传播行为导致事务失效,比如: PROPAGATION_NEVER、PROPAGATION_NOT_SUPPORTED
8、Spring中的Bean是线程安全的吗?
Spring中的Bean不是线程安全的,如果Bean中有可以修改的成员变量,那么就会有线程安全问题,可以通过加锁或修改@Scope的值为多例解决
七、计算机网络
1、TCP的三次握手,四次挥手
三次握手:
- 第一次握手:客户端要向服务端发送连接,请求客户端发送一个SYN标志设为1,序号设置为随机数x的TCP报文给服务端
- 第二次握手:服务端要响应客户端,并告知客户端我能正确收到报文,服务端返回一个标志位SYN,ACK都设为1,确认号ack设为x+1,seq设为y的TCP报文给客户端
- 第三次握手:客户端收到确认后,检查ack是否为x+1,如果是,则发送一个ACK标志位设为1,确认号ack设为y+1的TCP报文给服务端,至此三次握手完成,通信双方互相确认了对方的通信能力
四次挥手:
- 第一次挥手:客户端发送一个标志位FIN设为1,seq设为x的TCP报文,然后进入FIN_WAIT_1的状态
- 第二次挥手:服务端收到FIN后,向客户端发送一个标志位ACK设为1,确认号ack设为x+1,序号seq设为y的报文,同时进入到CLOSE_WAIT状态
- 第三次挥手:服务端发送一个标志FIN设为1,确认号ack设为x+1,seq设为z的报文,同时进入到LAST_ACK状态
- 第四次挥手:客户端确认收到FIN后,发送一个标志位ACK设为1,确认号ack为第三次挥手的序号+1,序号为x+1的报文给服务端,然后进入TIME_WAIT状态,如果2MSL后没有回复,说明服务端已经关闭连接,客户端也就关闭连接了
2、HTTPS及其原理
HTTP:超文本传输协议
HTTPS:超文本安全传输协议
HTTPS通过数据加密和身份验证的方式来保证信息传输安全
数据加密是通过SSL(安全套接字层)/TLS(传输层安全)来加密数据包,然后SSL再通过验证数字证书来验证服务端的身份,以此来达到客户端到服务端的数据传输安全
八、数据结构与算法
九、其他
1、跨域问题是如何产生的?又如何解决?
跨域问题是浏览器的同源策略造成的,浏览器的同源策略是一种安全机制,指的是从一个地址访问另一个地址时,如果协议,主机地址,端口有一个不同就是跨域请求,都相同就不是跨域请求。
此时浏览器会在请求头上加上origin:url地址,表示这个请求来自哪里,如果服务器允许来自该来源的跨域请求,则会在响应头上加上Access-control-allow-origin:*
解决方法:
一、 通过script标签的src属性进行跨域请求,调用回调函数callbak
二、添加响应头:Access-control-allow-origin:*
三、使用Nginx代理跨域
由于服务器之间没有跨域问题,浏览器可以通过访问Nginx去访问跨域地址