Java高手真经

目录

目录

一.Java基础

1.移位运算符

2.continue、break、return

3.对于包装数据类型来说,==比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用equals方法

4.为什么说是几乎所有对象实例都存放在与堆中?这是因为hotspot虚拟机引入了jit优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存

5.Java基本数类型的包装类型的大部分都用到了缓存机制来提升性能

6.为什么浮点数运算的时候会有精度丢失的风险

7.成员变量与局部变量的区别

8.静态变量

9.字符型常量和字符串常量的区别

10.可变长参数

11.面向过程和面向对象区别

12.封装、继承、多态

13.接口、抽象类

14.深拷贝和浅拷贝

 15.Object常见的方法

16.hashCode的作用

17.String,StringBuffer,StirngBuilder

18.字符串常量池的作用

19.Checked Exception 和 Unchecked Exception

20.try-with-resources

 21.泛型

22.反射

23.重要知识点

1.BigDecimal

2.unsafe

3.spi

4.语法糖

24.集合

25.集合(二)

1.HashMap和HashTable

2.hashSet如何检查重复

3.HashMap底层实现

4.ConcurrentHashMap

5.集合判空

6.CopyOnWriteArrayList

7.二叉树

8.红黑树

9.散列表

26.多线程

线程基础

线程和进程

并发和并行

创建线程的方式

线程状态

 线程顺序执行

notify()和notifiAll()

wait和sleep

停止正在运行的线程

线程中并发安全

synchronized关键字原理

JMM

volatile

ReentrantLock

  Synchronized与Lock的区别

死锁产生的条件

 ConcurrentHashMap

导致并发程序出现问题的根本原因

线程池

线程池中常见的阻塞队列

如何设置核心线程数

多线程使用场景

CountDownLatch

异步线程

如何控制某个方法允许并发访问线程的数量

ThreadLocal

二.java高级

1.String::intern()是一个本地方法,作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中。

2.可重入锁

3.synchronized原理

4.LockSupport

5.seata原理

5.1 AT模式

5.2 TCC模式

6.AQS

7.AOP

8.循环依赖

9.redis

缓存穿透

缓存击穿

缓存雪崩

双写一致

持久化

数据删除策略

数据淘汰策略

分布式锁

主从一致性

redis集群

分片集群结构

redis是单线程为啥块?

redis事务

三.nginx

四.mysql

1.慢查询

开源工具

mysql自带慢日志

分析sql

索引

聚簇索引

覆盖索引

mysql超大分页处理

事务

undo log 和redo log

MVCC

隐式字段:

undo log 

readview

主从同步

分库分表

垂直拆分

水平拆分

spring

spring中的单例bean是线程安全的吗?

AOP

事务失效的的场景

springbean的生命周期

springmvc的流程

mybatis

执行流程

延迟加载

mybatis缓存

微服务

负载均衡

 服务雪崩

服务降级

服务熔断

skywalking

限流

分布式事务

CAP定理

Base理论

解决方案

seta框架

项目中采用的哪种方案?

接口幂等性

分布式定时任务

消息中间件

消息不丢失

 生产者确认机制

消息持久化

消费者确认

如何保证消息不丢失

消息重复消费

消息堆积

延迟队列

死信队列

高可用机制

kafka

如何保证消息不丢失?

如何保证消费的顺序性

如何保证高可用性

kafka数据清理机制

 kafka中的高性能设计

设计模式

工厂模式

策略模式

责任链设计模式

常见技术场景

单点登录

权限认证

上传数据的安全性怎么控制?

负责项目的时候遇到的棘手问题

项目中日志是怎么采集的?

查看日志的命令

生产环境问题怎么排查

怎么快速定位系统瓶颈问题

JVM

jvm组成

 程序计数器

Java堆

 虚拟机栈

栈内存溢出情况

栈和堆的区别

方法区

 类加载器

双亲委派模型

​编辑 类装载的过程

垃圾回收

垃圾回收算法

垃圾回收器

强引用、软引用、弱引用、虚引用

jvm实践 

 jvm调优

jvm调优工具


一.Java基础

1.移位运算符

>> :左移运算符,向左移动若干位,高位丢弃,低位补零。 x >> 1 相当于 x乘以2(不溢出的情况)

<<:带符号右移,向右移若干位,高位补符号位,低位丢弃。正数高位补0,负数高位补1. x << 1,相当于x 除以2.

>>> :无符号右移,忽略符号位,空位以0补齐。

double、float在二进制中表现比较特殊,因此不能用来进行移位操作。

移位操作符实际上支持的类型只有int和long,编译器在对short、byte、char类型进行移位前,都会将其转换为int类型再操作。

如果移位的位数超过数值所占有的位数会怎样?

当int类型左移/右移位数大于等于32位操作时,会先求余后再进行左移/右移操作。也就是说左右移32位相当于不进行移位操作32%32=0,左右移位42位相当于左右移位10位(42%32=10).当long类型进行左右移操作时,由于long对应的二进制是64位,因此秋雨操作的基数就是64

2.continue、break、return

return:直接使用return结束方法执行;

return value:return一个特定值,用于有返回值函数的方法

3.对于包装数据类型来说,==比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用equals方法

基本数据类型的局部变量存放在Java虚拟机栈的局部变量中,基本数据类型的成员变量(未被static修饰)存放在Java虚拟机堆中。

4.为什么说是几乎所有对象实例都存放在与堆中?这是因为hotspot虚拟机引入了jit优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存

5.Java基本数类型的包装类型的大部分都用到了缓存机制来提升性能

Byte,Short,Integer,Long这4种包装类型默认创建了数值[-128,127]的相应类型的缓存数据,Character创建了数值在[0,127]范围的缓存数据,Boolean直接返回True or False

如果超出对应范围仍然会去创建新的对象,缓存的范围区间大小只是在性能和资源之间权衡。

Fload、Double并没有实现缓存机制

Integer i1 = 40;

Integer i2 = new Integer(40);

i1 == i2  false ;i1用到了缓存,i2 直接创建新对象

所有整型包装类对象之间的比较,全部使用equals方法比较

装箱其实调用了包装类的valueOf()方法,拆箱调用了xxxValue()方法

如果频繁拆装箱的话,也会严重系统的性能,应该尽量避免不必要的拆装箱操作。

6.为什么浮点数运算的时候会有精度丢失的风险

float a = 2.0f -1.9f;

System.out.println(a);//0.100000024

float b = 1.8f - 1.7f;

System.out.println(b); // 0.099999905

这个和计算机保存浮点数的机制有很大关系。计算机是二进制的,而且计算机在表示一个数时,宽度是有限的,无线循环的小数存储在计算机时,只能被截断,所以会导致小数精度发生损失的情况,比如十进制下的0.2就没办法精确转换成二进制小数:

0.2*2 = 0.4 ->0

0.4*2 = 0.8 ->0

0.8*2 = 1.6 ->1

0.6*2 = 1.2 -> 1

0.2*2 = 0.4 -> 0(发生循环)

...

如何解决浮点数运算的精度丢失?

BigDecimal可以实现对浮点数的运算,不会造成精度丢失。

基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险

在Java中,64位long整型是最大的整数类型。

BigInteger内部使用int[]数组来存储任意大小的整型数据,相对于常规类型的运算来说,BigInteger运算的效率会相对较低。

7.成员变量与局部变量的区别

语法形式:成员变量属于类的,局部变量是在代码块或方法中定义的变量或是方法的参数

成员变量可以被public,private,static等修饰符所修饰,而局部变量不能被访问控制修饰符及static所修饰 ;但是成员变量和局部变量都能被final 所修饰。

存储方式:从变量在内存中的存储方式来看,如果成员变量是使用static修饰的,那么这个成员变量是属于类的,如果没有static修饰,这个成员变量是属于实例的,而对象存在于堆内存,局部变量存在于栈内存。

生存时间: 变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。

默认值:从变量是否又默认值看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(被final修饰的成员变量必须显式地赋值),局部变量不会自动赋值

8.静态变量

可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,可以节省内存。

通常情况下,静态变量会被final关键字修饰为常量;

静态方法为什么不能调用非静态成员?

结合jvm的相关知识

1.静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后存在,需要通过类的实例对象去访问。

2.在类的非静态成员不存在时,静态方法已经存在了,此时调用的内存中还不存在的非静态成员,属于非法操作

9.字符型常量和字符串常量的区别

字符常量是单引号引起的字符,字符串常量是双引号引起的0个或若干个字符

字符常量相当于一个整型值(ASCII值),可以参加表达式运算,字符串常量代表一个地址值(该字符串在内存中的存放位置),字符常量只占2个字节,字符串常量占若干个字节。char在内存中占两个字节。

10.可变长参数

只能作为函数的最后一个参数,其前面可以有也可以没有任何其他参数

遇到方法重载的情况怎么办?会优先匹配固定参数还是可变参数方法呢?

会优先匹配固定参数的方法,因为固定参数的方法匹配度更高

11.面向过程和面向对象区别

主要区别在于解决问题的方式不同

面向过程把解决问题的过程拆成一个一个方法,通过一个个方法的执行解决问题

面向对象会先抽象出对象,然后用对象执行方法的方式解决问题

一个对象引用可以指向0个或1个对象;

一个对象可以 有n个引用指向它。

对象相等和引用相等的区别:

对象的相等一般比较的是内存中存放的内容是否相等;

引用相等比较的是指向的内存地址是否相等;

构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。

构造方法不能被重写,但是可以重载。

12.封装、继承、多态

封装:把一个对象的状态信息隐藏在对象内部,不允许外部外部对象直接访问对象的内部信息

可以提供一些可以被外界访问的方法来操作属性

继承:不同类型的对象,相互之间经常有一定数量的共同点,可以提高代码的重用性

子类拥有父类对象的所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有

子类可以拥有自己属性和方法,即子类可以对父类扩展

子类可以用自己的实现方式实现父类的方法

多态:一个对象具有多种的状态,具体表现为父类的引用指向子类的实例

引用类型的变量发出的方法调用到底是哪个类中的方法,必须在程序运行期间才能确定

多态不能调用只在子类存在但在父类不存在的方法

如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法

13.接口、抽象类

接口中的成员变量只能是public static final 类型,不能被修改且必须有初始值

抽象类的成员默认是default,可以在子类中被重新定义,也可以重新赋值

14.深拷贝和浅拷贝

  • 浅拷贝

浅拷贝会在堆上创建创建一个新的对象(区别于引用拷贝),不过,如果原对象内部属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用一个内部对象

  • 深拷贝

深拷贝会完全复制整个对象,包括这个对象所包含的内部对象

引用拷贝:两个不同的引用指向同一个对象


 15.Object常见的方法

public final native Class<?> getClass()

返回当前运行时对象的class对象,使用了final关键字修饰,不允许子类重写

public native int hashCode()

返回对象的hashCode,主要使用在哈希表中

public final native void notify()

唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念),如果有多个线程在的等待只会任意唤醒一个

pulic final native void notifyAll()

跟notify一样,唯一的区别就是会唤醒在此对象监视器上的等待的所有线程

public final native void wait(long timeout) throws InerruptedException

暂停线程的执行,sleep美欧释放锁,wait释放了锁

public final void wait(long timeout,int nacos) throws InterruptedException

多了nacos参数,这个参数表示额外时间

public final void wait() throws InterruptedException

和前2个wait一样,只不过这个方法一直等待,没有超时时间概念

protected void finalize() throws Throwable{}

实例被垃圾回收器回收的时候触发

String中的equals方法是被重写过的,因为Object的equals比较的是对象的内存地址,而String的equals比较的是对象的值

16.hashCode的作用

作用是获取哈希码(int整数)也称为散列码,哈希码的作用是确定该对象在哈希表中的索引位置

为什么要有hashCode?

当把对象加入HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCose值作比较,如果没有相符的hashCode,hashSet会假设对象没有重复出现。如果发现有相同的hashCode值对象,会调用equals方法来检查对象是否真的相同,如果两者相同,hashSet就不会让其加入操作成功,如果不同,就会重写散列到其他位置,这样大大减少了equals的次数,相应提高了执行速度。

hashCode和equals都是用于比较两个对象是否相等。

为什么jdk还要同时提供两个方法?

因为在一些容器中 (hashMap,hashSet),有了hashCode之后,判断元素是否在对应容器中的效率会更高

为什么不只提供hashCode方法呢?

因为两个对象的hashCode值相等并不代表两个对象相等

17.String,StringBuffer,StirngBuilder

StringBuffer对方法加了同步锁,是线程安全的。

StringBuilder并没有对方法加同步锁,非线程安全。

每次对String类型进行改变的时候,都会产生一个新的String对象,然后将指针指向新的String对象,StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象,相同情况下使用StringBuilder相比使用StringBuffer仅能获得10%-15%的性能提升,但却要冒多线程不安全的风险。

String为什么是不可变的?

String类中使用final关键字修饰字符数组来保存字符串,所以String对象是不可变的

修正:final关键字修饰的类 不能被继承

        修饰的方法不能被重写

        修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能指向其他对象

因此final并不是String不可变的根本原因

不可变的原因:

        保存字符串的数组被final修饰且为私有的,并且String没有提供/暴漏修改这个字符串的方法

      String类被final修饰导致其不能被继承,避免子类破坏Stirng不可变

字符串拼接使用“+”还是StringBuilder?

字符串对象通过+的字符串拼接方式,实际上是通过StringBuilder调用append()方法实现的,拼接完成之后调用toString得到一个String对象

但,在循环内使用+进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个StringBuilder以复用,会导致创建过多的StringBuilder对象

18.字符串常量池的作用

