java面试八股

计算机网络

网络体系结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2WEpnO6J-1659311586325)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220510103832871.png)]

应用层:为用户提供

分层的好处:

  1. 将复杂问题分解
  2. 各层功能独立,各司其职
  3. 服务定义精确,实现变化灵活,不影响相邻层
  4. 有利于系统的设计。。。。

1. 应用层

  1. 功能:网络服务和用户的接口
  2. 应用层的协议:HTTP,DNS,SMTP(电子邮件),FTP(文件传输协议)

2. 表示层

  1. 功能:数据的表示、安全、压缩
  2. 格式:JPEG、ASCII、加密格式等

3. 会话层

  1. 功能:建立、管理、终止会话

4. 传输层

  1. 功能:负责为主机进程之间的通信提供通⽤的数据传输服务
  2. tcp udp,网关

5. 网络层

  1. 功能:选择合适的⽹间路由和交换结点,根据ip地址寻找mac地址

  2. IP、ARP(地址解析协议)、路由器都在这一层

  3. ip报文里ttl(time to live)的作用:限制ip数据包在网络中的存在的时间,实际是一个IP数据包能够经过的最大的路由器跳数。

    tos表示服务类别

  4. ARP协议根据ip地址获取mac地址的一个TCP/IP协议

6. 数据链路层

  1. 功能:将网络层浇下来的IP数据报组装成帧、差错控制、流量控制、MAC寻址
  2. 交换机

7. 物理层

为数据链路层提供二进制传输的服务(利用传输介质传输比特流)

  1. 为数据端设备提供传送数据通路,尽可能屏蔽掉具体传输介质和物理设备的差异

计算机网络模型(TCP五层模型) - 知乎 (zhihu.com)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YKFKFoWu-1659311586326)(C:\Users\LandMoon\Desktop\面试\笔记\v2-cbe083ccc6e71f4c9fa7f8547099ed50_r.jpg)]

(116条消息) 计算机网络–TCP/IP四层模型_即将秃了的96年程序员的博客-优快云博客_计算机网络四层模型

三次握手

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Npknguh-1659311586327)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220316114154379.png)]

  1. seq:表示的是本端的数据是从哪个序号开始

​ ack:表示ack-1之前的数据已经接收到了,下一次希望收到的对方的数据是从哪一个序号开始,一般是对方传过来的seq+1,表示我已经读取了你上一次传过来的数据中的一段数据,希望你下一次发过来的seq是下一段数据开始的序号

四次挥手

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nPmuoxvI-1659311586328)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220316135217560.png)]

  1. TIME_WAIT状态的意义:

    1. 保证TCP的全双工连接能够可靠地关闭。

      假如Client发送的最后一次ACK包丢失了,没有被Server收到,那么超过一定时间后,Server会重新发一次FIN包,如果这个包被处于TIME_WAIT状态的Client收到,那么它就会重新发一次ACK包,并且重新开始计时,直到Client在整个TIME_WAIT过程中都没有收到来自Server的FIN包,就说明Server已经收到ACK包并closed了,那么Client也可以安心Closed了。如果Client在发完ACK包之后直接Closed了,那么当ACK丢包之后,Server会再发一个FIN包,但是这个包不会被Client回应,因此Server最终会收到RST,误以为是错误连接,这样不符合可靠连接的要求。

      MSL(Maximum Segment Lifetime),指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

    2. 保证本次连接的重复数据段从网络中消失

      如果Client直接closed了,接着又向Server发起了一个新的TCP连接,两个连接的端口号可能相同,然么此时如果上一次的连接有一些数据堵塞了,还没到达Server,而Client又发起了一个新的连接,由于区分不同TCP连接靠的是套接字(源IP地址和目标IP地址以及源端口号和目标端口号的组合为套接字),那么这批迟到的旧数据会被认为是新的连接的数据,从而导致数据混乱。

TCP和UDP的区别

  1. TCP需要建立连接,UDP不需要
  2. TCP是可靠的,UDP不可靠(UDP不可靠指的是接收方收到报文后不需要给出确认)
  3. TCP是一对一的,UDP还可以一对多、多对一、多对多
  4. TCP是面向字节流的,发送数据时以字节为单位,一个数据包可以拆分成若干组进行发送,而UDP是面向报文的,一个报文只能一次发完
  5. TCP有拥塞控制,UDP没有,所以当网络出现拥塞时UDP不会使发送速率降低
  6. TCP首部开销比UDP大
  7. UDP的主机不需要维持复杂的连接状态表

流量控制

如果发送方把数据发送得过快,接收方可能来不及接收,就会造成数据丢失,所谓的流量控制就是让发送方发送速率不要太快,要让接收方来得及接收。

实现方式是滑动窗口协议,接收方会维护一个接收窗口,其大小是根据自身资源情况动态调整的,在返回ACK时会将接收窗口大学校在TCP报文的窗口字段通知发送方,发送窗口的大小不能超过接收窗口的大小。

拥塞控制

(102条消息) TCP的快速重传_Drizzlejj的博客-优快云博客_tcp快速重传

HTTP和HTTPS

  1. HTTP可能会带来的问题:

    1. 使用明文通信,内容可能会被窃听;
    2. 不验证对方的身份,因此可能遭遇伪装;
    3. 无法证明报文的完整性,因此可能已被篡改
  2. HTTP + 加密 + 认证 + 完整性保护 = HTTPS(HTTP Secure)

    image-20220320195208960 image-20220320195341619
  3. 两种加密方式:

    1. 对称加密(共享密钥加密):发送方发送加密信息时需要将密钥也发送给对方,对方使用发送过来的密钥进行解密。

      带来的问题:密钥也有可能被窃听,那么加密就失去了意义。

      image-20220320200438163
    2. 非对称加密:采用一对非对称密钥,一把叫私有密钥,一把叫公开密钥。私有密钥不能让他人知道,公开密钥可以。发送方使用对方的公开密钥进行加密,对方收到信息后,使用自己的私有密钥进行解密,这样就不需要发送用来解密的私有密钥,不用担心密钥被窃听。

  4. HTTPS采用两种加密方式并用的混合加密机制,对称加密速度较快,但是不够安全,非对称加密则相反。因此用非对称加密来发送对称加密所需要用到的密钥,然后用对称加密发送信息。

    image-20220320201229720
  5. 认证:判断发送的公开密钥是否正确,通过一些认真机构颁发的公开密钥证书。

  6. 完整性保护:在双方通信的过程中,应用层发送数据时会附加一种叫做MAC(Message Authentication Code)的报文摘要,它能够查知报文是否遭到篡改,从而保证完整性。

  7. HTTPS由于采用SSL,所以导致速度变慢,体现在两个方面:

    1. 通信慢。与HTTP相比,出去与TCP连接,发送HTTP请求/响应之外,还需要进行SSL通信,因此通信量增加了;
    2. SSL必须进行加密处理。在服务器和客户端都要进行加密和解密处理,会消耗更多的CPU和内存等硬件资源。
  8. HTTPS的连接过程:

    preview
    1. 客户端向服务端发送请求,同时发送客户端支持的一套加密规则;
    2. 服务端判断客户端发送过来的加密规则是否符合自己所支持的,如果不是则断开连接,如果是,则给客户端发送公开密钥证书
    3. 客户端验证证书是否有效,有效则会取出密钥,并生成一个随机密钥(用于对称加密),使用服务端发送过来的公开密钥进行加密,使用HASH算法对握手消息进行摘要计算,并使用前面生成的随机密钥进行加密(对称加密),将加密后的随机密钥和摘要一起发送给服务器。
    4. 服务器用自己的私钥解密得到用于对称加密的密钥,用解密得到的密钥对HASH摘要进行解密,并验证握手消息是否一致,如果一致,服务器使用对称加密的密钥对握手消息进行加密并发送给客户端;
    5. 客户端对握手消息进行解密并判断是否一致,若一致,则握手结束。此后的数据传送都使用对称加密的密钥进行加密。

    总结:在此过程中,非对称加密密钥将对称加密的密钥进行加密,对称加密的密钥对真正需要传输的消息进行加密,HASH算法用于验证数据的完整性。

  9. cookie和session

    Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。

    如果说Cookie机制是通过检查客户身上的“通行证”来确定客户身份的话,那么Session机制就是通过检查服务器上的“客户明细表”来确认客户身份。Session相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。

从输入URL到页面加载发生了什么

前端经典面试题: 从输入URL到页面加载发生了什么? - SegmentFault 思否

HTTP请求的组成

由三大部分组成:请求行、请求头、空行、请求体

  1. 请求行:请求方法(get、post、head、delete、options、put。。。)、请求URL、HTTP版本
  2. 请求头:User-Agent、Accept、Content-Type、Host

数据库

引擎

MyISAM和InnoDB的区别

  1. 是否支持行级锁:MyISAM只有表级锁,InnoDB支持行级锁和表级锁,默认行级锁
  2. 是否支持事务:前者不支持,后者支持
  3. 是否支持外键:只有后者支持
  4. 是否支持MVCC:只有后者支持

MyISAM查询速度较快,索引对于只读数据的场景们还是可以使用MyISAM

事务

  1. 事物的一致性理解:

​ 保证事务只能把数据库从一个有效(正确)的状态转移到另一个有效(正确)的状态,比如数据库预先对一些字段有一些约束(如其值 不能小于0),如果事务的执行结果会破坏这个约束,那么就不会执行成功。

​ 使得计算机中的虚拟操作符合真是世界中的规律,比如从A中取出100块钱放入B中,那么结果就应该是A少了100,同时B多了100块。

