mysql优化

联合索引的最左前缀匹配原则

b+树是按照从左到右的顺序建立搜索树的,检索数据的时候,b+树会优先比较最左侧字段来确定下一步的比较方向,如果不适用最左前缀,就会导致b+树不知道该查哪个节点,从哪里开始寻找,导致没有用到索引。

1、概述

1.1、为什么要优化

  • 一个应用吞吐量瓶颈往往体现在数据库的数据处理速度
  • 随着应用的用户增多和使用过程中数据积累,数据库数据逐渐增多,数据库处理压力变大
  • 关系型数据库是存储在磁盘中,查询时需要经过多次io,导致读写速度较慢

1.2、如何让优化

  • 从表和字段的设计方面,考虑使用更优的存储和计算,例如使用可变长度或者定长字段中较短的,或者使用notnull字段
  • 数据库自身提供的优化功能,例如索引
  • 横向扩展,主从复制,读写分离,负载均衡和高可用
  • 典型的sql语句优化,收效甚微
  • 使用join代替子查询
  • 使用union代替手动创建的临时表
  • 事务处理

2、字段设计

2.1、典型方案

2.1.1、对精度有要求

2.1.2、尽量使用整形表示字符串

2.1.3、尽可能使用not null

2.1.4、定长和非定长的选择

2.1.5、其他

  • 字段数不要过多
  • 字段注释是必要的
  • 字段明明见名思意
  • 预留字段作为备用,以备扩展

2.2、三大范式

  • 第一范式:字段的原子性,关系型数据库固有的属性
  • 第二范式:消除主键的部分依赖,主键可能不只有一个------使用一个和业务无关的字段作为主键
  • 第三范式:消除对主键的传递依赖,高内聚,商品表分为商品简略信息表和商品详情表

3、存储引擎的选择

3.1、功能差异

Innodb支持行级锁,支持事务和外键

image-20210414104512256

3.2、存储差异

  • 存储方式,MyISAM的数据和索引是分开存储
  • 表可移动性 Innodb不可移动
  • 碎片空间 Innodb不产生
  • 有序存储 按照主键有序插入

3.3、选择依据

  • 读多写少
  • 读多写多
    • 支持事务和外键,保证数据一致性和完整性
    • 并发能力强,支持行级锁

4、索引

4.1、什么是索引

对数据进行排序,方便进行查询。

4.2、索引类型

普通索引,唯一索引,主键索引,全文索引

4.3、索引管理语法

  • 查看索引 show create table 表名desc 表名
  • 建立索引
  • 删除索引

4.4、执行计划explain

4.5、索引使用场景

  • where
  • order by
  • join
  • 索引覆盖

4.6、语法细节

4.7、索引存储结构

  • hash索引
  • b+树索引
    • 最左匹配原则

5、查询缓存

6、分区

  • 默认情况下,一张表对应一个存储文件,当存储文件数据量较大时,需要将存储文件岔开,分到多组存储文件,保证单个文件处理效率
  • partition by 分区函数(分区字段)(分区逻辑)
    • hash-分区字段为整形
    • key-分区字段为字符串
    • range-基于比较,只支持less than
    • list-基于状态值
  • 分区管理
    • 创建时分区
    • 修改表结构
  • 分区字段应该选择常用字段否则分区意义不大

7、水平分割和垂直分割

  • 水平分割,多张表结构相同的表存储同一类型数据,单独一张表要保证id唯一性
  • 垂直分割,分割字段到多张表,这些表记录是一一对应的

8、集群

8.1、主从复制

8.2、读写分离

  • 使用原生的javax.sql.Connection
  • 借助SpringAOP和Aspect实现数据源动态转换

8.3、负载均衡

  • 轮询
  • 加权轮询
  • 根据负载情况

8.4、高可用

为单机服务提供一个冗余机

  • 心跳检测
  • 虚IP
  • 主从复制

9、典型sql

10、慢查询日志

11、profile

12、典型服务器配置

13、压测工具

image-20210416162211233

携程面试准备

作者:牛客461209969号
链接:https://www.nowcoder.com/discuss/622602?type=post&order=time&pos=&page=1&channel=-1&source_id=search_post_nctrack
来源:牛客网

1.项目中为什么会选择redis
2.如果数据库请求量很大,怎么处理(我说redis缓存,他说缓存也不能把所有数据缓存进去,我说缓存热点数据,他说如果我不查热点数据呢,然后引导让我用算法解决,我回答了lru并讲解了lru算法)

  • 数据预热

    场景:服务器启动后迅速宕机
    大量请求过来,需要在缓存中获取数据,缓存中又没有,从而去数据库找,然后再将数据存入缓存,短时间内高强度操作redis导致出现问题
    解决方案:系统启动前,提前将相关的缓存数据直接加载到缓存系统,避免用户请求的时候,先查询数据库,然后再将数据缓存的问题。

  • 将过期时间散列

  • 分布式锁

  • 缓存穿透:黑客查询redis和mysql中不存在的数据,绕过了redis

    • 布隆过滤器,多次哈希
    • 缓存空数据,需要内存—设置较短的过期时间

数据库中的请求很大

缓存雪崩:

**
大量请求过来,短时间范围内,大量的key集中过期,

瞬间过期数据量太大,导致对数据库服务器造成压力
避免key过期时间集中,可以有效解决雪崩现象(约40%)。
其它策略:
1.限流,降级处理:短时间范围内牺牲一些客户体验限制一部分请求访问降低应用服务器压力,待业务低速运转后再逐步放开访问。
2.根据业务数据进行分类错峰,A类90分钟。B类80分钟,C类70分钟。过期时间使用固定时间+随机值的形式,稀释集中到期的key的数量。
3.对mysql严重耗时业务进行优化,对数据库瓶颈进行排查,例如超时查询、耗时较高事务
**
缓存击穿:

**
单个key形成高热数据,但是这个key过期

缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中redis后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。
应对策略:
1.以电商为例,对于主打商品,在活动期间,加大对此类信息key的过期时长。
2.现场调整,监控访问量,对自然流量激增的数据延长过期时间设置为永久性的key。
3.启动定时任务,高峰期来临之前,刷新数据有效器,确保不丢失。
**
缓存穿透:

**
访问不存在的数据,跳过了合法数据的redis数据缓存阶段,每次访问数据库,导致对数据库造成压力,一般是属于黑客攻击造成。
应对策略:
1.缓存null,对查询结果为null的数据进行缓存,长期使用,定期清理,设定短时限,例如30到60秒,最高5分钟
2.白名单策略(效率很低)。
3.使用布隆过滤器,将数据库中所有的查询条件放入布隆过滤器中,当一个查询请求过来时,先经过布隆过滤器筛选,如果判断请求查询值存在则继续查,如果不存在就直接丢弃。

img在这里插入图片描述
假设集合里面有3个元素{x, y, z},哈希函数的个数为3。首先将位数组进行初始化,将里面每个位都设置位0。对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中,这里就不分析存在误判的情况了

redis的伪代码

String get(String key) {
    String value = redis.get(key);     
    if (value  == null) {
        if(!bloomfilter.mightContain(key)){
            return null; 
        }else{
            value = db.get(key); 
            redis.set(key, value); 
        }    
    }
    return value;
}

//应对大量的请求,Nginx限流处理,做负载均衡

3.讲一下springCloud和dubbo的区别
(我回答了两个的特性)

mysql优化

