230212面试知识点整理

MVC,OOP,AOP

M代表Model(模型),该组件是对软件所处理问题逻辑的一种抽象,封装了问题的核心数据,逻辑和功能实现,独立于具体的界面显示以及I/O操作。

V代表View(视图),该组件将表示模型数据,逻辑关系以及状态信息,以某种形式展现给用户。视图组件从模型组件获得显示信息,并且对于相同的显示信息可以通过不同的显示形式或视图展现给用户。

C代表Controller(控制器),该组件主要负责用户与软件之间的交互操作,控制模型状态变化的传播,以确保用户界面与模型状态的统一。

微服务架构理念及实现技术

大部分企业选择微服务架构是业务驱动的,对于基于传统的J2EE技术栈的web项目而言,早期单体建构就是所谓的“一个war包打天下”,将应用程序的所有功能都打包成一个独立的war包,部署在tomcat的指定目录下就可以顺利运行。然而,软件项目是一个不断迭代和变化的过程,业务模块的增加,功能的扩展,人员的更迭、需求的变动最终都需要修改代码来实现。于是代码跟随版本的不断升级而逐渐膨胀变得难以维护。单体架构的灵活行、可扩展性、可运维性都明显下降,开发人员效率变低、系统稳定性变差、局部小问题导致“牵一发而动全身”。

在这种情况下,单体架构为了保证程序内部的高内聚、低耦合,就以后如了微服务的架构模式

微服务架构进行前后端职责划分的主要原则:

技能分离,前后端可以使用不同的特定语言或框架来实现最佳的微服务实践。

职责分离,前端主要负责和用户的交互逻辑,后端主要负责业务逻辑和资源的管理

部署分离,前端和后端可以做到独立发布,不存在发布过程的耦合,前端和厚度那可以根据约定的api进行版本迭代和独立演进

软件的设计原则

1.可靠性

软件可靠性意味着该软件在测试运行过程中避免可能发生故障的能力,且一旦发生故障后,具有解脱和排除故障的能力

2.健壮性

指软件对于规范要求以外的出入能够判断出这个输入不符合规范要求,并能有合理的处理方式。

3.可修改行

要求以科学的方法设计软件,使之有良好的结构和完备的文档,系统性能易于调整。

4.容易理解

是可靠性和可修改性的前提。不仅仅是文档的清晰可读,更要求软件本身具有简单明了的结构。

5.程序简便

6.可测试性

设计一个适当的数据集合,用来测试所建立的系统,并保证系统得到全面的检验。

7.效率性

一般用程序的执行时间和所占用的内存容量来度量。在达到原理要求功能指标的前提下,程序运行所需时间越短和占用内存越小,则效率越高

8.标准化原则

在结构上实现开放,基于业界开放式标准,符合国家和信息产业部的规范

9.先进性

满足客户需求,系统性能可靠,易于维护

10.可扩展性

软件设计完要留有升级接口和升级空间,对扩展开放,对修改关闭

11.安全性

要求系统能够保持用户信息,操作等多方面的安全要求,同时系统本身也要能够及时修复,处理各种安全漏洞,以提升安全性能

关系,文档性数据库

关系型数据的的ACID特性,原子性,一致性,隔离性,持久性

原子性:是指一个事务中的所有操作,要么全部完成,要么全部不完成,不会存在中间的状态。

一致性:是指事务从开始到结束执行之间的中间状态不会被其他事务所看见。

隔离性:是指数据库支持多个事务同时对数据进行读写或者修改的能力,并且整个过程中对各个事务来说都是相互隔离的。

持久性:是指每次事务提交以后都不会丢失。

缓存,消息队列,定时任务等相关技术

缓存

服务器本地缓存本地缓存是一级缓存,位于服务本机的内存中,在操作本地缓存的时候不需要网络IO不需要文件IO,直接从本机内存中读取数据,因此读写速度最快

存在的问题

1.本地缓存数据直接保存在JVM中,需要考虑缓存数据的大小,JVM的垃圾回收性能消耗

2.当服务是集群部署的时候,应该考虑是否需要做集群中本地缓存的数据同步

分布式缓存

当本地缓存被穿透的时候就会去查询分布式缓存,当在分布式化缓存中查询到数据的时候,直接将查询结果放到本地缓存中,Redis

缓存常用术语

1.缓存命中:当客户请求中的数据在缓存中,这个缓存中的数据就会被使用,这一行为被称之为缓存命中。

2.没有命中:缓存中没有查询到数据,并且数据库中可以查询到此数据,并将数据放到缓存中。

3.缓存穿透:查询一个在缓存中一定不存在的数据,即缓存中不存在,并且数据库中也不存在,数据库中没有数据,就不会将其写入缓存,就导致每次对于此数据的查询都需要去查询数据库,导致缓存失去意义。

4.存储成本:缓存没有命中时,从其他数据源取数据并放入缓存中的时间成本和空间成本就是存储成本

5.缓存失效:当缓存中的数据已经更新时,此数据已经失效

6.替代策略:当缓存没有命中时,并且缓存容量已满,就需要在缓存中去除一条旧数据,然后加入新数据,而应该去除哪些数据,就由替代策略来决定