​ 如何理解数据库事务中的一致性的概念? - sleep deep的回答 - 知乎 https://www.zhihu.com/question/31346392/answer/569142076

  1. 事务的隔离性以及相应的问题:

    事务并发可能会带来的问题:

    更新丢失:一个事务对某数据进行了修改,在提交之前,另一个事务对同一数据也进行了修改,覆盖了之前事务的修改;

    脏读

    不可重复读

    幻读

    事务的隔离级别:

    1)读取未提交:当同时开启多个事务,任意事务可以看到其他事务未提交的数据,能解决更新丢失的问题,但由此会带来脏读

    比如事务A先对某个数据进行了修改,在提交之前,事务B来查询同一个数据,然后事务A又回滚了之前的命令,那么事务B读取到的数据就是错的。

    2)读取已提交:一个事务只能看见其他已经提交的事务所带来的改变。

    某个事务A对数据进行了修改,在提交之前,事务B是无法看到的,只有等事务A提交了之后,事务B才能看到,但是当前事务A读取的是未提交的数据。该隔离级别可以解决脏读,但是存在不可重复读的问题。

    比如事务A先查询了某个数据,之后事务B对该数据进行了修改并提交,那么在事务A再次读取该数据时就会得到和前面不一致的结果。

    3)可重复读:同一个事务在多次读取同一个数据时得到的结果时一致的。可以解决不可重复读的问题,但是存在幻读的问题。

    某个事物A先查询了某个数据,之后事务B对该数据进行了修改,无论事务B是否提交,事务A对该数据读取的结果都是一致的,只有当事务A提交之后,才能看到事务B做出的修改。

    对于幻读,事务A在第一次查询时发现某个数据s不存在/存在,此时事务B插入/删除了数据s,并且提交了,那么事务A再次查询时会发现数据s又存在/不存在了,就像是个幻影一样。

    4)可串行化:要求事务序列化执行,事务只能一个接一个执行,不能并发执行。能解决幻读的问题,但可能导致大量的超时现象和锁竞争,实际很少使用。

  2. 锁和三级封锁协议

1)共享锁(Shared Lock)/S锁/读锁:加了读锁之后,该事务只能对该数据进行读取,而不能修改,并且同时其他事务也只能对该数据加读锁,而不能加写锁。

2)排它锁(Exclusive Lock)/S锁/写锁:加了写锁之后,该事务可以对该数据进行读取和修改,但其他事务不能对该数据加任何锁,但是其他事务可以读取该数据,不能对该数据进行写操作。

3)意向锁(Intention Lock):当给表中某一行加写锁或读锁时,会同时给这一行所处的表加上意向写锁(IX)或意向读锁(IS)。意向锁是表级锁,不会和行级的写锁和读锁发生冲突,只会和表级的写锁和读锁冲突。

当某个事务B要获取某张表的表级锁时,先判断该锁是否被其他事务用表锁锁住;然后判断表中是否有意向锁,如果有,则说明该表中某些行被行锁锁住了,因此事务B会阻塞。

如果没有意向锁,那么情况是这样的:首先判断该表是否被表锁锁住,然后判断表中的每一行是否被行锁锁住。第二步是非常耗时的。



三级封锁协议:

1)一级封锁协议:解决更新丢失问题,一个事务对数据进行写操作时,必须先对其加写锁,直到事务结束才释放(由于不同事物不能同时对同一个数据加写锁,所以不会出现更新丢失);

2)二级封锁协议:解决脏读问题,在一级的基础上,一个事务对数据进行读操作时,必须先加读锁,**读取完之后立即释放**。(当事务A对数据进行更新时,加了写锁,此时事务B要想读取该数据需要加读锁,而由于已经加了写锁,其他事务不能加任何锁,所以事务B只能等待事务A完成之后再加读锁进行读取操作);

3)三级封锁协议:解决不可重复读问题,在二级的基础上,一个事务要对数据进行读操作,必须先加读锁,且得**等到该事务完成才会释放读锁**。(事务A先对某数据加读锁,进行读操作,此时事务B要想对该数据进行修改,需要加写锁,而由于加了读锁之后,不能再加其他锁,所以事务B只能等待事务A完成释放了读锁之后才能进行写操作。

2. 

MVCC

概述:通过保存数据的历史版本,根据版本号的比较来判断数据是否显示,从而达到读取数据时不需要加锁就可以保证事务的隔离性。可以实现读已提交可重复读

实现过程:主要依赖事务版本号,版本链以及read view,其中事务版本号指的是事务开启时的系统版本号;版本链里记录的是某个数据的各个版本及其对应的版本号;read view中记录的是数据的某个版本对于当前事务是否可见。

每一次查询对应一个ReadView(当然可能是复用已有的ReadView),查询某数据时,会将该数据的版本号根据ReadView的比较规则来判断该数据的当前版本是否可见,如果不可见,则去版本链中取上一个版本,接着再判断。

读已提交和可重复读的区别在于生成Readview的策略不同

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eTcoLBMl-1659311586328)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220328224728642.png)]

MySql

delete\truncate\drop的区别

  • delete:

    delete table name;//一行一行删除表中所有数据,仅删除内容,表的结构还在,后面可以加where来删除某些符合条件的行。删除每一行会同时将该行的删除操作作为事务记录在日志中保存,以便进行回滚操作。
    
  • truncate:

    truncate table name;//一次性删除表中所有数据,不会记录日志,也无法进行回滚,速度更快。只删除数据,不删除表的结构。
    
  • drop:

    drop table name; //将表所占用的空间全部释放。
    drop table if exits name;
    

char和varchar

https://www.cnblogs.com/mingerlcm/p/9799813.html

  1. char(M):定长,M范围[0, 255],如果没有指定M则默认1,存入的数据如果长度超过M会报错,如果不够M则会在后面补空格,读取时会自动去掉后面的空格。适合存储较短、或所有长度都较为接近,经常变化的数据
  2. varchar:变长,M范围[0, 65535],需要1-2个额外字节来存储变量长度,如果变量小于255,则只需要一个;适合存储较长或者长度相差较大的数据

三大范式

1NF:每个属性都不可再细分;

2NF:在满足1的前提下,每个非主属性都必须完全依赖于主属性(除非是联合主键),即其他属性要能够由主属性完全决定;

3NF:在满足2的前提下,非主属性之间不能有传递依赖关系,假如A是主键,B依赖于A,C有可能依赖于B,这样不行。

上锁

  1. 共享锁: select * where id = 1 lock in share mode;
  2. 排他锁:select … for update;

索引

使用索引和不使用索引的区别:不使用索引,需要遍历双向链表来定位相应的数据页,使用了索引之后,数据变得相对有序,可以使用二分查找来定位。

索引的创建:

image-20220327214241136

B树和B+树:

B树:树中每个节点都存储数据;叶子节点之间没有连接

B+树:只有叶子节点才会存储数据,叶子节点之间也有连接

使用B+树的原因:

  1. B树每个节点种不仅包含数据的Key值,还包含data值,而每一页的存储空间是有限的,这就导致了每一页能够存储的节点数量较少,从而导致树的高度比较高,而树的高度每增加一层,查询时与磁盘IO就多一次;B+树除了叶子节点,只存储key值,这样每一页的节点数量就大增,降低B+树的高度,减少与磁盘IO次数;
  2. B+树的叶子节点之间有指针进行相连,因此在数据遍历时,只需要遍历叶子节点就行;

聚簇索引和非聚簇索引

  1. 聚簇索引:数据的物理地址顺序和索引顺序一致,可以通过二分法进行检索,效率较高;但是修改效率较慢,因为要改变物理顺序,适合经常搜索
  2. 非聚簇索引:只记录逻辑顺序,不改变物理顺序。

索引失效的情景

Mysql索引查询失效的情况 - 关键的疯狂 - 博客园 (cnblogs.com)

  1. like语句以”%“开头,索引失效,对于 like “3%”,索引有效
  2. or语句左右没有同时使用索引
  3. 数据出现隐式转换,如varchar不加单引号,则会自动转为int;
  4. 对于组合索引必须包含最左列索引,如组合索引为(col1, col2, col3),则索引生效的情况包括(col1或col1, col2或col1, col2, col3)
  5. 如果MySql估计全表扫描比索引块,则不会使用索引

哪些地方适合创建索引

  1. 经常需要查询的字段
  2. 主键自动建立唯一索引
  3. 经常作为表连接的字段
  4. 经常需要排序的字段,因为已经排序后,可以加快查询
  5. 经常出现在ORDER BY / GROUP BY / DISTINCT后面的字段

创建索引时应注意:

  1. 只应建立在小字段上,而不要对大文本或图片建立索引,(字段小的话,一页存储的数据越多,一次IO获取的数据越多,效率越高)
  2. 建立索引的字段应该非空,因为它们使得索引的统计和比较更加复杂
  3. 选择数据密度大(唯一值占总数的百分比很大)的字段做索引

数据库优化

1. 硬件性能:磁盘、CPU、内存

2. SQL语句优化

  • 应尽量避免where子句中使用!=、`<>`或对null进行判断,这会使得引擎放弃使用索引
  • 只返回必要的列和必要的行:减少使用SELECT * 语句和使用LIMIT来限制返回的数据

3. 索引的优化

注意会引起索引失效的情况,在适合的地方建立索引

4. 数据库表结构的优化

  • 分区:如按时间分区
  • 遵循三大范式
  • 尽量使用固定长度字段和限制字段的长度,降低物理存储空间,提高数据库处理速度

5. Java方面

  • 尽可能少创建对象
  • 大量数据操作和少量数据操作分开
  • 合理利用内存,将部分 数据缓存