1.MySQL数据库作发布系统的存储,一天五万条以上的增量,预计运维三年,怎么优化?

a. 设计良好的数据库结构,允许部分数据冗余,尽量避免join查询,提高效率。
b. 选择合适的表字段数据类型和存储引擎,适当的添加索引。
c. mysql库主从读写分离
d. 找规律分表,减少单表中的数据量提高查询速度。
e.添加缓存机制,比如memcached,apc等。
f. 不经常改动的页面,生成静态页面。
g. 书写高效率的SQL。比如 SELECT * FROM TABEL 改为 SELECT field_1, field_2, field_3 FROM TABLE.

2.实践中如何优化MySQL

最好是按照以下顺序优化:

  1. SQL语句及索引的优化

  2. 数据库表结构的优化

  3. 系统配置的优化

  4. 硬件的优化

3.优化数据库的方法

  1. 选取最适用的字段属性,尽可能减少定义字段宽度,尽量把字段设置NOTNULL,例如’省份’、’性别’最好适用ENUM
  2. 使用连接(JOIN)来代替子查询
  3. 适用联合(UNION)来代替手动创建的临时表
  4. 事务处理
  5. 锁定表、优化事务处理
  6. 适用外键,优化锁定表
  7. 建立索引
  8. 优化查询语句

4.如何通俗地理解三个范式?

答:

第一范式:1NF是对属性原子性约束要求属性具有原子性,不可再分解

第二范式:2NF是对记录惟一性约束,要求记录有惟一标识,即实体的惟一性

第三范式:3NF是对字段冗余性的约束,即任何字段不能由其他字段派生出来,它要求字段没有冗余。。

范式化设计优缺点:

优****点:

可以尽量得减少数据冗余,使得更新快,体积小

缺点:对于查询需要多个表进行关联,减少写得效率增加读得效率,更难进行索引优化

反范式化:

优点:可以减少表得关联,可以更好得进行索引优化

缺点:数据冗余以及数据异常,数据得修改需要更多的成本

5.说说对SQL语句优化有哪些方法?(选择几条)

(1)Where子句中:where表之间的连接必须写在其他Where条件之前,那些可以过滤掉最大数量记录的条件必须写在Where子句的末尾.HAVING最后。

(2)用EXISTS替代IN、用NOT EXISTS替代NOT IN。

(3) 避免在索引列上使用计算

(4)避免在索引列上使用IS NULL和IS NOT NULL

(5)对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

(6)应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描

(7)应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描\

Myisam和innodb的比较

  • 锁的范围,精细程度上。

Myisam表是表级锁。 Innodb是行级锁。

表级锁: 开销小,加锁时间短。

行级锁:相反。

Innodb:数据完整性,并发性好。事务,且是默认表引擎

适合银行转账,等数据安全要求高的应用

Myisam:压缩存储,适合 insert和select多的应用,博客,BBS等
高速并发插入,
压缩:
压缩后的数据
压缩后,不能再对表进行写操作
解压缩

让请求失败变高,面试官问,这种情况怎么处理(这个我说我不知道)

Mysql锁

悲观锁,正如其名,它指的是对数据被外界(包括当前系统的其它事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排它性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

锁的优化策略

  1. 读写分离
  2. 降低锁的持有时间
  3. 分段加锁
  4. 多个线程尽量以相同的顺序去获取资源,不能将锁的粒度过于细化,不然可能导致加锁释放锁的次数过多,效率反而不好如加一把大锁

第二部分 乐观锁

1 概念

1.1 理解方式一(来自网上其它小伙伴的博客)

乐观锁认为一般情况下数据不会造成冲突,所以在数据进行提交更新时才会对数据的冲突与否进行检测。如果没有冲突那就OK;如果出现冲突了,则返回错误信息并让用户决定如何去做。

1.2 理解方式二(来自网上其它小伙伴的博客)

乐观锁的特点是先进行业务操作,不到万不得已不会去拿锁。乐观地认为拿锁多半会是成功的,因此在完成业务操作需要实际更新数据的最后一步再去拿一下锁。

1.3 我的理解

理解一:就是 CAS 操作

理解二:类似于 SVN、GIt 这些版本管理系统,当修改了某个文件需要提交的时候,它会检查文件的当前版本是否与服务器上的一致,如果一致那就可以直接提交,如果不一致,那就必须先更新服务器上的最新代码然后再提交(也就是先将这个文件的版本更新成和服务器一样的版本)

2 如何实现乐观锁呢

首先说明一点的是:乐观锁在数据库上的实现完全是逻辑的,数据库本身不提供支持,而是需要开发者自己来实现。

常见的做法有两种:版本号控制及时间戳控制。

版本号控制的原理:

  • 为表中加一个 version 字段;
  • 当读取数据时,连同这个 version 字段一起读出;
  • 数据每更新一次就将此值加一;
  • 当提交更新时,判断数据库表中对应记录的当前版本号是否与之前取出来的版本号一致,如果一致则可以直接更新,如果不一致则表示是过期数据需要重试或者做其它操作(PS:这完完全全就是 CAS 的实现逻辑呀~)

至于时间戳控制,其原理和版本号控制差不多,也是在表中添加一个 timestamp 的时间戳字段,然后提交更新时判断数据库中对应记录的当前时间戳是否与之前取出来的时间戳一致,一致就更新,不一致就重试。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vhdCV46n-1618927777804)(C:\Users\ZSZ_\AppData\Roaming\Typora\typora-user-images\image-20210405111845822.png)]

1.自我介绍
2.mySQL索引B+Tree的理解,说一下
  • b+树只有叶子节点存放数据,由于树又宽又矮,可以减小io次数提高效率
3.直接开始码算法

a.删除链表重复元素

双指针

b.判断两颗二叉树是否相同

递归,迭代,深度优先,广度优先

c.字符串中第一次重复出现的字符

三道题都比较简单,a、b秒了,但是输入样例写不出来(还是用不习惯面试平台的问题,大家刷leetcode的一定要抽出时间刷刷牛客这种OJ)

写第二道的时候面试官让我写一下输入样例测试一下,就卡在这了一直没出来, 没做第三题,面试官说时间不太够了,就停下了。

4.详细说一下tcp协议?

给说了tcp连接的特点,三次握手四次挥手,为什么需要进行“三次”握手,四次挥手为什么多一次。

5.说一下从浏览器打开一个链接中间发生了什么?

从DNS域名解析说到HTTP说到TCP说到浏览器显示,blabla。。

1.DNS解析
2.建立TCP连接,发送HTTP请求
3.服务端处理请求并返回HTTP响应
4.浏览器解析渲染页面
5.关闭连接

1. DNS解析

回车敲响的那一刻,浏览器检查了输入框,www.didudidudu.com是什么鬼东西??我需要的可是IP地址呀!万般无奈之下找向了浏览器缓存,让其查找是否有这家伙的记录,结果并没有发现,此时找向系统缓存,主要去查找了系统中的hosts文件,同样没有,此时找向路由器缓存,查看路由器映射表,然而,并没有!于是,计算机将域名发给了本地DNS服务器(提供本地连接的服务商),本地DNS服务器找不到会将域名发送给其他服务器,进行递归过程,首先会发送到根域名服务器去找,返回顶级域名服务器的IP地址,再请求顶级域名服务器IP返回二级域名服务器IP,再请求二级域名服务器IP返回三级域名服务器IP…直到找到对应的IP地址,返回给浏览器。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gn0fXrc0-1618927777805)(mysql优化.assets/20190908184338232.png)]