7.缓存击穿:指缓存中没有但是数据库中有的数据(一般时缓存时间到期),这时由于并发用户特别多,同时读缓存没有读取到,又同时取数据库中读取,导致数据库压力瞬间增大,造成过大压力

8.缓存雪崩:当缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同,缓存击穿是指并发查询同一数据,缓存雪崩是指不同数据都过期了。

缓存穿透:查询一个null数据。解决方案:“布隆过滤器”对一定不存在的key进行过滤或者“缓存空数据

缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:默认是无加锁的;使用sync = true来解决击穿问题

缓存雪崩:大量的key同时过期。解决:可以通过“加锁排队”或者“加随机时间。加上过期时间

消息队列

消息队列:可以看做是一个存储消息的容器,它是分布式系统中的重要组件之一。

目的是:

1、为了通过异步处理来提高系统的性能来减少系统响应的时间

一般的步骤是客户端发起请求给服务端,服务端在请求给数据库,数据库在返回结果给服务端,最后客户端在接收服务端的响应。此时运用消息队列先是客户端发送请求给服务端,服务端发送消息给消息队列,客户端接收服务端的响应,在消息队列请求数据库。因此在使用消息队列时需要考虑业务的流程进行异步处理。

2、削峰/限流

将短时间高并发产生的事务消息存储在消息队列中,然后后端服务再根据自己能力慢慢消费这些消息,避免后端服务被击垮。

3、降低系统耦合性。

模块之间如果不存在直接调用,那么新增模块修改模块时对其他模块影响就比较小,对系统的扩展性更好。消息队列运用在:生产者也就是客户端发送消息到消息队列中,接受者也就是服务端处理消息就直接从消息队列中获取,不与其他系统耦合,这样也提高了系统的扩展性。另外为了避免消息队列服务器发生宕机而造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息,在消息队列服务器宕机时生产者服务器会选择分布式消息队列服务器集群中的其他服务器发送消息。

一般来说,有下面使用场景:

应用解耦

异步处理

流量削峰

消息通讯

日志处理

其中,应用解耦、异步处理是比较核心的。

使用消息队列带来的一些问题:

系统可用性降低:系统在某种程度上可用性降低,会担心消息丢失问题

系统复杂性提高:加入消息队列你需要担心消息没有被重复消费、需要处理消息被丢失的情况、保证消息被传递的顺序性。

一致性问题:异步的确可以提高响应速度,但是消费者未正确消费可能会导致数据不一致问题

JMS 与AMQP:

JMS是java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。JMS API时一个消息服务的标准、规范。ActiveMQ就是基于JMS规范实现的。

JMS两种消息模型:

1、点到点(P2P)模型

队列模型,生产者发送消息给消息队列,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。

2、发布订阅模型

使用主题Topic作为消息通信载体,类似于广播模式;发布者发布一条消息,该消息通过主题传递给所有订阅者,在一条消息广播之后才订阅的用户时收不到该条信息的。

AMQP:

一个提供统一消息服务的应用层标准高级消息队列协议(二进制应用层协议),是应用层协议的开放标准,为面向消息的中间件设计,兼容JMS。基于此协议的客户端可与消息中间件可传递消息,并不受客户端/中间件同产品,不同开发语言等条件限制。RabbitMQ就是基于AMQP协议实现的。

| 对比 |JMS |AMQP

| 定义 |Java API|协议

| 跨语言 | 否 |是

|跨平台 |否 |是

总结:AMQP为消息定义了线路层的协议,而JMS所定义的是API规范,在Java体系中,多个客户端可以通过JMS进行交互,不需要应用修改代码,但是其跨平台的支持较差。kafka唯一一点劣势是有可能消息重复消费。

Kafka:

分布式流式处理平台,关于流平台具有三个关键功能:

1、消息队列:发布和订阅消息流,这个功能类似于消息队列,这也是Kafka归类为消息队列的原因。

2、容错的持久方式存储记录消息流:Kafka会将消息持久化到磁盘,有效避免消息丢失的风险。

3、流失处理平台:在消息发布的时候进行处理,Kafka提供了一个完整的流失处理类库。

应用场景:

1、消息队列:建立实时流数据通道,以可靠的在系统或应用程序之间获取数据

2、数据处理:构建实时的流数据处理程序来转换或处理数据流。

与其消息队列相比优势:

1、性能,设计中大量使用了批量处理和异步的思想,最高可以每秒处理千万级别的消息。

2、兼容性,与周边生态系统兼容度最好

队列模型、发布订阅模型

RocketMQ的消息模型跟Kafka基本是完全一样的,唯一区别是Kafka中没有队列这个概念,与之对应的是分区Partition.

Producer(生产者)产生消息的一方,Consumer(消费者)消费消息的一方,Broker(代理)可以看作是一个独立的Kafka实例。多个KafkaBroker组成一个Kafka Cluster.

Kafka的多副本机制:

Kafka为分区(Partition)引入了多副本(Replica)机制。分区中的多个副本之间会有一个叫leader的,其他副本称为follower.发送的消息会被发送到leader副本,然后follower副本才能从leader副本中拉去消息进行同步。生产者和消费者只与leader副本交互,其他副本只是leader副本的拷贝,它们的存在只是为了保证消息存储的安全性。当Leader副本发生故障时从follower中选举出一个leader。

多分区以及多副本机制好处:

Kafka通过给特定Topic指定多个Patition,而各个Partition可以分布在不同的Broker上,方便提供较好的并发能力(负载均衡)。

Partition可以指定对于的Replica数,极大提供消息存储的安全性,提高了容灾能力,不过也增加了所需要的存储空间。

如果保证Kafka中消息消费的顺序,方法:

1、1个Topic只对应一个Partition.

2、发送消息的时候指定key/Partition.

Kafka如何保证消息不丢失:

生产者调用send方法发送消息之后,消息可能因为网络问题并没有发送过去。可以用get()方法调用结果,但是这样也让它变成了同步操作。还可以为Producer的retries(重试次数)设置一个合理的值。

消费者丢失消息的情况:手动关闭偏移量offset使每次真正消费完消息之后在手动提交offset.但是该方法会带来消息重复消费的问题。

Kafka弄丢消息:设置acks = all本来默认值为1,当配置为all时代表所有副本都要接收到该消息之后,该消息才算真正成功被发送。设置replication.factor>=3,保证每个分区至少有3个副本,设置min.insync.replicas>1,消息至少要写入到2个副本才算成功发送,一般设置replication.factor = min.insync.replicas+1;

Kafka如何保证消息不重复消费:

服务端侧已经消费的数据没有成功提交offset

kafka侧由于服务端处理业务时间长或者网络链接等等原因让kafka认为服务假死,出发了分区rebalance。

解决方案:消费消息服务做幂等校验。将enable.auto.commit参数设置为false,关闭自动提交,手动提交offset。

RocketMQ:

一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式的特点。采用Java语言开发的分布式消息系统

对于主题模型的实现来说每个消息中间件的底层设计都是不一样的,比如Kafka中的分区,RocketMQ中的队列,RabbitMQ中的Exchange。主题模型/发布订阅模型就是一个标准,这些中间件都是照着这个标准去实现而已。

Producer Group 生产者组,Consumer Group 消费者组,Topic 主题,RocketMQ通过使用一个Topic中配置多个队列并且每个队列维护每个消费者的消息位置实现了主题模式/发布订阅模式。

在RocketMQ中使用的是事务消息加上事务反查机制来解决事务问题的。

RabbitMQ:

采用Erlang语言实现AMQP的消息中间件,具体特点可靠性、灵活的路由、扩展性、高可用性、支持多种协议、多语言客户端、易用的管理界面、插件机制

消息 一般由2部分组成:消息头或者说Label和消息体payLoad,消息体是不透明的,而消息头由一系列的可选属性组成,属性包括routing-key路由键、priority相对于其他消息的优先权、delivery-mode该消息可能需要持久性存储。生产者把消息发给RabbitMQ后,RabbitMQ会根据消息头把消息发送给感兴趣的消费者。消息不是直接投递到Queue消息队列中的,必须还经过交换器Exchange,它一般会制定一个路由键,用来指定这个消息的路由规则,经过它过后再分配到队列中。如果路由不到,或许会返回给生产者,或许被丢弃掉。交换器由4种类型,不同类型对应着不同的路由策略:direct默认、fanout、topic、headers,RabbitMQ中通过Binding绑定将交换器和消息队列关联起来。

https://blog.csdn.net/m0_67561379/article/details/126974804

https://wenku.baidu.com/view/b16371d04593daef5ef7ba0d4a7302768e996fbd.html?_wkts_=1675583734796&bdQuery=java+%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97%E9%9D%A2%E8%AF%95%E9%A2%98

IO,多线程,集合

IO

特点:

1.先进先出:最先写入输出流的数据最先被输入流读取到

2.顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按照写入顺序读取一串字节,不能随机访问中间的数据(RandomAccessFile除外)

3.只读或只写:每个流只能是输入流或者输入流的一种,不能同时具备两个功能。

https://www.cnblogs.com/powerwu/articles/16652848.html

按照数据单位将流分为两类:字节流和字符流

区别:

1.字节流没有缓冲区,直接输出,而字符流时输出到缓冲区的,因此在输出时,不用调用close()方法时,信息已经输出了,而字符流只有在调用close()方法关闭缓冲区时,信息才输出。想要字符流在关闭时输出信息,则需要手动调用fluse()方法。

2.读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。

3.处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。

结论:对于处理纯文本数据,优先考虑字符流。除此之外都用字节流。

I/O(输入输出的定义):输入(InputStream)和输出(OutputStream),程序的输入和输出

InputStream和Reader与数据源相关联(即输入流)

OutputStream和Writer与目标媒介相关联(即输出流)

此外,还可以根据功能不同分为:节点流和处理流

节点流:以从或向一个特定的地方(节点)读写数据。如FileInputStream。

处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader。处理流的构造方法总是要带一个其他的流的对象做参数。一个流对象经过其他流的多次包装。

总结,流的划分可以有很多种:

1.按照流的操作单元划分,字节流和字符流

2.按照流的方向划分,输入流和输出流

3.按照角色功能不同划分,节点流和处理流

InputStream:输入流,字节流

OutputStream:输出流,字节流

Reader:输入流,字符流

Writer:输出流,字符流

FileInputStream:输入流,节点流

BufferedInputStream,DataInputStream:输入流,处理流

转换流的具体对象体现:

InputStreamReader:字节到字符的桥梁。