大表优化

  1. 限定数据范围,比如在用户查询订单时,将时间控制在一个月的范围内
  2. 读写分离,主库负责写,从库负责读
  3. 垂直分区
  4. 水平分区

主从复制

概念:指数据可以从一个MySql主数据复制到一个或多个从数据库。

实现原理

  1. 主数据库开启 binary log dump线程,将主数据库中的数据更改日志写入Binary log中;
  2. 从数据库I/O线程,负责从主数据库读取binary log,并写入本地的Relay log;
  3. 从数据库SQL线程,负责读取Relay log,解析出主数据库中执行的更改,并在从数据库中重新执行,保证主从数据库的一致性

好处

  1. 读写分离:主数据库负责写,从负责读:
    • 缓解了锁竞争,即使主数据库中加了锁,依然可以进行读操作
    • 从数据库可以使用MyISAM,提升查询性能,节约系统开销
    • 增加冗余,提高可用性
  2. 数据库实时备份,当系统中某个节点发生故障时,可以方便的故障切换
  3. 降低单个数据库磁盘I/O的频率,提高单台机器的性能

SQL

为什么不推荐使用SELECT * ,而使用 Select 所有字段?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bUYKqZt2-1659311586329)(C:\Users\LandMoon\Desktop\面试\watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3liNTQ2ODIyNjEy,size_16,color_FFFFFF,t_70)]

可读性更强

LIMIT的用法

select ... from tablename limit i, n;//从索引i开始,查询n个数;如果没有指定i,那么默认从0开始查。

删除

delete from table where...

between and

select ... from.. where age between 12 and 20;

<> 和 !=都表示不等于,或者not a = 1;

image-20220402213528928
where aa like "%北京%"; //表示aa中存在北京两个字

保留一位小数

round(avg(gpa),1)

distinct关键字

image-20220411011433709

(85条消息) sql distinct多个字段_SQL基础丨检索数据_weixin_39963819的博客-优快云博客

Having关键字

1、SQL出现having的原因是,where关键字无法与聚合函数一起使用

2、having关键字放在group by关键字后面,针对分组后的数据进行筛选

select university,
       avg(question_cnt) as avg_question_cnt,
       avg(answer_cnt) as avg_answer_cnt
from user_profile
group by university
having avg_question_cnt < 5 and avg_answer_cnt < 20

DATE_ADD()

(85条消息) sql:mysql:函数:获取当前日期+ DATE_ADD()_花和尚也有春天的博客-优快云博客_sql中date_add

DATE_ADD(date,INTERVAL expr type) //返回的是在date上加expr倍type的日期

date_diff()

(85条消息) SQL中DATEADD和DATEDIFF的使用方法_qq_23944441的博客-优快云博客_sql中dateadd的用法

date_diff(enddate, startdate)//以date_part为单位,计算后两个变量之间的差值。前者减去后者,有些只有后两个参数,表示天的差距

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Vz8KThN-1659311586329)(C:\Users\LandMoon\Desktop\面试_v_images\image-20220406012233517.png)]

last_day(date)

获取date所对应的是月份的最后一天

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-csoxzqEz-1659311586330)(C:\Users\LandMoon\Desktop\面试_v_images\2019051123462958.jpg)]

可以再使用DAY(last_day(date))获取每个月的总天数

timestampdiff(interval, start_time, end_time)

interval:时间单位,image-20220406112720673

MYSQL中TIMESTAMPDIFF和时间戳字段直接相减的区别

比如用e n d _ t i m e − s t a r t _ t i m e end_time - start_timeend_time−start_time,那么两个时间戳的时间差的进制会是按照100的。比如相差1分钟,但查询出来的是却是100。

但是用T I M E S T A M P D I F F ( s e c o n d , s t a r t _ t i m e , e n d _ t i m e ) TIMESTAMPDIFF(second, start_time, end_time)TIMESTAMPDIFF(second,start_time,end_time)的话,两个时间戳相差超过1分钟的话,就会是60。

原文链接:https://blog.youkuaiyun.com/qq_41688840/article/details/123450457

date_format(date, format)

(85条消息) MySQL时间格式转换函数date_format()用法详解_super_bert的博客-优快云博客_mysql 日期格式化

date:如'2022-04-04'

format:如'%Y-%m-%d',表示以年-月-日的形式

某个字段为null的判断要用is,不能用=

字符串的处理

mysql函数substring_index的用法 - 云+社区 - 腾讯云 (tencent.com)

1、LOCATE(substr , str ):返回子串 substr 在字符串 str 中第一次出现的位置,如果字符substr在字符串str中不存在,则返回02、POSITION(substr  IN str ):返回子串 substr 在字符串 str 中第一次出现的位置,如果字符substr在字符串str中不存在,与LOCATE函数作用相同;

3LEFT(str, length):从左边开始截取str,length是截取的长度,如果length大于str的长度,则返回str;

4RIGHT(str, length):从右边开始截取str,length是截取的长度;

5、SUBSTRING_INDEX(str, substr, n):返回字符substr在str中第n次出现位置之前的字符串;如果n是负数,则倒过来看

6、SUBSTRING(str, n, m):返回字符串str从第n个字符截取到第m个字符;

7REPLACE(str, n, m):将字符串str中的n字符替换成m字符;

8、LENGTH(str):计算字符串str的长度。

upper()和lower()函数

将括号中的字符串全变成大写或者小写。

concat(a, b)函数

将字符串a和b拼接。

replace intoinsesrt into的区别

当表中主键或者唯一索引与要插入的数据相同时,使用insert会报错,而使用replace会先删除相同的数据,再插入新数据。

(85条消息) SQL语法:Replace into 与 Insert into 的区别_魏晓蕾的博客-优快云博客

if函数

SQL的IF语句 - nichoo的博客 - 博客园 (cnblogs.com)

if (expr1, expr2, expr3) //如果expr1为真,则执行2,否则执行3

case when 判断语句 then 如果判断语句为真则执行 else 判断语句为假 end

SQL -利用Case When Then Else End 多条件判断 - Be-myself - 博客园 (cnblogs.com)

ifnull(expr, alt_value)

当expr为null时,则返回alt_value

sql执行的优先级

(85条消息) SQL的执行顺序_不想做靓仔的博客-优快云博客_sql执行顺序

在where子句中不能使用select中的别名,即as后面的名字

group by

(85条消息) 关于group by的用法 原理_KeepGoingPawn的博客-优快云博客_group的用法

窗口函数(OLAP函数)

最全的SQL窗口函数介绍及使用 - 知乎 (zhihu.com)

通俗易懂的学会:SQL窗口函数 - 知乎 (zhihu.com)

https://blog.youkuaiyun.com/weixin_39842918/article/details/113319871

sum()函数在有没有order by是不一样的。

基本语法:

<窗口函数> over (partition by <用于分组的列名>
            order by <用于排序的列名> rows between 2 proceeding and 3 following)

group by分组汇总后改变了表的行数,一行只有一个类别。而partiition by和rank函数不会减少原表中的行数

preview

窗口函数:

  1. 专用窗口函数,rank、dense_rank、row_number等
  2. 聚合函数, sum、avg、count、max、min等

窗口函数是对where或者group by子句处理后的结果进行操作,原则上只能写在select子句中

  • rank()

Redis

常见数据结构

  1. String
  2. Set
  3. ZSet
  4. Hash
  5. List

设置过期时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-urgiiFWE-1659311586330)(C:\Users\LandMoon\Desktop\面试_v_images\image-20220410171751335.png)]

过期的删除策略

  1. 定时删除:每个设置了过期时间的key都会创建一个定时器,到了过期时间就会立即删除,对内存很友好,但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  2. 惰性删除:当访问某个key时才会判断该key是否已经过期,如果过期则清除。该策略可以最大化节省CPU资源,但是对内存不太友好,可能会出现大量没有被访问的过期key,占用大量内存。
  3. 定期删除:每隔一段时间(100ms)就会随机抽取一些设置了过期时间的key,检查其是否过期,如果过期则清除。

Redis中同时使用了后两种过期策略。

缓存雪崩、穿透

缓存雪崩(机器宕机、大量数据同时过期)

指同一时间缓存大面积失效,导致后面的请求落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案:

  1. 缓存数据的过期时间随机设置,防止同一时间大量数据过期。
  2. 当并发量不是特别多的时候,使用加锁排队;
  3. 数据预热:系统上线后,将相关的缓存数据直接加载到缓存中
  4. 保持redis集群的高可用性,发现机器宕机尽快补上。(事前)
  5. 使用本地缓存+服务降级(事中)
  6. 利用redis持久化机制保存的数据尽快恢复(事后)

缓存穿透

指请求大量不存在的数据,由于缓存中没有这些数据,导致请求都落在数据库上,造成数据库短时间内承受大量请求。

解决方案:

  1. 缓存无效key:将缓存和数据库中都查询不到的key放入redis,并将有效时间设置短点,这样可以防止攻击用户反复用一个id暴力攻击
  2. 布隆过滤器:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求i时,先判断请求的值是否存在与过滤器中,如果不存在,则直接拦截掉。

缓存击穿

指同一条数据,在缓存中没有,但是在数据库中存在,此时由于大量用户从缓存中读取该数据没读到,又同时去数据库中读取,造成数据库压力瞬间增大。

解决方法:

  1. 设置热点数据永不过期
  2. 加互斥锁:当根据key获得的value值为空时,先锁上,再从数据库加载,加载完毕再释放锁。

一致性

1. 读缓存

  1. 读取数据时先从缓存中找,如果找到了则直接返回数据,如果没有则去数据库中找,找到之后再将从数据库中读取到的数据放入缓存中