img

2. 建立TCP连接

在这里插入图片描述

TCP/IP的建议需要经历三次握手

在这里插入图片描述 TCP的三次握手还是经常被问到的,这里概述一下

第一次握手:客户端A将标志位SYN置为1,随机产生一个值为seq=J(J的取值范围为=1234567)的数据包到服务器,客户端A进入SYN_SENT状态,等待服务端B确认;

第二次握手:服务端B收到数据包后由标志位SYN=1知道客户端A请求建立连接,服务端B将标志位SYN和ACK都置为1ack=J+1,随机产生一个值seq=K,并将该数据包发送给客户端A以确认连接请求,服务端B进入SYN_RCVD状态

第三次握手:客户端A收到确认后,检查ack是否为J+1ACK是否为1,如果正确则将标志位ACK置为1ack=K+1,并将该数据包发送给服务端B,服务端B检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端A和服务端B进入ESTABLISHED状态,完成三次握手,随后客户端A与服务端B之间可以开始传输数据了。

服务端处理请求并返回HTTP响应

服务端拿到Http请求处理后,再返回HttP响应,这里主要涉及到一部分Http的常见知识点,如各种状态码,各种Header的含义。把常见的状态码,常见的Header了解一下就行了。Http相关的知识前面的文章已经介绍过了,就不重复介绍了。

浏览器解析渲染页面

这部分内容偏前端一点。大家可以参考相关博问,后端几乎很少问。

关闭连接

这里要注意的一点是一个TCP连接是可以发送多个Http请求的,不是发送一次Http请求TCP连接就断了。默认情况下建立 TCP 连接不会断开,只有在请求报头中声明 Connection: close 才会在请求完成后关闭连接。这里又涉及到TCP四次挥手

6.反问环节

经典技术栈哈哈,面试官说用的java(果然)。到这面试就基本结束了,总的来说面试官非常好,面试体验也可以,许愿二面啊啊啊啊啊啊啊啊啊

作者:小姬炖蘑菇
链接:https://www.nowcoder.com/discuss/622549?type=post&order=time&pos=&page=1&channel=-1&source_id=search_post_nctrack
来源:牛客网

携程 后端开发 一面 (已过)

1.数据库三大范式,如果某些表在后期维护中违反了三大范式应该怎么处理?

2.jvm结构

3.jvm调参

4.线程池怎么理解,(单线程池,固定数量线程池,缓存线程池)应用场景

5.redis数据结构,持久化方法,过期策略

6.场景题:三本书,每本页码为1,1,1,2,2,2,。。。,。。。三本书共300页,如何分成三本,每本页码为1,2,3,。。。,100

7.场景题:N张车票,求最短路径

最短路径使用

深度优先,选取一个点,找到最短的边,把另一个顶点放入集合中,寻找相连的最小的边,。。。。,n各顶点n-1条边

广度优先

8.序列化的方法,如果有字段不需要被序列化呢?

携程 后端开发 二面 (已过)

1.项目

2.遇到最大的挑战

3.线程池

4.TCP三次握手四次挥手

5.报文超时会怎样(用Java写,没太懂他的意思,不会。。)

6.单例模式

7.手写一个二分查找(一个有重复数组中找到第一个出现的目标数)

8.开放式问题:多种交通方式,找回家路径

9.你的优缺点

10.表单重复提交问题的解决

11.hashset

作者:coco201903192152680
链接:https://www.nowcoder.com/discuss/586465?type=post&order=time&pos=&page=1&channel=-1&source_id=search_post_nctrack
来源:牛客网

一面(11月底):

1,写一个前缀树,要实现的方法包括put、search、startWith。(基本写出来了)

2,一个很大的无序数组,找出其中最小的k个数。时间复杂度。(只需要说思路,我具体说了用快速排序里面的那种思想)

很大的无序数组,比较排序

3,垃圾收集算法

复制算法

标记清楚

标记整理

分代垃圾收集

  • 新生代 复制算法
  • 老年代 标记整理

分区垃圾收集

4,老年代中有对象依赖年轻代中对象,怎么判断年轻代中的对象是否需要被回收。

根路径搜索,root可达性分析

5,B+树作为索引的结构比其他数据结构好在哪,比如二叉树哈希表、B树。

6,Linux操作系统中的调度算法

反问:做哪方面的业务?项目是web项目还是app?

一面问题我基本都回答出来了。因为我准备的很充分。

二面(12月初):
1,自我介绍:问了项目(我没准备项目,所以就说了我研究生论文发表做的项目
2,怎么想到用fork/join框架去做的,实际开发中都没人用(我项目中用到的,所以问了)
3,多线程安全问题
4,JVM中一个对象从创建到被回收所经历的整个过程
5,平时自己是怎么学习的
6,对于自己的职业生涯规划怎么想的
7,了解哪些设计模式,说一下单例模式
8,事务的隔离级别
9,连接数据库的操作怎么做的
反问:对于后台开发技术栈除了JAVA、数据库等,我还需要学习哪些知识?

面试官都挺好的,对于我不太清楚的都会引导我去说,所以每个问题我基本都能说出一些。但是对于一些后台相关的知识点问题,我说不知道的就不问了。我也不记得是些啥问题了所以没贴。

所以 很多问题都是在特定的环境下提出来的,看面经的时候要甄别,不能看到很多问题貌似都不知道就否定自己

作者:Mr.Fate
链接:https://www.nowcoder.com/discuss/571226?type=post&order=time&pos=&page=1&channel=-1&source_id=search_post_nctrack
来源:牛客网

1…哪些线程安全的list

Vector CopyAndWriteLinkedList

2.线程不安全的list会导致什么问题

并发操作时,会发生数据不一致

3.什么是深拷贝与浅拷贝

4.用java实现一下

5.项目

6.用过哪些框架

7.jvm的内容

7.算法题,将一个数组中正数放左边,零放中间,负数放右边。利用快排思想排两次

作者:菜鸟程序员小范
链接:https://www.nowcoder.com/discuss/571096?type=post&order=time&pos=&page=1&channel=-1&source_id=search_post_nctrack
来源:牛客网

1)二分查找(mid放错了位置 😂),结果搞了好久

(2)hashmap底层

(3) 详细描述一下put操作

(4)put操作怎么判断节点与目标节点的key是不是相等的

hashcode 和 equals

(5)==和equals有什么区别

(6)hashmap线程安全吗

不安全 currentHashmap线程安全

(7)线程的可见性

可见性:一个线程对共享变量值的修改能够及时地被其他线程看到

所有变量都存储在主内存中(分配给进程的内存);
每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(即主内存中该变量的一份拷贝);
主内存主要对应于java堆中对象的实例数据部分,而工作内存则对应于虚拟机栈中的部分区域;
从更底层来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存。

JMM的两条规定:
1.线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
2.不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成

语言层面上实现可见性
1.synchronized
2.volatile
3.final

共享变量可见性实现的原理:
线程1对共享变量的修改要想被线程2及时看到,必须经过如下2个步骤:
1.把工作内存1中更新过的共享变量刷新到主内存中;
2.将主内存中最新的共享变量的值更新到工作内存2中

语言层面上实现可见性
1.synchronized
2.volatile
3.final

JMM关于synchronized的两条规定:
1.线程加锁时会清空当前工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值
2.线程解锁时必须把共享变量的最新值刷新到主内存中