jvm为了提升性能和减少内存消耗针对字符串(String)专门开辟的一块区域,主要目的是为了避免字符串的重复创建

String aa = "ab";

String bb = "ab";

System.out.println(aa=bb);//true

String s1 = new String("abc");这句话创建了几个 字符串对象?

会创建1或2个字符串对象

如果字符串常量池中不存在字符串对象"abc"的引用,那么在堆空间创建,在字符串常量池中创建

字节码ldc命令:

用于判断字符串常量池中是否保存了对应的字符串对象引用,如果保存了直接返回,如果没有保存的话,会在堆中创建对用字符串对象并将字符串对象引用保存到字符串常量池中

String str1 = "str";

String str2 = "ing";

String str3  = "str"+"ing";//常量池中的对象

String str4 = str1+str2;//在堆上创建新的对象

String str5 = ”string" ;//常量池中的对象

str3 == str4 ;false

str3 == str5; true

在编译器可以确定值的字符串,也就是常量字符串,jvm会将其存入字符串常量

并且,字符串常量拼接得到字符串常量在编译阶段就已经被存放到字符串常量池,得益于编译器的优化

常量折叠:把常量表达式的值求出作为常量嵌套在最终生成的代码中,这是Javac编译器对源代码做的极少量优化措施之一。

对于String str3 = "str"+"ing";编译器会给优化成String str3 = "string";

并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以:

  • 基本数据类型byte,boolean,short,char,int,float,double,long以及字符串常量
  • final 修饰的基本数据类型和字符串常量
  • 字符串通过+拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型位运算(<<,>>,>>>)

引用的值在程序编译期间是无法确定的,编译器无法对其进行优化

String str4 =         new StringBuilder().append(str1).append(str2).toString();

对象引用和+的字符串拼接方式,实际上是通过StringBuilder调用append方法实现的,拼接完之后调用toString得到一个String对象;

不过,字符串使用final关键字声明后,可以让编译器当作常量来处理

final String str1 = "str";

final String str2 = "ing";

String c = "str"+"ing";//常量池中对象

String d =  str1+str2;//常量池中对象

c == d ; true

编译器在运行时才能知道其确切值的话,就无法对其优化

19.Checked Exception 和 Unchecked Exception

除了RuntimeException及其子类以外,其他的Exception类及其子类都属受检查异常

UncheckedException不受检查异常,在编译过程中,即使不处理不受检查异常也可以正常通过编译

try...catch ...finally

fianlly无论是否捕获或处理异常,finally块语句都会被执行,当在try或catch中遇到return语句时,finally将在方法返回之前被执行。

注意:

        不要在finally语句块中使用return。

当try语句和finally语句中都有return语句时,try语句中的return会被忽略

finally中的代码一定会执行吗?

不一定,finally之前虚拟机被终止运行的话,finally中的代码不会执行。

另外:程序所在的线程死亡;关闭cpu

20.try-with-resources

适用范围:任何实现Java.lang.AutoCloseable 或者 java.io.Closeable的对象

关闭资源和finally块执行顺序:在try-with-resources语句中,任何catch/finally块在声明的资源关闭后运行

异常使用需要注意的地方:

使用日志打印异常之后就不要再抛出异常了

 21.泛型

作用:增加代码的可读性和稳定性,类型转换,使用泛型后自动转换

泛型的使用方式:泛型类、泛型接口、泛型方法

编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型

public static<E> void printArray(E[] inputArray){

        for(E element:inputArray){

                System.out.printf("%s",element);

        }

}

在Java中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载优先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态方法的加载就已经完成了,所以静态方法是没有办法使用类上声明的泛型。只能使用自己声明的

22.反射

反射可以获取任意一个类的所有属性和方法,可以调用这些属性和方法

优缺点:

        可以让我们的代码更加灵活,为各种

23.重要知识点

1.BigDecimal

        为了避免精度丢失,使用BigDecimal来进行浮点数的运算

为了防止精度丢失,推荐使用BigDecimal(Strig value)构造方法或者BigDecimal.valueOf(Double val)静态方法来创建对象

注意:

禁止使用构造方法BigDecimal(double)的方式把double值转化为BigDecimal对象

因为BigDecimal(double)存在精度丢失风险,在精确计算或值比较的场景中会导致业务逻辑异常

如:BigDecimal g = new BigDecimal(0.1F) 实际的存储值为0.10000000149

优先推荐入参为String的构造方法或使用BigDecimal的valueO方法,此方法内部其实执行了Double的toString,而Double的toString按double的实际能表达的精度对尾数进行了截断

BigDecimal的等值比较使用compareTo(),而不是equals方法,equals会比较值和精度,compareTo会忽略精度

2.unsafe

unsafe类位于sun.misc包下的一个类,主要提供一些用于执行级别低、不安全操作的方法,如直接访问系统内存资源,自主管理内存资源等,这些方法在提升Java运行效率,增加Java语言底层资源操作能力方面起到了很大作用。但由于unsafe类使Java语言拥有了类似c语言指针一样操作内存空间的能力,增加了程序发生指针相关问题的风险.在程序中过度、不正确使用unsafe类会使得程序出错的概率大,使得Java这种安全语言变得不再"安全",因此对unsafe类一定要慎重

unsafe类提供的这些功能的实现需要依赖本地方法(native method),可以将本地方法看作是Java中使用其他编程语言编写的方法,本地方法使用native关键字修饰,Java中只是声明方法头,具体的实现则交给本地代码

unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,看上去可以获取unsafe实例,但是直接调用这个静态方法的时候,会抛出securityException异常。

因为在unsafe类中,会对调用者的classloader进行检查,判断当前类是否由Bootstrapclassloader加载,如果不是抛出SecurityException,也就是说只有启动类加载器才能够调用Unsafe类中的方法,来防止这些方法在不可信代码中被调用

如果要使用Unsafe这个类的话,该如何获取实例呢?

1.利用反射获得Unsafe类中已经实例化完成的单例对象theUnsafe

Field field = Unsafe.class.getDeclaredField("theUnsafe");

field.setAccessible(true);

return (Unsafe)field.get(null);

2.从getUnsafe方法的使用限制条件出发,通过Java命令行命令 -Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径默认追加到默认的bootstrap中,使得A被引导类加载器加载,从而通过Unsafe.getUnsafe()方法获取Unsafe实例

java - Xbootclasspath/a:${path} 

Unsafe功能:

  1. 内存操作

如果写过c或c++的程序员,一定对内存操作不会陌生,而在Java中不允许对内存直接进行操作的,对象内存的分配和回收都是jvm自己实现的,但Unsafe中,提供的接口可以对内存进行操作。        

     2.内存屏障

  编译器和cpu会保证程序输出结果一致的情况下,会对代码进行重排序,从指令优化角度提升性能。而指令重排序可能会带来一个不好的结果,导致cpu的高速缓存和内存中数据不一致,而内存。屏障(Memory Barrier)就是通过阻止屏障两边的指令重排序

   3.cas

比较并替换,是实现并发算法时常用到的一种技术。cas操作包含三个操作数:内存位置、预期原值、新值

执行cas的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则处理器不做任何操作。CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题

3.spi

面向对象的设计原则中,一般推荐模块之间基于接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。一旦代码里面涉及具体实现类,就违反了开闭原则。如果需要替换一种实现,就需要修改代码

为了实现在模块装配的时候不用在程序里动态指明,就需要一种服务发现机制。Java spi就提供了这样一个机制:为某个接口寻找服务发现机制

4.语法糖

switch支持string类型

Java中的switch自身本身就支持基本类型。如int,char,int类型直接进行数值的比较,char比较其ascii码,对于编译器来说,switch中其实只能使用整型,任何类型的比较都需要换成整型

字符串的switch是通过equals和hashCode方法实现的,hashCode方法返回的是int


虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class对象,比如并不存在List<String>.class或List<Integer>.class


枚举

使用enum来定义枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum,所有枚举类不能被继承


数值字面量

Java7中,数值字面量不管整数还是浮点数,都允许在数字之间插入任意多个下划线,这些下划线不会对字面量数值产生影响,目的就是方便阅读


泛型

泛型类型的参数不能用在Java异常处理的catch语句中。因为异常信息是由jvm在运行时刻进来的,由于类信息被擦除,jvm是无法区分两个类型异常MyException<String> 和MyException<Integer>


java5中,对Integer的操作引入了一个新功能来节省内存和提高性能,整型对象通过使用相同的对象引用实现了缓存和重用。

适用于数值区间:-128-127

只适用于自动装箱,使用构造函数创建对象不适用

24.集合

时间复杂度:只要代码的执行时间不随着n的增大而增大,这样的代码复杂度就是o(1)

空间复杂度:算法占用的额外存储空间和数据规模之间的增长关系

数组如何获取其他元素的地址值?

寻址公式:a[i]=baseAddress+i*dataTypeSize

baseAddress:数组的首地址

dataTypeSize:元素类型的大小

Collection接口、Map接口

Collection接口:子接口Set、List、Queue

List:有序、可重复的

Set:无须,不可重复

Map:key 无序,不可重复,value无序,可重复


Set基于hashMap实现的,底层采用hashMap来保存元素

TreeSet有序,唯一:红黑树(自平衡的排序二叉树)


queue

PriorityQueue:Object[]数组来实现二叉堆

ArrayQueue:Object[] 数组+双指针


Map

HashMap:jdk8之前是由数组+链表组成,数组是HashMap的主体,链表则是为了解决哈希冲突而存在的(拉链法解决冲突);

jdk8之后解决哈希冲突有了较大变化,当链表长度大于阈值(默认为8)时,会判断如果当前数组的长度小于64,那么会先进行数组扩容,而不是转换为红黑树,否则将链表转化为红黑树,以减少搜索时间

HashTable:数组+链表,数组是hashTable的主体,链表则是主要为了解决哈希冲突

TreeMap:红黑树(自平衡的排序二叉树)


ArrayList源码分析

成员变量

默认初始的容量

DEFAULT_CAPACITY=10

用于空实例的共享数组实例

EMPTY_ELEMENTDATA={}

与EMPTY_ELEMENTDATA区分开来,了解添加第一个元素要膨胀多少

DEFAULTCAPACITY_EMPTY_ELEMENTDATA={}

存储ArrayList元素的数组缓冲区

elementData

ArrayList的大小

size

构造方法

带初始化容量的构造函数

public ArrayList(int initialCapacity){

        if(initialCapacity > 0){

                this.elementData = new Object[initialCapaticy]

        }else if(initialCapacity == 0){

                this.elementData = EMPTY_ELEMENTDATA;

        }else{

                throw new IllegalArgumentException("Illegal Capacity:"+initialCapaticy)

        }

}

public ArrayList(){

        this.elementData = DEFAULTCAPACOTY_EMPTY_ELEMENTDATA

}

public ArrayList(Collection<? extends E> c){

        Object[] a = c.toArray();

        if((size == a.length)!=0){

                if(c.getClass == ArrayList.class){

                        elementData = a;

                }else{

                        elementData = Arrays.copyOf(a,size,Object[].class);

                }

        }else{

                elementData = EMPTY_ELEMENTDATA

        }

}

第一次添加数据

 

ArrayList底层的实现原理

底层使用动态的数组实现的

ArrayList初始化容量为0,当第一次添加数据的时候才会初始化容量为10

ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组

ArrayLsit在添加数据的时候:

  • 确保数组已使用长度size+1之后足够存下下一个数据
  • 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容
  • 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上
  • 返回添加成功布尔值

LinkeList

单向链表

链表中的每一个元素称为结点

物理存储单元上,非连续、非顺序的存储结构

每个结点包括两部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域

查询时间复杂度

  • 只有查询头结点的时候不需要遍历链表,时间复杂度为o(1)
  • 查询其他结点需要遍历链表,时间复杂度为o(n)

插入/删除操作时间复杂度

  • 只有在添加或删除头结点的时候不需要遍历链表,时间复杂度为o(1)
  • 添加或删除其他结点需要遍历链表找到对应的节点后,才能新增或删除结点,时间复杂度为o(n)

双向链表

对比单向链表

双向链表需要额外的两个空间来存储后继结点和前驱结点

支持双向遍历,也带来了双向链表操作的灵活性

时间复杂度

    查询操作

  • 查询头尾结点的时间复杂度是o(1)
  • 平均的查询时间复杂度为o(n)
  • 给定结点找前驱结点的时间复杂度为o(1)

  新增/删除操作

  •  头尾结点增删的时间复杂度为o(1)
  • 其他部分结点增删的时间复杂度为o(n)
  • 给定结点增删的时间复杂度为o(1)

ArrayList和LinkedList的区别

1.底层数据结构

ArrayList是动态的数组

LnkedList 是双向链表

2.操作数据效率

ArrayList按照下标查找的时候时间复杂度为o(1)(内存是连续的,根据寻址公式),LinkedList不支持下标查询

查找(未知索引):ArrayList需要遍历,链表也需要遍历,时间复杂度都是o(n)

LinkedListt头尾结点增删时间复杂度为o(1),其他都需要遍历,时间复杂度是o(n)

3.空间占用

ArrayList底层是数组,内存连续,节省内存

LinkedList是双向链表,需要存储数据,两个指针,更占用内存

4.线程安全

都不是线程安全

两种方案保证线程安全

  • 在方法内部使用,局部变量是线程安全的
  • Collections.synchronizedList(new ArrayList())

ArrayList和Array

ArrayList会根据实际存储的元素动态地扩容或缩容,而Array被创建之后就不能改变它的长度了

ArrayList允许使用泛型来确保类型安全,Array则不可以

ArrayList只能存储对象,Array可以直接存储基本类型数据,也可以存储对象