2. 写缓存

  1. 流程:先更新数据库,再删除缓存
  2. 为什么是删除缓存而不是更新缓存:一是更新缓存可能涉及到一些多表查询,更新缓存的代价较高;二是可能有些场景是写操作比较频繁,很少读取,那么这个时候频繁的更新缓存意义不大,因为更新之后并不会被频繁地访问到。
  3. 为什么不先删除缓存,再更新数据库:考虑到并发。比如,A线程先删除缓存,此时B线程来访问缓存,发现里面没有数据,因此去数据库里找数据,然后再将查询到的数据放入缓存,之后A线程更新数据库,此时就出现了缓存和数据库中的数据不一致。

3. 延时双删

  1. 流程:先清除缓存,再更新数据库,延迟N秒之后再执行一次缓存清除操作。
  2. N秒一般要大于一次缓存写操作的时间。

操作系统

进程、线程、协程

进程:

  1. 指程序一次动态执行的过程,是资源分配的基本单位
  2. 每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信
  3. 由于占据独立的内存空间,所以上下文进程间切换开销比较大,但相对比较稳定安全。

线程

  1. 线程又叫做轻量级进程,是CPU调度的最小单位

  2. 线程从属于进程,是程序的实际执行者,一个进程可以包含多个线程。

  3. 多个线程共享所属进程的资源,同时线程也拥有自己的专属资源。

  4. 线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定

协程

  1. 一种用户态的轻量级线程,协程的调度完全由用户控制。

进程间通信

每个进程各自有不同的用户地址空间,任何一个进程的全局变量对于里另一个进程都是不可见的,所以进程之间要交换数据必须通过内核,再内核中开辟一块缓冲区,进程1把数据从用户空间拷进内核缓冲区,进程 2再从内核缓冲区把数据读取出来,这种机制叫进程间通信。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-reB0Nv3f-1659311586331)(C:\Users\LandMoon\Desktop\面试\笔记\webp)]

进程间通信7种方式

  1. 管道(实质上是一个内核缓冲区)

    • 管道是半双工的,数据只能向一个方向流动,双方通信时,需要建立起里两个管道
    • 一个进程向管道中写的内容被管道另一端的进程读取。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
    • 只能用于父子进程或者兄弟进程(具有血缘关系的进程)
  2. 命名管道(FIFO)

    • 为了克服管道只能用于具有血缘关系的进程这个缺点
    • FIFO提供了一个路径名与之相关联,以FIFO的文件形式存在于文件系统中。这样,只要可以访问该路径,两个无血缘关系的进程也能通过FIFO相互通信。
  3. 信号:信号可以在任何时候发给某个进程,而无需知道该进程的状态

  4. 消息队列

    管道这种方式需要等管道里面的数据被读完之后命令才会退出,效率很低

    消息队列就可以解决这个问题,比如A进程要给B进程发送消息,A进程将数据放进对应的消息队列之后就可以正常返回了,B进程需要的时候再去消息队列中拿数据。

    缺点是通信不及时,并且不适合大数据的传输。存在用户态和内核态之间的数据拷贝开销。

    • 存放在内核中的消息链表,每个消息队列由消息队列标识符表示。
  5. 共享内存

    开辟一块物理内存空间,各个进程将同一块物理内存空间映射到自己的虚拟地址空间中,通过虚拟地址进行访问,进而实现数据共享。

    共享内存是最快的进程间通信方式,因为没有数据拷贝的操作。

  6. 信号量

    共享内存的问题是,当多个进程同时修改同一个共享内存,很有可能冲突。

    为了防止多进程竞争共享资源造成数据错乱,所以需要保护机制,使得共享资源在任意时刻只能被一个进程访问。

  7. 套接字

Spring

常用注解

(89条消息) Spring常用注解整理_猿码天地的博客-优快云博客_spring常用注解

  1. 声明Bean:

    • @Component:泛指各种组件,包括@Controller\Service\Repository,分别对应控制层、业务层、数据访问层
  2. 注入Bean

  3. 配置类注解

    • @Configuration:声明当前类为配置类
    • @Bean:注解在方法上,声明当前方法的返回值为一个Bean
    • @ComponentScan:从给定的路径中,找到需要装配的类并自动装配到Spring的容器内。
  4. 切面相关注解

  5. Bean的属性

  6. @Value注解:注入值

Java基础

Java的理解

平台无关性、GC、面向对象(封装、继承、多态)

  1. 平台无关性能(一次编译到处运行):Java源码首先被编译成字节码,再由不同的平台jvm进行进行解析。Java语言在不同的平台上运行不需要重新编译,JVM在执行字节码时,会把字节码转换成平台上的机器指令。
  2. GC:垃圾回收机制,正是因为有了它,所以java不需要像c或c++那样手动的去释放内存

值传递

应届生/社招面试最爱问的几道Java基础问题 - 掘金 (juejin.cn)

重载、重写

  • 重载:同一个类中,方法名相同,参数类型或个数或顺序不同,比如同一个类的不同构造器
  • 重写:子类对父类方法的重新改造,外部样子不能变,内部逻辑可以改变。

重载

概念

在同一个类中,方法名相同,参数列表不同的方法为重载方法。

Java中的么一个方法,都有自己的 签名,用来确定唯一性,签名由方法名+参数列表组成

判断依据

  1. 必须是在同一个类中;
  2. 方法名相同;
  3. 方法参数的个数、顺序或者类型不同
  4. 与方法的修饰符或者返回值没有关系

如何选择调用哪一个重载方法

根据调用方法时传递的实际参数类型,并且是根据变量的静态变量来确定的。

子类是否可以重写父类的private方法

不可以,private方法是私有的,其他类不能访问,包括子类,所以在子类中可以重新实现该方法,但那是一个该类新增的方法,与父类无关,不是重写或重载。

重载和重写是在编译时确实还是在运行时确定

重载:在编译时确定,在编译过程中,编译器必须根据参数类型以及长度来确定到底调用的是哪个方法,这也是Java编译时多态的体现。

重写:在运行时确定,因为在编译时,编译器是无法知道我们能调用的是父类方法还是子类方法,只有在实际运行时,我们才知道应该调用哪个方法,这也是Java运行时多态的体现。

String的不可变性

String 内部使用char数组存储数据,并且被声明为final,不可变且不可被继承。不可变的好处:

  • 可以缓存hash值,作为HashMap的key;
  • 常量池优化:String对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用;
  • 线程安全,因为不可变,所以天生安全;
  • 作为网络连接的参数,不可变性提供了安全性。

String、StringBuffer、StringBuilder

  • 可变性:String由final修饰,不可变;后两者继承的类都没有用final修饰,所以可变;
  • 线程安全:String不可变,故安全;StringBuffer加了synchronized,所以安全,StringBuilder不安全;
  • 性能:String类型进行改变时,每次都生成一个新的String对象,然后将指针指向新的String对象,性能较低;StringBuffer每次都是对自身进行操作,性能较高;相同情况下,StringBuilder仅能比StringBuffer多获得10~15%的性能提升,却可能出现线程不安全。

静态方法内为什么不可以调用非静态成员?

由于静态方法属于类本身,在类加载的时候就会分配内存,可以直接通过类名访问,非静态成员属于类的实例,只有在类实例化之后才会分配内存,所以,如果直接调用类的静态方法时,类的非静态成员可能不存在,访问一个内存中不存在的东西会出错。

JDK、JRE、JVM

JDK > JRE > JVM

JVM:

面向对象三大特性:

封装、继承、多态

封装:将对象的属性私有化,仅对外提供访问的接口。

继承:使用已存在的类作为基础建立新类,新类可以增加新的数据或功能,但不能选择性地继承父类。方便复用已有地代码。

多态:允许将子类类型赋值给父类类型。同一个属性和方法,在父类和不同子类中可以具有不同的含义。

三个必要条件:继承、重写、向上转型

class Person {
    public void run() {
        ...
    }
}

class Student extends Person{ //继承
    @Override //重写
    public void run() {
        ...
    }
}

Person person = new Student(); //向上转型

多态

概念

程序中定义的引用变量到底指向哪个类的实例对象,其中的方法调用到底是哪个类中实现的方法,是在程序运行期间才确定的。同一个属性和方法,在父类和不同子类中可以具有不同的含义。

表现形式

重载、重写、抽象类、接口

接口和抽象类的区别

  1. 实现:接口是implements,抽象类是extends
  2. 接口中不能有方法的实现,抽象类中可以有具体实现的方法,也可以有抽象方法
  3. 只能继承一个抽象类,但是可以实现多个接口

泛型

泛型的本质是参数化类型,即给类型指定一个参数,将明确类型的工作推迟到创建对象或者调用方法时才进行。

一般在创建对象时,将位置的类型确定为具体的类型,当没有指定泛型时,默认类型为Object类型。

好处:

  • 避免了类型强转的麻烦
  • 提供了编译器的类型安全,确保在泛型类型上只能使用正确类型的对象。

通配符

  • 常见通配符

    image-20220402161857259
  • ?无界通配符

    可以用来接收任意类型数据,但此时只能接收数据,不能往其中存数据。

    // ?代表可以接收任意类型
    // 泛型不存在继承、多态关系,泛型左右两边要一样
    //ArrayList<Object> list = new ArrayList<String>();这种是错误的
    //泛型通配符?:左边写<?> 右边的泛型可以是任意类型
    ArrayList<?> list1 = new ArrayList<Object>();
    ArrayList<?> list2 = new ArrayList<String>();
    ArrayList<?> list3 = new ArrayList<Integer>();
    
  • 上界通配符<? extends E>

    只能用来接收该类型及其子类。

    //ArrayList<? extends Animal> list = new ArrayList<Object>();//报错
    ArrayList<? extends Animal> list2 = new ArrayList<Animal>();
    ArrayList<? extends Animal> list3 = new ArrayList<Dog>();
    ArrayList<? extends Animal> list4 = new ArrayList<Cat>();
    
    
  • 下界通配符<? super E>

    只能接收该类及其父类。

    ArrayList<? super Animal> list5 = new ArrayList<Object>();
    ArrayList<? super Animal> list6 = new ArrayList<Animal>();
    //ArrayList<? super Animal> list7 = new ArrayList<Dog>();//报错
    //ArrayList<? super Animal> list8 = new ArrayList<Cat>();//报错
    
    

