![](https://volc1612.gitee.io/blog/images/高级工程师必备知识点/黄山.jpeg)
文章目录
- ***点击查看思维导图***
- `JAVA SE`基础
- 位运算符和短路运算符?
- `&`按位与操作?
- `|`按为或操作?
- `&`和`&&`的区别?
- `|`和`||`的区别?
- 用最有效率的方法计算`2`乘以`8`?
- 写个方法,传递两个非`0`的`int`数值进去,实现变量交换的方式,有几种方式?
- 值传递和引用传递(地址传递)
- 数据类型分类以及在`JVM`所占的字节大小?
- `i++`和`++i`的`return`返回值?
- `==`和`equals`的区别?
- `try-catch-finally`中`return`结果返回值?
- `try-with-resource`的使用?
- 找出某目录下的所有子目录以及子文件并打印到控制台上?
- 字符串
- 实例对象类型?
- 无状态对象和有状态对象?
- 面向对象的四大特性是?分别解释下?
- `Overload`和`Override`的区别?
- 接口是否可以继承接口?接口是否支持多继承?类是否支持多继承?接口里面是否可以有方法实现?
- 类的关键字作用域?
- 介绍下对象的`hashCode()`和`equals()`使用场景?
- `JDK8`里面接口新特性?
- 说下`Vector`和`ArrayList`、`LinkedList`联系和区别?分别的使用场景?
- 为什么ArrayList插入元素慢?
- 如果需要保证线程安全,`ArrayList`应该怎么做,用有几种方式?
- 了解`CopyOnWriteArrayList`吗?和`Collections.synchronizedList`实现线程安全有什么区别, 使用场景是怎样的?
- `ArrayList`的扩容机制是怎样的?
- 设计一个简单的`ArrayList`需要包含`remove(index)`、`get(index)` 、`indexOf(o)` ,`set(int index,Object obj)`、 扩容机制?
- `HashMap`和`Hashtable`的区别?
- `HashMap`和`TreeMap`应该怎么选择,使用场景?
- `Set`和`Map`的关系?
- `HashSet`和`TreeSet`区别?
- 常见`Map`的排序规则是怎样的?
- 如果需要线程安全,且效率高的`Map`,应该怎么做?
- 为什么`Collections.synchronizedMap`后是线程安全的?
- 为什么要序列化对象?serializable接口作用,解决什么问题?
- serialVersionUID用于控制序列化版本是否兼容?
- JDK为什么设计为一定要实现serializable接口才能序列化?
- `JDK`源码解析
- `HashMap`
- 看过`HashMap`源码吗,介绍下你了解的`HashMap`?
- 能否解释下什么是`Hash`碰撞?常见的解决办法有哪些,`hashmap`采用哪种方法?
- 你说`HashMap`底层是 数组+链表+红黑树,为什么要用这几类结构呢?
- 为啥选择红黑树而不用其他树,比如二叉查找树,为啥不一直开始就用红黑树,而是到`8`的长度后才变换?
- 说下`hashmap`的`put`和`get`的核心逻辑(`JDK8`以上版本)?
- 了解`ConcurrentHashMap`吗?为什么性能比`hashtable`高,说下原理?
- `jdk1.7`和`jdk1.8`里面`ConcurrentHashMap`实现的区别有没了解?
- 说下`ConcurrentHashMap`的`put`的核心逻辑(`JDK8`以上版本)?
- 并发编程
- 能否解释下什么是进程、线程、协程,他们之间的关系是怎样的?
- 协程对于多线程有什么优缺点吗?
- 说下并发和并行的区别,举些例子说下?
- 多线程实现方式?
- `java`线程常见的基本状态有哪些,这些状态分别是做什么的?
- 是否了解多线程开发里面常用的方法,`sleep`/`yield`/`join`/`wait`/`notify`/`notifyAll`, 分别解释下?
- 画下线程的状态转换图和这些转换过程常用的`api`也标记下?
- 平时业务代码里面使用过多线程吗,能举例几个多线程的业务场景吗?
- 在`Java`中可以有哪些方法来保证线程安全?
- 了解`volatile`关键字不?能否解释下,然后这和`synchronized`有什么大的区别?
- 并发编程三要素是否知道,能否分别解释下,举个简单的例子?
- 调度算法
- 你日常开发里面用过`java`里面有哪些锁?分别解释下?
- 你日常开发里面用过`java`里面有哪些锁?分别解释下?
- 上机实战:写个多线程死锁的例子?
- 那上面的例子怎么解决死锁,优化下代码?
- 上机实战:设计一个简单的不可重入锁?
- 上机实战:设计一个简单的可重入锁?
- 对`synchronized`了解不,能否介绍下你对`synchronized`的理解?
- 了解`CAS`不,能否解释下什么是`CAS`?
- 知道`AQS`吗?能否介绍下,它的核心思想是什么?
- 你知道的`AQS`有几种同步方式,实现同步器一般要覆盖哪些方法?
- `java`里面的公平锁和非公平锁你知道多少,有没看过`ReentrantLock`源码?
- 你可以说清`ReentrantLock`和`synchronized`的差别不?
- 知道`ReentrantReadWriteLock`吗?和`ReentrantLock`有啥不同?
- java队列
- JVM
- 堆内存中的数据是线程共享的吗?
- 中间件消息队列
- MySQL数据库的面试题你遇过多少
- 数据库设计查询和上线里面的坑你走过多少
- 生产环境数据库性能监控和优化面试环节
- redis和memcache区别?
- SpringIOC创建对象的三种方式?
- Spring Bean的声明周期?
- spring为什么默认使用JDK动态代理?
- Spring IOC存储的实例为什么默认是单实例的?
- 并发情况下Spring Bean是否安全?
- Spring IoC的控制反转的理解?
- 自动装配注解
- 设计模式
- 强引用,软引用,弱引用和虚引用?
- `ThreadLocal`为什么容易内存泄漏?
- TCP?
- 如果TCP已经建立了连接,但是客户端突然出现故障了怎么办?
- HTTP(超文本协议)介绍?
- GET 和 POST 有什么区别?
- http缺点?
- UDP协议的三大特点?
- TCP和UDP区别?
- 同一个包中可以创建内部类对象?
- Minor GC(Yang GD)和Full GC?
- 聚集索引和非聚集索引(辅助索引) 物理索引和逻辑索引?
- MySQL覆盖索引?
- mysql最左侧索引原则?
- B+Tree特点?
- java有8种基本数据类型?
- HashMap尾插?
- HashMap扩容机制?
- 为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?
- 1.8HashMap为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?
- HashMap什么情况下出现链表环?
- 为什么1.8扩容的时候为啥一定必须是2的多少次幂?
- 为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8,而不是7或者不是20呢?
- 如何判断链表有环?
- 表分区、水平拆分和垂直查分表?
- MySQL的FULLTEXT索引?
- 为什么索引能提高查询速度?
- 为什么HashMap默认设置为16?
- countdownlatch和join方法的区别?
- synchronized原理?
- 同步容器和并发容器?
- 设计模式?
- 如何排查内存溢出?
- JVM调优工具?
- ArrayBlockingQueue和LinkedBlockingQueue区别?
- MySQL主键和外键的作用?
- count(*),count(1),count(id)?
- UTF8Bmb4区别?
- select * 为什么效率低?
- 主从复制延迟解决方案?
- 说说String中hashcode的实现?
- String中hashcode方法为什么选择31这个质数而不选择更大的质数呢?
- hashcode值溢出?
- SpringCloud和Dubbo区别?
- 微服务的优缺点?
- 使⽤中碰到的坑?
- 什么是服务熔断和服务降级?
- eureka和zookeeper都可以提供服务的注册与发现功能,他们的区别?
- Eureka原理?
- Dubbo服务注册与发现原理?
- 限流
- SpringCloud核心组件?
- JAVA队列
- MQ消息队列
- ``Spring IOC`注入`bean`的方式?
- `spring`事务实现方式有哪些?
- `Spring`中的事务原理?
- AOP
- `Spring IOC`循环依赖?
- `SpringBoot`启动流程?
- Spring SpringMVC SpringBoot 常用注解说明?
- 静态,构造,父子类加载顺序?
- 缓存穿透,缓存击穿,缓存雪崩
- Spring有哪些模块?
- MyBatis懒加载原理?
- MyBatis分页原理?
- 如何实现自己的`spring boot starter`工程
- `springboot`组件有哪些?
- `springboot beanFactory`实现原理?
- `SpringBoot factory`文件位置?
- `spring`源码包有哪些?
- 为什么要使用`SpringCloud`?
- 双向链表的优势
- `LinkedHashMap`实现原理
- 执行下面sql一共创建几个索引?
- 为什么要分库
点击查看思维导图
JAVA SE
基础
位运算符和短路运算符?
位运算符: &
|
短路运算符:&&
||
&
按位与操作?
只有对应的两个二进制数为1
时,结果位才为1
1&1 = 1
1&0 = 0
0&1 = 0
0&0 = 0
|
按为或操作?
有一个为1
的时候,结果位就为1
1|1 = 1
1|0 = 1
0|1 = 1
0|0 = 0
&
和&&
的区别?
&
和&&
都可以实现和
这个功能
区别:&
两边都运算,而&&
先算&&
左侧,若左侧为false
那么右侧就不运算,判断语句中推荐使用&&
,效率更高。
|
和||
的区别?
|
和||
都又或
这个功能
区别:||
只要满足第一个条件,后面的条件就不再判断,而|
要对所有的条件进行判断。
用最有效率的方法计算2
乘以8
?
原理:将一个数左移n
位,相当于乘以2
的n
次方,位运算是CPU
直接支持的,所以效率高
答案:2
<<3
常见的JDK
源码里面HashMap
的默认容量16
int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
直接是二进制操作了,表示1
左移4
位,变成10000
,转为10
进制也就是16
, 直接以二进制形式去运行,效率更高。
写个方法,传递两个非0
的int
数值进去,实现变量交换的方式,有几种方式?
-
加减运算实现
public static void swap(int a, int b){ System.out.printf("a=%d, b=%d",a,b); a = a + b; b = a - b ; a = a - b; System.out.printf("\na=%d, b=%d",a,b); }
-
异或运算实现(一个数与另一个数异或两次是其本身, 一个数和自身异或结果是
0
)public static void swap2(int a, int b){ System.out.printf("a=%d, b=%d",a,b); a = a^b; // a1 = a^b b = b^a; // b = b^a^b a = a^b; // a = a1^b = a^b^a System.out.printf("\na=%d, b=%d",a,b); }
值传递和引用传递(地址传递)
形式参数(形参)和实际参数(实参)?
-
形式参数:定义方法时写的参数。
fun(int a, int b); // a和b就是形式参数
-
实际参数:调用方法时写的具体数值。
fun(1,3); // 1和3就是实际参数
值传递?
- 方法调用时,实际参数把它的值传递给对应的形式参数,基本数据类型是值传递,函数接收的是原始值的一个
copy
。 - 官方解释:值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递?
- 官方解释:引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
数据类型分类以及在JVM
所占的字节大小?
-
数字类型
- 整数类型
byte(1)
、short(2)
、int(4)
、long(8)
- 浮点类型
float(4)
、double(8)
- 整数类型
-
字符类型
char(2)
-
布尔类型
boolean(boolean类型的数组,每个boolean元素占1个字节,单个boolean变量占4个字节)
-
引用数据类型:除了基本数据类型其他都是引用类型
-
String
和Enum
分别是什么类型:引用类型
i++
和++i
的return
返回值?
++
不是原子操作
定义变量int i = 5;
return i++;
和 return ++i;
返回结果是什么
答: 5,6
==
和equals
的区别?
- 基本数据类型比较,要用
==
判断是否相等。 - 引用数据类型:
==
比较的是内存地址是否一样,不同对象的内存地址不一样,equals
比较的是具体的内容, 也可以让开发者去定义什么条件去判断两个对象是否一样。
try-catch-finally
中return
结果返回值?
public static int test1() {
int a = 1;
try {
System.out.println(a / 0);
a = 2;
} catch (ArithmeticException e) {
a = 3;
return a;
} finally {
a = 4;
}
return a;
}
public static int test2() {
int a = 1;
try {
System.out.println(a / 0);
a = 2;
} catch (ArithmeticException e) {
a = 3;
return a++;
} finally {
a = 1;
return a;
}
}
答案:test1()
return
3
test2()
return
1
。
在执行try
、catch
中的return
之前一定会执行finally
中的代码(如果finally
存在),如果finally
中有return
语句,就会直接执行finally
中的return
方法,所以finally
中的return
语句一定会被执行的。
执行流程:finally
执行前的代码里面有包含return
,则会先确定return
返回值,这时catch
中return a++;
结果值是3
,然后再执行finally
的代码结果值修改为1
,最后finally
再执行返回结果返回1
。
try-with-resource
的使用?
try-with-resource
是JDK7
之后的写法,JDK9
⼜进⾏了改良,但是变化不⼤,记住下⾯的写法即可
需要关闭的资源只要实现了java.lang.AutoCloseable
,就可以⾃动被关闭
try()
⾥⾯可以定义多个资源,它们的关闭顺序是最后在try()
定义的资源先关闭
从一个txt
文本里面,拷贝里面的内容到另外一个txt
文本里面?
try (
FileInputStream fis = new FileInputStream("/Users/xdclass/Desktop/test.txt");
BufferedInputStream bis = new BufferedInputStream(fis);
FileOutputStream fos = new FileOutputStream("/Users/xdclass/Desktop/copy.txt");
BufferedOutputStream bos = new BufferedOutputStream(fos);
) {
int size;
byte[] buf = new byte[1024];
while ((size = bis.read(buf)) != -1) {
bos.write(buf, 0, size);
}
} catch (Exception e) {
e.printStackTrace();
}
找出某目录下的所有子目录以及子文件并打印到控制台上?
public static void main(String[] args) {
//找出某目录下的所有子目录以及子文件并打印到控制台上
List<String> paths = new ArrayList<>();
getAllFilePaths(new File("/Users/xdclass/Desktop/小滴课堂-架构面试题教程/demo"),paths);
for(String path : paths){
System.out.println(path);
}
}
private static void getAllFilePaths(File filePath, List<String> paths) {
File[] files = filePath.listFiles();
if(files == null){
return;
}
for(File f : files){
if(f.isDirectory()){
paths.add(f.getPath());
getAllFilePaths(f,paths);
}else{
paths.add(f.getPath());
}
}
}
字符串
String str = new String("1");
创建了几个对象?
答案:
创建一个对象:常量池存在,则直接new
一个对象;
创建两个对象:常量池不存在,则在常量池创建一个对象,也在堆里面创建一个对象
比较字符串是否相等?
String str1= new String("xdclass.net");
String str2= "xdclass.net";
String str3= "xdclass.net";
System.out.println(str1 == str2) // false
System.out.println(str2 == str3) // true
答案:
比较引用的内存地址是否一样
第一个是false
: new
创建新的对象会开辟新的空间,所以地址不一样。
第二个是true
:都是从常量池里面获取,xdclass.net
存在于常量池中。
字符串拼接比较?
写出下面代码的各个结果?如果需要两个都为true
,应该怎么修改?
String s1 = "xdclass";
String s2 = s1 + ".net"; // 变量 + 常量 = 来自堆,s2在堆中创建了一个对象,指向常量值"xdclass.net"
String s3 = "xdclass" + ".net"; // 常量 + 常量 = 来自常亮池,s3为新的对象存放在常量池中
System.out.println(s2 == "xdclass.net");
System.out.println(s3 == "xdclass.net");
答案
第一条语句打印的结果为false
, s2 = s1 + ".net"
,构建了一个新的string
对象,并将对象引用赋予s2
变量,常量池中的地址不一样,但是值一样。
第二条语句打印的结果为true
,javac
编译可以对【字符串常量】直接相加的表达式进行优化,不用等到运行期再去进行加法运算处理,而是直接将其编译成一个这些常量相连的结果。
如果需要第一个输出为true
,那么s2
必须也为常量存在常量池中,只需要把变量s1
改为常量即可fianl String s1 = "xdclass";
不管是new String("XXX")
和直接常量赋值, 都会在字符串常量池创建.只是new String("XXX")
方式会在堆中创建一个对象去指向常量池的对象, 普通的常量赋值是直接赋值给变量。
String
、StringBuffer
与StringBuilder
的区别?分别在哪些场景下使用?
三者都是final
, 不允许被继承
在本质都是char[]
字符数组实现
String
、StringBuffer
与StringBuilder
中,String
是不可变对象,另外两个是可变的
StringBuilder
效率更快,因为它不需要加锁,不具备多线程安全
StringBuffer
里面操作方法用synchronized
,效率相对更低,是线程安全的;
- 使用场景
操作少量的数据用String
,但是常改变内容且操作数据多情况下最好不要用String
,因为每次生成中间对象性能会降低
单线程下操作大量的字符串用StringBuilder
,虽然线程不安全但是不影响
多线程下操作大量的字符串,且需要保证线程安全 则用StringBuffer
实例对象类型?
分层领域模型规约:
-
DO( Data Object):与数据库表结构一一对应,通过DAO层向上传输数据源对象。
-
DTO( Data Transfer Object):数据传输对象,Service或Manager向外传输的对象。
-
BO( Business Object):业务对象。 由Service层输出的封装业务逻辑的对象。
-
AO( Application Object):应用对象。 在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高。
-
VO( View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象。
-
POJO( Plain Ordinary Java Object):在本手册中, POJO专指只有setter/getter/toString的简单类,包括DO/DTO/BO/VO等。
-
Query:数据查询对象,各层接收上层的查询请求。 注意超过2个参数的查询封装,禁止使用Map类来传输。
领域模型命名规约: -
数据对象:xxxDO,xxx即为数据表名。
-
数据传输对象:xxxDTO,xxx为业务领域相关的名称。
-
展示对象:xxxVO,xxx一般为网页名称。
-
POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。
无状态对象和有状态对象?
-
无状态对象
无状态就是一次操作,不能保存数据/** * 无状态bean,不能存偖数据。因为没有任何属性,所以是不可变的。只有一系统的方法操作。 */ public class StatelessBeanService { // 虽然有billDao属性,但billDao是没有状态信息的,是Stateless Bean. BillDao billDao; public BillDao getBillDao() { return billDao; } public void setBillDao(BillDao billDao) { this.billDao = billDao; } public List<User> findUser(String Id) { return null; } }
-
有状态对象
有状态就是有数据存储功能/** * 有状态bean,有state,user等属性,并且user有存偖功能,是可变的。 */ public class StatefulBean { public int state; // 由于多线程环境下,user是引用对象,是非线程安全的 public User user; public int getState() { return state; } public void setState(int state) { this.state = state; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } }
面向对象的四大特性是?分别解释下?
-
抽象
关键词
abstract
声明的类叫作抽象类,abstract
声明的⽅法叫抽象⽅法
⼀个类⾥包含了⼀个或多个抽象⽅法,类就必须指定成抽象类
抽象⽅法属于⼀种特殊⽅法,只含有⼀个声明,没有⽅法体
抽象支付
pay
(金额,订单号),默认实现是本地支付,微信支付,支付宝支付,银行卡支付 -
封装
封装是把过程和数据包围起来,对数据的访问只能通过已定义的接⼝即⽅法
在java
中通过关键字private
,protected
和public
实现封装。
封装把对象的所有组成部分组合在⼀起,封装定义程序如何引⽤对象的数据,
封装实际上使⽤⽅法将类的数据隐藏起来,控制⽤户对类的修改和访问数据的程度。 适当的
封装可以让代码更容易理解和维护,也加强了代码的安全性
类封装
⽅法封装 -
继承
⼦类继承⽗类的特征和⾏为,使得⼦类对象具有⽗类的⽅法和属性,⽗类也叫基类,具有公共的⽅法和属性
动物<-猫
动物<-狗abstract class AbsPay{ } WeixinPay extends AbsPay{ } AliPay extends AbsPay{ }
-
多态
同⼀个⾏为具有多个不同表现形式的能⼒
优点:减少耦合、灵活可拓展
⼀般是继承类或者重写⽅法实现
Overload
和Override
的区别?
重载Overload
:表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同,参数个数或类型不同
重写Override
:表示子类中的方法可以与父类中的某个方法的名称和参数完全相同
接口是否可以继承接口?接口是否支持多继承?类是否支持多继承?接口里面是否可以有方法实现?
成员变量类 | 有无构造 | 继承类的数量 | 方法体 | 是否可以有main方法 | 实现方式 | 实现数量 | |
---|---|---|---|---|---|---|---|
接口 | 只有public final类型 | 无 | 接口可继承多个接口 | 1.8之前版本能有方法体,JDK1.8只能有default类型的法体 | 否 | 类使用使用implements实现接口 | 类可以实现多个接口 |
抽象类 | 各种类型 | 有 | 抽象类只能继承一个抽象类,可以实现多个接口 | 可以有各种类型的方法体具体实现,但是不能有default方法 | 是 | 类使用extends来继承抽象类 | 类型只能继承一个抽象类 |
类的关键字作用域?
作用域 | 当前类 | 同一package | 子孙类 | 其他package |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
friendly | √ | √ | × | × |
private | √ | × | × | × |
不写时默认为friendly
介绍下对象的hashCode()
和equals()
使用场景?
-
hashcode
顶级类
Object
里面的方法,所有的类都是继承Object
,返回是一个int
类型的数
根据一定的hash
规则(存储地址,字段,长度等),映射成一个数组,即散列值 -
equals
顶级类Object
里面的方法,所有的类都是继承Object
,返回是一个boolean
类型
根据自定义的匹配规则,用于匹配两个对象是否一样,一般逻辑如下// 判断地址是否一样
// 非空判断和Class
类型判断
// 强转
// 对象里面的字段一一匹配
使用场景:对象比较、或者集合容器里面排重、比较、排序
JDK8
里面接口新特性?
interface
中可以有static
⽅法,但必须有⽅法实现体,该⽅法只属于该接⼝,接⼝名直接调⽤ 该⽅法- 接⼝中新增
default
关键字修饰的⽅法,default
⽅法只能定义在接⼝中,可以在⼦类或⼦接⼝ 中被重写default
定义的⽅法必须有⽅法体 - ⽗接⼝的
default
⽅法如果在⼦接⼝或⼦类被重写,那么⼦接⼝实现对象、⼦类对象,调⽤该⽅法,以重写为准 - 本类、接⼝如果没有重写⽗类(即接⼝)的
default
⽅法,则在调⽤default
⽅法时,使⽤⽗类(接口) 定义的default
⽅法逻辑
说下Vector
和ArrayList
、LinkedList
联系和区别?分别的使用场景?
集合 | 数据结构 | 是否安全 | 优缺点 | 使用场景 | 查询时间复杂度 |
---|---|---|---|---|---|
ArrayList | 数组 | 否 | 查询和修改非常快,但是增加和删除慢,数组元素分配在内存中的内存地址是连续的,根据数组的(首地址+偏移量),直接计算出我想访问的第index个元素在内存中的位置,但是新增和删除则会移动每个元素的内存地址位置,效率比较慢 | 查询和修改多则用ArrayList | O(1) |
LinkedList | 双向链表 | 否 | 查询和修改非常快,但是增加和删除慢 查询时从首元素开始,依次获得下一个元素的地址 | 查询和修改多则用ArrayList | O(N) |
Vector | 数组 | 是,操作的时候使用synchronized 进行加锁 | 多线程场景 | O(1) |
为什么ArrayList插入元素慢?
插入元素,可能会进行扩容,调用以下方法进行数组赋值,重新将就数组元素放到新数组中。进行大量内存IO操作。
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
如果需要保证线程安全,ArrayList
应该怎么做,用有几种方式?
方式一:自己写个包装类,根据业务一般是add/update/remove
加锁
方式二:Collections.synchronizedList(new ArrayList<>());
使用synchronized
加锁
方式三:CopyOnWriteArrayList<>()
使用ReentrantLock
加锁
了解CopyOnWriteArrayList
吗?和Collections.synchronizedList
实现线程安全有什么区别, 使用场景是怎样的?
-
CopyOnWriteArrayList
:执行修改操作时,会拷贝一份新的数组进行操作(add
、set
、remove
等),代价十分昂贵,在执行完修改后将原来集合指向新的集合来完成修改操作,源码里面用ReentrantLock
可重入锁来保证不会有多个线程同时拷贝一份数组- 场景:读高性能,适用读操作远远大于写操作的场景中使用(读的时候是不需要加锁的,直接获取,删除和增加是需要加锁的, 读多写少)
-
Collections.synchronizedList
:线程安全的原因是因为它几乎在每个方法中都使用了synchronized
同步*锁- 场景:写操作性能比
CopyOnWriteArrayList
好,读操作性能并不如CopyOnWriteArrayList
- 场景:写操作性能比
-
CopyOnWriteArrayList
的设计思想是怎样的,有什么缺点?答案:设计思想:读写分离+最终一致
缺点:内存占用问题,写时复制机制,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象,如果对象大则容易发生Yong GC
和Full GC
ArrayList
的扩容机制是怎样的?
注意:JDK1.7
之前ArrayList
默认大小是10
,JDk1.7
之后是0
未指定集合容量,默认是0
,若已经指定大小则集合大小为指定的;
当集合第一次添加元素的时候,集合大小扩容为10
ArrayList
的元素个数大于其容量,扩容的大小=
原始大小+
原始大小/2
设计一个简单的ArrayList
需要包含remove(index)
、get(index)
、indexOf(o)
,set(int index,Object obj)
、 扩容机制?
System.arraycopy(Object src, int srcPos, Object dest, int destPos,int length)
参数介绍
Object src
: 原数组
int srcPos
: 从元数据的起始位置开始
Object dest
: 目标数组
int destPos
: 目标数组的开始起始位置
int length
: 要copy
的数组的长度
import java.io.Serializable;
public class MyArrayList implements Serializable {
//使用这个字段,来判断当前集合类是否被并发修改,即迭代器并发修改的fail-fast机制
private transient int modCount = 0;
//第一次扩容的容量
private static final int DEFAULT_CAPACITY = 10;
//用于初始化空的list
private static final Object[] EMPTY_ELEMENT_DATA = {};
//实际存储的元素
transient Object[] elementData;
//实际list集合大小,从0开始
private int size;
public MyArrayList() {
this.elementData = EMPTY_ELEMENT_DATA;
}
public MyArrayList(int initialCapcity) {
if (initialCapcity > 0) {
this.elementData = new Object[initialCapcity];
} else if (initialCapcity == 0) {
this.elementData = EMPTY_ELEMENT_DATA;
} else {
throw new IllegalArgumentException("参数异常");
}
}
public boolean add(Object e) {
//判断容量
ensureCapacityInternal(size + 1);
//使用下标赋值,尾部插入
elementData[size++] = e;
return true;
}
//计算容量+确保容量
private void ensureCapacityInternal(int minCapacity) {
//用于并发判断
modCount++;
//如果是初次扩容,则使用默认的容量
if (elementData == EMPTY_ELEMENT_DATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//是否需要扩容,需要的最少容量大于现在数组的长度则要扩容
if (minCapacity - elementData.length > 0) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新容量 < 最小容量, 则讲最新的容量赋值给新的容量
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
//创建新数组
Object[] objects = new Object[newCapacity];
//将旧的数组复制到新的数组里面
System.arraycopy(elementData, 0, objects, 0, elementData.length);
//修改引用
elementData = objects;
}
}
/**
* 通过下标获取对象
*
* @param index
* @return
*/
public Object get(int index) {
rangeCheck(index);
return elementData[index];
}
private void rangeCheck(int index) {
if (index > size || size < 0) {
throw new IndexOutOfBoundsException("数组越界");
}
}
/**
* 判断对象所在的位置
*
* @param o
* @return
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++) {
if (elementData[i] == null) {
return i;
}
}
} else {
for (int i = 0; i < size; i++) {
if (o.equals(elementData[i])) {
return i;
}
}
}
return -1;
}
public Object set(int index, Object obj) {
rangeCheck(index);
Object oldValue = elementData[index];
elementData[index] = obj;
return oldValue;
}
/**
* 根据索引删除元素
*
* @param index
* @return
*/
public Object remove(int index) {
rangeCheck(index);
//用于并发判断
modCount++;
Object oldValue = elementData[index];
//计算要删除的位置后面有几个元素
int numMoved = size - index - 1;
if (numMoved > 0) {
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
}
//将多出的位置为空,没有引用对象,垃圾收集器可以回收,如果不为空,将会保存一个引用,可能会造成内存泄露
elementData[--size] = null;
return oldValue;
}
//获取数组实际大小
public int size() {
return this.size;
}
}
HashMap
和Hashtable
的区别?
HashMap
:底层是基于数组+链表,非线程安全的,默认容量是16
、允许有空的健和值
Hashtable
:基于哈希表实现,线程安全的(加了synchronized
),默认容量是11
,不允许有null
的健和值
HashMap
和TreeMap
应该怎么选择,使用场景?
hashMap
: 散列桶(数组+链表),可以实现快速的存储和检索,但是确实包含无序的元素,适用于在map
中插入删除和定位元素treeMap
: 使用存储结构是一个平衡二叉树->红黑树,可以自定义排序规则,要实现Comparator
接口
能便捷的实现内部元素的各种排序,但是一般性能比HashMap
差,适用于安装自然排序或者自定义排序规则
(写过微信支付签名工具类就用这个类)
Set
和Map
的关系?
核心就是不保存重复的元素,存储一组唯一的对象
set
的每一种实现都是对应Map
里面的一种封装,
HashSet
对应的就是HashMap
,treeSet
对应的就是treeMap
HashSet
和TreeSet
区别?
集合 | 数据结构 | 允许null | 有序 | 遍历方式 |
---|---|---|---|---|
HashSet | 数组+链表 | 允许 | 无 | 多钟 如Object[] object = (String[])set.toArray(); |
TreeSet | 平衡二叉树->红黑树 | 不允许 | 有 | 只能迭代器,如for(Iterator iter = set.iterator(); iter.hasNext();) { iter.next();} |
常见Map
的排序规则是怎样的?
按照添加顺序使用LinkedHashMap
,按照自然排序使用TreeMap
,自定义排序TreeMap(Comparetor c)
。
如果需要线程安全,且效率高的Map
,应该怎么做?
答案:多线程环境下可以用concurrent
包下的ConcurrentHashMap
, 或者使用Collections.synchronizedMap()
,
ConcurrentHashMap
虽然是线程安全,但是他的效率比Hashtable
要高很多
为什么Collections.synchronizedMap
后是线程安全的?
答案:使用Collections.synchronizedMap
包装后返回的map
是加锁的
为什么要序列化对象?serializable接口作用,解决什么问题?
网络数据传输都必须转化为二进制字节才能传输;
serializable
只是一个标记接口
,没有任何实现类;
看一个实现了serializzable
接口的对象,并对该对象进行存储。
public class Parent implements Serializable {
private static final long serialVersionUID = 1234L;
public Parent1(String name,int age){
this.name = name;
this.age = age;
}
private String name;
private int age;
public String toString(){
return "Parent:"+name+" "+age;
}
}
测试类
public class TestSeri {
public static void seri(Parent parent){
try {
FileOutputStream fo = new FileOutputStream("src/est.txt");
ObjectOutputStream oos = new ObjectOutputStream(fo);
oos.writeObject(parent);
oos.flush();
oos.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args){
Parent parent = new Parent("ouym",24);
seri(parent);
}
序列话的过程主要有三步:
(1)新建文件输出流用来指定一个位置存储序列化的内容。
(2)新建一个ObjectOutputStream对象oos
(3)序列化:调用writeObject(parent)方法。
关键看第三部writeObject的内部实现过程:
public final void writeObject(Object obj) throws IOException {
//省略部分代码
try {
// 调用writeObject0()方法序列化
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
可见writeObject方法的主要实现过程交给了writeObject0(obj,false)方法。
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
// 一些省略代码
try {
// 一些省略代码,其他的细节我们不深入
// remaining cases
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum) obj, desc, unshared);
} else if (obj instanceof Serializable) {
// 被序列化对象实现了Serializable接口
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
depth--;
bout.setBlockDataMode(oldMode);
}
}
从上述代码第8行开始,我们知道String,数组和枚举类型可以直接序列化(不难猜出String和Enum类实现了Serializbale接口)。若不是上述三种类型的话,接下来重点看14到16行代码,如果对象实现了Serializbale接口的话,就用writeOrdinaryObject()方法进行序列化操作。这里我们就不看writeOrdinaryObject方法的细节了,因为我们已经找到了标题的答案。Serializable接口这是一个标识,告诉程序所有实现了他的对象都可以进行序列化。
serialVersionUID用于控制序列化版本是否兼容?
序列化一个类
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
private String address;
}
并对该对象进行修改(第二个版本)
public class Person implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
private Integer age;
private String address;
// 新增gender
private String gender;
}
再次进行反序列化Person对象,则会抛出InvalidClassException
异常。
JDK为什么设计为一定要实现serializable接口才能序列化?
总的就是说安全性问题,具体原因见解释:假如没有一个接口(即没有Serializable来标记是否可以序列化),让所有对象都可以序列化。那么所有对象通过序列化存储到硬盘上后,都可以在序列化得到的文件中看到属性对应的值(后面将会通过代码展示)。所以最后为了安全性(即不让一些对象中私有属性的值被外露),不能让所有对象都可以序列化。要让用户自己来选择是否可以序列化,因此需要一个接口来标记该类是否可序列化。
比如一个反例:
所有对象默认都可以序列化,那么有些人就可以把这个对象序列化存储了,并可以获取到这个对象的一些属性值,这样是绝对不安全的。所以JDK设计为让用户自己背锅选择可序列化的对象。那如果Person
对象中age
属性不希望序列化可以使用static
或者transient
修饰变量。
JDK
源码解析
HashMap
1.7
-1.8
链表插入头插改为尾插
HashMap
在jdk1.7
中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8
中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
1.7
源码在扩容resize()
过程中,在将旧数组上的数据转移到新数组上时,转移数据操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况。
由于JDK 1.8
转移数据操作 = 按旧链表的正序遍历链表、在新链表的尾部依次插入,所以不会出现链表逆序、倒置的情况,故不容易出现环形链表的情况。
参考: https://juejin.im/post/5aa5d8d26fb9a028d2079264
扩容机制
扩容
阈值是0.75
。
创建一个新的Entry
空数组,长度是原数组的2
倍。
ReHash
重哈希
重新分配Node
元素的Hash
值,根据Hash
值分配到不同的数组下标内。
看过HashMap
源码吗,介绍下你了解的HashMap
?
HashMap
底层(数组+链表+红黑树 jdk8
才有红黑树)
数组中每一项是一个链表,即数组和链表的结合体
Node<K,V>[] table
是数组,数组的元素是Entry
(Node
继承Entry
),Entry
元素是一个key-value
的键值对,它持有一个指向下个Entry
的引用,table
数组的每个Entry
元素同时也作为当前Entry
链表的首节点,也指向了该链表的下个Entry
元素
在JDK1.8
中,链表的长度大于8
,链表会转换成红黑树,但是在转化前,会再判断一次当前数组的长度,只有数组长度大于64
才处理。否则,进行扩容操作。此处先提到这,后续会有详细的讲解。
能否解释下什么是Hash
碰撞?常见的解决办法有哪些,hashmap
采用哪种方法?
Hash
碰撞是指根据key
获取到相同的hash
值,需要放到同一个bucket
中
- 解决方法
- 链表法
- 重哈希
- 开发地址发
你说HashMap
底层是 数组+链表+红黑树,为什么要用这几类结构呢?
数组Node<K,V>[] table
,根据对象的key
的hash
值进行在数组里面是哪个节点
链表的作用是解决hash
冲突,将hash
值一样的对象存在一个链表放在hash
值对应的槽位
红黑树JDK8
使用红黑树来替代超过8
个节点的链表,主要是查询性能的提升,从原来的O(n)
到O(logn)
,
通过hash
碰撞,让HashMap
不断产生碰撞,那么相同的key
的位置的链表就会不断增长,当对这个Hashmap
的相应位置进行查询的时候,就会循环遍历这个超级大的链表,性能就会下降,所以改用红黑树。
为啥选择红黑树而不用其他树,比如二叉查找树,为啥不一直开始就用红黑树,而是到8
的长度后才变换?
二叉查找树在特殊情况下也会变成一条线性结构,和原先的链表存在一样的深度遍历问题,查找性能就会慢,
使用红黑树主要是提升查找数据的速度,红黑树是平衡二叉树的一种,插入新数据后会通过左旋,右旋、变色等操作来保持平衡,解决单链表查询深度的问题
数据量少的时候操作数据,遍历线性表比红黑树所消耗的资源少,且前期数据少平衡二叉树保持平衡是需要消耗资源的,所以前期采用线性表,等到一定数之后变换到红黑树。
是大佬折中衡量的结果,经验积累值,就像loadFactor
默认值0.75
一样。
说下hashmap
的put
和get
的核心逻辑(JDK8
以上版本)?
put
核心流程
- get核心流程
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//获取首节点,hash碰撞概率小,通常链表第一个节点就是值,没必要去循环遍历,处于效率
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果不止一个节点,就需要循环遍历,存在多个hash碰撞
if ((e = first.next) != null) {
//判断是否是红黑树,如果是则调用树的查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//链表结构,则循环遍历获取节点
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
了解ConcurrentHashMap
吗?为什么性能比hashtable
高,说下原理?
ConcurrentHashMap
线程安全的Map
, hashtable
类基本上所有的方法都是采用synchronized
进行线程安全控制,高并发情况下效率就降低。
ConcurrentHashMap
是采用了分段锁的思想提高性能,锁粒度更细化。
jdk1.7
和jdk1.8
里面ConcurrentHashMap
实现的区别有没了解?
JDK8
之前,ConcurrentHashMap
使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment
类,这个类继承ReentrantLock
来保证线程安全
技术点:Segment+HashEntry
JKD8
的版本取消Segment
这个分段锁数据结构,底层也是使用Node
数组+链表+红黑树,从而实现对每一段数据进行加锁,也减少了并发冲突的概率,CAS
(读)+Synchronized
(写)
技术点:Node+Cas+Synchronized
说下ConcurrentHashMap
的put
的核心逻辑(JDK8
以上版本)?
spread(key.hashCode())
重哈希,减少碰撞概率
tabAt(i)
获取table
中索引为i
的Node
元素
casTabAt(i)
利用CAS
操作获取table
中索引为i
的Node
元素
put
的核心流程
1、key
进行重哈希spread(key.hashCode())
2、对当前table
进行无条件循环
3、如果没有初始化table
,则用initTable
进行初始化
4、如果没有hash
冲突,则直接用cas
插入新节点,成功后则直接判断是否需要扩容,然后结束
5、(fh = f.hash) == MOVED
如果是这个状态则是扩容操作,先进行扩容
6、存在hash
冲突,利用synchronized (f)
加锁保证线程安全
7、如果是链表,则直接遍历插入,如果数量大于8
,则需要转换成红黑树
8、如果是红黑树则按照红黑树规则插入
9、最后是检查是否需要扩容addCount()
推荐资料:
并发编程
能否解释下什么是进程、线程、协程,他们之间的关系是怎样的?
进程: 本质上是一个独立执行的程序,进程是操作系统进行资源分配和调度的基本概念,操作系统进行资源分配和调度的一个独立单位
线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程执行不同的任务,切换受系统控制。
协程: 又称为微线程,是一种用户态的轻量级线程,协程不像线程和进程需要进行系统内核上的上下文切换,协程的上下文切换是由用户自己决定的,有自己的上下文,所以说是轻量级的线程,也称之为用户级别的线程就叫协程,一个线程可以多个协程,线程进程都是同步机制,而协程则是异步
Java
的原生语法中并没有实现协程,目前python
、Lua
和GO
等语言支持
关系:一个进程可以有多个线程,它允许计算机同时运行两个或多个程序。线程是进程的最小执行单位,CPU
的调度切换的是进程和线程,进程和线程多了之后调度会消耗大量的CPU
,CPU
上真正运行的是线程,线程可以对应多个协程。
协程对于多线程有什么优缺点吗?
- 优点:
非常快速的上下文切换,不用系统内核的上下文切换,减小开销
单线程即可实现高并发,单核CPU
可以支持上万的协程
由于只有一个线程,也不存在同时写变量的冲突,在协程中控制共享资源不需要加锁 - 缺点:
协程无法利用多核资源,本质也是个单线程
协程需要和进程配合才能运行在多CPU
上
目前java
没成熟的第三方库,存在风险
调试debug
存在难度,不利于发现问题
说下并发和并行的区别,举些例子说下?
并发concurrency
:
一台处理器上同时处理任务, 这个同时实际上是交替处理多个任务,程序中可以同时拥有两个或者多个线程,当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU
运行时间划分成若干个时间段,再将时间段分配给各个线程执行
并行parallellism
:
多个CPU
上同时处理多个任务,一个CPU
执行一个进程时,另一个CPU
可以执行另一个进程,两个进程互不抢占CPU
资源,可以同时进行
并发指在一段时间内宏观上去处理多个任务。 并行指同一个时刻,多个任务确实真的同时运行。
例子:
并发是一心多用,听课和看电影,但是CPU
大脑只有一个,所以轮着来
并行:火影忍者中的影分身,有多个你出现,可以分别做不同的事情
一个项目经理A
和3
个程序B
C
D
的故事
单线程
并发:A
给B
讲完需求,B
自己去实现,期间A
继续给C
和D
讲,不用等待某个程序员去完成,期间项目经理没空闲下来
并行:直接找3
个项目经理分别分配给3
个程序员
多线程实现方式?
-
继承
Thread
继承
Thread
,重写里面run
方法,创建实例,执行start
优点:代码编写最简单直接操作
缺点:没返回值,继承一个类后,没法继承其他的类,拓展性差public class ThreadDemo1 extends Thread { @Override public void run() { System.out.println("继承Thread实现多线程,名称:"+Thread.currentThread().getName()); } } public static void main(String[] args) { ThreadDemo1 threadDemo1 = new ThreadDemo1(); threadDemo1.setName("demo1"); threadDemo1.start(); System.out.println("主线程名称:"+Thread.currentThread().getName()); }
-
实现
Runnable
自定义类实现
Runnable
,实现里面run
方法,创建Thread
类,使用Runnable
接口的实现对象作为参数传递给Thread
对象,调用Strat
方法优点:线程类可以实现多个几接口,可以再继承一个类
缺点:没返回值,不能直接启动,需要通过构造一个Thread
实例传递进去启动public class ThreadDemo2 implements Runnable { @Override public void run() { System.out.println("通过Runnable实现多线程,名称:"+Thread.currentThread().getName()); } } public static void main(String[] args) { ThreadDemo2 threadDemo2 = new ThreadDemo2(); Thread thread = new Thread(threadDemo2); thread.setName("demo2"); thread.start(); System.out.println("主线程名称:"+Thread.currentThread().getName()); } // JDK8之后采用lambda表达式 public static void main(String[] args) { Thread thread = new Thread(()->{ System.out.println("通过Runnable实现多线程,名称:"+Thread.currentThread().getName()); }); thread.setName("demo2"); thread.start(); System.out.println("主线程名称:"+Thread.currentThread().getName()); }
-
通过
Callable
和FutureTask
方式创建
callable
接口的实现类,并实现call
方法,结合FutureTask
类包装Callable
对象,实现多线程
优点:有返回值,拓展性也高
缺点:jdk5
以后才支持,需要重写call
方法,结合多个类比如FutureTask
和Thread
类public class MyTask implements Callable<Object> { @Override public Object call() throws Exception { System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName()); return "这是返回值"; } } public static void main(String[] args) { FutureTask<Object> futureTask = new FutureTask<>(()->{ System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName()); return "这是返回值"; }); // MyTask myTask = new MyTask(); // FutureTask<Object> futureTask = new FutureTask<>(myTask); //FutureTask继承了Runnable,可以放在Thread中启动执行 Thread thread = new Thread(futureTask); thread.setName("demo3"); thread.start(); System.out.println("主线程名称:"+Thread.currentThread().getName()); try { System.out.println(futureTask.get()); } catch (InterruptedException e) { //阻塞等待中被中断,则抛出 e.printStackTrace(); } catch (ExecutionException e) { //执行过程发送异常被抛出 e.printStackTrace(); } }
-
通过线程池创建线程
自定义
Runnable
接口,实现run
方法,创建线程池,调用执行方法并传入对象
优点:安全高性能,复用线程
缺点:jdk5
后才支持,需要结合Runnable
进行使用public class ThreadDemo4 implements Runnable { @Override public void run() { System.out.println("通过线程池+runnable实现多线程,名称:"+Thread.currentThread().getName()); } } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3); for(int i=0;i<10;i++){ executorService.execute(new ThreadDemo4()); } System.out.println("主线程名称:"+Thread.currentThread().getName()); //关闭线程池 executorService.shutdown(); }
-
一般常用的
Runnable
和第四种线程池+Runnable
,简单方便扩展,和高性能 (池化的思想)
java
线程常见的基本状态有哪些,这些状态分别是做什么的?
JDK
的线程状态分6
种,JVM
里面9
种,我们一般说JDK
的线程状态
常见的6
种状态
-
创建(
NEW
): 生成线程对象,但是并没有调用该对象start()
,new Thread()
-
就绪(
Runnable
):当调用线程对象的start()
方法,线程就进入就绪状态,但是此刻线程调度还没把该线程设置为当前线程,就是没获得CPU
使用权。 如果线程运行后,从等待或者睡眠中回来之后,也会进入就绪状态
注意:有些文档把就绪和运行两种状态统一称为 “运行中” -
运行(
Running
)
程序将处于就绪状态的线程设置为当前线程,即获得CPU
使用权,这个时候线程进入运行状态,开始运行run
里面的逻辑 -
阻塞(
Blocked
)
等待阻塞:进入该状态的线程需要等待其他线程作出一定动作(通知或中断),这种状态的话CPU
不会分配过来,他们需要被唤醒,可能也会无限等待下去。比如调用wait
(状态就会变成WAITING
状态),也可能通过调用sleep
(状态就会变成TIMED_WAITING
),join
或者发出IO
请求,阻塞结束后线程重新进入就绪状态同步阻塞:线程在获取
synchronized
同步锁失败,即锁被其他线程占用,它就会进入同步阻塞状态备注:相关资料会用细分下面的状态
等待(WAITING
):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。 -
超时等待(
TIMED_WAITING
):该状态不同于WAITING
,它可以在指定的时间后自行返回 -
死亡(
TERMINATED
):一个线程run
方法执行结束,该线程就死亡了,不能进入就绪状态
是否了解多线程开发里面常用的方法,sleep
/yield
/join
/wait
/notify
/notifyAll
, 分别解释下?
-
sleep
属于线程Thread
的方法
让线程暂缓执行,等待预计时间之后再恢复
交出CPU
使用权,不会释放锁
进入阻塞状态TIME_WAITGING
,睡眠结束变为就绪Runnable
-
yield
属于线程Thread
的方法
t1/t2/t3
暂停当前线程的对象,去执行其他线程
交出CPU
使用权,不会释放锁,和sleep
类似
作用:让相同优先级的线程轮流执行,但是不保证一定轮流
注意:不会让线程进入阻塞状态,直接变为就绪Runnable
,只需要重新获得CPU
使用权 -
join
属于线程Thread
的方法
在主线程上运行调用该方法,会让主线程休眠,不会释放已经持有的对象锁
让调用join
方法的线程先执行完毕,在执行其他线程
类似让救护车警车优先通过 -
wait
属于Object
的方法
当前线程调用对象的wait
方法,会释放锁,进入线程的等待队列
需要依靠notify
或者notifyAll
唤醒,或者wait(timeout)
时间自动唤醒 -
notify
属于Object
的方法
唤醒在对象监视器上等待的单个线程,选择是任意的 -
notifyAll
属于Object
的方法
唤醒在对象监视器上等待的全部线程
画下线程的状态转换图和这些转换过程常用的api
也标记下?
平时业务代码里面使用过多线程吗,能举例几个多线程的业务场景吗?
- 异步任务:用户注册、记录日志,异步处理业务逻辑和写入日志
- 定时任务:定期备份日志、备份数据库
- 分布式计算:
Hadoop
处理任务mapreduce
,master-wark
(单机单进程) - 服务器编程:
Socket
网络编程,一个连接一个线程
在Java
中可以有哪些方法来保证线程安全?
- 加锁,比如
synchronize
/ReentrantLock
- 使用
volatile
声明变量,轻量级同步,不能保证原子性(需要解释) - 使用线程安全类(原子类
AtomicXXX
,并发容器,同步容器CopyOnWriteArrayList
/ConcurrentHashMap
等 ThreadLocal
本地私有变量/信号量Semaphore
等
了解volatile
关键字不?能否解释下,然后这和synchronized
有什么大的区别?
volatile
是轻量级的synchronized
,保证了共享变量的可见性,被volatile
关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象
volatile
:保证可见性,但是不能保证原子性
synchronized
:保证可见性,也保证原子性
使用场景
1、不能修饰写入操作依赖当前值的变量,比如num++
、num=num+1
,不是原子操作,肉眼看起来是,但是JVM
字节码层面不止一步
2、由于禁止了指令重排,所以JVM
相关的优化没了,效率会偏弱
并发编程三要素是否知道,能否分别解释下,举个简单的例子?
-
原子性
一个不可再被分割的颗粒,原子性指的是一个或多个操作要么全部执行成功要么全部执行失败,期间不能被中断,也不存在上下文切换,线程切换会带来原子性的问题
int num = 1; // 原子操作 num++; // 非原子操作,从主内存读取num到线程工作内存,进行 +1,再把num写到主内存, 除非用原子类,即java.util.concurrent.atomic里的原子变量类
解决办法是可以用
synchronized
或Lock
(比如ReentrantLock
) 来把这个多步操作“变成”原子操作,但是volatile
,前面有说到不能修饰有依赖值的情况public class XdTest { private int num = 0; //使用lock,每个对象都是有锁,只有获得这个锁才可以进行对应的操作 Lock lock = new ReentrantLock(); public void add1(){ lock.lock(); try { num++; }finally { lock.unlock(); } } //使用synchronized,和上述是一个操作,这个是保证方法被锁住而已,上述的是代码块被锁住 public synchronized void add2(){ num++; } }
解决核心思想:把一个方法或者代码块看做一个整体,保证是一个不可分割的整体
-
有序性
程序执行的顺序按照代码的先后顺序执行,因为处理器可能会对指令进行重排序
JVM
在编译java
代码或者CPU
执行JVM
字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)int a = 3 //1 int b = 4 //2 int c =5 //3 int h = a*b*c //4
上面的例子 执行顺序
1,2,3,4
和2,1,3,4
结果都是一样,指令重排序可以提高执行效率,但是多线程上可能会影响结果假如下面的场景,正常是顺序处理
// 线程1 before(); // 处理初始化工作,处理完成后才可以正式运行下面的`run`方法 flag = true; // 标记资源处理好了,如果资源没处理好,此时程序就可能出现问题 // 线程2 while(flag){ run(); //核心业务代码 }
指令重排序后,导致顺序换了,程序出现问题,且难排查
//线程1 flag = true; //标记资源处理好了,如果资源没处理好,此时程序就可能出现问题 //线程2 while(flag){ run(); //核心业务代码 } before();//处理初始化工作,处理完成后才可以正式运行下面的run方法
-
可见性
一个线程
A
对共享变量的修改,另一个线程B
能够立刻看到// 线程 A 执行 int num = 0; // 线程 A 执行 num++; // 线程 B 执行 System.out.print("num的值:" + num);
线程
A
执行i++
后再执行线程B
,线程B
可能有2
个结果,可能是0
和1
。因为
i++
在线程A
中执行运算,并没有立刻更新到主内存当中,而线程B
就去主内存当中读取并打印,此时打印的就是0
;也可能线程A
执行完成更新到主内存了,线程B
的值是1
。
所以需要保证线程的可见性
synchronized
、lock
和volatile
能够保证线程可见性
调度算法
进程调度算法?
先来先服务调度算法:
按照作业/进程到达的先后顺序进行调度 ,即:优先考虑在系统中等待时间最长的作业
排在长进程后的短进程的等待时间长,不利于短作业/进程
短作业优先调度算法:
短进程/作业(要求服务时间最短)在实际情况中占有很大比例,为了使得它们优先执行
对长作业不友好
高响应比优先调度算法:
在每次调度时,先计算各个作业的优先权:优先权=响应比=(等待时间+要求服务时间)/要求服务时间,
因为等待时间与服务时间之和就是系统对该作业的响应时间,所以 优先权=响应比=响应时间/要求服务时间,选 择优先权高的进行服务需要计算优先权信息,增加了系统的开销
时间片轮转调度算法:
轮流的为各个进程服务,让每个进程在一定时间间隔内都可以得到响应
由于高频率的进程切换,会增加了开销,且不区分任务的紧急程度
优先级调度算法:
根据任务的紧急程度进行调度,高优先级的先处理,低优先级的慢处理
如果高优先级任务很多且持续产生,那低优先级的就可能很慢才被处理
线程调度算法?
线程调度是指系统为线程分配CPU
使用权的过程,主要分两种
协同式线程调度(分时调度模式):线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里
抢占式线程调度:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java
中,Thread.yield()
可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞
Java
线程调度就是抢占式调度,优先让可运行池中优先级高的线程占用CPU
,如果可运行池中的线程优先级相同,那就随机选择一个线程
所以我们如果希望某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。
JAVA
的线程的优先级,以1
到10
的整数指定。当多个线程可以运行时,VM
一般会运行最高优先级的线程(Thread.MIN_PRIORITY
至Thread.MAX_PRIORITY
)
在两线程同时处于就绪runnable
状态时,优先级越高的线程越容易被系统选择执行。但是优先级并不是100%
可以获得,只不过是机会更大而已。
有人会说,wait,notify
不就是线程本身控制吗?
其实不是,wait
是可以让出执行时间,notify
后无法获取执行时间,随机等待队列里面获取而已
你日常开发里面用过java
里面有哪些锁?分别解释下?
-
悲观锁
当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞,比如
synchronized
-
乐观锁
每次去拿数据的时候都认为别人不会修改,更新的时候会判断是别人是否回去更新数据,通过版本来判断,如果数据被修改了就拒绝更新,比如CAS
是乐观锁,但严格来说并不是锁,通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现,CAS
不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响小结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会比悲观锁多
-
公平锁
指多个线程按照申请锁的顺序来获取锁,简单来说 如果一个线程组里,能保证每个线程都能拿到锁 比如
ReentrantLock
(底层是同步队列FIFO:First Input First Output
来实现) -
非公平锁
获取锁的方式是随机获取的,保证不了每个线程都能拿到锁,也就是存在有线程饿死,一直拿不到锁,比如synchronized
、ReentrantLock
小结:非公平锁性能高于公平锁,更能重复利用
CPU
的时间 -
可重入锁
也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁
-
不可重入锁
若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
小结:可重入锁能一定程度的避免死锁
synchronized
、ReentrantLock
重入锁private void meathA(){ //获取锁 TODO meathB(); } private void meathB(){ //获取锁 TODO //其他操作 }
-
自旋锁
一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁。
小结:不会发生线程状态的切换,一直处于用户态,减少了线程上下文切换的消耗,缺点是循环会消耗
CPU
常见的自旋锁:
TicketLock
,CLHLock
,MSCLock
你日常开发里面用过java
里面有哪些锁?分别解释下?
-
共享锁
也叫
S
锁/读锁,能查看但无法修改和删除的一种数据锁,加锁后其它用户可以并发读取、查询数据,但不能修改,增加,删除数据,该锁可被多个线程所持有,用于资源数据共享 -
互斥锁
也叫
X
锁/排它锁/写锁/独占锁/独享锁/ 该锁每一次只能被一个线程所持有,加锁后任何线程试图再次加锁的线程会被阻塞,直到当前线程解锁。例子:如果 线程A
对data1
加上排他锁后,则其他线程不能再对data1
加任何类型的锁,获得互斥锁的线程即能读数据又能修改数据 -
死锁
两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法让程序进行下去
死锁的
4
个必要条件- 互斥条件:资源不能共享,只能由一个线程使用,进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源。
- 请求与保持条件:线程已经获得一些资源,但因请求其他资源发生阻塞,对已经获得的资源保持不释放
- 不可抢占:有些资源是不可强占的,当某个线程获得这个资源后,系统不能强行回收,只能由线程使用完自己释放
- 循环等待条件:多个线程形成环形链,每个都占用对方申请的下个资源
只要发生死锁,上面的条件都成立;只要一个不满足,就不会发生死锁
-
Synchronized
锁的升级
下面三种是Jvm
为了提高锁的获取与释放效率而做的优化 针对Synchronized
的锁升级,锁的状态是通过对象监视器在对象头中的字段来表明,是不可逆的过程,无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)- 偏向锁:一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,获取锁的代价更低。
- 轻量级锁:当锁是偏向锁的时候,被其他线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,但不会阻塞,且性能会高点。
- 重量级锁:当锁为轻量级锁的时候,其他线程虽然是自旋,但自旋不会一直循环下去,当自旋一定次数的时候且还没有获取到锁,就会进入阻塞,该锁升级为重量级锁,重量级锁会让其他申请的线程进入阻塞,性能也会降低。
-
分段锁
-
行锁
-
表锁
上机实战:写个多线程死锁的例子?
线程在获得了锁A
并且没有释放的情况下去申请锁B
,
这时另一个线程已经获得了锁B
,在释放锁B之前又要先获得锁A
因此闭环发生,陷入死锁循环
/**
* 小滴课堂
* 面试专题第一季
*
*/
public class DeadLockDemo {
private static String locka = "locka";
private static String lockb = "lockb";
public void methodA(){
synchronized (locka){
System.out.println("我是A方法中获得了锁A "+Thread.currentThread().getName() );
//让出CPU执行权,不释放锁
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(lockb){
System.out.println("我是A方法中获得了锁B "+Thread.currentThread().getName() );
}
}
}
public void methodB(){
synchronized (lockb){
System.out.println("我是B方法中获得了锁B "+Thread.currentThread().getName() );
//让出CPU执行权,不释放锁
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(locka){
System.out.println("我是B方法中获得了锁A "+Thread.currentThread().getName() );
}
}
}
public static void main(String [] args){
System.out.println("主线程运行开始运行:"+Thread.currentThread().getName());
DeadLockDemo deadLockDemo = new DeadLockDemo();
new Thread(()->{
deadLockDemo.methodA();
}).start();
new Thread(()->{
deadLockDemo.methodB();
}).start();
System.out.println("主线程运行结束:"+Thread.currentThread().getName());
}
}
那上面的例子怎么解决死锁,优化下代码?
- 常见的解决办法
- 调整申请锁的范围
- 调整申请锁的顺序
public class FixDeadLockDemo {
private static String locka = "locka";
private static String lockb = "lockb";
public void methodA(){
synchronized (locka){
System.out.println("我是A方法中获得了锁A "+Thread.currentThread().getName() );
//让出CPU执行权,不释放锁
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized(lockb){
System.out.println("我是A方法中获得了锁B "+Thread.currentThread().getName() );
}
}
public void methodB(){
synchronized (lockb){
System.out.println("我是B方法中获得了锁B "+Thread.currentThread().getName() );
//让出CPU执行权,不释放锁
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized(locka){
System.out.println("我是B方法中获得了锁A "+Thread.currentThread().getName() );
}
}
public static void main(String [] args){
System.out.println("主线程运行开始运行:"+Thread.currentThread().getName());
FixDeadLockDemo deadLockDemo = new FixDeadLockDemo();
for(int i=0; i<10;i++){
new Thread(()->{
deadLockDemo.methodA();
}).start();
new Thread(()->{
deadLockDemo.methodB();
}).start();
}
System.out.println("主线程运行结束:"+Thread.currentThread().getName());
}
}
上机实战:设计一个简单的不可重入锁?
// 不可重入锁:若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
private void methodA(){
//获取锁 TODO
methodB();
}
private void methodB(){
//获取锁 TODO
//其他操作
}
/**
* 不可重入锁 简单例子
* 不可重入锁:若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
*/
public class UnreentrantLock {
private boolean isLocked = false;
public synchronized void lock() throws InterruptedException {
System.out.println("进入lock加锁 "+Thread.currentThread().getName());
//判断是否已经被锁,如果被锁则当前请求的线程进行等待
while (isLocked){
System.out.println("进入wait等待 "+Thread.currentThread().getName());
wait();
}
//进行加锁
isLocked = true;
}
public synchronized void unlock(){
System.out.println("进入unlock解锁 "+Thread.currentThread().getName());
isLocked = false;
//唤醒对象锁池里面的一个线程
notify();
}
}
public class Main {
private UnreentrantLock unreentrantLock = new UnreentrantLock();
//加锁建议在try里面,解锁建议在finally
public void methodA(){
try {
unreentrantLock.lock();
System.out.println("methodA方法被调用");
methodB();
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
unreentrantLock.unlock();
}
}
public void methodB(){
try {
unreentrantLock.lock();
System.out.println("methodB方法被调用");
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
unreentrantLock.unlock();
}
}
public static void main(String [] args){
//演示的是同个线程
new Main().methodA();
}
}
// 同一个线程,重复获取锁失败,形成死锁,这个就是不可重入锁
上机实战:设计一个简单的可重入锁?
可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁
/**
* 可重入锁 简单例子
* 不可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁
*/
public class ReentrantLock {
private boolean isLocked = false;
//用于记录是不是重入的线程
private Thread lockedOwner = null;
//累计加锁次数,加锁一次累加1,解锁一次减少1
private int lockedCount = 0;
public synchronized void lock() throws InterruptedException {
System.out.println("进入lock加锁 "+Thread.currentThread().getName());
Thread thread = Thread.currentThread();
//判断是否是同个线程获取锁, 引用地址的比较
while (isLocked && lockedOwner != thread ){
System.out.println("进入wait等待 "+Thread.currentThread().getName());
System.out.println("当前锁状态 isLocked = "+isLocked);
System.out.println("当前count数量 lockedCount = "+lockedCount);
wait();
}
//进行加锁
isLocked = true;
lockedOwner = thread;
lockedCount++;
}
public synchronized void unlock(){
System.out.println("进入unlock解锁 "+Thread.currentThread().getName());
Thread thread = Thread.currentThread();
//线程A加的锁,只能由线程A解锁,其他线程B不能解锁
if(thread == this.lockedOwner){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
lockedOwner = null;
//唤醒对象锁池里面的一个线程
notify();
}
}
}
}
public class Main {
//private UnreentrantLock unreentrantLock = new UnreentrantLock();
private ReentrantLock reentrantLock = new ReentrantLock();
//加锁建议在try里面,解锁建议在finally
public void methodA(){
try {
reentrantLock.lock();
System.out.println("methodA方法被调用");
methodB();
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
reentrantLock.unlock();
}
}
public void methodB(){
try {
reentrantLock.lock();
System.out.println("methodB方法被调用");
}catch (InterruptedException e){
e.fillInStackTrace();
} finally {
reentrantLock.unlock();
}
}
public static void main(String [] args){
for(int i=0 ;i<10;i++){
//演示的是同个线程
new Main().methodA();
}
}
}
对synchronized
了解不,能否介绍下你对synchronized
的理解?
synchronized
是解决线程安全的问题,常用在 同步普通方法、静态方法、代码块中;
synchronized
是非公平、可重入
每个对象有一个锁和一个等待队列,锁只能被一个线程持有,其他需要锁的线程需要阻塞等待。锁被释放后,对象会从队列中取出一个并唤醒,唤醒哪个线程是不确定的,不保证公平性
-
两种形式
-
方法
生成的字节码文件中会多一个ACC_SYNCHRONIZED
标志位,当一个线程访问方法时,会去检查是否存在ACC_SYNCHRONIZED
标识,如果存在,执行线程将先获取monitor
,获取成功之后才能执行方法体,方法执行完后再释放monitor
。在方法执行期间,其他任何线程都无法再获得同一个monitor
对象,也叫隐式同步 -
代码快
加了
synchronized
关键字的代码段,生成的字节码文件会多出monitorenter
和monitorexit
两条指令,每个monitor
维护着一个记录着拥有次数的计数器, 未被拥有的monitor
的该计数器为0
,当一个
线程获执行monitorenter
后,该计数器自增1
;当同一个线程执行monitorexit
指令的时候,计数器再自减1
。当计数器为0
的时候,monitor
将被释放.也叫显式同步
两种本质上没有区别,底层都是通过
monitor
来实现同步, 只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成 -
# 查看字节码
javac XXX.java
javap -v XXX.class
- 同步方法字节码
- 同步代码块字节码
jdk1.6
后进行了优化,你知道哪些大的变化?
有得到锁的资源进入Block
状态,涉及到操作系统用户模式和内核模式的切换,代价比较高
jdk6
进行了优化,增加了从偏向锁到轻量级锁再到重量级锁的过渡,但是在最终转变为重量级锁之后,性能仍然较低
了解CAS
不,能否解释下什么是CAS
?
全称是Compare And Swap
,即比较再交换,是实现并发应用到的一种技术
底层通过Unsafe
类实现原子性操作操作包含三个操作数 —— 内存地址(V)
、预期原值(A)
和新值(B)
。
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 ,若果在第一轮循环中,a
线程获取地址里面的值被b线程修改了,那么a
线程需要自旋,到下次循环才有可能机会执行。
CAS
这个是属于乐观锁,性能较悲观锁有很大的提高
AtomicXXX
等原子类底层就是CAS
实现,一定程度比synchonized
好,因为后者是悲观锁
底层调用C++
写的代码,直接请求CPU
调用
CAS
会存在什么比较严重的问题?
1、自旋时间长CPU
利用率增加,CAS
里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu
资源会一直被占用
2、存在ABA
问题
能否解释下什么是ABA问题,怎么避免这个问题呢?
如果一个变量V
初次读取是A
值,并且在准备赋值的时候也是A
值,那就能说明A
值没有被修改过吗?其实是不能的,因为变量V
可能被其他线程改回A
值,结果就是会导致CAS
操作误认为从来没被修改过,从而赋值给V
给变量加一个版本号即可,在比较的时候不仅要比较当前变量的值 还需要比较当前变量的版本号。
在java5
中,已经提供了AtomicStampedReference
来解决问题,检查当前引用是否等于预期引用,其次检查当前标志是否等于预期标志,如果都相等就会以原子的方式将引用和标志都设置为新值
知道AQS
吗?能否介绍下,它的核心思想是什么?
AQS
的全称为(AbstractQueuedSynchronizer)
抽象队列同步器,这个类在java.util.concurrent.locks
包下面。它是一个Java
提高的底层同步工具类,比如CountDownLatch
、ReentrantLock
,Semaphore
,ReentrantReadWriteLock
,SynchronousQueue
,FutureTask
等等皆是基于AQS
的
只要搞懂了AQS
,那么J.U.C
中绝大部分的api
都能轻松掌握
简单来说包含:
- 一个
int
类型的变量state
(用于计数器,类似gc
的回收计数器)表示同步状态,并提供了一系列的CAS
操作来管理这个同步状态对象; - 一个是线程标记(当前线程是谁加锁的);
- 一个是阻塞队列(用于存放其他未拿到锁的线程);
例子:线程A
调用了lock()
方法,通过CAS
将state
赋值为1
,然后将该锁标记为线程A
加锁。如果线程A
还未释放锁时,线程B
来请求,会查询锁标记的状态,因为当前的锁标记为 线程A
,线程B
未能匹配上,所以线程B
会加入阻塞队列,直到线程A
触发了 unlock()
方法,这时线程B
才有机会去拿到锁,但是不一定肯定拿到
-
acquire(int arg)
源码讲解,好比加锁
lock
操作 -
tryAcquire()
尝试直接去获取资源,如果成功则直接返回,
AQS
里面未实现但没有定义成abstract
,因为独占模式下只用实现tryAcquire-tryRelease
,而共享模式下只用实现tryAcquireShared-tryReleaseShared
,类似设计模式里面的适配器模式 -
addWaiter()
根据不同模式将线程加入等待队列的尾部,有
Node.EXCLUSIVE
互斥模式、Node.SHARED
共享模式;如果队列不为空,则以通过compareAndSetTail
方法以CAS
将当前线程节点加入到等待队列的末尾。否则通过enq(node)
方法初始化一个等待队列 -
acquireQueued()
使线程在等待队列中获取资源,一直获取到资源后才返回,如果在等待过程中被中断,则返回
true
,否则返回false
-
release(int arg)
源码讲解 好比解锁
unlock
独占模式下线程释放指定量的资源,里面是根据tryRelease()
的返回值来判断该线程是否已经完成释放掉资源了;在自义定同步器在实现时,如果已经彻底释放资源(state=0)
,要返回true
,否则返回false
-
unparkSuccessor(Node node)
方法用于唤醒等待队列中下一个线程
你知道的AQS
有几种同步方式,实现同步器一般要覆盖哪些方法?
独占式: 比如ReentrantLock
共享式:比如Semaphore
存在组合:组合式的如ReentrantReadWriteLock
,AQS
为使用提供了底层支撑,使用者可以自由组装实现
- boolean tryAcquire(int arg)
- boolean tryRelease(int arg)
- int tryAcquireShared(int arg)
- boolean tryReleaseShared(int arg)
- boolean isHeldExclusively()
不需要全部实现,根据获取的锁的种类可以选择实现不同的方法,比如
实现支持独占锁的同步器应该实现tryAcquire
、 tryRelease
、isHeldExclusively
实现支持共享获取的同步器应该实现tryAcquireShared
、tryReleaseShared
、isHeldExclusively
java
里面的公平锁和非公平锁你知道多少,有没看过ReentrantLock
源码?
公平锁和非公平锁核心区别?
你可以说清ReentrantLock
和synchronized
的差别不?
ReentrantLock
和synchronized
使用的场景是什么,实现机制有什么不同?
ReentrantLock
和synchronized
都是独占锁
-
synchronized
1、是悲观锁会引起其他线程阻塞,
java
内置关键字,
2、无法判断是否获取锁的状态,锁可重入、不可中断、只能是非公平
3、加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单但显得不够灵活
4、一般并发场景使用足够、可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁
5、synchronized
操作的应该是对象头中mark word
,参考原先原理图片 -
ReentrantLock
1、是个
Lock
接口的实现类,是悲观锁,
2、可以判断是否获取到锁,可重入、可判断、可公平可不公平
3、需要手动加锁和解锁,且 解锁的操作尽量要放在finally
代码块中,保证线程正确释放锁
4、在复杂的并发场景中使用在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
5、创建的时候通过传进参数true
创建公平锁,如果传入的是false
或没传参数则创建的是非公平锁
6、底层不同是AQS
的state
和FIFO
队列来控制加锁
知道ReentrantReadWriteLock
吗?和ReentrantLock
有啥不同?
-
ReentrantReadWriteLock
1、读写锁接口
ReadWriteLock
接口的一个具体实现,实现了读写锁的分离,
2、支持公平和非公平,底层也是基于AQS
实现
3、允许从写锁降级为读锁
流程:先获取写锁,然后获取读锁,最后释放写锁;但不能从读锁升级到写锁
4、重入:读锁后还可以获取读锁;获取了写锁之后既可以再次获取写锁又可以获取读锁
核心:读锁是共享的,写锁是独占的。 读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,主要是提升了读写的性能 -
ReentrantLock
是独占锁且可重入的,相比
synchronized
而言功能更加丰富也更适合复杂的并发场景,但是也有弊端,假如有两个线程A/B
访问数据,加锁是为了防止线程A
在写数据, 线程B
在读数据造成的数据不一致; 但线程A
在读数据,线程C
也在读数据,读数据是不会改变数据没有必要加锁,但是还是加锁了,降低了程序的性能,所以就有了ReadWriteLock
读写锁接口 -
场景
读多写少,比如设计一个缓存组件 或 提高
Collection
的并发性
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}}
class RWDictionary {
private final Map<String, Data> m = new TreeMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}}
java队列
并发编程里面解决生产消费者模型你知道哪几种方式?
核心:要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据
常用的同步方法是采用信号或加锁机制
-
wait()
/notify()
方法 -
await()
/signal()
方法用
ReentrantLock
和Condition
实现等待/通知模型 -
Semaphore
信号量 -
BlockingQueue
阻塞队列ArrayBlockingQueue
LinkedBlockingQueue
put
方法用来向队尾存入元素,如果队列满,则阻塞
take
方法用来从队首取元素,如果队列为空,则阻塞
你知道阻塞队列BlockingQueue
不?介绍下常见的阻塞队列?
BlockingQueue
: j.u.c
包下的提供了线程安全的队列访问的接口,并发包下很多高级同步类的实现都是基于阻塞队列实现的
1、当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满
2、从阻塞队列读数据时,如果队列为空,线程将会阻塞等待直到队列里面是非空的时候
常见的阻塞队列
-
ArrayBlockingQueue
基于数组实现的一个阻塞队列,需要指定容量大小,
FIFO
先进先出顺序 -
LinkedBlockingQueue
基于链表实现的一个阻塞队列,如果不指定容量大小,默认
Integer.MAX_VALUE
,FIFO
先进先出顺序 -
PriorityBlockingQueue
一个支持优先级的无界阻塞队列,默认情况下元素采用自然顺序升序排序,也可以自定义排序实现
java.lang.Comparable
接口 -
DelayQueue
延迟队列,在指定时间才能获取队列元素的功能,队列头元素是最接近过期的元素,里面的对象必须实现
java.util.concurrent.Delayed
接口并实现CompareTo
和getDelay
方法
你知道非阻塞队列ConcurrentLinkedQueue
不,它怎么实现线程安全的?
线程安全原因:
ConcurrentLinkedQueue
是基于链表实现的无界线程安全队列,采用FIFO
进行排序
保证线程安全的三要素:原子、有序、可见性
1、底层结构是Node
,链表头部和尾部节点是head
和tail
,使用节点变量和内部类属性使用volatile
声明保证了有序和可见性
2、插入、移除、更新操作使用CAS
无锁操作,保证了原子性
3、假如多线程并发修改导致 CAS
更新失败,采用for
循环插入保证更新操作成功
平时多线程用的挺多的,写出3
条你遵循的多线程最佳实践?
-
给不同模块的线程起名称,方便后续排查问题
-
使用同步代码块或者同步的方法的时候,尽量减小同步范围
-
多用并发集合少用同步集合
同步集合:
Hashtable
/Vector
/同步工具类包装Collections.synXXX
并发集合:ConcurrentHashMap
、CopyOnWriteArrayList
-
线上业务需要使用多线程,优先考虑线程池是否更加合适,然后判断哪种线程池比较好,最后才是自己创建单一线程
用过线程池不? 有什么好处, java
里有哪些是常用的线程池?
好处:重用存在的线程,减少对象创建销毁的开销,有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,且可以定时定期执行、单线程、并发数控制,配置任务过多任务后的拒绝策略等功能
-
newFixedThreadPool
一个定长线程池,可控制线程最大并发数
- 特点
- 核心线程数和最大线程数大小一样
- 没有所谓的非空闲时间,即
keepAliveTime
为0
- 阻塞队列为无界队列
LinkedBlockingQueue
- 特点
-
newCachedThreadPool
一个可缓存线程池
-
线程池特点
- 核心线程数为0
- 最大线程数为
Integer.MAX_VALUE
- 阻塞队列是
SynchronousQueue
- 非核心线程空闲存活时间为
60
秒
-
工作机制
- 提交任务
- 因为没有核心线程,所以任务直接加到SynchronousQueue队列。
- 判断是否有空闲线程,如果有,就去取出任务执行。
- 如果没有空闲线程,就新建一个线程执行。
- 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。
-
newSingleThreadExecutor
一个单线程化的线程池,用唯一的工作线程来执行任务
-
-
newScheduledThreadPool
一个定长线程池,支持定时/周期性任务执行
线程池的5中状态?
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
线程池各个状态切换框架图:
【阿里巴巴编码规范】 线程池不允许使用 Executors
去创建,要通过 ThreadPoolExecutor
的方式原因?
Executors
创建的线程池底层也是调用 ThreadPoolExecutor
,只不过使用不同的参数、队列、拒绝策略等,如果使用不当,会造成资源耗尽问题;
直接使用ThreadPoolExecutor
让使用者更加清楚线程池允许规则,常见参数的使用,避免风险
常见的线程池问题:
-
newFixedThreadPool
和newSingleThreadExecutor
队列使用
LinkedBlockingQueue
,队列长度为Integer.MAX_VALUE
,可能造成堆积,导致OOM
-
newScheduledThreadPool
和newCachedThreadPool
线程池里面允许最大的线程数是
Integer.MAX_VALUE
,可能会创建过多线程,导致OOM
ThreadPoolExecutor
构造函数里面的参数你是否掌握,能否解释下各个参数的作用?
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
corePoolSize
核心线程数,线程池也会维护线程的最少数量,默认情况下核心线程会一直存活,即使没有任务也不会受存
keepAliveTime
控制
坑:在刚创建线程池时线程不会立即启动,到有任务提交时才开始创建线程并逐步线程数目达到corePoolSize
-
maximumPoolSize
线程池维护线程的最大数量,超过将被阻塞
坑:当核心线程满,且阻塞队列也满时,才会判断当前线程数是否小于最大线程数,才决定是否创建新线程 -
keepAliveTime
非核心线程的闲置超时时间,超过这个时间就会被回收,直到线程数量等于
corePoolSize
-
unit
指定
keepAliveTime
的单位,如TimeUnit.SECONDS
、TimeUnit.MILLISECONDS
-
workQueue
线程池中的任务队列,常用的是
ArrayBlockingQueue
、LinkedBlockingQueue
、SynchronousQueue
-
threadFactory
创建新线程时使用的工厂
-
handler
RejectedExecutionHandler
是一个接口且只有一个方法,线程池中的数量大于maximumPoolSize
,对拒绝任务的处理策略,默认有4种策略AbortPolicy
、CallerRunsPolicy
、DiscardOldestPolicy
、DiscardPolicy
JVM
为什么会出现脏读?
JAVA
内存模型简称JMM
JMM
规定所有的变量存在在主内存,每个线程有自己的工作内存,线程对变量的操作都在工作内存中进行,不能直接对主内存就行操作
使用volatile
修饰变量
每次读取前必须从主内存属性最新的值
每次写入需要立刻写到主内存中
volatile
关键字修修饰的变量随时看到的自己的最新值,假如线程1
对变量v
进行修改,那么线程2
是可以马上看见
你说volatile
可以避免指令重排,能否解释下什么是指令重排?
指令重排序分两类 编译器重排序和运行时重排序
JVM
在编译java
代码或者CPU
执行JVM
字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)
int a = 3 //1
int b = 4 //2
int c =5 //3
int h = a*b*c //4
定义顺序 1,2,3,4
计算顺序 1,3,2,4
和 2,1,3,4
结果都是一样
虽然指令重排序可以提高执行效率,但是多线程上可能会影响结果,有什么解决办法?
解决办法:内存屏障
解释:内存屏障是屏障指令,使CPU
对屏障指令之前和之后的内存操作执行结果的一种约束
知道happens-before
吗,能否简单解释下?
先行发生原则,volatile
的内存可见性就提现了该原则之一
例子:
//线程A操作
int k = 1;
//线程B操作
int j = k;
//线程C操作
int k = 2
分析:
假设线程A
中的操作k=1
先行发生于线程B
的操作j=k
,那确定在线程B
的操作执行后,变量j
的值一定等于1
,依据有两个:一是先行发生原则,k=1
的结果可以被观察到;二是第三者线程C
还没出现,线程A
操作结束之后没有其他线程会修改变量k
的值。
但是考虑线程C
出现了,保持线程A
和线程B
之间的先行发生关系,线程C
出现在线程A
和线程B
的操作之间,但是线程C
与线程B
没有先行发生关系,那j
的值会是多少?答案是1
和2
都有可能,因为线程C
对变量k
的影响可能会被线程B
观察到,也可能不会,所以线程B
就存在读取到不符合预期数据的风险,不具备多线程安全性
八大原则(对这个不理解,一定要去补充相关博文知识)
- 程序次序规则
- 管程锁定规则
volatile
变量规则- 线程启动规则
- 线程中断规则
- 线程终止规则
- 对象终结规则
- 传递性
堆内存中的数据是线程共享的吗?
首先了解下,JVM分配对象内存的过程:
线程中给对象分配内存,主要是对象的引用指向这个内存区域,然后进行初始化操作,可能有多个线程在堆上申请空间,对象的内存分配过程就必须进行同步控制。但是我们都知道,无论是使用哪种同步方案(实际上虚拟机使用的可能是CAS),都会影响内存的分配效率。
而Java对象的分配是Java中的高频操作,所有,人们想到另外一个办法来提升效率。这里我们重点说一个HotSpot虚拟机的方案:
每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块”私有”内存中分配,当这部分区域用完之后,再分配新的”私有”内存。
这种方案被称之为TLAB分配,即Thread Local Allocation Buffer。这部分Buffer是从堆中划分出来的,但是是本地线程独享的。
这里值得注意的是,我们说TLAB是线程独享的,但是只是在“分配”这个动作上是线程独占的,至于在读取、垃圾回收等动作上都是线程共享的。而且在使用上也没有什么区别。
所以,“堆是线程共享的内存区域”这句话并不完全正确,因为TLAB是堆内存的一部分,他在读取上确实是线程共享的,但是在内存分分配上,是线程独享的。
TLAB的空间其实并不大,所以大对象还是可能需要在堆内存中直接分配。那么,对象的内存分配步骤就是先尝试TLAB分配,空间不足之后,再判断是否应该直接进入老年代,然后再确定是再eden分配还是在老年代分配。
中间件消息队列
你用过消息队列,引入队列有啥优缺点,对比其他消息中间产品,选择这款的原因是啥?
-
优点:解耦系统、异步化、削峰
-
缺点: 系统可用性降低、复杂度增高、维护成本增高
-
主流消息队列
Apache ActiveMQ
、Kafka
、RabbitMQ
、RocketMQ
-
ActiveMQ
:http://activemq.apache.org/Apache
出品,历史悠久,支持多种语言的客户端和协议,支持多种语言Java
,.NET
,C++
等,基于JMS Provider
的实现- 缺点:吞吐量不高,多队列的时候性能下降,存在消息丢失的情况,比较少大规模使用
-
Kafka
:http://kafka.apache.org/- 是由
Apache
软件基金会开发的一个开源流处理平台,由Scala
和Java
编写。Kafka
是一种高吞吐量的分布式发布订阅消息系统,它可以处理大规模的网站中的所有动作流数据(网页浏览,搜索和其他用户的行动),副本集机制,实现数据冗余,保障数据尽量不丢失;支持多个生产者和消费者 - 缺点:不支持批量和广播消息,运维难度大,文档比较少, 需要掌握
Scala
- 是由
-
RabbitMQ
:http://www.rabbitmq.com/- 是一个开源的
AMQP
实现,服务器端用Erlang
语言编写,支持多种客户端,如:Python
、Ruby
、.NET
、Java
、JMS
、C
、用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不错 - 缺点:使用
Erlang
开发,阅读和修改源码难度大
- 是一个开源的
-
RocketMQ
:http://rocketmq.apache.org/- 阿里开源的一款的消息中间件, 纯
Java
开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点, 性能强劲(零拷贝技术),支持海量堆积, 支持指定次数和时间间隔的失败消息重发,支持consumer
端tag
过滤、延迟消息等,在阿里内部进行大规模使用,适合在电商,互联网金融等领域使用 - 缺点:成熟的资料相对不多,社区处于新生状态但是热度高
- 阿里开源的一款的消息中间件, 纯
消息队列的发送方式有哪几种,使用场景分别是怎样的?
发送方式一般分三种
-
SYNC
同步发送
应用场景:重要通知邮件、报名短信通知、营销短信系统等 -
ASYNC
异步发送
应用场景:对RT
时间敏感,可以支持更高的并发,回调成功触发相对应的业务,比如注册成功后通知积分系统发放优惠券 -
ONEWAY
无需要等待响应
应用场景:主要是日志收集,适用于某些耗时非常短,但对可靠性要求并不高的场景, 也就是LogServer
, 只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求 不等待应答
发送方式汇总对比
发送方式 | 发送TPS | 发送结果反馈 | 可靠性 |
---|---|---|---|
同步发送 | 快 | 有 | 不丢失 |
异步发送 | 快 | 有 | 不丢失 |
单向发送 | 最快 | 无 | 可能丢失 |
有没用过延迟消息,使用场景是怎样的?
什么是延迟消息:Producer
将消息发送到消息队列broker
服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer
进行消费
使用场景一:通过消息触发一些定时任务,比如在某一固定时间点向用户发送提醒消息
使用场景二:消息生产和消费有时间窗口要求,比如在天猫电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条 延时消息。这条消息将会在 30
分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略
如何保证消息队列里消息的生成和消费的顺序性
你用的队列是否支持顺序消息,是怎么实现顺序消息的?
什么是顺序消息:
消息的生产和消费顺序一致
全局顺序:topic
下面全部消息都要有序(少用),性能要求不高,所有的消息严格按照FIFO
原则进行消息发布和消费的 场景,并行度成为消息系统的瓶颈, 吞吐量不够
使用场景:在证券处理中,以人民币兑换美元为例子,在价格相同的情况下,先出价者优先处理,则可以通过全局顺序的方式按照 FIFO
的方式进行发布和消费
局部顺序:只要保证一组消息被顺序消费即可,性能要求高
使用场景:电商的订单创建,同一个订单相关的创建订单消息、订单支付消息、订单退款消息、订单物流消息、订单交易成功消息 都会按照先后顺序来发布和消费
(阿里巴巴集团内部电商系统均使用局部顺序消息,既保证业务的顺序,同时又能保证业务的高性能)
下面是用RocketMQ
举例(用kafka
或rabbitmq
类似)
一个topic
下面有多个queue
-
顺序发布
对于指定的一个
Topic
,客户端将按照一定的先后顺序发送消息
举例:订单的顺序流程是:创建、付款、物流、完成,订单号相同的消息会被先后发送到同一个队列中,
根据MessageQueueSelector
里面自定义策略,根据同个业务id
放置到同个queue
里面,如订单号取模运算再放到selector
中,同一个模的值都会投递到同一条queue
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { //如果是订单号是字符串,则进行hash,得到一个hash值 Long id = (Long) arg; long index = id % mqs.size(); return mqs.get((int)index); }
-
顺序消费
对于指定的一个
Topic
,按照一定的先后顺序接收消息,即先发送的消息一定会先被客户端接收到。
举例:消费端要在保证消费同个topic
里的同个队列,不应该用MessageListenerConcurrently
,
应该使用MessageListenerOrderly
,自带单线程消费消息,不能再Consumer
端再使用多线程去消费,消费端分配到的queue
数量是固定的,集群消费会锁住当前正在消费的队列集合的消息,所以会保证顺序消费。注意:
顺序消息暂不支持广播模式
顺序消息不支持异步发送方式,否则将无法严格保证顺序
不能再Consumer
端再使用多线程去消费
你的业务系统有没做消息的重复消费处理,是怎么做的?
-
幂等性:一个请求,不管重复来多少次,结果是不会改变的。
-
RabbitMQ
、RocketMQ
、Kafka
等任何队列不保证消息不重复,如果业务需要消息不重复消费,则需要消费端处理业务消息要保持幂等性-
方式一:
Redis
的setNX()
, 做消息id
去重java
版本目前不支持设置过期时间//Redis中操作,判断是否已经操作过 TODO boolean flag = jedis.setNX(key); if(flag){ //消费 }else{ //忽略,重复消费 }
-
方式二:
redis
的Incr
原子操作:key
自增,大于0
返回值大于0
则说明消费过,(key
可以是消息的md5
取值, 或者如果消息id
设计合理直接用id
做key
)int num = jedis.incr(key); if(num == 1){ //消费 }else{ //忽略,重复消费 }
-
方式三:数据库去重表
- 设计一个去重表,某个字段使用
Message
的key
做唯一索引,因为存在唯一索引,所以重复消费会失败
CREATE TABLE message_record ( id int(11) unsigned NOT NULL AUTO_INCREMENT, key varchar(128) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY key (key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 设计一个去重表,某个字段使用
-
消息队列常见问题之如何保证消费的可靠性传输?
消息可靠性传输,是非常重要,消息如果丢失,可能带来严重后果,一般从是个角度去分析
-
producer
端
不采用oneway
发送,使用同步或者异步方式发送,做好重试,但是重试的Message key
必须唯一
投递的日志需要保存,关键字段,投递时间、投递状态、重试次数、请求体、响应体 -
broker
端
多主多从架构,需要多机房
同步双写、异步刷盘 (同步刷盘则可靠性更高,但是性能差点,根据业务选择)
机器断电重启:异步刷盘,消息丢失;同步刷盘消息不丢失
硬件故障:可能存在丢失,看队列架构 -
consumer
端
消息队列一般都提供的ack
机制,发送者为了保证消息肯定消费成功,只有消费者明确表示消费成功,队列才会认为消息消费成功,中途断电、抛出异常等都不会认为成功——即都会重新投递,每次在确保处理完这个消息之后,在代码里调用ack
,告诉消息队列消费成功
消费端务必做好幂等性处理
消息消费务必保留日志,即消息的元数据和消息体,
消息队列常见问题之消息发生大量堆积应该怎么处理?
- 消息堆积了
10
小时,有几千万条消息待处理,现在怎么办? - 修复
consumer
, 然后慢慢消费?也需要几小时才可以消费完成,新的消息怎么办?
核心思想:紧急临时扩容,更快的速度去消费数据
-
修复
Consumer
不消费问题,使其恢复正常消费,根据业务需要看是否要暂停 -
临时
topic
队列扩容,并提高消费者能力,但是如果增加Consumer
数量,但是堆积的topic
里面的message queue
数量固定,过多的consumer
不能分配到message queue
-
编写临时处理分发程序,从旧
topic
快速读取到临时新topic
中,新topic
的queue
数量扩容多倍,然后再启动更多consumer
进行在临时新的topic
里消费 -
直到堆积的消息处理完成,再还原到正常的机器数量
MySQL数据库的面试题你遇过多少
常说的事务ACID是什么
你知道Mysql事务的四大特性不,简单说下?
事务的四大特性ACID
-
原子性Atomicity:
一个事务必须被事务不可分割的最小工作单元,整个操作要么全部成功,要么全部失败,一般就是通过commit和rollback来控制 -
一致性Consistency:
数据库总能从一个一致性的状态转换到另一个一致性的状态,比如小滴课堂下单支付成功后,开通视频播放权限,只要有任何一方发生异常就不会成功提交事务 -
隔离性Isolation:
一个事务相对于另一个事务是隔离的,一个事务所做的修改是在最终提交以前,对其他事务是不可见的 -
持久性Durability:
一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失
脏读-不可重复读-幻读你知道多少
能否简单解释下脏读、不可重复读、幻读的意思?
-
脏读
事务中的修改即使没有提交,其他事务也能看见,事务可以读到未提交的数据称为脏读
-
不可重复读
同个事务前后多次读取,不能读到相同的数据内容,中间另一个事务也操作了该同一数据
-
幻读
当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,发现两次不一样,产生幻读
幻读和不可重复读的区别是:前者是一个范围,后者是本身,从总的结果来看, 两者都表现为两次读取的结果不一致
常见的隔离级别由低到高有哪几种,mysql默认是哪种?
事务的隔离级别越高,事务越安全,但是并发能力越差。
-
Read Uncommitted(未提交读,读取未提交内容)
事务中的修改即使没有提交,其他事务也能看见,事务可以读到为提交的数据称为脏读
也存在不可重复读、幻读问题例子:
小滴课堂运营小姐姐配置了一个课程活动,原价500元的课程,配置成50元,但是事务没提交。
你刚好看到这个课程那么便宜准备购买,但是Anna小姐姐马上回滚了事务,重新配置并提交了事务,你准备下单的时候发现价格变回了500元 -
Read Committed(提交读,读取提交内容)
一个事务开始后只能看见已经提交的事务所做的修改,在事务中执行两次同样的查询可能得到不一样的结果,也叫做不可重复读(前后多次读取,不能读到相同的数据内容),也存幻读问题例子:
老王在小滴课堂有1000积分,准备去兑换《面试专题课程》,查询数据库确实有1000积分
但是老王的女友同时也在别的地方登录,把1000积分兑换了《SpringCloud微服务专题课程》,且在老王之前提交事务;当系统帮老王兑换《面试专题课程》是发现积分预计没了,兑换失败。老王事务A事先读取了数据,他女友事务B紧接了更新了数据且提交了事务,事务A再次读取该数据时,数据已经发生了改变
-
Repeatable Read(可重复读,mysql默认的事务隔离级别)
解决脏读、不可重复读的问题,存在幻读的问题,使用 MMVC机制 实现可重复读例子
老王在小滴课堂有1000积分,准备去兑换《面试专题课程》,查询数据库确实有1000积分
老王的女友同时也在别的地方登录先兑换了这个《面试专题课程》,老王的事务提交的时候发现存在了,之前读取的没用了,像是幻觉幻读问题:MySQL的InnoDB引擎通过MVCC自动帮我们解决,即多版本并发控制
-
Serializable(可串行化)
解决脏读、不可重复读、幻读,可保证事务安全,但强制所有事务串行执行,所以并发效率低
Mysql常见的存储引擎你知道不
说下你知道Mysql常见的存储引擎,新版Mysql默认是哪个?
常见的有多类,InnoDB、MyISAM、MEMORY、MERGE、ARCHIVE、CSV等
一般比较常用的有InnoDB、MyISAM
MySQL 5.5以上的版本默认是InnoDB,5.5之前默认存储引擎是MyISAM
存储引擎InnoDB、MyISAM异同点和选择
mysql的存储引擎 innodb和myisam有什么区别,应该怎么选择?
区别项 | Innodb | myisam |
---|---|---|
事务 | 支持 | 不支持 |
锁粒度 | 行锁,适合高并发 | 表锁,不适合高并发 |
是否默认 | 默认 | 非默认 |
支持外键 | 支持外键 | 不支持 |
适合场景 | 读写均衡,写大于读场景,需要事务 | 读多写少场景,不需要事务 |
全文索引 | MySQL5.6之前不支持,可以通过插件实现, 更多使用ElasticSearch | 支持全文索引 |
Mysql数据库索引你知道多少
mysql常用的功能索引有哪些?分别在什么场景下使用?创建语句是怎样的?
索引名称 | 特点 | 创建语句 |
---|---|---|
普通索引 | 最基本的索引,仅加速查询 | CREATE INDEX idx_name ON table_name(filed_name) |
唯一索引 | 加速查询,列值唯一,允许为空;组合索引则列值的组合必须唯一 | CREATE UNIQUE INDEX idx_name ON table_name(filed_name_1,filed_name_2) |
主键索引 | 加速查询,列值唯一,一个表只有1个,不允许有空值 | ALTER TABLE table_name ADD PRIMARY KEY ( filed_name ) |
组合索引 | 加速查询,多条件组合查询 | CREATE INDEX idx_name ON table_name(filed_name_1,filed_name_2); |
覆盖索引 | 索引包含所需要的值,不需要“回表”查询,比如查询,两个字段,刚好是 组合索引 的两个字段 | |
全文索引 | 对内容进行分词搜索,仅可用于Myisam, 更多用ElasticSearch做搜索 | ALTER TABLE table_name ADD FULLTEXT ( filed_name ) |
数据库索引的好处和坏处,你常用的最佳实践
你们线上数据量每天有多少新增,都是存储在mysql库吗,有没做优化?
中型公司或者业务发展好的公司,一天新增几百万数据量
业务核心数据存储在Mysql里面,针对业务创建合适的索引
打点数据、日志等存储在ElasticSearch或者MongoDB里面
你创建索引的时候主要考虑啥,使用索引的优缺点有哪些,使用应该注意些什么?
考虑点:结合实际的业务场景,在哪些字段上创建索引,创建什么类型的索引
索引好处:
快速定位到表的位置,减少服务器扫描的数据
有些索引存储了实际的值,特定情况下只要使用索引就能完成查询
索引缺点:
索引会浪费磁盘空间,不要创建非必要的索引
插入、更新、删除需要维护索引,带来额外的开销
索引过多,修改表的时候重构索引性能差
- 索引优化实践
- 前缀索引,特别是
TEXT
和BLOG
类型的字段,只检索前面几个字符,提高检索速度 - 尽量使用数据量少的索引,索引值过长查询速度会受到影响
- 选择合适的索引列顺序
- 内容变动少,且查询频繁,可以建立多几个索引
- 内容变动频繁,谨慎创建索引
- 根据业务创建适合的索引类型,比如某个字段常用来做查询条件,则为这个字段建立索引提高查询速度
- 组合索引选择业务查询最相关的字段
- 前缀索引,特别是
数据库设计查询和上线里面的坑你走过多少
数据库查询关键词执行顺序
数据库查询的指令有多个,说下执行顺序 select、where、from、group by、having、order by?
from 从哪个表查询
where 初步过滤条件
group by 过滤后进行分组[重点]
having 对分组后的数据进行二次过滤[重点]
select 查看哪些结果字段
order by 按照怎样的顺序进行排序返回[重点]
select video_id,count(id) num from chapter group by video_id having num >10
order by video_id desc
官方地址:https://www.percona.com/downloads/percona-toolkit/LATEST/
其他资料:https://www.cnblogs.com/zishengY/p/6852280.html
设计数据库表时相似字段类型你能区分吗《上》
varchar(len) char(len) len存储的是字符还是字节?MySQL中的varchar和char有什么区别,应该怎么选择?
对比项 | char(16) | varchar(16) |
---|---|---|
长度特点 | 长度固定,存储字符 | 长度可变,存储字符 |
长度不足情况 | 插入的长度小于定义长度时,则用空格填充 | 小于定义长度时,按实际插入长度存储 |
性能 | 存取速度比varchar快得多 | 存取速度比char慢得多 |
使用场景 | 适合存储很短的,固定长度的字符串,如手机号,MD5值等 | 适合用在长度不固定场景,如收货地址,邮箱地址等 |
2038年1月19号会有多少系统产生bug,相似字段类型区分《下》
MySQL中的datetime和timestamp有什么区别?
- 存储空间
类型 | 占据字节 | 范围 | 时区问题 |
---|---|---|---|
datetime | 8 字节 | 1000-01-01 00:00:00到 9999-12-31 23:59:59 | 存储与时区无关,不会发生改变 |
timestamp | 4 字节 | 1970-01-01 00:00:01 到 2038-01-19 11:14:07 | 存储的是与时区有关,随数据库的时区而发生改变 |
- 时间范围
可表示的时间范围不同。timestamp可表示范围:1970-01-01 00:00:00~2038-01-09 03:14:07,datetime支持的范围更宽1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
- 索引速度
索引速度不同。timestamp更轻量,索引相对datetime更快。
- 跨库问题
不同的数据库对时间类型有不同的解释,如Oracle中的date和mysql中的date就不能直接兼容转换为实现跨平台性,将时间记录为unix时间戳
为什么timestamp只能到2038年?
MySQL的timestamp类型是4个字节,最大值是2的31次方减1,结果是2147483647,
转换成北京时间就是2038-01-19 11:14:07
场景模拟之千万级Mysql数据表分页查询优化
线上数据库的一个商品表数据量过千万,做深度分页的时候性能很慢,有什么优化思路?
现象:千万级别数据很正常,比如数据流水、日志记录等,数据库正常的深度分页会很慢
慢的原因:
select * from product limit N,M
MySQL执行此类SQL时需要先扫描到N行,然后再去取M行,N越大,MySQL扫描的记录数越多,SQL的性能就会越差
-
后端、前端缓存
-
使用ElasticSearch分页搜索
-
合理使用 mysql 查询缓存,覆盖索引进行查询分页
select title,cateory from product limit 1000000,100
-
如果id是自增且不存在中间删除数据,使用子查询优化,定位偏移位置的 id
select * from oper_log where type='BUY' limit 1000000,100; //5.秒 select id from oper_log where type='BUY' limit 1000000,1; // 0.4秒 select * from oper_log where type='BUY' and id>=(select id from oper_log where type='BUY' limit 1000000,1) limit 100; //0.8秒
BAT大厂里面 应用版本更新,数据库上线流程
你公司里面产品迭代更新,开发好代码和数据库,上线流程是怎样的?
生产环境数据库性能监控和优化面试环节
生产环境的数据库,你会做哪些操作保证安全
针对线上的数据库,你会做哪些监控,业务性能 + 数据安全 角度分析?
大厂一般都有数据库监控后台,里面指标很多,但是开发人员也必须知道
-
业务性能
-
应用上线前会审查业务新增的sql,和分析sql执行计划
比如是否存在 select * ,索引建立是否合理 -
开启慢查询日志,定期分析慢查询日志
-
监控CPU/内存利用率,读写、网关IO、流量带宽 随着时间的变化统计图
-
吞吐量QPS/TPS,一天内读写随着时间的变化统计图
-
-
数据安全
-
短期增量备份,比如一周一次。 定期全量备份,比如一月一次
-
检查是否有非授权用户,是否存在弱口令,网络防火墙检查
-
导出数据是否进行脱敏,防止数据泄露或者黑产利用
-
数据库 全量操作日志审计,防止数据泄露
-
数据库账号密码 业务独立,权限独立控制,防止多库共用同个账号密码
-
高可用 主从架构,多机房部署
-
你知道Mysql里面有多少种日志
Mysql有多少种常见的日志,分别解释日志的作用?
-
redo 重做日志
作用:确保事务的持久性,防止在发生故障,脏页未写入磁盘。重启数据库会进行redo log执行重做,到达事务一致性 -
undo 回滚日志
作用:保证数据的原子性,记录事务发生之前的数据的一个版本,用于回滚。
innodb事务的可重复读和读取已提交 隔离级别就是通过mvcc+undo实现 -
errorlog 错误日志
作用:Mysql本身启动、停止、运行期间发生的错误信息 -
slow query log 慢查询日志
作用:记录执行时间过长的sql,时间阈值可以配置,只记录执行成功 -
binlog 二进制日志
作用:用于主从复制,实现主从同步 -
relay log 中继日志
作用:用于数据库主从同步,将主库发送来的binlog先保存在本地,然后从库进行回放 -
general log 普通日志
作用:记录数据库操作明细,默认关闭,开启会降低数据库性能
层层套路之数据库主从复制里面知识考查
你们数据库是单点的吗?有没做多节点优化 ,怎么做的?
我们公司数据库不是单节点,是多节点的,有做主从复制
既然搭建过数据库主从复制,你能画下流程图说下异步复制原理不?
层层套路之数据库主从同步遇到的问题
你们搭建数据库主从复制的目的有哪些?
- 容灾使用,用于故障切换
- 业务需要,进行读写分离减少主库压力
既然你们搭建了主从同步,且你们日增量数据量也不少,有没遇到同步延迟问题?为什么会有同步延迟问题,怎么解决?
保证性能第一情况下,不能百分百解决主从同步延迟问题,只能增加缓解措施。
现象:主从同步,大数据量场景下,会发现写入主库的数据,在从库没找到。
-
原因
-
主从复制是单线程操作,当主库TPS高,产生的超过从库sql线程执行能力
-
从库执行了大的sql操作,阻塞等待
-
服务器硬件问题,如磁盘,CPU,还有网络延迟等
-
-
解决办法
-
业务需要有一定的容忍度,程序和数据库直接增加缓存,降低读压力
-
业务适合的话,写入主库后,再写缓存,读的时候可以读缓存,没命中再读从库
-
读写分离,一主多从,分散主库和从库压力
-
提高硬件配置,比如使用SSD固态硬盘、更好的CPU和网络
-
进行分库分表,减少单机压力
-
Mysql主从复制数据一致性校验方案怎么做
什么场景下会出现主从数据不一致?
1、本身复制延迟导致
2、主库宕机或者从库宕机都会导致复制中断
3、把一个从库提升为主库,可能导致从库和主库的数据不一致性
4、主库执行更改前有执行set sql_log_bin=0,会使主库不记录binlog,从库也无法变更这部分数据
5、主从实例版本不一致,特别是高版本是主,低版本为从的情况下,主数据库上面支持的功能,从数据库上面可能不支持该功能
6、从节点未设置只读,误操作写入数据
是否有做过主从一致性校验,你是怎么做的,如果没做过,你计划怎么做?如果不一致你会怎么修复?
Mysql主从复制是基于binlog复制,难免出现复制数据不一致的风险,引起用户数据访问前后不一致的风险
所以要定期开展主从复制数据一致性的校验并修复,避免这些问题
解决方案之一,使用Percona
公司下的工具
-
pt-table-checksum工具进行一致性校验
- 原理
主库利用表中的索引,将表的数据切割成一个个chunk
(块),然后进行计算得到checksum值。
从库也执相应的操作,并在从库上计算相同数据块的checksum,然后对比主从中各个表的checksum是否一致并存储到数据库,最后通过存储校验结果的表就可以判断出哪些表的数据不一致
- 原理
-
pt-table-sync(在从库执行)工具进行修复不一致数据,可以修复主从结构数据的不一致,也可以修复非主从结构数据表的数据不一致
-
原理
在主库上执行数据的更改,再同步到从库上,不会直接更改成从的数据。在主库上执行更改是基于主库现在的数据,也不会更改主库上的数据,可以同步某些表或整个库的数据,但它不同步表结构、索引,只同步不一致的数据
-
注意:
默认主库要检查的表在从库都存在,并且同主库表有相同的表结构
如果表中没有索引,pt-table-checksum将没法处理,一般要求最基本都要有主键索引
pt-table-sync工具会修改数据,使用前最好备份下数据,防止误操作
pt-table-checksum怎么保证某个chunk的时候checksum数据一致性?
当pt工具在计算主库上某chunk的checksum时,主库可能在更新且从库可能复制延迟,那该怎么保证主库与从库计算的是”同一份”数据,答案把要checksum的行加上for update锁并计算,这保证了主库的某个chunk内部数据的一致性
官方地址:https://www.percona.com/downloads/percona-toolkit/LATEST/
其他资料:https://www.cnblogs.com/zishengY/p/6852280.html
如何防止主从数据不一致?
- 主库binlog采用ROW格式。
- 主从实例数据库版本保持一致。
- 主库做好账号权限把控,不可以执行set sql_log_bin=0。
- 从库开启只读,不允许人为写入。
- 定期进行主从一致性检验。
mysql -load data csv.file超大数据量迁移?
Load的处理机制是:在执行load之前,会关掉索引,当load全部执行完成后,再重新创建索引。
Insert的处理机制是:每插入一条则更新一次数据库,更新一次索引。
redis和memcache区别?
-
数据存储位置
memecache
把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。- redis有部份存在硬盘上,这样能保证数据的持久性,支持数据的持久化(笔者注:有快照和AOF日志两种持久化方式,在实际应用的时候,要特别注意配置文件快照参数,要不就很有可能服务器频繁满载做dump)。
-
数据支持类型
- redis在数据支持上要比memecache多的多(memcache简单的key-value结构的数据)。
-
数据持久化
- redis支持,memcache不支持。
-
集群管理的不同
- Memcached本身并不支持分布式,因此只能在客户端通过像一致性哈希这样的分布式算法来实现Memcached的分布式存储。
- Redis已经支持了分布式存储功能。
Redis Cluster
,哨兵sentinel
,分布式锁redlock
。
-
生态环境
SpringIOC创建对象的三种方式?
-
默认构造方法创建
-
需要创建的对象
public class HelloIoc { public void sayHello(){ System.out.println("Hello Spring IOC"); } }
-
xml配置
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <!--利用无参构造构造器--> <!-- 创建对象的第一种方式:利用无参构造构造器 id:唯一的标识符 class:类的全类名 --> <bean id="helloIoc" class="com.ustcinfo.User"></bean> <!--别名 属性 name : 和 bean 的id 属性对应--> <alias name="helloIoc" alias="helloIoc2"/> </beans>
-
test
@Test public void TestHelloIoc(){ //从spring容器获得 //1 获得容器 String xmlPath="bean.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath); //2获得内容 --不需要自己new,都是从spring容器获得 HelloIoc helloIoc = (HelloIoc) applicationContext.getBean("helloIoc"); helloIoc.sayHello(); //利用配置文件 alias 别名属性创建对象 HelloIoc helloIoc2 = (HelloIoc) applicationContext.getBean("helloIoc2"); helloIoc2.sayHello(); }
-
-
静态工厂方式
-
实例化对象
public class HelloStaticFactory { public HelloStaticFactory(){ System.out.println("HelloStaticFactory constructor"); } //静态工厂方法 public static HelloIoc getInstances(){ return new HelloIoc(); } }
-
xml配置
<!-- 创建对象的第二种方式:利用静态工厂方法 factory-method:静态工厂类的获取对象的静态方法 class:静态工厂类的全类名 --> <bean id="helloStaticFactory" factory-method="getInstances" class="com.spring.HelloStaticFactory"></bean>
-
test
@Test public void TestHelloIoc2(){ //从spring容器获得 //1 获得容器 String xmlPath="bean.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath); //2获得内容 --不需要自己new,都是从spring容器获得 HelloIoc helloFactoryIoc = (HelloIoc) applicationContext.getBean("helloStaticFactory"); helloFactoryIoc.sayHello(); }
-
-
实例工厂方式
-
实例化对象
public class HelloInstanceFactory { public HelloInstanceFactory(){ System.out.println("实例工厂方法构造函数"); } //利用实例工厂方法创建对象 public HelloIoc getInstance(){ HelloIoc instanceIoc = new HelloIoc(); return instanceIoc; } }
-
bean xml
<!-- 创建对象的第三种方式:利用实例工厂方法 factory-bean:指定当前Spring中包含工厂方法的beanID factory-method:工厂方法名称 --> <bean id="instanceFactory" class="com.spring.HelloInstanceFactory"></bean> <bean id="instance" factory-bean="instanceFactory" factory-method="getInstance"></bean>
-
test
@Test public void TestHelloIoc3(){ //从spring容器获得 //1 获得容器 String xmlPath="bean.xml"; ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath); //2获得内容 --不需要自己new,都是从spring容器获得 HelloIoc helloFactoryIoc = (HelloIoc) applicationContext.getBean("instance"); helloFactoryIoc.sayHello(); }
-
Spring Bean的声明周期?
- singleton单例非懒加载对象: IOC容器启动的时候会调用方法创建对象并放到IOC容器中,以后每次获取的就是直接从容器中拿(大Map.get)的同一个bean。
- prototype多实例: IOC容器启动的时候,IOC容器启动并不会去调用方法创建对象, 而是每次获取的时候才会调用方法创建对象。
spring
容器在创建时会初始化一些处理器实例对象到容器中,这些用这些处理器对象在容器中处理维护业务bean
的增强。
BeanFactoryPostProcessor
- bean的后置处理器;
- 实现这个接口的bean自定义bean加载数据;
- 调用
postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
- 实例化
BeanPostProcessor
实现类 - 实例化
InstantiationAwareBeanPostProcessorAdapter
实现类 - 执行InstantiationAwareBeanPostProcessorAdapter
postProcessorBefore
-
创建
BeanFactory
对象refreshBeanFactory()
: 刷新或创建beanFactory
;
-
初始化业务
bean
对象
finishBeanFactoryInitialization(beanFactory)
在`doCreateBean()`中首先进行`bean`实例化工作,主要由`createBeanInstance()`实现,该方法返回一个`BeanWrapper`对象。`BeanWrapper`对象是`Spring`的一个低级`Bean`基础结构的核心接口,为什么说是低级呢?因为这个时候的`Bean`还不能够被我们使用,连最基本的属性都没有设置。而且在我们实际开发过程中一般都不会直接使用该类,而是通过`BeanFactory`隐式使用。 `BeanWrapper`接口有一个默认实现类`BeanWrapperImpl`,其主要作用是对`Bean`进行“包裹”,然后对这个包裹的`bean`进行操作,比如后续注入`bean`属性。 在实例化`bean`过程中,`Spring`采用“策略模式”来决定采用哪种方式来实例化`bean`,一般有反射和`CGLIB`动态字节码两种方式。
-
业务bean的属性赋值
populateBean(beanName, mbd, instanceWrapper)
属性赋值。
-
激活
Aware
- 作用:
例如:`BeanNameAware`接口是为了让自身`Bean`能够感知到,获取到自身在`Spring`容器中的`id`属性; 同理,其他的`Aware`接口也是为了能够感知到自身的一些属性。 比如实现了`ApplicationContextAware`接口的类,能够获取到`ApplicationContext`,实现了`BeanFactoryAware`接口的类,能够获取到`BeanFactory`对象。
当`Spring`完成`bean`对象实例化并且设置完相关属性和依赖后,则会开始`bean`的初始化进程`(initializeBean())`,初始化第一个阶段是检查当前`bean`对象是否实现了一系列以`Aware`结尾的的接口。 `Aware`接口为`Spring`容器的核心接口,是一个具有标识作用的超级接口,实现了该接口的`bean`是具有被`Spring`容器通知的能力,通知的方式是采用回调的方式。
- 作用:
-
BeanPostProcessor
前置处理器- 作用:
它主要是对`Spring`容器提供的`bean`实例对象进行有效的扩展,允许`Spring`在初始化`bean`阶段对其进行定制化修改,注入其它组件, 生命周期注解功能等。
- 作用:
-
InitializingBean
和init-method
`InitializingBean`是一个接口,它为`Spring Bean`的初始化提供了一种方式,它有一个`afterPropertiesSet()`方法,在`bean`的初始化进程中会判断当前`bean`是否实现了`InitializingBean`,如果实现了则调用`afterPropertiesSet()`进行初始化工作。然后再检查是否也指定了`init-method()`,如果指定了则通过反射机制调用指定的 `init-method()`。
-
DisposableBean
和destroy-method
AnnotationConfigApplicationContext.close()
关闭容器是触发。`DisposableBean`和`destroy-method`则用于对象的自定义销毁工作。 当一个`bean`对象经历了实例化、设置属性、初始化阶段,那么该`bean`对象就可以供容器使用了(调用的过程)。当完成调用后,如果是`singleton`类型的`bean`,则会看当前`bean`是否应实现了`DisposableBean`接口或者配置了`destroy-method`属性,如果是的话,则会为该实例注册一个用于对象销毁的回调方法,便于在这些 `singleton`类型的`bean`对象销毁之前执行销毁逻辑。 但是,并不是对象完成调用后就会立刻执行销毁方法,因为这个时候`Spring`容器还处于运行阶段,只有当`Spring`容器关闭的时候才会去调用。但是,`Spring`容器不会这么聪明会自动去调用这些销毁方法,而是需要我们主动去告知`Spring`容器。
spring为什么默认使用JDK动态代理?
JDK 和 CGLib动态代理性能对比-教科书上的描述
我们不管是看书还是看文章亦或是我那个上搜索参考答案,可能很多时候,都可以找到如下的回答:
关于两者之间的性能的话,JDK动态代理所创建的代理对象,在以前的JDK版本中,性能并不是很高,虽然在高版本中JDK动态代理对象的性能得到了很大的提升,但是他也并不是适用于所有的场景。主要体现在如下的两个指标中:
1、CGLib所创建的动态代理对象在实际运行时候的性能要比JDK动态代理高不少,有研究表明,大概要高10倍;
2、但是CGLib在创建对象的时候所花费的时间却比JDK动态代理要多很多,有研究表明,大概有8倍的差距;
3、因此,对于singleton的代理对象或者具有实例池的代理,因为无需频繁的创建代理对象,所以比较适合采用CGLib动态代理,反正,则比较适用JDK动态代理。
因为spring容器中的bean默认是单实例;
Spring IOC存储的实例为什么默认是单实例的?
单例只在初始化加载的时候实例化一次,一方面提高了效率,另一方面大大降,低了内存开销。
并发情况下Spring Bean是否安全?
Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。但实际上,大部分的Spring bean并没有可变的状态(比如Serview类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全。
Spring根本就没有对bean的多线程安全问题做出任何保证与措施。对于每个bean的线程安全问题,根本原因是每个bean自身的设计。不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock、CAS等这些实现线程同步的方法了。
- 最浅显的解决办法就是将多态bean的作用域由“singleton”变更为“prototype”
- 使用ThreadLocal
Spring IoC的控制反转的理解?
控制:IOC容器控制了对象的创建
反转:要创建的对象被动接受其依赖的对象,而不是该对象自己创建其依赖的对象
自动装配注解
@Autowired
默认根据bean类型装配,如果类型匹配到多个,那么在根据属性名和bean的id进行匹配(可以由Qualifier注解强制匹配指定的bean id),找不到则报错;@Resource
根据名称查找bean;@Resource
不支持@Primary
功能;@Resource
不支持@Autowired(required = false)
的功能,使用Resource
注解如果没有IOC
容器中没有对应的ID则会报错;
设计模式
静态代理和动态代理?
- 静态代理其实就是在程序运行之前,提前写好被代理方法的代理类,编译后运行。在程序运行之前,class已经存在。
public interface Target {
public String execute();
}
实现
public class TargetImpl implements Target {
@Override
public String execute() {
System.out.println("TargetImpl execute!");
return "execute";
}
}
代理类
public class Proxy implements Target{
private Target target;
public Proxy(Target target) {
this.target = target;
}
@Override
public String execute() {
System.out.println("perProcess");
String result = this.target.execute();
System.out.println("postProcess");
return result;
}
}
测试
public class ProxyTest {
public static void main(String[] args) {
Target target = new TargetImpl();
Proxy p = new Proxy(target);
String result = p.execute();
System.out.println(result);
}
}
-
动态代理主要是通过反射机制,在运行时动态生成所需代理的class
-
JDK动态代理具体实现原理
-
通过实现InvocationHandler接口创建自己的调用处理器;
-
通过为Proxy类指定ClassLoader对象和一组interface来创建动态代理;
-
通过反射机制获取动态代理类的构造函数,其唯一参数类型就是调用处理器接口类型;
-
通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数参入;
-
-
JDK动态代理: 要代理的类需要实现接口
Spirng默认采用JDK动态代理实现机制
接口
public interface Target {
public String execute();
}
实现类
public class TargetImpl implements Target {
@Override
public String execute() {
System.out.println("TargetImpl execute!");
return "execute";
}
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class DynamicProxyHandler implements InvocationHandler{
private Target target;
public DynamicProxyHandler(Target target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("========before==========");
Object result = method.invoke(target,args);
System.out.println("========after===========");
return result;
}
}
测试
import java.lang.reflect.Proxy;
public class DynamicProxyTest {
public static void main(String[] args) {
Target target = new TargetImpl();
DynamicProxyHandler handler = new DynamicProxyHandler(target);
Target proxySubject = (Target) Proxy.newProxyInstance(TargetImpl.class.getClassLoader(),TargetImpl.class.getInterfaces(),handler);
String result = proxySubject.execute();
System.out.println(result);
}
}
无论是动态代理还是静态代理,都需要定义接口,然后才能实现代理功能。这同样存在局限性,因此,为了解决这个问题,出现了第三种代理方式:cglib代理。
- cglib代理动态代理
CGLib采用了字节码底层继承要代理类来实现(如果被代理类被final关键字所修饰,那么抱歉会失败),其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。
在创建动态代理对象时,需要设置一个或多个回调拦截器,这个回调拦截器必须要实现MethodInterceptor
接口,代理类对象执行execute方法时,顺序执行拦截器;
目标类
public class Target {
public String execute() {
String message = "-----------test------------";
System.out.println(message);
return message;
}
}
通用代理类
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class MyMethodInterceptor implements MethodInterceptor{
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println(">>>>MethodInterceptor start...");
Object result = proxy.invokeSuper(obj,args);
System.out.println(">>>>MethodInterceptor ending...");
return "result";
}
}
测试
import net.sf.cglib.proxy.Enhancer;
public class CglibTest {
public static void main(String ... args) {
System.out.println("***************");
Target target = new Target();
CglibTest test = new CglibTest();
Target proxyTarget = (Target) test.createProxy(Target.class);
String res = proxyTarget.execute();
System.out.println(res);
}
public Object createProxy(Class targetClass) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(targetClass);
enhancer.setCallback(new MyMethodInterceptor());
return enhancer.create();
}
}
观察者模式
给你举个栗子:假设有三个人,小美(女,22),小王和小李。小美很漂亮,小王和小李是两个程序猿,时刻关注着小美的一举一动。有一天,小美说了一句:“谁来陪我打游戏啊。”这句话被小王和小李听到了,结果乐坏了,蹭蹭蹭,没一会儿,小王就冲到小美家门口了,在这里,小美是被观察者,小王和小李是观察者,被观察者发出一条信息,然后观察者们进行相应的处理,看代码:
public interface Person {
//小王和小李通过这个接口可以接收到小美发过来的消息
void getMessage(String s);
}
这个接口相当于小王和小李的电话号码,小美发送通知的时候就会拨打getMessage
这个电话,拨打电话就是调用接口,看不懂没关系,先往下看
public class LaoWang implements Person {
private String name = "小王";
public LaoWang() {
}
@Override
public void getMessage(String s) {
System.out.println(name + "接到了小美打过来的电话,电话内容是:" + s);
}
}
public class LaoLi implements Person {
private String name = "小李";
public LaoLi() {
}
@Override
public void getMessage(String s) {
System.out.println(name + "接到了小美打过来的电话,电话内容是:->" + s);
}
}
代码很简单,我们再看看小美的代码:
public class XiaoMei {
List<Person> list = new ArrayList<Person>();
public XiaoMei(){
}
public void addPerson(Person person){
list.add(person);
}
//遍历list,把自己的通知发送给所有暗恋自己的人
public void notifyPerson() {
for(Person person:list){
person.getMessage("你们过来吧,谁先过来谁就能陪我一起玩儿游戏!");
}
}
}
我们写一个测试类来看一下结果对不对
public class Test {
public static void main(String[] args) {
XiaoMei xiao_mei = new XiaoMei();
LaoWang lao_wang = new LaoWang();
LaoLi lao_li = new LaoLi();
//小王和小李在小美那里都注册了一下
xiao_mei.addPerson(lao_wang);
xiao_mei.addPerson(lao_li);
//小美向小王和小李发送通知
xiao_mei.notifyPerson();
}
}
装饰者模式
对已有的业务逻辑进一步的封装,使其增加额外的功能,如Java中的IO流就使用了装饰者模式,用户在使用的时候,可以任意组装,达到自己想要的效果。 举个栗子,我想吃三明治,首先我需要一根大大的香肠,我喜欢吃奶油,在香肠上面加一点奶油,再放一点蔬菜,最后再用两片面包夹一下,很丰盛的一顿午饭,营养又健康。(ps:不知道上海哪里有卖好吃的三明治的,求推荐~)那我们应该怎么来写代码呢? 首先,我们需要写一个Food类,让其他所有食物都来继承这个类,看代码:
public class Food {
private String food_name;
public Food() {
}
public Food(String food_name) {
this.food_name = food_name;
}
public String make() {
return food_name;
};
}
代码很简单,我就不解释了,然后我们写几个子类继承它:
//面包类
public class Bread extends Food {
private Food basic_food;
public Bread(Food basic_food) {
this.basic_food = basic_food;
}
public String make() {
return basic_food.make()+"+面包";
}
}
//奶油类
public class Cream extends Food {
private Food basic_food;
public Cream(Food basic_food) {
this.basic_food = basic_food;
}
public String make() {
return basic_food.make()+"+奶油";
}
}
//蔬菜类
public class Vegetable extends Food {
private Food basic_food;
public Vegetable(Food basic_food) {
this.basic_food = basic_food;
}
public String make() {
return basic_food.make()+"+蔬菜";
}
}
这几个类都是差不多的,构造方法传入一个Food
类型的参数,然后在make
方法中加入一些自己的逻辑,如果你还是看不懂为什么这么写,不急,你看看我的Test
类是怎么写的,一看你就明白了
public class Test {
public static void main(String[] args) {
Food food = new Bread(new Vegetable(new Cream(new Food("香肠"))));
System.out.println(food.make());
}
}
看到没有,一层一层封装,我们从里往外看:最里面我new了一个香肠,在香肠的外面我包裹了一层奶油,在奶油的外面我又加了一层蔬菜,最外面我放的是面包,是不是很形象,哈哈~ 这个设计模式简直跟现实生活中一摸一样,看懂了吗? 我们看看运行结果吧
运行结果
香肠+奶油+蔬菜+面包
一个三明治就做好了~
工厂模式
简单工厂模式
简单工厂模式:一个抽象的接口,多个抽象接口的实现类,一个工厂类,用来实例化抽象的接口
// 抽象产品类
abstract class Car {
public void run();
public void stop();
}
// 具体实现类
class Benz implements Car {
public void run() {
System.out.println("Benz开始启动了。。。。。");
}
public void stop() {
System.out.println("Benz停车了。。。。。");
}
}
class Ford implements Car {
public void run() {
System.out.println("Ford开始启动了。。。");
}
public void stop() {
System.out.println("Ford停车了。。。。");
}
}
// 工厂类
class Factory {
public static Car getCarInstance(String type) {
Car c = null;
if ("Benz".equals(type)) {
c = new Benz();
}
if ("Ford".equals(type)) {
c = new Ford();
}
return c;
}
}
public class Test {
public static void main(String[] args) {
Car c = Factory.getCarInstance("Benz");
if (c != null) {
c.run();
c.stop();
} else {
System.out.println("造不了这种汽车。。。");
}
}
}
工厂方法模式
工厂方法模式:有四个角色,抽象工厂模式,具体工厂模式,抽象产品模式,具体产品模式。不再是由一个工厂类去实例化具体的产品,而是由抽象工厂的子类去实例化产品
// 抽象产品角色
public interface Moveable {
void run();
}
// 具体产品角色
public class Plane implements Moveable {
@Override
public void run() {
System.out.println("plane....");
}
}
public class Broom implements Moveable {
@Override
public void run() {
System.out.println("broom.....");
}
}
// 抽象工厂
public abstract class VehicleFactory {
abstract Moveable create();
}
// 具体工厂
public class PlaneFactory extends VehicleFactory {
public Moveable create() {
return new Plane();
}
}
public class BroomFactory extends VehicleFactory {
public Moveable create() {
return new Broom();
}
}
// 测试类
public class Test {
public static void main(String[] args) {
VehicleFactory factory = new BroomFactory();
Moveable m = factory.create();
m.run();
}
}
抽象工厂模式
抽象工厂模式:与工厂方法模式不同的是,工厂方法模式中的工厂只生产单一的产品,而抽象工厂模式中的工厂生产多个产品
/抽象工厂类
public abstract class AbstractFactory {
public abstract Vehicle createVehicle();
public abstract Weapon createWeapon();
public abstract Food createFood();
}
//具体工厂类,其中Food,Vehicle,Weapon是抽象类,
public class DefaultFactory extends AbstractFactory{
@Override
public Food createFood() {
return new Apple();
}
@Override
public Vehicle createVehicle() {
return new Car();
}
@Override
public Weapon createWeapon() {
return new AK47();
}
}
//测试类
public class Test {
public static void main(String[] args) {
AbstractFactory f = new DefaultFactory();
Vehicle v = f.createVehicle();
v.run();
Weapon w = f.createWeapon();
w.shoot();
Food a = f.createFood();
a.printName();
}
}
简单工厂和抽象工厂有什么区别?
简单工厂和工厂方法模式的区别就在于需要创建对象的复杂程度上。而且抽象工厂模式是三个里面最为抽象、最具一般性的。抽象工厂模式的用意为:给客户端提供一个接口,可以创建多个产品族中的产品对象。
强引用,软引用,弱引用和虚引用?
引用类型被 | 垃圾回收时间 | 示例 | 用途 | 生存时间 | 表示方法 |
---|---|---|---|---|---|
强引用 | 从来不会 | Object o=new Object(); // 强引用 | 对象的一般状态,多只new 出来的对象 | JVM 停止运行时终止 | |
软引用 | 当内存不足时 | String str=new String(“abc”);// 强引用 SoftReference softRef=new SoftReference(str);// 软引用 | 可用来实现内存敏感的高速缓存,浏览器页面对象,用于获取历史页面对象 | 内存不足时终止 | java.lang.ref.SoftReference类来表示 |
弱引用 | 一旦发现了只具有弱引用的对象 | String str=new String(“abc”);WeakReference abcWeakRef =new WeakReference(str); | 如果这个对象是偶尔的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。 | 垃圾回收后终止 | java.lang.ref.WeakReference类来表示 |
虚引用 | 正常垃圾回收时 | 用来跟踪对象被垃圾回收的活动,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收 | 垃圾回收后终止 | java.lang.ref.PhantomReference类表示 |
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
ThreadLocal
为什么容易内存泄漏?
C c = new C(b);
b = null;
当 b 被设置成null时,那么是否意味这一段时间后GC工作可以回收 b 所分配的内存空间呢?答案是否定的,因为即使 b 被设置成null,但 c 仍然持有对 b 的引用,而且还是强引用,所以GC不会回收 b 原先所分配的空间,既不能回收,又不能使用,这就造成了 内存泄露。
那么如何处理呢?
可以通过c = null;,也可以使用弱引用WeakReference w = new WeakReference(b);。因为使用了弱引用WeakReference,GC是可以回收 b 原先所分配的空间的。
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// tab是弱引用元素数组;key为当前ThreadLocal对象,value为设置的值
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
查看源代码发现引用关系是这样的:当前ThreadLocal -> 当前Thread —> ThreaLocalMap[ThreadLocalMap getMap(Thread t)] -> Entry[keyhash]对象(Entry(ThreadLocal<?> k, Object v)Entry对象引用了弱引用对象ThreadLocal) -> value
private static ThreadLocal<T> threadLocal = new ThreadLocal<>();
threadLocal.set(T);
threadLocal
一直没有被外部强引用引用他,GC
势必会回收new ThreadLocal<>()
,这是key(Entry[keyhash]对象作为key)
这个key
就为null
;
ThreadLocalMap
中就会出现key
为null
的Entry
,就没有办法访问这些key
为null
的Entry
的value
,如果当前线程再迟迟不结束的话,这些key
为null
的Entry
的value
就会一直存在一条强引用链:Thread Ref
-> Thread
-> ThreaLocalMap
-> Entry
-> value
永远无法回收,造成内存泄漏。
ThreadLocalMap
的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal
的get()
,set()
,remove()
的时候都会清除线程ThreadLocalMap
里所有key
为null
的value
。
我们要考虑一种会发生内存泄漏的情况,如果ThreadLocal被设置为null后,而且没有任何强引用指向它,根据垃圾回收的可达性分析算法,ThreadLocal将会被回收。这样一来,ThreadLocalMap中就会含有key为null的Entry,而且ThreadLocalMap是在Thread中的,只要线程迟迟不结束,这些无法访问到的value会形成内存泄漏。为了解决这个问题,ThreadLocalMap中的getEntry()、set()和remove()函数都会清理key为null的Entry,以下面的getEntry()函数的源码为例。
为什么使用ThreadLocal为弱引用而不是强引用?
再分析一遍所谓内存泄漏:
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 。(在web应用中,每次http请求都是一个线程,tomcat容器配置使用线程池时会出现内存泄漏问题)
-
key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
-
key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用get(),set(),remove()的时候会被清除。
-
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:
弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用get(),set(),remove()的时候会被清除。
TCP?
三次握手
三次握手:
- 客户端–发送带有SYN标志的数据包–一次握手–服务端
- 服务端–发送带有SYN/ACK标志的数据包–二次握手–客户端
- 客户端–发送带有带有ACK标志的数据包–三次握手–服务端
详细分析:
- TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
- TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
- TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。
- TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
- 当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。
为什么TCP客户端最后还要发送一次确认呢?
一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。
如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
四次挥手
四次挥手:
- 客户端-发送一个FIN,用来关闭客户端到服务器的数据传送
- 服务器-收到这个FIN,它发回一个ACK,确认序号为收到的序号加1 。和SYN一样,一个FIN将占用一个序号
- 服务器-关闭与客户端的连接,发送一个FIN给客户端
- 客户端-发回ACK报文确认,并将确认序号设置为收到序号加1
详细分析:
- 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
- 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。 这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
- 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
- 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
- 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
- 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
为什么客户端最后还要等待2MSL?
MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
为什么建立连接是三次握手,关闭连接确是四次挥手呢?
建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。 而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
TCP特点?
-
面向连接
数据传输之前需要建立连接:三次握手
数据传输结束之后需要释放连接: 四次挥手-
TCP连接的时候为何是三次握手而不是两次?
三次握手解决的主要是同步请求报文SYN超时的问题 如果只有两次握手(前两次),客户端发起的SYN同步报文如果发生丢失或者超时的现象,那么SYN同步报文在网络路由中逗留,客户端会启用超时重传策略,重新发送一个SYN,服务端收到之后会发送一个同步确认报文(SYNACK),连接建立完毕。此时如果第一次超时的SYN传递成功,server端会误认为客户端又进行了一次连接请求,造成误会。 三次握手中,客户端再收到server端的同步确认报文(SYNACK)之后,会发送一个ACK确认报文,进行连接的建立。在第一种情况下客户端对server超时报文的同步确认报文不会有ACK确认报文,所以server端不再进行操作。
-
TCP的四次挥手为何要进行两方面的断开呢?
因为客户端与server端的连接通道是全双工的,两条通道都可以发送或者接收,挥手两次达到半关闭状态,挥手四次才可以全部关闭连接。
-
-
可靠传输
- 特点
- 无差错
- 不丢失
- 不重复
- 按序到达
TCP具体是通过怎样的方式来保证数据的顺序化传输呢?主机每次发送数据时,TCP就给每个数据包分配一个序列号并且在一个特定的时间内等待接收主机对分配的这个序列号进行确认,如果发送主机在一个特定时间内没有收到接收主机的确认,则发送主机会重传此数据包。接收主机利用序列号对接收的数据进行确认,以便检测对方发送的数据是否有丢失或者乱序等,接收主机一旦收到已经顺序化的数据,它就将这些数据按正确的顺序重组成数据流并传递到高层进行处理。 具体步骤如下: (1)为了保证数据包的可靠传递,发送方必须把已发送的数据包保留在缓冲区; (2)并为每个已发送的数据包启动一个超时定时器; (3)如在定时器超时之前收到了对方发来的应答信息(可能是对本包的应答,也可以是对本包后续包的应答),则释放该数据包占用的缓冲区; (4)否则,重传该数据包,直到收到应答或重传次数超过规定的最大次数为止。 (5)接收方收到数据包后,先进行CRC校验,如果正确则把数据交给上层协议,然后给发送方发送一个累计应答包,表明该数据已收到,如果接收方正好也有数据要发给发送方,应答包也可方在数据包中捎带过去。
- 原因: TCP拥有停止等待协议特性
- 误差错情况
- 超时重传
- 确认丢失
- 确认迟到
- 误差错情况
- 特点
-
面向字节流
-
流量控制(滑动窗口协议:接收窗口可以通过报文字段动态调整发送窗口速率)
-
阻塞控制
- 特点
- 慢开始、拥塞控制、快恢复、快重传
- 简单描述TCP慢启动(慢开始)的特点
- 特点
如果TCP已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
所谓的无状态,是指浏览器每次向服务器发起请求的时候,不是通过一个连接,而是每次都建立一个新的连接。如果是一个连接的话,服务器进程中就能保持住这个连接并且在内存中记住一些信息状态。而每次请求结束后,连接就关闭,相关的内容就释放了,所以记不住任何状态,成为无状态连接。
HTTP(超文本协议)介绍?
Http协议是建立在TCP协议基础之上的,当浏览器需要从服务器获取网页数据的时候,会发出一次Http请求。Http会通过TCP建立起一个到服务器的连接通道,当本次请求需要的数据完毕后,Http会立即将TCP连接断开,这个过程是很短的。所以Http连接是一种短连接,是一种无状态的连接。
所谓的无状态,是指浏览器每次向服务器发起请求的时候,不是通过一个连接,而是每次都建立一个新的连接。如果是一个连接的话,服务器进程中就能保持住这个连接并且在内存中记住一些信息状态。而每次请求结束后,连接就关闭,相关的内容就释放了,所以记不住任何状态,成为无状态连接。
http传输流:
发送端在层与层间传输数据时,没经过一层都会被加上首部信息,接收端每经过一层都会删除一条首部
http常用状态码?
2XX 成功
200 OK,表示从客户端发来的请求在服务器端被正确处理
204 No content,表示请求成功,但响应报文不含实体的主体部分
206 Partial Content,进行范围请求
3XX 重定向
301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
302 found,临时性重定向,表示资源临时被分配了新的 URL
303 see other,表示资源存在着另一个 URL,应使用 GET 方法丁香获取资源
304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
307 temporary redirect,临时重定向,和302含义相同
4XX 客户端错误
400 bad request,请求报文存在语法错误
401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
403 forbidden,表示对请求资源的访问被服务器拒绝
404 not found,表示在服务器上没有找到请求的资源
5XX 服务器错误
500 internal sever error,表示服务器端在执行请求时发生了错误
503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求
http组成?
起始行、消息头、空行和消息体
- 起始行:GET /home HTTP/1.1
也就是方法 + 路径 + http版本。
对于响应报文来说,起始行一般张这个样:
HTTP/1.1 200 OK
- 请求头
展示一下请求头和响应头在报文中的位置:
- 空格
很重要,用来区分开头部和实体。
问: 如果说在头部中间故意加一个空行会怎么样?
那么空行后的内容全部被视为实体。
- 实体
就是具体的数据了,也就是body部分。请求报文对应请求体, 响应报文对应响应体。
GET 和 POST 有什么区别?
- 从长度的角度: GET请求的url长度大小会受到浏览器限制,post没有
- 从缓存的角度,GET 请求会被浏览器主动缓存下来,留下历史记录,而 POST 默认不会。
- 从编码的角度,GET 只能进行 URL 编码,只能接收 ASCII 字符,而 POST 没有限制。
- 从参数的角度,GET 一般放在 URL 中,因此不安全,POST 放在请求体中,更适合传输敏感信息。
- 从幂等性的角度,GET是幂等的,而POST不是。(幂等表示执行相同的操作,结果也是相同的)
- 从TCP的角度,GET 请求会把请求报文一次性发出去,而 POST 会分为两个 TCP 数据包,首先发 header 部分,如果服务器响应 100(continue), 然后发 body 部分。(火狐浏览器除外,它的 POST 请求只发一个 TCP 包)
http缺点?
HTTP 缺点
- 无状态
所谓的优点和缺点还是要分场景来看的,对于 HTTP 而言,最具争议的地方在于它的无状态。
在需要长连接的场景中,需要保存大量的上下文信息,以免传输大量重复的信息,那么这时候无状态就是 http 的缺点了。
但与此同时,另外一些应用仅仅只是为了获取一些数据,不需要保存连接上下文信息,无状态反而减少了网络开销,成为了 http 的优点。
- 明文传输
即协议里的报文(主要指的是头部)不使用二进制数据,而是文本形式。
这当然对于调试提供了便利,但同时也让 HTTP 的报文信息暴露给了外界,给攻击者也提供了便利。WIFI陷阱就是利用 HTTP 明文传输的缺点,诱导你连上热点,然后疯狂抓你所有的流量,从而拿到你的敏感信息。
- 队头阻塞问题
当 http 开启长连接时,共用一个 TCP 连接,同一时刻只能处理一个请求,那么当前请求耗时过长的情况下,其它的请求只能处于阻塞状态,也就是著名的队头阻塞问题。接下来会有一小节讨论这个问题。
UDP协议的三大特点?
-
无连接
-
尽最大努力交付
不管在不在,知不知道,只管发送
-
面向报文: 既不合并,也不拆分
UDP协议的功能?
- 复用
- 分用
- 差错检测
TCP和UDP区别?
一、UDP:
1、将数据源和目的地封装到数据包中,不需要建立连接
2、每个数据包的大小限制在64k以内
3、因无连接,是不可靠协议
4、不需要建立连接,速度快
例子:聊天、对讲机就是UDP的,面向无连接(不管在不在,知不知道,只管发送,求速度),丢数据也不管。速度快。数据被分成包
二、TCP:
1、建立连接,形成传输数据的通道
2、在连接中进行大量数据的传输
3、通过三次握手完成连接、是可靠协议
4、必须建立连接,效率会稍低
例子:电话通话,必须连接,对方同意才可以发送数据(不然就等待),不能丢失数据。
区别总结:
TCP是面向连接的,支持可靠传输的面向字节流的,具有流量控制和拥塞控制的协议,UDP只具有复用,分用和差错检测的功能,且是无连接的。
同一个包中可以创建内部类对象?
一个包中是不能直接访问另一个包中的内部类,所以无法直接创建另一个包中内部类对象。
内部类做为其外部类的成员,因此可以使用任意访问控制符如private、protected和public修饰。
外部类的上一级程序单元是包,所以它只有2个作用域:同一个包和任何位置。因此只需两种访问权限:包访问权限和公开访问权限。正好对应省略访问控制符和public访问控制符。省略访问控制符是包访问权限,即同一包中的其他类可访问省略访问控制符的成员。因此,如果一一个外部类不使用任何访问控制符修饰,则只能被同一一个包中其他类访问。而内部类的_上一级程序单元是外部类,它就具有四个作用域:同一个类、同一个包、父子类和任何位置,因此可以使用四种访问控制权限。
Minor GC(Yang GD)和Full GC?
新生代内存不够用时候发生MGC也叫YGC,JVM内存不够的时候发生FGC
聚集索引和非聚集索引(辅助索引) 物理索引和逻辑索引?
聚集索引和非聚集索引可以类比为字典查询方式,按照拼音查询和部首查询,拼音是有序的,汉子在字典中也是有序的,而部首笔画索引,虽然笔画相同的字在笔画索引中相邻,但是实际存储页码却不相邻。
- 聚集索引: 正文内容按照一个特定维度排序存储,这个特定的维度就是聚集索引
- 非聚集索引(辅助索引): 索引项顺序存储,但索引项对应的内容却是随机存储的
- 物理索引: 物理索引是存储在磁盘上的实际索引结构
- 逻辑索引: 逻辑索引是对物理索引的引用
create table student (
`id` INT UNSIGNED AUTO_INCREMENT,
`name` VARCHAR(255),
PRIMARY KEY(`id`),
KEY(`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
以student表为例,该表中主键id是该表的聚集索引、name为非聚集索引;表中的每行数据都是按照聚集索引id排序存储的;比如要查找name='Arla’和name='Arle’的两个同学,他们在name索引表中位置可能是相邻的,但是实际存储位置可能差的很远。name索引表节点按照name排序,检索的是每一行数据的主键。聚集索引表按照主键id排序,检索的是每一行数据的真实内容,也就是说查询name='Arle’的记录时,首先通过name索引表查找到Arle的主键id(可能有多个主键id,因为有重名的同学),再根据主键id的聚集索引找到相应的行记录;
每张表只有一个聚集索引
MySQL覆盖索引?
覆盖索引:如果查询条件使用的是普通索引(或是联合索引的最左原则字段),查询结果是联合索引的字段或是主键,不用回表操作,直接返回结果,减少IO磁盘读写读取正行数据
覆盖索引
建立两列以上的索引,即可查询复合索引里的列的数据而不需要进行回表二次查询,如index(col1, col2),执行下面的语句
select col1, col2 from t1 where col1 = ‘213’;
要注意使用复合索引需要满足最左侧索引的原则,也就是查询的时候如果where条件里面没有最左边的一到多列,索引就不会起作用。
覆盖索引是select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖。索引的字段不只包含查询列,还包含查询条件、排序等。
mysql最左侧索引原则?
如果一个表中只有a,b,c,d四个字段,给他们加上联合索引(a,b,c),此时
where a and b and c
where a and c
where c and a
都是用到索引的但是
where b and c 不会用到索引
如果一个表中只有a,b,c三个字段,给他们加上联合索引(a,b,c),此时
SELECT * FROM test WHERE c=2是否用到索引?答案是用到的
如果表中的字段除了(col1,col2,col3),还有别的字段。那么的 EXPLAIN 中结果将不会是 INDEX 而是 ALL。如果表中的字段只有(col1,col2,col3),那么的 EXPLAIN 中结果才会是 INDEX ,这种情况用到了mysql的覆盖索引。
最左侧原则不是要求索引(a,b,c)必须按照where a=x and b=x and c=x 才可以。顺序是可以互换的,关键是要有这个a,而且这个a一定要是等值匹配。
个人认为,所谓最左前缀原则就是先要看第一列,在第一列满足的条件下再看左边第二列,以此类推。
索引是因为B+树结构 所以查找快 如果单看第三列 是非排序的。
多列索引是先按照第一列进行排序,然后在第一列排好序的基础上再对第二列排序,如果没有第一列的话,直接访问第二列,那第二列肯定是无序的,直接访问后面的列就用不到索引了。
所以如果不是在前面列的基础上而是但看后面某一列,索引是失效的。
B+Tree特点?
B-tree 索引可以用于使用 =, >, >=, <, <= 或者 BETWEEN 运算符的列比较。如果 LIKE 的参数是一个没有以通配符起始的常量字符串的话也可以使用这种索引。
java有8种基本数据类型?
数值类型byte、short、int、long、float、double。 数值类型又可以分为整数类型byte、short、int、long和浮点数类型float、double
字符类型char
布尔类型boolean
占用字节分别是byte(1),char(2),short(2),int(4),float(4),long(8),double(8),boolean(boolean类型的数组,每个boolean占1个字节,单个boolean变量占4个字节)
HashMap尾插?
HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
1.7源码在扩容resize()过程中,在将旧数组上的数据 转移到 新数组上时,转移数据操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况
由于 JDK 1.8 转移数据操作 = 按旧链表的正序遍历链表、在新链表的尾部依次插入,所以不会出现链表逆序、倒置的情况,故不容易出现环形链表的情况。
参考: https://juejin.im/post/5aa5d8d26fb9a028d2079264
HashMap扩容机制?
1.扩容
阈值是0.75
创建一个新的Entry空数组,长度是原数组的2倍。
2.ReHash
为什么采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?
根据HashMap的容量大小(数组长度),按需取 哈希码一定数量的低位 作为存储的数组下标位置,从而 解决 “哈希码与数组大小范围不匹配” 的问题
1.8HashMap为什么在计算数组下标前,需对哈希码进行二次处理:扰动处理?
加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性 & 均匀性,最终减少Hash冲突
HashMap什么情况下出现链表环?
ReHash在并发的情况下可能会形成链表环。
为什么1.8扩容的时候为啥一定必须是2的多少次幂?
1.7源码定位数组下标
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
- 元素随机平均分布到数组
上述代码也相当于对length求模。 注意最后return的是h&(length-1)。如果length不为2的幂,比如15。那么length-1的2进制就会变成1110。在h为随机数的情况下,和1110做&操作。尾数永远为0。那么0001、1001、1101等尾数为1的位置就永远不可能被entry占用。这样会造成浪费,不随机等问题。 length-1 二进制中为1的位数越多,那么分布就平均。
- 扩容时快速定位元素下标位置
1.8扩容源码
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
resize过程中不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图(一方面位运算更快,另一方面抗碰撞的Hash函数其实挺耗时的)
参考: https://blog.csdn.net/dalong3976/article/details/83934609
为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8,而不是7或者不是20呢?
jdk作者选择8,一定经过了严格的运算,觉得在长度为8的时候,与其保证链表结构的查找开销,不如转换为红黑树,改为维持其平衡开销。
如何判断链表有环?
方法一:节点索引比较
遍历链表,每遍历到一个新节点,就从头节点重新遍历新节点之前的所有节点,用新节点ID和此节点之前所有节点ID依次作比较。如果发现新节点之前的所有节点当中存在相同节点ID,则说明该节点被遍历过两次,链表有环
方法二:指针追踪
首先创建两个指针1和2(在java里就是两个对象引用),同时指向这个链表的头节点。然后开始一个大循环,在循环体中,让指针1每次向下移动一个节点,让指针2每次向下移动两个节点,然后比较两个指针指向的节点是否相同。如果相同,则判断出链表有环,如果不同,则继续下一次循环。
表分区、水平拆分和垂直查分表?
分区
分区优点:
-
可以让单表存储更多的数据
-
分区表的数据更容易维护,可以通过清楚整个分区批量删除大量数据,也可以增加新的分区来支持新插入的数据。另外,还可以对一个独立分区进行优化、检查、修复等操作
-
部分查询能够从查询条件确定只落在少数分区上,速度会很快
-
分区表的数据还可以分布在不同的物理设备上,从而搞笑利用多个硬件设备
-
可以使用分区表赖避免某些特殊瓶颈,例如InnoDB单个索引的互斥访问、ext3文件系统的inode锁竞争
-
可以备份和恢复单个分区
适合场景
- 最适合的场景数据的时间序列性比较强,则可以按时间来分区,如下所示:
CREATE TABLE members (
firstname VARCHAR(25) NOT NULL,
lastname VARCHAR(25) NOT NULL,
username VARCHAR(16) NOT NULL,
email VARCHAR(35),
joined DATE NOT NULL
)
PARTITION BY RANGE( YEAR(joined) ) (
PARTITION p0 VALUES LESS THAN (1960),
PARTITION p1 VALUES LESS THAN (1970),
PARTITION p2 VALUES LESS THAN (1980),
PARTITION p3 VALUES LESS THAN (1990),
PARTITION p4 VALUES LESS THAN MAXVALUE
);
查询时加上时间范围条件效率会非常高,同时对于不需要的历史数据能很容的批量删除。
- 如果数据有明显的热点,而且除了这部分数据,其他数据很少被访问到,那么可以将热点数据单独放在一个分区,让这个分区的数据能够有机会都缓存在内存中,查询时只访问一个很小的分区表,能够有效使用索引和缓存
垂直拆分
常见的是把一个多字段的大表按常用字段和非常用字段进行拆分,每个表里面的数据记录数一般情况下是相同的,只是字段不一样,使用主键关联
垂直拆分的优点是:
-
可以使得行数据变小,一个数据块(Block)就能存放更多的数据,在查询时就会减少I/O次数(每次查询时读取的Block 就少)
-
可以达到最大化利用Cache的目的,具体在垂直拆分的时候可以将不常变的字段放一起,将经常改变的放一起
-
数据维护简单
缺点:
-
主键出现冗余,需要管理冗余列
-
会引起表连接JOIN操作(增加CPU开销)可以通过在业务服务器上进行join来减少数据库压力
-
依然存在单表数据量过大的问题(需要水平拆分)
-
事务处理复杂
水平拆分
水平拆分的优点是:
-
不存在单库大数据和高并发的性能瓶颈
-
应用端改造较少
-
提高了系统的稳定性和负载能力
缺点:
分片事务难以解决 ,跨节点Join性能较差,逻辑复杂
MySQL的FULLTEXT索引?
使用全文索引
EXPLAIN select * FROM t_index WHERE MATCH(gender) AGAINST(‘r’);
联合name,en_name字段必须是FULLTEXT的联合索引,否则执行下面语句会报错,联合fulltext索引不能使用单个字段查询
select * FROM test WHERE MATCH(name,en_name) AGAINST(‘r hate’);
注意: match() 函数中指定的列必须和全文索引中指定的列完全相同,否则就会报错,无法使用全文索引,这是因为全文索引不会记录关键字来自哪一列。如果想要对某一列使用全文索引,请单独为该列创建全文索引。
如图表中只有gender字段作为FULLTEXT索引,执行语句
select * FROM t_index WHERE MATCH(gender) AGAINST('11');
查询是没有数据的,原因是MySQL最小搜索长度 MyISAM 引擎下默认是 4,InnoDB 引擎下是 3,也即,MySQL 的全文索引只会对长度大于等于 4 或者 3 的词语建立索引;
如图表中gender和school创建联合FULLTEXT索引,执行语句
select * FROM t_index WHERE MATCH(gender,school) AGAINST('1131 222');
查询是没有数据的,原因是只有gender匹配上1131时,才去匹配school,虽然school可以匹配上222,但是根据最左索引原则,则无数据。
为什么索引能提高查询速度?
查询汉字可以通过拼音或部首查询,能够快速定位汉字所在的页码,索引也是这个道理
- 索引是使记录数据有序化的技术
- 它可以指定按某列/某几列预先排序
- 根据索引值进行分类并按需排序
- 查询数据先查询索引表,获取数据所在行的物理地址
- 就不用再进行全表扫描了
比如上图,action值为2的索引值分类存储在了索引空间,可以快速地查询到索引值所对应的列。
索引的优缺点
优势:以快速检索,减少I/O次数,加快检索速度;根据索引分组和排序,可以加快分组和排序;
为什么HashMap默认设置为16?
原因一:均匀分布在桶中,减少hash碰撞
index = hash & (n - 1)
16是2的幂次方,length - 1的值是所有二进制位全为1,hash是key的hashcode作为随机数,hash和n-1与运算后最后一位有可能是0或1;如果length不是2的幂次方,那么hash和n-1与运算后最后一位有只能是0,所以0001,1001,100001,这些脚标所在的桶永远都是空的。
原因二:作为经验值
在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。
太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。
所以,16就作为一个经验值被采用了
countdownlatch和join方法的区别?
CountDownLatch与join的区别:调用thread.join() 方法必须等thread 执行完毕,当前线程才能继续往下执行,而CountDownLatch通过计数器提供了更灵活的控制,只要检测到计数器为0当前线程就可以往下执行而不用管相应的thread是否执行完毕
synchronized原理?
-
实现原理
JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。
具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。
当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record)区域,同时将锁对象的对象头中 Mark Word 拷贝到锁记录中,再尝试使用 CAS 将 Mark Word 更新为指向锁记录的指针。
如果更新成功,当前线程就获得了锁。
如果更新失败 JVM 会先检查锁对象的 Mark Word 是否指向当前线程的锁记录。
如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。
不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁。
偏向锁->轻量级锁->重量级锁-
锁升级示意图
-
synchronized
是解决线程安全的问题,常用在 同步普通方法、静态方法、代码块中;
synchronized
是非公平、可重入
每个对象有一个锁和一个等待队列,锁只能被一个线程持有,其他需要锁的线程需要阻塞等待。锁被释放后,对象会从队列中取出一个并唤醒,唤醒哪个线程是不确定的,不保证公平性
-
两种形式
-
方法
生成的字节码文件中会多一个ACC_SYNCHRONIZED
标志位,当一个线程访问方法时,会去检查是否存在ACC_SYNCHRONIZED
标识,如果存在,执行线程将先获取monitor
,获取成功之后才能执行方法体,方法执行完后再释放monitor
。在方法执行期间,其他任何线程都无法再获得同一个monitor
对象,也叫隐式同步 -
代码快
加了
synchronized
关键字的代码段,生成的字节码文件会多出monitorenter
和monitorexit
两条指令,每个monitor
维护着一个记录着拥有次数的计数器, 未被拥有的monitor
的该计数器为0
,当一个
线程获执行monitorenter
后,该计数器自增1
;当同一个线程执行monitorexit
指令的时候,计数器再自减1
。当计数器为0
的时候,monitor
将被释放.也叫显式同步
两种本质上没有区别,底层都是通过
monitor
来实现同步, 只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成 -
# 查看字节码
javac XXX.java
javap -v XXX.class
- 同步方法字节码
- 同步代码块字节码
同步容器和并发容器?
- 同步容器
同步容器通过synchronized关键字修饰容器保证同一时刻内只有一个线程在使用容器,从而使得容器线程安全
Hashtable/Vector/同步工具类包装Collections.synXXX
- 并发容器
并发容器指的是允许多线程同时使用容器,并且保证线程安全。而为了达到尽可能提高并发,Java并发工具包中采用了多种优化方式来提高并发容器的执行效率,核心的就是:锁、CAS(无锁)、COW(读写分离)、分段锁。
ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap(ConcurrentSkipListSet和ConcurrentSkipListMap原理一样,它是实现了高并发线程安全的TreeSet。)
设计模式?
AQS继承模板方法设计模式
如何排查内存溢出?
内存溢出现象
- 堆/Perm 区不断增长, 没有下降趋势(回收速度赶不上增长速度), 最后不断触发FullGC
- 每次FullGC后, 堆/Perm 区在慢慢的增长, 最后不断触发FullGC, 甚至crash
- 排查内存溢出
- 使用 jmap 查看哪些对象个数非常多,内存占用多
- 虚拟机统计信息监视工具
jstat”监视虚拟机各种运行状态信息。
jstat命令格式为:
jstat [ option vmid [interval[s|ms] [count]] ] - 分析 dump 文件和堆占用情况
- 定位具体的类和相关代码的调用过程,一步一步的查找问题所在
JVM调优工具?
jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗GChisto,一款专业分析gc日志的工具
ArrayBlockingQueue和LinkedBlockingQueue区别?
-
保证安全机制
ArrayBlockingQueue: ReenTrantLock
LinkedBlockingQueue: 各种锁 -
使用锁对象
ArrayBlockingQueue: ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行
LinkedBlockingQueue: 对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能
- 额外内存开销
ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象
MySQL主键和外键的作用?
定义
主键: 唯一标识一条记录,不能有重复的,不允许为空
外键: 表的外键是另一表的主键, 外键可以有重复的, 可以是空值
作用
主键: 用来保证数据完整性
外键: 用来和其他表建立联系用的
索引: 是提高查询排序的速度
个数
主键: 主键只能有一个
外键: 一个表可以有多个外键
索引: 一个表可以有多个唯一索引
不适合使用外键的场景: 互联网行业,用户量大,并发度高,为此数据库服务器很容易成为性能瓶颈,尤其受IO能力限制
外键缺点:
有性能问题
1.数据库需要维护外键的内部管理;
2.外键等于把数据的一致性事务实现,全部交给数据库服务器完成;
3.有了外键,当做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,而不得不消耗资源;
4.外键还会因为需要请求对其他表内部加锁而容易出现死锁情况;
优点:
- 由数据库自身保证数据一致性,完整性,更可靠,因为程序很难100%保证数据的完整性,而用外键即使在数据库服务器当机或者出现其他问题的时候,也能够最大限度的保证数据的一致性和完整性
- 有主外键的数据库设计可以增加ER图的可读性,这点在数据库设计时非常重要。
- 外键在一定程度上说明的业务逻辑,会使设计周到具体全面。
外键的使用:
address表中user_id为外键,关联了user表中的id,当删除user中的数据时,如果address表中有关联的user_id数据,那么删除失败;
可以使用SET FOREIGN_KEY_CHECKS = 0
设置当前连接对于外键关联失效,这时使用当前连接就可以删除user表中的数据。
count(*),count(1),count(id)?
执行效果:
count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL
count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL
count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计
执行效率:
列名为主键,count(列名)会比count(1)快
列名不为主键,count(1)会比count(列名)快
如果表多个列并且没有主键,则 count(1) 的执行效率优于 count()
如果有主键,则 select count(主键)的执行效率是最优的
如果表只有一个字段,则 select count()最优
UTF8Bmb4区别?
mb4就是most bytes 4的意思,专门用来兼容四字节的unicode,mysql支持的 utf8 编码最大字符长度为 3 字节,如果遇到 4 字节的宽字符就会插入异常了。而emoji一个表情的长度是4个字节。
select * 为什么效率低?
例如,有一个表为t(a,b,c,d,e,f),其中,b列有索引,那么,在磁盘上有两棵b+树,即聚集索引和辅助索引,分别保存(a,b,c,d,e,f)和(b,a),如果查询条件中where条件可以通过b列的索引过滤掉一部分记录,查询就会先走辅助索引,如果用户只需要a列和b列的数据,直接通过辅助索引就可以知道用户查询的数据,如果用户select *,获取了不需要的数据,则首先通过辅助索引过滤数据,然后通过聚集索引获取所有的列,这就多了一次b+树查询,速度必然会慢很多。
主从复制延迟解决方案?
判断主从延迟工具?
Maatkit
工具包中的Seconds_Behind_Master
和mk-heartbeat
延迟的解决:
网络方面:将从库分布在相同局域网内或网络延迟较小的环境中。
硬件方面:从库配置更好的硬件,提升随机写的性能。
配置方面:从库配置sync_binlog=0,innodb_flush_log_at_trx_commit=2,logs-slave-updates=0,增大innodb_buffer_pool_size,让更多操作在Mysql内存中完成,减少磁盘操作。或者升级Mysql5.7版本使用并行复制。
架构方面:比如在事务当中尽量对主库读写,其他非事务中的读在从库。消除一部分延迟带来的数据库不一致。增加缓存降低一些从库的负载。
说说String中hashcode的实现?
源码解析
/**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using {@code int} arithmetic, where {@code s[i]} is the
* <i>i</i>th character of the string, {@code n} is the length of
* the string, and {@code ^} indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
// 默认是0
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
// 这里的for循环等价于多项式: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
以String a = "cat"
为例,首先,c,a,t 的ascii码值分别为99,97,116;字符个数3,也就是总共3次循环
第一次循环:h1=310+val[0]=val[0]=‘c’=99
第二次循环:h2=31h1+val[1]=3199+‘a’=3199+97=3166
第三次循环:h3=31h2+val[2]=313166+‘t’=31*3166+116=98262
所以cat的hashcode值为98262
再用多项式的表示方法检验一下s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1]
9931的平方+9731+116结果也等于98262
String中hashcode方法为什么选择31这个质数而不选择更大的质数呢?
降低 hash 的冲突率
核心多项式: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
分析如下
这里先分析质数2。首先,假设 n = 6,然后把质数2和 n 带入上面的计算公式中第一项的后半部分,就为2^(6-1)
。并仅计算公式中次数最高的那一项,结果是2^5 = 32,是不是很小。所以这里可以断定,当字符串长度不是很长时,用质数2做为乘子算出的哈希值,数值不会很大。也就是说,哈希值会分布在一个较小的数值区间内,分布性不佳,最终可能会导致冲突率上升
上面说了,质数2做为乘子会导致哈希值分布在一个较小区间内,那么如果用一个较大的大质数101会产生什么样的结果呢?根据上面的分析,我想大家应该可以猜出结果了。就是不用再担心哈希值会分布在一个小的区间内了,因为101^5 = 10,510,100,501。但是要注意的是,这个计算结果太大了。如果用 int 类型表示哈希值,结果会溢出,最终导致数值信息丢失。尽管数值信息丢失并不一定会导致冲突率上升,但是我们暂且先认为质数101(或者更大的质数)也不是很好的选择。最后,我们再来看看质数31的计算结果: 31^5 = 28629151,结果值相对于32和10,510,100,501来说。是不是很nice,不大不小。
JVM优化乘法计算
31 * i 可以被JVM优化为(i << 5) - i。
现代JVM可以优化运算,即乘法运算可以被移位和减法运算取代,31 * i
可以用位移和减法代替为(i << 5) - i
hashcode值溢出?
在字符串过长时可能导致int型溢出,但Java中溢出并不会导致运行时错误,而只是溢出位丢失,程序仍然可以执行。如果int型是作为算术运算结果的,那溢出当然会导致结果不正确;但此处int型是作为hash使用的,因此溢出也是可以接受的。
SpringCloud和Dubbo区别?
- SpringCloud
Spring Cloud 是一套完整的微服务解决方案,基于 Spring Boot 框架,准确的说,它不是一个框架,而是一个大的容器,它将市面上较好的微服务框架集成进来,从而简化了开发者的代码量。
是一系列框架的有序集合,它利用 Spring Boot 的开发便利性简化了分布式系统的开发,比如服务发现、服务网关、服务路由、链路追踪等。Spring Cloud 并不重复造轮子,而是将市面上开发得比较好的模块集成进去,进行封装,从而减少了各模块的开发成本。换句话说:Spring Cloud 提供了构建分布式系统所需的“全家桶”。
- Dubbo
Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
其核心部分包含:
- 远程通讯: 提供对多种基于长连接的NIO框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。
- 集群容错: 提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。
- 自动发现: 基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。
Dubbo能做什么?
1.透明化的远程方法调用,就像调用本地方法一样调用远程方法,只需简单配置,没有任何API侵入。
2.软负载均衡及容错机制,可在内网替代F5等硬件负载均衡器,降低成本,减少单点。 - 服务自动注册与发现:不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者。
微服务的优缺点?
-
优点
单⼀职责:每个微服务仅负责⾃⼰业务领域的功能;
⾃治:⼀个微服务就是⼀个独⽴的实体,它可以独⽴部署、升级,服务与服务之间通过REST等形式的标准接⼝进⾏通信,并且⼀个微服务实例可以被替换成另⼀种实现,⽽对其它的微服务不产⽣影响。
逻辑清晰:微服务单⼀职责特性使微服务看起来逻辑清晰,易于维护。
简化部署:单系统中修改⼀处需要部署整个系统,⽽微服务中修改⼀处可单独部署⼀个服务;
可扩展:应对系统业务增⻓的⽅法通常采⽤横向(Scale out)或纵向(Scale up)的⽅向进⾏扩展。分布式系统中通常要采⽤Scale out的⽅式进⾏扩展。
技术异构:不同的服务之间,可以根据⾃⼰的业务特点选择不通的技术架构,如数据库等。 -
缺点
复杂度⾼:服务调⽤要考虑被调⽤⽅故障、过载、消息丢失等各种异常情况,代码逻辑更加复杂;对于微服务间的事务性操作,因为不同的微服务采⽤了不同的数据库,将⽆法利⽤数据库本身的事务机制保证⼀致性,需要引⼊⼆阶段提交等技术。
运维复杂:系统由多个独⽴运⾏的微服务构成,需要⼀个设计良好的监控系统对各个微服务的运⾏状态进⾏监控。运维⼈员需要对系统有细致的了解才对够更好的运维系统。
通信延迟:微服务之间调⽤会有时间损耗,造成通信延迟。
使⽤中碰到的坑?
超时:确保Hystrix超时时间配置为⻓于配置的Ribbon超时时间。
feign path:feign客户端在部署时若有contextpath应该设置 path="/***"来匹配你的服务名。
版本:springboot和springcloud版本要兼容。
什么是服务熔断和服务降级?
服务熔断:当下游的服务因为某种原因突然变得不可用或响应过慢,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
服务降级:
这里有两种场景:
当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,增加响应速度!
当下游的服务因为某种原因不可用,上游主动调用本地的一些降级逻辑,避免卡顿,迅速返回给用户!
服务熔断和服务降级两者异同
相同点
从可用性和可靠性触发,为了防止系统崩溃。
最终让用户体验到的是某些功能暂时不能用。
不同点
服务熔断一般是下游服务故障导致的,而服务降级一般是从整体系统负荷考虑,由调用方控制。
eureka和zookeeper都可以提供服务的注册与发现功能,他们的区别?
Zookeeper保证CP
当向注册中⼼查询服务列表时,我们可以容忍注册中⼼返回的是⼏分钟以前的注册信息,但不能接受服务直接down掉不可⽤。也就是说,服务注册功能对可⽤性的要求要⾼于⼀致性。但是zk会出现这样⼀种情况,当master节点因为⽹络故障与其他节点失去联系时,剩余节点会重新进⾏leader选举。问题在于,选举leader的时间太⻓,30 ~ 120s, 且选举期间整个zk集群都是不可⽤的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因⽹络问题使得zk集群失去master节点是较⼤概率会发⽣的事,虽然服务能够最终恢复,但是漫⻓的选举时间导致的注册⻓期不可⽤是不能容忍的
Eureka保证AP
Eureka看明⽩了这⼀点,因此在设计时就优先保证可⽤性。Eureka各个节点都是平等的,⼏个节点挂掉不会影响正常节点的⼯作,剩余的节点依然可以提供注册和查询服务。⽽Eureka的客户端在向某个Eureka注册或如果发现连接失败,则会⾃动切换⾄其它节点,只要有⼀台Eureka还在,就能保证注册服务可⽤(保证可⽤性),只不过查到的信息可能不是最新的(不保证强⼀致性)。除此之外,Eureka还有⼀种⾃我保护机制,如果在15分钟内超过85%的节点都没有正常的⼼跳,那么Eureka就认为客户端与注册中⼼出现了⽹络故障,此时会出现以下⼏种情况:Eureka不再从注册列表中移除因为⻓时间没收到⼼跳⽽应该过期的服务
Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可⽤)
当⽹络稳定时,当前实例新的注册信息会被同步到其它节点中
因此, Eureka可以很好的应对因⽹络故障导致部分节点失去联系的情况,⽽不会像zookeeper那样使整个注册服务瘫痪。
Eureka原理?
consumer : 服务消费方,eureka client 角色,可以从 eureka server 上拉取到其他已注册服务的信息,从而根据这些信息找到自己所需的服务,然后发起远程调用。
provider : 服务提供方,eureka client 角色,可以向 eureka server 上注册和更新自己的信息,当然作为 eureka client ,它也可以从server 上获取到其他服务的信息。
Eureka server : 服务注册中心,提供服务注册和服务发现功能;
同步复制 : eureka server 之间进行注册服务信息的同步,这样可以保证集群中每个server 都能提供完整的服务信息。
- 每30s发送⼼跳检测重新进⾏租约,如果客户端不能多次更新租约,它将在90s内从服务器注册中⼼移除。
- 注册信息和更新会被复制到其他Eureka 节点,来⾃任何区域的客户端可以查找到注册中⼼信息,每30s发⽣⼀次复制来定位他们的服务,并进⾏远程调⽤。
- 客户端还可以缓存⼀些服务实例信息,所以即使Eureka全挂掉,客户端也是可以定位到服务地址的。consumer中Ribbon的LoadBalancer会读取这个缓存,来知道当前有哪些实例可以调用,从而进行负载均衡。
Dubbo服务注册与发现原理?
- 服务容器负责启动,加载,运⾏服务提供者。
- 服务提供者在启动时,向注册中⼼注册⾃⼰提供的服务。
- 服务消费者在启动时,向注册中⼼订阅⾃⼰所需的服务。
- 注册中⼼返回服务提供者地址列表给消费者,如果有变更,注册中⼼将基于⻓连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选⼀台提供者进⾏调⽤,如果调⽤失败,再选另⼀台调⽤。
- 服务消费者和提供者,在内存中累计调⽤次数和调⽤时间,定时每分钟发送⼀次统计数据到监控中⼼。
限流
- http限流
使⽤nginx的limitzone来完成
- dubbo限
dubbo提供了多个和请求相关的filter:ActiveLimitFilter ExecuteLimitFilter TPSLimiterFilter
-
springcloud限流
-
hystrix配置参数
semaphore.maxConcurrentRequests,coreSize,maxQueueSize和queueSizeRejectionThreshold设置信号量模式下的最⼤并发量、线程池⼤⼩、缓冲区⼤⼩和缓冲区降级阈值。#不设置缓冲区,当请求数超过coreSize时直接降级
hystrix.threadpool.userThreadPool.maxQueueSize=-1 #超时时间⼤于我们的timeout接⼝返回时间 hystrix.command.userCommandKey.execution.isolation.thread.timeoutInMilliseconds=15000
这个时候我们连续多次请求/user/command/timeout接⼝,在第⼀个请求还没有成功返回时,查看输出⽇志可以发现只有第⼀个请求正常的进⼊到user-service的接⼝中,其它请求会直接返回降级信息。这样我们就实现了对服务请求的限流。
-
漏桶算法
⽔(请求)先进⼊到漏桶⾥,漏桶以⼀定的速度出⽔,当⽔流⼊速度过⼤会直接溢出,可以看出漏桶算法能强⾏限制数据的传输速率。 -
令牌桶算法
除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。如图所示,令牌桶算法的原理是系统会以⼀个恒定的速度往桶⾥放⼊令牌,⽽如果请求需要被处理,则需要先从桶⾥获取⼀个令牌,当桶⾥没有令牌可取时,则拒绝服务。
-
-
redis计数器限流
SpringCloud核心组件?
- Eureka:各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从⽽知道其他服务在哪⾥
- Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从⼀个服务的多台机器中选择⼀台
- Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求Hystrix:发起请求是通过
- Hystrix的线程池来⾛的,不同的服务⾛不同的线程池,实现了不同服务调⽤的隔离,避免了服务雪崩的问题Zuul:如果前端、移动端要调⽤后端系统,统⼀从
- Zuul⽹关进⼊,由Zuul⽹关转发请求给对应的服务
JAVA队列
队列的使用场景?
多线程操作共同的队列时不需要额外的同步。
阻塞队列和非阻塞队列区别?
阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。使用阻塞队列不用处理获取元素为null的情况。
- 阻塞队列实用锁机制保证线程安全
- 非阻塞队列实用CAS保证数据安全
常用的阻塞队列和非阻塞队列有哪些?
阻塞队列:
ArrayBlockingQueue: 使用ReentrantLock锁保证元素安全
LinkedBlockingQueue
DelayQueue
非阻塞队列:
ConcurrentLinkedQueue: 使用CAS
MQ消息队列
什么是RabbitMQ
?
采用AMQP(Advanced Message Queuing Protocol)
高级消息队列协议,一个提供统一消息服务的应用层标准高级消息队列协议的一种消息队列技术,最大的特点就是消费并不需要确保提供方存在,实现了服务之间的高度解耦。
为什么要使用MQ?
- 在分布式系统下具备异步,削峰,负载均衡等一系列高级功能
- 拥有持久化的机制,队列中的信息也可以保存下来
- 实现消费者和生产者之间的解耦
- 对于高并发场景下,达到一定量的限流作用,减少系统压力
- 异步处理业务
使用场景?
- 服务间异步通信
- 顺序消费
- 定时任务
- 请求削峰
RabbitMQ相关概念
-
Broker
它提供一种传输服务,它的角色就是维护一条从生产者到消费者的路线,保证数据能按照指定的方式进行传输,
-
Exchange
消息交换机,类似路由,它指定消息按什么规则,路由到哪个队列。有多种交换器
fanout
: 1:N 可以把一个消息并行发布到多个队列上去direct
: 1:1topic
: N:1 多个交换器可以路由消息到同一个队列 -
Queue
消息的载体,每个消息都会被投到一个或多个队列。
-
Binding
: 绑定,它的作用就是把exchange
和queue
按照路由规则绑定起来. -
Routing Key
路由关键字,
exchange
根据这个关键字进行消息投递。 -
vhost
虚拟主机,一个
broker
里可以有多个vhost
,用作不同用户的权限分离。 -
Producer
消息生产者,就是投递消息的程序.
-
Consumer
消息消费者,就是接受消息的程序.
-
Channel(信道)
信道是生产消费者与
MQ
通信的渠道,生产者publish
或是消费者subscribe
一个队列都是通过信道来通信的,可建立多个channel
,信道是建立在TCP
连接上的虚拟连接,就是说MQ
在一条TCP
上建立成百上千个信道来达到多个线程处理。 -
持久化(duration)
开启持久化功能,需同时满足:消息投递模式选择持久化、交换器开启持久化、队列开启持久化
-
确认机制(ACK)
- 发送方确认模式:消息发送到交换器–发送完毕–>消息投递到队列或持久化到磁盘异步回调通知生产者
- 消费者确认机制:消息投递消费者-
ack
-删除该条消息-投递下一条
provider
生产消息发送方式有哪些?
SYNC
同步发送
应用场景:重要通知邮件、报名短信通知、营销短信系统等。
ASYNC
异步发送
应用场景:对RT时间敏感,可以支持更高的并发,回调成功触发相对应的业务,比如注册成功后通知积分系统发放优惠券。
ONEWAY
无需要等待响应
应用场景:主要是日志收集,适用于某些耗时非常短,但对可靠性要求并不高的场景, 也就是LogServer
, 只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求 不等待应答。
如何确保消息正确地发送至RabbitMQ
?如何确保消息接收方消费了消息?如何保证消息一致性的?
-
发送方确认模式
-
transaction
模式transaction
机制就是说,发送消息前,开启事物(channel.txSelect())
,然后发送消息,如果发送过程中出现什么异常,事物就会回滚(channel.txRollback())
,如果发送成功则提交事物(channel.txCommit())
。 -
confirm
模式- 将信道设置成
confirm
模式(发送方确认模式)。 - 一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一
ID
)。 - 发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
- 将信道设置成
-
-
接收方确认机制
- 消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,
RabbitMQ
才能安全地把消息从队列中删除。 - 接收方确认消息并没有用到超时机制,
RabbitMQ
仅通过Consumer
的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ
给了Consumer
足够长的时间来处理消息。
- 消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,
-
消息一致性特殊场景
- 如果消费者接收到消息,在确认之前断开了连接或取消订阅,
RabbitMQ
会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)。 - 如果消费者接收到消息却没有确认消息,连接也未断开,则
RabbitMQ
认为该消费者繁忙,将不会给该消费者分发更多的消息。
- 如果消费者接收到消息,在确认之前断开了连接或取消订阅,
如何避免消息重复投递或重复消费?
-
解决
provider
重复投递的问题- 成功投递后
rabbitMQ
就会发送一个ACK
给生产者(包含消息的唯一ID
),这就使得生产者知道消息已经正确到达目的队列了。 - 在消息生产时,
MQ
内部针对每条生产者发送的消息生成一个inner-msg-id
,作为去重的依据(消息投递失败并重传),避免重复的消息进入队列。 - 将信道设置成
confirm
模式(发送方确认模式),保存投递成功生成的唯一ID
,把这个唯一ID
进行存储。
- 成功投递后
-
解决
consumer
重复消费问题Redis
的setNX
操作Redis
的Incr
原子操作- 数据库唯一主键或索引
如何保证消息可靠性传输的?
如何处理消息队列的数据丢失?
使用队列数据持久化,将queue
的持久化标识durable
设置为true
,则代表是一个持久的队列,这样设置以后,rabbitMQ
就算挂了,重启后也能恢复数据。
如何处理消费者丢失数据?
-
自动确认模式
provider
和consumer
在没有断开情况下,消费者消息成功后会进行ACK
确认通知消息队列,消息队列在没有确认成功消费该消息时重回队列,自动确认可能存在重复消费的问题。 -
手动确认模式
需要关闭
autoAck
,等自己处理完之后在finally
代码块中发送ack
给MQ
。 -
不确认模式
acknowledge="none"
不使用确认机制,对于MQ
来说只要消息发送完成会立即在队列移除,无论客户端异常还是断开,只要发送完就移除,不会重发。
如何保证消息的顺序性?
拆分多个queue
,每个queue
一个consumer
,就是多一些queue
而已,确实是麻烦点;这样也会造成吞吐量下降,可以在消费者内部采用多线程的方式取消费。
消息基于什么传输?
由于TCP
连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ
使用信道的方式来传输数据。信道是建立在真实的TCP
连接内的虚拟连接,且每条TCP
连接上的信道数量没有限制。
TCP连接和信道之间的关系?
-
在应用程序与代理服务器之间创建一个
TCP
连接,TCP
一旦打开,就会创建AMQP
信道 -
电缆相当于
TCP
,信道是一条独立光纤束,一条TCP
连接上创建多少条信道是没有限制的。
为什么不使用TCP
连接发送AMQP
命令?
1.创建和销毁TCP
会话,开销大,性能瓶颈
2.如果使用TCP
,每个线程连接到RabbitMQ
,都要创建连接,造成浪费和性能瓶颈
3.使用一个TCP
连接,能够满足性能,并且保证线程私密性。效果更好。
AMQP
消息路由组成关系?
AMQP
消息路由必须三部分:交换器,队列,绑定
生产者把消息发布到交换器,消息最后到达队列,被消费者接收,绑定决定了消息如何从路由器路由到特定的队列。
RabbitMQ
是如何分发消息的?
若该队列至少有一个消费者订阅,消息将以循环(round-robin)
的方式发送给消费者。每条消息只会分发给一个订阅的消费者(前提是消费者能够正常处理消息并进行确认)
消息是如何路由的?
消息路由到队列过程: 消息提供方->交换器(进行路由)->一至多个队列
消息发布到交换器时,消息将拥有一个路由键routing key
,在消息创建时设定。通过队列路由键,可以把队列绑定到交换器上。消息到达交换器后,RabbitMQ
会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则);常用的交换器主要分为一下三种
- 订阅模式
fanout
:如果交换器收到消息,将会广播到所有绑定的队列上 - 路由模式
direct
:如果路由键完全匹配,消息就被投递到相应的队列 - 通配符模式
topic
:可以使来自不同源头的消息能够到达同一个队列。 使用topic
交换器时,可以使用通配符
MQ镜像集群模式?
集群环境下无论元数据还是queue
里的消息都会存在于多个实例上,然后每次你写消息到queue
的时候,都会自动把消息到多个实例的queue
里进行消息同步。
集群模式下执行方案:
- 多主多从架构
- 同步双写,异步刷盘
同步双写:可靠性更高,消息不容易丢失,性能差
异步刷盘:机器重启或断电后消息容易丢失
六种消费模式?
simple
模式
- *消息产生消息,将消息放入队列。
- *消息的消费者(
consumer
) 监听消息队列,如果队列中有消息,就消费掉,消息被拿走后,自动从队列中删除(隐患 消息可能没有被消费者正确处理,已经从队列中消失了,造成消息的丢失,这里可以设置成手动的ack
,但如果设置成手动ack
,处理完后要及时发送ack
消息给队列,否则会造成内存溢出)。 - 简单模式只有一个消费者
应用场景: 聊天室
work
工作模式(资源的竞争)
work
模式可以有多个消费者
应用场景:红包
-
保证只有一个
consumer
消费成功,// 这个是每次只处理一条数据,只有接收到ack确认码,才去拿取下一条消息 channel.basicQos(0,1,false);
publish
/subscribe
发布订阅(共享资源)
-
每个消费者监听自己的队列;
-
生产者将消息发给
broker
,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息。
应用场景:邮件群发,群聊天,广告
routing
路由模式
-
交换机和
Queue
根据路由规则事先绑定 -
根据业务功能定义路由字符串,交换机根据这个路由字符串找到对应的
Queue
,从而发布消息到Queue
中
应用场景: 项目中的error
报错
topic
主题模式(路由模式的一种)
-
路由模式的一种实现
-
路由功能添加模糊匹配
#
匹配一个词或多个词*
只匹配一个词
RPC
模式
(RPC) Remote Procedure Call Protocol 远程过程调用协议
在一个大型的公司,系统由大大小小的服务构成,不同的团队维护不同的代码,部署在不同的机器。但是在做开发时候往往要用到其它团队的方法,因为已经有了实现。但是这些服务部署不同的机器上,想要调用就需要网络通信,这些代码繁琐且复杂,一不小心就会写的很低效。RPC协议定义了规划,其它的公司都给出了不同的实现。比如微软的wcf,以及现在火热的WebApi。
在RabbitMQ中RPC的实现也是很简单高效的,现在我们的客户端、服务端都是消息发布者与消息接收者。
首先客户端通过RPC向服务端发出请求
我这里有一堆东西需要你给我处理一下,correlation_id:这是我的请求标识,erply_to:你处理完过后把结果返回到这个队列中。
服务端拿到了请求,开始处理并返回
correlation_id:这是你的请求标识 ,原封不动的给你。 这时候客户端用自己的correlation_id与服务端返回的id进行对比。是我的,就接收。
simple
和work
消息模式区别?
消息模式 | 消费者个数 | 是否手动消息确认 | 优化消费者能力 |
---|---|---|---|
simple | 一个 | 默认进行消息完成确认 | 不能平衡 |
work | 多个 | 需手动完成确认 channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false) | MQ默认是轮询发布,也就说每个消费者获取到的消息数量相同,consumer可以使用channel.basicQos(1);设置basicQos=1,含义是:同一时刻服务器只会发一条消息给消费者,消费者确认消费后,队列才能发下一条消息,从而实现了能者多劳的意义,具有优化消费能力的特点 |
``Spring IOC注入
bean`的方式?
构造方法注入
在spring
的配置文件中注册UserService
,将UserDaoJdbc
通过constructor-arg
标签注入到UserService
的某个有参数的构造方法
<!-- 注册userService -->
<bean id="userService" class="com.lyu.spring.service.impl.UserService">
<constructor-arg ref="userDaoJdbc"></constructor-arg>
</bean>
<!-- 注册jdbc实现的dao -->
<bean id="userDaoJdbc" class="com.lyu.spring.dao.impl.UserDaoJdbc"></bean>
如果只有一个有参数的构造方法并且参数类型与注入的bean的类型匹配,那就会注入到该构造方法中。
public class UserService implements IUserService {
private IUserDao userDao;
public UserService(IUserDao userDao) {
this.userDao = userDao;
}
public void loginUser() {
userDao.loginUser();
}
}
@Test
public void testDI() {
ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
// 获取bean对象
UserService userService = ac.getBean(UserService.class, "userService");
// 模拟用户登录
userService.loginUser();
}
setter
注入
<!-- 注册userService -->
<bean id="userService" class="com.lyu.spring.service.impl.UserService">
<!-- 写法一 -->
<!-- <property name="UserDao" ref="userDaoMyBatis"></property> -->
<!-- 写法二 -->
<property name="userDao" ref="userDaoMyBatis"></property>
</bean>
<!-- 注册mybatis实现的dao -->
<bean id="userDaoMyBatis" class="com.lyu.spring.dao.impl.UserDaoMyBatis"></bean>
注:上面这两种写法都可以,spring
会将name
值的每个单词首字母转换成大写,然后再在前面拼接上"set"
构成一个方法名,然后去对应的类中查找该方法,通过反射调用,实现注入。
切记:name
属性值与类中的成员变量名以及set
方法的参数名都无关,只与对应的set
方法名有关,下面的这种写法是可以运行成功的
public class UserService implements IUserService {
private IUserDao userDao1;
public void setUserDao(IUserDao userDao1) {
this.userDao1 = userDao1;
}
public void loginUser() {
userDao1.loginUser();
}
}
还有一点需要注意:如果通过set
方法注入属性,那么spring
会通过默认的空参构造方法来实例化对象,所以如果在类中写了一个带有参数的构造方法,一定要把空参数的构造方法写上,否则spring没有办法实例化对象,导致报错。
基于注解的注入
@Bean
: 导入第三方的类或包的组件- 包扫描+组件的标注注解(
@ComponentScan
:@Controller
,@Service
@Reponsitory
@Componet
),一般是针对 我们自己写的类,使用这个 @Import
:[快速给容器导入一个组件]
spring
事务实现方式有哪些?
-
编程式事务管理对基于
POJO
的应用来说是唯一选择。我们需要在代码中调用beginTransaction()
、commit()
、rollback()
等事务管理相关的方法,这就是编程式事务管理。 -
基于
TransactionProxyFactoryBean
的声明式事务管理 -
基于
@Transactional
的声明式事务管理 -
基于
Aspectj AOP
配置事务
Spring
中的事务原理?
AOP
Spring IOC
循环依赖?
检测是否存在循环依赖?
A对象依赖B对象,B对象又依赖A对象。
Bean
在创建的时候可以给该Bean打标,如果递归调用回来发现正在创建中的话,即说明了循环依赖了。
Spring
是如何解决循环依赖的?
构造器循环依赖
通过构造器注入构成的循环依赖,此依赖是无法解决的,只能抛出BeanCurrentlyInCreationException异常表示循环依赖
setter循环依赖
通过setter注入方式构成的循环依赖。
原理:对于setter注入造成的依赖是通过Spring容器提前暴露刚完成构造器注入但未完成其他步骤(比如setter注入)的bean来完成的,而且只能解决单例作用域的bean循环依赖。
Spring解决循环依赖
Spring中会通过各种Bean中间状态来达到Bean还未实例化完成时提前将Bean提前注入到依赖Bean的属性中,假设说Bean有三种状态分别是青年态(一级缓存)、胚胎态(二级缓存)、小蝌蚪态(三级缓存)其中青年态代表Bean已经实例化完成,可以直接使用了,胚胎态代表Bean已经存在了但是还在创建中,还未创建完毕,小蝌蚪态代表还未开始创建,但是随时可以进行创建,三个状态就类似于三个等级,可以逐步提升从小蝌蚪状态提升到胚胎状态然后再提升到青年态,然后Spring开始创建Bena时会提前将Bean存放到小蝌蚪态的缓存集合中,当发现存在循环依赖时会使用存在于小蝌蚪状态缓存集合中的Bean
Spring
的循环依赖的理论依据其实是基于Java
的引用传递,当我们获取到对象的引用时,对象的field
或则属性是可以延后设置的(但是构造器必须是在获取引用之前)。
Spring
的单例对象的初始化主要分为三步:
createBeanInsrtance
实例化->populateBean
属性填充->InitializeBean
初始化
createBeanInstance
:实例化,其实也就是调用对象的构造方法实例化对象。populateBean
:填充属性,这一步主要是多bean的依赖属性进行填充。initializeBean
:调用spring xml
中的init
方法。
从上面讲述的单例bean
初始化步骤我们可以知道,循环依赖主要发生在第一、第二部。也就是构造器循环依赖和field
循环依赖。
/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
这三级缓存分别指:
singletonFactories
: 单例对象工厂的cache
earlySingletonObjects
:提前暴光的单例对象的Cache
singletonObjects
:单例对象的cache
我们在创建bean的时候,首先想到的是从cache中获取这个单例的bean,这个缓存就是singletonObjects。主要调用方法就就是:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
上面的代码需要解释两个参数:
isSingletonCurrentlyInCreation()
判断当前单例bean
是否正在创建中,也就是没有初始化完成(比如A
的构造器依赖了B
对象所以得先去创建B
对象, 或则在A
的populateBean
过程中依赖了B
对象,得先去创建B
对象,这时的A
就是处于创建中的状态。)allowEarlyReference
是否允许从singletonFactories
中通过getObject
拿到对象
分析getSingleton()
的整个过程,Spring
首先从一级缓存singletonObjects
中获取。如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects
中获取。如果还是获取不到且允许singletonFactories
通过getObject()
获取,就从三级缓存singletonFactory.getObject()
(三级缓存)获取,如果获取到了则:
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
从singletonFactories
中移除,并放入earlySingletonObjects
中。其实也就是从三级缓存移动到了二级缓存。
从上面三级缓存的分析,我们可以知道,Spring
解决循环依赖的诀窍就在于singletonFactories
这个三级cache
。这个cache
的类型是ObjectFactory
,定义如下:
public interface ObjectFactory<T> {
T getObject() throws BeansException;
}
这个接口在下面被引用
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
这里就是解决循环依赖的关键,这段代码发生在createBeanInstance
之后,也就是说单例对象此时已经被创建出来(调用了构造器)。这个对象已经被生产出来了,虽然还不完美(还没有进行初始化的第二步和第三步),但是已经能被人认出来了(根据对象引用能定位到堆中的对象),所以Spring
此时将这个对象提前曝光出来让大家认识,让大家使用。
这样做有什么好处呢?让我们来分析一下A
的某个field
或者setter
依赖了B
的实例对象,同时B
的某个field
或者setter
依赖了A
的实例对象,这种循环依赖的情况。A
首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories
中,此时进行初始化的第二步,发现自己依赖对象B
,此时就尝试去get(B)
,发现B
还没有被create
,所以走create
流程,B
在初始化第一步的时候发现自己依赖了对象A
,于是尝试get(A)
,尝试一级缓存singletonObjects
(肯定没有,因为A
还没初始化完全),尝试二级缓存earlySingletonObjects
(也没有),尝试三级缓存singletonFactories
,由于A
通过ObjectFactory
将自己提前曝光了,所以B
能够通过ObjectFactory.getObject
拿到A
对象(虽然A
还没有初始化完全,但是总比没有好呀),B
拿到A
对象后顺利完成了初始化阶段1
、2
、3
,完全初始化之后将自己放入到一级缓存singletonObjects
中。此时返回A
中,A
此时能拿到B
的对象顺利完成自己的初始化阶段2
、3
,最终A
也完成了初始化,进去了一级缓存singletonObjects
中,而且更加幸运的是,由于B
拿到了A
的对象引用,所以B
现在hold
住的A
对象完成了初始化。
知道了这个原理时候,肯定就知道为啥Spring
不能解决A
的构造方法中依赖了B
的实例对象,同时B
的构造方法中依赖了A
的实例对象这类问题了!因为加入singletonFactories
三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决。
解读SpringIOC
源码以及后置处理器如何工作的?
SpringIOC
源码解读
// 这个构造方法会把Spring所有的环境都准备好
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
// 这个类有父类,所以会先初始化父类的构造方法,接着初始化自己的构造方法
// 调用无参构造方法进行初始化一个读取器和扫描仪
this();
// 把配置类加载进 DefaultListableBeanFactory 的map集合中
// 配置类可以一次性传多个,这个方法执行后,只是把配置类加载进了 DefaultListAbleBeanFactory的map集合中
// 还没有扫描其他的的加了组件的类
register(annotatedClasses);
// 实例化所有被加了组件的对象
refresh();
}
refresh
方法
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 调用容器准备刷新的方法,获取容器的当时时间,同时给容器设置同步标识
// 这个方法不是重点,可以暂时认为他不干任何事情
prepareRefresh();
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 为BeanFactory配置类加载器、后置处理器等等
// 这个方法比较重要
prepareBeanFactory(beanFactory);
try {
postProcessBeanFactory(beanFactory);
// 扫描注解配置下需要初始化到容器的bean有哪些,并放到beanFactory中的beanDefinitionMap中
invokeBeanFactoryPostProcessors(beanFactory);
registerBeanPostProcessors(beanFactory);
initMessageSource();
initApplicationEventMulticaster();
onRefresh();
registerListeners();
finishBeanFactoryInitialization(beanFactory);
finishRefresh();
}
}
}
prepareBeanFactory
方法
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
//添加一个类加载器
beanFactory.setBeanClassLoader(getClassLoader());
//bean的表达式解析,以后再讲,在前台页面可以获取到bean表达式的一些属性
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
//对象与string类型的转换 <property >
beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));
//重要方法
//为Spring添加后置处理器 ApplicationContextAwareProcessor 实现了BeanProcessor接口
//我们主要查看 重写接口的 postProcessBeforeInitialization ()方法
//主要作用:判断当前bean对象实现了里面的那些接口,从而在bean类获取对应的对象
//一般我们会实现 ApplicationContextAware 接口获取 applicationContext
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
//以下是添加自动注入忽略的列表,不是重点
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);
//等讲到bean的实例化的过程时,会说明
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
beanFactory.registerResolvableDependency(ResourceLoader.class, this);
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
beanFactory.registerResolvableDependency(ApplicationContext.class, this);
//这个ApplicationListenerDetector作用: 某个类实现了ApplicationListener接口,可以获取到一个ApplicationEvent对象
//ApplicationEvent对象存储有reader,scanner,registry,beanFactory等对象
//注意实现ApplicationListener的前提是,这个bean必须是单例的
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));
//剩余的代码省略.....
}
Spring
中有哪些后置处理器?
实现了BeanPostProcessor
接口的实现类
org.springframework.context.support.ApplicationContextAwareProcessor
org.springframework.context.annotation.ConfigurationClassPostProcessor
org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor
org.springframework.context.support.ApplicationListenerDetector
后置处理器如何初始化Bean
的?
-
创建定义
Bean
,返回一个包装Bean
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
-
为包装
Bean
属性赋值populateBean(beanName, mbd, instanceWrapper);
-
执行
Bean
初始化的初始化方法initializeBean(beanName, exposedObject, mbd)
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean(java.lang.String, java.lang.Object, org.springframework.beans.factory.support.RootBeanDefinition)
beanName
: bean名称exposedObject
: 要初始化的Bean
,先定义出来了,并作为暴露Bean
以便后续使用
-
applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName)
在这个方法中,获取所有前置处理器,并执行处理器实现方法
postProcessBeforeInitialization()
。 -
applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName)
在这个方法中,获取所有后置处理器,并执行处理器实现方法
postProcessAfterInitialization()
。 -
添加到
Spring IOC
容器中addSingleton(beanName, singletonObject);
/** Cache of singleton objects: bean name --> bean instance */ private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); /** Cache of singleton factories: bean name --> ObjectFactory */ private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); /** Cache of early singleton objects: bean name --> bean instance */ private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); /** Set of registered singletons, containing the bean names in registration order */ private final Set<String> registeredSingletons = new LinkedHashSet<>(256); /** * Add the given singleton object to the singleton cache of this factory. * <p>To be called for eager registration of singletons. * @param beanName the name of the bean * @param singletonObject the singleton object */ protected void addSingleton(String beanName, Object singletonObject) { synchronized (this.singletonObjects) { this.singletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } }
SpringBoot
启动流程?
@SpringBootApplication
注解
@SpringBootApplication
该注解是一个复合注解,包括@SpringBootConfiguration
,@EnableAutoConfiguration
,@ComponentScan
,使用这三个注解来代替@SpringBootApplication
可以起到相同的作用。
@SpringBootConfiguration
@SpringBootConfiguration
也是来源于 @Configuration
,二者功能都是将当前类标注为配置类注入容器,并将当前类里以 @Bean
注解标记的方法的实例注入到容器中,实例名即为方法名。
@EnableAutoConfiguration
@EnableAutoConfiguration
注解启用自动配置,其可以帮助SpringBoot
应用将所有符合条件的@Configuration
配置都加载到当前IoC
容器之中。
-
@EnableAutoConfiguration
借助AutoConfigurationImportSelector
的帮助,而后者通过实现selectImports()
方法来导出Configuration
@Override public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } try { AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader .loadMetadata(this.beanClassLoader); AnnotationAttributes attributes = getAttributes(annotationMetadata); List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); configurations = removeDuplicates(configurations); configurations = sort(configurations, autoConfigurationMetadata); Set<String> exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = filter(configurations, autoConfigurationMetadata); fireAutoConfigurationImportEvents(configurations, exclusions); return configurations.toArray(new String[configurations.size()]); } catch (IOException ex) { throw new IllegalStateException(ex); } }
-
AutoConfigurationImportSelector
类的selectImports()
方法里面通过调用Spring Core``包里
SpringFactoriesLoader类的
loadFactoryNames()`方法 -
最终通过
SpringFactoriesLoader.loadFactoryNames()
读取了ClassPath
下面的META-INF/spring.factories
文件来获取所有导出类。
\org\springframework\boot\spring-boot-autoconfigure\1.5.9.RELEASE\spring-boot-autoconfigure-1.5.9.RELEASE.jar!\META-INF\spring.factories
- 总结
从ClassPath
下扫描所有的META-INF/spring.factories
配置文件,并将spring.factories
文件中的EnableAutoConfiguration
对应的配置项通过反射机制实例化为对应标注了@Configuration
的形式的IoC
容器配置类,然后注入IoC
容器。
@ComponentScan
@ComponentScan
对应于XML
配置形式中的<context:component-scan>
,用于将一些标注了特定注解的bean
定义批量采集注册到Spring
的IoC
容器之中,这些特定的注解大致包括:
@Controller
@Entity
@Component
@Service
@Repository
SpringApplication
是Springboot的核心启动类
SpringApplication
的run()
流程
- 通过 SpringFactoriesLoader 加载 META-INF/spring.factories 文件,获取并创建 SpringApplicationRunListener 对象
- 然后由 SpringApplicationRunListener 来发出 starting 消息
- 创建参数,并配置当前 SpringBoot 应用将要使用的 Environment
- 完成之后,依然由 SpringApplicationRunListener 来发出 environmentPrepared 消息
- 创建 ApplicationContext
- 初始化 ApplicationContext,并设置 Environment,加载相关配置等
- 由 SpringApplicationRunListener 来发出 contextPrepared 消息,告知SpringBoot 应用使用的 ApplicationContext 已准备OK
- 将各种 beans 装载入 ApplicationContext,继续由 SpringApplicationRunListener 来发出 contextLoaded 消息,告知 SpringBoot 应用使用的 ApplicationContext 已装填OK
- refresh ApplicationContext,完成IoC容器可用的最后一步
- 由 SpringApplicationRunListener 来发出 started 消息
- 完成最终的程序的启动
- 由 SpringApplicationRunListener 来发出 running 消息,告知程序已运行起来了
Spring SpringMVC SpringBoot 常用注解说明?
-
Spring常用注解
@Autowired
@Qualifier
@Resource
@Service
使用注解来构造IoC容器
@Component
@Controller
@Service
@Repository -
Spring MVC常用注解
@Controller
@RequestMapping
@PathVariable
@RequestParam
@ResponseBody -
Spring Boot常用注解
@SpringBootApplication
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
@RestController
@Profiles
参考文章:https://www.jianshu.com/p/9dc7a385d19e
参考文章:https://blog.csdn.net/qq_35119422/article/details/81559410
静态,构造,父子类加载顺序?
class Fu2 {
static int fuStaticVariable = 3;
int fuVariable = 4;
public Fu2() {
System.out.println(fuVariable);
System.out.println(fuStaticVariable);
System.out.println("Fu-构造");
}
{
System.out.println(fuVariable);
System.out.println(fuStaticVariable);
System.out.println("Fu-非静态代码块");
}
static {
System.out.println(fuStaticVariable);
System.out.println("Fu-静态代码块");
}
}
public class Zi2 extends Fu2 {
static {
new Fu2();
System.out.println("Zi-静态代码块");
}
static Fu2 fu2 = new Fu2();
public Zi2() {
System.out.println(ziVariable);
System.out.println(fu2);
System.out.println("Zi-构造");
}
{
System.out.println(fu2);
System.out.println("Zi-非静态代码块");
}
int ziVariable = 1;
public static void main(String[] args) {
new Zi2();
}
/**
* 1. 加载 fuStaticVariable = 3
* 2. 3
* 3. Fu-静态代码块
* 4. 加载 fuVariable = 4
* 5. 4
* 6. 3
* 7. Fu-非静态代码块
* 8. 4
* 9. 3
* 10. Fu-构造
* 11. Zi-静态代码块
* 12. 4
* 13. 3
* 14. Fu-非静态代码块
* 15. 4
* 16. 3
* 17. Fu-构造
* 18. 4
* 19. 3
* 20. Fu-非静态代码块
* 21. 4
* 22. 3
* 23. Fu-构造
* 24. fu2对象
* 25. Zi-非静态代码块
* 26. 加载 ziVariable = 1
* 27. 1
* 28. fu2对象
* 29. Zi-构造
*/
}
总结
1. 父类优于子类
2. 静态优于非静态
3. 非静态优于构造
4. 只有new对象或new子对象才会加载父非静态成员变量和非静态方法,最后加载构造
缓存穿透,缓存击穿,缓存雪崩
-
缓存穿透
正常情况下,查询的数据都存在,如果请求一个不存在的数据,也就是缓存和数据库都查不到这个数据,每次都会去数据库查询,这种查询不存在数据的现象我们称为缓存穿透。
-
解决办法
-
缓存空值
之所以发生穿透,是因为缓存中没有存储这些数据的
key
,从而每次都查询数据库
我们可以为这些key
在缓存中设置对应的值为null
,后面查询这个key
的时候就不用查询数据库了
当然为了健壮性,我们要对这些key
设置过期时间,以防止真的有数据 -
BloomFilter
BloomFilter
类似于一个hbase set
用来判断某个元素(key)
是否存在于某个集合中
我们把有数据的key
都放到BloomFilter
中,每次查询的时候都先去BloomFilter
判断,如果没有就直接返回null
注意BloomFilter
没有删除操作,对于删除的key
,查询就会经过BloomFilter
然后查询缓存再查询数据库,所以BloomFilter
可以结合缓存空值用,对于删除的key
,可以在缓存中缓存null
-
-
-
缓存击穿
在高并发的情况下,大量的请求同时查询同一个
key
时,此时这个key
正好失效了,就会导致同一时间,这些请求都会去查询数据库,这样的现象我们称为缓存击穿。-
带来的问题
会造成某一时刻数据库请求量过大。
-
解决办法
采用分布式锁,只有拿到锁的第一个线程去请求数据库,然后插入缓存,当然每次拿到锁的时候都要去查询一下缓存有没有。
-
-
缓存雪崩
当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了。-
解决办法
采用集群,降低服务宕机的概率
ehcache
本地缓存 +Hystrix
限流&
降级
ehcache
本地缓存的目的也是考虑在Redis Cluster
完全不可用的时候,ehcache
本地缓存还能够支撑一阵
使用Hystrix
进行限流&
降级 ,比如一秒来了5000
个请求,我们可以设置假设只能有一秒2000
个请求能通过这个组件,那么其他剩余的3000
请求就会走限流逻辑。
-
Spring有哪些模块?
MyBatis懒加载原理?
它的原理是,使用CGLIB
创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName()
,拦截器invoke()
方法发现a.getB()
是null
值,那么就会单独发送事先保存好的查询关联B
对象的sql
,把B
查询上来,然后调用a.setB(b)
,于是a
的对象b
属性就有值了,接着完成a.getB().getName()
方法的调用。这就是延迟加载的基本原理。
MyBatis分页原理?
PageHelper
分页插件,使用动态代理
org.apache.ibatis.binding.MapperProxy#invoke
com.github.pagehelper.PageInterceptor
org.apache.ibatis.plugin.Plugin#invoke
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)
org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke
org.apache.ibatis.executor.CachingExecutor@32ba35b2
动态代理
分页插件的基本原理是使用Mybatis
提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql
,然后重写sql
,根据dialect
方言,添加对应的物理分页语句和物理分页参数。
如何实现自己的spring boot starter
工程
springboot
组件有哪些?
starter
spring-boot-starter-web
spring-boot-starter-thymeleaf
spring-boot-starter-data-redis
spring-boot-starter-data-jpa
spring-boot-starter-data-elasticsearch
spring-boot-starter-data-mongodb
spring-boot-starter-test
spring-boot-starter-jdbc
spring-boot-starter-aop
spring-boot-starter-security
spring-boot-starter-actuator
spring-boot-starter-tomcat
Auto-configuration
Srping Boot Cli
actuator
springboot beanFactory
实现原理?
BeanFactory
是一个接口,它是Spring
中工厂的顶层规范,是SpringIoc
容器的核心接口,它定义了getBean()
、containsBean()
等管理Bean
的通用方法。
FactoryBean
其实就是个工厂Bean
接口,本质上也还是个bean
,调用getObject()
方法所返回的bean
。
SpringBoot factory
文件位置?
\org\springframework\boot\spring-boot\1.5.9.RELEASE\spring-boot-1.5.9.RELEASE.jar!\META-INF\spring.factories
spring
源码包有哪些?
为什么要使用SpringCloud
?
如果微服务架构选型选择了dubbo,那么后续一些其他的组件,还需要我们自己去评估目前市面上一些开源的组件是否符合我们的需要,而如果架构选型选择了springcloud,那么就能省心很多,springcloud本身就提供了一整套的微服务的解决方案,虽然有很多组件目前看起来依然不是很成熟,不过这依然大大降低了我们在架构选型上的工作量。
双向链表的优势
-
LinkedList
集合是双向链表数据结构。先看下
LinkedList
获取元素源码/** * Returns the (non-null) Node at the specified element index. */ Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
在
node(index).item
获取脚标元素时判断脚标在后半部分还是前半部分,从而提高查询效率
LinkedHashMap
实现原理
LinkedHashMap
数据结构:【数组+链表/红黑树】(即散列表) + 【内部实现双向循环列表】,在put元素时,数据结构和存储处理方式和hashmap相同,只不过在存放成功后又维护了一个双向链表。
-
链表顺序(插入顺序和访问顺序)
-
默认按照插入顺序排序
-
访问排序
当主动传入的accessOrder参数为true时,使用put方法新加入的元素,如果遇到了哈希冲突,并且对key值相同的元素进行了替换,就会被放在双向链表的尾部,当元素超过上限且removeEldestEntry方法返回true时,直接删除最早元素以便新元素插入。如果没有冲突直接放入,同样加入到链表尾部。使用get方法时会把get到的元素放入双向链表尾部。
// true 为访问顺序排序,当访问某个元素后,会把这个元素放到链表的尾部 Map<String, String> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);
源码
当访问元素时,把该元素方法链表尾部
void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.after = null; if (b == null) head = a; else b.after = a; if (a != null) a.before = b; else last = b; if (last == null) head = p; else { p.before = last; last.after = p; } tail = p; ++modCount; } }
-
执行下面sql一共创建几个索引?
create table t1(
C1 char(10) not null PRIMARY key,
C2 INT,
C3 CHAR(10),
C4 CHAR(10) NOT NULL,
CONSTRAINT c4 UNIQUE (c1,c4)
);
CREATE INDEX index1 on t1(c2 ASC);
show index from t1;
t1 0 PRIMARY 1 C1 A 0 BTREE
t1 0 c4 1 C1 A 0 BTREE
t1 0 c4 2 C4 A 0 BTREE
t1 1 index1 1 C2 A 0 YES BTREE
4个
为什么要分库
服务器资源是有限的,比如宽带,IO,内存,以及mysql服务的连接数,IO处理,高并发下,MySQL
效率达到瓶颈,为了缓解MySQL
压力和提高查询效率,把没有关联的表进行分类放到不同的数据库中。