ArrayList创建时不需要指定大小,而Array创建时必须指定大小


ArrayList和Vector

ArrayList是List的主要实现类,底层使用Object[]存储,适用于频繁的查找工作,线程不安全

Vector是List的古老实现类,底层使用Oject[]存储,线程安全


vector和stack区别

都是线程安全的,都是使用synchronized关键字进行同步处理。

Stack继承Vector,是一个后进先出的栈,Vector是一个列表

随着并发编程的 发展,vector和stack已经被淘汰


LinkedList为什么不能实现RandomAccess接口

RandomAccess是一个标记接口,用来表明实现该接口的类支持随机访问(可以通过索引快速访问元素),LinkedList底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问

ArrayListLikedList
不保证线程安全不保证线程安全
底层数据结构:Object数组

底层数据结构:双向链表

jdk1.6之前为循环列表,1.7取消了玄幻

插入和删除是否受元素位置影响

插入和删除的时间复杂度受元素位置的影响

插入和删除是否受元素位置的影响

采用链表存储,头尾插入或删除不受元素位置影响

是否支持快速随机访问

支持        

是否支持高校的随机访问

不支持

内存空间占用

空间花费主要体现list列表的结尾会预留一定的容量空间

内存空间占用

空间花费体现在它的每一个元素都需要消耗比ArrayList更多的空间(存储后继和直接前驱以及数据)

项目中一般不用

仅仅在头尾插入或删除元素的时间复杂度为       O(1),其他情况增删元素的平均时间复杂度都是O(n)

双向列表:包含两个指针,一个prev指向前一个节点,一个next指向后一个节点

双向循环列表:最后一个节点的next指向head,而head的prev指向最后一个节点构成一个环

RandomAccess接口中没什么定义,RandomAccess接口不过是一个标识,标识这个接口的类具有随机访问功能

ArrayList实现了RandomAccess接口,而LinkedList没有实现,我觉得和底层数据结构有关!ArrayList底层是数组,LinkedList底层是链表,数组天然支持随机访问,时间复杂度为o(1),所以称为快速随机访问,链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度o(n),所以不支持快速随机访问,RandomAccess接口只是标识,并不是说ArrayList实现RandomAccess接口才具有快速随机访问功能。

Comparable和Comparator区别

都是用于Java中排序的接口,Comparable出自java.lang,有compareTo()

Compartor出自java.util,有一个compare(Object obj1,Object obj2)方法来排序

无序性和不可重复性

无序性不等于随机性,无序性指存储的数据在底层数组中并非按照数组索引顺序添加,而是根据数据的哈希值决定的

不可重复性是指添加的元素按照equals判断时,返回false,需要同时重写equals和hashCode

HashSet底层数据结构是哈希表

LinkedHashSet底层数据结构是哈希表和链表

TreeSet底层数据结构是红黑树,元素是有序的,排序方式有自然排序和定制排序

Queue

单端队列,只能从一端插入元素,另一端删除元素,实际上一般遵守先进先出规则

Queue扩展了Collection接口,根据因容量问题而导致操作失败后处理方式的不同可分为:1.操作失败后会抛出异常,2.会返回特殊值

Queue抛出异常返回特殊值
插入队尾add(E e)offer(E e)
删除队首remove()poll()
查询队首元素element()peek()

Deque是双端队列,在队列的两端可以插入删除元素

Deque扩展了Queue的接口,增加了在队首和队尾进行插入和删除的方法,根据失败后处理方式的不同分为两类:

Deque抛出异常返回特殊值
插入队首addFirst(E e)offerFirst(E e)
插入队尾addLast(E e)offerLast(E e)
删除队首removeFirst()pollFirst()
删除队尾removeLast()pollLast()
查询队首元素getFirst()peekFirst()
查询队尾元素getLast()peekLast()

Deque还提供了push()和pop()等其他方法,可用于模拟栈

ArrayDeque和LinkedList都实现了Deque接口

ArrayDeque是基于可变长数组和双指针来实现,LinkedList是链表来实现

ArrayDque不支持null数据,但LinkedList支持

ArrayDeque插入时可能存在扩容过程,不过均摊后的插入操作,依然为o(1),虽然LinkedList不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比较慢

性能角度看,ArrayDeque来实现队列要比LinkedList更好。ArrayDeque也可以用于实现栈

BlockingQueue

继承Queue,阻塞队列队列中没有元素时一直阻塞,直到有元素;如果队列已满,一直等待队列可以放入新元素时再放入

常用的阻塞队列:

ArrayBlockingQueue

数组实现的有界阻塞队列

创建时需要指定容量大小,支持公平和非公平两种方式的锁访问机制

LinkedBlockingQueue单向链表实现的可选有界阻塞队列,在创建时可以指定容量大小,如果不指定默认为Integer.MAX_VALUE,也支持公平和非公平锁访问机制
SynchronousQueue

同步队列,一种不存储元素的阻塞队列

每次插入操作都必须等待对应的删除操作

反之删除操作也必须等待插入操作,因此

常用于线程之间的直接传递数据

DelayQueue延迟队列,其中的元素只有到了其指定的延迟时间,才能从队列中出队。

ArrayBlockQueue和LinekdBlockQueue区别:

底层实现:ArrayBlockingQueue基于数组,LinkedBlockingQueue基于链表实现

是否有界:ArrayBlockingQueue在创建时必须指定容量

                  LinkedBlickingQueue可以指定,不指定默认为Integer.MAX_VALUE

锁是否分离:ArrayBlockingQueue中的锁是没有分离的,生产和消费用的同一个锁

LinkedBlockingQueue中的锁是分离的,生成用的putLock,消费是takeLock,可以防止生产者和消费者线程之间的锁争夺;

内存占用:ArrayBlockingQueue需要提前分配数组内存,LinkedBlockingQueue是动态分配链表节点内存。ArrayBlockingQueue在创建时就会占用一定的内存空间,且往往申请的内存空间比实际所用的内存更大,LinkedBlockingQueue则是根据元素的增加而逐渐占用内存空间

25.集合(二)

1.HashMap和HashTable

HashMap常见属性

默认的初始容量DEFAULT_INITAL_CAPACITY

默认的加载因子DEFAULT_LAOD_FACTOR

扩容阀值=数组容量*加载因子

HashMap是懒惰加载,在创建对象时并没有初始化数组

HashMap扩容:

 

在jdk1.7的hashmap中在数组进行扩容的时候,以为链表是头插法,在进行数据迁移的时候,有肯能导致死循环

HashTable基本被淘汰,不要在代码中使用它

HashMap可以存储null的key和value,null作为键只有一个,null作为值可有多个,HashTable不允许有null键和null值

初始容量大小和每次扩充容量大小:

        如果不指定容量初始值:

                HashTable默认的初始大小为11,之后每次扩充,容量为原来的2n+1

                HashMap的默认初始大小为16,之后每次扩充,容量变为原来的2倍

    如果指定了容量初始值:

HashTable会直接使用给定的大小;

HashMap会将其扩充为2的幂次方大小;HashMap总是使用2的幂作为哈希表的大小

底层数据结构:

jdk1.8以后的hashMap在解决哈希冲突时有了较大变化,当链表长度大于阀值(默认8)时,将链表转为红黑树(转换成红黑树前会判断,如果当前的数组长度小于64,那么会优先进行数组扩容,而不是转换为红黑树),以减少搜索时间

2.hashSet如何检查重复

当你把对象加入HashSet时,HashSet会先计算对象的hashCode值来判断对象加入位置,同时也会与其他加入的对象hashCode值作比较,如果没有相符的hashCode,hashSet会假设对象没有重复出现,但是如果发现有相同hashCode值的对象,就会调用equals方法来检查hashCode相等的对象是否真的相同,如果两者相同,hashSet就不会让加入操作成功。

3.HashMap底层实现

jdk1.8之前

底层是数组和链表结合在一起使用也就是链表散列,hashMap通过key的hashCode经过扰动函数经过处理后得到的hash值,通过(n-1)&hash判断当前元素存放位置(这里的n指的数组长度),如果当前位置存在元素的话,就判断该元素与要存入元素的hash值 key是否相同,如果相同,直接覆盖,不相同就通过拉链法解决冲突

jdk1.8之后

当链表长度大于8时,判断数组长度是否小于64,如果小于会先选择进行数组扩容,否则转换为红黑树

hashMap的长度为什么是2幂次方?

为了能让HashMap存储高效,尽量减少碰撞,也就是尽量把数据分配均匀,hash值的范围值-2147483648 到 2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射的比较均匀松散,,一般应用很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的,所以这个散列值是不能直接拿来用的,用之前还要对数组的长度取模运算,得到的余数才能用来存放的位置就是数组对应的下标。这个数组的计算方法是(n-1)&hash ;n代表数组长度        

取余(%)操作中如果除数是2的幂次则等价于与其除数减1的与操作 hash%length == hash&(length-1)的前提是length是2的n次方 。采用二进制位操作,相对于%能够提高运算效率

4.ConcurrentHashMap

ConcurrentHashMap已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS操作

Collections工具类的使用:

  • 排序
  • 查找、替换操作
  • 同步控制(不推荐,需要线程安全集合的类使用JUC包下并发集合)

排序操作

reverse(List list)//反转

shuffle(List list)//随机排序

sort(List list)//自然排序的升序排序

sort(List list,Comparator  c)//定制排序,由Comparator控制排序逻辑

swap(List list,int i,int j)//交换两个索引位置的元素

rotate(List list,int distance)//旋转,当distance为正数时,将list后distance个元素整体移到前面

查找、替换操作

int binarySearch(List list,Object key)//对list进行二分查找,返回索引,list必须是有序的

int max(Collection coll)//根据元素的自然顺序,返回最大的元素

int max(Collection coll,Comparator c)//根据定制排序,返回最大元素

void fill(List list,Object obj)//用指定的元素代替指定list中的所有元素

int frequency(Collection c,Object o)//统计元素出现次数

5.集合判空

阿里巴巴开发手册描述:

判断所有集合内部的元素是否为空,使用isEmpty(),而不是size == 0

因为ksEmpty方法的可读性更好,时间复杂度为o(1);

绝大部分集合size方法的时间复杂度是o(1),不过,也有很多复杂度不是o(1),比如java.util.concurrent包下的某些集合ConcurrentLinkedQueue,ConcurrentHashMap

集合转Map

在使用java.util.stream.Collectors类的toMap()方法转为Map集合时,一定要注意value为null时会抛NPE异常

集合遍历

不要在foreach循环里进行元素的remove/add操作,remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁

fail-fast:

java中的fail-fast机制,默认指JAVA集合的一种错误检测机制。当多个线程对部分集合进行结构上的改变操作时,有可能会产生fail-fast机制,这个时候会发生ConcurrentModificationException

failt-safe:

为了避免触发fail-fast机制,导致异常,可以使用Java中提供的一些采用了fail-safe机制的集合类.fail-safe的集合容器在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历

java.util包下面的所有的集合类都是fail-fast的,而java.util.concurrent包下的所有类都是fail-safe的

集合转数组

使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全一致,长度为0空数组

String[] s = new String[]{"a1","a2"}

List<String> list = Arrays.asList(s);

list.toArray(new String[0]);

由于JVM优化,new String[0]作为Collection.toArray()方法的参数现在使用更好,new String[0]就是起一个模板的作用,指定了返回数组的类型,0是为了节省空间

集合去重

阿里巴巴开发手册:

可以利用Set元素唯一的特性,可以快速对一个集合进行去重操作,避免使用Listcontains进行遍历去重或者判断包含操作。

注意

Arrays.asList()返回的不是java.util.ArrayList,而是java.util.Arrays的一个内部类,这个内部类并没有实现集合的修改或者重写这些方法

数组转ArrayList

最简单的方法

List list = new ArrayList<>(Arrays.asList("a","b","c"));

使用Java8的stream推荐

Integer[] myArray = {1,2,3};

Arrays.stream(myArray).collect(Collectors.toList());

基本类型也可以实现转换(依赖boxed的装箱操作)

int[] myArray2 = {1,2,3};

Arrays.stream(myArray).boxed().collect(Collectors.toList());

对于不可变集合,可以使用ImmutableList类及其of()、copyOf()

Immutable.of("String","elements");

Immutable.copyOf(sStringArray);

对于可变集合,可以使用Lists.newArrayList(anotherListCollection);

Lists.newArrayList(asStringArray);

Lists.newArraList("or","string","elements");

6.CopyOnWriteArrayList

思想:

写时复制:COW

如果有多个调用者同时请求相同资源(内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源内容时,系统才会真正复制一份专用副本给调用者,而其他调用者所见到的最初资源仍然不变

当需要修改CopyOnWriteArrayList时,不会直接修改原数组,而是会先创建底层数组副本,对副本数组进行修改,修改完后再将修改后的数组赋值回去,这样保证写操作不会影响读操作了。

7.二叉树

每个节点最多两个叉,并不是要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点

常见的二叉树:

满二叉树

完全二叉树

二叉搜索树

红黑树

8.红黑树

平衡的二叉搜索树

红黑树的特质

  • 节点要么是红色,要么是黑色
  • 根节点是黑色
  • 叶子节点是黑色空节点
  • 红黑树中红色节点的子节点都是黑色
  • 从任意节点到叶子节点的所有路径都包含相同数目的黑色节点

9.散列表

将key映射为数组下标的函数叫做散列函数

散列函数要求:

  • 散列函数计算得到的散列值>=0
  • key1==key2 -> hash(key1)==hash(key2)
  • key1!=key2 -> hash(key1)!=hash(key2)

散列冲突:拉链法

散列表中,数组的每个下标我们称之为桶或槽,每个桶或槽会对应一条链表,所有散列值相同的元素都放到槽位对应的链表中

26.多线程

线程基础

线程和进程

进程

程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理io的

线程

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给cpu执行,一个进程能可以分为一到多个线程

对比

  • 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
  • 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
  • 线程更轻量,线程上下文切换一般比进程上下问切换低(上线下文切换是指从一个线程切换到另一个线程)
并发和并行

单核cpu

单核cpu下线程实际还是串行执行的

操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为15毫米)分给不同的程序使用。只是由于cpu在线程间的切换非常快,人类感觉是同时进行。