线程执行互斥代码的过程:
1.获得互斥锁
2.清空工作内存
3.从主内存拷贝变量的最新副本到工作内存
4.执行代码
5.将更改后的共享变量的值刷新到主内存
6.释放互斥锁

重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而作的优化,但是多线程中代码交错执行时,重排序可能会造成内存可见性问题
1.编译器优化的重排序(编译器优化)
2.指令级并行重排序(处理器优化)
3.内存系统的重排序(处理器优化)

as-if-serial:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(java编译器、运行时和处理器都会保证java在单线程下遵循as-if-serial语义)
有数据依赖的语句不会进行重排序,比如a=1,b=2,sum=a+b;那么a=1和b=2可以重排序,但是sum一定得在a=1和b=2之后执行。
if(ready) { result = number3; },这句可以进行重排序,排序结果:int mid = number3; if(ready) { result = mid; }

导致共享变量在线程间不可见的原因:
1.线程的交叉执行
2.重排序结合线程交叉执行
3.共享变量更新后的值没有在工作内存与主内存间及时更新

volatile如何实现内存可见性:通过加入内存屏障和禁止重排序优化来实现的
对volatile变量执行写操作时,会在写操作后加入一条store屏障指令
对volatile变量执行读操作时,会在读操作前加入一条load屏障指令
通俗地讲,volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当变量的值发生变化时,又会强迫线程将最新的值刷新到主内存,这样就能保证在任何时刻,不同线程总能看到该变量的最新值。

// 如果还有子线程在运行,主线程就让出CPU资源,直到所有子线程都运行完了,主线程再继续执行while(Thread.activeCount() > 1) { Thread.yield(); }

volatile不保证原子性
保证自增操作的原子性:
1.使用synchronized关键字
2.使用java.util.concurrent.locks包下的ReentrantLock

3.使用java.util.concurrent.atomic包下的AtomicInterger

在多线程中安全使用volatile变量的条件:
1.对变量的写操作不依赖其当前值
不满足:i++、count+=5
满足:boolean变量
2.一个表达式中不能同时含有两个或两个以上的volatile变量
不满足:不变式low < up

问:即使没有保证可见性的措施,很多时候共享变量依然能够在主内存和工作内存间得到及时的更新?
答:一般只有在短时间内高并发的情况下才会出现变量得不到及时更新的情况,因为CPU在执行时会很快刷新缓存,所以一般情况下很难看到这种问题,也就是说线程不安全的程序不一定会出现问题,而且有些线程安全问题很难重现。

对64位(long、double)变量的读写可能不是原子操作:
java内存模型允许JVM将没有被volatile修饰的64位数据类型的读写操作划分为两次32位读写操作
导致问题:有可能会出现读取到“半个变量”的情况
解决方法:加volatile关键字

很多商用的虚拟机下,把64位变量当作一个原子操作了,不用刻意的去用处理了。

(8)单例模式,解释重点的关键字(getInstance()方法上的sychronized没搞懂什么意思 😪 😪)

(9)你有什么要问我的吗

高频面试题

JVM

Jvm内存结构

Jvm内存结构,一般是面试官对Java虚拟机这块考察的第一问。讲真,还没背会,自己罚自己面壁思过。

Java虚拟机的内存结构一般可以从线程共有和线程私有两部分起头作答,然后再详细说明各自的部分,类似树状结构的作答,好处就是思路清晰,面试官听着也舒服。

线程共有的包括Java堆和方法区,线程私有的包括虚拟机栈、本地方法栈和程序计数器。这些内容回答一遍后,就可以开始详细叙述每个点的详细部分。

Java堆是用于存放Java程序运行时所需的对象等数据,Java堆又分为新生代和老年代。我们平常所说的垃圾回收,主要回收的就是堆区。更细一点划分新生代又可划分为Eden区和2个Survivor区(From Survivor和To Survivor)。

方法区中最为重要的是类的类型信息、常量池、域信息、方法信息。总之,方法区保存的信息,大部分来自于 class 文件,是 Java 应用程序运行必不可少的重要数据。

程序计数器用于存放下一条运行的指令,这里是唯一无内存溢出的区域。如果当前程序正在执行一个Java方法,则程序计数器记录正在执行的Java字节码地址,如果当前线程正在执行一个Native方法,则程序计数器为空。

虚拟机栈和本地方法栈用于存放函数调用堆栈信息。虚拟机执行java程序的时候,每个方法都会创建一个栈帧栈帧存放在java虚拟机栈中,通过压栈出栈的方式进行方法调用。

很多人分不清虚拟机栈和本地方法栈的区别,因为本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

垃圾回收算法

当你回答Jvm内存结构,八九不离十,下一问就问你垃圾回收算法。

我简单说一下,垃圾回收算法你可以先回答引用计数法,回答完后说明一下此算法的缺点(无法解决互相引用问题),再引入标记-清除算法,再说一下它的缺点(空间碎片问题),然后说一下复制算法、标记-压缩算法如何解决空间碎片问题,最后说一下分代。到了这个时候,你完全可以举例新生代,老年代使用的是哪种算法,进行一个补充。一般回答到这,面试官就没得问了。如果还要继续深入,你可以了解垃圾回收器,CMS、G1、并行、串行等,已备不时之需。

下面分别说一下上面提到的算法。

引用计数法:引用计数法的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减一。只要对象A的引用计数器的值为0,则对象A就不能再被使用。

标记-清除算法:标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。

复制算法:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存中的角色,完成垃圾回收。

标记-压缩算法:首先从根节点开始,对所有可达的对象做一次标记,但之后,它并不是简单的清理未标记的对象,而是将所有的存活对象压缩到内存空间的一端。之后,清理边界外所有的空间。

分代:将内存区域根据对象的特点分成不同的内存区域,根据每块区域对象的特征不同使用不同的回收算法,以提高垃圾回收的效率。

说在这里,我非常想补充一个冷门的考点,也是面试官不一定问的问题。

哪些可以作为GC中root的对象?这个问题的标准回答如下:

1.被启动类加载的类和创建的对象2.栈内存中引用的对象3.方法区中静态和常量引用的对象4.本地方法中JNI引用的对象垃圾收集器

垃圾收集器有独占式的串行收集器,也有加了多线程的并行收集器。如何选择一个合适的垃圾收集器主要参考的就是GC策略的指标。包括以下几个部分:

吞吐量:指在应用程序的生命周期内,应用程序所花费的时间和系统总运行时间的比值。 举个例子,如果系统运行了 100min,GC 耗时 1min,那么系统的吞吐量就是(100-1)/100=99%。

垃圾回收器负载:和吞吐量正好相反,垃圾回收器负载指垃圾回收器耗时与系统运行总时间的比值。

停顿时间:指垃圾回收器正在运行时,应用程序的暂停时间。对于独占回收器而言,停顿时间可能会比较长。使用并发的回收器时,由于垃圾回收器和应用程序交替运行,程序的停顿时间会变短,但是,由于其效率很可能不如独占垃圾回收器,故系统的吞吐量可能会较低。

垃圾回收频率:指垃圾回收器多长时间会运行一次。一般来说,对于固定的应用而言,垃圾回收器的频率应该是越低越好。

反应时间:指当一个对象被称为垃圾后多长时间内,它所占据的内存空间会被释放。

堆分配:不同的垃圾回收器对堆内存的分配方式可能是不同的。一个良好的垃圾收集器应该有一个合理的堆内存区间划分。

科普完以上知识后,我简单说一下串行、并行、CMS、G1。