OutputStreamWriter:字符到字节的桥梁。

InputStream is = new FileInputStream(new File(src));

OutputStream os = null;

byte[] y = new byte[10];

int len;

while(len = is.read(y) != -1) {

os.write(y, 0 , len);

}

os.flush();

同步IO:

阻塞IO

非阻塞IO

IO多路复用

异步IO:同步阻塞和同步非阻塞

BIO:同步阻塞IO,Blocking IO。 每个客户端的Socket连接请求,服务端都会对应有个处理线程与之对应,对于没有分配到处理线程的连接就会被阻塞或者拒绝。相当于是一个连接一个线程。

特点:1.使用一个独立的线程维护一个socket连接,随着连接数量的增多,对虚拟机造成一定压力

2.使用流来读取数据,流是阻塞的,当没有可读/可写数据时,线程等待,会在成资源的浪费

NIO:同步非阻塞IO,No blocking IO。

服务器端保存一个socket连接列表,然后对这个列表进行轮询,如果发现某个socket端口上有数据可读/可写时,则调用该socket连接对应读/写操作。如果某个端口socket连接已经中断,则调用相应的析构方法关闭该端口,充分利用服务器资源,效率得到提升。

Selector:允许单线程处理多个channel。

channel:基本上所有的IO在NIO中都从一个channel开始,类似流,数据从channel读到buffer,也可从buffer中写到channel。

buffer:可以读写的内存块。

AIO:异步非阻塞IO

用户进行读/写后,将立刻返回,由内核去完成数据读取以及拷贝各种,完成后通知用户,执行回调函数(callback),此时数据已从内核拷贝至用户空间,用户线程只需要对数据进行处理即可,不需要关注读写,用户不需要等待内核对数据的复制操作,用户在得到通知时数据已经被复制到用户空间。

多线程

进程:就是正在运行中的程序

线程:就是进程中的单个顺序控制流,也可以理解为一条执行路径

单线程:一个进程中包含一个顺序控制流

多线程:一个进程中包含多个顺序控制流

java中,线程A和线程B,堆内存和方法区内存共享,但是栈内存独立,一个线程一个栈

线程的生命周期:新建->就绪->运行->(阻塞/锁池/死亡)

新建,就绪,运行,阻塞,死亡

多线程的实现方式:

1.继承Thread类,调用start方法启动线程

单纯的调用run()方法不会启动线程,不会分配新的分支栈。

Thread thread= new Thread();

thread.start();

2.实现Runnable接口,重写run方法,启动线程

new Thread(new Runnable() {

@Override

public void run() {

}

},"线程2").start();

3.实现callable接口,重写call方法,创建FutureTask

FutureTask task = new FutureTask(new Callable() {

@Override

public Object call() throws Exception {

//执行任务

}

});

Thread thread = new Thread(task);

thread.start();

// 会造成线程阻塞

Boject obj = task.get();

线程控制:

yield():使当前线程让步,重新回到争夺CPU执行权的队列中

sleep():使当前正在执行的线程停留指定的毫秒数

join():等死(等待当前线程销毁后,再继续执行其他的线程)

interrupt():终止线程睡眠

线程安全

1.是否具备多线程的环境

2.是否有共享数据

3.是否有多条语句操作共享数据

4.线程排队执行,线程同步机制

实例变量:在堆中。

静态变量:在方法区。

局部变量:在栈中。

局部变量永远都不会存在线程安全问题,因为局部变量不共享(一个线程一个栈)

实例变量在堆中,堆只有一个,静态变量在方法区中,方法区只有一个,堆和方法去都是多线程共享的,所以可能存在线程安全问题。

局部变量+常量:不会存在线程安全问题。

成员变量:可能会存在线程安全问题

线程同步的利弊:

好处:解决了线程同步的数据安全问题

弊端:当线程很多的时候,每个线程都会去判断同步上面的这个锁,很耗费资源,降低效率

同步线程的方式:

同步语句块:synchronized(this){方法体} synchronized括号后的数据必须使多线程共享的数据,才能达到多线程排队

如果解决线程安全问题:

1.尽量使用局部变量代替“实例变量”和“静态变量”

2.如果必须是实例变量,考虑创建多个对象,这样实例变量的内存就不共享了

3.使用synchronized

synchronized和Lock的区别:

1.Lock时显示锁(需要手动开启和关闭),synchronized是隐式锁,除了作用域自动释放。

2.Lock只有代码块锁,synchronized有代码块锁和方法锁。

3.使用Lock锁,JVM将花费较少的时间来调度线程,性能能好,并且有更好的扩展性。

ReentrantLock:可重入锁

lock():获得锁

unlock():释放锁

ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅降低程序运行效率,不推荐使用

