目录
第一章-Java基础篇
1、你是怎样理解OOP面向对象 难度系数:⭐
2、重载与重写区别 难度系数:⭐
3、接口与抽象类的区别 难度系数:⭐
4、深拷贝与浅拷贝的理解 难度系数:⭐
5、sleep和wait区别 难度系数:⭐
6、什么是自动拆装箱 int和Integer有什么区别 难度系数:⭐
7、==和equals区别 难度系数:⭐
8、String能被继承吗 为什么用final修饰 难度系数:⭐
9、String buffer和String builder区别 难度系数:⭐
10、final、finally、finalize 难度系数:⭐
11、Object中有哪些方法 难度系数:⭐
12、说一下集合体系 难度系数:⭐
13、ArrarList和LinkedList区别 难度系数:⭐
14、HashMap底层是 数组+链表+红黑树,为什么要用这几类结构 难度系数:⭐⭐
15、HashMap和HashTable区别 难度系数:⭐
16、线程的创建方式 难度系数:⭐
17、线程的状态转换有什么(生命周期) 难度系数:⭐
18、Java中有几种类型的流 难度系数:⭐
19、请写出你最常见的5个RuntimeException 难度系数:⭐
20、谈谈你对反射的理解 难度系数:⭐
21、什么是 java 序列化,如何实现 java 序列化 难度系数:⭐
22、Http 常见的状态码 难度系数:⭐
23、GET 和POST 的区别 难度系数:⭐
24、Cookie 和Session 的区别 难度系数:⭐
第二章-Java高级篇
1、HashMap底层源码 难度系数:⭐⭐⭐
2、JVM内存分哪几个区,每个区的作用是什么 难度系数:⭐⭐
3、Java中垃圾收集的方法有哪些 难度系数:⭐
4、如何判断一个对象是否存活(或者GC对象的判定方法) 难度系数:⭐
5、什么情况下会产生StackOverflowError(栈溢出)和OutOfMemoryError(堆溢出)怎么排查 难度系数:⭐⭐
6、什么是线程池,线程池有哪些(创建) 难度系数:⭐
7、为什么要使用线程池 难度系数:⭐
8、线程池底层工作原理 难度系数:⭐
9、ThreadPoolExecutor对象有哪些参数 怎么设定核心线程数和最大线程数 拒绝策略有哪些 难度系数:⭐
10、常见线程安全的并发容器有哪些 难度系数:⭐
11、Atomic原子类了解多少 原理是什么 难度系数:⭐
12、synchronized底层实现是什么 lock底层是什么 有什么区别 难度系数:⭐⭐⭐
13、了解ConcurrentHashMap吗 为什么性能比HashTable高,说下原理 难度系数:⭐⭐
14、ConcurrentHashMap底层原理 难度系数:⭐⭐⭐
15、了解volatile关键字不 难度系数:⭐
16、synchronized和volatile有什么区别 难度系数:⭐⭐
17、Java类加载过程 难度系数:⭐
18、什么是类加载器,类加载器有哪些 难度系数:⭐
19、简述java内存分配与回收策略以及Minor GC和Major GC(full GC) 难度系数:⭐⭐
20、如何查看java死锁 难度系数:⭐
21、Java死锁如何避免 难度系数:⭐
第三章-java框架篇
1、简单的谈一下SpringMVC的工作流程 难度系数:⭐
2、说出Spring或者SpringMVC中常用的5个注解 难度系数:⭐
3、简述SpringMVC中如何返回JSON数据 难度系数:⭐
4、谈谈你对Spring的理解 难度系数:⭐
5、Spring中常用的设计模式 难度系数:⭐
6、Spring循环依赖问题 难度系数:⭐⭐
常见问法
什么是循环依赖?
两种注入方式对循环依赖的影响?
相关概念
三级缓存
四个关键方法
debug源代码过程
总结
其他衍生问题
7、介绍一下Spring bean 的生命周期、注入方式和作用域 难度系数:⭐
8、请描述一下Spring 的事务管理 难度系数:⭐
9、MyBatis中 #{}和${}的区别是什么 难度系数:⭐
10、Mybatis 中一级缓存与二级缓存 难度系数:⭐
11、MyBatis如何获取自动生成的(主)键值 难度系数:⭐
12、简述Mybatis的动态SQL,列出常用的6个标签及作用 难度系数:⭐
13、Mybatis 如何完成MySQL的批量操作 难度系数:⭐
14、谈谈怎么理解SpringBoot框架 难度系数:⭐⭐
15、Spring Boot 的核心注解是哪个 它主要由哪几个注解组成的 难度系数:⭐
16、Spring Boot自动配置原理是什么 难度系数:⭐
17、SpringBoot配置文件有哪些 怎么实现多环境配置 难度系数:⭐
18、SpringBoot和SpringCloud是什么关系 难度系数:⭐
19、SpringCloud都用过哪些组件 介绍一下作用 难度系数:⭐
20、Nacos作用以及注册中心的原理 难度系数:⭐⭐
21、Feign工作原理 难度系数:⭐⭐
第四章-MySQL
1、Select 语句完整的执行顺序 难度系数:⭐
2、MySQL事务 难度系数:⭐⭐
3、MyISAM和InnoDB的区别 难度系数:⭐
4、悲观锁和乐观锁的怎么实现 难度系数:⭐⭐
5、聚簇索引与非聚簇索引区别 难度系数:⭐⭐
6、什么情况下mysql会索引失效 难度系数:⭐
7、B+tree 与 B-tree区别 难度系数:⭐⭐
8、以MySQL为例Linux下如何排查问题 难度系数:⭐⭐
9、如何处理慢查询 难度系数:⭐⭐
10、数据库分表操作 难度系数:⭐
11、MySQL优化 难度系数:⭐
12、SQL语句优化案例 难度系数:⭐
13、你们公司有哪些数据库设计规范 难度系数:⭐
14、有没有设计过数据表?你是如何设计的 难度系数:⭐
15、常见面试SQL 难度系数:⭐
第五章-Redis
1、介绍下Redis Redis有哪些数据类型 难度系数:⭐
2、Redis提供了哪几种持久化方式 难度系数:⭐
3、Redis为什么快 难度系数:⭐
4、Redis为什么是单线程的 难度系数:⭐
5、Redis服务器的的内存是多大 难度系数:⭐
6、为什么Redis的操作是原子性的,怎么保证原子性的 难度系数:⭐
7、Redis有事务吗 难度系数:⭐
8、Redis数据和MySQL数据库的一致性如何实现 难度系数:⭐⭐
9、缓存击穿,缓存穿透,缓存雪崩的原因和解决方案(或者说使用缓存的过程中有没有遇到什么问题,怎么解决的) 难度系数:⭐
10、哨兵模式是什么样的 难度系数:⭐⭐
11、Redis常见性能问题和解决方案 难度系数:⭐
12、MySQL里有大量数据,如何保证Redis中的数据都是热点数据 难度系数:⭐⭐
13、Redis集群方案应该怎么做 都有哪些方案 难度系数:⭐⭐
14、说说Redis哈希槽的概念 难度系数:⭐⭐
15、Redis有哪些适合的场景 难度系数:⭐
16、Redis在项目中的应用 难度系数:⭐
第六章-分布式技术篇
第七章-Git
1、工作中git开发使用流程(命令版描述)
开发一个新功能流程: (master线上分支,dev测试分支)
2、Reset 与Rebase,Pull 与 Fetch 的区别
3、git merge和git rebase的区别
4、git如何解决代码冲突
5、项目开发时git分支情况
第八章-Linux
1、Linux常用命令
2、如何查看测试项目的日志
3、如何查看最近1000行日志
4、Linux中如何查看某个端口是否被占用
5、查看当前所有已经使用的端口情况
第九章-电商项目篇之尚品汇商城
1、介绍下最近做的项目
1.1 项目背景:
1.2 项目功能:
1.3 技术栈:
1.4 自己负责的功能模块:
1.5 项目介绍参考:
1.6 项目架构图:
1.7 整体业务介绍:
1.8 后台管理系统功能:
1.8.1 后台主页:
1.8.2 商品模块:
1).商品管理:
2).商品分类管理:
3).商品平台属性管理:
4).品牌管理:
5).商品评论管理:
1.8.3 销售模块:
1).促销秒杀管理:
2).礼券、积分管理:
3).关联/推荐管理:
1.8.4 订单模块:
1).订单管理:
2).支付:
3).结算:
1.8.5 库存模块:
1).库存管理:
2).查看库存明细记录。
3).备货/发货:
4).退/换货:
1.8.6 内容模块:
1).内容管理:
2).广告管理:
3).可自由设置商城导航栏目以及栏目内容、栏目链接。
1.8.7 客户模块:
1).客户管理:
2).反馈管理:
3).消息订阅管理:
4).会员资格:
1.8.8 系统模块:
1).安全管理:
2).系统属性管理:
3).运输与区域:
4).支付管理:
5).包装管理:
6).数据导入管理:
1.8.9 报表模块:
2、项目开发周期:
3、项目参与人数:
4、公司开发相关各岗位职责:
4.1 项目经理(PM):
4.2 产品(PD):
4.3 界面设计(UI):
4.4 开发组长(TL):
4.5 测试(QA):
4.6 运维(SRE):
5、项目开发流程:
5.1 需求分析
5.2 系统设计
5.3 编码开发
5.4 系统测试
5.5 部署实施
6、项目版本控制:
7、一般项目服务器数量:
开发测试阶段:
生产环境:
8、上线后QPS并发量,用户量、同时在线人数并发数等问题:
9、你们项目的微服务是怎么拆分的,拆分了多少?
10、如何解决并发问题的?
11、如何保证接口的幂等性?
12、你们项目中有没有用到什么设计模式?
13、生产环境出问题,你们是怎么排查的?
14、你做完这个项目后有什么收获?
15、在做这个项目的时候你碰到了哪些问题?你是怎么解决的?
第十章-数据结构和算法
1、怎么理解时间复杂度和空间复杂度
2、数组和链表结构简单对比
3、怎么遍历一个树
4、冒泡排序(Bubble Sort)
5、快速排序(Quick Sort)
6、二分查找(Binary Search)
1、你所知道的设计模式有哪些
2、单例模式(Binary Search)
2.1 单例模式定义
2.2 单例模式的特点
2.3 单例的四大原则
2.4 实现单例模式的方式
1)饿汉式(立即加载):
2)懒汉式(延迟加载):
3)同步锁(解决线程安全问题):
4)双重检查锁(提高同步锁的效率):
5) 静态内部类:
6)内部枚举类实现(防止反射和反序列化攻击):
3、工厂设计模式(Factory)
3.1 什么是工厂设计模式?
3.2 简单工厂(Simple Factory)
3.3 工厂方法(Factory Method)
3.4 抽象工厂(Abstract Factory)
3.5 三种工厂方式总结
4、代理模式(Proxy)
4.1 什么是代理模式?
4.2 为什么要用代理模式?
4.3 有哪几种代理模式?
4.4 静态代理(Static Proxy)
4.5 JDK动态代理(Dynamic Proxy)
4.6 CGLib动态代理(CGLib Proxy)
4.7 简述动态代理的原理, 常用的动态代理的实现方式
面向对象是利于语言对现实事物进行抽象。面向对象具有以下特征:
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
属于Thread类中的方法
释放cpu给其它线程 不释放锁资源
sleep(1000) 等待超过1s被唤醒
属于Object类中的方法
释放cpu给其它线程,同时释放锁资源
wait(1000) 等待超过1s被唤醒
wait() 一直等待需要通过notify或者notifyAll进行唤醒
wait 方法必须配合 synchronized 一起使用,不然在运行时就会抛出IllegalMonitorStateException异常
-
锁释放时机代码演示
-
public static void main(
String[] args) {
-
Object o
= new
Object();
-
Thread thread
= new Thread(() -
> {
-
synchronized (o) {
-
System.out.println(
"新线程获取锁时间:"
+ LocalDateTime.now()
+
" 新线程名称:"
+ Thread.currentThread().getName());
-
try {
-
-
/
/wait 释放cpu同时释放锁
-
o.wait(
2000);
-
-
/
/sleep 释放cpu不释放锁
-
/
/Thread.sleep(
2000);
-
System.out.println(
"新线程获取释放锁锁时间:"
+ LocalDateTime.now()
+
" 新线程名称:"
+ Thread.currentThread().getName());
-
} catch (InterruptedException e) {
-
throw new RuntimeException(e);
-
}
-
}
-
});
-
-
thread.
start();
-
-
try {
-
Thread.sleep(
100);
-
} catch (InterruptedException e) {
-
throw new RuntimeException(e);
-
}
-
-
System.out.println(
"主线程获取锁时间:"
+ LocalDateTime.now()
+
" 主线程名称:"
+ Thread.currentThread().getName());
-
-
synchronized (o){
-
System.out.println(
"主线程获取释放锁锁时间:"
+ LocalDateTime.now()
+
" 主线程名称:"
+ Thread.currentThread().getName());
-
}
-
}
Java
基本数据类型,如int,float,double,boolean,char,byte,不具备对象的特征,不能调用方法。
java为什么要引入自动装箱和拆箱的功能?主要是用于java集合中,List
list集合如果要放整数的话,只能放对象,不能放基本类型,因此需要将整数自动装箱成对象。
实现原理:javac编译器的语法糖,底层是通过Integer.valueOf()和Integer.intValue()方法实现。
区别:
如果比较的是基本数据类型,那么比较的是变量的值
如果比较的是引用数据类型,那么比较的是地址值(两个对象是否指向同一块内存)
如果没重写equals方法比较的是两个对象的地址值
如果重写了equals方法后我们往往比较的是对象中的属性的内容
equals方法是从Object类中继承的,默认的实现就是使用==
HashMap是线程不安全的,HashTable是线程安全的,其中的方法是Synchronized,在多线程并发的情况下,可以直接使用HashTable,但是使用HashMap时必须自己增加同步处理。
HashMap只有containsValue和containsKey方法;HashTable有contains、containsKey和containsValue三个方法,其中contains和containsValue方法功能相同。
Hashtable中,key和value都不允许出现null值。HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。
HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。
-
#### 代码演示
-
import java.util.concurrent.
*;
-
public
class threadTest{
-
public static void main(
String[] args) throws ExecutionException, InterruptedException {
-
/
/继承thread
-
ThreadClass thread
= new ThreadClass();
-
thread.
start();
-
Thread.sleep(
100);
-
System.out.println(
"#####################");
-
-
/
/实现runnable
-
RunnableClass runnable
= new RunnableClass();
-
new Thread(runnable).
start();
-
Thread.sleep(
100);
-
System.out.println(
"#####################");
-
-
/
/实现callable
-
FutureTask futureTask
= new FutureTask(new CallableClass());
-
futureTask.
run();
-
System.out.println(
"callable返回值:"
+ futureTask.
get());
-
Thread.sleep(
100);
-
System.out.println(
"#####################");
-
-
/
/线程池
-
ThreadPoolExecutor threadPoolExecutor
= new ThreadPoolExecutor(
1,
1,
2, TimeUnit.SECONDS, new ArrayBlockingQueue
<>(
10));
-
threadPoolExecutor.execute(thread);
-
threadPoolExecutor.shutdown();
-
Thread.sleep(
100);
-
System.out.println(
"#####################");
-
-
/
/使用并发包Executors
-
ExecutorService executorService
= Executors.newFixedThreadPool(
5);
-
executorService.execute(thread);
-
executorService.shutdown();
-
}
-
}
-
-
class ThreadClass extends Thread{
-
@
Override
-
public void
run() {
-
System.out.println(
"我是继承thread形式:"
+ Thread.currentThread().getName());
-
}
-
}
-
-
class RunnableClass implements Runnable{
-
@
Override
-
public void
run(){
-
System.out.println(
"我是实现runnable接口:"
+ Thread.currentThread().getName());
-
}
-
}
-
-
class CallableClass implements Callable
<
String
> {
-
@
Override
-
public
String
call(){
-
System.out.println(
"我是实现callable接口:");
-
return
"我是返回值,可以通过get方法获取";
-
}
-
}
Java
空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象。
指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。
字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符。
数组角标越界异常,常见于操作数组对象时发生。
方法传递参数错误。
数据类型转换异常。
所谓的反射机制就是java语言在运行时拥有一项自观的能力。通过这种能力可以彻底了解自身的情况为下一步的动作做准备。
Java的反射机制的实现要借助于4个类:class,Constructor,Field,Method;其中class代表的时类对 象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象。通过这四个对象我们可以粗略的看到一个类的各个组成部分。
在Java运行时环境中,对于任意一个类,可以知道这个类有哪些属性和方法。对于任意一个对象,可以调用它的任意一个方法。这种动态获取类的信息以及动态调用对象的方法的功能来自于Java 语言的反射(Reflection)机制。
在运行时判断任意一个对象所属的类。
在运行时构造任意一个类的对象。
在运行时判断任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的方法
无论客户端做怎样的设置,session 都能够正常工作。当客户端禁用 cookie 时将无法使用 cookie
在存储的数据量方面:session 能够存储任意的java 对象,cookie 只能存储 String 类型的对象
HashMap的底层结构在jdk1.7中由数组+链表实现,在jdk1.8中由数组+链表+红黑树实现,以数组+链表的结构为例。
JDK1.8之前Put方法:
JDK1.8之后Put方法:
HashMap基于哈希表的Map接口实现,是以key-value存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的key、value都可以为null。此外,HashMap中的映射不是有序的。
JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引值相同)而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。
补充:将链表转换成红黑树前会判断,即使阈值大于8,但是数组长度小于64,此时并不会将链表变为红黑树。而是选择进行数组扩容。
这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡 。同时数组长度小于64时,搜索时间相对要快些。所以综上所述为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树。具体可以参考 treeifyBin方法。
当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于8并且数组长度大于64时,链表转换为红黑树时,效率也变的更高效。
注意:可以结合百度hashmap源码解析进行更深入的了解。
史上最详细的 JDK 1.8 HashMap 源码解析_程序员囧辉的博客-CSDN博客
java虚拟机主要分为以下几个区
本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务。
java堆是所有线程所共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都在这里创建,因此该区域经常发生垃圾回收操作。
内存空间小,字节码解释器工作时通过改变这个计数值可以选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理和线程恢复等功能都需要依赖这个计数器完成。该内存区域是唯一一个java虚拟机规范没有规定任何OOM情况的区域。
JEP 122: Remove the Permanent Generation 介绍 静态变量、字符串常量从永久代移动到堆中
采用分区分代回收思想:
a) 效率高,缺点:需要内存容量大,比较耗内存
b) 使用在占空间比较小、刷新次数多的新生区
a) 效率比较低,会差生碎片。
a) 效率低速度慢,需要移动对象,但不会产生碎片。
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
参考https://www.cnblogs.com/boboooo/p/13164071.html
public class StackOverFlowTest {
private static int count = 1;
public static void main(String[] args) {
//模拟栈溢出
//getDieCircle();
//模拟堆溢出
getOutOfMem();
}
public static void getDieCircle(){
System.out.println(count++);
getDieCircle();
}
public static void getOutOfMem(){
while (true) {
Object o = new Object();
System.out.println(o);
}
}
}
Java
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new 线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率
在 JDK 的 java.util.concurrent.Executors 中提供了生成多种线程池的静态方法。
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(4);
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(4);
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
然后调用他们的 execute 方法即可。
这4种线程池底层 全部是ThreadPoolExecutor对象的实现,阿里规范手册中规定线程池采用ThreadPoolExecutor自定义的,实际开发也是。
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
创建一个定长的线程池,而且支持定时的以及周期性的任务执行。例如延迟3秒执行。
第一:降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进 行统一的分配,调优和监控
参数与作用:共7个参数
在ThreadPoolExecutor中有一个与它相关的配置:allowCoreThreadTimeOut(默认为false),当allowCoreThreadTimeOut为false时,核心线程会一直存活,哪怕是一直空闲着。而当allowCoreThreadTimeOut为true时核心线程空闲时间超过keepAliveTime时会被回收。
线程池能容纳的最大线程数,当线程池中的线程达到最大时,此时添加任务将会采用拒绝策略,默认的拒绝策略是抛出一个运行时错误(RejectedExecutionException)。值得一提的是,当初始化时用的工作队列为LinkedBlockingDeque时,这个值将无效。
当非核心空闲超过这个时间将被回收,同时空闲核心线程是否回收受allowCoreThreadTimeOut影响。
常用有三种队列,即SynchronousQueue,LinkedBlockingDeque(无界队列),ArrayBlockingQueue(有界队列)。
ThreadFactory是一个接口,用来创建worker。通过线程工厂可以对线程的一些属性进行定制。默认直接新建线程。
也是一个接口,只有一个方法,当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution法。默认是抛出一个运行时异常。
线程池大小设置:
如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1 如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。
一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/
线程 CPU 时间 )* CPU 数目
这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner测试大量运行次数求出平均值)
拒绝策略:
Java 的原子类都存放在并发包 java.util.concurrent.atomic下,如下图:
基本类型
数组类型
引用类型
Synchronized原理:
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。
代码块的同步是利用monitorenter和monitorexit这两个字节码指令。它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程则会进入阻塞状态,直到其他线程释放锁。
参考:一篇文章讲透synchronized底层实现原理_忘了带罗盘的船夫的博客-CSDN博客
Lock原理:
Lock与synchronized的区别:
synchronized |
Lock |
关键字 |
类 |
自动加锁和释放锁 |
需要手动调用unlock方法释放锁 |
jvm层面的锁 |
API层面的锁 |
非公平锁 |
可以选择公平或者非公平锁 |
锁是一个对象,并且锁的信息保存在了对象中 |
代码中通过int类型的state标识 |
有一个锁升级的过程 |
无 |
ConcurrentHashMap是线程安全的Map容器,JDK8之前,ConcurrentHashMap使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全,JKD8的版本取消Segment这个分段锁数据结构,底层也是使用Node数组+链表+红黑树,从而实现对每一段数据就行加锁,也减少了并发冲突的概率。
hashtable类基本上所有的方法都是采用synchronized进行线程安全控制,高并发情况下效率就降低 ,ConcurrentHashMap是采用了分段锁的思想提高性能,锁粒度更细化
-
public V put(K
key, V
value) {
-
Segment
<K,V
> s;
-
if (
value
=
=
null)
-
throw new NullPointerException();
-
int hash
= hash(
key);
-
/
/ hash 值无符号右移
28位(初始化时获得),然后与 segmentMask
=
15 做与运算
-
/
/ 其实也就是把高
4位与segmentMask(
1111)做与运算
-
-
/
/ this.segmentMask
= ssize
-
1;
-
/
/对hash值进行右移segmentShift位,计算元素对应segment中数组下表的位置
-
/
/把hash右移segmentShift,相当于只要hash值的高
32-segmentShift位,右移的目的是保留了hash值的高位。然后和segmentMask与操作计算元素在segment数组中的下表
-
int j
= (hash
>>> segmentShift) & segmentMask;
-
/
/使用unsafe对象获取数组中第j个位置的值,后面加上的是偏移量
-
if ((s
= (Segment
<K,V
>)UNSAFE.getObject
/
/ nonvolatile; recheck
-
(segments, (j
<
< SSHIFT)
+ SBASE))
=
=
null)
/
/
in ensureSegment
-
/
/ 如果查找到的 Segment 为空,初始化
-
s
= ensureSegment(j);
-
/
/插入segment对象
-
return s.put(
key, hash,
value,
false);
-
}
-
-
/
**
-
* Returns the segment
for the given
index, creating it
and
-
* recording
in segment
table (via CAS)
if
not already
present.
-
*
-
* @param k the
index
-
* @
return the segment
-
*
/
-
@SuppressWarnings(
"unchecked")
-
private Segment
<K,V
> ensureSegment(int k) {
-
final Segment
<K,V
>[] ss
= this.segments;
-
long u
= (k
<
< SSHIFT)
+ SBASE;
/
/ raw offset
-
Segment
<K,V
> seg;
-
/
/ 判断 u 位置的 Segment 是否为
null
-
if ((seg
= (Segment
<K,V
>)UNSAFE.getObjectVolatile(ss, u))
=
=
null) {
-
Segment
<K,V
> proto
= ss[
0];
/
/
use segment
0
as
prototype
-
/
/ 获取
0号 segment 里的 HashEntry
<K,V
> 初始化长度
-
int cap
= proto.
table.
length;
-
/
/ 获取
0号 segment 里的 hash 表里的扩容负载因子,所有的 segment 的 loadFactor 是相同的
-
float lf
= proto.loadFactor;
-
/
/ 计算扩容阀值
-
int threshold
= (int)(cap
* lf);
-
/
/ 创建一个 cap 容量的 HashEntry 数组
-
HashEntry
<K,V
>[] tab
= (HashEntry
<K,V
>[])new HashEntry[cap];
-
if ((seg
= (Segment
<K,V
>)UNSAFE.getObjectVolatile(ss, u))
=
=
null) {
/
/ recheck
-
/
/ 再次检查 u 位置的 Segment 是否为
null,因为这时可能有其他线程进行了操作
-
Segment
<K,V
> s
= new Segment
<K,V
>(lf, threshold, tab);
-
/
/ 自旋检查 u 位置的 Segment 是否为
null
-
while ((seg
= (Segment
<K,V
>)UNSAFE.getObjectVolatile(ss, u))
-
=
=
null) {
-
/
/ 使用CAS 赋值,只会成功一次
-
if (UNSAFE.compareAndSwapObject(ss, u,
null, seg
= s))
-
break;
-
}
-
}
-
}
-
return seg;
-
}
-
-
final V put(K
key, int hash, V
value,
boolean onlyIfAbsent) {
-
/
/ 获取 ReentrantLock 独占锁,获取不到,scanAndLockForPut 获取。
-
HashEntry
<K,V
> node
= tryLock() ?
null : scanAndLockForPut(
key, hash,
value);
-
V oldValue;
-
try {
-
HashEntry
<K,V
>[] tab
=
table;
-
/
/ 计算要put的数据位置
-
int
index
= (tab.
length
-
1)
& hash;
-
/
/ CAS 获取
index 坐标的值
-
HashEntry
<K,V
>
first
= entryAt(tab,
index);
-
for (HashEntry
<K,V
> e
=
first;;) {
-
if (e !
=
null) {
-
/
/ 检查是否
key 已经存在,如果存在,则遍历链表寻找位置,找到后替换
value
-
K k;
-
if ((k
= e.
key)
=
=
key ||
-
(e.hash
=
= hash
&
&
key.equals(k))) {
-
oldValue
= e.
value;
-
if (!onlyIfAbsent) {
-
e.
value
=
value;
-
+
+modCount;
-
}
-
break;
-
}
-
e
= e.
next;
-
}
-
else {
-
/
/
first 有值没说明
index 位置已经有值了,有冲突,链表头插法。
-
if (node !
=
null)
-
node.setNext(
first);
-
else
-
node
= new HashEntry
<K,V
>(hash,
key,
value,
first);
-
int c
=
count
+
1;
-
/
/ 容量大于扩容阀值,小于最大容量,进行扩容
-
if (c
> threshold
&
& tab.
length
< MAXIMUM_CAPACITY)
-
rehash(node);
-
else
-
/
/
index 位置赋值 node,node 可能是一个元素,也可能是一个链表的表头
-
setEntryAt(tab,
index, node);
-
+
+modCount;
-
count
= c;
-
oldValue
=
null;
-
break;
-
}
-
}
-
}
finally {
-
unlock();
-
}
-
return oldValue;
-
}
Java
-
public V put(K
key, V
value) {
-
return putVal(
key,
value,
false);
-
}
-
-
/
** Implementation
for put
and putIfAbsent
*
/
-
final V putVal(K
key, V
value,
boolean onlyIfAbsent) {
-
/
/
key 和
value 不能为空
-
if (
key
=
=
null ||
value
=
=
null) throw new NullPointerException();
-
int hash
= spread(
key.hashCode());
-
int binCount
=
0;
-
for (Node
<K,V
>[] tab
=
table;;) {
-
/
/ f
= 目标位置元素
-
Node
<K,V
> f; int n, i, fh;
/
/ fh 后面存放目标位置的元素 hash 值
-
if (tab
=
=
null || (n
= tab.
length)
=
=
0)
-
/
/ 数组桶为空,初始化数组桶(自旋
+CAS)
-
tab
= initTable();
-
else
if ((f
= tabAt(tab, i
= (n
-
1)
& hash))
=
=
null) {
-
/
/ 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出
-
if (casTabAt(tab, i,
null,new Node
<K,V
>(hash,
key,
value,
null)))
-
break;
/
/
no
lock
when adding
to empty bin
-
}
-
else
if ((fh
= f.hash)
=
= MOVED)
-
tab
= helpTransfer(tab, f);
-
else {
-
V oldVal
=
null;
-
/
/ 使用
synchronized 加锁加入节点
-
synchronized (f) {
-
if (tabAt(tab, i)
=
= f) {
-
/
/ 说明是链表
-
if (fh
>=
0) {
-
binCount
=
1;
-
/
/ 循环加入新的或者覆盖节点
-
for (Node
<K,V
> e
= f;;
+
+binCount) {
-
K ek;
-
if (e.hash
=
= hash
&
&
-
((ek
= e.
key)
=
=
key ||
-
(ek !
=
null
&
&
key.equals(ek)))) {
-
oldVal
= e.val;
-
if (!onlyIfAbsent)
-
e.val
=
value;
-
break;
-
}
-
Node
<K,V
> pred
= e;
-
if ((e
= e.
next)
=
=
null) {
-
pred.
next
= new Node
<K,V
>(hash,
key,
-
value,
null);
-
break;
-
}
-
}
-
}
-
else
if (f instanceof TreeBin) {
-
/
/ 红黑树
-
Node
<K,V
> p;
-
binCount
=
2;
-
if ((p
= ((TreeBin
<K,V
>)f).putTreeVal(hash,
key,
-
value)) !
=
null) {
-
oldVal
= p.val;
-
if (!onlyIfAbsent)
-
p.val
=
value;
-
}
-
}
-
}
-
}
-
if (binCount !
=
0) {
-
if (binCount
>= TREEIFY_THRESHOLD)
-
treeifyBin(tab, i);
-
if (oldVal !
=
null)
-
return oldVal;
-
break;
-
}
-
}
-
}
-
addCount(
1L, binCount);
-
return
null;
-
}
Java
通过一个类的全限定名获取该类的二进制流。
将该二进制流中的静态存储结构转化为方法去运行时数据结构。
在内存中生成该类的Class对象,作为该类的数据访问入口。
文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.
元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。
字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。
符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。
初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
类加载器就是把类文件加载到虚拟机中,也就是说通过一个类的全限定名来获取描述该类的二进制字节流。
启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用
扩展类加载器(extension class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类
系统类加载器(system class loader)也叫应用类加载器:它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它
用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现
栈区:栈分为java虚拟机栈和本地方法栈
堆区:堆被所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例。堆区是gc的主要区域,通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为Eden区,主要放新创建对象,From survivor 和 To survivor 保存gc后幸存下的对象,默认情况下各自占比 8:1:1。
方法区:被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)
程序计数器:当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。线程私有的。
当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC.Minor GC通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生GC的频率较高,回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行一次Minor GC这样可以加快老年代的回收速度。
-
####演示死锁
-
package com.ssg.mst;
-
public
class 死锁 {
-
-
private static
final
String
lock
1
=
"lock1";
-
private static
final
String
lock
2
=
"lock2";
-
public static void main(
String[] args) {
-
Thread thread
1
= new Thread(() -
> {
-
while (
true) {
-
synchronized (
lock
1) {
-
try {
-
System.out.println(Thread.currentThread().getName()
+
lock
1);
-
Thread.sleep(
1000);
-
synchronized (
lock
2){
-
System.out.println(Thread.currentThread().getName()
+
lock
2);
-
}
-
} catch (InterruptedException e) {
-
throw new RuntimeException(e);
-
}
-
}
-
}
-
});
-
-
Thread thread
2
= new Thread(() -
> {
-
while (
true) {
-
synchronized (
lock
2) {
-
try {
-
System.out.println(Thread.currentThread().getName()
+
lock
2);
-
Thread.sleep(
1000);
-
synchronized (
lock
1){
-
System.out.println(Thread.currentThread().getName()
+
lock
1);
-
}
-
} catch (InterruptedException e) {
-
throw new RuntimeException(e);
-
}
-
}
-
}
-
});
-
-
thread
1.
start();
-
thread
2.
start();
-
}
-
}
Java
死锁代码演示
造成死锁的几个原因
1.一个资源每次只能被一个线程使用
2.一个线程在阻塞等待某个资源时,不释放已占有资源
3.一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
4.若干线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中
1.要注意加锁顺序,保证每个线程按同样的顺序进行加锁
2.要注意加锁时限,可以针对锁设置一个超时时间
3.要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决
只要说出几个注解并解释含义即可,如上答案只做参考
Step1:在项目中加入json转换的依赖,例如jackson,fastjson,gson等
Step2:在请求处理方法中将返回值改为具体返回的数据的类型, 例如数据的集合类List
Step3:在请求处理方法上使用@ResponseBody注解
Spring 是一个开源框架,为简化企业级应用开发而生。Spring 可以是使简单的JavaBean 实现以前只有EJB 才能实现的功能。Spring 是一个 IOC 和 AOP 容器框架。
Spring 容器的主要核心是:
控制反转(IOC),传统的 java 开发模式中,当需要一个对象时,我们会自己使用 new 或者 getInstance 等直接或者间接调用构造方法创建一个对象。而在 spring 开发模式中,spring 容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用spring 提供的对象就可以了,这是控制反转的思想。
依赖注入(DI),spring 使用 javaBean 对象的 set 方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想。
面向切面编程(AOP),在面向对象编程(oop)思想中,我们将事物纵向抽成一个个的对象。而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。AOP 底层是动态代理,如果是接口采用 JDK 动态代理,如果是类采用CGLIB 方式实现动态代理。
比如:RestTemplate、JmsTemplate、JpaTemplate
请解释一下spring中的三级缓存
三级缓存分别是什么?三个Map有什么异同?
什么是循环依赖?请你谈谈?看过spring源码吗?
如何检测是否存在循环依赖?实际开发中见过循环依赖的异常吗?
多例的情况下,循环依赖问题为什么无法解决?
官方解释
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependency-resolution
实例化:堆内存中申请空间
初始化:对象属性赋值
名称 |
对象名 |
含义 |
一级缓存 |
singletonObjects |
存放已经经历了完整生命周期的Bean对象 |
二级缓存 |
earlySingletonObjects |
存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完) |
三级缓存 |
singletonFactories |
存放可以生成Bean的工厂 |
package org.springframework.beans.factory.support;
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
/**
单例对象的缓存:bean名称—bean实例,即:所谓的单例池。
表示已经经历了完整生命周期的Bean对象
第一级缓存
*/
private final Map
/**
早期的单例对象的高速缓存: bean名称—bean实例。
表示 Bean的生命周期还没走完(Bean的属性还未填充)就把这个 Bean存入该缓存中也就是实例化但未初始化的 bean放入该缓存里
第二级缓存
*/
private final Map
/**
单例工厂的高速缓存:bean名称—ObjectFactory
表示存放生成 bean的工厂
第三级缓存
*/
private final Map
}
需要22个断点(可选)
1,A创建过程中需要B,于是A将自己放到三级缓里面,去实例化B
2,B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A
3,B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态)
然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。
1,Spring创建 bean主要分为两个步骤,创建原始bean对象,接着去填充对象属性和初始化。
2,每次创建 bean之前,我们都会从缓存中查下有没有该bean,因为是单例,只能有一个。
3,当创建 A的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了B,接着就又去创建B,同样的流程,创建完B填充属性时又发现它依赖了A又是同样的流程,不同的是:这时候可以在三级缓存中查到刚放进去的原始对象A。
所以不需要继续创建,用它注入 B,完成 B的创建既然 B创建好了,所以 A就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成
Spring解决循环依赖依靠的是Bean的"中间态"这个概念,而这个中间态指的是已经实例化但还没初始化的状态—>半成品。实例化的过程又是通过构造器创建的,如果A还没创建好出来怎么可能提前曝光,所以构造器的循环依赖无法解决
问题1:为什么构造器注入属性无法解决循环依赖问题?
由于spring中的bean的创建过程为先实例化 再初始化(在进行对象实例化的过程中不必赋值)将实例化好的对象暴露出去,供其他对象调用,然而使用构造器注入,必须要使用构造器完成对象的初始化的操作,就会陷入死循环的状态
问题2:一级缓存能不能解决循环依赖问题? 不能
在三个级别的缓存中存储的对象是有区别的 一级缓存为完全实例化且初始化的对象 二级缓存实例化但未初始化对象 如果只有一级缓存,如果是并发操作下,就有可能取到实例化但未初始化的对象,就会出现问题
问题3:二级缓存能不能解决循环依赖问题?
理论上二级缓存可以解决循环依赖问题,但是需要注意,为什么需要在三级缓存中存储匿名内部类(ObjectFactory),原因在于 需要创建代理对象 eg:现有A类,需要生成代理对象 A是否需要进行实例化(需要) 在三级缓存中存放的是生成具体对象的一个匿名内部类,该类可能是代理类也可能是普通的对象,而使用三级缓存可以保证无论是否需要是代理对象,都可以保证使用的是同一个对象,而不会出现,一会儿使用普通bean 一会儿使用代理类
Bean的生命周期
(1)默认情况下,IOC容器中bean的生命周期分为五个阶段:
(2)当加入了Bean的后置处理器后,IOC容器中bean的生命周期分为七个阶段:
只需要回答出第一点即可,第二点也回答可适当 加分。
注入方式:
通过 setter 方法注入
通过构造方法注入
Bean的作用域
总共有四种作用域:
(1)声明式事务管理的定义:用在 Spring 配置文件中声明式的处理事务来代替代码式的处理事务。这样的好处是,事务管理不侵入开发的组件,具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话,也只需要在定义文件中重新配置即可,这样维护起来极其方便。
基于 TransactionInterceptor 的声明式事务管理:两个次要的属性: transactionManager,用来指定一个事务治理器, 并将具体事务相关的操作请托给它; 其他一个是 Properties 类型的transactionAttributes 属性,该属性的每一个键值对中,键指定的是方法名,方法名可以行使通配符, 而值就是表现呼应方法的所运用的事务属性。
(2)基于 @Transactional 的声明式事务管理:Spring 2.x 还引入了基于 Annotation 的体式格式,具体次要触及@Transactional 标注。@Transactional 可以浸染于接口、接口方法、类和类方法上。算作用于类上时,该类的一切public 方法将都具有该类型的事务属性。
(3)编程式事物管理的定义:在代码中显式挪用 beginTransaction()、commit()、rollback()等事务治理相关的方法, 这就是编程式事务管理。Spring 对事物的编程式管理有基于底层 API 的编程式管理和基于 TransactionTemplate 的编程式事务管理两种方式。
#{}是预编译处理,${}是字符串替换;
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
Mybatis在处理${}时,就是把${}替换成变量的值;
使用#{}可以有效的防止SQL注入,提高系统安全性。
一级缓存是SqlSession级别的缓存,默认开启。
二级缓存是NameSpace级别(Mapper)的缓存,多个SqlSession可以共享,使用时需要进行配置开启。
在
示例:
insert into names (name) values (#{ name})
Java
动态SQL是MyBatis的强大特性之一 基于功能强大的OGNL表达式。
动态SQL主要是来解决查询条件不确定的情况,在程序运行期间,根据提交的条件动态的完成查询
常用的标签:
MyBatis完成MySQL的批量操作主要是通过
例如:
insert into tbl_employee(last_name,email,gender,d_id) values
(#{ curr_emp.lastName},#{ curr_emp.email},#{ curr_emp.gender},#{ curr_emp.dept.id})
Java
Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。
Spring Boot的优点
Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。
spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。除此之外,还提供了各种启动器,开发者能快速上手。
Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starter-web启动器就能拥有web的功能,无需其他配置。
Spring Boot配置过程中无代码生成,也无需XML配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是Spring4.x的核心功能之一。
Spring Boot提供一系列端点可以监控服务及应用,做健康检测。
Spring Boot缺点:
Spring Boot虽然上手很容易,但如果你不了解其核心技术及流程,所以一旦遇到问题就很棘手,而且现在的解决方案也不是很多,需要一个完善的过程。
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,
首先它得是一个配置文件,其次根据类路径下是否有这个类去自动配置。
@EnableAutoConfiguration是实现自动配置的注解
@Configuration表示这是一个配置文件
具体参考文档:
Spring Boot自动配置原理、实战
Spring Boot 的核心配置文件是 application 和 bootstrap 配置文件。
application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。
bootstrap配置文件的特性:
bootstrap 配置文件有以下几个应用场景:
提供多套配置文件,如:
applcation.properties
application-dev.properties
application-test.properties
application-prod.properties
运行时指定具体的配置文件,具体请看这篇文章《Spring Boot Profile 不同环境配置》。
Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务,Spring Cloud是一个基于Spring Boot实现的开发工具;Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架; Spring Boot使用了默认大于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,Spring Cloud很大的一部分是基于Spring Boot来实现,必须基于Spring Boot开发。
可以单独使用Spring Boot开发项目,但是Spring Cloud离不开 Spring Boot。
Nacos英文全称Dynamic Naming and Configuration Service,Na为naming/nameServer即注册中心,co为configuration即注册中心,service是指该注册/配置中心都是以服务为核心。
Nacos注册中心分为server与client,server采用Java编写,为client提供注册发现服务与配置服务。而client可以用多语言实现,client与微服务嵌套在一起,nacos提供sdk和openApi,如果没有sdk也可以根据openApi手动写服务注册与发现和配置拉取的逻辑。
服务注册原理
服务注册方法:以Java nacos client v1.0.1 为例子,服务注册的策略的是每5秒向nacos server发送一次心跳,心跳带上了服务名,服务ip,服务端口等信息。同时 nacos server也会向client 主动发起健康检查,支持tcp/http检查。如果15秒内无心跳且健康检查失败则认为实例不健康,如果30秒内健康检查失败则剔除实例。
主程序入口添加了@EnableFeignClients注解开启对FeignClient扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。当程序启动时,会进行包扫描,扫描所有@FeignClient的注解的类,并且讲这些信息注入Spring IOC容器中,当定义的的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate.当生成代理时,Feign会为每个接口方法创建一个RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装HTTP请求需要的全部信息,如请求参数名,请求方法等信息都是在这个过程中确定的。然后RequestTemplate生成Request,然后把Request交给Client去处理,这里指的时Client可以时JDK原生的URLConnection,Apache的HttpClient,也可以时OKhttp,最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发器服务之间的调用。
SQL Select 语句完整的执行顺序:
(1)from 子句组装来自不同数据源的数据;
(2)where 子句基于指定的条件对记录行进行筛选;
(3)group by 子句将数据划分为多个分组;
(4)使用聚集函数进行计算;
(5)使用 having 子句筛选分组;
(6)计算所有的表达式;
(7)select 的字段;
(8)使用order by 对结果集进行排序。
事务的基本要素(ACID)
MySQL事务隔离级别:
事务隔离级别 |
脏读 |
不可重复读 |
幻读 |
读未提交(read-uncommitted) |
是 |
是 |
是 |
读提交(read-committed) |
否 |
是 |
是 |
可重复读(repeatable-read) |
否 |
否 |
是 |
串行化(serializable) |
否 |
否 |
否 |
事务的并发问题
如何解决脏读、幻读、不可重复读
小结:
不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
MyISAM |
InnoDB |
|
事务 |
不支持 |
支持 |
锁 |
表锁 |
表锁、行锁 |
文件存储 |
3个 |
1个 |
外键 |
不支持 |
支持 |
悲观锁:select...for update是MySQL提供的实现悲观锁的方式。
例如:select price from item where id=100 for update
此时在items表中,id为100的那条数据就被我们锁定了,其它的要执行select price from items where id=100 for update的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。MySQL有个问题是select...for update语句执行中所有扫描过的行都会被锁上,因此在MySQL中用悲观锁务必须确定走了索引,而不是全表扫描,否则将会将整个数据表锁住。
乐观锁:乐观锁相对悲观锁而言,它认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回错误信息,让用户决定如何去做。
利用数据版本号(version)机制是乐观锁最常用的一种实现方式。一般通过为数据库表增加一个数字类型的 “version” 字段,当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值+1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据,返回更新失败。
举例:
//1: 查询出商品信息
select (quantity,version) from items where id=100;
//2: 根据商品信息生成订单
insert into orders(id,item_id) values(null,100);
//3: 修改商品的库存
update items set quantity=quantity-1,version=version+1 where id=100 and version=#{version};
都是B+树的数据结构
聚簇索引:将数据存储与索引放到了一块、并且是按照一定的顺序组织的,找到索引也就找到了数据,数据的物理存放顺序与索引顺序是一致的,即:只要索引是相邻的,那么对应的数据一定也是相邻地存放在磁盘上的
非聚簇索引叶子节点不存储数据、存储的是数据行地址,也就是说根据索引查找到数据行的位置再取磁盘查找数据,这个就有点类似一本书的目录,比如我们要找第三章第一节,那我们先在这个目录里面找,找到对应的页码后再去对应的页码看文章。
优势:
1、查询通过聚簇索引可以直接获取数据,相比非聚簇索引需要第二次查询(非覆盖索引的情况下)效率要高
2、聚簇索引对于范围查询的效率很高,因为其数据是按照大小排列的
3、聚簇索引适合用在排序的场合,非聚簇索引不适合
劣势;
1、维护索引很昂贵,特别是插入新行或者主键被更新导至要分页(pagesplit)的时候。建议在大量插入新行后,选在负载较低的时间段,通过OPTIMIZETABLE优化表,因为必须被移动的行数据可能造成碎片。使用独享表空间可以弱化碎片
2、表因为使用uuId(随机ID)作为主键,使数据存储稀疏,这就会出现聚簇索引有可能有比全表扫面更慢,所以建议使用int的auto_increment作为主键
3、如果主键比较大的话,那辅助索引将会变的更大,因为辅助索引的叶子存储的是主键值,过长的主键值,会导致非叶子节点占用占用更多的物理空间
失效条件:
#查询条件用到了计算或者函数
explain SELECT * from test_slow_query where age = 20
explain SELECT * from test_slow_query where age +10 = 30
#模糊查询
EXPLAIN SELECT * from test_slow_query where NAME like '%吕布'
EXPLAIN SELECT * from test_slow_query where NAME like '%吕布%'
EXPLAIN SELECT * from test_slow_query where NAME like '吕布&'
#用到了or条件
EXPLAIN SELECT * from test_slow_query where NAME = '吕布' or name = "aaa"
#类型不匹配查询
explain SELECT * from test_slow_query where NAME = 11
explain SELECT * from test_slow_query where NAME = '11'
SQL
原理:分批次的将磁盘块加载进内存中进行检索,若查到数据,则直接返回,若查不到,则释放内存,并重新加载同等数据量的索引进内存,重新遍历
结构: 数据 向下的指针 指向数据的指针
特点:
1,节点排序
2 .一个节点了可以存多个元索,多个元索也排序了
结构: 数据 向下的指针
特点:
1.拥有B树的特点
2.叶子节点之间有指针
3.非叶子节点上的元素在叶子节点上都冗余了,也就是叶子节点中存储了所有的元素,并且排好顺序
从结构上看,B+Tree 相较于 B-Tree 而言 缺少了指向数据的指针 也就红色小方块;
Mysq|索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在Mysql中一个Innodb页就是一个B+树节点,一个Innodb页默认16kb,所以一般情况下一颗两层的B+树可以存2000万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL语句
文章推荐:B-Tree和B+Tree的区别 - 简书
类似提问方式:如果线上环境出现问题比如网站卡顿重则瘫痪 如何是好?
--->linux--->mysql/redis/nacos/sentinel/sluth--->可以从以上提到的技术点中选择一个自己熟悉单技术点进行分析
以mysql为例
1,架构层面 是否使用主从
2,表结构层面 是否满足常规的表设计规范(大量冗余字段会导致查询会变得很复杂)
3,sql语句层面(⭐)
前提:由于慢查询日志记录默认是关闭的,所以开启数据库mysql的慢查询记录 的功能 从慢查询日志中去获取哪些sql语句时慢查询 默认10S ,从中获取到sql语句进行分析
3.1 explain 分析一条sql
Id:执行顺序 如果单表的话,无参考价值 如果是关联查询,会据此判断主表 从表
Select_type:simple
Table:表
Type: ALL 未创建索引 、const、 常量ref其他索引 、eq_ref 主键索引、
Possible_keys
Key 实际是到到索引到字段
Key_len 索引字段数据结构所使用长度 与是否有默认值null 以及对应字段到数据类型有关,有一个理论值 有一个实际使用值也即key_len的值
Rows 检索的行数 与查询返回的行数无关
Extra 常见的值:usingfilesort 使用磁盘排序算法进行排序,事关排序 分组 的字段是否使用索引的核心参考值
还可能这样去提问:sql语句中哪些位置适合建索引/索引建立在哪个位置
Select id,name,age from user where id=1 and name=”xxx” order by age
总结: 查询字段 查询条件(最常用) 排序/分组字段
补充:如何判断是数据库的问题?可以借助于top命令
在业务系统中,除了使用主键进行的查询,其他的都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。
慢查询的优化首先要搞明白慢的原因是什么?是查询条件没有命中索引?是加载了不需要的数据列?还是数据量太大?
所以优化也是针对这三个方向来的
首先分析语句,看看是否加载了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。
分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。
具体处理流程 (阿里云RDS为例)
1.开启慢查询设置
type:连接类型
key: MYSQL使用的索引
rows:显示MYSQL执行查询的行数,简单且重要,数值越大越不好,说明没有用好索引
extra:该列包含MySQL解决查询的详细信息。
水平分表
步长法:1000万一张表拆分
取模法:举例:根据用户id取模落入不能的表
垂直分表:大表拆小表。商品信息 spu_info spu_image ...
可以说使用Mycat或者ShardingSphere等中间件来做,具体怎么做就要结合具体的场景进行分析了。可以参考:MySQL分库分表,写得太好了!-mysql分库分表
(1)尽量选择较小的列
(2)将where中用的比较频繁的字段建立索引
(3)select子句中避免使用‘*’
(4)避免在索引列上使用计算、not in 和<>等操作
(5)当只需要一行数据的时候使用limit 1
(6)保证单表数据不超过200W,适时分割表。针对查询较慢的语句,可以使用explain 来分析该语句具体的执行情况。
(7)避免改变索引列的类型。
(8)选择最有效的表名顺序,from字句中写在最后的表是基础表,将被最先处理,在from子句中包含多个表的情况下,你必须选择记录条数最少的表作为基础表。
(9)避免在索引列上面进行计算。
(10)尽量缩小子查询的结果
例1:where 子句中可以对字段进行 null 值判断吗?
可以,比如 select id from t where num is null 这样的 sql 也是可以的。但是最好不要给数据库留NULL,尽可能的使用 NOT NULL 填充数据库。不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,空间就固定了, 不管是否插入值(NULL 也包含在内),都是占用 100 个字符的空间的,如果是 varchar 这样的变长字段,null 不占用空间。可以在 num 上设置默认值 0,确保表中 num 列没有 null 值,然后这样查询:select id from t where num= 0。
例2:如何优化?下面的语句?
select * from admin left join log on admin.admin_id = log.admin_id where log.admin_id>10
优化为:select * from (select * from admin where admin_id>10) T1 lef join log on T1.admin_id = log.admin_id。
使用 JOIN 时候,应该用小的结果驱动大的结果(left join 左边表结果尽量小如果有条件应该放到左边先处理, right join 同理反向),同时尽量把牵涉到多表联合的查询