串行回收器主要有两个特点:第一:使用单线程进行垃圾回收;第二:独占式垃圾回收。并行回收器只是简单的把使用单线程进行垃圾回收改为多线程进行垃圾回收,它依旧是独占式垃圾回收。

CMS是Concurrent Mark Sweep的缩写,意为并发标记清除,从名称上可以得知,它使用的是标记-清除算法,同时它又是一个使用多线程并发回收的垃圾收集器。与CMS收集器相比,G1收集器是基于标记-压缩算法的。

那为什么CMS和G1不是独占式的垃圾回收器?

CMS工作时,主要步骤有:初始标记、并发标记、重新标记、并发清除和并发重置。其中初始标记和重新标记是独占系统资源的,而并发标记、并发清除和并发重置是可以和用户线程一起执行的。因此,从整体上来说,CMS收集不是独占式的,它可以在应用程序运行过程中进行垃圾回收。

Sychronized

在普通方法上锁 -在静态方法中上锁

在代码块----在类中上锁

对象在堆中分为三个部分,对象头 实例数据 对齐填充

对象头中主要包括两部分信息

1、自身运行收的数据,锁状态标志位和线程持有的锁,这部分被称为MarkWord

2、另一部分是类型指针,jvm通过这个指针确定这个对象是哪个类的实例

synchronized的对象锁,其指针指向的是一个monitor对象(由C++实现)的起始地址。每个对象实例都会有一个 monitor。其中monitor可以与对象一起创建、销毁;亦或者当线程试图获取对象锁时自动生成

monitor是由ObjectMonitor实现(ObjectMonitor.hpp文件,C++实现的),对于我们来说主要关注的是如下代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j5W348yw-1618927777807)(mysql优化.assets/u=778709442,1686419142&fm=173&app=25&f=JPEG)]

我们可以看到这里定义了**WaitSet** 和 _EntryList俩个队列,其中WaitSet 用来保存每个等待锁的线程对象。

owner,它指向持有ObjectMonitor对象的线程。当多个线程同时访问一段同步代码时,会先存放到 _EntryList 集合中,接下来当线程获取到对象的monitor时,就会把_owner变量设置为当前线程。同时count变量+1。如果线程调用wait() 方法,就会释放当前持有的monitor,那么_owner变量就会被置为null,同时_count减1,并且该线程进入 WaitSet集合中,等待下一次被唤醒。

当然,若当前线程顺利执行完方法,也将释放monitor,重走一遍刚才的内容,也就是_owner变量就会被置为null,同时_count减1,并且该线程进入 WaitSet集合中,等待下一次被唤醒。

因为这个锁对象存放在对象本身,也就是为什么Java中任意对象可以作为锁的原因。

  • 修饰对象

MDove:根据虚拟机规范要求,在执行monitorenter指令时,首先要尝试获取对象锁,也就是上文我们提到了monitor对象。如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器(_count)加1。当然与之对应执行monitorexit指令时,锁的计数器(_count)也会减1。

MDove:如果当前线程获取锁失败,那么就会被阻塞住,进入_WaitSet 中,等待锁被释放为止。

小A:等等,我看到字节码中,有俩个monitorexit指令,这是为什么呢?

MDove:是这样的,编译器需要确保方法中调用过的每条monitorenter指令都要执行对应的monitorexit 指令。为了保证在方法异常时,monitorenter和monitorexit指令也能正常配对执行,编译器会自动产生一个异常处理器,它的目的就是用来执行 异常的monitorexit指令。而字节码中多出的monitorexit指令,就是异常结束时,被执行用来释放monitor的。

以看到:字节码中并没有monitorenter指令和monitorexit指令,取得代之的是ACC_SYNCHRONIZED标识,JVM通过ACC_SYNCHRONIZED标识,就可以知道这是一个需要同步的方法,进而执行上述同步的过程,也就是_count加1,这些过程。

ThreadLocal

“在多线程环境下,如何防止自己的变量被其它线程篡改”

什么是ThreadLocal,首先,它是一个数据结构,有点像HashMap,可以保存"key : value"键值对,但是一个ThreadLocal只能保存一个,并且各个线程的数据互不干扰。

做个不恰当的比喻,从表面上看ThreadLocal相当于维护了一个Map,key就是当前的线程,value就是需要存储的对象。至于为什么说不恰当,因为实际上是ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。

实现原理

set方法和get方法的源码
public void set(T value) {//放入T类型的value
    //获取当前线程
    Thread t = Thread.currentThread();
    //实际存储的数据结构类型
    ThreadLocalMap map = getMap(t);
    //如果存在map就直接set,没有就创建map并set
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    //thread中维护了一个ThreadLocalMap
    return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
      //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}

可以发现,每个线程中都有一个ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程threadLocals变量中,当执行set方法中,是从当前线程的threadLocals变量获取。

所以在线程1中set的值,对线程2来说是摸不到的,而且在线程2中重新set的话,也不会影响到线程1中的值,保证了线程之间不会相互干扰。

如上述代码所示,我们可以看出来每个线程持有一个ThreadLocalMap对象。每创建一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象;否则的话,新创建一个ThreadLocalMap并赋值给threadLocals变量。

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

那每个线程中的ThreadLoalMap究竟是什么?

ThreadLoalMap

静态内部类

在ThreadLoalMap中,也是初始化一个大小16的Entry数组,Entry对象用来保存每一个key-value键值对,只不过这里的key永远都是ThreadLocal对象,是不是很神奇,通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中。

这里需要注意的是,ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了。

img

hash冲突

没有链表结构,那发生hash冲突了怎么办?

先看看ThreadLoalMap中插入一个key-value的实现

private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

每个ThreadLocal对象都有一个hash值threadLocalHashCode每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647

在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下:
1、如果当前位置是空的,那么正好,就初始化一个Entry对象放在位置i上;
2、不巧,位置i已经有Entry对象了,如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value;
3、很不巧,位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置

这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置

可以发现,set和get如果冲突严重的话,效率很低,因为ThreadLoalMap是Thread的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。

内存泄露

ThreadLocal可能导致内存泄漏,为什么?
先看看Entry的实现:

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

通过之前的分析已经知道,当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

如何避免内存泄露

既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。

如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("占小狼");
    // 其它业务逻辑
} finally {
    localName.remove();
}

我们可以看出来每个线程持有一个ThreadLocalMap对象。每创建一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象;否则的话,新创建一个ThreadLocalMap并赋值给threadLocals变量。

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

实现原理

set方法

ThreadLocal 的特性

ThreadLocal和synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是:

synchronized是通过线程等待牺牲时间来解决访问冲突
ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

正因为ThreadLocal的线程隔离特性,所以它的应用场景相对来说更为特殊一些。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal实现。但是在使用ThreadLocal的时候,需要我们考虑内存泄漏的风险。

至于为什么会有内存泄漏的风险,则是因为在我们使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key和value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生 GC 时会被回收,但Entry对象和value并没有被回收,因此如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,从而发生内存泄露。既然已经发现有内存泄露的隐患,自然有应对的策略。在调用ThreadLocal的get方法时会自动清除ThreadLocalMap中key为null的Entry对象,其触发逻辑就在getEntry方法中:

引出来强软弱虚引用

我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存中;如果内存空间在进行垃圾收集后还是很紧张,则可以抛弃这些对象

【既偏门又非常高频的面试题】强引用、软引用、弱引用、虚引用有什么区别?