JVM

Java内存模型(JMM)

概念

JMM规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存的是主内存中变量的副本,线程对变量的所有操作(读取、赋值等)都必须在自己的工作内存中进行,而不是直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值得传递都需要通过主内存来完成。

三个特征

  1. 原子性:一个操作不能被打断,要么全部执行,要么全部不执行。
  2. 可见性:一个线程对共享变量做了修改之后,其他的线程立即能够看到该变量的修改。
  3. 有序性:CPU是否按照既定代码顺序依次执行指令。

JVM内存结构

image-20220330172651670

包括类装载系统、执行引擎、运行时数据区、本地方法接口。

运行时数据区

程序计数器、本地方法栈、Java虚拟机栈;方法区、堆

  • 程序计数器:字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制;在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪了。唯一一个不会OOM的内存区域,生命周期与线程同步。
  • Java虚拟机栈:描述的是Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的。虚拟机栈是由一个个栈帧(包括局部变量表、操作数栈、动态链接、出口信息)组成,每一个函数调用都会有一个对应的栈帧被压入Java栈,每一个函数调用结束后,都会有一个栈帧被弹出。
  • 本地方法栈:与上一个类似,为虚拟机使用到的Native方法服务,Native方法这指的是一个Java调用非java代码的接口。
  • :所有线程共享的一块内存区域,在虚拟机启动时创建,其唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。从垃圾回收的角度,由于现在收集器基本都是采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代。新生代又可分为eden、s0、s1三部分。刚创建的对象优先存放在eden区,在经过一个新生代垃圾回收之后,如果对象还存活,则会进入s0或s1,并且对象的年龄加1,当它的年龄增加到一定程度(默认15),则会晋升到老年代中。
  • 方法区:用于存储已被虚拟机加载的类信息常量静态变量即时编译器编译后的代码等数据。

Java对象的创建过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K3LKycCc-1659311586331)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220330193305618.png)]

  1. 类加载检查:检查此对象是否被加载过
  2. 分配内存:在类加载检查之后,虚拟机将为新生对象分配内存。分配方式有”指针碰撞“(堆内存规整)和”空闲列表“(堆内存不规整)两种.
  3. 初始化零值:内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步保证了对象的实例字段在Java代码中可以不赋初值就直接使用。
  4. 设置对象头:上一步之后,虚拟机要对对象进行必要的设置,例如该对象是哪个类的实例,如何才能找到该类的元数据信息等。这些信息存放在对象头中。
  5. 执行init方法:上面工作完成后,从虚拟机来看,一个新的对象已经产生了,但从java代码来看,对象还没有init,所有的字段为零,所以一般会接着执行init方法。

对象的访问定位方式

java程序通过栈上的 reference 数据来操作堆上的具体对象。

句柄

如果使用句柄的话,那么Java堆中会划分出一块内存来作为句柄池, reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mhRdCLH6-1659311586332)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220330200816311.png)]

直接指针

如果使用直接指针,那么 reference 中存储的直接就是对象的地址

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vr9MgpdE-1659311586332)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220330200938880.png)]

这两种方式各自的优势:

  1. 使用句柄最大的好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的示例数据指针,而 reference 本身不需要修改。
  2. 使用直接指针最大的好处是速度快,节省了一次指针定位的时间开销。

堆内存中对象的分配的基本策略

堆空间的基本结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1w7QF84c-1659311586332)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220330201811872.png)]

eden、s0、s1属于新生代,tentired属于老年代,新创建的对象会优先在eden空间分配内存,经过一次新生代垃圾回收之后,如果对象还存活,则会进入s0或s1,同时年龄加1,当它的年龄增加到一定程度,就会被放入老年代中。

一些大对象和长期存活的对象会直接存放在老年代。大对象指的是,需要大量连续内存空间的Java对象,比如很长的字符串以及数组。

Minor GC 和 Full GC的不同

Minor GC:发生在新生代的GC,非常频繁,回归速度一般也比较快。当eden区满了就会发生minor gc。回收方式为:停止-复制(stop and copy)清理法,即将Eden区和一个survivor中仍然存活的对象拷贝到另一个survivor中。

Full GC(Major GC ?):发生在老年代的GC,速度较慢。在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间,如果大于,则直接触发一次Full GC;否则,查看是否设置了-XX:+HandlePromotionFailure,即是否设置了允许空间担保,如果允许,则只会进行minor GC,否则,即使老年代剩余内存足够,也会进行Full GC。清理方式为标记-整理算法,即标记出仍然存活的对象(存在引用),将所有存活的对象向一端移动,以保证内存的连续。

如何判断对象的死亡(垃圾判断方法)

对堆进行垃圾回收前的第一步就是要判断哪些对象已经死亡(既不能再被任何途径使用的对象)

引用计数法

给对象添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;当计数器为0则表示对象不能再被使用。

可达性分析算法

该算法的基本思想就是通过一系列被称为”GC Roots“ 的对象作为起点,从这些节点开始向下搜索,节点走过的路径称为引用链,当一个对象到 GC Roots没有任何引用链相连的话,则证明该对象是不可用的。

可以作为“gc roots”的对象

(1)虚拟机栈中栈针的局部变量表中引用的对象

(2)方法区中类静态变量引用的对象。

(3)方法区中常量引用的对象

(4)本地方法栈中JNI引用的对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uihq0brl-1659311586333)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220330220223167.png)]

两个相互引用的对象能被回收吗?

如果是用引用计数法不能,可达性分析法可以。

强、软、弱、虚引用

强引用

new 一个对象就是强引用,强引用永远不会被回收,即便内存不足,JVM宁愿抛出 OOM。当强引用和对象之间的关联被中断了,就可以被回收了,例如

Object o = new Object();
o = null;//将其赋值为null就可以将关联中断

软引用

当内存足够时,垃圾回收器不会回收仅具有软引用的对象;如果内存不足,就会回收这些对象的内存。软引用可用来实现内存敏感的高速缓存

MyObject aRef = new  MyObject();  
SoftReference aSoftRef=new SoftReference(aRef);  

//如果将aRef设为null,那么MyObject()对象就成了软引用对象了
aRef = null;

软引用可以和⼀个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。

JVM在分配空间时,会进行一次GC,但是这次GC并不会回收只有软引用的对象,如果JVM发现在进行了一次回收之后,内存还是不足,那么会尝试第二次GC,回收软引用的对象。

弱引用(作用)

只有弱引用的对象比只有软引用的对象生命周期更短,在JVM进行垃圾回收时,无论内存是否充足,都会回收只有弱引用的对象。不过垃圾回收器的优先级较低,因此不一定会很快发现只有弱引用的对象。

虚引用

虚引用与其他集中引用不同,其并不会决定对象的生命周期。如果一个对象仅有虚引用,那么其和没有引用一样,任何时候都可能被回收。虚引用必须和ReferenceQueue配合使用,即当回收器准备回收一个对象时,如果发现其有虚引用,那么i就会在回收对象之前,将这个虚引用加入到阈值相关联的queue中。

为一个对象设置虚引用的唯一目的是为了能在这个对象被收集器回收时收到一个系统通知。

应用:后两者使用较少,软引用较多,软引用可以加速JVM堆垃圾内存回收的速度,可以维护系统的运行安全,防止OOM

废弃常量

运行时,常量池主要回收的是废弃的常量,如果没有任何对象引用一个常量的话,就说明该常量是废弃常量。

无用的类

方法区主要回收的是无用的类。

一个无用的类要满足以下3个条件:

  • 该类所有的实例都已经被回收;
  • 加载该类的ClassLoader已被回收
  • 该类对应的 java.land.Class 对象没有在任何地方被引用

满足以上3个条件才有可能被回收,而不是一定。

垃圾回收算法

标记-清除法

首先标记出所有需要清楚的对象,标记完成后统一回收所有需要回收的对象。标记的过程就是前面所说的垃圾判定方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SML5hHEP-1659311586333)(C:\Users\LandMoon\Desktop\面试\笔记\f0cfp90yvb.png)]

主要缺点:1是效率不稳定,如果堆中存在大量需要回收的对象,那么就需要进行大量的标记和清除动作;2是容易导致内存的碎片化,当需要为大对象分配内存时容易出现因为连续内存不足而提前出发另一次垃圾回收。

复制法

将内存分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块内存中,然后一次性将使用过的内存清理掉。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B9NzoR4r-1659311586333)(C:\Users\LandMoon\Desktop\面试\笔记\n86b5pja7p.png)]

当存活对象较多时,就需要进行较多的复制操作,另外最大的缺点是可使用内存降为了原来的一半。

标记整理法

标记过程与标记-清除法一样,但是标记之后不是将可回收对象直接清理,而是让所有的存活对象都移动内存的一端,然后直接清理掉端边界以外的内存。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2LlOJgVh-1659311586334)(C:\Users\LandMoon\Desktop\面试\笔记\hrh9m1xyy2.png)]

优点是不会出现内存碎片化的问题