class Bank {

private int account = 100;

//需要声明这个锁

private Lock lock = new ReentrantLock();

public int getAccount() {

return account;

}

//这里不再需要synchronized

public void save(int money) {

lock.lock();

try{

account += money;

}finally{

lock.unlock();

}

}

使用Condition控制线程通信

一个Lock对象上可以绑定多个Condition对象,这样实现了本线程只唤醒对方线程。

class Resource{

private String name;

private int count=1;

private boolean flag=false;

private Lock lock = new ReentrantLock();/*Lock是一个接口,ReentrantLock是该接口的一个直接子类。*/

private Condition condition_pro=lock.newCondition(); /*创建代表生产者方面的Condition对象*/

private Condition condition_con=lock.newCondition(); /*使用同一个锁,创建代表消费者方面的Condition对象*/

public void set(String name){

lock.lock();//锁住此语句与lock.unlock()之间的代码

try{

while(flag)

condition_pro.await(); //生产者线程在conndition_pro对象上等待

this.name=name+"---"+count++;

System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);

flag=true;

condition_con.signalAll();

}

finally{

lock.unlock(); //unlock()要放在finally块中。

}

}

public void out(){

lock.lock(); //锁住此语句与lock.unlock()之间的代码

try{

while(!flag)

condition_con.await(); //消费者线程在conndition_con对象上等待

System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);

flag=false;

condition_pro.signqlAll(); /*唤醒所有在condition_pro对象下等待的线程,也就是唤醒所有生产者线程*/

}

finally{

lock.unlock();

}

}

}

使用阻塞队列(BlockingQueue)控制线程通信

BlockingQueue是一个接口,也是Queue的子接口

特征:

当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;但当消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。程序的两个线程通过交替向BolckingQueue中放入、取出元素,即可很好的控制线程通信。

支持以下两个阻塞的方法:

put(E e):尝试把e元素放入BlockingQueue中,如果该队列已满,则阻塞该线程。

take():尝试从 BlockingQueue的头部取出元素,如果该队列已空,则阻塞该线程。

BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法,这些方法归纳起来可以分为如下三组:

(1)在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法, 当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。

(2)在队列头部删除并返回删除的元素。包括remove()、poll()、和take()方法,当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。

(3)在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。

BlockingQueue接口包含如下5个实现类:

ArrayBlockingQueue :基于数组实现的BlockingQueue队列。

LinkedBlockingQueue:基于链表实现的BlockingQueue队列。

PriorityBlockingQueue:它并不是保准的阻塞队列,该队列调用remove()、poll()、take()等方法提取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。 它判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行定制排序。

SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。

DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现,不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法),

DelayQueue根据集合元素的getDalay()方法的返回值进行排序。

死锁:当两个线程或者多个线程相互锁定的情况就叫死锁

避免死锁的原则:顺序上锁,反向解锁,不要回头

线程分为两大类:用户线程和守护线程(后台线程),守护线程具有代表性的就是:垃圾回收线程

守护线程的特点:一般守护线程是一个死循环,所有的用户线程只要结束,守护线程自动结束。

注意:主线程main方法是一个用户线程

Thread类的常用方法:

1.start():启动当前线程,调用当前线程的run()方法。

2.run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作在此方法中。

3.currentThread():静态方法,返回当前代码执行的线程。

4.getName():获取当前线程的名字。

5.setName():设置当前线程的名字。

6.yield():释放当前CPU的执行权。

7.join():在线程a中调用线程b的join(),此时线程a进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。

8.stop():已过时,当执行此方法时,强制结束当前线程。

9.sleep(long militime):当线程睡眠指定的毫秒数,在指定时间内,线程时阻塞状态。

10.isAlive():判断当前线程是否存活。

生产者和消费者

wait和notify方法

1.wait和notify方法不是线程对象的方法,是java中任何一个java对象都有的方法,Object对象自带的。

2.wait()方法

Object obj = new Object();

obj.wait();

表示:让正在obj对象上活动的线程进入等待状态,无期限等待,直到被唤醒为止。

3.notify()方法

Object obj = new Object();

obj.notify();

表示:唤醒正在obj对象上等待的线程。

notifyAll()方法:唤醒obj对象上处于等待的所有线程。

注意:wait和notify方法都需要建立在synchronized线程同步的基础上。

重点:obj.wait()方法会让正在obj对象上活动的当前线程进入等待状态,并且释放之前占有的obj对象的锁。

obj.notify()方法只会通知,不会释放之前占有的obj对象的锁。

sleep和notify的异同:

1.相同点:两个方法一旦执行,都可以放线程进入阻塞状态。

2.不同点:

两个方法的声明位置不同:Thread类中声明sleep(),Object类中声明wait();

调用要求不同:sleep()可以在任何需要的场景下调用,wait()必须在同步代码块中调用;

关于是否释放同步监视器:如果两个方法都使用在同步代码块中的同步方法中,sleep不会释放锁,wait会释放锁

生产者和消费者模式

线程池

首先创建一些线程,他们的集合称之为线程池。线程池在系统启动时会创建大量空闲线程,程序将一个任务传递给线程池,线程池就会启动一条线程来执行这个任务,执行结束后线程不会销毁,而是再次返回到线程池中成为空闲状态,等待执行下一个任务。

工作机制:

在线程池的编程模式下,任务是分配给整个线程池的,而不是直接提交给某个线程,线程池拿到任务后,就会在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲线程。

使用线程池的原因:

多线程运行时,系统不断创建和销毁新的线程,成本非常高,会过度的消耗系统资源,从而可能导致系统资源崩溃,使用线程池就是最好的选择。

好处:

1.降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

3.提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

使用Executors工厂类产生线程池

Executor线程池框架的最大优点是把任务的提交和执行解耦。

使用Executors执行多线程任务的步骤如下:

• 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池;