一般将这种线程轮流使用cpu的做法称为并发

多核cpu

每个核都可以调度运行线程,这时候线程可以是并行的

并发是同一时间应对多件事件的能力

并行是同一时间动手做多件事情的能力

创建线程的方式
  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 线程池创建

runnable和callable有什么区别?

Runnable接口run方法没有返回值

Callable接口call方法有返回值,需要FutureTask获取结果

Callable接口的call方法允许抛出异常,Runnable接口的run方法异常只能内部消化,不能继续上抛

run()和start()区别

start()用来启动线程,通过线程调用run方法执行run方法中定义的逻辑代码,start方法只能被 调用一次

run()封装了要被线程执行的代码,可以被调用多次

线程状态

NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING 、TERMINATED

创建线程对象(NEW)

 线程顺序执行

可以使用线程中的join方法

t.join;阻塞调用此方法的线程进入timed_waiting,直到线程t执行完成后,此线程再继续执行

notify()和notifiAll()

notify()随机唤醒一个等待线程

notifyAll()唤醒所有等待线程

wait和sleep

两者都是让线程暂时放弃cpu的使用权,进入阻塞状态

不同点

1.方法归属不同

sleep是Thread类静态方法

wait(long),wait()是 Object的成员方法,每个对象都有

2.醒来时机不同

执行sleep(long)和wailt(long)的线程都会再等待相应毫秒后醒来

wait(long),wait()还可以被notify()唤醒,wait()如果不唤醒就一直等下去

它们都可以被打断唤醒

3.锁特性不同(重点)

wait方法调用必须先获取wait对象的锁,而sleep则无此限制

wait方法执行后会释放对象锁,允许其他线程获得该对象锁

而sleep如果在synchronized代码块中执行,并不会释放对象锁

停止正在运行的线程

1.使用退出标志,可以正常退出线程

2.使用stop方法(不推荐,已废弃)

3.使用interrupt方法中断线程 

打断阻塞的线程

打断正常的线程

线程中并发安全

synchronized关键字原理

Synchronized对象锁采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个锁时就会阻塞

monitor是由jvm提供,c++实现,monitor是jvm级别的对象,线程获得锁需要使用对象(锁)关联monitor

monitor结构

waitset、EntryList 、Owner

Owner:存储当前获取锁的线程,只能有一个线程获取

EntryList:关联没有抢到锁的线程,处于Blocked状态的线程

WaitSet:关联调用了wait方法的线程,处于waiting状态的线程

monitor属于重量级锁,锁升级?
monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。

jdk1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题

对象的内存结构

在hotspot虚拟机中,对象在内存中存储的布局可分为3块布局:对象头(Header)、实例数据(Instance Data)、对齐填充;

对象头:MarkWord对象头、KlassWord描述对象实例的具体类型

实例数据:成员变量

对齐填充:如果对象头+实例数据不是8的倍数,则通过对齐填充补齐(无意义)

hashcode25位的对象标识hash码
age对象分代年龄占4位
biased_lock偏向锁标识,占1位,0表示没有开始偏向锁,1表示开启了偏向锁
thread持有偏向锁的线程id,占23位
epoch偏向时间戳,占2位
ptr_to_lock_record轻量级锁状态下,指向栈中锁记录的指针
ptr_to_heavyweight_monitor重量级锁状态下,指向对象监视器monitor的指针,占30位

每个Java对象都可以关联一个monitor对象,如果使用synchronized给对象上锁之后,该对象头的markword中就设置指向monitor对象的指针

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行cas操作

java6引入偏向锁来做进一步优化:只有第一次使用cas将线程id设置到对象的mark word头,之后发现这个线程id是自己的就表示没有竞争,不用重新cas,以后只要不发生竞争,这个对象就归线程所有

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况

描述
重量级锁底层使用monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换、成本较高,性能比较低
轻量级锁线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级锁修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是cas操作,保证原子性
偏向锁

一段很长时间内都只被一个线程使用锁,可以使用偏向锁,第一次获得锁,会有一个cas操作,之后该线程再获取锁,只需要判断markword中是否是自己的线程id即可,而不是开销相对较大的cas

命令

JMM

Java内存模型,定义共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性

volatile

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰后,那么就具备了两层语义:

保证线程之间的可见性

禁止进行指令重排序

禁用优化:

方案一:在程序运行时加vm参数-Xint表示禁用即时编译器,不推荐,得不偿失(其他程序还要使用)

方案二:在变量前加上volatile,当前告诉jit,不要对volatile修饰的变量做优化

禁止指令排序

用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果

@Actor保证方法内的代码在同一个线程下执行 

读操作加的屏障是阻止下方其他读操作越过屏障排到volatile变量读之上

写操作加的屏障是阻止上方其他写操作越过屏障拍到vilatile变量写之下

使用技巧

写变量让volatile修饰的变量在代码最后位置

读变量让volatile修饰的变量在代码最开始位置

aqs:抽象队列同步器,是构建锁或其他同步组件的基础框架

synchronizedaqs
关键字,c++语言实现Java语言实现
悲观锁,自动释放锁悲观锁,手动开启和关闭
锁竞争激烈都是重量级锁,性能差锁竞争激烈的情况下,提供了多种解决方案

非公平锁:新的线程与队列中的线程共同来抢资源

公平锁:新的线程到队列中等待,只让队列的head线程获取锁,是公平锁

ReentrantLock

相比synchronized具备以下特点

  • 可中断
  • 可以设置超时超时时间
  • 可以设置公平锁
  • 支持多个条件变量
  • 与synchronized一样,都支重入

实现原理

  • 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
  • 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
  • 当exclusiveOwnerThread为null时,则会唤醒在双向队列中等待的线程
  • 公平锁体现在按照先后顺序获取锁,非公平锁体现在不排队的线程也可以抢锁

  Synchronized与Lock的区别

语法层面

synchronized是关键字,源码在jvm中,用c++语言实现

Lock是接口,源码由jdk提供,用java语言实现

使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放

功能层面

二者均属于悲观锁,都具备基本互斥,同步,锁重入功能

Lock提供了许多synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量

Lock有适合不同场景的实现,如ReentrantLock、ReentrantReadWriteLock(读写锁)

性能层面

在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁、性能不赖

在激烈竞争时,Lock的实现通常会提供方更好的性能

死锁产生的条件

死锁:一个线程需要同时获取多把锁,这时容易发生死锁

 ConcurrentHashMap

底层数据结构

jdk1.7底层采用分段数组+链表实现

jdk1.8采用的数据结构是跟HashMap1.8的结构一样,数组+链表/红黑二叉树

加锁方式

jdk1.7采用Segment分段锁,底层使用的是ReentrantLock

jdk1.8采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好

导致并发程序出现问题的根本原因

并发编程三大特征

原子性、可见性、有序性

原子性:一个线程在cpu中操作不可暂停,也不可中断,要不执行完成,要不不执行

可见性:让一个线程对共享变量的修改对另一个线程可见

线程池

核心参数

corePoolSize 核心线程数

maxmulPoolSize 最大线程数=核心线程+救急线程的最大数目

keepAliveTime 生存时间-救急线程的生存时间,生存时间内没有新任务,此线程执行会释放

unit时间单位-救急线程的生存时间单位,如秒、毫秒等

workQueu:当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

threadFactory 线程工厂,可以定制线程对象的创建,如设置线程名称、是否是守护线程

handler 拒绝策略-当所有线程都在繁忙,workQueue也放满时,会触发拒绝策略

拒绝策略:

        AbortPolicy:直接抛出异常,默认策略

        CallerRunPolicy:用调用者所在的线程来执行任务

        DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务

        DiscardPolicy:直接丢弃任务

线程池中常见的阻塞队列

ArrayBlockingQueue基于数组结构的有界阻塞队列,FIFO
LinkedBlocingQueue 基于链表结构的有界阻塞队列FIFO
DelayedWorkQueue        是一个优先级队列,它可以保证每次出队的任务都是当前执行时间最靠前的
SynchronousQueue不存储元素的阻塞队列,每个插入操作都必须等待一个移除操作

如何设置核心线程数

IO密集型任务

一般来说:文件读写、DB读写、网络请求等

核心线程数大小设置为2N+1

CPU密集型任务

一般来说:计算型代码、Bitmap转换、Gson转换

核心线程数大小设置为N+1

如何确定核心线程数

1.高并发、任务执行时间短->(cpu核+1),减少上下文切换

2.并发不高、任务执行时间长

IO密集型:2*CPU核数+1

CPU密集型:CPU核数+1

3.并发高、业务执行时间长

解决这种类型的的任务关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,参考2

线程池的种类

  1. newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
  2. newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序FIFO执行
  3. newCacheThreadPool
  4. newScheduledThreadPool

多线程使用场景

CountDownLatch

闭锁/倒计时锁,用来进行线程同步协作,等待所有线程完成倒计时

构造参数用来初始化等待计数值

await()用来等待计数归零

countDown()用来让计数减1

异步线程

如何控制某个方法允许并发访问线程的数量

semaphore信号量,是juc包下的一个工具类,底层是aos,可以通过其限制执行的线程数量

  • 创建semaphore对象,可以给一个容量
  • semaphore.acquire()请求一个信号量,这时候的信号量个数-1,一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候会阻塞,直到其他线程释放了信号量
  • semaphore.release()释放信号量,此时信号量个数+1

ThreadLocal

多线程中对于解决线程安全的一个操作类,会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal同时实现了线程内资源共享

是一个线程内部存储类,从而让多个线程只操作自己内部的值。从而实现线程数据隔离

ThreadLocal可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题

同时实现了线程内资源共享

二.java高级

1.String::intern()是一个本地方法,作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中。

sun.mic.Version.init()中 有个launcher_name="java"

  

2.可重入锁

又名递归锁

同一个线程在外层方法获取锁的时候,再进入该线程内层方法会自动获取锁(前提,锁对象得是同一个对象)不会因为之前已经获取过还没有释放而阻塞。

java中的ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。

自己可以获取自己内部锁

可重入锁种类:隐式锁synchronized关键字使用的锁,默认是可重入锁

显示锁:Lock也有ReentrantLock这样的可重入锁

3.synchronized原理

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针

当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,java虚拟会将该锁对象的持有线程设置为当前线程,并将其计数器加1;

当目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁;

当执行monitorexit时,java虚拟机则需要将锁对象的计数器减1,计数器为0代表锁已被释放。

lock和unlock使用次数要匹配

4.LockSupport

LockSupport中的park和unpark的作用分别是阻塞线程和解除阻塞线程

3种唤醒和等待的方法

1.Object中的wait方法让线程等待,notify方法唤醒线程

2.JUC包中的Condition的await方法让线程等待,signal方法唤醒线程

3.LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

wait、notify方法,两个都去掉同步代码块,会异常;必须在在同步块或方法里且成对出现使用;

将notify放在wait前,程序无法执行,无法唤醒

Lock lock = new ReentrantLock();

Condition condition = lock.newCondition();

condition.await();

condition.signal();

必须放在lock块,signal放在aeait前,程序无法执行,无法唤醒

LockSupport

使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可

permit只有两个值:1和0,默认0

LockSupport不用在同步块或方法或lock中出现;unpark 可以在park前执行

5.seata原理

seata是一款开源的分布式事务解决方案,为用户提供了AT,TCC,SAGA,XA几种不同的事务模式

AT模式:无侵入式的分布式事务解决方案,适合不希望对业务进行改造的场景,但由于需要添加全局事务锁,影响高并发系统的性能。该模式主要关注多DB访问的数据一致性,也包括多服务下的多DB数据访问一致性问题

TCC:高性能的分布式事务解决方案,适用于对性能要求比较高的场景。主要关注业务拆分,在按照业务横向扩展资源时,解决服务间调用的一致性问题

SAGA:长事务的分布式事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统

核心组件:

TM和RM是作为Seata客户端与业务系统集成在一起,TC作为seata的服务端独立部署

事务协调器TC:维护全局事务的运行状态,负责协调并驱动全局事务提交或回滚

事务管理器TM:事务发起方,控制全局事务的范围,负责开启一个全局事务,并最终发起全局提交或回滚全局的

资源管理器RM:事务参与方,管理本地事务正在处理的资源,负责向TC注册本地事务,汇报本地事务状态,接受TC的命令来驱动本地事务的提交或回滚。

seata分布式事务整体执行机制,可以大致分为两个阶段提交:

1.发起方TM向TC申请开启一个全局事务,全局事务创建成功并生成唯一的全局事务标识XID,该XID在后续事务的服务调用链路的上下文传播(通过aop实现)

2.RM向TC注册分支事务,汇报资源准备情况,并与XID进行绑定(branch分支事务指分布式事务中每个独立的本地局部事务)

3.TM向TC发起XID下所有分支事务的全局提交或 回滚请求(事务一阶段结束)

4.TC汇总事务信息,决定分布式事务是提交或回滚

5.TC通知所有RM提交/回滚资源,事务二阶段结束

5.1 AT模式

AT模式原理:

基于XA事务(XA是基于数据库实现的分布式事务协议)演进而来,需要数据库支持,如果是Mysql,则需要5.6以上版本才支持XA协议。seata会在第一阶段拦截并解析用户的sql,并保存其变更前后的数据镜像,形成undo log,并自动生成事务第二阶段的提交和回滚操作

AT模式RM驱动分支事务的行为分为2个阶段:

1.执行阶段

1.代理jdbc数据源,拦截并解析业务sql,生成更新前后的镜像数据,形成undo log

2.向TC注册分支

3.分支注册成功后,把业务数据的更新和undo log放在同一个本地事务中提交

2.完成阶段

全局提交,收到TC的分支提交请求后,异步删除相应分支的 undo log

全局回滚,收到TC的分支回滚请求,查询分支对应的undo log记录,,生成补偿回滚的sql语句,执行分支回滚并返回结果给TC

5.2 TCC模式

TCC模式RM驱动分支事务的行为分为两个阶段:

1.执行阶段

向TC注册分支

执行业务定义的Try方法

向TC上报Try方法执行情况:成功或失败

2.完成阶段

全局提交:收到TC的分支提交请求,执行业务定义的Confirm方法

全局回滚:收到TC的分支回滚请求,执行业务定义的Cancel方法

6.AQS

字面:抽象的队列同步器

用来构建锁或其他同步器组件的重量级基础框架及整个juc体系的基石,通过内置的fifo队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。

能干嘛? 加锁会导致阻塞,有阻塞就需要排队,实现排队必然需要有某种形式的队列进行管理

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的fifo队列来完成资源的排队工作,将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过cas完成对state值的修改。

Node:

exclusive表示线程正在以独占的方式等待锁

Lock接口的实现类,基本上都是聚合一个队列同步器的子类完成线程访问控制的;

ReentrantLock lock = new ReentrantLock();

默认是非公平锁;

公平锁:先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列;

非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。

7.AOP

spring4 -> spring5 aop的执行顺序,执行顺序是不同的

spring 4:

 spring5:

8.循环依赖

构造方法注入对循环依赖的解决不友好

AB循环依赖问题只要A的注入方式是setter且singleton,就不会有循坏依赖问题

原型方式的bean是不支持循环依赖的,会报错

DefaultSingletonBeanRegistry

第一级缓存 singletonObjects 存放已经经历了完整生命周期的Bean对象

第二级缓存 earlySingletonObjects 存放早起暴露出来的bean对象,bean的生命周期未结束(属性还未填充完)

第三级缓存 Map<Stirng,ObjectFactory<?>> singleFactories        存放可以生成bean的工厂

实例化:内存中申请一块内存空间

初始化属性填充:完成属性的各种赋值

1.A创建过程中需要B,于是A将自己放到三级缓存里,去实例化B

2.B实例化的时候发现需要A,于是B先查一级缓存,二级缓存,再查三级缓存,找到了A,然后把A放到二级缓存,从三级缓存中删除A

3.B顺利初始化完毕,将自己放到一级缓存里(此时B里的A依然是创建中状态)

然后接着回来创建A,此时B已经创建结束,直接从一级缓存里拿到B,完成创建,并将A放到一级缓存

执行的方法:getSingleton-doCreateBean-populateBean-addSingleton

9.redis

查看redis版本

redis-server -v

也可以进入redis-client: 输入info

查看命令帮助: help @类型名词

redis的hash对应java 的Map<String,Map<Object,Object>>

hash应用场景

set 点赞,共同关注的,推荐的

zset 热搜排序


缓存穿透

查询一个不存在的数据,mysql查询不到数据库也不会直接写入缓存,就会导致每次请求查数据库

解决方案:

1.缓存空数据,查询返回的数据为空,仍把这个结果缓存。

  优点:简单  缺点:消耗内存,可能会发生不一致的问题

2.布隆过滤器

查询时候,先走布隆过滤器,如果布隆过滤器中不存在,直接返回,存在,查redis,redis中存在,返回,不存在,查询db

布隆过滤器:

bitmap相当于一个以bit为单位的数组,数组中每个单元只能存储二进制数0或1

作用检索一个元素是否在一个集合中 

存储数据:id为1的数据,通过多个hash函数获取hash值,根据hash值计算数组对应位置改为1;

查询数据:使用相同hash函数获取hash值,判断对应位置是否都为1

存在误判:

id1、id2存在,id3不存在

数组越小误判率就越大,数组越大误判率就越小,但是同时带来了更多的内存消耗

redisson、guava都提供了的对布隆过滤器的实现 

缓存击穿

给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把db压垮

解决方案

1.互斥锁

2.逻辑过期

热点的key不是设置过期时间

数据里面增加一条过期字段;如

{"id":123,"title":"哈哈哈","expire":13544545454}

缓存雪崩

同一时间段大量的缓存key同时失效或者redis服务宕机,导致大量请求达到数据库,带来巨大压力

解决方案

给不同的key添加不同的失效时间

给redis集群提供高服务的可用性

给缓存添加降级限流策略

给业务添加多级缓存 如 guava

双写一致

修改了数据库中的数据也要同时更新缓存中的数据,缓存和数据库中的数据要保持一致

读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间

写操作:延迟双删

      删除缓存------修改数据库-------延时---删除缓存

异步通知保证数据的最终一致性

持久化

两种方式:RDB、AOF

rdb:redis daabase backup file redis数据备份文件,把redis中的所有数据都记录到磁盘中。redis实例故障重启后,从磁盘读取快照文件,恢复数据

命令: save 主进程来指向rdb,会阻塞所有命令

bgsave 开启子进程指向rdb,避免主进程收到影响

save 900 1 900秒内,如果有1个key被修改,则指向save

rdb执行原理:

basave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存中的数据并写入RDB文件

页表:记录虚拟地址与物理地址的映射关系

AOF

追加文件,redis处理的每一个命令都记录在AOF文件,可以看作是命令日志文件

AOF命令记录的频率

appendfsync always 每执行一次命令,立即记录到aof文件

appendfsync everysec 写命令执行完先放入AOF缓冲区,然后每隔1秒将缓冲区数据写到aof文件,是默认方案

appendfsync no 写命令执行完先放入aof缓冲区,由操作系统决定何时将缓冲区的内容写回磁盘

因为是记录命令,aof文件会比rdb文件大的多,而且aof会记录对同一个key的多次写操作,但最后一次写操作才有意义。通过执行bgrewrite命令,可以让aof文件执行重写功能,用最少的命令达到相同效果。

redis也会在触发阈值时自动去重写aof文件,阈值也可以在redis.conf中配置

auto-aof-rewrite-percentage 100

auto-aof-rewrite-min-size 64mb

rdb:可以容忍数分钟的数据丢失,追求更快的启动速度

aof:对数据安全性要求较高

数据删除策略

惰性删除:使用的时候检测是否过期,过期删除,造成大量过期key还在内存中

定期删除:每隔一段时间,就对一些key进行检查,删除里面过期的key

定期清理有两种模式:

slow模式是定时任务,执行频率默认为10Hz,每次不超过25ms

fast模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

数据淘汰策略

当redis内存不够用时,此时再向redis中添加新的key时,redis就会按照某一种规则将内存中数据删掉,这种数据的删除规则称之为内存的淘汰策略

redis支持8中不同策略来选择要删除的key:

noeviction:不淘汰任何key,内存满时不允许写入新数据 默认这种策略

volatile-ttl:设置了ttl的key,比较key的剩余ttl值,越小越先被淘汰

allkeys-random:对全体key,随机进行淘汰

volatile-ttl:设置了ttl的key,随机进行淘汰

allkeys-lru:对全体key,基于lru算法进行淘汰

lru最近最少使用

lfu最少频率使用

volatile-lru:设置了ttl的key,基于lru算法进行淘汰

allkeys-lfu:对全体key,基于lfu算法进行淘汰

volatile-lfu:设置了ttl的key,基于lfu算法进行淘汰

优先使用allkeys-lru         

分布式锁

redis实现分布式锁如何合理控制有效时长?

releaseTime 默认是30秒

redisson实现的分布式锁是可重入的 

 

主从一致性

RedLock:不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n/2+1),避免在一个redis实例上创建锁。

实现复杂,性能差,运维繁琐

redis集群

共有三种方案

主从复制

哨兵模式

分片集群

 主从同步的流程

主从复制:单节点redis并发能力有限,要进一步提高redis的并发能力,就需要搭建主从集群,实现读写分离

同步流程:

        全量同步

1.从节点执行命令,建立连接

2.向主节点请求同步数据(replid,offset)

3.主节点判断是否是第一次请求,是第一次,返回master的数据版本信息,发送从节点

4.从节点保持数据版本信息

5.主节点 执行bgsave命令,生成rdb

6.发送从节点rdb文件

7.从节点情况本地数据,加载rdb文件

8.主节点生成rdb文件过程中,主节点会生成新的repl_baklog记录rdb期间的命令

9.发送repl_baklog中命令给从节点

10.从节点执行接收到的命令

replication_id简称replid,数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave会继承master节点的replid

offset:偏移量,随着repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的 offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要同步

增量同步

1.从节点重启或后期数据变化

2.从节点发送同步数据请求

3.主节点判断replid是否一致

4.不是第一次,回复continue

5.去repl_baklog中获取offset后的数据

6.发送offset后的命令

7.执行命令

哨兵模式:

哨兵机制来实现主从集群的自动故障恢复

监控:sentinel会不断检查master和slave是否按预期工作

自动故障恢复:如果master故障,sentinel会将一个slave提升为master,当故障实例恢复后,也以新的master为主

通知:sentinel充当redis客户端的服务发现来原,当集群发生故障转移时,会将最新信息推送给redis的客户端

服务状态监控:

        sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令

主管下线:如果sentinel节点发现某实例未在规定时间内响应,则认为该实例主管下线

客观下线:若超过指定数量(quorum)sentinel都认为该实例主管下线,则该实例客观下线,quorum最好设置为sentinel实例数量的一半

哨兵选主规则:

1.首先判断主与从节点断开时间长短,如超过指定值就排除该从节点

2.判断从节点的slave-prority值,越小优先级越高

3.如果slave-prority一样,则判断slave节点的offset值,越大优先级越高

4.判断slave节点的运行id,越小优先级越高

哨兵脑裂:由于主节点和从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,但是客户端还在往主节点写数据,哨兵选择新的master,后来原master恢复了,老的master成了新master的从节点,开始同步从节点数据,之前l老master与哨兵断开那会写的数据就会被清除,丢失。

防止脑裂:

reids中配置两个参数

min-replicas-to-write 1 表示最少的slave节点为1

min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒

分片集群结构

主从和哨兵可以解决高并发、高可用问题,但是依然有两个问题没解决:海量数据存储问题、高并发写问题

redis分配引入了哈希槽的概念,redis集群中有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽

分片集群有什么作用?

集群中有多个master,每个master保持不同的数据

每个master可以有多个slave节点

master之间通过ping监测彼此健康状态

客户请求可以访问集群任意节点,最终都会被转发到正确节点

redis是单线程为啥块?

纯内存操作,执行速度非常块

采用单线程,避免上下文切换,多线程还要考虑线程安全问题

使用I/O多路复用模型,非阻塞io

阻塞IO:

阶段一:

用户进程尝试读取数据(比如网卡数据)

此时数据尚未到达,内核需要等待数据

此时用户进程也处于阻塞状态

阶段二:

数据达到并拷贝到内核缓冲区,代表已就绪

将内核数据拷贝到用户缓冲区

拷贝过程中,用户进程依然阻塞等待

拷贝完成,用户进程解除阻塞,处理数据

非阻塞IO:

阶段一:

用户进程尝试读取数据(比如网卡数据)

此时数据未到达,内核需要等待数据

返回异常给用户进程

用户进程拿到error后,再次尝试读取

循环往复,直到数据就绪

阶段二

将内核数据拷贝到用户缓冲区

拷贝过程中,用户进程依然阻塞等待

拷贝完成,用户进程解除阻塞,处理数据

IO多路复用:

利用单个线程来同时监听多个socket,并在某个socket可读、可写时得到通知,从而避免无效的等待,充分利用cpu资源。不过监听socket的方式、通知的方式有多种实现

select

poll

epoll

select、poll只会通知用户进程socket就绪,但不确定是哪个socket,需要用户逐个遍历socket来确认

epoll通知socket就绪的同时,把已就绪的socket写入用户空间

redis网络模型:

连接应答处理器、命令回复处理器、命令请求处理器

redis事务

multi、exec、discard、watch四个命令

三.nginx

1.常用版本

Nginx开源版

Nginx plus商业版

Openrestry

Tengine

四.mysql

1.慢查询

  • 聚合查询
  • 多表查询
  • 表数据量过大查询
  • 深度分页查询

开源工具

调试工具:Arthas

运维工具: Prometheus,Skywalking

mysql自带慢日志

mysql的配置文件配置:

开启sql慢日志查询

slow_query_log=1

设置慢日志的时间

long_query_time=2

分析sql

type这条sql的连接类型,性能由好到差为NULL,system,const,eq_ref,ref,range,index,all

system查询系统中的表
const根据主键查询
eq_ref主键索引查询或唯一索引查询
ref索引查询
range范围查询
index索引树扫描
all全盘扫描

可以采用mysql自带的分析工具explain

通过key和key_len检查是否命中了索引(索引本身存在是否有失效的情况)

通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描

通过extra建议判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或者修改返回字段来修复

红黑树:只能有2个分叉,数据量多的话,层级会很深

B-tree,B树是一种多叉路平衡查找树,想对于二叉树,B树每个节点可以有多个分支,以一颗最大度数为(max-degree)为5(5阶)的b-tree为例,B树每个节点最多存储4个key