缺点是GC暂停的时间较长,因为需要将所有的存活对象拷贝到另一个新的地方,还需要更新其引用地址。

分代收集法

将堆分为新生代、老年代、永久代。

新生代的收集:Minor GC

老年代的收集:Major GC

永久代:用于存放静态文件(class类、方法)以及常量,主要回收两部分:废弃常量和无用的类。

HotSpot为什么要分为新生代和老年代

主要是为了提升GC效率,见Minor GC和Major GC

常见的垃圾回收器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XDFwAM2R-1659311586334)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220331164813027.png)]

垃圾回收器是回收算法的具体实现

Serial收集器

Serial(串行)是最基本、历史最悠久的收集器。这是一个单线程收集器,”单线程“不仅仅意味着他只会使用一条垃圾收集线程去回收,而且它在进行回收时,必须暂停其他所有的工作线程(Stop the World),直到回收结束。停顿很难受,但是简单,并且相较于其他收集器的单线程而言,较为高效,因为没有线程交互的开销。可以用于运行在Client模式下的虚拟机。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2afTzWeO-1659311586335)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220331165423556.png)]

ParNew收集器

Serial收集器的多线程并行(下图中有误,是并行)版本,除了使用多线程进行收集外,其余行为和Serial一致。适合运行在Server模式下的虚拟机。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F5pUhITQ-1659311586335)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220331172749937.png)]

Parallel Scavenge收集器

类似于ParNew收集器,同样是新生代收集器,基于复制法,能够并行收集的多线程收集器。

该收集器的关注点是吞吐量,吞吐量指的是处理器用于运行用户代码的时间与处理器总消耗时间的比值,比如虚拟机完成某个任务共花了100分钟,其中垃圾回收1分钟,代码运行时间99分钟,那么吞吐量就是99%。高吞吐量可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不太需要太多与用户交互的分析任务。

该收集器提供了两个参数来精确控制吞吐量:

  • 控制最大垃圾收集停顿时间:-XX:MaxGCPauseMillis

  • 直接设置吞吐量大小:-XX:GCTimeRatio

Serial Old收集器

Serial收集器的老年代版本,同样是单线程。主要两个用途:一种是在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用,另一种是作为CMS收集器发生失败时的后备方案。

Parallel Old收集器

Parallel Scavenge的老年代版本。使用多线程和“标记-整理”算法。

CMS收集器(Concurrent Mark Sweep)

与Parallel Scavenge关注点不同,它关注的是尽可能缩短垃圾回收时间用户线程的停顿时间,比较注重用户体验。第一次实现了让垃圾收集线程和用户线程(基本上)同时工作。

基于标记-清除(Mark-Sweep)算法,运作过程相对复杂,分为四个步骤:

  • 初始标记(CMS initial mark):仅标记能直接与GC Roots相关联的对象,速度很快,不过需要暂停其他线程;
  • 并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历所有可达对象(无法保证),可以和用户线程并发执行;耗时很长
  • 重新标记(CMS remark):修正并发标记过程中,因用户程序继续执行而导致标记产生变动的那一部分对象;耗时比初始标记长,但远小于并发标记
  • 并发清除(CMS concurrent sweep):清除前面标记阶段锁标记的对象,可以与用户线程并发。

两个并发过程耗时最长,在此过程中,用户线程可以并发执行,所以用户体验较好。

优点:并发收集,停顿少

缺点:

  • 对CPU资源敏感(面向并发设计的程度都如此):在并发阶段用户线程不会停顿,从而因为占用一部分线程,而导致在CPU资源不充足的情况下出现卡顿;

  • 无法处理浮动垃圾:因为在并发清除过程中,用户线程也会产生一部分可回收对象,而由于标记过程已经执行完毕,所以只能等到下一次GC时才能清理;

  • 它所使用的回收算法----“标记-清除”算法会导致收集结束时会有大量空间碎片产生。

G1(Garbage-First)收集器

面向服务器的垃圾收集器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能。

具备以下特征:

  • 并行与并发
  • 分代收集
  • 空间整合
  • 可预测的停顿

其运作分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(G1名字的由来)。

在G1出现之前,所其他的收集器收集的范围要么是整个新生代,要么是整个老年代,要么是整个Java堆,而G1可以面向堆内存里任何部分来组成回收集,衡量标准不再是它属于哪个分代,而是哪块内存存放的垃圾最多,回收收益最大。基于Region的堆内存布局是它能够实现这个目标的关键,它不再将内存分为固定的分代区域,而是将连续的Java堆分为多个大小相等的独立区域(Region),每一个区域可以根据需要扮演不同的角色对于不同的角色,收集器会采用不同的策略去处理。

能够建立可预测的停顿时间模型的原因:以Region为最小回收单位,跟踪每个Region回收所能够带来的收益,并维护一个优先级列表,在用户设定允许的收集停顿时间内,优先回收那些收益最大的Region。

CMS和G1的区别:

  • 最大区别就是内存结构不同,CMS是传统的分代,G1是Region
  • 垃圾回收方式不同,CMS使用“标记-清除”算法,会出现内存碎片,G1从整体上看是使用“标记-整理”算法,不过两个Region之间是“复制”’法,都不会带来内存碎片的问题。

类加载过程

加载---->连接---->初始化,其中连接过程又可分为三步:验证---->准备---->解析。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vTuKtAFr-1659311586335)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220331230706854.png)]

类加载器

作用:用于加载类,此外,在JVM中,每一个类都需要由其全限定名和加载它的加载器一起来确定唯一性。

分类:

  • BoostrapClassLoader(启动类加载器):最顶层的加载器,由C++实现,负责加载%JAVA_HOME%/lib目录下的jar包和类。
  • ExtensionClassLoader(扩展类加载器):主要负责加载目录%JAVA_HOME%/lib/ext下面的jar包和类
  • ApplicationClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类

双亲委派模型

双亲委派模型要求除了顶层的启动类加载器之外,其他的类加载器都要有自己的父加载器(不是继承关系,是由优先级来决定的)。

工作过程:类加载器在接收到类加载请求时,不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此最终所有的请求都会传递到最顶层的启动加载器,只有当父类加载器反馈自己无法完成加载请求时,子加载器本身才会去尝试自己加载。

image-20220401010630370

双亲委派模型代码的实现过程

先检查请求加载的类型是否已经被加载过,若没有,则调用父加载器的loadClass()方法,如果父加载器为空,则默认使用启动类加载器作为父加载器。假如父类加载器加载失败,并且抛出ClassNotFoundException异常的话,才会调用自己的findClass()方法尝试加载。

双亲委派模型的好处:避免重复加载+避免核心类篡改

好处就是Java类随着它的类加载器一起具备了带有优先级的层次关系,通过这种层次关系可以避免类的重复加载,比如父类加载器已经加载了该类时,子类加载器就不用再次加载;

此外就是保证Java核心api中定义的类不会被随意替换,比如用户自定义了一个java.lang.Object类,通过双亲委派模型传递到启动类加载器,而启动类加载器在java核心api中发现该类已被加载,则不会重新加载用户自定义的Object类,而是返回已经加载过的Object.class,这样就可以防止核心api被随意篡改。

Java容器

ArrayList

  1. 线程不安全,因其可能会涉及到多线程操作的函数都没有加锁(如add()),可能会出现java.util.ConcurrentModificationException异常。

  2. 解决方法:

    • new Vector<>(): 不推荐,因为Vector加了synchronized,所以效率低;空间满了之后,直接扩容一倍,ArrayList只是一半;Vector分配内存时需要连续的存储空间,如果数据量太大容易失败;只能在尾部进行插入和删除操作,效率低
    • Collections.synchronizedList(new ArrayList<>()):
    List<String> list = Collections.synchronizedList(new ArrayList<>());
    
    • new CopyOnWriteArrayList<>():写时复制,主要是一种读写分离思想,主要用于读多写少的情景,比如黑名单等

ArrayList和LinkedList的区别:

  1. ArrayList是基于动态数组的数据结构,LinkedList基于链表

  2. 对于随机访问get和set,ArrayList更好,因为LinkedList要移动指针

  3. 二者的CRUD时间,ArrayList要考虑到数组拷贝

  4. 空间占用:ArrayList需要预留出一部分空间,而LinkedList则是单个元素占用内存较大,因为要保存节点指针信息

ArrayList扩容

1. 初始容量为10
1. 当元素个数超过当前容量时,先预计扩容至1.5倍,如果还是比元素个数小,那么直接扩容至元素个数
1. 如果预计扩充容量大于最大容量,则直接将数组容量扩充至Integer.MAX_VALUE

HashCode和equals重写:

在hashmap中是通过hashcode决定元素放入哪个索引(桶)中,然后通过equals判断和索引中对应链表中的每个元素是否相同,如果有相同 ->不放,如果不相同 -> 放 ( jdk version >8尾插法、<8头插法)
如果不重写hashcode方法,hash值就是地址值;重写hashcode方法,hash值是根据对象中的成员变量值经过一系列的算法求得。
如果不重写equals方法,调用equals方法默认走地址值;重写equals方法后调用equals是通过成员变量值。
如果在hashmap中如果只重写了hashcode没重写equals,如果我们添加两个相同内容的User对象放入hashmap,添加完第一个User后,添加第二个User时,hash值和第一个User相同,会走equals到同一个索引中找是否存在相同的元素,此时我们因为没重写equals,比较的是地址值(两个虽然内容相同的对象地址值是不同的),因此第二个相同的User也插入到了同一个索引中。
如果在hashmap中如果只重写了equals没重写hashcode
这个最好理解,上面说到:两个虽然内容相同的对象地址值是不同的,因为没重写hashcode,hash值是地址值 ===> 两个相同内容的对象地址值不同,hash值也不同 ===> 他们两个被放到了hashmap不同的索引中。