• 创建Runnable实现类或Callable实现类的实例,作为线程执行任务;

• 调用ExecutorService对象的submit()方法来提交Runnable实例或Callable实例;

• 当不想提交任务时,调用ExecutorService对象的shutdown()方法来关闭线程池。

1.newCachedThreadPoll():创建一个可缓存的线程池

ExecutorService threadPoll = Executors.newCachedThreadPoll();

threadPoll.execute(new Runnable() {

@Override

public void run() {

// 执行任务

}

});

threadPoll.shutdown();//关闭线程池

2.newFixedThreadPool:创建一个固定线程数量的线程池,线程池中线程数量始终不变,不会新建也不会摧毁

例子:如果有一个新任务提交时,线程池中如果有空闲的线程则立即使用空闲线程来处理任务,如果没有,则会把这个新任务存在一个任务队列中,一旦线程空闲了,则按照FIFO方式处理任务队列中的任务。先进先出

3.newSingleThreadExecutor():创建一个单一线程,每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,再按照FIFO方式顺序执行任务队列中的任务。

4.newScheduledThreadPool():创建一个可以控制线程池内线程定时或者周期性执行某任务的线程池

5.newSingleThreadScheduledExecutor():创建一个可以控制线程池内线程定时或者周期性执行某任务的线程池。区别时该线程池大小为1,上面的可以指定线程池大小。

ThreadLocal

ThreadLocal它并不是一个线程,而是一个可以在每个线程中存储数据的数据存储类,通过它可以在指定的线程中存储数据,数据存储之后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到该线程的数据。 即多个线程通过同一个ThreadLocal获取到的东西是不一样的,就算有的时候出现的结果是一样的(偶然性,两个线程里分别存了两份相同的东西),但他们获取的本质是不同的。使用这个工具类可以简化多线程编程时的并发访问,很简洁的隔离多线程程序的竞争资源。

线程池核心参数:

1.corePoolSize:核心线程大小。线程池一直运行,核心线程就不会停止。

2.maximumPoolSize :线程池最大线程数量。非核心线程数量=maximumPoolSize-corePoolSize

3.keepAliveTime:非核心线程的心跳时间。如果非核心线程在keepAliveTime内没有运行任务,非核心线程会消亡。

4.workQueue :阻塞队列。ArrayBlockingQueue,LinkedBlockingQueue等,用来存放线程任务。

5.defaultHandler:饱和策略。ThreadPoolExecutor类中一共有4种饱和策略。通过实现RejectedExecutionHandler接口。

AbortPolicy : 线程任务丢弃报错。默认饱和策略。

DiscardPolicy : 线程任务直接丢弃不报错。

DiscardOldestPolicy : 将workQueue队首任务丢弃,将最新线程任务重新加入队列执行。

CallerRunsPolicy :线程池之外的线程直接调用run方法执行。

6.ThreadFactory :线程工厂。新建线程工厂。

线程池执行任务流程:

1.线程池执行execute/submit方法向线程池添加任务,当任务小于核心线程数corePoolSize,线程池中可以创建新的线程。

2.当任务大于核心线程数corePoolSize,就向阻塞队列添加任务。

3.如果阻塞队列已满,需要通过比较参数maximumPoolSize,在线程池创建新的线程,当线程数量大于maximumPoolSize,说明当前设置线程池中线程已经处理不了了,就会执行饱和策略。

https://blog.csdn.net/weixin_42082952/article/details/122441769

Redis

客户端:Jedis、Lettuce、Redisson

存在再内存中的,是内存数据库,所以读写速度非常块,被广泛应用为缓存方向

除了缓存之外,Redis也经常用来做分布式锁,甚至消息队列

提供了很多种数据类型来支持不同的业务场景,Redis还支持事务,持久化,Lua脚本,多种集群方案。

优点:

1.读写性能极高

2.支持数据持久化

3.支持事务

4.数据结构丰富

5.支持主从复制

6.丰富的特性

缺点:

1.数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景只要局限再较小数据量的高性能操作和运算上。

2.主机宕机,宕机前有部分数据未能及时同步到从机,切换ip后还是会引起数据不一致问题,降低了系统的可用性。

常见数据结构类型:

1.字符串类型(String)

String数据结构是最简单的key-value类型

常用命令:set、get、strlen、exists、dect、incr、setex

应用场景:计数器:记录播放量,记录访问数(Redis记总数,MySQL记录日志)、登录验证码

2.列表类型(list)

是简单的字符串列表,按照插入顺序排序,元素可以重复

常用命令:rpush、lpop、lpush、rpop、lrange、llen

应用场景:消息队列,慢查询

3.集合类型(set)

是String类型的无序无重复集合

常用命令:sadd、spop、smembers、scard

应用场景:需要存放的数据不能重复,以及需要获取多个数据源交集和并集等场景

4.哈希类型(hash)

hash是一个String类型的field和value的映射表,hash特别适合与存储对象

常用命令:hset、hget、hgetall

应用场景:系统中对象的存储

5.有序集合类型zset(sorted set)

有序集合zset和set集合一样,也是String类型元素的集合,且不允许重复的成员。不同的是zset每个元素都会关联一个分数(分数可以重复),redis通过分数来为集合中的成员进行从小到大的排序

常用命令:zadd、zscore、zcard、zrange