B+tree是在B-tree基础上的一种优化,使其更适合实现外存储索引结构,Innodb存储引擎就是使用B+tree实现其索引结构

B树与B+tree对比:

1.磁盘读写代价B+树更低

2.查询效率B+ 树更稳定

3.B+树便于扫库和区间查询

索引

什么是索引?

索引(index)是帮助mysql高效获取数据的的数据结构(有序)

提高数据检索的效率,降低数据库的IO成本(不需要全表扫描)

通过索引列对数据进行排序,降低数据排序的成本,降低了cpu的消耗

聚簇索引

聚簇索引(聚集索引)

非聚簇索引(二级索引)

分类含义特点
聚集索引将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据必须哟而且只有一个
二级索引将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键可以存在多个

聚集索引选取规则

如果存在主键 ,主键索引就是聚集索引

如果不存在主键,将使用唯一索引作为聚集索引

如果表没有主键,没有唯一索引,则innodb会自动生成一个rowid作为隐藏的聚集索引

回表

通过二级索引找到对应的主键值,到聚集索引中查找对整行数据,这个过程就是回表

覆盖索引

查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到

mysql超大分页处理

在数据量比较大时,如果limit分页查询,查询时,越往后,分页查询效率越低

因为,当进行分页查询时,如果执行limit9000000,10,此时需要mysql排序前9000010记录,仅仅返回9000000-9000010的记录,其他记录丢弃,查询排序的待机非常大

优化思路:创建覆盖索引能够比较好的提高性能,可以通过覆盖索引+子查询形式进行优化

select * from tb t,(select id form tb order by id limit 9000000,10) a where t.id = a.id

sql优化经验

  • 表的设计优化
  • 索引优化
  • sql语句优化
  • 主从复制、读写分离
  • 分库分表

表的设计优化:

        参考阿里开发手册

1.设置合适的数值(tinyint,int,bigint),要根据实际情况选择

2.设置合适字符串类型(char、varchar)char定长效率高,varchar可变长度,效率稍低

sql语句优化:

1.select语句务必指明字段名称(避免直接使用select *)

2.sql语句要避免造成索引失效的写法

3.尽量使用union all 代替 union,union会多一次过滤,效率低

4.避免在where字句中对字段进行表达式操作

5.join 优化能用inner join就不用left join right join,如必须使用一定要以小表为驱动,内连接会对两个表进行优化,优先把小表放到外边,把大表放到里边,left join right join不会重新调整顺序

事务

并发事务问题:脏读、不可重复读、幻读

隔离级别:读未提交、读已提交、可重复读、串行化

问题描述
脏读一个事务读到另一个事务还没有提交的数据
不可重复读一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读
幻读一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已存在,好像出现了幻影
隔离级别脏读不可重复读幻读
read uncommitted未提交读
read committed读已提交×
repeatable read(默认)可重复读××
seriaizable串行化×××
undo log 和redo log

缓冲池(buffer pool):主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查时,先操作缓冲池中的数据(若没有数据,则从磁盘加载并缓存),以一定频率刷新到磁盘,从而减少磁盘io,加快处理速度

数据页(page):是Innodb存储引擎磁盘管理的最小单元,每个页的大小默认为16kb,页中存储的是行数据

redo log

记录的是事务提交时数据页的物理修改,是用来实现事务的持久性

日志文件由重做日志缓冲(redo log buffer)及重做日志文件(redo log file),前者是在内存中,后者在磁盘中,当事务提交后把所有修改信息都存到日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用

undo log

回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚和MVCC(多版本并发控制)。undo log 和 redo log记录物理日志不一样,它是逻辑日志

可以认为当delete一条记录时,undo log中会记录一条对应的insert 记录,反之亦然

当update一条记录时,它记录一条对应的相反的update记录,当执行rollback时,就可以从undo log中的逻辑记录读取到相应内容并信息回滚

undo log可以实现事务的一致性和原子性

区别

redo log记录的是数据页的物理变化,服务宕机可用来同步数据

undo log记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据

redo log保证了事务的持久性,undo log保证了事务的原子性和一致性

MVCC

事务中的隔离线是如何保证的?

锁:排它锁(如一个事务获取了一个数据行的排它锁,其他事务就不能再获取该行的其他锁)

mvcc:多版本并发控制

MVCC 多版本并发控制,指维护一个数据的多个版本,使得读写操作没有冲突

MVCC的具体实现依赖于数据库中的隐式字段、undo log、readView

隐式字段:
隐藏字段含义
DB_TRX_ID最近修改事务id,记录插入这条记录或最后一次修改记录的事务id
DB_ROLL_PTR回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本
DB_ROW_ID隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段

undo log回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志

当insert时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除

当update、delete的时候,产生的undo log日志不仅在回滚时需要,mvcc版本访问也需要,不会立即被删除。

undo log 

版本链 

不同事务或相同事务对同一条记录进行修改,会导致该条记录的undo log生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。 

readview

是快照读sql执行时mvcc提取数据的依据,记录并维护当前活跃的事务(未提交)id

当前读:读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁,如select ... lock in share mode(共享锁),select...for update,update,insert,delete都是一种当前读

快照读:简单的select(不加锁)就是快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁是非阻塞读

read committed:每次select ,都生成一个快照读

repeatable read:开启事务后第一个select语句才是快照读的地方。

readview包含了四个核心字段

字段含义
m_ids当前活跃的事务id集合
min_trx_id最小活跃事务id
max_trx_id预先分配事务id,当前最大事务id(因为事务id是自增的)
creator_trx_idreadview创建者的事务id

版本链数据访问规则:

trx_id代表是当前事务id

1.trx_id == creator_trx_id ?可以访问该版本

2.trx_id < min_trx_id? 可以访问该版本

3.trx_id > max_trx_id?不可以访问该版本

4.min_trx_id <= trx_id <= max_trx_id ?如果trx_id不在m_ids中可以访问该版本

不同的隔离级别,生成readView的时机不同:

read committed:在事务中每一次执行快照读时生成readView

repeatable read:在事务第一次执行快照读时生成readView,后续复用该readView

主从同步

mysql主从复制的核心是二进制文件

二进制文件(binlog)记录了所有的ddl和dml语言,不包括select、show语句

复制三步

1.master主库在事务提交时,会把数据变更记录在二进制日志文件binlog中

2.从库读取主库的二进制日志文件binlog,写入到从库的中继日志relay log

3.重做中继日志中的事件,将改变反映它自己的数据

分库分表

垂直拆分

垂直分库:以表为依据,根据业务不同将不同表拆分到不同库中

特点:按业务对数据分级管理、维护、监控、扩展;高并发下,提高磁盘io和数据连接数

垂直分表:hubu依据,根据字段属性不同字段拆分到不同表中

拆分规则:把不常用的字段单独放在一张表、把text、blob等大字段拆分出来放在附表

特点:冷热数据分离;减少io过度争抢,两表互不影响

水平拆分

水平分库:将一个库的数据拆分到多个库中

路由规则:

  • 根据id节点取模
  • 按id范围路由,节点1(1-100w),节点2(100w-200w)

特点:解决了单库大数量,高并发的性能瓶颈问题;提高系统的稳定性和可用性

水平分表:将一个表的数据拆分到多个表中(可以再同一个库中)

特点:优化单一表数据量过大而产生的性能问题;避免id争抢并减少锁表的几率

spring

spring中的单例bean是线程安全的吗?

不是线程安全的

一般在spring的bean中都是注入无状态的对象,没有线程安全问题,如果在bean中定义了可修改的成员变量,是要考虑线程安全问题的,可以使用多例或者加锁来解决

AOP

面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为切面,减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性

事务失效的的场景
  • 异常捕获处理
  • 抛出检查异常
  • 非public方法:spring为方法创建代理、添加事务通知,前提条件是该方法是pulic

springbean的生命周期

spring容器在实例化时,会将xml配置的bean的信息封装成一个BeanDefinition对象,spring根据beanDefinition来创建bean对象,里面有很多的属性来描述bean

BeanDefinition->构造方法->依赖注入->aware接口(BeanNameAware、BeanFactoryAware、ApplicationContextAware)->BeanPostProcessor#before->初始化方法(IntializingBean接口、PostConstruct)->BeanPostProcessor#after->销毁bean

springmvc的流程

视图阶段:

请求->DispatcherServlet前端控制器->查询handler,HandlerMapper 处理器映射器->

HandlerExecutionChain处理器执行链(如有)->DispatcherServlet->请求handler ,到handlerAdapter-> 请求handler,处理器handler(为啥不直接请求handler,多了handlerAdapter,用来处理参数、处理返回值)->选中handler->返回modelAndView,DispatcherServlet->返回视图解析器ViewResolver->返回view对象,DispatcherServlet

前后的分离阶段:

mybatis

执行流程

1.mybatis-config.xml(加载运行环境+映射文件)

2.构建会话工厂sqlSessionFactory,全局一个,生产sqlSession

3.创建会话sqlSession,项目与数据库的会话,包含了执行sql语句的所有方法

4.executor执行器 真正执行数据库操作接口,也负责查询缓存的维护

5.mappedStatement对象

延迟加载

mybatis支持延迟加载,但默认没有开启,什么叫延迟加载?

使用CGLIB创建目标对象的代理对象

当调用目标方法的xx.getXX()方法,进入拦截器invoke方法,发现xx.getXX()为null,执行sql查询

把xx查询出来,然后调用xx.setXX(XX xx),接着完成xx.getXX()方法的调用

mybatis缓存

 一级、二级缓存都是保存在本地,本地缓存,叫做PerpetualCache,本质是一个HashMap

一级缓存:作用域是session级别

二级缓存:作用域是namespace和mapper的作用域,不依赖session,默认是关闭的

开启二级缓存:

mybatis-config.xml

<setting name="cacheEnabled" value="true"/>

对应的mapper文件中添加<cache/>

二级缓存需要缓存的数据实现Serializable接口

只有会话提交或关闭后,一级缓存中的数据才会转移到二级缓存

一级缓存:基于PerpetualCache的HashMap本地缓存,其作用域为session,当session进行flush或close之后,该session中的所有cache就将清空,默认打开一级缓存。

当一个作用域(一级缓存session/二级缓存NameSpaces)进行了新增、修改、删除操作后,默认该作用域下所有select 中的缓存将被clear

微服务

服务注册、服务发现、服务监控

服务注册:服务提供这把自己的信息注册到注册中心,注册中心保存这些信息,如服务名称,ip,端口等等;

服务发现:消费者向注册中心拉去服务列表信息,如果服务提供者有集群,则消费者会利用负载均衡算法,选择一个发起调用

服务监控:服务提供者每隔30秒向注册中心发送心跳,报告健康状态,如果注册中心90秒没接收到心跳,从注册中心剔除。

nacos与eureka不同之处

nacos  discovery增加了ephemeral 用来设置临时实例或非临时实例

默认是临时实例,设置为非临时实例后,注册中心会主动询问服务提供者是否存活

nacos会主动推送服务变更信息给消费者

临时实例心跳不正常会被剔除,非临时实例则不会剔除

nocoa集群默认采用ap模式,当集群中存在非临时实例时,采用cp模式;eureka采用ap方式

负载均衡

ribbon,发起远程调用feign就会使用ribbon

负载均衡策略:

  • roundRobinRule 简单轮询服务列表来选择服务器
  • weightedResponseTimeRule:按照权重选择服务器,响应时间越长,权重越小
  • RandomRule 随机选择一个可用服务器
  • ZoneAvoidanceRule 区域敏感策略,如果没有zone概念,多个服务做轮询

如何自定义负载均衡

自己创建类实现IRule接口,然后再通过配置类或配置文件配置即可。

全局:

局部:

 服务雪崩

一个服务失败,导致整条链路的服务都失败的情形

服务降级

服务降级是服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃

服务熔断

Hystrix熔断机制,用于监控微服务调用情况,默认是关闭的。如果需要开启需要在引导类上添加注解@EnableCircuitBreaker,如果检测到10秒请求的失败率超过50%,就触发熔断机制,之后每隔5秒重写尝试请求微服务,如果微服务不能响应,继续走熔断机制,如果微服务可达,则关闭熔断机制,恢复正常请求。

skywalking

APM:应用性能监控工具

服务:业务资源应用系统

端点:应用系统对外暴漏的功能接口

实例:物理机

skywalking主要可以监控接口、服务、物理实例的一些状态,特别是在压测的时候可以看到众多服务中哪些服务和接口比较慢,可以针对性的分析和优化

设置告警规则,项目上线后,如果报错,可以设置给相关负责人发送短信和邮件

限流

为什么要限流?

并发的确大(突发流量)

防止用户恶意刷接口

限流的实现方式:

tomcat:可以设置最大连接数

nginx:漏桶算法

   控制速率(突发流量)

控制并发连接数

网关 令牌桶算法

   微服务路由设置添加局部过滤器RquestRateLimiter

自定义拦截器

分布式事务

CAP定理

一致性、可用性、分区容错性

分布式系统无法满足这三个指标

一致性:用户访问分布式系统中的任意节点,得到的数据必须一致

可用性:用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝

分区:因为网络故障或其他原因导致分布式系统中的部分节点与其他节点失去连接,形成独立分区

容错:在集群出现分区时,整个系统也要持续对外提供服务

分布式系统节点之间肯定是需要网络连接的,分区P是必然存在的

Base理论

base理论是对cap的一种解决思路,包含三个思想:

基本可用:分布式系统在出现故障时,允许损失部分可用性,即保证核心可用

软状态:在一定时间内,允许出现中间状态,比如临时的不一致状态