具体使用场景是什么?在 JDK 1.2 版之后, Java 对引用的概念进行了扩充,将引用分为强引用( Strong Reference )、软引用( Soft Reference )、弱引用( Weak Reference )和虚引用( Phantom Reference ) 4 种,这 4 种引用强度依次逐渐减弱,强软弱虚

除强引用外,其他 3 种引用均可以在 java.lang.ref 包中找到它们的身影。都是 java.lang.ref.Reference 的子类

Reference 子类中只有终结器引用是包内可见的,其他 3 种引用类型均为 public ,可以在应用程序中直接使用

  • 强引用( StrongReference ) :最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似 Object obj = new Object() 这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
  • 软引用( SoftReference ):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。内存不足才回收
  • 弱引用( WeakReference ):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象只要 GC 就回收
  • 虚引用( PhantomReference ):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

6 - 再谈引用:强引用

强引用( Strong Reference )—— 不回收

在 Java 程序中,最常见的引用类型是强引用(普通对象 99% 以上都是强引用),也就是我们最常见的普通对象引用,也是默认的引用类型

当在 Java 语言中使用 new 操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用

强引用的对象是可触及的,垃圾收集器就永远不会回收掉被引用的对象。

对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null ,就是可以当做垃圾被收集了,当然具体回收时机还是要看垃圾收集策路

相对的,软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的,在一定条件下,都是可以被回收的。所以,强引用是造成 Java 内存泄漏的主要原因之一

强引用具备以下特点

  • 强引用可以直接访问目标对象。
  • 强引用所指向的对象在任傾时候都不会被系统回收,虚拟机宁愿抛出 OOM 异常,也不会回收强引用所指向对象。
  • 强引用可能导致内存泄漏。

7 - 再谈引用:软引用

软引用( Soft reference )—— 内存不足即回收

软引用是用来描述一些还有用但非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常

软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选地把引用存放到一个引用队列( Reference Queue )

类似弱引用,只不过 Java 虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。

在 JDK 1.2 版之后提供了 java.lang.ref.SoftReference 类来实现软引用。

SoftReference<User> userSoftRef = new SoftReference<User>(new User(1, "songhk"));
User user = userSoftRef.get();

8 - 再谈引用:弱引用

弱引用( Weak Reference )—— 发现即回收

弱引用也是用来描述那些非必需对象,只被弱引用关联的对象只能生存到下次垃圾收集发生为止。在系统 GC 时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象

但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间

弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。

软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。

在 JDK 1.2 之后提供了 java.lang.ref.WeakReference 类来实现弱引用

WeakReference<User> userWeakRef = new WeakReference<User>(new User(1, "songhk"));

弱引用对象与软引用对象的最大不同就在于,当 GC 在进行回收时,需要通过算法检査是否回收软引用对象,而对于弱引用对象, GC 总是进行回收。弱引用对象更容易、更快被 GC 回收。

面试题:你开发中使用过 WeakHashMap 吗?

9 - 再谈引用:虚引用

虚引用( Phantom Reference )—— 对象回收跟踪

也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个。

一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收

它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用 get() 方法取得对象时,总是 null 。

**为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。**比如:能在这个对象被收集器回收时收到一个系统通知

虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。

由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。

在 JDK 1.2 版之后提供了 PhantomReference 类来实现虚引用。

这样对应的value就不会 GC Roots 可达,从而在下次 GC 的时候就可以被回收了。但我们要知道,这仅是在调用ThreadLocal的get方法之后,才有可能执行的逻辑;特别地,当我们误用“先get再set”的使用逻辑时,就更会加大内存泄漏的风险。因此,ThreadLocal的最佳实践就是在使用完ThreadLocal之后,使用finally关键字显示调用ThreadLocal的remove方法,防止内存泄漏。

Sychronized

moniter

monitorenter

monitorexit --两个

其中一个放入到finnally

ACC-SYNCHRONIZED:同步方法的标志

获取monitor

通过观察锁的字节码文件,发现是通过monitorenter,monitorexit ,同步方法是通过ACC-SYNCHRONIZED标识

非静态方法,锁的是实例对象,静态同步方法,锁住的是class类

代码块,锁的是当前对象

常量池

String类型的常量池 :

  • 直接通过字面量声明 “com.atzsz”
  • 如果不是用双引号声明,可以使用String提供的intern()方法

StringTable 为什么调整

  1. permSize默认空间较小
  2. 永久代垃圾回收频率低

Mysql面试题

1、Msql复制原理和流程—主从复制

基本原理流程,3个线程以及之间的关联;

  1. 主:binlog线程——记录下所有改变了数据库数据的语句,放进master上的binlog中;

  2. 从:io线程——在使用start slave 之后,负责从master上拉取 binlog 内容,放进 自己的relay log中;

  3. 从:sql执行线程——执行relay log中的语句;

详解:mysql主从复制

MySQL数据库自身提供的主从复制功能可以方便的实现数据的多处自动备份,实现数据库的拓展。多个数据备份不仅可以加强数据的安全性,通过实现读写分离还能进一步提升数据库的负载性能。

下图就描述了一个多个数据库间主从复制与读写分离的模型(来源网络):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bUfqavbS-1618927777808)(mysql优化.assets/20180807141409955)]

在一主多从的数据库体系中,多个从服务器采用异步的方式更新主数据库的变化,业务服务器在执行写或者相关修改数据库的操作是在主服务器上进行的,读操作则是在各从服务器上进行。如果配置了多个从服务器或者多个主服务器又涉及到相应的负载均衡问题,关于负载均衡具体的技术细节还没有研究过,今天就先简单的实现一主一从的主从复制功能。

Mysql主从复制的实现原理图大致如下(来源网络):

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3LLQ4gqS-1618927777809)(mysql优化.assets/20180807141444637)]

MySQL之间数据复制的基础是二进制日志文件(binary log file)。一台MySQL数据库一旦启用二进制日志后,其作为master,它的数据库中所有操作都会以“事件”的方式记录在二进制日志中,其他数据库作为slave通过一个I/O线程与主服务器保持通信,并监控master的二进制日志文件的变化,如果发现master二进制日志文件发生变化,则会把变化复制到自己的中继日志中,然后slave的一个SQL线程会把相关的“事件”执行到自己的数据库中,以此实现从数据库和主数据库的一致性,也就实现了主从复制。

2、Mysql中的myisam与innodb的区别

1、InooDB支持事务,而MyISAM不支持事务

2、InnoDB支持行级锁,而MyISAM支持表级锁

3、InnoDB支持MVCC,而MyISAM不支持

4、InnoDB支持外键,而MyISAM不支持

5、InnoDB不支持全文索引,而MyISAM支持

<2>InnoDB引擎的四大特性

插入缓冲二次写自适应哈希索引预读

<3>InooDB和MyISAM的select count(*)哪个更快,为什么

myisam更快,因为myisam内部维护了一个计算器,可以直接调取。MyISAM的索引和数据是分开的,并且索引是有压缩的,内存使用率就对应提高了不少。能加载更多索引,而Innodb是索引和数据是紧密捆绑的,没有使用压缩从而会造成Innodb比MyISAM体积庞大不小。

3、MySQL中的varchar和char的区别以及varchar(50)中的50代表的涵义

(1)varchar和char

char是一种固定长度的类型,varchar则是一种可变长度的类型

(2)varchar(50)的涵义

最多存放50个字符,varchar(50)和(200)存储hello所占空间一样,但后者在排序时会消耗更多的内存,因为order by col采用fixed_length计算col长度(memory引擎也一样)