HashMap底层实现

(87条消息) Java中HashMap底层实现原理(JDK1.8)源码分析_tuke_tuke的博客-优快云博客_hashmap底层实现原理

hashmap的容量等于数组的长度。

  1. JDK1.8之前:底层是数组+链表结合在一起,也就是散列表,如果出现hash碰撞则使用拉链法。

    拉链法:创建一个链表数组,如果遇到hash冲突,就将其加入以当前hash值为头节点的链表中。

  2. JDK1.8之后:解决hash冲突时有了较大变化,当链表长度大于阈值(默认8)时,将链表转化为红黑树,以减少搜索时间。

HashMap的长度为什么是2的幂次

  • hash值上下限很大,所以要对数组长度取余放入

  • 取余(%)操作中,如果出书是2的幂次方,则等价于与其除数减1的与(&)操作。而采用二进制的&操作比%快很多,所以能够提高效率。

    hash % length == hash & (length - 1)

HashMap扩容

初始容量、负载因子

数组扩容、rehash

多线程并发操作下的rehash会造成元素之间形成一个循环链表

HasSet的实现

基于HashMap实现,默认构造函数是构建一个容量为16,负载因子为0.75的HashMap,所有放入HashSet中的值都是由Hash Map的key来保存,hashmap的value则存储了一个PRESENT,这是一个静态的Object对象。

HashSet去重

往HashSet添加元素时,会先调用元素的hashCode方法得到hash值,如果当前hash值没有元素,那么直接插入;如果已经有元素了,接着调用该元素的equals方法。

HashSet线程不安全

解决方法: new CopyOnWriteArraySet<>(),其底层还是使用的CopyOnWriteArrayList<>()

HashSet的底层结构就是HashMap,为什么HashMap添加时需要两个值,而Hash Set只需要一个?

LinkedHashSet可以保持有序不重复

HashMap线程不安全

原理同ArrayList.

解决方法:

  1. Collections.synchronizedMap(new HashMap<>());
  2. 使用ConcurrentHashMap<>();

ConcurrentHashMap

线程安全的实现:

  1. jdk1.7:采用分段锁,对整个数组进行分段,每一把锁只锁一段数据,多线程访问不同数据端的数据时不会存在锁竞争,提高并发效率
  2. 1.8:采用Node数组+链表 +红黑树的结构,并发控制采用synchronized + CAS,只锁当前hashcode位置的链表或数根节点,这样只要hash不冲突,就不会产生并发。

JAVA并发

公平锁和非公平锁

公平锁:当一个线程来获取锁时,如果锁已被占用或者等待队列里面已经有线程在排队了,则该线程会进入队列排序,先到先得。

非公平锁:无论等待队列里是否有线程在排队,新来的线程会直接去尝试获得锁,获取不到再进入队列。

可重入锁

对于同一线程而言,外层函数获得锁之后,内层函数能够获得同一把锁

自旋锁

尝试获取锁的线程不会立即阻塞,而是采用循环的方式取尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

读写锁

类似于数据库中的读写锁

CAS

操作数

包括三个操作数:内存位置(V)、预期原值(A)、新值(B)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I1IhAzwi-1659311586336)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220418110841716.png)]

线程

1. volatile :JVM提供的轻量级同步机制

  1. 三大特性:
    • 保证可见性
    • 不保证原子性(原子性:某个线程在做某个业务时,中间不可以被加塞或者分割,要么同时成功,要么同时失败)
    • 禁止指令重排

2. synchronized和ReentrantLock的对比

  1. 二者都是可重入锁:(同)

    可重入锁的意思是,自己可以再次获取自己内部的锁。当一个线程获得了某个对象的锁,在锁释放之前,该线程可以再次获取该对象的锁,这样可以避免死锁。同一线程每次获取锁,锁的计数器+1。

  2. 底层实现上:synchronized依赖于JVM,是java关键字;而ReentrantLock依赖于API

  3. 是否手动释放:synchronized不需要手动释放锁,在执行完相应代码后会自动让线程释放对锁的占用;ReentrantLock则需要手动释放锁,一般通过lock()和unlock()结合try/finally语句块来完成,一般unlock是在finally中完成。

  4. 是否可中断:synchronized是不可中断类型的锁,正在等待的线程会一直等待下去,直到锁释放;ReentrantLock则可以中断,可以通过tryLock设置超时时间或者通过lock.lockInterruptibly()来实现这个机制,也就是说,正在等待的线程可以选择放弃等待,改为处理其他事情。

  5. 是否公平锁:synchronized为非公平锁,而ReentrantLock则可以指定公平锁还是非公平锁;公平锁就是先等待的线程先获得锁。

  6. 锁是否可绑定条件Condition:synchronized不可绑定,只能通过wait()/notify()/notifyAll()方法要么随机唤醒一个线程,要么唤醒全部线程;而ReentrantLock可以通过结合Condition实现”选择性通知“,线程对象可以注册在指定的Conditon中,而Condition实例的signalAll()方法只会唤醒注册在该Condition实例中的所有等待线程。

3. synchronized关键字和volatile的区别

  1. volatile是线程同步的轻量级实现,性能更好。不过在jdk1.6之后,synchronized效率得到了很大的提升,实际开发中synchronized使用的概率更高。
  2. volatile只能修饰变量,synchronized还可以修饰方法和代码块。
  3. 访问volatile变量时不会执行加锁操作,因此多线程访问时不会发生阻塞,synchronized则可能会出现阻塞
  4. volatile关键字能保证数据的可见性,但不能保证原子性,sync二者都可以保证
  5. volatile解决的是变量在多线程之间的可见性,sync解决的是多线程之间访问资源的同步性

4. 线程的状态

  1. 初始状态(NEW):即new了一个Tread(),此时已经有了相应的内存空间,但还处于不可运行状态。
  2. 就绪状态(RUNNABLE):创建了线程之后,调用其start()方法即可启动线程,处于就绪状态,并进入线程队列排队,此时已经具备了运行条件,需要等待CPU资源
  3. 运行状态(RUNNING):当线程就绪并且获得处理器资源时,线程开始运行,自动调用该线程的run()方法,其中定义了该线程所需要执行的一些操作。
  4. 阻塞状态(BLOCKED):(没有获取到锁)正在执行的线程在某些特殊情况下被认为挂起或需要执行耗时的输入/输出时,会让cpu暂时中止自己的执行,比如调用sleep(),suspend(),wait()等方法,只有当引起阻塞的原因被消除后,线程才会转入就绪状态。
  5. 终止状态(Terminated):线程调用stop()方法或者run()方法执行结束后,即处于死亡状态。

Java程序每次运行最少启动两个线程:一个是main线程,一个是垃圾回收线程。

5.线程代码

//用实现Runnable接口来开启线程,需要重写run方法
class MyThread implements Runnable {
    @Override
    public void run() {
        do something//这里面的东西就是线程运行时所自动执行的
    }
}

Thread t1 = new Thread(new myThread());
t1.start();//调用该方法启动

Runnable和Callable的区别:

  1. Runnable不能抛异常,C可以
  2. C有返回值,R没有

6. 创建线程的方法

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 线程池

继承和实现的比较:

  • 实现的方式可以实现多个接口,但是继承只能继承一个类
  • 实现的方式更适合处理多个线程有共享数据的情况。
  • 这几种方式都需要重写run()/call()方法。(call()可以有返回值)。开启线程时都是用start()方法 。
class MyThread1 extends Thread {
    @Override
    public void run() {
        System,out.println(111);
    }
}

class MyThread2 implements Runnable {
    @Override
    public void run() {
        System.out.println(222);
    }
}

class MyThread implements Callable {
    @Override 
	public object call() throws Exception {
        return null;
    }
}

public class ThreadDemo {
    
    public static void main(String[] args) {
        //方法一,继承
        Thread thread1 = new MyThread1();
        thread1.start();
        
        //方法二,实现Runnable
        Thread thread2 = new Thread(new MyThread2());
        thread2.start();
        
        //方法三,实现Callable,要用FutureTask对象来接收
        FutureTask futureTask = new FutureTask(new MyThread3());
        Thread thread3 = new Thread(futureTask);
        thread3.start();
    }
    
}

线程池

线程池的好处:

  1. 不需要手动重复创建线程(带来两个好处):
    • 降低资源消耗:减少线程创建和销毁带来的资源消耗
    • 提高响应速度:任务到来时不需要等待线程创建,可以直接执行
  2. 易于管理:使用线程池可以统一分配、监控、调优

线程池的种类

  1. FixedThreadPool,定长线程池
  2. SingleThreadExecutor,单线程线程池
  3. CachedThreadPool,可缓存(扩容)的线程池
  4. ScheduledThreadPool,定时的线程池,支持定时及周期性执行任务

阻塞队列的种类(BlockingQueue)

  1. ArrayBlockingQueue:由数组结构组成的有界阻塞队列
  2. LinkedBlockingQueue:由链表结构组成的有界队列(由于界限非常大,相当于无界)
  3. SynchronousQueue:单个元素的队列,生产一个消费一个

线程池的参数

  1. corePoolSize:核心线程数,线程池中的常驻核心线程数
  2. maximumPoolSize:最大线程数,线程池所能够容纳的最大线程数
  3. keepAliveTime:多余的空闲线程的存活时间,当线程池线程数量超过核心线程数时,当空闲时间达到该值后,多余的空闲线程会被销毁,直到剩下corePoolSize个
  4. unit:keepAliveTime的单位
  5. workQueue:任务队列,如果当前运行线程数到达corePoolSize数后,新任务会先存放在队列里
  6. threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程池,一般用默认
  7. handler:拒绝策略,表示当队列满了并且工作线程达到最大线程数时,如何拒绝新的任务请求