最终一致性:虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致

cap定理

分布式系统节点通过网络连接,一定会出现分区问题

当分区出现时,系统的一致性和可用性就无法同时满足

Base理论

基本可用

软状态

最终一致

解决分布式事务的思想和模型

最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据(AP)

强一致性:各分支事务执行完业务不提交,等待彼此结果,而后统一提交或回滚(CP)

解决方案
seta框架

XA模式

RM一阶段工作

向tc注册分支事务

执行分支业务sql但不提交

报告执行状态到tc

TC二阶段工作

tc检查各分支事务的执行状态

如果都成功,通知所有rm提交事务

如果有失败,通知所有rm回滚事务

RM二阶段工作

接受tc指令,提交或回滚事务

AT模式:弥补了xa模型中资源锁定周期过长的缺陷

阶段一RM工作

注册分支事务

记录undo log(数据快照)

执行业务sql并提交

报告事务状态

阶段二提交时RM工作

删除undo log

阶段二回滚RM工作

根据undo log恢复数据到更新前

TCC模式

try:资源的检测和预留

confirm:完成资源操作业务;要求try成功,confirm一定要能成功

cancel:预留资源释放,可理解为try的反向操作

项目中采用的哪种方案?

seata的XA模式:cp,需要互相等待各个分支事务提交,可以保证强一致性,性能差

seata的AT模式:ap,底层采用undo log实现,性能好

seata的TCC模式:ap,性能较好,不过需要人工编码实现

MQ:在A服务写数据的时候,需要在同一个事务内发送消息到另外一个事务,异步,性能好

接口幂等性

幂等:多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。

数据库唯一索引

token+redis

分布式锁

分布式定时任务

解决集群任务的重复执行问题

cron表达式定义灵活

定时任务失败了,重试和统计

任务量大,分片执行

路由策略:

round:轮询

failover:故障转移 ,按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度

sharding_broadcast:分片广播,广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数,并根据分片参数开发分片任务

如果大数据量的任务同时都需要执行,怎么解决?

让多个实例一块去执行(部署集群),路由策略为分片广播

在任务执行的代码中可以获取分片总数和当前分片,按照取模的方式分摊到各个实例中执行。 

消息中间件

消息不丢失

rabbitmq 消息丢失可能发生的节点

 生产者确认机制

rabbitmq提供了pulisher confirm机制来避免消息发送到mq过程中丢失,消息发送到mq以后,会返回一个结果给发送者,表示消息是否处理成功

消息发送失败:

        发送到交换机失败或发送到消息队列失败

消息失败后如何处理呢?

回调方法及时重发

记录日志

保存到数据库后定时重发,成功发送后即刻删除表中的数据

消息持久化

mq默认是内存存储消息,开启持久化功能可以确保缓存在mq中的消息不丢失

交换机持久化

@Bean

public DirectExchange simpleExchange(){

       //三个参数 交换机名称 、是否持久化、当没有queue与其绑定时是否自动删除

        return new DirectExchange("simple.direct",true,false);

}

队列持久化

public Queue simpleQueue(){

        //使用QueueBuilder构建队列,durable就是持久化的

        return QueueBuilder.durable("simple.queue").build();

}

消息持久化,springamqp中的消息默认是持久化的,可以通过MessageProperties的Deliver       yMode来指定

MessageBuilder.withBody(message.getBytes(StandardCharsets.UTF_8))

.setDeliveryMode(MessageDeliveryMode.PERSISTENT)//持久化

.build();

消费者确认

rabbitmq支持消费者确认机制,即消费者处理消息后可以向mq发送ack回执,mq收到ack回执后才会删除该消息

springamqp允许配置三种确认模式

manual:手动ack,需要在业务代码结束后,调用api发送ack

auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack,抛出异常则返回nack

none:关闭ack,mq假定消费者获取消息后会成功处理,因此消息投递后立即被删除

可以利用spring的retry机制,在消费者出现异常时利用本地重试,设置重试次数,当次数达到了以后,如果消息依然失败,将消息投递到异常交换机,交由人工处理

如何保证消息不丢失

开启生产者确认机制,确保生产者的消息能到达队列

开启持久化功能,确保消息未消费前在队列中不会丢失

开启消费这确认机制为auto,由spring确认消息处理成功后完成ack

开启消费者失败重试机制,多次重试失败后将消息投递到异常交换机中,交由人工处理

消息重复消费

什么情况下会导致重复消费?

网络抖动

消费者挂了

解决方案

每条消息设置一个唯一的标识id

幂等方案:分布式锁,数据库锁

延迟队列、死信交换机

延迟队列:进入队列的消息会被延迟消费的队列

延迟队列=死信加交换机+ttl

死信交换机

当一个队列中的消息满足下列情况之一时,可以成为死信

  • 消费者使用basic.reject或basic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息是一个过期消息,超时无人消费
  • 要投递的队列消息堆积满了,最早的消息可能成为死信

如果该队列设置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机DLX

QueueBuilder.durable("simple.queue").ttl(10000).deadLetterExchange("dl.direct").build();

TTL:

消息所在的队列设置了存活时间

消息本身设置了存活时间

延迟队列插件实现延迟队列DelayExchange

声明一个交换机,添加delayed属性为true

发送消息时,添加x-delay头,值为超时时间

消息堆积

当生产者发送消息的速度超过了消费者处理速度,就会导致队列中的消息堆积,直到队列存储消息达到上限,之后发送的消息就会被成为死信,可能会被丢弃,这就是消息对接的问题

解决消息堆积三种思路

  • 增加更多消费者,提高消费速度
  • 在消费者内开启线程池加快消息处理速度
  • 扩大队列容积,提高堆积上限

惰性队列

特征:

  • 接收到信息后直接存入磁盘非内存
  • 消费者要消费消息时才会从磁盘中读取并加载到内存
  • 支持数百万条的消息存储

@Bean

public Queue lazyQueue(){

        return QueueBuilder.durable("lazy.queue").lazy().build()

}

或者

@RabbitListener(queuesToDeclare=@Queue(name="lazy.quque",durable="true",arguments=@Argument(name="x-quque-mode",value="lazy"))){

xxxxxx

}

延迟队列

死信队列

高可用机制

在生产环境下,使用集群来保证高可用性

普通集群、镜像集群、仲裁队列

普通集群

会在集群的各个节点共享部分数据,包括:交换机、队列元信息、不包含队列中的消息

当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回。

队列所在节点宕机,队列中的消息就会丢失

镜像集群

本质是主从模式

交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份

创建队列的节点被称为队列的主节点,备份到其他节点的叫做该队列的镜像节点

一个队列的主节点可能是另外一个队列的镜像节点

所有操作都是主节点完成,然后同步给镜像节点

主宕机后,镜像节点会替代成新的主

仲裁节点:

是3.8版本以后才有的新功能,用来代替镜像队列

与镜像队列一样,都是主从模式,支持主从同步数据

使用非常简单,没有复杂的配置

主从同步基于raft协议,强一致性

@Bean

public Queue quorumQueue(){

   return QueueBuilder.durable("quorum.queue").quorum().build();

}

kafka

如何保证消息不丢失?

使用kafka在消息的收发过程中都会出现消息丢失,kafka分别给出了解决方案

  • 生产者发送消息到Brocker丢失
  • 消息在Broker中存储丢失
  • 消费者从Broker接受消息丢失

生产者发送消息到Broker

//同步发送
RecordMetadata rocordMetadata = kafkaProduer.send(rcord).get();

//异步发送
kafkaProducer.send(record,new Callback(){
    @Override
    public void onCompletion(RecordMetadata recordMetaData,Exception e){

        if(e!=null){
            System.out.println("消息发送失败");

        }
        recordMetadata.offset();
        recordMetadata.partition();
        String topic =  recordMetadata.topic();
}

})

//消息重试
prop.put(ProducerConfig.RETRIES_CONFIG,10);

消息在broker中存储丢失

发送确认机制acks

确认机制说明
acks=0生产者在写入消息之前不会等待任何来自服务器的响应,消息有丢失的风险,但是速度最快
acks=1(默认值)只要集群首领节点收到消息,生产者就会收到一个来自服务器的成功响应
acks=all只有当所有参与赋值的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应

消费者从broker中接受消息丢失

消费者默认是自动按期提交已经消费的便宜量,默认是每隔5秒提交一次,如果出现重平衡的现象,可能会重复消费或丢失数据

如何防止呢?

禁用自动提交偏移量,改为手动

同步 

如何保证消费的顺序性

topic分区中,消息只能由消费者组中的唯一一个消费者处理,所以消息肯定是按照先后顺序进行处理的。但是它也仅仅保证topic的一个分区顺序处理,不能保证跨分区的消息先后处理,所以,要想顺序处理topic的所有消息,只能提供一个分区

2种解决方案:

  • 发送消息时候指定分区
  • 发送消息时按照相同的业务设置相同的key
如何保证高可用性

集群模式

一个kafka集群由多个broker组成,这样集群中某一台机器宕机,其他机器上的broker也依然能够对外提供服务。

分区备份机制

一个topic有多个分区,每个分区有多个副本,其中有一个leader,其余的是follower,副本存储在不同的broker中

所有的分区副本的内容都是相同的,如果leader发生故障时,会自动将其中的一个follower提升为leader

ISR(in-sync replica)需要同步复制保存follower

如果leader失效后,需要选出新的leader,选举的原则如下:

  • 选举是有限从isr中选定,因为这个列表中follower的数据与leader同步的
  • 如果isr中的follower都不行了,就只能从其他follower中获取
kafka数据清理机制

文件存储机制

.index索引文件

.log数据文件

.timeindex时间索引文件

为什么要分段?

删除无用文件方便,提高磁盘利用率

查找数据便捷

总述:

kafka中topic的数据存储在分区上,分区如果文件过大会分段存储segment

每个分段都在磁盘上以索引(xxx.index)和日志文件(xxx.log)的形式存储

分段的好处是,第一能够减少单个文件内容的大小,查找数据方便,第二方便kafka进行日志清理

数据清理机制

日志的清理策略有两个:

1.根据消息的保留时间,当消息在kafka中保存的时间超过了指定的时间,就会触发清理过程

2.根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阀值,则开始删除最久的消息,需要手动开启

 kafka中的高性能设计
  • 消息分区 不受单台服务器的限制,可以不受限的处理更多的数据
  • 顺序读写 磁盘顺序读写,提升读写效率
  • 页缓存 把磁盘中的数据缓存到内存中,把对数据的访问变为对内存的访问
  • 零拷贝 减少上下文切换及数据拷贝
  • 消息压缩 减少磁盘io和网络io
  • 分批发送 将消息打包批量发送,减少网络开销

设计模式

工厂模式

策略模式

该模式定义了一些列算法,并将每个算法封装起来,使它们可以互相替换,且算法的变化不会影响使用算法的客户

责任链设计模式

为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一对象的引用而形成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

常见技术场景

单点登录

权限认证

RBAC:ROLE-Based Access Control 基于角色的访问控制

上传数据的安全性怎么控制?

使用非对称加密(或对称加密),给前端一个公钥让他把数据加密后传到后台,后台负责解密后处理数据

对称加密:文件加密和解密使用相同的密钥,即加密密钥也可以作为解密密钥

非对称加密:公开密钥和私有密钥,公有密钥加密,私有密钥解密;优点:与对称加密相比,安全性更高;缺点:加密和解密速度慢,建议少量数据加密

负责项目的时候遇到的棘手问题

1.设计模式

2.线上bug(CPU飙高、内存泄露、线程死锁)

3.调优(慢接口、慢sql、缓存方案)

4.组件封装(分布式锁、接口幂等、分布式事务、支付通用)

项目中日志是怎么采集的?

采集日志的方式:elk、常规采集:按天保存到一个日志文件

查看日志的命令

实时监控日志变化
tail -f xxx.log
tail -n 100 -f xxx.log

按照行号查询
日志行号区间: cat -n xxx.log|tail -n +100|head -n 100(查询100行至200行的日志)
按照关键字找日志信息
cat -n xxx.log | grep "debug"
按照日期查询
sed -n '/2023-05-18 14:22:31.070/,/2023-05-18 14:27:14.158/p' xxx.log
日志太多,处理方式
 分页查询日志信息 cat -n xxx.log | grep "debug" | more
  筛选过滤以后,输出到一个文件 cat -n xx.log | grep "debug" > debug.txt

生产环境问题怎么排查

1.分析日志

2.远程debug

远程debug

前提条件:远程的代码和本地的代码要保持一致

1.远程代码需要配置启动参数,把项目打包放到服务器后启动项目的参数

java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 project-1.0-SNAPSHOT.jar

2.idea中设置远程debug,找到idea中的Remote JVM Debug

怎么快速定位系统瓶颈问题

  • 压测(性能测试) 项目上线之前测评系统的压力

压测目的:给出系统当前的性能情况,定位系统性能瓶颈或潜在性能瓶颈

指标:响应时间、qps、并发数、吞吐量、cpu利用率、内存使用率

  • 监控工具、链路追踪工具,项目上线之后监控
  • 线上诊断工具Arthas(阿尔萨斯),项目上线之后监控、排查

JVM

JVM:程序的运行环境(java二进制字节码的运行环境)

好处:一次编写,到处运行;自动内存管理,垃圾回收机制

jvm组成

 程序计数器

线程私有的,内部保存的字节码的行号,用于记录正在执行的字节码指令的地址

javap -v xx.class 打印堆栈信息

Java堆

线程共享的区域,主要用来保存对象实例、数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出oom