应用场景:需要对数据根据某个权重进行排序的场景

redis持久化

redis的一种持久化方式叫快照(RDB),另一种方式只追加文件(AOF)

快照持久化(RDB):定期的全盘缓存

在指定的时间间隔内将内存中的数据集快照写入磁盘,它恢复时将快照文件直接督导内存中

优势:适合大规模的数据恢复:对数据完整性和一致性要求不高

劣势:在一定间隔时间做一次备份,如果redis意外down掉的话,就会失去最后一次快照后的所有修改

AOF持久化:日志文件;触发机制,每次记录

以日志的形式来记录每个写操作,将redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以写文件,redis启动之处会读取该文件重新构建数据。

优势:同步持久化;每秒同步

劣势:相同数据集的数据而言aof文件要远大于rdb文件,恢复速度鳗鱼rdb

aof运行效率要慢于rdb,每秒同步策略效率较好,不同步效率和rdb相同

两者的比较:

AOF文件比RDB更新效率高

优先使用AOF

AOF更安全

RDB比AOF性能好

Redis如何做内存优化?

1.控制key的数量:当时用Redis存储大量数据的时候,通常会存在大量键,过多的键同样会消耗大量内存。

2.缩减键值对象:降低Redis内存使用最直接的方式就是缩减key和value的长度

3.编码优化:Redis对外提供了string,list,hash,zet,set等类型,但是Redis内部针对不同类型存在编码的概念,所谓编码就是具体使用哪种底层数据结果来实现。编码不同将直接影响数据的内存占用和读写效率

如果现在有个读超高并发的系统,用Redis来抗住大部分读请求,你会怎么设计?

如果是读高并发的话,先看并发的数量级是多少,因为Redis单机的读QPS在万级,每秒几万没问题,使用一主多从+哨兵集群的缓存架构来承载每秒10W的读并发,主从复制,读写分离,

使用哨兵集群主要是提高缓存架构的可用性,解决单点故障问题。主库负责写,多个从库负责读,支持水平扩容,根据读请求的QPS来决定加多少个Redis从实例。如果读并发继续增加的话,只需要增加Redis实例就行了。

如果需要缓存1T+的数据,选择Redis cluster模式,每个主节点存一部分数据,假设一个master存32G,那只需要n*32G>=1T,n个这样的master节点就可以支持1T+的海量数据的存储。

Redis的序列化方式:

Jackson2JsonRedisSerializer、StringRedisSerializer、JdkSerializationRedisSerializer

Jackson2JsonRedisSerializer序列化方式用时最短,而且数据存储格式最为直观,更重要的是:如果以后想要使用这些存储的数据,就要进行反序列化操作,这个时候将json字符串转换为对象还是非常简便的

StringRedisSerializer序列化方式用时次之,数据存储格式也比较直观,麻烦的是:以后要对数据进行反序列化操作时,获取对象的属性值非常麻烦

JdkSerializationRedisSerializer序列化方式用时最长,数据的存储格式不直观,是默认的序列化方式

推荐使用Jackson2JsonRedisSerializer序列化方式

分布式问题

10.什么是分布式锁?为什么用分布式锁?

锁在程序中的作用就是同步工具,保证共享资源在同一时刻只能被一个线程访问,Java中的锁我们很熟悉,像synchronized、lock都是常见的,但是Java的锁只能保证单机的时候有效,分布式集群环境就无能无力了,这个时候我们就需要用到分布式锁。

分布式锁,就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源

需要满足的特点:

互斥性:在任何时刻,对于同一数据,只有一台应用可以获取到分布式锁

高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署

防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁

独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁。

11.常见的分布式锁有哪些解决方案?

三种解决方案:关系型数据库、Redis、ZooKeeper

12.Redis实现分布式锁

分布式锁的三大核心要素:加锁、解锁、锁超时

JVM原理

https://blog.csdn.net/weixin_45105261/article/details/110311485

JVM运行内存模型

概念:JVM把文件.class字节码加载到内存,对数据进行校验,转换和解析,并初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制

1)Bootstrap ClassLoader /启动类加载器

$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类

2)Extension ClassLoader/扩展类加载器

负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包

3)App ClassLoader/ 系统类加载器

负责记载classpath中指定的jar包及目录中class

4)Custom ClassLoader/用户自定义类加载器(java.lang.ClassLoader的子类)

属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

类加载双亲委派机制介绍和分析

在这里,需要着重说明的是,JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

双亲委派机制的作用

1.保证安全性

防止重复加载同一个class。通过委托去向上面问,加载过了就不用再加载一遍。保证数据安全。

2.保证唯一性

保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:

↓加载(Loading)

↓验证(Verification)

↓准备(Preparation)

↓解析(Resolution)

↓初始化(Initialization)

↓使用(Using)

↓卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称链接。

运行时数据区

方法区——线程共享:存储了每个类的信息(包括类的名称、修饰符、方法信息、字段信息)、类中静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息以及编译器编译后的代码等

堆——线程共享:存储对象实例以及数组

Java栈(虚拟机栈)——非线程共享

程序计数器——非线程共享

本地方法栈——非线程共享

(1)JVM运行时会分配好方法区和堆,而JVM每遇到一个线程,就为其分配一个程序计数器、Java栈、本地方法栈,当线程终止时,三者(程序计数器、Java栈、本地方法栈)所占用的内存空间也会释放掉。