线程池的工作过程

  1. 首先创建线程池,等待任务请求

  2. 当执行execute()方法添加了一个请求时,线程池会进行如下判断:

    • 当前工作线程数是否已经达到corePoolSize,如果没有则运行该任务
    • 如果达到就将该任务放入缓存队列;
    • 如果缓存队列已经满了,并且当前工作线程数没有达到maxPoolSize,那么就会创建非核心线程来运行当前任务;
    • 如果队列已经满了并且工作线程数已经达到maxPoolSize,那么线程池就会启动饱和拒绝策略
  3. 当一个线程完成任务时,会从队列中取出下一个任务来执行

  4. 当一个线程空闲时间达到KeepAliveTime时,如果当前工作线程数量大于corePoolSize,那么该线程会被停掉;所有的任务完成后,线程池中的数量最终会收缩到corePoolSize的大小

拒绝策略

  • AbortPolicy: 默认,丢弃当前任务,并抛出异常
  • DiscardPolicy:直接丢弃任务,不抛出异常
  • CallerRunsPolicy:既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后将当前任务加入队列中,与第二种的区别在于,第二种是抛弃当前任务,这一种是抛弃等待最久的

不使用Executors创建线程池

原因:

  1. FixedThreadPool和SingleThreadPool:

    允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量请求,导致OOM

  2. CachedThreadPool和ScheduledThreadPool:

    允许的创建线程数量为Integer.MAX_VALULE,可能会创建大量请求,从而导致OOM

手写线程池——ThreadPoolExecutors

final int corePoolSize = 2;
final int maxPoolSize = 5;
final Long keepAliveTime = 1L;
final int queueCapacity = 3;

ExecutorService threadPool = 
    new ThreadPoolExecutors(corePoolSize, 
                            maxPoolSize, 
                            keepAliveTime, 
                            TimeUnit.SECONDS, 
                            new LinkedBlockingQUEUE<Runnable>(queueCapacity), 
                            EXecutors.defaultThreadFactory(), 
                            new ThreadPoolExecutor.AbortPolicy());

class MyThread impplements Runnable {
    @Override
    public 	void run() {
        do....
    }
}

try {
      threadPool.execute(new MyThread());
} catch (Exception e) {
    e.printStackTrace();
} finally {
    threadPool.shutdown();
}

线程池参数的设置

  • CPU密集型:该任务需要大量地占用CPU,CPU的压力较大,因此尽可能设置小点

    corePoolSize = CPU核数 + 1

  • IO密集型:需要大量地进行IO, CPU经常处于等待IO的状态,因此需要尽可能设置大点

    corePoolSize = CPU核数 * 2

    或corePoolSize = CPU核数 / (1 - 阻塞系数),阻塞系数一般在0.8~0.9

死锁

概念

两个或多个并发进程在执行过程中,因争夺对方资源而造成互相等待的现象

产生的必要条件

  1. 互斥:一个资源一次只能被一个进程使用
  2. 占有并等待:一个进程正占有至少一个资源,并在等待被其他进程占有的资源
  3. 非抢占:已经分配给一个进程的资源不能被其他进程抢占,只能等其执行完毕并自愿释放
  4. 循环等待:若干个进程之间形成一种首尾相接的资源等待关系,环路中每个进程都在等待下一个进程的资源

AQS

原理

AQS是可以给我们实现锁的一个框架,如果被请求的共享资源空闲,那么当前请求资源的线程被设置为有效工作线程,并且将该共享资源设置为占用状态;如果被请求的资源处于被占用状态,那么该线程就会放入到一个队列中,等待资源的释放。

内部实现主要维护了一个FIFO的CLH队列和一个标志资源状态的state变量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BGv7Pba9-1659311586336)(C:\Users\LandMoon\Desktop\面试\笔记\image-20220326112210498.png)]

设计模式

1. 单例模式

单例模式详解 - 知乎 (zhihu.com)

单例模式五种实现 - SegmentFault 思否

  1. 介绍
  • 某个类只能有一个实例:构造器私有化
  • 它必须自行创建这个实例:含有一个该类的静态变量来保存这个唯一的实例
  • 它必须自行向整个系统提供这个实例:对外提供获取该实例对象的方式:直接暴露、用静态变量的get方法获取
  1. 实现:

    1. 将类的构造方法定义为私有方法。这样其他类的代码就无法通过调用该类的构造方法来实例化该类的对象,只能通过该类提供的静态方法来得到该类的唯一实例。
    2. 定义一个私有的类的静态实例。
    3. 提供一个公有的获取实例的静态方法
  2. 常见形式:

    • 饿汉式:线程安全、非懒加载、效率高,但可能造成内存浪费
    • 懒汉式:线程安全、懒加载,但由于用synchronized修饰效率低
    • 双重检查锁:线程安全、懒加载、虽然用了synchronized,但是由于只有第一次初始化时才进行同步,所以不会有效率上的问题。
    • 静态内部类
    • 枚举式
  3. 饿汉式:不管需不需要,直接创建对象,不存在线程安全问题 。

    public class Singleton1 {
        //用一个静态变量来保存
        public static Singleton1 singleton1 = new Singleton1();
        
        //构造器私有化
        private Singleton1() {}
        
        //对外提供获取的接口
        public static Singleton1 getSingleton1() {
            return singleton1;
        }
        
    }
    
  4. 懒汉式:延迟创建对象,(使用时才创建)

    public class Singleton2 {
        
        //先不创建实例
        public static Singleton2 instance;
        
        //构造器私有化
        private Singleton2() {}
        
        public static synchronized Singleton2 getInstance() {
            if (instance == null) {
                instance = new Singleton2();
            }
            
            return instance;
        }
        
    }
    
  5. 双重检查锁:

    public class Singleton3 {
        
        public static volatile Singleton3 instance;
        
        private Singleton3() {}
        
        public static Singleton3 getInstance() {
            if (instance == null) {
                synchronized (Singleton3.class) {
                    if (instance == null) { //这一步是为了防止多个线程同时进来
                        instance = new Singleton3();
                    }
                }
            }
            
            return instance;
        }
        
    }
    
  6. 静态内部类:线程安全、懒加载、效率高

    public class Singleton4 {
        
        private static class InstanceHolder {
            private static Singleton4 instance = new Singleton4();
        }
        
        private Singleton4() {}
        
        public Singleton4 getInstance() {
            return InstanceHolder.instance;
        }
        
    }
    

    外部类加载时并不会立即加载内部类,内部类不被加载则不会去初始化instance,因此不占内存,实现了懒加载。

    保证线程安全和唯一性的原理:

    虚拟机会保证一个类的()方法(初始化)在多线程环境中被正确地加锁、同步,当多个线程同时去初始化一个类,只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。其他线程唤醒之后不会再次进入()方法,因此只会初始化一次。

  7. 枚举类:

    public enum Singleton{
        INSTANCE;
        public void do() {};
    }
    
    //调用
    Singleton.INSTANCE.do();
    

Spring

spingboot四大组件

starter

starter能够抛弃以前繁琐的配置,将其统一集成进starter,我们只需要在maven中引入starter依赖,springboot就能自动扫描到要加载的信息并启动相应的默认配置,能自动通过classpath路径下的类发现需要的bean,并注入ioc容器。

TODO

  1. 多线程会带来的问题

    可见性问题

    原子性问题

    有序性问题

  2. HashMap查找的时间复杂度:O(1)

  3. HashMap的扩容

  4. JMM

  5. 创建多个线程的方式

  6. 优先队列

  7. 布隆过滤器

d Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
}

       return instance;
   }

}


5. 双重检查锁:

~~~java
public class Singleton3 {
    
    public static volatile Singleton3 instance;
    
    private Singleton3() {}
    
    public static Singleton3 getInstance() {
        if (instance == null) {
            synchronized (Singleton3.class) {
                if (instance == null) { //这一步是为了防止多个线程同时进来
                    instance = new Singleton3();
                }
            }
        }
        
        return instance;
    }
    
}
  1. 静态内部类:线程安全、懒加载、效率高

    public class Singleton4 {
        
        private static class InstanceHolder {
            private static Singleton4 instance = new Singleton4();
        }
        
        private Singleton4() {}
        
        public Singleton4 getInstance() {
            return InstanceHolder.instance;
        }
        
    }
    

    外部类加载时并不会立即加载内部类,内部类不被加载则不会去初始化instance,因此不占内存,实现了懒加载。

    保证线程安全和唯一性的原理:

    虚拟机会保证一个类的()方法(初始化)在多线程环境中被正确地加锁、同步,当多个线程同时去初始化一个类,只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。其他线程唤醒之后不会再次进入()方法,因此只会初始化一次。

  2. 枚举类:

    public enum Singleton{
        INSTANCE;
        public void do() {};
    }
    
    //调用
    Singleton.INSTANCE.do();
    

Spring

spingboot四大组件

starter

starter能够抛弃以前繁琐的配置,将其统一集成进starter,我们只需要在maven中引入starter依赖,springboot就能自动扫描到要加载的信息并启动相应的默认配置,能自动通过classpath路径下的类发现需要的bean,并注入ioc容器。

TODO

  1. 多线程会带来的问题

    可见性问题

    原子性问题

    有序性问题

  2. HashMap查找的时间复杂度:O(1)

  3. HashMap的扩容

  4. JMM

  5. 创建多个线程的方式

  6. 优先队列

  7. 布隆过滤器

  8. sort

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值