4、InnoDB的事务与日志的实现方式

(1)有多少种日志

错误日志:记录出错信息,也记录一些警告信息或者正确的信息

查询日志:记录所有对数据库请求的信息,不论这些请求是否得到了正确的执行

慢查询日志:设置一个阈值,将运行时间超过该值的所有SQL语句都记录到慢查询的日志文件中

二进制日志:记录对数据库执行更改的所有操作

中继日志,事务日志。

(2)事务的4种隔离级别

原子性

一致性

隔离性

持久性

读未提交:脏读意味着我在这个事务中(A中),事务B虽然没有提交,但它任何一条数据变化,我都可以看到!

读已提交

可重复读

串行化

读未提交产生脏读问题:

读已提交 这种隔离级别出现的问题是——**不可重复读(**Nonrepeatable Read):不可重复读意味着我们在同一个事务中执行完全相同的select语句时可能看到不一样的结果。

(1)这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)
(2)它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变

可重复读

(1)这是MySQL的默认事务隔离级别
(2)它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行
(3)此级别可能出现的问题——幻读(Phantom Read):当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行

可串行化

(1)这是最高的隔离级别
(2)它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。
(3)在这个级别,可能导致大量的超时现象和锁竞争

image-20210405194145534

5、一张表里面有ID自增主键,当insert了17条记录之后,删除了第15,16,17条记录,再把mysql重启,再insert一条记录,这条记录的ID是18还是15 ?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5DxlbIOv-1618927777811)(mysql优化.assets/20180808195842609)]

6、mysql为什么用自增列作为主键

  • 如果我们定义了主键(PRIMARY KEY),那么InnoDB会选择主键作为聚集索引、如果没有显式定义主键,则InnoDB会选择第一个不包含有NULL值唯一索引作为主键索引、如果也没有这样的唯一索引,则InnoDB会选择内置6字节长的ROWID作为隐含的聚集索引(ROWID随着行记录的写入而主键递增,这个ROWID不像ORACLE的ROWID那样可引用,是隐含的)。
  • 数据记录本身被存于主索引(一颗B+Tree)的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,因此每当有一条新的记录插入时,MySQL会根据其主键将其插入适当的节点和位置,如果页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)
  • 如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页
  • 如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置,此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面

7、为什么使用数据索引能提高效率

  1. 数据索引的存储是有序的
  2. 在有序的情况下,通过索引查询一个数据是无需遍历索引记录的
  3. 极端情况下,数据索引的查询效率为二分法查询效率,趋近于 log2(N)

8、B+树索引和哈希索引的区别

B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且叶子节点的指针相互链接,是有序的

哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从跟节点到叶子节点逐级查找,只需要 一次哈希算法即可,是无序的

哈希索引的优势:

  1. 等值查询。哈希索引具有绝对优势(前提是:没有大量重复键值,如果大量重复键值时,哈希索引的效率很低,因为存在所谓的哈希碰撞问题。)

哈希索引不适用的场景:

  1. 不支持范围查询
  2. 不支持索引完成排序
  3. 不支持联合索引最左前缀匹配规则

通常,B+树索引结构适用于绝大多数场景,像下面这种场景用哈希索引才更有优势:

在HEAP表中,如果存储的数据重复度很低(也就是说基数很大),对该列数据以等值查询为主,没有范围查询、没有排序的时候,特别适合采用哈希索引,例如这种SQL:

select id,name from table where name='李明'; — 仅等值查询

而常用的InnoDB引擎中默认使用的是B+树索引,它会实时监控表上索引的使用情况,如果认为建立哈希索引可以提高查询效率,则自动在内存中的“自适应哈希索引缓冲区”建立哈希索引(在InnoDB中默认开启自适应哈希索引),通过观察搜索模式,MySQL会利用index key的前缀建立哈希索引,如果一个表几乎大部分都在缓冲池中,那么建立一个哈希索引能够加快等值查询。

注意:在某些工作负载下,通过哈希索引查找带来的性能提升远大于额外的监控索引搜索情况和保持这个哈希表结构所带来的开销。但某些时候,在负载高的情况下,自适应哈希索引中添加的read/write锁也会带来竞争,比如高并发的join操作。like操作和%的通配符操作也不适用于自适应哈希索引,可能要关闭自适应哈希索引。

9、B树和B+树的区别

1、B树,每个节点都存储key和data,所有的节点组成这可树,并且叶子节点指针为null,叶子节点不包含任何关键字信息

2、B+树,所有的叶子节点中包含全部关键字的信息,及指向含有这些关键字记录的指针,且叶子节点本身依关键字的大小自小到大的顺序链接,所有的非终端节点可以看成是索引部分,节点中仅含有其子树根节点中最大(或最小)关键字

10、为什么说B+比B树更适合实际应用中操作系统的文件索引和数据库索引?

  1. B+的磁盘读写代价更低 B+的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
  2. B±tree的查询效率更加稳定 由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

11、mysql联合索引

  1. 联合索引是两个或更多个列上的索引。对于联合索引:Mysql从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分。例如索引是key index (a,b,c). 可以支持a 、 a,b 、 a,b,c 3种组合进行查找,但不支持 b,c进行查找 .当最左侧字段是常量引用时,索引就十分有效。
  2. 利用索引中的附加列,您可以缩小搜索的范围,但使用一个具有两列的索引 不同于使用两个单独的索引。复合索引的结构与电话簿类似,人名由姓和名构成,电话簿首先按姓氏对进行排序,然后按名字对有相同姓氏的人进行排序。如果您知 道姓,电话簿将非常有用;如果您知道姓和名,电话簿则更为有用,但如果您只知道名不姓,电话簿将没有用处。

12、什么情况下应不建或少建索引

  1. 表记录太少
  2. 经常插入、删除、修改的表
  3. 数据重复且分布平均的表字段,假如一个表有10万行记录,有一个字段A只有T和F两种值,且每个值的分布概率大约为50%,那么对这种表A字段建索引一般不会提高数据库的查询速度。
  4. 经常和主字段一块查询但主字段索引值比较多的表字段

13、MySQL分区

什么是表分区?

表分区,是指根据一定规则,将数据库中的一张表分解成多个更小的,容易管理的部分。从逻辑上看,只有一张表,但是底层却是由多个物理分区组成。

表分区与分表的区别

分表:指的是通过一定规则,将一张表分解成多张不同的表。比如将用户订单记录根据时间成多个表。

分表与分区的区别在于:分区从逻辑上来讲只有一张表,而分表则是将一张表分解成多张表。

表分区有什么好处?
  1. 分区表的数据可以分布在不同的物理设备上,从而高效地利用多个硬件设备。 2. 和单个磁盘或者文件系统相比,可以存储更多数据
  2. 优化查询。在where语句中包含分区条件时,可以只扫描一个或多个分区表来提高查询效率;涉及sum和count语句时,也可以在多个分区上并行处理,最后汇总结果。
  3. 分区表更容易维护。例如:想批量删除大量数据可以清除整个分区。
  4. 可与使用分区表来避免某些特殊的瓶颈,例如InnoDB的单个索引的互斥访问,ext3问价你系统的inode锁竞争等。

14、分区表的限制因素

1、一个表最多只能有1024个分区

2、MySQL5.1中,分区表达式必须是整数,或者返回整数的表达式。在MySQL5.5中提供了非整数表达式分区的支持。