(2)程序计数器、Java栈、本地方法栈的生命周期与所属线程相同,而方法区和堆的生命周期与JAVA程序运行生命周期相同,所以gc只发生在线程共享的区域(大部分发生在Heap上)

首先会在方法区加载Class模板,每个模板都有该类的属性以及方法和常量池,以及静态变量随着类加载而加载到静态方法区,并在java堆中生成一个代表这个类的Class对象的内存区域,为类变量分配内存并设置类变量初始值,例如String类型为null, 而后在栈中存储该对象的引用地址,地址指向堆中类的内存地址,最后初始化,例如构造方法,赋值给堆中的对象内存数据中。每一个线程都会给他分配指定的 java栈,程序计数器,本地方法栈,所以说这三个是线程独享,是线程安全,而堆和方法区大家一起使用的,因而是线程不安全

https://blog.csdn.net/zhaohuodian/article/details/126212581

Sun的JVMGenerationalCollecting(垃圾回收)原理是这样的:把对象分为年青代(Young)、年老代(Tenured)、持久代(Perm),对不同生命周期的对象使用不同的算法。(基于对对象生命周期分析)

1.Young(年轻代)

年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Tenured。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。

2.Tenured(年老代)

年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。

3.Perm(持久代)

用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

举个例子:当在程序中生成对象时,正常对象会在年轻代中分配空间,如果是过大的对象也可能会直接在年老代生成(据观测在运行某程序时候每次会生成一个十兆的空间用收发消息,这部分内存就会直接在年老代分配)。年轻代在空间被分配完的时候就会发起内存回收,大部分内存会被回收,一部分幸存的内存会被拷贝至Survivor的from区,经过多次回收以后如果from区内存也分配完毕,就会也发生内存回收然后将剩余的对象拷贝至to区。等到to区也满的时候,就会再次发生内存回收然后把幸存的对象拷贝至年老区。

通常我们说的JVM内存回收总是在指堆内存回收,确实只有堆中的内容是动态申请分配的,所以以上对象的年轻代和年老代都是指的JVM的Heap空间,而持久代则是之前提到的MethodArea,不属于Heap。

关于JVM内存管理的一些建议

1、手动将生成的无用对象,中间对象置为null,加快内存回收。

2、对象池技术如果生成的对象是可重用的对象,只是其中的属性不同时,可以考虑采用对象池来较少对象的生成。如果有空闲的对象就从对象池中取出使用,没有再生成新的对象,大大提高了对象的复用率。

3、JVM调优通过配置JVM的参数来提高垃圾回收的速度,如果在没有出现内存泄露且上面两种办法都不能保证JVM内存回收时,可以考虑采用JVM调优的方式来解决,不过一定要经过实体机的长期测试,因为不同的参数可能引起不同的效果。如-Xnoclassgc参数等。

GC回收机制的几种方式:

1.引用计数法------过时

概念: 引用计数是垃圾收集器中的早期策略。堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1,引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

缺点: 这样很浪费资源,要对所有对象引用计数,并且计数器本身也浪费资源

2.标记-清除(Mark-Sweep)算法

标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。(需要两次扫描)

主要缺点:

1.一个是效率问题,标记和清除过程的效率都不高。

2.另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致:当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作。

3.*标记-整理(Mark-Compact)算法====标记-清除-整理

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。(需要三次扫描)

优点:减少了内存碎片

缺点:更加降低了效率

4.复制算法

幸存区分为了两块,一个是form,一个是to-------谁空谁to

理解: 当新的对象存在Eden区时,进行GC时会将存活下来的复制幸存区的to区,并把原本为from区的也复制to区,这样原本为from区变成to区,to变成了from区,然后Eden区和to区每次清除都必定为空的,减少了内存碎片,适用于对象存活时间较短的项目中,而后在幸存from区超过15次都没有清除的就复制到老年区

总结:

这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

5.分代收集(Generational Collection)算法------堆分区使用不同算法(重点)

概念:

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和年轻代,在堆区之外还有一个代就是永久代,它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

⚪使用:

老年代的特点是每次垃圾收集时只有少量对象需要被回收,-------- (生命周期长)标记-清除-整理

而年轻代的特点是每次垃圾回收时都有大量的对象需要被回收,--------(生命周期短) 复制算法

*那么就可以根据不同代的特点采取最适合的收集算法。

老年代(Old Generation)的回收算法:标记-整理和标记清除一起使用

老年代的特点是每次回收都只回收少量对象,一般使用的是标记-整理和标记清除一起使用。

a)在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

b)内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC或Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

原因:因为老年代的对象通常生命周期大,只回收少量对象,使用标记清除算法被标记的数量较少,产生的内存碎片不会很多,可以多次标记清除后再进行标记整理内存碎片。

拦截器和过滤器的区别:

https://www.cnblogs.com/panxuejun/p/7715917.html

过滤器:Filter

拦截器:Inteceptor

自定义starter

https://www.cnblogs.com/wlong-blog/p/16805604.html

自定义切面

https://www.cnblogs.com/goloving/p/16022911.html

分布式,权限,单点登录,缓存,消息等机制

SaaS,Cloud,Container,分布式监控

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值