年轻代被划分为三部分:eden区和两个大小严格相同的survivor区,根据jvm策略,在经过几次垃圾收集后,任然存活于survivor的对象将被移动到老年代区间

老年代主要保存生命周期长的对象,一般是一些老的对象

元空间:保存类信息、静态变量、常量、编译后的代码 

 虚拟机栈

每个线程运行时所需要的内存,称为虚拟机栈,先进后出

每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存

每个线程只能有一个活动栈帧,对应着正在执行的那个方法

垃圾回收是否涉及栈内存?

垃圾回收主要指堆内存,当栈帧弹栈以后,内存就会释放

栈内存分配的越大越好吗?

未必,默认的栈内存通常为1024k

栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线城市则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半。

方法内的局部变量是否线程安全?

 如果方法内部的局部变量没有逃离方法的作用范围,它是线程安全

如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

栈内存溢出情况
  • 栈帧过多导致栈内存溢出,典型问题:递归调用
  • 栈帧过大导致栈内存溢出
栈和堆的区别

栈内存一般会用来存储局部变量和方法调用,但是堆内存是用来存储对象和数组的,堆会gc垃圾回收,栈不会

栈内存是线程私有的,堆内存是线程共有的

两者异常错误不同,内存不足都会抛出异常

堆:java.lang.OutOfMemoryError

栈:  java.lang.StackOverFlowError

方法区

methodArea是各个线程共享的内存区域

主要存储类的信息、运行时常量池

虚拟机启动时创建、关闭虚拟机时释放

如果方法区的内存无法满足分配请求,就会抛出OutOfMemoryError:Metaspace

运行时常量池

可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

直接内存

并不属于jvm的内存结构,不由jvm进行管理,是虚拟机的系统内存,常见于NIO操作时,用于数据缓冲区,它分配回收成本较高,但读写性能高

常规io的数据拷贝流程

nio数据拷贝流程

 类加载器

类加载器:用于装载字节码文件(.class文件)

运行时数据区:用于分配存储空间

执行引擎:执行字节码文件或本地方法

垃圾回收器:用于对jvm中的垃圾内容进行回收

jvm只会运行二进制文件,类加载器的作用就是将字节码文件加载到jvm中,从而让Java程序能够启动起来。

Bootstrap ClassLoader 启动类加载器由c++编写实现,加载JAVA_HOME/jre/lib
ExtClassLoader 扩展类加载器加载JAVA_HOME/jre/lib/ext
AppClassLoader从classpath中加载,应用类加载器加载开发者自己编写的Java类
CustomizeClassLoader自定义类加载器,实现自定义类加载规则
双亲委派模型

加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类

为什么采用双亲委派?

  • 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性
  • 为了安全,保证类API不会被修改

由于是双亲委派机制,java.lang.String在启动类加载器得到加载,因为在核jre库中有相同名字的类文件,但该类中并没有main方法,这样就能防止恶意篡改核心api库。

 类装载的过程

类从加载到虚拟机中开始,直到卸载为止

生命周期:加载、验证、准备、解析、初始化、使用、卸载

其中验证、准备、解析三个部分统称为连接

 加载

通过类的全名,获取类的二进制数据流

解析类的二进制数据流为方法区的数据结构(java类模型)

创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

验证

验证类是否符合jvm规范,安全性检查

1.文件格式验证

2.元数据验证

3.字节码验证

4.符号引用验证

准备

为类变量分配内存并设置类变量初始值

static变量,分配空间在准备阶段完成(设置默认值),赋值在初始化阶段完成

static变量是final的基本类型,字符串常量,值已确定,赋值在准备阶段完成

static变量是final引用类型的,赋值也会在初始化阶段完成

解析

把类中的符号引用转换为直接引用

比如:方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接指向方法。

初始化

对类的静态变量、静态代码块执行初始化操作

  • 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类
  • 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行
  • 子类去调用父类的静态变量时候,只会初始化父类

使用

jvm开始从入口方法开始执行用户的程序代码

调用静态类成员信息(静态字段、静态方法)

使用new关键字为其创建对象实例

卸载

当用户程序代码执行完毕后,jvm便开始销毁创建的class对象

垃圾回收

对象什么时候可以被垃圾去回收

如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收

两种方式确定什么是垃圾:引用计数法,可达性分析算法

引用计数法:一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收。当对象间出现了循环引用的话,则引用计数就会失效。

可达性分析:现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾

采用可达性分析来探索所有存活的对象

扫描堆中的对象,看是否能够沿着GC ROOT 对象为起点的引用链找到该对象,找不到,表示可以回收

哪些对象可以作为GC ROOT?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象

  • 方法区中类静态属性引用的对象 
  • 方法区中常量引用的对象 

  • 本地方法栈中JNI(一般native方法)引用的对象 
垃圾回收算法
  • 标记清除算法
  • 复制算法
  • 标记整理算法

标记清除算法

将垃圾回收分为2个阶段,分别是标记和清除

1.根据可达性分析算法得出的垃圾进行标记

2.对这些标记为可回收的内容进行垃圾回收

优点:标记和清除速度较快

缺点:碎片化较为严重,内存不连贯

标记整理算法

 优缺点同标记清除算法,解决了标记清除算法的碎片化的问题,同时标记压缩算法多了一步,对象移动内存位置的步骤,其效率也有一定的影响。

复制算法

将内存区域划分成大小相等的两块

 

 优点:在垃圾较多的情况下,效率较高;清理后,内存无碎片

  缺点:分配的2块内存空间,在同一时刻,只能使用一半,内存使用率较低

jvm的分代回收

java8,堆分成了2份:新生代和老年代(1:2)

新生代1/3堆空间,老年代2/3堆空间

新生代内部又分了三个区域:

eden,新生的对象都分配到这里

幸存者区suivivor(分成from 和 to) 

eden:from:to = 8:1:1

工作机制:

新创建对象,都会先分配到eden区

当eden区内存不足,标记eden与from(现阶段没有)的存活对象

将存活对象采用复制算法复制到to中,复制完毕后,eden和from内存得到释放

经过一段时间eden内存又出现不足,标记eden和to区存活对象,将存活的对象复制到from区

当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

MinorGC、MixedGC、FullG的区别

MinorGC(young GC)发生在新生代的垃圾回收,暂停时间短(STW)

MixedGC新生代+老年代部分区域的垃圾回收,G1收集器特有

FullGC 新生代+老年代完整垃圾回收,暂停时间长(STW),应尽力避免

STW:暂停所有应用程序线程,等待垃圾回收的完成

垃圾回收器
  • 串行垃圾收集器
  • 并行垃圾收集器
  • CMS(并发)垃圾收集器
  • G1垃圾收集器

串行垃圾收集器

serial、serial old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑

serialserial old
作用于新生代,采用复制算法作用于老年代,采用标记-整理算法

垃圾回收时,只有一个线程工作,并且Java应用中的所有线程都要暂停,等待垃圾回收的完成

并行垃圾收集器

parallel new 和 parallel old 是一个并行垃圾回收器,jdk8默认使用此垃圾回收器

parNewparallel old
作用于新生代,采用复制算法作用于老年代,采用标记-整理算法

垃圾回收时,多个线程工作,并且Java应用中的所有线程都要暂停,等待垃圾回收的完成

CMS(并发)垃圾收集器

cms concurrent mark sweep 是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验好。最大特点是在进行垃圾回收时,应用仍然能正常进行。

 G1垃圾回收器

应用在新生代和老年代,在jdk9之后默认使用G1

划分成多个区,每个区都可以充当eden、survivor、old、humongous,humongous时专门为大对象准备的;采用复制算法;响应时间于吞吐量兼顾;分成三个阶段:新生代回收、并发标记、混合收集;如果并发失败(即回收速度赶不上创建新对象速度),会触发Full GC

Young collection (年轻代垃圾回收)

初始时,所有区域都处于空闲状态

创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象

当伊甸园需要垃圾回收时,跳出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程  

随着时间流逝,伊甸园内存又不足

将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升到老年代

Young collection+Concurrent mark(年轻代垃圾回收+并发标记)

当老年代占用内存超过阈值(默认45%),触发并发标记,这时无需暂停用户线程

并发标记后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程

这些都完成以后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域

混合收集阶段,参与复制的有eden、survior、old

Mixed collection 混合垃圾回收

复制完成,内存得到释放,进入下一论的新生代回收、并发标记、混合收集

强引用、软引用、弱引用、虚引用

强引用

只有所有GC Roots对象不引用该对象,该对象才能被垃圾回收

软引用

仅有软引用引用该对象,在垃圾回收后,内存仍不足时会再次触发垃圾回收

弱引用

仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收该对象

 

虚引用 

必须配置引用队列使用,被引用对象回收时,会将虚引用入队,由Reference handler线程调用虚引用相关方法释放直接内存

jvm实践 

 jvm调优

调优的参数设置

war包部署在tomcat中设置

jar包部署在启动参数设置

war包tomcat中设置

修改TOMCAT_HOME/bin/catalina.sh文件

JAVA_OPTS="-Xms512m -Xmx1024m"

jar包启动参数设置

nohup java -Xms512m -Xmx1024m -jar xxx.jar --spring.profiles.active=prod &

nohup:用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行

&:让命令在后台执行,当用户退出(挂起)的时候,命令自动也跟着退出

 调优参数

Java HotSpot VM Options

  • 设置堆空间大小
  • 虚拟机栈的设置
  • 年轻代中eden和survivor区的大小比例
  • 年轻代晋升老年代阈值
  • 设置垃圾回收收集器

设置堆空间大小

为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生的额外时间,通常把最大、初始大小设置我i相同的值

最大大小默认是物理内存的1/4,初始大小是物理内存的1/64;堆太小,可能频繁导致年轻代和老年代的垃圾回收,会产生stw,暂停用户线程;堆内存大肯定是好的,存在风险,假如发生了fullgc,会扫描整个堆空间,暂停用户线程的时间长。

虚拟机栈的设置

虚拟机栈的设置,每个线程默认会开启1m的内存,用于存放栈帧、调用参数、局部变量等,但一般256k就够用,通常减少每个线程的堆栈,可以产生更多的线程,但实际上还受限于操作系统。

-Xss 对每个线程stack大小的调整,-Xss128k

年轻代eden区和survior区的大小比例

默认比例为8:1.通过增大eden区的大小,来减少YGC发生的次数,但有时,虽然次数少了,但eden区满的时候,占用内存空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优

-XXSurvivorRatio=8,表示年轻代中的分配比例:survivor:eden=2:8

年轻代晋升老年代阈值

-XX:MaxTenuringThreshold=threshold

默认值是15,取值范围0-15

设置垃圾回收收集器

通过增大吞吐量提供系统性能,可以通过设置并行 垃圾回收收集器

-XX:+UseParallelGC

-XX:+UsseParallelOldGC

-XX:+UseG1GC

jvm调优工具

命令工具

jps 进程状态信息

jstack 查看Java进程内线程堆栈信息  jstack pid

jmap 查看堆转信息

jmap   -heap pid

jmap -dump:format=b,file=heap.hprof pid

jhat 堆转储快照分析工具

jstat jvm统计监测工具

jstat -gcutil pid

可视化工具

jconsole 用于对jvm的内存,线程,类的监控

visualvm 监控线程 内存情况

内存泄露排查

1.获取堆内存快照dump

2.visualvm去分析dump文件

3.通过查看堆信息的情况,定位内存溢出问题

使用jmap命令获取运行中程序dump文件

jmap -dump:format=b,file=heap.hprof pid

使用vm参数获取dump文件

有的情况是内存溢出后程序则会直接中断,而jmap只能打印在运行中的程序,所以建议通过参数的方式生产dump文件

-XX:+HeapDumpOnOutOfMemoryError
-XX:+HeapDumpPath=/home/app/dumps/

cpu飙高排查

1.使用top命令查看占用cpu情况

2.通过top命令,可以查看哪一个进程占用cpu高

3.根据进程找线程

ps H -eo pid ,tid,%cpu | grep pid

找到cpu高的线程id

linux 十进制转十六进制  printf “%x\n” 2276

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
谢谢大家的支持,我会陆续上传相关电子书 由于体积较大,本书分四卷压缩,请都下载完再解压! Java高手真经 高级编程篇 下载(一) http://download.csdn.net/source/3275208 Java高手真经 高级编程篇 下载(二) http://download.csdn.net/source/3275230 Java高手真经 高级编程篇 下载(三) http://download.csdn.net/source/3275245 Java高手真经 高级编程篇 下载(四) http://download.csdn.net/source/3275262 本书讲解Java Web开发中的高级开发技术,包括企业级的开发技术EJB、各种Java EE的分布式开发技术、Java Web的各种开源技术与框架,这3部分内容层层递进,涵盖了Java EE开发中的各种分布式与业务核心技术。讲解的主要内容包括如下。   Java Web企业级开发技术EJB:包括会话Bean、消息驱动Bean、实体Bean、拦截器、依赖注入、定时器、JPA持久化、JPQL查询语言。   Java Web分布式开发技术:包括JTA事务管理、JAAS验证与授权服务、JNDI命名和目录服务、JMS消息服务、JavaMail邮件服务、WebService、JMX管理、JCA连接器。   Java Web开源技术与框架:包括工作流、规则引擎、搜索引擎、缓存引擎、任务调度、身份认证、报表服务、系统测试、集群与负载均衡。   随书附赠光盘内容为本书各种原型包、系统源程序。本书内容循序渐进,通俗易懂,覆盖了Java Web高级开发的各种技术。无论对于Java软件设计还是软件开发,本书都是精通开发Java Web应用的必备的实用手册。   本书适合作为Java相关培训机构的教材,也可作为Java自学人员的参考手册。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值