3、如果分区字段中有主键或者唯一索引的列,那么多有主键列和唯一索引列都必须包含进来。即:分区字段要么不包含主键或者索引列,要么包含全部主键和索引列。

4、分区表中无法使用外键约束

5、MySQL的分区适用于一个表的所有数据和索引,不能只对表数据分区而不对索引分区,也不能只对索引分区而不对表分区,也不能只对表的一部分数据分区。

15、如何判断当前Mysql是否支持分区?

命令:show variables like ‘%partition%’

16、Mysql支持的分区类型有哪些?

1、RANGE分区:这种模式允许将数据划分不同范围。例如可以将一个表通过年份划分成若干个分区

2、List分区:这种模式允许系统通过预定义的列表的值来对数据进行分割。按照list中的值分区,与RANGE的区别是,range分区的区间范围值是连续的

3、HASH分区:这种模式允许通过对表的一个或多个列的Hash Key进行计算,最后通过这个Hash码不同数值对应的数据区域进行分区。例如可以建立一个对表主键进行分区的表

4、KEY分区:上面Hash模式的一种延伸,这里的Hash Key是Mysql系统产生的

17、行级锁定的优点

1、当在许多线程中访问不同的行时只存在少量锁定冲突

2、回滚时只有少量的更改

3、可以长时间锁定单一的行

18、行级锁定的缺点

1、比页级或表级锁定占用更多的内存

2、当在表的大部分中使用时,比页级或表级锁定速度慢,因为你必须获取更多的锁

3、如果你在大部分数据上经常进行GROUP BY操作或者必须经常扫描整个表,比其它锁定明显慢很多

4、用高级别锁定,通过支持不同的类型锁定,你也可以很容易地调节应用程序,因为其锁成本小于行级锁定

19、Mysql优化

1、开启查询缓存,优化查询

2、explain你的select查询,这可以帮你分析你的查询语句或是表结构的性能瓶颈。EXPLAIN的查询结果还会告诉你你的索引主键被如何利用的,你的数据表是如何被搜索和排序

3、当只要一行数据时使用limit 1,Mysql数据库引擎会在找到一条数据后停止搜索,而不是继续往后查找下一条符合记录的数据

4、为搜索字段建索引

5、使用ENUM而不是VARCHAR,如果你有一个字段,比如“性别”,“国家”,“民族”,“状态”或“部门”,你知道这些字段的取值是有限而且固定的,那么,你应该使用ENUM而不是VARCHAR

6、Prepared Statement Prepared Statements很像存储过程,是一种运行在后台的sql语句集合,我们可以从使用prepared statement获得很多好处,无论是性能问题还是安全问题。Prepared Statements可以检查一些你绑定好的变量,这样可以保护你的程序不会受到“SQL注入式”攻击

7、垂直分表

8、选择正确的存储引擎

20、key和index的区别

1、key是数据库的物理结构,它包含两层意义和作用,一是约束(偏重于约束和规范数据库的结构完整性),二是索引(辅助查询用的)。包括primary key,unique key,foregin key等

2、index是数据库的物理结构,它只是辅助查询的,它创建时会在另外的表空间(mysql中的innodb表空间)以一个类似目录的结构存储。索引要分类的话,分为前缀索引、全文本索引

索引

单值索引:即一个索引只包含单个列, 一个表可以有多个单列索引

CREATE TABLE customer (id INT(10) UNSIGNED AUTO_INCREMENT ,customer_no VARCHAR(200),customer_name
VARCHAR(200),
PRIMARY KEY(id),
KEY (customer_name)
);
单独建单值索引:
CREATE INDEX idx_customer_name ON customer(customer_name);

唯一索引:索引列的值必须唯一, 但允许有空值

CREATE TABLE customer (id INT(10) UNSIGNED AUTO_INCREMENT ,customer_no VARCHAR(200),customer_name
VARCHAR(200),
PRIMARY KEY(id),
KEY (customer_name),
UNIQUE (customer_no)
);
单独建唯一索引:
CREATE UNIQUE INDEX idx_customer_no ON customer(customer_no);

复合索引:即一个索引包含多个列

CREATE TABLE customer (id INT(10) UNSIGNED AUTO_INCREMENT ,customer_no VARCHAR(200),customer_name
VARCHAR(200),
PRIMARY KEY(id),
KEY (customer_name),
UNIQUE (customer_name),
KEY (customer_no,customer_name)
);
单独建索引:
CREATE INDEX idx_no_name ON customer(customer_no,customer_name);

主键索引:设定为主键后数据库会自动建立索引, innodb为聚簇索引

CREATE TABLE customer (id INT(10) UNSIGNED AUTO_INCREMENT ,customer_no VARCHAR(200),customer_name
VARCHAR(200),
PRIMARY KEY(id)
);
单独建主键索引:
ALTER TABLE customer add PRIMARY KEY customer(customer_no);
删除建主键索引:
ALTER TABLE customer drop PRIMARY KEY ;
修改建主键索引:
必须先删除掉(drop)原索引, 再新建(add)索引

基本语法

很多好处,无论是性能问题还是安全问题。Prepared Statements可以检查一些你绑定好的变量,这样可以保护你的程序不会受到“SQL注入式”攻击

7、垂直分表

8、选择正确的存储引擎

20、key和index的区别

1、key是数据库的物理结构,它包含两层意义和作用,一是约束(偏重于约束和规范数据库的结构完整性),二是索引(辅助查询用的)。包括primary key,unique key,foregin key等

2、index是数据库的物理结构,它只是辅助查询的,它创建时会在另外的表空间(mysql中的innodb表空间)以一个类似目录的结构存储。索引要分类的话,分为前缀索引、全文本索引

索引

单值索引:即一个索引只包含单个列, 一个表可以有多个单列索引

CREATE TABLE customer (id INT(10) UNSIGNED AUTO_INCREMENT ,customer_no VARCHAR(200),customer_name
VARCHAR(200),
PRIMARY KEY(id),
KEY (customer_name)
);
单独建单值索引:
CREATE INDEX idx_customer_name ON customer(customer_name);

唯一索引:索引列的值必须唯一, 但允许有空值

CREATE TABLE customer (id INT(10) UNSIGNED AUTO_INCREMENT ,customer_no VARCHAR(200),customer_name
VARCHAR(200),
PRIMARY KEY(id),
KEY (customer_name),
UNIQUE (customer_no)
);
单独建唯一索引:
CREATE UNIQUE INDEX idx_customer_no ON customer(customer_no);

复合索引:即一个索引包含多个列

CREATE TABLE customer (id INT(10) UNSIGNED AUTO_INCREMENT ,customer_no VARCHAR(200),customer_name
VARCHAR(200),
PRIMARY KEY(id),
KEY (customer_name),
UNIQUE (customer_name),
KEY (customer_no,customer_name)
);
单独建索引:
CREATE INDEX idx_no_name ON customer(customer_no,customer_name);

主键索引:设定为主键后数据库会自动建立索引, innodb为聚簇索引

CREATE TABLE customer (id INT(10) UNSIGNED AUTO_INCREMENT ,customer_no VARCHAR(200),customer_name
VARCHAR(200),
PRIMARY KEY(id)
);
单独建主键索引:
ALTER TABLE customer add PRIMARY KEY customer(customer_no);
删除建主键索引:
ALTER TABLE customer drop PRIMARY KEY ;
修改建主键索引:
必须先删除掉(drop)原索引, 再新建(add)索引

基本语法

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值