@MRsunhuimin
2022-07-08T07:39:57.000000Z
字数 28772
阅读 504
面试问题总结
一对一关系是指对于实体集A与实体集B,A中的每一个实体至多与B中一个实体有关系;反之,在实体集B中的每个实体至多与实体集A中一个实体有关系。
一对多关系是指实体集A与实体集B中至少有N(N>0)个实体有关系;并且实体集B中每一个实体至多与实体集A中一个实体有关系。
多对多关系是指实体集A中的每一个实体与实体集B中至少有M(M>0)个实体有关系,并且实体集B中的每一个实体与实体集A中的至少N(N>0)个实体有关系。
ER图分为实体、属性、关系三个核心部分。实体是长方形体现,而属性则是椭圆形,关系为菱形。
ER图的实体(entity)即数据模型中的数据对象,例如人、学生、音乐都可以作为一个数据对象,用长方体来表示,每个实体都有自己的实体成员(entity member)或者说实体对象(entity instance),例如学生实体里包括张三、李四等,实体成员(entity member)/实体实例(entity instance) 不需要出现在ER图中。
ER图的属性(attribute)即数据对象所具有的属性,例如学生具有姓名、学号、年级等属性,用椭圆形表示,属性分为唯一属性( unique attribute)和非唯一属性,唯一属性指的是唯一可用来标识该实体实例或者成员的属性,用下划线表示,一般来讲实体都至少有一个唯一属性。
ER图的关系(relationship)用来表现数据对象与数据对象之间的联系,例如学生的实体和成绩表的实体之间有一定的联系,每个学生都有自己的成绩表,这就是一种关系,关系用菱形来表示。
https://blog.csdn.net/JokerLJG/article/details/119656022
整体流程
1.用户发出请求(可能是填写表单),表单的数据在展示层被匹配为VO。
2.展示层把VO转换为服务层对应方法所要求的DTO,传送给服务层。
3.服务层首先根据DTO的数据构造(或重建)一个DO,调用DO的业务方法完成具体业务。
4.服务层把DO转换为持久层对应的PO(可以使用ORM工具,也可以不用),调用持久层的持久化方法,把PO传递给它,完成持久化操作。
5.对于一个逆向操作,如读取数据,也是用类似的方式转换和传递,略。
VO(Value Object)值对象
视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。VO就是展示用的数据,不管展示方式是网页,还是客户端,还是APP,只要是这个东西是让人看到的,这就叫VO。
VO通常用于前端和服务端Controller交互。
DTO(Data Transfer Object)数据传输对象
数据传输对象,这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。
DTO通常用于Controller和Service交互。
BO(business object) 业务对象
业务层对象,是简单的真实世界的软件抽象,通常位于中间层。BO 的主要作用是把业务逻辑封装为一个对象,这个对象可以包括一个或多个其它的对象。也有认为BO就是PO的组合。
PO(Persistent Object)持久化对象
持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。
PO就是数据库中的记录,一个PO的数据结构对应着库中表的结构,表中的一条记录就是一个PO对象
通常PO里面除了get,set之外没有别的方法。对于PO来说,数量是相对固定的,一定不会超过数据库表的数量
DO(Domain Object)领域对象
上面这些概念基本上已经涵盖了全部的流程,DO只是跟其中一个概念相同,但是跟哪个概念相同呢?
现在主要有两个版本:
阿里巴巴的开发手册中的定义:DO( Data Object)这个等同于下面的PO
在DDD(Domain-Driven Design)领域驱动设计中,DO(Domain Object)这个等同于上面的BO
DAO(data access object) 数据访问对象
DAO(Data Access Object)数据访问对象,它是一个面向对象的数据库接口,负责持久层的操作,为业务层提供接口,主要用来封装对数据库的访问,常见操作无外乎 CURD。我们也可以认为一个 DAO 对应一个 POJO 的对象,它位于业务逻辑与数据库资源中间,可以结合 PO 对数据库进行相关的操作。
POJO(plain ordinary java object) 简单无规则 java 对象
纯的传统意义的 java 对象。就是说在一些 Object/Relation Mapping 工具中,能够做到维护数据库表记录的 persisent object 完全是一个符合 Java Bean 规范的纯 Java 对象,没有增加别的属性和方法。我的理解就是最基本的 Java Bean ,只有属性字段及 setter 和 getter 方法!。
POJO 可认为是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。
https://blog.csdn.net/qq_29235677/article/details/91994857
RPC:
远程过程调用,像调用本地服务(方法)一样调用服务器的服务
支持同步、异步调用
客户端和服务器之间建立TCP连接,可以一次建立一个,也可以多个调用复用一次链接(建立连接耗时)大公司多用RPC
PRC数据包小
protobuf
thrift
rpc:编解码,序列化,链接,丢包,协议(成本大)
Rest(Http):
http请求,支持多种协议和功能
开发方便成本低
http数据包大
java开发:HttpClient,URLConnection
spring cloud暂时用这种方式
抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。
从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规
范。
相同点
接口和抽象类都不能实例化
都位于继承的顶端,用于被其他实现或继承
都包含抽象方法,其子类都必须覆写这些抽象方法
不同点
参数 | 抽象类 | 接口 |
---|---|---|
声明 | 抽象类使用abstract关键字声明 | 接口使用interface关键字声明 |
实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
访问修饰符 | 抽象类中的方法可以是任意访问修饰符 | 接口方法默认修饰符是public。并且不允许定义为 private 或者protected |
多继承 | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 |
字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final |
的 |
主键
关系型数据库中的某一个属性组能唯一标识一条记录,该属性组就可以成为一个主键
主键只能有一个,不能有重复的,不允许为空,用来保证数据完整性
外键
外键用于与另一张表的关联。是能确定另一张表记录的字段,用于保持数据的一致性。
表的外键是另一表的主键, 外键可以有重复的, 可以是空值。
用来和其他表建立联系用的
一个表可以有多个外键
主键和唯一索引的区别
其实,两者肯定不同: 主键是一种约束,唯一索引是一种索引,两者在本质上就是不同的。
而为何有此一问,无非是因为主键创建后生成主键索引(一种特殊的唯一索引),那么主键索引和唯一索引有何区别?
最主要区别:1、唯一性索引列允许空值, 而主键索引列不允许为空值。
2、一个表中只有一个主键索引,而唯一索引可以有多个
分布式锁的三个 主要核心要素:
1.安全性、互斥性。在同一时间内,不允许多个 client 同时获得锁。
2.活性。无论 client 出现 crash 还是遭遇网络分区,你都需要确保任意故障场景下,都不会出现死锁,常用的解决方案是超时和自动过期机制。
3.高可用、高性能。加锁、释放锁的过程性能开销要尽量低,同时要保证高可用,避免单 点故障。
1.基于数据库实现
这个方案一般不会使用,虽然基于数据库实现的分布式锁,是最容易理解的。但是因为数据库需要落到硬盘上,频繁读 取数据库会导致 IO 开销大,因此这种分布式锁适用于并发量低,对性能要求低的场景。
要实现分布式锁,最简单的方式就是创建一张锁表,然后通过操作该表中的数据来实现。当我们要锁住某个资源时,就在该表中增加一条记录,想要释放锁的时候就删除这条记录。通过select。。for update访问同一条数据,for update会锁定数据,其他线程只能等待。
2.redis
利用 SETNX 和 SETEX
基本命令主要有:
SETNX(SET If Not Exists):当且仅当 Key 不存在时,则可以设置,否则不做任何动作。
SETEX:可以设置超时时间
其原理为:通过 SETNX 设置 Key-Value 来获得锁,随即进入死循环,每次循环判断,如果存在 Key 则继续循环,如果不存在 Key,则跳出循环,当前任务执行完成后,删除 Key 以释放锁。
这种方式可能会导致死锁,为了避免这种情况,需要设置超时时间。
3.zookeeper
除了数据库实现分布式锁的方式以外,我们还可以基于 Zookeeper 实现。Zookeeper 是一种提供“分布式服务协调“的中心化服务,正是 Zookeeper 的以下两个特性,分布式应用程序才可以基于它实现分布式锁功能。
顺序临时节点:Zookeeper 提供一个多层级的节点命名空间(节点称为 Znode),每个节点都用一个以斜杠(/)分隔的路径来表示,而且每个节点都有父节点(根节点除外),非常类似于文件系统。
节点类型可以分为持久节点(PERSISTENT )、临时节点(EPHEMERAL),每个节点还能被标记为有序性(SEQUENTIAL),一旦节点被标记为有序性,那么整个节点就具有顺序自增的特点。一般我们可以组合这几类节点来创建我们所需要的节点,例如,创建一个持久节点作为父节点,在父节点下面创建临时节点,并标记该临时节点为有序性。
Watch 机制:Zookeeper 还提供了另外一个重要的特性,Watcher(事件监听器)。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知给用户。
我们熟悉了 Zookeeper 的这两个特性之后,就可以看看 Zookeeper 是如何实现分布式锁的了。
首先,我们需要建立一个父节点,节点类型为持久节点(PERSISTENT) ,每当需要访问共享资源时,就会在父节点下建立相应的顺序子节点,节点类型为临时节点(EPHEMERAL),且标记为有序性(SEQUENTIAL),并且以临时节点名称 + 父节点名称 + 顺序号组成特定的名字。
在建立子节点后,对父节点下面的所有以临时节点名称 name 开头的子节点进行排序,判断刚刚建立的子节点顺序号是否是最小的节点,如果是最小节点,则获得锁。
如果不是最小节点,则阻塞等待锁,并且获得该节点的上一顺序节点,为其注册监听事件,等待节点对应的操作获得锁。
当调用完共享资源后,删除该节点,关闭 zk,进而可以触发监听事件,释放该锁。
集群方式
redis集群:两台为一组.一台主机一台备机,平时主机工作,备机不工作,备机每隔几秒就会给主机发送一个ping,主机在正常工作的时候会发送一个peng给备机,如果不返回pang,可能会连续ping三次,都不返回那么备机就会上,备机里有主机的全部内容,这个叫做心跳检测机制(高可用的作用),主从热备
Slave宕机
配置主从复制的时候才配置从的redis,从的会从主的redis中读取主的redis的操作日志,求达到主从复制。
1)在Redis中从库重新启动后会自动加入到主从架构中,自动完成同步数据;
2)如果从数据库实现了持久化,可以直接连接到主的上面,只要实现增量备份(宕机到重新连接过程中,主的数据库发生数据操作,复制到从数据库),重新连接到主从架构中会实现增量同步。
Master 宕机
假如主从都没数据持久化,此时千万不要立马重启服务,否则可能会造成数据丢失,正确的操作如下:在slave数据上执行SLAVEOF ON ONE,来断开主从关系并把slave升级为主库此时重新启动主数据库,执行SLAVEOF,把它设置为从库,连接到主的redis上面做主从复制,自动备份数据。
以上过程很容易配置错误,可以使用redis提供的哨兵机制来简化上面的操作。简单的方法:redis的哨兵(sentinel)的功能。
持久化
使用Redis的持久化机制,来保证Redis地数据不会因为故障面丢失。Redis的持久化机制有两种,第一种是RDB快照(默认),第二独是AOF日志。快照是一次全量备份,AOF日志是连续的增量备份。
RDB是一个快照文件,数据很紧凑,适合用于灾难恢复,而且恢复大数据集时的速度比AOF的恢复速度要快。保存整个数据集的快照,也不可能太频繁。因此服务器故障时候会丢失数据。
Redis可以通过创建快照来获得存储在内存⾥⾯的数据在某个时间点上的副本。Redis创建快照之后,可
以对快照进⾏备份,可以将快照复制到其他服务器从⽽创建具有相同数据的服务器副本(Redis主从结构,主要⽤来提⾼Redis性能),还可以将快照留在原地以便重启服务器的时候使⽤。
快照持久化是Redis默认采⽤的持久化⽅式,在redis.conf配置⽂件中默认有此下配置:
save 900 1 #在900秒(15分钟)之后,如果⾄少有1个key发生变化,
Redis就会⾃动触发BGSAVE命令创建快照。
save 300 10 #在300秒(5分钟)之后,如果⾄少有10个key发生变化,
Redis就会⾃动触发BGSAVE命令创建快照。
save 60 10000 #在60秒(1分钟)之后,如果⾄少有10000个key发生变化,
Redis就会⾃动触发BGSAVE命令创建快照。
AOF的默认策略为每秒钟 fsync一次,在这种配置下Redis仍然可以保持良好的性能。AOF文件的体积通常要大于RDB文件的体积恢复速度慢。
与快照持久化相⽐,AOF持久化 的实时性更好,因此已成为主流的持久化⽅案。默认情况下Redis没有
开启AOF(append only file)⽅式的持久化,可以通过appendonly参数开启:
appendonly yes
开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写⼊硬盘中的AOF文件。
AOF⽂件的保存位置和RDB⽂件的位置相同,都是通过dir参数设置的,默认的⽂件名是appendonly.aof。
在Redis的配置⽂件中存在三种不同的 AOF 持久化⽅式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF⽂件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
1)灵活可扩展性
RocketMQ 天然支持集群,其核心四组件(Name Server、Broker、Producer、Consumer)每一个都可以在没有单点故障的情况下进行水平扩展。
2)海量消息堆积能力
采用零拷贝原理实现超大的消息的堆积能力,据说单机已可以支持亿级消息堆积,而且在堆积了这么多消息后依然保持写入低延迟。
3)支持顺序消息
可以保证消息消费者按照消息发送的顺序对消息进行消费。顺序消息分为全局有序和局部有序,一般推荐使用局部有序,即生产者通过将某一类消息按顺序发送至同一个队列来实现。
4)多种消息过滤方式
消息过滤分为在服务器端过滤和在消费端过滤。服务器端过滤时可以按照消息消费者的要求做过滤,优点是减少不必要消息传输,缺点是增加了消息服务器的负担,实现相对复杂。消费端过滤则完全由具体应用自定义实现,这种方式更加灵活,缺点是很多无用的消息会传输给消息消费者。
5)支持事务消息
RocketMQ 除了支持普通消息,顺序消息之外还支持事务消息,这个特性对于分布式事务来说提供了又一种解决思路。
6)回溯消费
回溯消费是指消费者已经消费成功的消息,由于业务上需求需要重新消费,RocketMQ 支持按照时间回溯消费,时间维度精确到毫秒,可以向前回溯,也可以向后回溯。
1)启动 Namesrv 后开始监听端口,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心。
2)Broker 启动,跟所有的 Namesrv 保持长连接,定时发送心跳包。
3)收发消息前,先创建 Topic。创建 Topic 时,需要指定该 Topic 要存储在哪些 Broker上,也可以在发送消息时自动创建 Topic。
4)Producer 向该 Topic 发送消息。
5)Consumer 消费该 Topic 的消息。
总体分两个部分:
调度中心:负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统和任务解耦,提高了系统可用性和稳定性。通调度性能不在受限于任务模块。
执行器:负责接收调度中的请求并执行任务逻辑。任务模块专注于任务的执行操作,开发和运维更加简单和高校。
设计思想:
调度和任务两个部分相互解耦,全异步化和轻量化,可以提高系统的稳定性和扩展性。
xxl-job原理
2.1、执行器注册
执行器启动主要是把自己注册到调度中心然后保存在数据库(xxl_job_registry表),并定时发送心跳,保持续约。执行器正常关闭,也主动告知调度中心注销,这种是主动注册。
如果执行器网络故障,调度中心就不知道执行器的情况,如果把任务路由给一个不可用的执行器,就会导致任务失败。所以调度中心需要不断的对执行器探活(RocketMQ的NameServer 管理broker一样),调度中心会启动一个后台线程定时调用执行器接口,如果发现异常就下线。
2.2、调度中心和任务执行
1.JobRegistryMonitorHelper 不停的更新注册表,把超时的执行器剔除(每隔30s执行一次)
2.创建线程池
2.1调度器线程ScheduleThread:计算预读取的任务数(默认6000),然后while 循环不停的获取到期的任务
2.2时间轮线程池
3.获取任务锁:第一步获取数据库排它锁,如果没有成功说明其他的调度中心在加载任务
4.查询任务:获取锁后, 查询任务
5.调度任务
6.任务触发,选择执行器:按照配置的路由策略,不通路由策略获取方式也不一样
7.远程执行:拿到执行器之后,runExecutor 触发远程的执行器
8.执行器处理远程调用,回调
2.3、 时间轮
一批任务都是不同的时间执行,执行时间精确到秒,如何实现对所有的任务调度这个就是时间轮。
2.4、任务超时
如果任务在指定的时间范围内没有返回结果,就不在等结果,抛出异常。
2.5、失败重试
如果任务执行失败,会更新在xxl_job_log日志表里。调度中心有个后台线程monitorThread。第一步就是查日志表里结果不是200的任务,为了防止集群下同时处理一个失败任务,用了数据库的乐观锁(版本号),如果失败重试次数>0,代表重试,就要重新触发。
2.6、故障转移
如果一个执行器挂了,就找另一个执行器执行,直到找到一个正常的执行器。
2.7、任务数据分片
这里的是数据分片,需要用到分片参数 sharding param,调度器负责把这个分片参数分发给每个执行器(执行器个数和参数个数相等),怎么根据分片参数对数据分片是Job自己的事情。
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同⼀个对象(基本数据类型==比较的是值,引⽤数据类型==比较的是内存地址)。
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
说明:
String 中的 equals 方法是被重写过的,因为 object 的 equals ⽅法是比较的对象的内存地址,⽽ String 的 equals 方法比较的是对象的值。
当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引⽤。如果没有就在常量池中重新创建一个 String 对象。
1 public static void main(String[] args) {
2 //double类型比较大小
3 Double d1 = new Double("2.5");
4 Double d2 = new Double("7.5");
5 int res = d1.compareTo(d2);
6 if(res > 0) {
7 System.out.println("d1 > d2");
8 }
9 if(res < 0) {
10 System.out.println("d1 < d2");
11 }
12 if(res == 0) {
13 System.out.println("d1 = d2");
14 }
15 }
线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的⽅法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使⽤ ConcurrentHashMap吧!);
2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
3. 对Null key 和Null value的⽀持: HashMap 中,null 可以作为键,这样的键只有⼀个,可以有⼀个或多个键所对应的值为 null。。但是在 HashTable 中 put进的键值只要有一个 null,直接抛出 NullPointerException。
4. 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable会直接使⽤你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的 tableSizeFor() ⽅法保证,下面给出了源代码)。也就是说 HashMap 总是使⽤2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表⻓度⼤于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
过程:
判断当前表是否空(长度为0,或者null);
如果是则调用resize()方法进行初始化操作;
根据由Key计算出的Hash值对应的节点是否为空;
如果为空,则将Value赋值给该节点;
如果不为空,则判断Key是否相同
如果相同,则替换为最新的Value;
如果不同,则判断是否为树节点;
如果是,则调用putTreeVal()以树节点的方式添加到表中;
如果不是,则遍历链表,如果找到了相同的Key则替换Value;
如果找不到则,在链表最后插入,判断是否达到了TREEIFY_THRESHOLD的阈值,如果打到了则进行树化;
结束
创建线程有三种方式:
继承 Thread 重新 run 方法;
实现 Runnable 接口,没有返回值;
实现 Callable 接口,有返回值。
NEW 尚未启动
RUNNABLE 正在执行中
BLOCKED 阻塞的(被同步锁或者 IO 锁阻塞)
WAITING 永久等待状态
TIMED_WAITING 等待指定的时间重新被唤醒的状态
TERMINATED 执行完成
NEW:毫无疑问表示的是刚创建的线程,还没有开始启动。
RUNNABLE: 表示线程已经触发 start()方式调用,线程正式启动,线程处于运行中状态。
BLOCKED:表示线程阻塞,等待获取锁,如碰到 synchronized、lock 等关键字等占用临界区的情况,一旦获取到锁就进行 RUNNABLE 状态继续运行。
WAITING:表示线程处于无限制等待状态,等待一个特殊的事件来重新唤醒,如
通过wait()方法进行等待的线程等待一个 notify()或者 notifyAll()方法,通过 join()方
法进行等待的线程等待目标线程运行结束而唤醒,一旦通过相关事件唤醒线程,线
程就进入了 RUNNABLE 状态继续运行。
TIMED_WAITING:表示线程进入了一个有时限的等待,如 sleep(3000),等待 3 秒
后线程重新进行 RUNNABLE 状态继续运行。
TERMINATED:表示线程执行完毕后,进行终止状态。需要注意的是,一旦线程通过 start 方法启动后就再也不能回到初始 NEW 状态,线程终止后也不能再回到RUNNABLE 状态
线程池创建有七种方式,最核心的是最后一种:
newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目nThreads;
newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
newScheduledThreadPool(int corePoolSize):和newSingleThreadScheduledExecutor()类似,创建的是个 ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建 ForkJoinPool,利用 Work-Stealing 算法,并行地处理任务,不保证处理顺序;
ThreadPoolExecutor():是最原始的线程池创建,上面 1-3 创建方式都是对 ThreadPoolExecutor 的封装。
补充:线程池状态
RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
线程池中 submit() 和 execute() 方法有什么区别?
execute():只能执行 Runnable 类型的任务。
submit():可以执行 Runnable 和 Callable 类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。
两者最主要的区别在于:sleep 方法没有释放锁,⽽ wait 方法释放了锁 。
两者都可以暂停线程的执⾏。
Wait 通常被⽤于线程间交互/通信,sleep 通常被⽤于暂停执⾏。
wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或者notifyAll() ⽅法。sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(longtimeout)超时后线程会⾃动苏醒。
为什么我们调用 start() ⽅法时会执行 run() ⽅法,为什么我们不能直接调⽤
run() 方法?
这是另⼀个⾮常经典的 java 多线程⾯试问题,⽽且在⾯试中会经常被问到。很简单,但是很多⼈都会
答不上来!
new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动⼀个线程并使线程进入了就绪状态,当分配到时间⽚后就可以开始运⾏了。 start() 会执行线程的相应准备工作,然后自动执行run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程⾥执⾏。
SQL 标准定义了四个隔离级别:
READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻⽌脏读,但是幻读或不可重复读仍有可能发⽣。
REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是⼀致的,除⾮数据是被本身事务⾃⼰所修改,可以阻⽌脏读和不可重复读,但幻读仍有可能发⽣。
SERIALIZABLE(可串行化): 最搞的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防⽌脏读、不可重复读以及幻读。
这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 REPEATABLE-READ(可重读) 事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如SQL Server) 是不同的。所以说InnoDB 存储引擎的默认⽀持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的 SERIALIZABLE(可串⾏化) 隔离级别。因为隔离级别越低,事务请求的锁越少,所以⼤部分数据库系统的隔离级别都是READCOMMITTED(读取提交内容) ,但是你要知道的是InnoDB 存储引擎默认使⽤ REPEAaTABLE-READ(可重读) 并不会有任何性能损失。InnoDB 存储引擎在分布式事务的情况下⼀般会⽤到 SERIALIZABLE(可串行化) 隔离级别。
脏读 :表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录 A,此时该事务还未提交,然后另一个事务尝试读取到了记录 A。
幻读 :指同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了。
左连接是以左表为基准,显示所有数据;内连接,只显示两个表中有联系(都存在)的所有数据
https://blog.csdn.net/lisheng19870305/article/details/123336799
explain查看执行计划,判断索引使用情况
1.不满足最左匹配原则
如果在使用联合索引时,没注意最左前缀原则,很有可能导致索引失效
2.使用了select *
在该sql中用了select *,从执行结果看,走了全表扫描,没有用到任何索引,查询效率是非常低的
3.索引列上有计算
id字段的主键索引,在有计算的情况下失效
4.索引列使用了函数
SUBSTR函数,全表扫描,索引失效
5.字段类型不同
查询字符字段时,用双引号“和单引号'都可以。
如果字段是varchar类型,参数不用引号,会认为是int类型,不走索引;
mysql发现如果是int类型字段作为查询条件时,它会自动将该字段的传参进行隐式转换,把字符串转换成int类型
6.like左边包含%
7.列对比
过滤出表中某两列值相同的记录。比如user表中id字段和height字段,查询出这两个字段中值相同的记录
id字段本身是有主键索引的,同时height字段也建了普通索引的,并且两个字段都是int类型,类型是一样的。
但如果把两个单独建了索引的列,用来做列对比时索引会失效。
8.使用or关键字
如果使用了or关键字,那么它前面和后面的字段都要加索引,不然所有的索引都会失效,这是一个大坑
9.not in 和not exist
10.order by
除了遵循最左匹配原则之外,有个非常关键的地方是,后面还是加了limit关键字,如果不加它索引会失效
如果order by语句中没有加where或limit关键字,该sql语句将不会走索引
1、查看该项目最近1000行日志,并且实时查看日志的更新
tail -f logs/debug.log -n 1000
2、只查看日志尾1000行,不会随着日志更新结果也更新
cat logs/debug.log | tail -n 1000
3、只查看日志头1000行,不会随着日志更新结果也会更新
cat logs/debug.log | head -n 1000
4、根据关键词查看日志,返回http相关行日志
cat logs/debug.log | grep 'http'
5、忽略大小写,返回关键词日志
grep -i 'http' logs/debug.log
6、查看某个时间段内日志
里面的时间可以为具体时分秒,不过具体时间的话,日志不一定有该时间点的日志,所以我觉得可以用最小单位分钟来定位
sed -n '/2022-04-20 11:25/,/2022-04-20 11:28/p' logs/debug.log
特别说明:该命令中的两个日期值必须是日志文件中包含的值,否则该命令无效.; 先 grep '2022-04-20 12:20:10' logs/debug.log 来确定日志中是否有该 时间点
7、日志内容特别多,打印在屏幕上不方便查看
(1)使用more和less命令,这样就分页打印了,通过点击空格键翻页
cat -n logs/debug.log |grep "debug" |more
(2)使用 >xxx.txt 将其保存到文件中,到时可以拉下这个文件分析
cat -n logs/debug.log |grep "debug" >debug.txt
8、查找关键字及其前后的信息
根据关键字查看后10行日志
cat logs/debug.log | grep "新增用户" -A 10
根据关键字查看前10行日志
cat logs/debug.log | grep "新增用户" -B 10
根据关键字查看前后10行日志,并显示出行号
cat -n logs/debug.log | grep "合同编码" -C 10
9.循环实时查看最后100行记录:tail -fn 100 express.log
https://blog.csdn.net/weixin_40228200/article/details/120622664
静态查看进程
ps aux #查看进程使用情况
ps aux | less # 查看进程使用情况,考虑到显示过多,使用分管符和less命令查看
ps aux --sort CPU #查看进程使用情况,并按照CPU的使用率升序排列
ps aux --sort -CPU #查看进程使用情况,并按照CPU的使用率降序排列
ps aux --sort rss #查看进程使用情况,并按照内存的使用升序排列
ps aux --sort -rss #查看进程使用情况,并按照内存的使用降序排列
ps -ef | grep XXX #查看指定的进程是否开启
ps axo user,pid,ppid…… #查看进程,并且只查看后面列举出的信息
动态查看进程
top命令可以动态查看进程,top命令常用参数如下:
-d 表示刷新的时间间隔,单位为s
-p 表示查看指定PID的进程
-u 表示查看指定用户的进程
-n 表示top刷新指定次数后退出
top命令使用示例:
top -d 1
top -p 100
top -n 3 >123.txt
top -u nginx
netstat -nultp
Vector是常用的Collection集合中的线程安全的集合,其实现线程安全的原理是为其所有需要保证线程安全的方法都添加了synchronized关键字,锁住了整个对象。
使用锁的种类:互斥锁
Hashtable
Hashtable与Vector类似,都是为每个方法添加了synchronized关键字,来实现的线程安全,锁住了整个对象。
使用锁的种类:互斥锁
使用Collections包装成线程安全,本质上是将原本的集合在执行之前加上了synchronized(){}的对象锁,将对象先锁定再来运行。
使用锁的种类:互斥锁
List<String> list = Collections.synchronizedList(new ArrayList<>());
Set<String> set = Collections.synchronizedSet(new HashSet<>());
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
java.util.concurrent包提供了映像、有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentLinkedQueue。这些集合通过复杂的算法,通过允许并发的访问数据结构的不同部分来使竞争极小化。
这些集合返回弱一致性的迭代器。这意味着迭代器不一定能反映出他们被构造之后的所有的修改,但是,他们不会将同一个值返回两次,也不会抛出ConcurrentModificationException的异常。
https://blog.csdn.net/JokerLJG/article/details/123971153
@Autowired是Spring提供的注解,用于自动装配。
按类型装配(默认使用的装配方式)。
按名称装配(结合@Qualifier注解使用)。
required:默认值true。值为true时,表示必须注入,如bean不存在则会报错;值为false时,表示bean存在就注入,不存在则不注入。
@Autowired的作用范围:成员变量、构造器、方法、参数、注解
当我们使用自动配置的方式装配Bean时,如果这个Bean有多个候选者,假如其中一个候选者具有@Primary注解修饰,该候选者会被选中,作为自动装配的bean。
通过@Autowired和@Qualifier的结合使用可以按名称装配。
下面列举常见@Autowired装配未生效的情况:
@Autowired所在类未加@Controller、@Service、@Component、@Repository等注解,或者或者一些其它情况(如直接new对象的到实例)。这些情况会导致该类的bean并没有交给spring容器去管理,spring就无法完成自动装配的功能。
注解未被@ComponentScan扫描到。
@Resource是JDK自带的注解,用于自动装配。
CommonAnnotationBeanPostProcessor类是Resource的注解处理器。
@Resource默认按照名称自动注入。
既没指定name,也没指定type,自动按照名称装配(当注解写在字段上时,默认取字段名,当注解写在setter方法上时,默认取属性名进行装配。);如果没有匹配,则退而按照类型装配,找不到则抛出异常。
如果没有指定 name 属性,
如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。
如果指定了type,则从上下文中找到类似匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。
如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。
Resource注解的主要属性:
name:指定需注入的bean的名称
type: 指定需注入的bean的类型
@Resource的作用范围:类、成员变量、方法。
@Autowired | @Resource |
---|---|
Spring定义的注解 | JSR-250定义的注解 |
默认按类型自动装配 | 默认按名称自动装配 |
一个参数:required(默认true),表示是否必须注入 | 七个参数:最重要的两个参数是name、type |
默认按类型自动装配如果要按名称自动装配,需要使用@Qualifier一起配合 | 默认按名称自动装配如果指定了name,则按名称自动装配;如果指定了type,则按类型自动装配 |
作用范围:构造器、方法、参数、成员变量、注解 | 作用范围:类、成员变量、方法 |
Redis不同于Memcached的很重⼀点就是,Redis⽀持持久化,⽽且⽀持两种不同的持久化操作。Redis的
⼀种持久化⽅式叫快照(snapshotting,RDB),另⼀种⽅式是只追加⽂件(append-only
file,AOF)。
首先我们要知道,RocketMQ 中间件的 Producer、Broker 以及 Consumer 三个组成部分都有肯能导致消息的丢失。
Producer 如何保证消息不丢失
1.采取 send() 同步发送消息,发送结果是同步感知的。
2.发送失败后可以重试,设置重试次数,默认 3 次。
producer.setRetryTimesWhenSendFailed(10);
3.集群部署,比如发送失败了的原因可能是当前 Broker 宕机了,重试的时候会发送到其它的 Broker 上。
Broker 如何保证消息不丢失
1.修改刷盘策略为同步刷盘,默认情况下是异步刷盘(即消息到内存后就返回确认信息)。
2.集群部署,默认异步复制到 slave,可以采用同步的复制方式,master 节点将会同步等待 slave 节点复制完成,才会返回确认响应。
Consumer 如何保证消息不丢失
消费过程需要注意返回消息状态,只有当业务逻辑真正执行成功后,才能返回 CONSUME_SUCCESS 的 ACK 确认。
https://blog.csdn.net/winfield821/article/details/124372503
微服务相比单体服务主要是解耦,优缺点主要从开发、运行和维护三个方面看。
角度 | 优点 | 缺点 |
---|---|---|
开发 | 各模块不会相互影响,不容易造成代码冲突,容易做 code review 和功能测试。可以采用各自的技术栈。局部修改易部署。 | 需要考虑分布式的复杂性,比如事务和系统容错等。 |
运行 | 某个模块的异常不会影响到其它模块的运行。 | 通信成本较高,增加网络延迟。 |
维护 | 各模块按需扩缩容。此外,自己的数据不需要开放给无关的人员。 | 运维难度提升,需要做流量调度、链路监控等。 |
业务切分 | 开发人员关注自己的业务,便于组织管理。 |
SpringBoot内置了三种servlet容器供大家选择,默认的是tomcat,三种servlet容器 tomcat,jetty 和 undertow
Spring Boot 默认已经使用了 SLF4J + LogBack
https://blog.csdn.net/weixin_41858337/article/details/98046669
日志记录器(Logger)是日志处理的核心组件。log4j具有5种正常级别(Level)。日志记录器(Logger)的可用级别Level (不包括自定义级别 Level), 以下内容就是摘自log4j API (http://jakarta.apache.org/log4j/docs/api/index.html):
static Level WARN:WARN level表明会出现潜在错误的情形。
static Level ERROR:ERROR level指出虽然发生错误事件,但仍然不影响系统的继续运行。
static Level FATAL:FATAL level指出每个严重的错误事件将会导致应用程序的退出。
另外,还有两个可用的特别的日志记录级别: (以下描述来自log4jAPIhttp://jakarta.apache.org/log4j/docs/api/index.html):
static Level ALL:ALL Level是最低等级的,用于打开所有日志记录。
static Level OFF:OFF Level是最高等级的,用于关闭所有日志记录。
日志等级从低到高的顺序是: DEBUG < INFO < WARNING < ERROR < Fatal;通过在这里定义的级别,您可以控制到应用程序中相应级别的日志信息的开关。比如在这里定义了INFO级别,则应用程序中所有DEBUG级别的日志信息将不被打印出来,也是说大于等于的级别的日志才输出。
https://blog.csdn.net/m0_54864585/article/details/125244321
SpringBoot分别提供3中方式读取项目的application.properties配置文件的内容。这个方式分别为:Environment类、@Value注解以及@ConfigurationProperties注解。
Environment是用来读取应用程序运行时的环境变量的类,可以通过key-value的方式读取application.properties和系统环境变量,命令行输入参数,系统属性等,具体如下:
@Value底层就是Environment.java
读取配置文件属性- @ConfigurationProperties使用@ConfigurationProperties首先建立配置文件与对象的映射关系,然后在控制器方法中使用@Autowired注解将对象注入。
https://blog.csdn.net/wangbiao007/article/details/53183764
BeanFactory是管理Bean的工厂,FactoryBean为Bean提供了更加灵活的方式。
BeanFactory和FactoryBean其实没有什么比较性的,只是两者的名称特别接近,所以有时候会拿出来比较一番,BeanFactory是提供了OC容器最基本的形式,给具体的IOC容器的实现提供了规范,FactoryBean可以说为IOC容器中Bean的实现提供了更加灵活的方式,FactoryBean在IOC容器的基础上给Bean的实现加上了一个简单工厂模式和装饰模式,我们可以在getObject()方法中灵活配置。其实在Spring源码中有很多FactoryBean的实现类,要想深入准确的理解FactoryBean,只有去读读Spring源码了。
:
https://www.cnblogs.com/5ishare/p/8683971.html
流程说明(重要):
1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet 。
2. DispatcherServlet 根据请求信息调⽤ HandlerMapping ,解析请求对应的 Handler 。
3. 解析到对应的 Handler (也就是我们平常说的 Controller 控制器)后,开始由
HandlerAdapter 适配器处理。
4. HandlerAdapter 会根据 Handler 来调⽤真正的处理器开处理请求,并处理相应的业务逻
辑。
5. 处理器处理完业务后,会返回⼀个 ModelAndView 对象, Model 是返回的数据对象, View
是个逻辑上的 View 。
6. ViewResolver 会根据逻辑 View 查找实际的 View 。
7. DispaterServlet 把返回的 Model 传给 View (视图渲染)。
8. 把 View 返回给请求者(浏览器)
https://blog.csdn.net/smilehappiness/article/details/119712824
Spring 的 bean 作用域(scope)类型
1、singleton:单例,默认作用域。
2、prototype:原型,每次创建一个新对象。
3、request:请求,每次Http请求创建一个新对象,适用于WebApplicationContext环境下。
4、session:会话,同一个会话共享一个实例,不同会话使用不用的实例。
5、global-session:全局会话,所有会话共享一个实例。
1、在@Controller/@Service等容器中,默认情况下,scope值是单例-singleton的,也是线程不安全的。
2、尽量不要在@Controller/@Service等容器中定义静态变量,不论是单例(singleton)还是多实例(prototype)他都是线程不安全的。
3、默认注入的Bean对象,在不设置scope的时候他也是线程不安全的。
4、一定要定义变量的话,用ThreadLocal来封装,这个是线程安全的
https://zhuanlan.zhihu.com/p/340397290
过滤器应用场景
1)过滤敏感词汇(防止sql注入)
2)设置字符编码
3)URL级别的权限访问控制
4)压缩响应信息
1.使用spring boot提供的FilterRegistrationBean注册Filter
2.使用原生servlet注解定义Filter
两种方式的本质都是一样的,都是去FilterRegistrationBean注册自定义Filter
拦截器应用场景
拦截器本质上是面向切面编程(AOP),符合横切关注点的功能都可以放在拦截器中来实现,主要的应用场景包括:
登录验证,判断用户是否登录。
权限验证,判断用户是否有权限访问资源,如校验token
日志记录,记录请求操作日志(用户ip,访问时间等),以便统计请求访问量。
处理cookie、本地化、国际化、主题等。
性能监控,监控请求处理时长等。
通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取Locale、Theme信息等,只要是多个处理器都需要的即可使用拦截器实现)
https://blog.csdn.net/weixin_43758984/article/details/105538304
无参构造方法的作用
1.无参构造方法一般是用来初始化:如为变量赋处置,初始化对象等。
2.无参构造方法可以去除,JAVA会给每个类一个默认的无参构造方法;当一个类中若存在有参构造方法,若想再使用无参的构造方法,就必须写出无参构造方法。 在类中若存在有参构造方法时,最好加上一个无参构造方法。
3.无参构造方法地作用是为了new出对象的;
4.可以省略不写,这时系统会自动加上无参构造。
5.有一点需要你注意一下:如何你只写了有参构造方法(初始化成员变量),没有写无参构造。这时系统不会自动添加无参构造的,此时程序会报错。
建议:构造方法都是用来初始化的,可以不写无参的构造方法,系统会默认给的。但是后面会学到继承,一般子类的构造方法都是默认调用父类无参构造方法,所以避免自己在写了有参构造方法之后,会出现问题,一般都建议手动写上无参构造方法。当然也可以手动在子类的构造方法第一行写上访问父类的有参构造方法,也是可以得。建议最好还是自己手动写上。
https://blog.csdn.net/liaofeifei_book/article/details/117929430
https://blog.csdn.net/fly910905/article/details/122756111
实现前提
实现这种设计的一个很重要的因素是:String类型是不可变的,实例化后,不可变,就不会存在多个同样的字符串实例化后有数据冲突;
运行时,实例创建的全局字符串常量池中会有一张表,记录着长相持中每个唯一的字符串对象维护一个引用,当垃圾回收时,发现该字符串被引用时,就不会被回收。
实现原理
为了提高性能并减少内存的开销,JVM在实例化字符串常量时进行了一系列的优化操作:
在JVM层面为字符串提供字符串常量池,可以理解为是一个缓存区;
创建字符串常量时,JVM会检查字符串常量池中是否存在这个字符串;
若字符串常量池中存在该字符串,则直接返回引用实例;若不存在,先实例化该字符串,并且,将该字符串放入字符串常量池中,以便于下次使用时,直接取用,达到缓存快速使用的效果。
创建字符串对象的两种方法
直接使用双引号声明出来的String对象会直接储存在常量池中。
String s1 = "abc"
使用new方法创建出来的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中
String s2 = new String("xyz");
public class Test1{
public class void main(String[] args){
String s1 = "abc";
String s2 = "abc";
System.out.println(s1);//abc
System.out.println(s1 == s2);//true
char[] charArray = {'a','b','c'};
String s3 = new String(charArray);
System.out.println(s3);//abc
System.out.println(s1 == s3);//false
}
}
从结果看,两个布尔值的结果不相同,虽然两个输出两个字符串的结果相同,都为abc,但比较两者时则不同,这是因为引用类型比较时,比较的是其地址值;不同于基本数据类型比较的是其数据值。
采用直接创建String类型对象的方法创建对象时,JVM首先会去字符串常量池中查找是否存在"abc" 这个对象,如果不存在,则在字符串常量池中创建”abc“这个对象,然后将池中”abc“对象的引用地址返回给对象s1,这样s1的地址就在常量池中;
如果存在,则不创建任何对象,直接将存在的“abc”的地址返回给对象s2。这就是为什么s1等于s2的原因。
而通过new方法创建的String对象,其创建的字符串是放在堆当中的,将堆当中的字符串地址返回赋值给s3,s1和s3的存放地址不相同,一个在字符串常量池中,一个在堆当中,字符串常量池外,因此返回的值是false。
克隆对象:
实现 Cloneable 接口并重写 Object 类中的 clone() 方法。
实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
对于jvm底层,String str = new String("123")创建对象流程是什么?
在常量池中查找是否存在"123"这个字符串;若有,则返回对应的引用实例;若无,则创建对应的实例对象;
在堆中new一个String类型的"123"字符串对象;
将对象地址复制给str,然后创建一个应用。
注意:
若常量池里没有"123"字符串,则创建了2个对象;若有该字符串,则创建了一个对象及对应的引用。
浅克隆:当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复
制。
深克隆:除了对象本身被复制外,对象所包含的所有成员变量也将复制。
克隆的对象可能包含一些已经修改过的属性,而 new 出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠克隆方法了。
实现 Cloneable 接口并重写 Object 类中的 clone() 方法。
实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
https://blog.csdn.net/qq_44143902/article/details/120712900
被static修饰的属性是所有类共享的
首先我们来说一下static修饰的变量。被static修饰的变量为类变量,其初始化是在类加载的时候就已经初始化赋值,当然也可以在构造方法中对其进行修改或重新赋值。static修饰的变量可以通过 类名.变量名 的形式进行调用,也可以通过 对象.变量名 进行调用,虽然两种方式都可以调用到static修饰的变量,但是推荐使用前者,这样也便于后续一些问题的理解。
接下来说一下transient。transient是序列化和反序列化时用于标记属性是否可序列化反序列化的“标记”,带有transient修饰的成员变量,在类中相当于一个(没有static修饰的)成员变量,对于该类的使用没有任何影响,只是在序列化的时候,带有transient标记的变量,无法被序列化和反序列化。
讲到这里,还要讲一下我个人对于序列化的一点形象化理解。序列化是将对象的信息保存在文件中,注意这里所说的是对象,而不是类,所以任何与类相关的变量、方法都不能被序列化(即保存在文件中),这是首先序列化是用来存储对象的(再次重申)。序列化,是将符合要求(非静态,非transient修饰的属性)的对象内容保存在文件中,如果有属性是transient修饰的,则对象中会有这个属性,但是其值为默认值(int类型默认值为0,String默认值为null…)
下面说一下两者在使用时的区别
1.如果在一个类中定义了一个使用static修饰的变量,则在序列化的时候,该变量不可以被序列化,也就是说,当我们对一个对象进行序列化的时候,系统识别到这个对象里的age属性是类变量,那么就不会被序列化。但是我们在实验的时候发现,反序列化得到的对象可以调用age,这又是为什么呢?这是因为jvm的原因,由于age是类变量,在创建对象之前,age属性就已经被加载好了,当对象调用age的时候,系统发现没有成员变量age,但是有个Person类的类变量age,而且系统发现你的对象,正是Person类的实例,那就让你调用吧。(此处有点像第一点中所说的对于static变量的调用方法)。而此时我们换一台电脑,或者重启后,再对文件进行反序列化,会发现age的值成了0,这是由于虽然age无法被序列化,但是age这个属性是存在的,既然无法被序列化,就没有值了,那就用int类型的系统默认初始值0。
2.transient修饰的变量跟普通的成员变量在使用上没有任何区别,但是在序列化的时候,一旦系统识别到你的age属性有transient修饰,则age就不可以被序列化,还是同上一点所讲的一样,不能被序列化,并不是这个属性不存在了,而是属性的值无法被保存起来,也就是该属性的值就是默认值,相当于在创建类定义属性的时候不赋值,就是默认值。
https://blog.csdn.net/qq_40408443/article/details/123280733
ClassLoader是Java的类加载器,用于把class文件加载到JVM中。
什么是加载 : 加载就是根据类的全限定名(全限定名 : 包名 + 类名)获取到其定义的二进制字节流,并将其加载到内存中. 此时需要借助类加载器来帮助完成
类加载器分为4类 :
%JAVA_HOME% : 为JDK设置的环境变量路径. 如环境变量里设置了%java_home%=C:\jdk1.8.0
1. Bootstrap Classloader : 启动类加载器,用来加载 %JAVA_HOME%/jre/lib 下的, 如 rt.jar中的class文件 或者 xbootclasspath选项指定的jar包
2. Extension Classloader : 扩展类加载器 , 用来加载 %JAVA_HOME%/jre/ext 中的class文件 或者 -Djava.ext.dirs指定目录下的jar包
3. Application Classloader : 应用类加载器 , 用来加载classpath下的class文件
4. Custom Classloader:用户自定义类加载器,用来加载自定义内容.此加载器需要用户自己继承Classloader类
首先需要在你的项目创建新模块,此模块专门用来配置服务网关
一个功能可以用来解决跨域问题
首先跨域问题是怎么产生的?
1 访问的端口号不一样,前端用的端口号9002,和后端的端口号8002此时前端访问后端的端 口就会产生跨域问题
2 访问的协议不一样,例如http和https
3 访问的ip地址不一样,例如一个是localhost:8080,一个是192.168.2.130:8080
用gateway如何解决跨域问题
在我们新建好的模块先添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
然后新建配置类
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
就可以解决跨域问题。
gateway还可以用来作为反向代理,可以代替nignx
只需要在配置文件中添加如下配置
# 服务端口
server.port=80
# 服务名
spring.application.name=service-gateway
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#使用服务发现路由
spring.cloud.gateway.discovery.locator.enabled=true
#设置路由id 就是模块暴露给nacos的服务名
# 例如这个模块暴露的就是 service-gateway
spring.cloud.gateway.routes[0].id=service-hello
#设置路由的uri 就是将暴露的服务名作为路由地址
#如果你使用ip地址加端口号,你的项目每换一个环境,就要修改一次路由地址不方便
spring.cloud.gateway.routes[0].uri=lb://service-hello
#设置路由断言,代理servicerId为service-hello的/hello/路径
#就是去寻找路径为/*/hello/**的controller,*是所有的意思
spring.cloud.gateway.routes[0].predicates= Path=/*/hello/**
#设置路由id
spring.cloud.gateway.routes[1].id=service-user
#设置路由的uri
spring.cloud.gateway.routes[1].uri=lb://service-user
#设置路由断言,代理servicerId为service-user的/user/路径
spring.cloud.gateway.routes[1].predicates= Path=/*/user/**
https://blog.csdn.net/Dream_Weave/article/details/85165972
Nginx服务器的负载均衡策略可以划分为两大类:即内置策略和扩展策略。内置策略主要包含轮询、加权轮询和IP hash三种;扩展策略主要通过第三方模块实现,种类比较丰富,常见的有url hash、fair等。
https://blog.csdn.net/godloveleo9527/article/details/106816989?
最好情况需比较n-1次,最坏情况需比较n(n-1)/2。
冒泡排序的最好时间复杂度为O(n),最坏时间复杂度为O(n²),平均时间复杂度为O(n²),空间复杂度为O(1),它是一种稳定的排序算法。