文章目录
- 项目介绍
- 1.不动产项目
- 项目难点
- 机器学习算法调研
- 图像提取算法调研
- 数据集-ImageNet
- Xception
- VGG
- Inception
- Densenet
- Mobilenet
- 系统流程图
- 2.图书项目
- 技术栈
- Shiro
- Mybatis
- MyBatis:
- Mybatis Plus:
- 面试问题
- Java基础
- 基本数据类型
- 反射
- 接口和抽象类
- 异常
- 代理模式
- 1. 静态代理
- 2. 动态代理
- 2.1 JDK 动态代理机制
- 2.2 CGLIB 动态代理机制
- 2.3 二者对比
- 代码到运行的过程
- Java 集合
- 1. List
- 2. Set
- 3. Queue
- 4. Map
- HashMap
- HashMap v.s Hashtable(5点)
- ConcurrentHashMap v.s Hashtable(2点)
- Java并发
- JMM(Java内存模型)
- 什么是JMM?
- 为啥需要JMM?
- Java内存区域和 JMM 有什么区别?
- JMM如何抽象线程和主内存之间的关系?
- happens-before 原则
- 并发编程三个重要特性
- 线程
- 1. ==进线程区别==
- 2. 多线程
- 3. 线程安全
- 4. 线程通信
- 5. 创建线程的三种方式
- 6. 生命周期&状态
- 7. 上下文切换
- 8. sleep()和wait()
- volatile
- 如何保证变量的可见性?
- 如何禁止指令重排序?
- volatile 可以保证原子性么?
- 乐观锁/悲观锁
- 1. 悲观锁
- 2. 乐观锁
- synchronized
- 1. 使用场景
- 2. 底层原理
- synchronized 同步语句块(monitor两个指令)
- synchronized 修饰方法(`ACC_SYNCHRONIZED` 标识)
- 3. 和volatile区别
- ReentrantLock
- 和synchronized区别
- AQS(抽象队列)
- 1. 原理
- 2. Semaphore作用
- 3. Semaphore原理
- 4. CountDownLatch
- ThreadLocal
- 1. 作用+场景
- 2. 原理+实现
- 3. 内存泄露问题
- 线程池
- 1. 线程池作用
- 2. 创建线程池方法
- 3. 线程池饱和策略
- 4. 常用阻塞队列
- 5. 线程池处理任务流程
- 6. 线程池大小
- 7. 线程池参数
- Future
- JVM
- 一、内存结构
- 运行时数据区域
- 线程私有:程序计数器+俩栈
- 程序计数器:很小的内存空间,当前线程的**行号**指示器
- 虚拟机栈:保存方法的局部变量、操作数、动态链接、方法返回地址
- 本地方法栈
- 线程共享:堆+方法区+直接内存
- 堆:运行时数据区域,保存所有类的实例和数组
- 字符串常量池
- 方法区:存储加载的类信息、class/method/field等元数据、常量、静态变量
- 运行时常量池
- 直接内存(非运行时数据区域)
- HotSpot虚拟机对象
- 1. 对象创建(默写)
- 2. 对象的内存布局
- 3. 对象的访问定位
- 二、类加载机制
- 三、垃圾回收(GC)
- 1. 引用类型
- 2. 垃圾收集
- 3. 垃圾回收算法(内存回收方法论)
- 4. 垃圾收集器(内存回收具体实现)
- **Serial:单 新 复制**
- **ParNew:多 新 复制**
- **Parallel Scavenge:多 新 复制**
- **Serial Old:单 老 整理**
- **Parallel Old:多 老 整理**
- **CMS:多 老 清除**
- **G1:多 新/老 整理+复制**
- 总结
- 5. GC调优
- 四、JVM调优
- JVM参数总结
- OOM排查
- 1、堆内存不足
- 原因
- 解决方法
- 2、永久代/元空间溢出
- 原因
- 解决方法
- 3、方法栈溢出
- 原因
- 解决方法
- 4、GC overhead limit exceeded
- 原因
- 解决方法
- 设计模式
- 单例模式
- 工厂模式
- 责任链模式
- Jdk中的设计模式
- MySQL
- 一、存储引擎
- 二、索引
- 1. 优缺点
- 2. 底层数据结构
- B&B+树
- 3. 索引类型
- 三、事务
- 1. ACID
- 2. 并发事务问题
- 3. 并发事务控制方式
- 锁
- MVCC
- RC和RR下 MVCC 的差异
- 4. 隔离级别
- 四、锁机制
- 1. 行级锁、表级锁
- 2. 共享锁、排它锁
- 3. 意向锁
- 4. 当前读、快照读
- 5. 自增锁(了解)
- 五、日志
- 1. undo log(回滚)
- 为什么需要Buffer pool?
- 2. redo log(重做)
- 刷盘时机
- 3. binlog(归档)
- Redis
- 为什么用redis做mysql缓存?
- 数据结构
- 常见数据结构
- 底层数据结构
- 特殊数据结构
- 线程模型
- 1. 单线程模式
- 2. 单线程 v.s 多线程
- IO多路复用
- 缓存设计
- 缓存读写策略
- 1. 旁路缓存模式
- 2. 读写穿透
- 3. 写回策略
- 数据库和缓存一致性
- 1. 先更db还是先更缓存
- 2. 先更db还是先删缓存
- 先更db,再删缓存:cache aside 旁路缓存策略
- 持久化机制
- RDB快照
- AOF日志
- 怎么选择RDB还是AOF?
- 生产问题
- 缓存穿透
- 缓存击穿
- 缓存雪崩
- 过期删除&内存淘汰策略
- 过期删除
- 内存淘汰策略
- 数据结构
- ==排序算法==
- 快速排序
- 计算机网络
- 输入url到页面展示
- HTTP(应用层)
- 基本概念
- GET和POST
- HTTP缓存技术
- HTTPS 1.0 1.1 2.0
- HTTPS
- TCP v.s UDP(传输层)
- 七点区别
- TCP为什么可靠
- 三次握手(建立TCP连接)
- 四次挥手(断开TCP连接)
- TCP Keepalive 和 HTTP Keep-Alive
- DNS(域名-IP映射)
- 操作系统
- 内核态和用户态
- 进程和线程
- 进程通信7种方式
- 线程同步的4种方式
- 内存管理
- 虚拟内存管理方式
- 死锁
- Spring
- 概述
- Spring,Spring MVC,Spring Boot 之间什么关系?
- IoC(控制反转)
- @Component 和 @Bean 的区别是什么?
- @Autowired 和 @Resource 的区别是什么?
- Bean的生命周期
- AoP(面向切面编程)
- 自动装配原理
- Sping中的设计模式
- 注解
- 常用注解
- 事务 `@Transactional`
- **1. Spring事务介绍**
- **2. spring事务管理方式**
- **3. Spring事务管理接口**
- 4. 事务属性
- 5. @Transactional注解使用
- 分布式
- RPC
- ZooKeeper 特点
- 常见RPC框架
- Dubbo
- 分布式锁
- 基于Redis实现分布式锁
- 基于ZooKeeper实现分布式锁
- Linux
- 常用命令
- 查询进程占用内存
- 银行
- 查询进程占用内存
- 银行
项目介绍
1.不动产项目
项目难点
- 模型准确率20%出头->找原因:数据量不足学习不充分+模型本身的问题(全选的基于树的模型)
- 针对数据:扩充数据集,包装法筛特征
- 针对模型:增加模型选型(岭回归)+stacking boosting bagging集成+网格法调参
- 结果:准确率90%
怎么部署到服务器上的?
- docker
- 前端:
- 本地vue项目运行npm run build生成dist文件夹,然后通过Xftp6传输到远程服务器,同时还要将相应的dockerfile和default.conf文件传输过去,并且因为用的是https加密,所以传过加密证书。
- 在远程docker进入相应的前端文件夹执行docker build -t gangoffivecloud .命令
即可将dist打包成一个docker镜像名为gangoffivecloud。
然后执行docker run -p 8081:8085 -d --name gangoffivecloud gangoffivecloud
即可将名为gangoffivecloud的镜像运行到名为gangoffivecloud的容器里。 - 这里前端就已经开始在服务器上运行了。
- 后端的话,将springboots打包成一个GangOfFive-0.0.1-SNAPSHOT.jar的jar文件,这里我用的是idea
- 然后通过Xftp6把jar包和相应dockerfile传过去。
- 在服务器处进入相应的后端文件夹,运行docker build -t gangoffive .命令就可以得到一个名为gangoffive的docker镜像,然后执行命令docker run -p 8085:8085 -d --name gangoffive gangoffive便可将名gangoffive的docker镜像运行在名为gangoffive的容器里。
- 此时后端也在服务器里运行了。
- mysql:
- docker pull mysql:latest
下载mysql数据库最新镜像,然后
docker run -itd --name mysql-test -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql
将mysql镜像运行到名为mysql-test的容器里,并且初始root管理员密码为123456
后端yml里怎么写连接远程数据库
- docker pull mysql:latest
- 前端:
机器学习算法调研
12种基学习器,评价指标为RMSE、MAE、MAPE和R2,最终选定Catboost和LightGBM
图像提取算法调研
数据集-ImageNet
Xception
论文:Xception: Deep Learning with Depthwise Separable Convolutions (CVPR 2017)
源码:Keras开源代码
VGG
Very Deep Convolutional Networks for Large-Scale Image Recognition
Inception
Densenet
Mobilenet
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WHQGEfqE-1686815973681)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20230419190536360.png)]
系统流程图
2.图书项目
JPA和Mybatis区别
技术栈
前端服务器Nginx,后端服务器Tomcat,开发前端内容时,可以把前端的请求通过前端服务器转发给后端(称为反向代理)
用户信息明文存储在数据库中,不安全
Shiro
三大概念
- Subject:负责存储与修改当前用户的信息和状态
- SecurityManager:安全相关的操作实际上是由她管理的
- Realms:负责从数据源中获取数据并加工后传给 SecurityManager
四大功能
- Authentication(认证)
- Authorization(授权)
- Session Management(会话管理)
- Cryptography(加密)
Mybatis
-
MyBatis:
1)所有SQL语句全部自己写
2)手动解析实体关系映射转换为MyBatis内部对象注入容器
3)不支持Lambda形式调用 -
Mybatis Plus:
1)强大的条件构造器,满足各类使用需求
2)内置的Mapper,通用的Service,少量配置即可实现单表大部分CRUD操作
3)支持Lambda形式调用
4)提供了基本的CRUD功能,连SQL语句都不需要编写
5)自动解析实体关系映射转换为MyBatis内部对象注入容器 -
Mybatis Plus 加载流程:
1、加载配置文件(数据源,以及映射文件),解析配置文件,生成Configuration,MapperedStatement
2、通过使用Configuration对象,创建sqlSessionFactory,用来生成SqlSeesion
3、sqlSession通过调用api或者mapper接口传入statementId找到对应的MapperedStatement,来调用执行sql
4、通过Executor核心器,负责sql动态语句的生成和查询缓存的维护,来进行sql的参数转换,动态sql的拼接,生成Statement对象
5、借助于MapperedStatement来访问数据库,它里面封装了sql语句的相关信息,以及返回结果信息
面试问题
-
Spring Security和Shiro的区别?
- Shiro比Spring更容易使用,实现和理解
- Shiro 功能强大、且 简单、灵活
- Shiro是 Apache 下的项目,比较可靠,不跟任何的框架或者容器绑定,可以独立运行
-
项目中redis用什么客户端部署?
-
Java 访问 Redis 主要是通过 Jedis 和 Lettuce 两种由不同团队开发的客户端(提供访问、操作所需的 API),Jedis 比较原生,Lettuce 提供的能力更加全面。
-
本项目用Spring Data Redis,Spring Data Redis是在 Lettuce 的基础上做了一些封装,与 Spring 生态更加贴合,使用起来也更简便。
-
-
java怎么连接数据库?
配置maven依赖->配置数据库(application.properties)
-
项目还有哪些不足之处?
-
后端提升响应速度
仅就软件来说,努力的方向有三个,一是 代码,二是 “技术应用”,三是 “优化设计”。
-
系统安全问题
-
-
sql注入的原因及解决
- 原因:程序开发过程中不注意书写规范,对sql语句和关键字未进行过滤,导致客户端可以通过全局变量get或者post提交sql语句到服务器端正常运行;
- 解决:预防SQL注入大概有两种思路 ,一是提升对输入內容的查验;二是应用参数化语句来传递客户输入的內容。
- 过滤掉一些常见的数据库关键字:select、insert、update、delete、and等
- 对于常用的方法加以封装,避免直接暴露sql语句
- 开启安全模式,safe_mode=on
- SQL中**# 与$ 的区别**
- #将传入的数据都当成一个字符串,在很大程度上能够防止sql注入
- 将传入的数据直接显示生成在 s q l 中, ∗ ∗ 将传入的数据直接显示生成在sql中,** 将传入的数据直接显示生成在sql中,∗∗方式无法防止sql注入**
- 一般能用#的就别用$
-
用户注册登录流程
-
用户管理
-
用户信息: 显示用户的基本信息(昵称、联系方式、角色、部门等)
-
组织架构: 显示、配置(增删改)组织架构,一般为树结构
-
用户操作: 为用户分配角色(多对多)、组织架构(多对多),删除用户
-
用户黑白名单: 对特殊用户进行特别控制
-
-
角色管理
-
角色信息: 显示角色的基本信息(名称、权限等)
-
角色操作: 根据需要增删角色、为角色分配权限(多对多,按不同粒度分配,并实现权限的互斥性检验)
-
-
权限管理:菜单、功能、数据
-
开发要点:
- 用户、角色、权限、组织架构表结构设计
- 用户身份验证、授权、会话管理,用户信息的加密存储
- 不同粒度权限的具体实现
-
菜单权限:
- 使用 “全局前置守卫”(router.beforeEach),在导航触发时向后端发送一个包含用户信息的请求
- 后端查询数据库中该用户可以访问的菜单(也就是 vue 的路由信息)并返回
- 前端把接收到的数据添加到路由里,并根据新的路由表动态渲染出导航栏,使不同用户登录后看到不同的菜单。同时,由于路由表也是按需加载的,所以用户也无法通过 URL 访问没有权限的页面
-
功能权限:
- 不管三七二十一前端组件全部加载出来,但需要调用后端接口时进行判断,如果无权限则弹出相应提示。这种适合对按钮的控制,图表直接不加载数据就显得不是很友好
-
数据权限:
- 可访问性控制:可访问性可以针对表、字段或满足某些条件的数据。针对表、字段的控制,主要依靠在业务逻辑执行前进行判断,比如在调用对收支信息表的查询前判断当前用户是否具有财务权限。而访问特定数据,可以直接通过 SQL 语句(WHERE 条件)来实现,比如当前用户只能查询出自身拥有的书籍,就可以通过类似
SELECT * FROM book WHERE uid = #{uid}
的语句来实现。 - 数据量控制:常见的比如一天内普通用户只能访问 2000 条数据(公众号好像就有这个限制),可以通过引入计数机制来实现,调用接口或执行业务逻辑时先进行判断,同时限制本次查询的最大数量。此外,还有需要对一次的访问量进行控制、对某段时间能够处理的数据量进行控制等应用场景等等
- 可访问性控制:可访问性可以针对表、字段或满足某些条件的数据。针对表、字段的控制,主要依靠在业务逻辑执行前进行判断,比如在调用对收支信息表的查询前判断当前用户是否具有财务权限。而访问特定数据,可以直接通过 SQL 语句(WHERE 条件)来实现,比如当前用户只能查询出自身拥有的书籍,就可以通过类似
-
用户加密加盐
- 用户注册时,输入用户名密码(明文),向后台发送请求
- 后台将密码加上随机生成的盐并 hash,再将 hash 后的值作为密码存入数据库,盐也作为单独的字段存起来
- 用户登录时,输入用户名密码(明文),向后台发送请求
- 后台根据用户名查询出盐,和密码组合并 hash,将得到的值与数据库中存储的密码比对,若一致则通过验证
-
认证方案
- session:许多语言在网络编程模块都会实现会话机制,即 session。利用 session,我们可以管理用户状态,比如控制会话存在时间,在会话中保存属性等。其作用方式通常如下:
- 服务器接收到第一个请求时,生成 session 对象,并通过响应头告诉客户端在 cookie 中放入 sessionId
- 客户端之后发送请求时,会带上包含 sessionId 的 cookie
- 服务器通过 sessionId 获取 session ,进而得到当前用户的状态(是否登录)等信息
- 也就是说,客户端只需要在登录的时候发送一次用户名密码,此后只需要在发送请求时带上 sessionId,服务器就可以验证用户是否登录了。
- token:虽然 session 能够比较全面地管理用户状态,但这种方式毕竟占用了较多服务器资源,所以有人想出了一种无需在服务器端保存用户状态(称为 “无状态”)的方案,即使用 token(令牌)来做验证。
- 一个真正的 token 本身是携带了一些信息的,比如用户 id、过期时间等,这些信息通过签名算法防止伪造,也可以使用加密算法进一步提高安全性,但一般没有人会在 token 里存储密码,所以不加密也无所谓,反正被截获了结果都一样。(一般会用 base64 编个码,方便传输)
- 在 web 领域最常见的 token 解决方案是 JWT(JSON Web Token)
- 思路:
- 用户使用用户名密码登录,服务器验证通过后,根据用户名(或用户 id 等),按照预先设置的算法生成 token,其中也可以封装其它信息,并将 token 返回给客户端(可以设置到客户端的 cookie 中,也可以作为 response body)
- 客户端接收到 token,并在之后发送请求时带上它(利用 cookie、作为请求头或作为参数均可)
服务器对 token 进行解密、验证
- token 的优势是无需服务器存储!!!
- session:许多语言在网络编程模块都会实现会话机制,即 session。利用 session,我们可以管理用户状态,比如控制会话存在时间,在会话中保存属性等。其作用方式通常如下:
延时双删:先清除缓存,在更新数据库后,等一段时间,再去第二次执行删除操作。
项目中学到了什么?
技术方面
沟通交流
项目难点?如何解决?
Java基础
Java 程序从源代码到运行的过程:
基本数据类型
反射
反射就是把java类中的各种成分映射成一个个的Java对象,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
Spring/Spring Boot、MyBatis 这些框架中大量使用了动态代理,而动态代理的实现也依赖反射。
- 优缺点
- 优点:
- 增加程序的灵活性,避免将程序写死到代码里
- 代码简洁,提高代码的复用率,外部调用方便
- 对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法
- 缺点:
- 性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。
- 使用反射会模糊程序内部逻辑:程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术,因而会带来维护问题。反射代码比相应的直接代码更复杂。
- 安全限制:使用反射技术要求程序必须在一个没有安全限制的环境中运行。
- 内部暴露:由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用--代码有功能上的错误,降低可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。
- 优点:
- 场景:
接口和抽象类
抽象类:abstract
-
a、抽象类不能被实例化只能被继承;
-
b、包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法;
-
c、抽象类中的抽象方法的修饰符只能为public或者protected,默认为public;
-
d、一个子类继承一个抽象类,则子类必须实现父类抽象方法,否则子类也必须定义为抽象类;
-
e、抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。
接口:interface
- a、接口可以包含变量、方法;变量被隐士指定为public static final,方法被隐士指定为public abstract(JDK1.8之前);
- b、接口支持多继承,即一个接口可以extends多个接口,间接的解决了Java中类的单继承问题;
- c、一个类可以实现多个接口;
- d、JDK1.8中对接口增加了新的特性:(1)、默认方法(default method):JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰;定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法;(2)、静态方法(static method):JDK 1.8中允许使用static关键字修饰一个方法,并提供实现,称为接口静态方法。接口静态方法只能通过接口调用(接口名.静态方法名)。
异常
- **异常类层次结构图:**所有异常的祖先都是
Throwable
类
-
Checked Exception 和 Unchecked Exception 有什么区别?
- Checked Exception:编译过程中,若没有被
catch
或者throws
关键字处理的话,就没办法通过编译。 - Unchecked Exception:不处理也可以正常通过编译。
- Checked Exception:编译过程中,若没有被
-
try-catch-finally-return执行顺序?
- 不管是否有异常产生,finally块中代码都会执行
- 当try和catch中有return语句时,finally块仍然会执行
- finally是在return后面的表达式运算执行的,所以函数返回值在finally执行前确定的,无论finally中的代码怎么样,返回的值都不会改变,仍然是之前return语句中保存的值
- finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值
public static int getInt() { int a = 10; try { System.out.println(a / 0); a = 20; } catch (ArithmeticException e) { a = 30; return a; /* * return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了 * 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40 * 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30 */ } finally { a = 40; } return a; } //结果是30!
代理模式
1. 静态代理
针对每个目标类都单独创建一个代理类
2. 动态代理
从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
Spring AOP、RPC框架的实现依赖动态代理
-
2.1 JDK 动态代理机制
InvocationHandler
接口和Proxy
类是核心
-
2.2 CGLIB 动态代理机制
- JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。
- 在 CGLIB 动态代理机制中
MethodInterceptor
接口和Enhancer
类是核心。
-
2.3 二者对比
- 灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
- JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
代码到运行的过程
-
编译
- 在Java中指将**.java**文件转化为 .class文件(字节码文件)的过程。
- 其中这个字节码文件,真正的实现了跨平台、跨语言。因为JVM里运行的就是.class文件,只要符合这个格式就能运行。所以在任何平台,用任何语言只要你能把程序编译成字节码文件就能在JVM里运行。
-
加载
-
类加载器会在指定的classpath中找到.class这些文件,然后读取字节流中的数据,将其存储在JVM方法区
-
根据.class的信息建立一个Class对象,作为运行时访问这个类的各种数据的接口(一般也在方法区)。
-
验证格式、语义等
-
为类的静态变量分配内存并设为JVM默认的初值,对于非静态的变量,则不会为它们分配内存。
静态变量的初值为JVM默认的初值,而不是我们在程序中设定的初值。
-
字节码文件中存放的部分方法、字段等的符号引用可以解析为其在内存中的直接引用,无需等到运行时解析。
-
此时,执行引擎会调用()方法对静态字段进行代码中编写的初始化操作。
-
-
执行
- 引擎寻找main()方法,执行其中字节码指令
- 对象实例会被放进JVM的java堆
- 一个线程产生一个java栈,当运行到一个方法就创建一个栈帧(包含局部变量表、操作栈、方法返回值),将它入栈,方法执行结束出栈。
Java 集合
Java集合两大接口: Collection
接口,主要用于存放单一元素;Map
接口,主要用于存放键值对
1. List
-
List和ArrayList的区别
- List是一个接口,而ArrayList是List接口的一个实现类
-
ArrayList和LinkedList区别
2. Set
3. Queue
4. Map
Map常见实现类 底层+线程安全:
HashMap
-
使用ArrayList、HashMap,需要线程安全怎么办呢?
Collections.synchronizedList(list);
Collections.synchronizedMap(m);
底层使用synchronized代码块锁虽然也是锁住了所有的代码,但是锁在方法里边,并所在方法外边性能可以理解
-
构造方法(四种)
- HashMap有几个构造方法,但最主要的就是指定初始值大小和负载因子的大小。
- 如果我们不指定,默认HashMap的大小为16,负载因子的大小为0.75
- 在HashMap里用的是**位运算((n - 1) & hash)**来代替取模,能够更加高效地算出该元素所在的位置。
- 为什么HashMap的大小只能是2次幂,因为只有大小为2次幂时,才能合理用位运算替代取模。
- 而负载因子的大小决定着哈希表的扩容和哈希冲突。
- 比如现在我默认的HashMap大小为16,负载因子为0.75,这意味着数组最多只能放12个元素,一旦超过12个元素,则哈希表需要扩容。
-
put()方法
- 首先对key做hash运算,计算出该key所在的index。
- 如果没碰撞,直接放到数组中,如果碰撞了,需要判断目前数据结构是链表还是红黑树,根据不同的情况来进行插入。
- 假设key是相同的,则替换到原来的值。最后判断哈希表是否满了(当前哈希表大小*负载因子),如果满了,则扩容
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // table未初始化或者长度为0,进行扩容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已经存在元素(处理hash冲突) else { Node<K,V> e; K k; //快速判断第一个节点table[i]的key是否与插入的key一样,若相同就直接使用插入的值p替换掉旧的值e。 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); // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法 // 这个方法会根据 HashMap 数组来决定是否转换为红黑树。 // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 跳出循环 break; } // 判断链表中结点的key值与插入的元素的key值是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循环 break; // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 p = e; } } // 表示在桶中找到key值、hash值与插入元素相等的结点 if (e != null) { // 记录e的value V oldValue = e.value; // onlyIfAbsent为false或者旧值为null if (!onlyIfAbsent || oldValue == null) //用新值替换旧值 e.value = value; // 访问后回调 afterNodeAccess(e); // 返回旧值 return oldValue; } } // 结构性修改 ++modCount; // 实际大小大于阈值则扩容 if (++size > threshold) resize(); // 插入后回调 afterNodeInsertion(evict); return null; }
-
get()方法
- 对key做hash运算,计算出该key所在的index,然后判断是否有hash冲突
- 假设没有冲突直接返回,假设有冲突则判断当前数据结构是链表还是红黑树,分别从不同的数据结构中取出。
HashMap v.s Hashtable(5点)
ConcurrentHashMap v.s Hashtable(2点)
- 底层数据结构不同
- 实现线程安全方式不同
Java并发
JMM(Java内存模型)
-
什么是JMM?
- Java 定义的并发编程相关的一组规范
- 除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范
- 主要目的是为了简化多线程编程,增强程序可移植性的。
-
为啥需要JMM?
- 并发编程下,CPU多级缓存和指令重排会导致程序运行出现一些问题,JMM定义一些规范解决这些问题。
- JMM 屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。
-
Java内存区域和 JMM 有什么区别?
- JVM 内存结构:和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例。
- Java 内存模型:和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的。
-
JMM如何抽象线程和主内存之间的关系?
-
Java内存模型(不仅仅是JVM内存分区):调用栈和本地变量存放在线程栈上,对象存放在堆上。
-
主内存 & 本地内存
- 主内存:所有线程创建的实例对象都放主内存
- 本地内存:每个线程都有一个本地内存存储共享变量的副本(本地内存时JMM抽象出来的概念)
-
JMM示意图:每个线程有个本地内存放副本,共享变量放主内存中
-
从示意图看,线程1和2咋通信?
- 线程1:本地内存中修改过的共享变量副本值–(同步)–>主内存
- 线程2:到主内存中读取对应共享变量的值
-
多线程下,可能出现的线程安全问题?
- when 线程1修改共享变量,线程2读取同一个共享变量,线程2读取的是修改前的值还是修改后的?
- 不确定!因为线程1和2都是先将共享变量 主内存–(拷贝)–>对应线程工作内存
-
So,JMM定义了8种同步操作&一些同步规则,规定一个变量如何从工作内存同步到主内存
- 同步操作:lock、unlock、read、load、use、assign、store、write
-
-
happens-before 原则
-
程序员追求:易于理解和编程的强内存模型;编译器和处理器追求:较少约束的弱内存模型
-
happens-before 设计思想
- 编译器和处理器的约束尽可能少->只要不改变程序的执行结果,编译器和处理器怎么进行重排序都行*(比如两个赋值语句)*
- 对改变程序执行结果的重排序,编译器和处理器必须禁止*(比如赋完值以后再用这个值)*
-
和 JMM 的关系
-
-
并发编程三个重要特性
-
原子性
含义:一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。
Java实现:
synchronized
、各种Lock
以及各种原子类synchronized
和各种Lock
可以保证任一时刻只有一个线程访问该代码块,因此可以保障原子性。各种原子类是利用 CAS (compare and swap) 操作(可能也会用到volatile
或者final
关键字)来保证原子操作。 -
可见性
含义:一个线程对共享变量进行修改,另外的线程立即可以看到修改后的最新值。
Java实现:
synchronized
、volatile
以及各种Lock
-
有序性
含义:代码的执行顺序未必就是编写代码时候的顺序。
Java实现:
volatile
关键字可以禁止指令进行重排序优化
线程
- 什么是线程?
- 比进程更小的执行单位
- 线程切换工作时负担比进程小得多
- 进程和线程的区别
- 进程可以有多个线程,同类的多个线程共享进程的堆和方法区资源,每个线程有自己的程序计数器、虚拟机栈和本地方法栈
- 各进程是独立的,同一进程中的线程可能会互相影响
- 线程执行开销小,但不利于资源的管理和保护;进程相反
- 为啥程序计数器是私有的?
- 为了线程切换后能恢复到正确的执行位置
- 为啥虚拟机栈和本地方法栈私有?
- 为了保证线程中的局部变量不被别的线程访问到
- 堆和方法区了解
- 堆是进程中最大的一块内存,主要用于存放新创建的对象
1. 进线程区别
- 进程是系统进行资源分配和调度的独立单位,每一个进程都有它自己的内存空间和系统资源
- 进程实现多处理机环境下的进程调度,分派,切换时,都需要花费较大的时间和空间开销
- 为了提高系统的执行效率,减少处理机的空转时间和调度切换的时间,以及便于系统管理,所以有了线程,线程取代了进程了调度的基本功能
- 简单来说,进程作为资源分配的基本单位,线程作为资源调度的基本单位
- 1.从资源角度:进程是系统资源分配的最小单元,线程是cpu分配的最小单元。
2.从从属关系来看:一个进程可以包含一个或多个线程。
3.从切换的角度:进程间切换的代价高,线程间切换的代价低
4.从jvm的角度:进程内的线程会共享进程的 堆 方法区;线程独享 虚拟机栈 本地方法栈 程序计数器。
2. 多线程
- 为什么使用多线程?
- 提高资源利用效率
- 总体上
- 计算机底层:线程切换调度成本小于进程;多核CPU->多个线程可以同时运行,减少上下文切换开销
- 互连网发展趋势:多线程是高并发系统的基础,可以提高系统整体的并发能力和性能
- 计算机底层
- 单核时代:提高单进程利用CPU和IO系统的效率,一个线程IO阻塞,其他线程还能用CPU
- 多核时代:提高进程利用多核CPU的能力
- 多线程带来的问题?
- 内存泄露、死锁、线程不安全
- 实际应用
- 要跑一个定时任务,该任务的链路执行时间和过程都非常长,我这边就用一个线程池将该定时任务的请求进行处理。
- 这样做的好处就是可以及时返回结果给调用方,能够提高系统的吞吐量。
3. 线程安全
-
什么是线程安全?
-
在Java世界里边,所谓线程安全就是多个线程去执行某类,这个类始终能表现出正确的行为,那么这个类就是线程安全的。
-
-
怎么解决线程安全问题?
-
其实大部分时间我们在代码里边都没有显式去处理线程安全问题,因为这大部分都由框架所做了,Tomcat、Druid、SpringMVC等等
-
解决线程安全问题的思路有以下:
- 能不能保证操作的原子性,考虑atomic包下的类够不够我们使用。
- 能不能保证操作的可见性,考虑volatile关键字够不够我们使用
- 如果涉及到对线程的控制(比如一次能使用多少个线程,当前线程触发的条件是否依赖其他线程的结果),考虑CountDownLatch/Semaphore等等。
- 如果是集合,考虑java.util.concurrent包下的集合类。
- 如果synchronized无法满足,考虑lock包下的类(盲目使用会影响程序性能)
-
总的来说,就是先判断有没有线程安全问题,如果存在则根据具体的情况去判断使用什么方式去处理线程安全的问题。
-
-
死锁
- 死锁原因:当前线程拥有其他线程需要的资源,当前线程等待其他线程已拥有的资源,都不放弃自己拥有的资源。
- 避免死锁:
- 固定加锁的顺序,比如我们可以使用Hash值的大小来确定加锁的先后
- 尽可能缩减加锁的范围,等到操作共享变量的时候才加锁。
- 使用可释放的定时锁(一段时间申请不到锁的权限了,直接释放掉)
4. 线程通信
- 共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信。
- volatile共享内存
- 消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信。
- wait/notify等待通知方式
join方式
- wait/notify等待通知方式
- 管道流
- 管道输入/输出流的形式
5. 创建线程的三种方式
-
1、继承Thread类
- 重写run方法,start()启动
class MyThread extends Thread{ @Override public void run(){ System.out.println("这是重写的run方法,也叫执行体"); System.out.println("线程号:" + currentThread().getName()); } } public class Test{ public static void main(String[] args) throws Exception{ Thread t1 = new MyThread(); t1.start(); } }
- 优点:简单,访问当前现线程直接使用currentThread()
- 缺点:继承Thread类,无法继承其他类
-
2、实现Runable接口
class MyThread implements Runable{ @Override public void run(){ System.out.println("这是重写的run方法,也叫执行体"); System.out.println("线程号:" + Thread.currentThread().getName()); } } public class Test{ public static void main(String[] args) throws Exception{ MyThread myThread = new MyThread(); Thread t1 = new Thread(myThread); t1.start(); } }
- 优点:可以继承别的类,多个线程共享一个对象,适合处理同一份资源的情况
- 缺点:访问当前线程需要使用Thread.currentThread()
-
3、Callable接口:
- 实现Callble接口,重写call()方法,作为执行体;
- 创建实现类的实例,用FutureTask包装;
- 使用FutureTask对象作为Thread对象创建线程;
- 使用FutureTask对象的get()方法获得子线程执行结束后的返回值
class MyThread implements Callable{ @Override public Object call() throws Exception{ System.out.println("线程号:" + Thread.currentThread().getName()); return 10; } } public class Test{ public static void main(String[] args) throws Exception{ Callable callable = new MyThread(); FutureTask task = new FutureTask(callable); new Thread(task).start(); System.out.println(task.get()); Thread.sleep(10);//等待线程执行结束 //task.get() 获取call()的返回值。若调用时call()方法未返回,则阻塞线程等待返回值 //get的传入参数为等待时间,超时抛出超时异常;传入参数为空时,则不设超时,一直等待 System.out.println(task.get(100L, TimeUnit.MILLSECONDS)); } }
-
Runnable和Callable的区别:
- Callable规定的方法是call(),Runnable规定的方法是run().
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
- call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
-
start()与run()的区别:
- start用于启动线程,线程处于就绪状态,直到得到CPU时间片才运行,再自动执行run方法;
- run方法只是类的普通方法,直接调用相当于只有主线程一个线程;
-
sleep() 与 wait() 与 join() 与 yield():
- 作用:sleep()与wait()都可以暂停线程执行
- sleep:线程让出CPU资源,不会释放锁;
- wait:线程让出CPU, 释放锁;与notify(), notifyAll() 一起使用
- 基于哨兵思想:检查特定条件直到满足,才继续进行
- 需要监视器监视当前线程:如synchronized, Condition类
- notify:随机唤醒单个线程
- notifyAll:唤醒所有线程
- 基于哨兵思想:检查特定条件直到满足,才继续进行
- yield:暂停当前线程,给其他具有相同优先级的线程(包括自己)运行的机会;
- join:让主线程等待子线程结束之后在结束; 比如需要子线程的运行结果的时候,由子线程调用;
- sleep 和 yield 是Thread的静态方法; join是线程对象调用; wait, notify, notifyAll 是Object类的方法,所有对象都可以调用。
- 作用:sleep()与wait()都可以暂停线程执行
6. 生命周期&状态
六个状态:
-
NEW: 初始状态,线程被创建出来但没有被调用
start()
。 -
RUNNABLE: 运行状态,线程被调用了
start()
等待运行的状态。 -
BLOCKED :阻塞状态,需要等待锁释放。
-
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
-
TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
-
TERMINATED:终止状态,表示该线程已经运行完毕。
7. 上下文切换
- 什么是上下文?
- 线程执行中自己的运行条件和状态(比如程序计数器、栈信息
- 为什么会上下文切换?
- 主动出让CPU
- 时间片用完
- 调用了阻塞类型的系统中断
- 被终止或结束运行
- 什么是上下文切换?
- 线程切换,意味着要保存当前线程的上下文,留着线程下次占用CPU的时候恢复现场,并加载下一个将要占用CPU的线程上下文,所以要上下文切换
- 切换的时候干啥?
- 每次要保存信息恢复信息,占用CPU,所以不能频繁切换
8. sleep()和wait()
- 二者异同
- 为什么wait()不定义在Thread中?sleep()定义在Thread中?
wait()
是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁。- 每个对象(Object)都拥有对象锁
- 既然要释放当前线程占有的对象锁并让其进入WAITING状态,操作的对象自然是
Object
,而不是当前线程Thread
sleep()
是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁
- 可以直接调用Theard类的run方法吗?
- 调用
start()
方法方可启动线程并使线程进入就绪状态,直接执行run()
方法的话不会以多线程的方式执行。
- 调用
volatile
-
如何保证变量的可见性?
- 用 **
volatile
**关键字 - 如果我们将变量声明为
volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。 volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。
- 用 **
-
如何禁止指令重排序?
-
用 **
volatile
**关键字 -
如果我们将变量声明为
volatile
,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。 -
volatile使用场景:双重校验锁实现对象单例(线程安全)
-
public class Singleton { private volatile static Singleton uniqueInstance;//volatile修饰! private Singleton() {} public static Singleton getUniqueInstance() { if (uniqueInstance == null) {//没有实例化过才进入加锁代码 synchronized (Singleton.class) {//类对象加锁 if (uniqueInstance == null) { /*这句话分三步进行: 1.为 uniqueInstance 分配内存空间 2.初始化 uniqueInstance 3.将 uniqueInstance 指向分配的内存地址 JVM指令重排,可能变成1->3->2,因此需要volatile*/ uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
-
-
-
volatile 可以保证原子性么?
- 不能,只能保证可见性和有序性
- 怎么改进?
synchronized
、Lock
或者AtomicInteger
乐观锁/悲观锁
1. 悲观锁
- 假设最坏的情况,认为共享资源每次被访问的时候就会出现问题
- 每次获取资源都会上锁
- 共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
- 实现:
synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现 - 导致的问题
- 高并发场景下,锁竞争->线程阻塞->大量->系统上下文切换->增加系统开销
- 死锁
2. 乐观锁
- 假设最好的情况,认为共享资源每次被访问的时候都不会出现问题
- 只有提交修改的时候验证对应资源是否被其他线程修改了(版本号 or CAS)
- 实现:
java.util.concurrent.atomic
包下面的原子变量类(比如AtomicInteger
、LongAdder
) - 实现方法:
- 版本号机制
- 数据表中加上一个数据版本号
version
字段,表示数据被修改的次数 - 数据被修改时,version值+1
- 当线程 A 要更新数据值时,在读取数据的同时也会读取
version
值,在提交更新时,若刚才读取到的 version 值为当前数据库中的version
值相等时才更新,否则重试更新操作,直到更新成功。
- 数据表中加上一个数据版本号
- CAS(比较与交换)
- 思想:用一个预期值和要更新的变量值进行比较,两值相等才会进行更新
- 三个操作数
- V :要更新的变量值(Var)
- E :预期值(Expected)
- N :拟写入的新值(New)
- 当且仅当V==E时,CAS通过原子方式用N更新V;
- V!=E时,说明有其它线程更新了V,则当前线程放弃更新。
- 版本号机制
- 问题:
- ABA问题
- 描述:变量V刚开始是A值,最后也是A值,能证明其它线程没改过它吗?不能!这就是ABA问题
- 解决:变量前面追加版本号或者时间戳
- 循环时间长开销大
- 描述:CAS会用到自旋进行重试,不成功就一直循环执行直到成功;长时间不成功,会给CPU带来非常大的时间开销
- 解决:JVM支持的pause命令,作用有两个
- 延迟流水线执行指令,使CPU不会消耗过多的执行资源
- 避免退出循环时因内存顺序引起CPU流水线被清空,提高CPU效率
- 只能保证一个共享变量的原子操作
- 描述:CAS只对单个共享变量有效,跨多个共享变量时无效
- 解决:JDK1.5之后,提供了
AtomicReference
类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行 CAS 操作
- ABA问题
synchronized
synchronized
主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
1. 使用场景
-
1. 修饰实例方法(锁当前对象实例)
-
给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 。
-
synchronized void method() { //业务代码 }
-
-
2. 修饰静态方法(锁当前类)
-
给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。
-
synchronized static void method() { //业务代码 }
-
-
3. 修饰代码块(锁指定对象/类)
-
synchronized(object)
表示进入同步代码库前要获得 给定对象的锁。 -
synchronized(类.class)
表示进入同步代码前要获得 给定 Class 的锁 -
synchronized(this) { //业务代码 }
-
-
注意:构造方法不能用synchronized修饰!(构造方法本身就线程安全)
2. 底层原理
底层依赖于JVM,分为修饰方法和修饰代码块两个部分
-
synchronized 同步语句块(monitor两个指令)
-
synchronized
同步语句块的实现使用的是monitorenter
和monitorexit
指令,其中monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。 -
在执行**
monitorenter
**时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。 -
对象锁的的拥有者线程才可以执行
monitorexit
指令来释放锁。在执行monitorexit
指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。 -
如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
-
-
synchronized 修饰方法(
ACC_SYNCHRONIZED
标识)-
synchronized
修饰的方法并没有monitorenter
指令和monitorexit
指令,取得代之的确实是ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。JVM 通过该ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 -
如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。
-
-
同步语句块用
monitorenter
指令和monitorexit
指令,修饰方法用**ACC_SYNCHRONIZED
** 标识,不过两者的本质都是对对象监视器 monitor 的获取
- monitor是啥?
- Monitor 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。
- Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联,同时 monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
Synchronized
是通过对象内部的一个叫做监视器锁(monitor)来实现的,monitor锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为重量级锁。
3. 和volatile区别
synchronized
和 volatile
是互补的存在,区别有四点:
- 性能:
volatile
关键字是线程同步的轻量级实现,性能比synchronized好 - 使用:
volatile
只能用于变量,synchronized
可以修饰方法以及代码块 - 并发特性:
volatile
能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证 - 场景:
volatile
主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性
ReentrantLock
ReentrantLock
实现了Lock
接口,是一个可重入且独占式的锁- 底层:AQS(
AbstractQueuedSynchronizer
)ReentrantLock
里面有一个内部类Sync
,Sync
继承 AQS,添加锁和释放锁的大部分操作实际上都是在Sync
中实现的Sync
有公平锁FairSync
和非公平锁NonfairSync
两个子类- 公平锁:先等待的线程先获得锁
- 非公平锁:随机线程获得锁,性能更好,但可能某些线程永远无法获得锁(
ReentrantLock
默认)
和synchronized区别
-
都是可重入锁(相同点)
-
可重入锁也叫递归锁,指的是线程可以再次获取自己的内部锁
-
JDK所有现成的
Lock
实现类,包括synchronized
关键字锁都是可重入的
-
-
依赖对象不同
-
synchronized 依赖于 JVM
-
ReentrantLock 依赖于 API(需要lock()和unlock()方法配合try/finally语句块完成)
-
-
ReentrantLock功能更多
ReentrantLock
增加了三点高级功能:- 等待可中断:正在等待的线程可以选择放弃等待,处理其他事情
- 可中断锁:获取锁的过程可以被中断,进行其他逻辑处理【ReentrantLock】
- 不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理【synchronized】
- 可实现公平锁:
ReentrantLock
可以指定是公平锁还是非公平锁(默认非公平)synchronized
只能是非公平锁
- 可实现选择性通知(锁可以绑定多个条件):
synchronized
关键字与wait()
和notify()
/notifyAll()
方法相结合可以实现等待/通知机制ReentrantLock
类需要借助于Condition
接口与newCondition()
方法实现
- 等待可中断:正在等待的线程可以选择放弃等待,处理其他事情
AQS(抽象队列)
AQS就是一个抽象类,翻译过来就是抽象队列同步器
使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock
,Semaphore
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
}
1. 原理
核心思想:
-
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
-
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,
这个机制 AQS 是用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中。
- CLH队列是一个虚拟的双向队列
- AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配
- 在 CLH 同步队列中,一个节点表示一个线程
AQS 使用 int 成员变量 state
表示同步状态,通过内置的 线程等待队列 来完成获取资源线程的排队工作
// 共享变量,使用volatile修饰保证线程可见性
private volatile int state;
2. Semaphore作用
-
作用:
Semaphore
(信号量)可以用来控制同时访问特定资源的线程数量(想OS中咋用的) -
// 初始共享资源数量 final Semaphore semaphore = new Semaphore(5); // 获取1个许可 semaphore.acquire(); // 释放1个许可 semaphore.release();
当初始的资源个数为 1 的时候,Semaphore
退化为排他锁。
-
Semaphore 两种模式:
-
公平模式: 调用
acquire()
方法的顺序就是获取许可证的顺序,遵循 FIFO; -
非公平模式: 抢占式的。
-
-
应用场景:
Semaphore
通常用于那些资源有明确访问数量限制的场景比如限流
3. Semaphore原理
-
Semaphore
是共享锁的一种实现,它默认构造 AQS 的state
值为permits
-
调用
semaphore.acquire()
:相当于OS中P(S),state>=0表示可以获取成功,使用CAS操作state减1;state<0表示数量不足,创建Node结点加入阻塞队列,挂起当前线程 -
调用
semaphore.release()
:相当于OS中V(S),使用CAS操作state+1,释放许可证成功后,唤醒同步队列中的一个线程。被唤醒的线程重新尝试state-1,如果state>=0则获取令牌成功,否则重新进入阻塞队列,挂起线程
4. CountDownLatch
-
作用:允许count个线程阻塞在一个地方,直到所有线程任务执行完毕;一次性的,计数器的值只能在构造方法中初始化一次,使用完毕后不能再次使用
-
原理:共享锁的一种实现,默认构造AQS的state值为count
- 使用countDown()方法:CAS操作减少state,直到state为0
- 使用await()方法:
- state不为0,说明任务还没执行完毕,await()一直阻塞,之后的语句不会被执行
CountDownLatch
会自旋 CAS 判断state == 0
,是的话释放所有线程,await()之后的语句执行
-
场景:多线程读取多个文件处理,具体如下:
- 读取处理6个文件,没有执行顺序依赖,但返回给用户时要将几个文件的处理结果进行统计整理
- 定义:线程池+count为6的
CountDownLatch
对象 - 线程池读取任务,处理完后count-1,调用
CountDownLatch
对象的await()
方法,所有文件读取完后截止执行后面的逻辑
基于AQS管程的CountDownLatch工具类配合线程池模拟了一个高并发的场景,对缓存进行了一个测试,我先让主线程创建一个CountDownLatch,然后把它的初始值设为一,然后创建一个线程池,在线程池中创建一百个线程让他们启动起来之后都调用CountDownLatch.await()方法进入条件变量等待队列等待,然后主线程等待一秒等线程池中的线程都起动起来进入等待状态,然后主线程调用CountDownLatch.countDown()将CountDownLatch减为一,然后线程池中的大量线程就会唤醒,这样就模拟出来了一个高并发的场景对缓存进行测试,是这样的一个测试
ThreadLocal
1. 作用+场景
-
作用:本地线程变量,让每个线程绑定自己的值,可以将
ThreadLocal
类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。 -
使用场景:解决多线程中因为数据并发产生不一致的问题。
-
1、存储用户Session
-
2、线程间数据隔离
-
3、进行事务操作,用于存储线程事务信息。
-
4、数据库连接,
Session
会话管理。 -
5、数据跨层传递(controller,service,dao):
- 用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
- 在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。
- 比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。(线程隔离,表面上他们使用的是同一个ThreadLocal, 但是实际上使用的值value却是自己独有的一份)
package com.kong.threadlocal; public class ThreadLocalDemo05 { public static void main(String[] args) { User user = new User("jack"); new Service1().service1(user); } } class Service1 { public void service1(User user){ //给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。 UserContextHolder.holder.set(user); new Service2().service2(); } } class Service2 { public void service2(){ User user = UserContextHolder.holder.get(); System.out.println("service2拿到的用户:"+user.name); new Service3().service3(); } } class Service3 { public void service3(){ User user = UserContextHolder.holder.get(); System.out.println("service3拿到的用户:"+user.name); //在整个流程执行完毕后,一定要执行remove UserContextHolder.holder.remove(); } } class UserContextHolder { //创建ThreadLocal保存User对象 public static ThreadLocal<User> holder = new ThreadLocal<>(); } class User { String name; public User(String name){ this.name = name; } } 执行的结果: service2拿到的用户:jack service3拿到的用户:jack
-
ThreadLocal 在项目中的使用 - 知乎 (zhihu.com):
- 问题:用户认证中,拦截器获取用户身份信息,为了后续业务逻辑代码方便使用,用redis做缓存,但是value存到redis后,用户的key值必须一步步往下传递,丑陋
- 解决:
ThreadLocal
作为一级缓存使用, redis 作为二级缓存使用,同一个线程内, 用户信息所有方法共享。任何地方只要需要使用用户信息, 直接从threadlocal 取。
-
2. 原理+实现
-
ThreadLocal
类原理:-
每个线程都有一个
ThreadLocalMap
(ThreadLocal内部类) -
Map 中元素的键为
ThreadLocal
,而值对应线程的变量副本。 -
Map 是数组实现,使用线性探测解决hash冲突,需要手动调用set、get、remove防止内存泄漏。
-
-
Thread
类源代码:
public class Thread implements Runnable {
//......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......
}
- set()方法:
- 最终的变量是放在了当前线程的
ThreadLocalMap
中,并不是存在ThreadLocal
上,ThreadLocal
可以理解为只是ThreadLocalMap
的封装,传递了变量值。
- 最终的变量是放在了当前线程的
public void set(T value) {
//获取当前请求的线程
Thread t = Thread.currentThread();
//取出 Thread 类内部的 threadLocals 变量(哈希表结构)
ThreadLocalMap map = getMap(t);
if (map != null)
// 将需要存储的值放入到这个哈希表中
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
- get()方法:
public T get() {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//3、如果map数据不为空,
if (map != null) {
//3.1、获取threalLocalMap中存储的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
常用方法还有**remove()
**。
ThreadLocal
数据结构:- 每个
Thread
中都具备一个ThreadLocalMap
(ThreadLocal
的内部静态类),而ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。
- 每个
3. 内存泄露问题
-
内存泄露
-
ThreadLocalMap
中使用的 key 为ThreadLocal
的弱引用,而 value 是强引用。所以,如果ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,
ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,产生内存泄露。
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k);//代表ThreadLocal对象是一个弱引用 value = v; } }
- (实际私用ThreadLocal场景都是采用线程池,而线程池中的线程都是复用的,这样就可能导致非常多的entry(null,value)出现,从而导致内存泄露)
-
-
如何避免?
- 每次使用完
ThreadLocal
都调用它的**remove()
方法清除数据** - 尽可能不让它在线程存储值,避免使用线程池的时候值一直在线程对象存储
- 每次使用完
-
为什么key不是强引用?
- key如果是强引用,且没有手动remove,会和value一样伴随线程的整个生命周期
- 假设使用完
ThreadLocal
,引用被回收,但是由于ThreadLocalMap
的Entry强引用了ThreadLocal
, 造成ThreadLocal
无法被回收,也无法避免内存泄露。 - key弱引用原因:事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null 进行判断,如果为 null 的话,那么会把 value 置为 null 的.这就意味着使用threadLocal , CurrentThread 依然运行的前提下,就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收,对应value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏
-
如何正确使用
ThreadLocal
?- 将
ThreadLocal
变量定义成**private static
**的,这样的话ThreadLocal
的生命周期就更长,由于一直存在ThreadLocal
的强引用,所以ThreadLocal
也就不会被回收,也就能保证任何时候都能根据ThreadLocal
的弱引用访问到Entry的value值,然后remove它,防止内存泄露 - 每次使用完ThreadLocal,都调用它的**remove()**方法,清除数据。
- 将
线程池
线程池就是管理一系列线程的资源池,有任务处理->线程池拿线程->处理完不销毁->线程等待下一个任务
1. 线程池作用
提供限制和管理资源的方式 && 维护一些基本统计信息
- **降低资源消耗。**重复利用已创建的线程降低创建销毁的消耗。
- **提高响应速度。**任务到达时,任务不需要等到线程创建就能立即执行。
- **提高线程可管理性。**线程池统一分配、调优和监控线程。
2. 创建线程池方法
-
ThreadPoolExecutor
构造函数源码:在执行的时候,重点就在于它维护了一个ctl参数,这个ctl参数的用高3位表示线程池的状态,低29位来表示线程的数量
-
Executor
框架的工具类Executors
3. 线程池饱和策略
线程达到最大数量,队列也被放满了任务,ThreadPoolExecutor.
定义的策略:
.AbortPolicy
(默认):抛出异常拒绝新任务的处理。.CallerRunsPolicy
:调用自己的线程运行任务。.DiscardPolicy
:不处理新任务,直接丢弃。.DiscardOldestPolicy
:丢弃最老未处理的任务请求。
4. 常用阻塞队列
新任务到来->当前运行线程数==核心线程数?->√->新任务放队列
不同线程池用不同的阻塞队列
LinkedBlockingQueue
(无界队列):队列永远不会被放满SynchronousQueue
(同步队列):没有容量,不存储元素。DelayedWorkQueue
(延迟阻塞队列):内部采用“堆”
5. 线程池处理任务流程
6. 线程池大小
-
太大->增加上下文切换成本
-
太小->大量请求/任务堆积
-
CPU密集型任务(N+1):线程数设置为 N(CPU核心数)+1
-
I/O密集型任务(2N):线程处理I/O不会占用CPU,可以将CPU交出给其他线程,所以可以多配点线程
7. 线程池参数
ThreadPoolExecutor
3 个最重要的参数:
corePoolSize
:核心线程池容量 ,任务队列未达到队列容量时,最大可以同时运行的线程数量。maximumPoolSize
:最大线程池容量 ,任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue
:工作队列容量, 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor
其他常见参数 :
keepAliveTime
:线程池中的线程数量大于corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime
才会被回收销毁;unit
:keepAliveTime
参数的时间单位。threadFactory
:executor 创建新线程的时候会用到。handler
:饱和策略。
Future
- 核心思想:异步调用,主要用在多线程领域
- 作用:用在执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低
- 功能:在java中是个泛型接口,包含下面四个功能
- 取消任务;
- 判断任务是否被取消;
- 判断任务是否已经执行完成;
- 获取任务执行结果。
(简单理解就是:我有一个任务,提交给了 Future
来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后,我就可以 Future
那里直接取出任务执行结果。)
-
Callable和Future的关系
-
FutureTask
提供了Future
接口的基本实现,常用来封装Callable
和Runnable
-
//FutureTask 有两个构造函数,可传入Callable或者Runnable对象 public FutureTask(Callable<V> callable) { if (callable == null) throw new NullPointerException(); this.callable = callable; this.state = NEW; } public FutureTask(Runnable runnable, V result) { // 通过适配器RunnableAdapter来将Runnable对象runnable转换成Callable对象 this.callable = Executors.callable(runnable, result); this.state = NEW; }
-
JVM
一、内存结构
运行时数据区域
JDK1.7运行时数据区域:线程共享 堆、方法区、直接内存;线程私有程序计数器、虚拟机栈、本地方法栈
JDK1.8运行时数据区域:
线程私有:程序计数器+俩栈
-
程序计数器:很小的内存空间,当前线程的行号指示器
- 作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如顺序、选择、循环
- 多线程下,程序计数器用于记录当前线程执行的位置
- 注意:程序计数器是唯一一个不会出现
OutOfMemoryError
的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
- 作用:
-
虚拟机栈:保存方法的局部变量、操作数、动态链接、方法返回地址
- 单位:栈帧,栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
- 程序运行中栈的两种错误:
StackOverFlowError
:栈内存大小不允许动态扩展,线程请求栈深度>当前 JVM 栈最大深度,抛出OutOfMemoryError
:栈内存大小允许动态扩展,JVM 动态扩展栈时无法申请到足够的内存空间,抛出
-
本地方法栈
- 和虚拟机栈的区别:
- 虚拟机栈为虚拟机执行 Java 方法(字节码)服务
- 本地方法栈为虚拟机使用到的 Native 方法 服务
- 本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
- 方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现
StackOverFlowError
和OutOfMemoryError
两种错误。
- 和虚拟机栈的区别:
线程共享:堆+方法区+直接内存
-
堆:运行时数据区域,保存所有类的实例和数组
-
JVM管理内存中最大的一块;所有线程共享的一块内存区域
-
垃圾收集器管理的主要区域,因此也被称作 GC 堆
-
堆内存结构:新生代(Eden,s0,s1)、老年代(tenured)、永久代/元空间(元空间使用本地内存)
- 对象都会首先在 Eden 区域分配
- 在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1
- 当它的年龄增加到阈值(默认为 15 岁,
-XX:MaxTenuringThreshold
设置),就会被晋升到老年代中
-
堆容易出现的
OutOfMemoryError
错误:java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
:JVM花太多时间垃圾回收,只能回收很少的堆空间java.lang.OutOfMemoryError: Java heap space
:堆内存空间不足以存放新创建的对象
-
字符串常量池
-
作用:避免字符串重复创建,是JVM为提升性能+减少内存消耗 为String类专门开辟的一块区域
-
// 在堆中创建字符串对象”ab“ // 将字符串对象”ab“的引用保存在字符串常量池中 String aa = "ab"; // 直接返回字符串常量池中字符串对象”ab“的引用 String bb = "ab"; System.out.println(aa==bb);// true!!!
-
存放位置:JDK1.7之前-永久代;1.7之后-字符串常量池和静态变量移动到堆中
-
-
-
方法区:存储加载的类信息、class/method/field等元数据、常量、静态变量
-
1.8之后拆成了 加载的类信息(元数据区) 和 运行时常量池(堆)
-
存放的是程序中永远唯一的元素(感觉就是类、方法的定义结果,以及静态变量)
-
和永久代以及元空间的关系:
- 类比:永久代、元空间->类(具体实现);方法区->接口(抽象概念)
-
元空间溢出会得到OOM错误:
java.lang.OutOfMemoryError: MetaSpace
- 你可以使用
-XX:MaxMetaspaceSize
标志设置最大元空间大小,默认值为 unlimited - 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由
MaxPermSize
控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。
- 你可以使用
-
运行时常量池
- .class文件中:类的版本、字段、方法、接口等描述信息+常量池表
- 有啥:各种字面量+符号引号
- 字面量:源代码中固定值的表示法,包括整数、浮点数、字符串字面量(通过字面就能知道含义
- 符号引用:包括类符号引用、字段符号引用、方法符号引用、接口方法引用…
-
-
直接内存(非运行时数据区域)
- 特殊的内存缓冲区,并不在java堆或方法区中分配,在本地内存上分配
- 不是虚拟机运行时数据区域,也不是虚拟机规范中定义的内存区域,但也被频繁使用。也可能OOM。
HotSpot虚拟机对象
1. 对象创建(默写)
- 类加载检查
- 检查这个指令的参数是否能在常量池中定位到这个类的符号引用
- 检查这个符号引用代表的类是否已被加载过、解析和初始化过
- 如果没有,那必须先执行相应的类加载过程。
- 分配内存
- 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
- 对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
- 分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
- 内存分配的两种方式:
- 指针碰撞:
- 适用场合:堆内存规整(即没有内存碎片)的情况下。
- 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
- 使用该分配方式的 GC 收集器:Serial, ParNew
- 空闲列表:
- 适用场合:堆内存不规整的情况下。
- 原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
- 使用该分配方式的 GC 收集器:CMS
- 指针碰撞:
- 内存分配并发问题
- 创建对象时要保证线程安全,虚拟机采用两种方式来保证:
- CAS+失败重试:CAS 是乐观锁的一种实现方式。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
- 创建对象时要保证线程安全,虚拟机采用两种方式来保证:
- 初始化零值:内存分配完后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
- 设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,对象的哈希码,这些信息存放在对象头中。
- 执行init方法:虚拟机视角对象已经产生,但从java视角,对象创建才刚开始,
<init>
方法还没有执行,所有的字段都还为零。一般来说,执行 new 指令之后会接着执行<init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
2. 对象的内存布局
Object对象占用16字节,对象在内存中的布局可以分为 3 块区域:对象头(8个字节)、实例数据和对齐填充。
-
对象头具体包含哪些内容?
-
Mark Word:存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等
-
Klass pointer:虚拟机通过这个指针来确定这个对象是哪个类的实例
-
数组长度(仅针对数组对象)
-
-
JVM是怎么升级锁的?
-
无锁->偏向锁->轻量级锁->重量级锁。(随着竞争的激烈而逐渐升级,只能升级,不能降级)
-
无锁:
- 没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功
-
偏向锁:
- 指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。
-
轻量级锁(自旋锁):
- 当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。
-
重量级锁:
- 当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
-
3. 对象的访问定位
Java 程序通过栈上的 reference 数据来操作堆上的具体对象。访问对象的两种主流方式:
-
使用句柄:Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
(好处:reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。)
-
直接指针:reference 中存储的直接就是对象的地址。(速度快,节省一次指针定位开销)
二、类加载机制
- 类加载器:通过类的全限定性类名(带包路径,如java.lang.String)获取该类的二进制字节流
Bootstrap
(启动类加载器):加载java核心库(如$JAVA_HOME/jre/lib/rt.jar)- 好处:不需要太多的安全检查,提升性能
Extension
(扩展类加载器):加载拓展库($JAVA_HOME/jre/lib/ext/*.jar)AppClass
(应用程序类加载器):根据java的类路径加载类,一般的java应用的类($CLASSPATH)- 用户自定义:
- 加载方式:双亲委派机制
- 过程:
- 收到类加载请求,不会自己加载,而是逐级向上委托,最终到达顶层的Bootstrap;
- 如果父类加载器完成,则返回;否则,交给子类尝试。
- 好处:
- 避免类的重复加载:java类由于加载机制,形成了优先级的层次关系
- 防止核心类被篡改:防止下层的同名类篡改核心API库
- 打破这个机制:
- 意义:由于类加载器受到加载范围的限制,某些情况下父类加载器无法加载需要的类(不属于其加载范围,但根据机制只能其加载),所以需要子类加载器加载。
- 方法:使用自定义加载器
- 如何:重写loadClass()方法 or 重写findClass()方法
- 1、找到ClassLoader类的loadClass()方法,拷贝
- 2、写自定义加载器类,粘贴
- 3、删去loadClass()中的双亲委派机制的部分
- 4、判断类是否为系统类,是的话使用双亲委派机制,否则自己的方法加载。
- 应用:
- Tomcat:webappClassLoader加载自己目录下的class文件,目的;1、隔离每个webapp下的class和lib;2、使用单独的加载器以免被恶意破坏;3、热部署(修改文件不用重启自动重新装载)
- 过程:
三、垃圾回收(GC)
1. 引用类型
2. 垃圾收集
-
垃圾:不再被使用的对象,死亡的对象
-
哪些垃圾需要回收?
-
引用计数法:每个对象添加一个引用计数器,当为0时,就表示死亡;
- 问题:循环引用,不可回收造成内存泄露;空间占用
-
可达性分析算法:以根对象集合(GC Roots)为起点,分析GC Roots连接的对象是否可达,解决了循环引用问题。
-
GC Roots:就是对象!
- 1、虚拟机栈中的栈帧中的局部变量所引用的对象;
- 2、java本地方法栈中(java本地接口)引用的对象;
- 3、方法区中静态变量引用的对象;(全局对象)
- 4、方法区中常量引用的对象(全局对象)
- 5、所有被同步锁持有的对象
-
问题:多线程下更新了访问过的对象的引用
-
误报:原被引用的对象不再被引用。影响较小,顶多减少GC次数。
-
漏报:将已被访问过的对象设置为未被访问过。影响较大,可能会使引用对象被GC,导致jvm崩溃。
- 解决方法:Stop-the-world,等所有线程到达安全点,再进行Stop-the-world。
- Stop-the-world:用户运行至安全点(safe point)或者安全区域之后,就会挂起,进入暂停状态。
- 解决方法:Stop-the-world,等所有线程到达安全点,再进行Stop-the-world。
-
-
-
-
什么时候回收?
- Minor GC:young gc,发生在年轻代的gc
- 大多数情况下,对象直接在年轻代中的Eden区进行分配,如果Eden区域没有足够的空间,那么就会触发YGC(Minor GC)
- Major GC:old gc,发生在老年代的gc
- **Full GC:**回收整个堆和方法区
- 触发条件:
- System.gc()
- 通过Minor GC进入老年代的平均大小 > 老年代的可用内存
- 老年代空间不足;
- 触发条件:
- Minor GC:young gc,发生在年轻代的gc
3. 垃圾回收算法(内存回收方法论)
4. 垃圾收集器(内存回收具体实现)
-
Serial:单 新 复制
-
单线程,所有线程stw,
-
新生代标记-复制,老年代标记-整理
-
缺点:需要停止所有工作线程,效率低
-
场景:对应用的实时性要求不高的client级别(桌面应用)的默认方式,单核服务器
-
-
ParNew:多 新 复制
-
Serial的多线程版本,stw, 复制算法
-
新生代标记-复制,老年代标记-整理
-
实际线程默认和cpu数量相同
-
优点:有效利用cpu
-
缺点:和Serial一样
-
场景:Sever模式下的新生代,和CMS配合
-
-
Parallel Scavenge:多 新 复制
-
新生代收集器、复制算法,多线程
-
与ParNew不同点:追求和精确控制高吞吐量,而ParNew尽可能缩短用户线程的停顿时间;
- 高吞吐:线程运行时间/线程运行时间 + 垃圾回收时间
- 例子:PS就是100秒回收一次垃圾,持续10秒,吞吐量为90%;PN就是50秒回收一次垃圾,持续7秒,吞吐量为86%。
-
场景:注重高效利用CPU
-
Serial Old:单 老 整理
-
Serial的老年代版本,标记整理算法
-
场景:client、单核。与PS收集器搭配
-
-
Parallel Old:多 老 整理
-
Parallel Scavenge的老年代版本,多线程,标记整理算法
-
JDK7,8默认老年代收集器
-
-
CMS:多 老 清除
-
Concurrent Mark Sweep
-
多线程、标记清除算法
-
特点:获取最短回收停顿时间
-
-
流程:
- 初始标记:GC Roots直接关联的对象,需要Stw
- 并发标记:与用户线程并发,不需要Stw(所以对象可能发生变化),执行Gc Roots Tracing(遍历整个对象图)
- 重新标记:修正并发标记期间,发生变动的一些标记,需要Stw
- 并发清除:标记清除,不会stw
- 并发重置:移除标记,避免影响下次gc
-
三色标记算法(用于并发标记阶段)
-
黑色对象不再扫描:第一次扫描的时候已经标记完成,并且孩子也标记,自己已经标记,fields都标记完成
-
灰色:自己标记完成,还没来得及标记fields,则继续扫描其孩子就可以了
-
白色:没有遍历到的结点
-
问题:
-
错标(标记过不是垃圾的,变成了垃圾,也叫浮动垃圾)
- 如下图,遍历到E,变灰,此时执行
objD.fieldE = null
,DE断开,EFG变成不可达,应该回收,但因为E灰,仍然会被当做存活对象遍历下去,EFG不会被回收
- 如下图,遍历到E,变灰,此时执行
-
漏标(如下图,GC 线程已经遍历到E,E变灰,应用进程执行
1. G = objE.fieldG; 2. objE.fieldG = null; 3. objD.fieldG = G;
,EG断开,此时切回到GC,因为EG断,G不会灰;尽管D重新引用G,但D已黑,所以不会重新遍历,G就会一直白,被当做垃圾处理,影响程序正确性)
-
-
解决方法:CMS用写屏障+增量更新处理,G1用写屏障+SATB原始快照处理
- 写屏障:指给某个对象的成员变量赋值操作前后,加入一些处理
- 增量更新:当对象 D 的成员变量的引用发生变化时(
objD.fieldG = G;
),我们可以利用写屏障,将 D 新的成员变量引用对象 G 记录下来。即:不要求保留原始快照,而是针对新增的引用,将其记录下来等待遍历。 - SATB:当对象 E 的成员变量的引用发生变化时(
objE.fieldG = null;
),我们可以利用写屏障,将 E 原来成员变量的引用对象 G 记录下来。即:当原来成员变量的引用发生变化之前,记录下原来的引用对象。
-
-
优点:并发收集,低停顿
-
缺点:
- 对CPU资源敏感
- 标记清除产生空间碎片
- 并发清除过程中会产生新的垃圾,只能等下一次
-
G1:多 新/老 整理+复制
-
标记整理(整体) + 复制(局部)
-
特点:
- 并行与并发
- 分代收集:自己采用不同的收集方式去处理不同代的垃圾,不需要和其他收集器合作
- 空间整合:无空间碎片
- 可预测的停顿:可预测时间的停顿模型
-
原理:
-
将java堆分为大小相同的独立区域Region,新生代和老年代区域混合;
- 并发标记:估计哪些region基本是垃圾,就会从这些块下手(所以叫G1);
- 停顿预测模型:
- 根据之前垃圾回收的数据统计,估计出用户指定停顿时间内的回收块个数;
- “尽力”满足指定的目标停顿时间,基于目标选择回收的区块数量;
-
-
-
-
步骤
- Minor GC
* 复制算法、并行、stw * 动态调整年轻区大小:根据历史数据,和用户指定的停顿时间目标
-
老年代收集:
- 初始标记:stw,伴随young gc,对Gc Roots直接关联对象进行标记
- 扫描根引用区:即survivor区的对象直接到老年代对象的引用。(因为进行了YGC,新生代中只有survivor区存在对象)
- 并发标记:寻找整个堆的存活对象,并发执行,可以被YGC中断
- 如果整个region都是垃圾,直接被回收
- 标记的同时计算region活性比例
- 最终标记:stw,完成最后的标记,使用了STAB 算法
- 由于用户线程同时进行,需要修正之前的标记
- 采用了比CMS更快的初始快照法:SATB算法
- 筛选回收:stw,复制算法。计算各个region的存活对象和垃圾比例,排序,识别可以混合回收的区域 。
- 并发清理:识别并清理全是垃圾的老年代region
-
混合式收集:
- 除了整个年轻代收集, 还有上个阶段标记出的老年代的垃圾最多的区块;
- 持续回收所有被标记的分区
- 恢复到年轻代收集,进行新的周期
- 必要时的Full GC:
- G1的初衷就是避免Full GC
总结
-
7种收集器关系
如果两个收集器之间存在连线,就说明它们可以搭配使用
大致思路:串行Serial不够用了->并行PS->想和用户线程并发->CMS,但需要和新生代配合,PS不行->设计ParNew和CMS配合
- 收集器流程
- 收集器总结
收集器 | 串/并行/并发 | 新/老 | 收集算法 | 目标 | 场景 |
---|---|---|---|---|---|
Serial | 串行 | 新 | 复制 | 响应速度优先 | 单CPU环境下的Client模式 |
Serial Old | 串行 | 老 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
ParNew | 并行 | 新 | 复制 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新 | 复制 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
G1 | 并发 | both | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用,将来替换CMS |
5. GC调优
- 常用工具
jstat
- 首先我们使用
jstat
查看了 GC 的情况。又通过查看GC log,分析了GC 的详细状况。 - 使用
jstat -gcutil ${pid} 1000
每隔一秒打印一次 GC 统计信息。 - 直接查看 GC log 不太直观,可以借助一些可视化JVM分析工具来帮助我们分析
- 首先我们使用
- 在线分析工具GCeasy
- 我们把 GC log 上传到https://gceasy.io后, GCeasy 会根据GC log生成各个维度的图表,让我们更直观的分析JVM问题。
-
什么是heap dump
Heap dump文件是一个二进制文件,它保存了某一时刻JVM堆中对象使用情况。Heap Dump文件是指定时刻的Java堆栈的快照,是一种镜像文件。Heap Dump一般都包含了一个堆中的Java Objects, Class等基本信息。同时,当你在执行一个转储操作时,往往会触发一次GC,所以你转储得到的文件里包含的信息通常是有效的内容(包含比较少,或没有垃圾对象了) 。我们可以这么理解:heap dump记录内存信息的,thread dump是记录CPU信息的。
-
怎么得到heap dump
一般来说,两种方式:
-
事先开启HeadDumpOnOutOfMemoryError,这样出现OOM的时候能自动留下Dump,留好第一现场。这是最推荐的方式。
JVM的启动参数中加入如下的一些参数:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/oom //第一个参数意思是在OOM的时候自动dump内存快照出来,第二个参数是说把内存快照存放在哪里
-
手工得到heap dump,这个方式就很多了,命令或者一些第三方工具,都可以获得指定java进程的heap dump。比如jdk自带的jvisualvm.exe,这个是图形化的也不用额外下载。其他方式可以自己去,有很多。通过jvisualvm.exe获取heap jump.
-
- GC问题判断
- 常见场景分析与解决
JVM频繁full gc问题排查
-
常见原因:full gc 触发条件是 老年代空间不足, 所以追因的方向就是导致 老年代空间不足的原因:大量对象频繁进入老年代 + 老年代空间释放不掉
- 系统并发高、执行耗时过长,或者数据量过大,导致 young gc频繁,且gc后存活对象太多,但是survivor 区存放不下(太小 或 动态年龄判断) 导致对象快速进入老年代 老年代迅速堆满
- 发程序一次性加载过多对象到内存 (大对象),导致频繁有大对象进入老年代 造成full gc
- 存在内存溢出的情况,老年代驻留了大量释放不掉的对象, 只要有一点点对象进入老年代 就达到 full gc的水位了
- 元数据区加载了太多类 ,满了 也会发生 full gc
- 堆外内存 direct buffer memory 使用不当导致
- 也许, 你看到老年代内存不高 重启也没用 还在频繁发生full gc, 那么可能有人作妖,在代码里搞执行了 System.gc();
-
定位思路:如果有监控,那么通过图形能比较直观、快速的了解gc情况;
如果没有监控,那么只能看gc日志或jstat来分析 这是基本技能 一定要熟练- 观察年轻代 gc的情况,多久执行一次、每次gc后存活对象有多少 survivor区多大
存活对象比较多 超过survivor区大小或触发动态年龄判断 => 调整内存分配比例 - 观察老年代的内存情况 水位情况,多久执行一次、执行耗时多少、回收掉多少内存
如果在持续的上涨,而且full gc后回收效果不好,那么很有可能是内存溢出了 => dump 排查具体是什么玩意 - 如果年轻代和老年代的内存都比较低,而且频率低 那么又可能是元数据区加载太多东西了
- 其实如果是自己负责的系统,可能要看是不是发版改了什么配置、代码
- 观察年轻代 gc的情况,多久执行一次、每次gc后存活对象有多少 survivor区多大
频繁young gc问题排查
-
分析方法
- 分析 GC 日志:可以通过启用 GC 日志并分析日志来了解 Young GC 的发生情况和原因。可以使用工具如 GCEasy 和 GCViewer 等来分析 GC 日志。
- 使用 JDK 工具:JDK 提供了一些工具来分析和调试垃圾回收问题,如 jstat、jmap、jstack 等。可以使用这些工具来分析 Young GC 的情况。
- 增加内存监控和报警:如果 Young GC 次数频繁,可以考虑增加内存监控和报警机制,及时发现和解决问题。
-
解决方法
- 增加新生代大小:如果频繁发生 Young GC,可以尝试增加新生代大小,让更多的对象存活于新生代中,减少垃圾回收次数。
- 增大Young gc触发阙值
- 调整堆内存大小:如果堆内存太小,可能会导致频繁进行 Young GC,因此可以尝试增加堆内存大小。
- 检查代码中的内存泄漏:频繁进行 Young GC 可能是由于程序存在内存泄漏导致的。可以通过分析堆转储文件来检查是否存在内存泄漏。
- 调整 GC 策略:Java 提供了多种 GC 策略,可以尝试调整 GC 策略来优化 Young GC 的性能。比如:采用g1回收器
四、JVM调优
JVM参数总结
-
堆内存相关
-
显式指定堆内存
–Xms
和-Xmx
-Xms<heap size>[unit] # 最小堆内存,如-Xms2G -Xmx<heap size>[unit] # 最大堆内存,如-Xmx5G
-
显式新生代内存(Young Generation)
-
方式一:
-XX:NewSize=<young size>[unit] # 最小新生代内存,如-XX:NewSize=256m -XX:MaxNewSize=<young size>[unit] # 最大新生代内存,如-XX:NewSize=1024m
-
方式二:
-Xmn256m # 新生代分配256m内存(最小、最大相同)
-
Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法
-
方式三:通过
-XX:NewRatio=<int>
来设置老年代与新生代内存的比值。-XX:NewRatio=1 # 设置老年代和新生代所占比值为1:1
-
-
显式指定永久代/元空间的大小
-
jdk1.8之前:永久代还没有被移除
-XX:PermSize=N #方法区 (永久代) 初始大小 -XX:MaxPermSize=N #方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
-
jdk1.8:元空间取代永久代
-XX:MetaspaceSize=N # Metaspace使用过程中触发Full GC的阈值,只对触发起作用,元空间初始大小对64位JVM来说是固定的,所以这句话不是设置元空间初始大小的 -XX:MaxMetaspaceSize=N #设置 Metaspace 的最大大小
-
-
-
垃圾回收相关
-
垃圾回收器:JVM的四种GC实现
-XX:+UseSerialGC #串行 -XX:+UseParallelGC #并行 -XX:+UseParNewGC #CMS -XX:+UseG1GC #G1
-
GC日志记录:一定会配置打印GC日志的参数,便于分析GC相关问题
# 必选 # 打印基本 GC 信息 -XX:+PrintGCDetails -XX:+PrintGCDateStamps # 打印对象分布 -XX:+PrintTenuringDistribution # 打印堆数据 -XX:+PrintHeapAtGC # 打印Reference处理信息 # 强引用/弱引用/软引用/虚引用/finalize 相关的方法 -XX:+PrintReferenceGC # 打印STW时间 -XX:+PrintGCApplicationStoppedTime # 可选 # 打印safepoint信息,进入 STW 阶段之前,需要要找到一个合适的 safepoint -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 # GC日志输出的文件路径 -Xloggc:/path/to/gc-%t.log # 开启日志文件分割 -XX:+UseGCLogFileRotation # 最多分割几个文件,超过之后从头文件开始写 -XX:NumberOfGCLogFiles=14 # 每个文件上限大小,超过就触发分割 -XX:GCLogFileSize=50M
-
-
处理OOM
- JVM 提供了一些参数,这些参数将堆内存转储到一个物理文件中,以后可以用来查找泄漏:
# 指示JVM在遇到OOM错误时将heap转储到物理文件中 -XX:+HeapDumpOnOutOfMemoryError # HeapDumpPath表示要写入文件的路径; 可以给出任何文件名; 但是,如果 JVM 在名称中找到一个 <pid> 标记,则当前进程的进程 id 将附加到文件名中,并使用.hprof格式 -XX:HeapDumpPath=./java_pid<pid>.hprof # 发出紧急命令,以便在内存不足的情况下执行。 # 内存不足时重启服务器,可以设置参数: -XX:OnOutOfMemoryError="shutdown -r" -XX:OnOutOfMemoryError="< cmd args >;< cmd args >" # 一种策略,限制在抛出OOM错误之前在 GC 中花费的 VM 时间的比例 -XX:+UseGCOverheadLimit
-
其他
-server
: 启用“ Server Hotspot VM”; 此参数默认用于 64 位 JVM-XX:+UseStringDeduplication
: Java 8u20 引入了这个 JVM 参数,通过创建太多相同 String 的实例来减少不必要的内存使用; 这通过将重复 String 值减少为单个全局char []
数组来优化堆内存。-XX:+UseLWPSynchronization
: 设置基于 LWP (轻量级进程)的同步策略,而不是基于线程的同步。- ``-XX:LargePageSizeInBytes `: 设置用于 Java 堆的较大页面大小; 它采用 GB/MB/KB 的参数; 页面大小越大,我们可以更好地利用虚拟内存硬件资源; 然而,这可能会导致 PermGen 的空间大小更大,这反过来又会迫使 Java 堆空间的大小减小。
-XX:MaxHeapFreeRatio
: 设置 GC 后, 堆空闲的最大百分比,以避免收缩。-XX:SurvivorRatio
: eden/survivor 空间的比例, 例如-XX:SurvivorRatio=6
设置每个 survivor 和 eden 之间的比例为 1:6。-XX:+UseLargePages
: 如果系统支持,则使用大页面内存; 请注意,如果使用这个 JVM 参数,OpenJDK 7 可能会崩溃。-XX:+UseStringCache
: 启用 String 池中可用的常用分配字符串的缓存。-XX:+UseCompressedStrings
: 对 String 对象使用byte []
类型,该类型可以用纯 ASCII 格式表示。-XX:+OptimizeStringConcat
: 它尽可能优化字符串串联操作
OOM排查
-
为什么有gc还会出现内存泄露?
- 通过这种有向图的内存管理方式,当一个内存对象失去了所有的引用之后,GC 就可以将其回收。反过来说,如果这个对象还存在引用,那么它将不会被 GC 回收,哪怕是 Java 虚拟机抛出 OOM。
-
java内存泄露的根本原因?
- 内存对象明明已经不需要的时候,还仍然保留着这块内存和它的访问方式(引用)。
-
1、堆内存不足
java.lang.OutOfMemoryError: Java heap space
-
原因
1、代码中可能存在大对象分配
2、可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。 -
解决方法
1、检查是否存在大对象的分配,最有可能的是大数组分配
2、通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题
3、如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存
4、还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性
-
2、永久代/元空间溢出
java.lang.OutOfMemoryError: PermGen space java.lang.OutOfMemoryError: Metaspace
-
原因
永久代是 HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。
JDK8后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:
- 字符串常量由永久代转移到堆中
- 和永久代相关的JVM参数已移除
出现永久代或元空间的溢出的原因可能有如下几种:
1、在Java7之前,频繁的错误使用String.intern方法
2、生成了大量的代理类,导致方法区被撑爆,无法卸载
3、应用长时间运行,没有重启 -
解决方法
1、检查是否永久代空间或者元空间设置的过小
2、检查代码中是否存在大量的反射操作
3、dump之后通过mat检查是否存在大量由于反射生成的代理类
4、放大招,重启JVM
-
3、方法栈溢出
java.lang.OutOfMemoryError : unable to create new native Thread
-
原因
出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过jstack出来一共8000多个线程。
-
解决方法
1、通过
-Xss
降低的每个线程栈大小的容量
2、线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:- /proc/sys/kernel/pid_max
- /proc/sys/kernel/thread-max
- max_user_process(ulimit -u)
- /proc/sys/vm/max_map_count
-
4、GC overhead limit exceeded
java.lang.OutOfMemoryError:GC overhead limit exceeded
-
原因
这个是JDK6新加的错误类型,一般都是堆太小导致的。
Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。 -
解决方法
1、检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
2、添加参数-XX:-UseGCOverheadLimit
禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。
3、dump内存,检查是否存在内存泄露,如果没有,加大内存。
设计模式
单例模式
- 什么是单例模式?
- 在整个运行时域,一个类只有一个实例对象
- 单例模式有什么好处?
- 对于有一些对象,其实例我们只需要一个,比方说:线程池、缓存(cache)、日志对象等,如果创建多个实例,就会导致许多问题产生,比如资源使用过量、程序行为不可控,或者导致不一致的结果。
- 单例模式注意的三个问题
- 线程安全?
- 懒加载?
- 通过反射破坏?
-
懒汉式 - 线程不安全
- 最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
- 这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
public class Singleton{ private static Singleton instance; private Singleton(){} public static Singleton getInstance(){ if(instance == null){ instance = new Singleton(); } return instance; } }
-
懒汉式 - 线程安全:在方法声明时加锁
- 这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
- 优点:第一次调用才初始化,避免内存浪费。
- 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
- getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
public class Singleton{ private static Singleton instance; private Singleton(){} public static synchronized Singleton getInstance(){ if(instance == null){ instance = new Singleton(); } return instance; } }
-
双重检验锁(DCL)
这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
-
第一次判断是否为 null:
- 第一次判断是在Synchronized同步代码块外,理由是单例模式只会创建一个实例,并通过 getUniqueInstance 方法返回 Singleton 对象,所以如果已经创建了 Singleton 对象,就不用进入同步代码块,不用竞争锁,直接返回前面创建的实例即可,这样大大提升效率。
-
第二次判断 uniqueInstance 是否为 null:
- 第二次判断原因是为了保证同步;假若线程A通过了第一次判断,进入了同步代码块,但是还未执行,线程B就进来了(线程B获得CPU时间片),线程B也通过了第一次判断(线程A并未创建实例,所以B通过了第一次判断),准备进入同步代码块,假若这个时候不判断,就会存在这种情况:线程B创建了实例,此时恰好A也获得执行时间片,如果不加以判断,那么线程A也会创建一个实例,就会造成多实例的情况。
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进⼊加锁代码 if (uniqueInstance == null){ //类对象加锁 synchronized (Singleton.class) { if (uniqueInstance == null){ uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
-
-
饿汉式 - static final field
- 这种方式比较常用,但容易产生垃圾对象。
- 优点:没有加锁,执行效率会提高。
- 缺点:类加载时就初始化,浪费内存。
- 它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
public class Singleton{ //类加载时就初始化 private static final Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return instance; } }
-
静态内部类 - static nested class
这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
public class Singleton{ private static class SingletonHolder(){ private static final Singleton instance = new Singleton(); } private Singleton(){} public static final Singleton getInstance(){ return SingletonHolder.instance; } }
项目中:一般我们项目里用静态内部类的方式实现单例会比较多(如果没有Spring的环境下),代码简洁易读
如果有Spring环境,那还是直接交由Spring容器管理会比较方便(Spring默认就是单例的)
-
枚举 - Enum
- 这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
public class Singleton{ private Singleton(){} /** *枚举类型是线程安全的,并且只会装载一次 */ public enum SingletonEnum{ INSTANCE; private final Singleton instance; SingletonEnum(){ instance = new Singleton(); } private Singleton getInstance(){ return instance; } } public static Singleton getInstance(){ return SingletonEnum.INSTANCE.getInstance(); } }
工厂模式
工厂模式最主要解决的问题就是创建者和调用者的耦合,那么代码层面其实就是取消对new的使用。
Spring IOC容器可以理解为应用了「工厂模式」(通过ApplicationContext或者BeanFactory去获取对象)
-
简单工厂模式
也叫静态工厂模式
你要去买一台手机,你不用关心手机是怎么生产出来的,里面的零件具体又是怎么制造的,这些通通都交给工厂去处理,你尽管去买手机就好了。
问题:
随着手机品牌增多,工厂生产也需要对应的增加,工厂内部就需要不断的调整。
从代码层面——对内部代码需要增加(也就是需要修改内部代码:那么就会违反OOP原则—开闭原则:一个软件实体应当对扩展开放,对修改关闭 -
工厂方法模式
当新的手机品牌出现,不是放在同一个工厂生产,而是自己拥有独立工厂生产。那么就解决了上面静态工厂模式违反关闭原则的问题。
工厂方法模式解决简单工厂模式是需要付出代价的!
看到上图工厂方法模式图里新增用虚线画的Huawei品牌,每新增一个品牌就需要增加,对应新的工厂,会发现需要花费很大的成本,现在才三个新的品牌,那么等到十个、一百个的时候就会变得更加的复杂和难以维护。 -
抽象方法模式
在工厂方法模式中,一个具体的工厂负责生产一类具体的产品,即一对一的关系,但是,如果需要一个具体的工厂生产多种产品对象,那么就需要用到抽象工厂模式了。
- 简单工厂 : 使用一个工厂对象用来生产同一等级结构中的任意产品。(不支持拓展增加产品)
- 工厂方法 : 使用多个工厂对象用来生产同一等级结构中对应的固定产品。(支持拓展增加产品)
- 抽象工厂 : 使用多个工厂对象用来生产不同产品族的全部产品。(不支持拓展增加产品;支持增加产品族)
责任链模式
比如说,我这边在处理请求的时候,会用到责任链模式进行处理(减免if else 并且让项目结构更加清晰)
Jdk中的设计模式
JDK中涉及的设计模式总结-CSDN博客
MySQL
一、存储引擎
-
MySQL 支持哪些存储引擎
- MySQL 5.5.5 前:默认MyISAM;后:默认InnoDB
- 只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。
-
存储引擎架构
- 插件式架构,支持多种存储引擎(不同数据库表可以设置不同搜索引擎)
- 存储引擎是基于表的,而不是数据库
-
MyISAM 和 InnoDB 区别(7点)
- MyISAM 性能还行,特性不错,但不支持事务和行级锁,最大的缺陷就是崩溃后无法安全恢复。
- 为什么InnoDB性能更强大?
- InnoDB 的性能比 MyISAM 更强大,不管是在读写混合模式下还是只读模式下,随着 CPU 核数的增加,InnoDB 的读写能力呈线性增长(InnoDB是支持高并发)。MyISAM 因为读写不能并发(是串行的),它的处理能力跟核数没关系。
- InnoDB是怎样支持高并发的?
- Innodb控制并发的手段有两个:锁(共享锁和排它锁:读读并行) 和 MVCC(读写并行)
二、索引
常见的索引结构有: B 树, B+树 和 Hash、红黑树。在 MySQL 中,无论是 Innodb 还是 MyIsam,都使用了 B+树作为索引结构。(相当于数据的目录)
1. 优缺点
优点 :
- 使用索引可以大大加快 数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。
- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
缺点 :
- 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
- 索引需要使用物理文件存储,也会耗费一定空间。
2. 底层数据结构
B&B+树
-
为啥不用哈希表做MySQL索引数据结构?
- Hash 索引不支持顺序和范围查询,并且,每次 IO 只能取一个。
- 试想,
SELECT * FROM tb1 WHERE id < 500;
,树直接遍历比 500 小的叶子节点就够了;哈希还要把1-499数据hash计算一遍来定位
-
B树和B+树区别?(3点)
-
B+树比B树的优势
1.单一节点存储更多的元素,使得查询的IO次数更少;
2.所有查询都要查找到叶子节点,查询性能稳定;
3.所有叶子节点形成有序链表,便于范围查询。
3. 索引类型
-
主键索引:一张数据表只能有一个主键,且不能为null,不能重复
-
二级索引
- 二级索引的叶子节点存储的数据是主键,而不是实际数据。也就是说,通过二级索引,可以定位主键的位置。
- 分类
- 唯一索引:属性列不能出现重复数据,是一种约束,不是为了查询效率
- 普通索引:唯一作用就是为了快速查询数据
- 前缀索引:只适用于字符串,对文本前几个字符创建索引
- 全文索引:为了检索大文本数据中的关键字
-
聚簇索引&非聚簇索引
- 聚簇索引data放数据,非聚簇索引data放指向数据的指针,示意图如下:
- 聚簇索引
- 优点
- 查询速度非常快:因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。相比于非聚簇索引, 聚簇索引少了一次读取数据的 IO 操作。
- 对排序查找和范围查找优化:聚簇索引对于主键的排序查找和范围查找速度非常快。
- 缺点
- 依赖于有序的数据:因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
- 更新代价大:如果对索引列的数据被修改时,那么对应的索引也将会被修改,而且聚簇索引的叶子节点还存放着数据,修改代价肯定是较大的,所以对于主键索引来说,主键一般都是不可被修改的。
- 优点
- 非聚簇索引
- 优点
- 更新代价比聚簇索引要小 。非聚簇索引的更新代价就没有聚簇索引那么大了,非聚簇索引的叶子节点是不存放数据的
- 缺点
- 依赖于有序的数据:跟聚簇索引一样,非聚簇索引也依赖于有序的数据
- 可能会二次查询(回表):这应该是非聚簇索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
- 优点
-
覆盖索引
-
如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为 覆盖索引
-
InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。而覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!
-
如下图,查询的name字段就是索引,查到name直接返回就行,不用再找name对应的key:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uDgIIvmr-1686815973685)(C:\Users\HP\AppData\Roaming\Typora\typora-user-images\image-20230601222840313.png)]
-
-
联合索引
- 使用表中的多个字段创建索引,就是 联合索引
- 比如,将商品表中的 product_no 和 name 字段组合成联合索引
(product_no, name)
,创建联合索引的方式如下:
CREATE INDEX index_product_no_name ON product(product_no, name);
-
最左前缀匹配原则:在使用联合索引时,MySQL会根据联合索引中的字段顺序,从左到右依次到查询条件中去匹配。所以,我们在使用联合索引时,可以将区分度高的字段放在最左边,这也可以过滤更多数据。
- 如果不遵循最左匹配原则,联合索引会失效
- 比如,如果创建了一个
(a, b, c)
联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:- where a=1;
- where a=1 and b=2 and c=3;
- where a=1 and b=2;
- 需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要。
- 但是,如果查询条件是以下这几种,因为不符合最左匹配原则,联合索引就会失效:
- where b=2;
- where c=3;
- where b=2 and c=3;
- 上面这些查询条件之所以会失效,是因为
(a, b, c)
联合索引,是先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。所以,b 和 c 是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的。
-
以联合索引(a,b)为例,B+树(叶子节点是双向链表)如下:
- 可以看到,a 是全局有序的(1, 2, 2, 3, 4, 5, 6, 7 ,8),而 b 是全局是无序的(12,7,8,2,3,8,10,5,2)。因此,直接执行
where b = 2
这种查询条件没有办法利用联合索引的,利用索引的前提是索引里的 key 是有序的。
- 可以看到,a 是全局有序的(1, 2, 2, 3, 4, 5, 6, 7 ,8),而 b 是全局是无序的(12,7,8,2,3,8,10,5,2)。因此,直接执行
-
联合索引范围查询
-
并不是查询过程使用了联合索引查询,就代表联合索引中的所有字段都用到了联合索引进行索引查询,也就是可能存在部分字段用到联合索引的 B+Tree,部分字段没有用到联合索引的 B+Tree 的情况。这种特殊情况就发生在范围查询!
-
联合索引的最左匹配原则会一直向右匹配直到遇到「范围查询」就会停止匹配。也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。
-
比如:
-
select * from t_table where a > 1 and b = 2
- a字段在联合索引的 B+Tree 中进行索引查询
- 但是在符合 a > 1 条件的二级索引记录的范围里,b 字段的值是无序的(a==5,b==8;a==6,b==10;a==7,b==5)
- 有 a 字段用到了联合索引进行索引查询,而 b 字段并没有使用到联合索引
-
select * from t_table where a >= 1 and b = 2
- a字段变成大于等于!a字段用联合索引
- 虽然在a>=1条件的二级索引范围里,b字段无序,但是对于a=1的二级索引范围里,b字段有序,因此可以通过a==1时,ab的联合索引减少需要扫描的二级索引范围
- a 和 b 字段都用到了联合索引进行索引查询
-
SELECT * FROM t_table WHERE a BETWEEN 2 AND 8 AND b = 2
- 由于 MySQL 的 BETWEEN 包含 value1 和 value2 边界值,所以类似于第2条查询语句
- a 和 b 字段都用到了联合索引进行索引查询
-
SELECT * FROM t_user WHERE name like 'j%' and age = 22
,联合索引(name, age)
-
a 字段可以在联合索引的 B+Tree 中进行索引查询,形成的扫描区间是[‘j’,‘k’)。
-
虽然在符合前缀为 ‘j’ 的 name 字段的二级索引记录的范围里,age 字段的值是「无序」的,但是对于符合 name = j 的二级索引记录的范围里,**age字段的值是「有序」**的
-
所以,当二级索引记录的 name 字段值为 ‘j’ 时,可以通过 age = 22 条件减少需要扫描的二级索引记录范围,因此age可以利用联合索引查询
- a 和 b 字段都用到了联合索引进行索引查询
-
-
-
索引下推
- 索引下推是 MySQL 5.6 版本中提供的一项索引优化功能,可以在非聚簇索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数。
三、事务
1. ACID
原子性(Atomicity
) : 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;【undo log】
一致性(Consistency
): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;【最终目的】
隔离性(Isolation
): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;【锁+MVCC】
持久性(Durability
): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。【redo log】
只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!
InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?
- 持久性是通过 redo log (重做日志)来保证的;
- 原子性是通过 undo log(回滚日志) 来保证的;
- 隔离性是通过 MVCC 或锁机制来保证的;
- 一致性则是通过持久性+原子性+隔离性来保证;
2. 并发事务问题
-
脏读(Dirty read)
某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的
-
丢失修改(Lost to modify)
在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
-
不可重复读(Unrepeatable read)
在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
-
幻读(Phantom read)
在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
3. 并发事务控制方式
锁
悲观控制模式
- 共享锁(S 锁) :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
- 排他锁(X 锁) :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。
MVCC
乐观控制模式
多版本并发控制(MVCC,Multiversion concurrency control) 在 MySQL 中实现所依赖的手段主要是: 隐藏字段、read view、undo log。
在事务隔离级别 RC
和 RR
(InnoDB 存储引擎的默认事务隔离级别)下,InnoDB
存储引擎使用 MVCC
(非锁定一致性读),但它们生成 Read View
的时机却不同
-
在 RC 隔离级别下的
每次select
查询前都生成一个Read View
(m_ids 列表) -
在 RR 隔离级别下只在事务开始后
第一次select
数据前生成一个Read View
(m_ids 列表)
4. 隔离级别
-
读取未提交(READ-UNCOMMITTED) : 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
-
读取已提交(READ-COMMITTED) : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
-
可重复读(REPEATABLE-READ) : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。(InnoDB 存储引擎默认隔离级别)
-
可串行化(SERIALIZABLE) : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
隔离级别是怎么实现的?
- 读取已提交 和 可重复读 隔离级别:基于 MVCC
- 可串行化 隔离级别:锁
四、锁机制
1. 行级锁、表级锁
- 表级锁:针对非索引字段,对当前操作的整张表加锁
- 优点:实现简单、资源消耗少、加锁快、不会出现死锁
- 缺点:触发锁冲突的概率最高,高并发下效率极低
- 行级锁:针对索引字段加的锁,只针对当前操作的行记录进行加锁
- 优点:大大减少db操作的冲突,加锁粒度最小,并发度高
- 缺点:加锁开销大,加锁慢,会出现死锁
- 注意事项:行锁针对索引字段加锁,当我们执行
UPDATE
、DELETE
语句时,如果WHERE
条件中字段没有命中唯一索引或者索引失效的话,就会导致扫描全表对表中的所有行记录进行加锁。 - 有哪几类行锁:
- 记录锁(Record Lock):单个行记录上的锁
- 间隙锁(Gap Lock):锁定一个范围,不包括记录本身
- 临键锁(Next-Key Lock):
- Record Lock+Gap Lock,锁定一个范围,包含记录本身
- 主要目的是为了解决幻读问题(MySQL 事务部分提到过)。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
- InnoDB 默认的隔离级别 可重复读 下,行锁默认使用的是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。
2. 共享锁、排它锁
-
共享锁(S 锁) :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
-
排他锁(X 锁) :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。
S锁 X锁 S锁 不冲突 冲突 X锁 冲突 冲突
3. 意向锁
-
作用:如何判断表中的记录没有行锁呢,一行一行遍历肯定是不行,性能太差。用意向锁可以快速判断是否可以对某个表使用表锁
-
分类
- 意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些记录加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
- 意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
-
意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。
-
意向锁之间是兼容的:
IS 锁 IX 锁 IS 锁 兼容 兼容 IX 锁 兼容 兼容 -
意向锁和(表级)共享锁、(表级)排它锁互斥:
IS 锁 IX 锁 S 锁 兼容 互斥 X 锁 互斥 互斥
4. 当前读、快照读
-
快照读(一致性非锁定锁):就是单纯的
SELECT
语句,但不包括下面这两类SELECT
语句:SELECT ... FOR UPDATE SELECT ... LOCK IN SHARE MODE
- 快照即记录的历史版本,每行记录可能存在多个历史版本
- 快照读的情况下,如果读取的记录正在执行 UPDATE/DELETE 操作,读取操作不会因此去等待记录上 X 锁的释放,而是会去读取行的一个快照。
- 什么时候用?
- 只有在事务隔离级别 RC(读取已提交) 和 RR(可重读)下,InnoDB 才会使用当前读
- 在 RC 级别下,对于快照数据,一致性非锁定读总是读取被锁定行的最新一份快照数据。
- 在 RR 级别下,对于快照数据,一致性非锁定读总是读取本事务开始时的行数据版本。
- 只有在事务隔离级别 RC(读取已提交) 和 RR(可重读)下,InnoDB 才会使用当前读
- 适用的业务场景:对于数据一致性要求不是特别高且追求极致性能
-
当前读 (一致性锁定读):就是给行记录加 X 锁或 S 锁
-
常见sql语句类型:
# 对读的记录加一个X锁 SELECT...FOR UPDATE # 对读的记录加一个S锁 SELECT...LOCK IN SHARE MODE # 对修改的记录加一个X锁 INSERT... UPDATE... DELETE...
-
5. 自增锁(了解)
- InnoDB 中的自增主键会涉及一种比较特殊的表级锁— 自增锁(AUTO-INC Locks)
五、日志
- undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC。
- redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复;
- binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制;
1. undo log(回滚)
一个事务在执行过程中,在还没有提交事务之前,如果 MySQL 发生了崩溃,要怎么回滚到事务之前的数据呢?
-
怎么用undo log实现回滚?
- 在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;
- 在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;
- 在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。
-
undo log两大作用:
-
实现事务回滚,保障事务的原子性。
事务处理过程中,如果出现了错误或者用户执行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
-
实现 MVCC。
MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
- RC隔离级别:每个select生成一个Read View,也意味着事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
- RR隔离级别:启动事务时生成一个Read View,整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。
-
-
undo log如何刷盘(持久化到磁盘)?
- 和数据页刷盘策略一样,需要redo log保证持久化
- buffer pool中有undo页,对undo页的修改会记录到redo log。redo log每秒刷盘,提交事务也会刷盘,undo log和数据页靠这个机制保证持久化
-
为什么需要Buffer pool?
-
Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能
-
有了Buffer Pool以后,
- 读取数据:先找BP,再找磁盘,找到就读取
- 修改数据:BP中找到就直接修改BP中数据所在的页,然后将其页设置为脏页;为了减少磁盘IO,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘
-
BP缓存什么?
- 以“页”为单位的原因:InnoDB 会把存储的数据划分为若干个页,以页作为磁盘和内存交互的基本单位,一个页的默认大小为 16KB。因此,Buffer Pool 同样按「页」来划分。
- 在 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的
16KB
的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页。 - BP存储内容:Buffer Pool 除了缓存「索引页」和「数据页」,还包括了 Undo 页,插入缓存、自适应哈希索引、锁信息等等。
-
undo页记录什么?
- 开启事务后,InnoDB 层更新记录前,首先要记录相应的 undo log,如果是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面。
-
查询一条记录,只需要缓冲一条记录吗?
- 查询一条记录时,InnoDB 是会把整个页的数据加载到 Buffer Pool 中,将页加载到 Buffer Pool 后,再通过页里的「页目录」去定位到某条具体的记录。
-
2. redo log(重做)
-
为什么需要redo log?
- buffer pool提高读写效率 --> 但是BP基于内存 --> 内存不可靠,断电重启没来得及落盘的脏页数据会丢失 --> 为了防止断电数据丢失的问题 --> 当一条记录需要更新时,InnoDB引擎先更新内存标记脏页,然后将对这个页的修改以redo log形式记录下来 --> 更新完成
-
redo log
(重做日志)是InnoDB
存储引擎独有的,它让MySQL
拥有了崩溃恢复能力。【持久性+完整性】 -
什么是redo log?
- 物理日志,记录某个数据也做了什么修改,每执行一个事务就会产生一条或者多条物理日志
- 事务提交时,先将redo log持久化到磁盘,可以不用等buffer pool里的脏页数据持久化到磁盘
- 系统崩溃时,虽然脏页数据没持久化,但是redo log持久化了,因此mysql重启后,根据redo log将所有数据恢复到最新状态
-
redo log 和 undo log 区别在哪?
- redo log 记录了此次事务「完成后」的数据状态,记录的是更新之后的值;
- undo log 记录了此次事务「开始前」的数据状态,记录的是更新之前的值;
- 事务提交之前发生崩溃,重启后通过undo log撤销事务;
- 事务提交之后发生崩溃,重启后通过redo log重做事务。
-
刷盘时机
(刷盘就是持久化到磁盘的意思…
InnoDB
存储引擎为redo log
的刷盘策略提供了innodb_flush_log_at_trx_commit
参数,它支持三种策略:- 0:设置为 0 的时候,表示每次事务提交时不进行刷盘操作
- 1:设置为 1 的时候,表示每次事务提交时都将进行刷盘操作(默认值)
- 2:设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 内容写入 page cache
3. binlog(归档)
-
什么是binlog?
- mysql完成一条更新语句之后,server层还会生成一条binlog,之后事务提交的时候,会将该事务执行过程中产生的所有binlog统一写入binlog文件
- binlog文件:记录了所有db 表结构变更 & 表数据修改的日志,不会记录查询类的操作,比如SELECT和SHOW操作
-
redo log 和 binlog 有什么区别?
-
不小心删库,能用redo log恢复数据吗?
- 不能!只能用binlog恢复!
- 因为redo log文件是循环写,边写边擦除,只记录未被刷入磁盘的物理日志,刷过的会从redo log中删除
- binlog文件保存的是全量的日志,即所有数据变更的情况!理论上只要在binlog上的数据都可以恢复
Redis
为什么用redis做mysql缓存?
1、高性能
- MySQL从硬盘读取,慢。将用户缓存数据存在redis中,下次直接从缓存获取,相当于直接操作内存。
- 为啥这么快?
- 基于内存,访问速度是磁盘的上千倍
- 基于reactor模式设计开发了一套高效事件处理模型,主要是单线程事件循环和io多路复用
- 内置多种优化后的数据结构实现
2、 高并发
- 单台设备的Redis的QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的10倍
- 直接访问 Redis 能够承受的请求远远大于直接访问 MySQL
数据结构
5 种基础数据结构 :String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
3 种特殊数据结构 :HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。
-
跳表
- 跳表(skiplists)是一种有序的数据结构,它通过在每个节点中维持多个指向其他的节点指针,从而达到快速访问队尾目的。
- 当数据量很大时,跳表的查找复杂度就是 O(logN)。
-
压缩列表
- 压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
- 压缩列表将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下一个节点。
- 在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。
-
特殊数据结构
- bitmap统计活跃用户
- Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 Bitmap 本身会极大的节省储存空间。
- HyperLogLog 统计页面 UV
- bitmap统计活跃用户
线程模型
IO多路复用
IO多路复用是啥?
- IO 多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;
- 一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;
- 没有文件句柄就绪就会阻塞应用程序,交出CPU。
- 多路是指网络连接,复用指的是同一个线程!
IO多路复用的三种机制?(都是系统调用)
- select
- poll
- epoll
- poll 事件回调机制
- 惊群现象:
缓存设计
缓存读写策略
1. 旁路缓存模式
- 适用:读请求比较多的场景
- 服务端同时维系 db 和 cache,以 db 的结果为准
- 缓存读写步骤:
- 写:
- 先更新 db
- 然后直接删除 cache
- 读:
- 从 cache 中读取数据,读取到就直接返回
- cache 中读取不到的话,就从 db 中读取数据返回
- 再把数据放到 cache 中
- 写:
2. 读写穿透
-
适用:读请求比较多的场景
-
原则:应用程序只和缓存交互,不再和数据库交互
-
读写穿透策略:
3. 写回策略
- 适用:写请求比较多的场景
- 策略:在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。
- 场景:不能应用到我们常用的数据库和缓存的场景中,是计算机体系结构中的设计,比如 CPU 的缓存
- 问题:数据不是强一致性的,而且会有数据丢失的风险
数据库和缓存一致性
1. 先更db还是先更缓存
-
先更db,再更缓存
如下图,更新顺序,最终db存2,缓存为1,不一致!
-
先更缓存,再更db
如下图,更新顺序,最终db存1,缓存为2,不一致!
无论是「先更新数据库,再更新缓存」,还是「先更新缓存,再更新数据库」,这两个方案都存在并发问题,当两个请求并发更新同一条数据的时候,可能会出现缓存和数据库中的数据不一致的现象。
2. 先更db还是先删缓存
阿旺定位出问题后,思考了一番后,决定在更新数据时,不更新缓存,而是删除缓存中的数据。然后,到读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。
先更db,再删缓存:cache aside 旁路缓存策略
- 适用:读请求比较多的场景
- 服务端同时维系 db 和 缓存,以 db 的结果为准
- 缓存读写步骤:
- 写策略:
- 先更新 db
- 然后直接删除 cache
- 读策略:
- 从 cache 中读取数据,读取到就直接返回
- cache 中读取不到的话,就从 db 中读取数据返回
- 再把数据放到 cache 中
- 写策略:
先删缓存,再更db
可以看到,先删除缓存,再更新数据库,在「读 + 写」并发的时候,还是会出现缓存和数据库的数据不一致的问题。
先更db,再删缓存
「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。
持久化机制
redis内存数据库,但会把缓存数据存到硬盘
->自带两种持久化技术:AOF日志&RDB快照
->redis默认开启RDB快照,重启redis,之前的缓存数据会被重新加载
RDB快照
- 通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本
- AOF 文件的内容是操作命令;
- RDB 文件的内容是二进制数据。
AOF日志
- AOF工作流程:
- 命令追加(append):写命令追加到AOF缓冲区
- 文件写入(write):AOF缓冲区->AOF文件
- 文件同步(fsync):根据fsync策略同步硬盘
- 文件重写(rewrite):AOF文件越来越大->定期重写,达到压缩目的
- 重启加载(load):redis重启时,加载AOF文件数据恢复
-
三种AOF持久化方式:
这 3 种持久化方式的主要区别在于
fsync
同步 AOF 文件的时机(刷盘)。appendfsync always
:主线程调用write
执行写操作后,后台线程(aof_fsync
线程)立即会调用fsync
函数同步 AOF 文件(刷盘),fsync
完成后线程返回,这样会严重降低 Redis 的性能(write
+fsync
)。appendfsync everysec
:主线程调用write
执行写操作后立即返回,由后台线程(aof_fsync
线程)每秒钟调用fsync
函数(系统调用)同步一次 AOF 文件(write
+fsync
,fsync
间隔为 1 秒)appendfsync no
:主线程调用write
执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次(write
但不fsync
,fsync
的时机由操作系统决定)。
怎么选择RDB还是AOF?
-
Redis 保存的数据丢失一些也没什么影响的话,可以选择使用 RDB。
-
不建议单独使用 AOF,因为时不时地创建一个 RDB 快照可以进行数据库备份、更快的重启以及解决 AOF 引擎错误。
-
如果保存的数据要求安全性比较高的话,建议同时开启 RDB 和 AOF 持久化或者开启 RDB 和 AOF 混合持久化。
生产问题
缓存穿透
-
大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
-
解决
- 非法请求的限制
- 业务层判断请求是否合法
- 缓存无效 key
- 如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间
- 布隆过滤器
- 把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
- 非法请求的限制
缓存击穿
-
缓存击穿中,请求的 key 对应的是 热点数据(频繁被访问的数据) ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
-
解决
- 设置热点数据永不过期或者过期时间比较长,由后台异步更新缓存。
- 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间,比如秒杀场景下的数据在秒杀结束之前不过期。
- 互斥锁方案:请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。
-
缓存穿透和缓存击穿有什么区别?
- 缓存穿透中,请求的 key 既不存在于缓存中,也不存在于数据库中。
- 缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)
缓存雪崩
-
缓存在同一时间大面积的失效 or Redis故障宕机,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
-
解决
- 针对 Redis 服务不可用的情况:
- 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
- 限流,避免同时处理大量的请求。
- 针对热点缓存失效的情况:
- 设置不同的失效时间比如随机设置缓存的失效时间。
- 缓存永不失效(不太推荐,实用性太差)。
- 设置二级缓存。
- 针对 Redis 服务不可用的情况:
-
缓存雪崩和缓存击穿有什么区别?
- 缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在与缓存中(通常是因为缓存中的那份数据已经过期)。
过期删除&内存淘汰策略
过期删除
-
每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。
-
当我们查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中:
-
如果不在,则正常读取键值;
-
如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。
-
-
redis使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。
- 惰性删除:不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
- 优点:因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。
- 缺点:如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。
- 定期删除:每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
- 优点:通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
- 缺点:难以确定删除操作执行的时长和频率。如果执行的太频繁,就会对 CPU 不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放。
- 惰性删除:不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
内存淘汰策略
相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
-
在设置了过期时间的数据中进行淘汰:
-
volatile-lru(least recently used):从已设置过期时间的数据集(
server.db[i].expires
)中挑选最近最少使用的数据淘汰。(Redis3.0 之前,默认的内存淘汰策略) -
volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;
-
volatile-ttl:从已设置过期时间的数据集(
server.db[i].expires
)中挑选将要过期的数据淘汰。 -
volatile-random:从已设置过期时间的数据集(
server.db[i].expires
)中任意选择数据淘汰。
-
-
所有数据范围内淘汰:
-
allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
-
allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。
-
allkeys-random:从数据集(
server.db[i].dict
)中任意选择数据淘汰。
-
-
不进行数据淘汰:
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。(Redis3.0 之后,默认的内存淘汰策略)
数据结构
排序算法
快速排序
- 思想:
- 用**“轴”**将序列划分成两部分,轴左小于轴,轴右大于轴,再分别对左半部分和右半部分递归进行
- 每一轮确定一个**“轴”**的最终位置
- 实现:如果找第k大的,就count==num.length-k时,返回num[i],即第k大的
public class QuickSort {
private static int count;//
public static void main(String[] args) {
int[] num = {3,45,78,64,52,11,64,55,99,11,18};
System.out.println(arrayToString(num,"未排序"));
QuickSort(num,0,num.length-1);
System.out.println(arrayToString(num,"排序"));
System.out.println("数组个数:"+num.length);
System.out.println("循环次数:"+count);
}
//快排
private static void QuickSort(int[] num, int left, int right) {
//如果left等于right,即数组只有一个元素,直接返回
if(left>=right) {
return;
}
//设置最左边的元素为基准值
int key=num[left];
//数组中比key小的放在左边,比key大的放在右边,key值下标为i
int i=left;
int j=right;
while(i<j){
//j向左移,直到遇到比key小的值
while(num[j]>=key && i<j){
j--;
}
//i向右移,直到遇到比key大的值
while(num[i]<=key && i<j){
i++;
}
//i和j指向的元素交换
if(i<j){
int temp=num[i];
num[i]=num[j];
num[j]=temp;
}
}
num[left]=num[i];
num[i]=key;
count++;
QuickSort(num,left,i-1);
QuickSort(num,i+1,right);
}
//数组转字符串
private static String arrayToString(int[] arr,String flag) {
String str = "数组为("+flag+"):";
for(int a : arr) {
str += a + "\t";
}
return str;
}
}
计算机网络
输入url到页面展示
基础版本:
- 解析URL,生成 HTTP 请求消息
- DNS 域名解析
- 将 HTTP 的传输工作交给操作系统的协议栈
- TCP 可靠传输
- IP 远程定位
- MAC 两点传输
- 网卡,将数字信号转化为电信号
- 交换机转发包
- 路由器,转发网络包到下一个路由器或目标设备
- 服务器接收数据,响应数据包
HTTP(应用层)
基本概念
-
是什么?超文本传输协议
-
超文本
- 超越普通文本的文本,可能是文字、图片、视频等混合体
- HTML是最常见的超文本
-
传输
- 双向协议:数据在A,B之间传输,但允许中间有中转或接力(A,B可能是浏览器和百度网站)
- HTTP 是一个在计算机世界里专门用来在两点之间传输数据的约定和规范。
-
协议
所以,HTTP 是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。
-
-
状态码
- 2xx:
- 200(OK):请求被成功处理。
- 204(No Content):服务端已经成功处理了请求,但是没有返回任何内容。
- 206(Partial Content):服务器已经成功处理了部分 GET 请求。
- 3xx:
- 301(永久重定向):说明请求的资源已经不存在了,需改用新的 URL 再次访问。
- 302(临时重定向):说明请求的资源还在,但暂时需要用另一个 URL 来访问。
- 304(未修改):所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
- 305(使用代理):所请求的资源必须通过代理访问
- 307(临时重定向):与302类似。使用GET请求重定向
- 4xx:
- 400(Bad Request):客户端请求的语法错误,服务器无法理解
- 401(Unauthorized):请求要求用户的身份认证
- 403(Forbidden):服务器理解请求客户端的请求,但是拒绝执行此请求
- 404(Not Found):服务器无法根据客户端的请求找到资源(网页)
- 405(Method Not Allowed):客户端请求中的方法被禁止
- 5xx:
- 500(Internal Server Error):服务器内部错误,无法完成请求
- 501(Not Implemented):服务器不支持请求的功能,无法完成请求
- 502(Bad Gateway):作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应
- 503(Service Unavailable):由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中
- 504(Gateway Time-out):充当网关或代理的服务器,未及时从远端服务器获取请求
- 505(HTTP Version not supported):服务器不支持请求的HTTP协议的版本,无法完成处理
-
常见字段
- Host 字段:客户端发送请求时,用来指定服务器的域名
- Content-Length 字段:服务器返回的数据长度
- Connection 字段:长链接(Keep-Alive)or not
- Content-Type 字段:数据格式
- Content-Encoding 字段:服务器返回数据的压缩格式(gzip…)
GET和POST
-
区别
-
语义
-
GET :从服务器获取指定资源
-
POST :根据报文body处理指定资源
-
-
安全性(不破坏服务器上资源)
-
GET√(因为GET只读
-
POST×(因为POST会修改服务器资源
-
-
幂等(执行多次操作结果相同)
- GET√(只读,所以服务器上数据每次结果都相同
- POST×(多次提交数据就会创建多个资源
-
可被缓存?
- GET√
- POST×
-
HTTP缓存技术
HTTPS 1.0 1.1 2.0
-
短连接:浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接
-
长连接:建立SOCKET连接后不管是否使用都保持连接,直到一方关闭连接
- TCP保活机制:通过在服务器端设置一个保活定时器,当定时器开始工作后就定时的向网络通信的另一端发出保活探测的TCP报文,如果接收到了ACK报文,那么就证明对方存活,可以继续保有连接;否则就证明网络存在故障。
- TCP长连接是通过保活机制来实现的
HTTPS
HTTPS加密过程
HTTPS加密过程 - 简书 (jianshu.com)
-
客户端发起HTTPS请求
- 用户在浏览器里输入一个HTTPS网址,然后连接到服务端的443端口。
-
服务端的配置
-
采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面。证书其实就是一对公钥和私钥。
-
如果对公钥不太理解,可以想象成一把钥匙和一个锁头,只是世界上只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。
-
-
给客户端传送证书
-
这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。
-
【避免篡改】
用哈希算法对报文提取定长摘要;用私钥对摘要进行加密,作为数字签名;将数字签名附加到报文末尾发送给客户端;
-
-
客户端解析证书
-
【避免篡改】
用公钥对服务器的数字签名进行解密;用同样的算法重新计算出报文的数字签名;比较解密后的签名与自己计算的签名是否一致,如果不一致,说明数据被篡改过;如果发现异常,则会弹出一个警示框,提示证书存在的问题。 -
如果证书没有问题,那么就生成一个随机值。然后用证书(也就是【公钥】)对这个【随机值进行加密】。就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。
-
-
给服务端传送加密信息
- 传送的是用证书加密后的随机值,目的是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个【随机值来进行加密解密】了。
-
服务端解密信息
- 服务端用私钥解密后,得到了客户端传过来的随机值,然后把内容通过该随机值进行对称加密,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。
-
传输给客户端加密后的信息
- 服务端用私钥加密后的信息,可以在客户端用随机值解密还原。
-
客户端解密信息
- 客户端用之前生产的私钥解密服务端传过来的信息,于是获取了解密后的内容。整个过程第三方即使监听到了数据,也束手无策。
「安全」的HTTP协议(客户端与服务端的传输链路中进行加密)
- 认证问题:
-
保密问题:
-
HTTPS的优缺点
- 优点:相比于http,https可以提供更加优质保密的信息,保证了用户数据的安全性,此外https同时也一定程度上保护了服务端,使用恶意攻击和伪装数据的成本大大提高。
- 缺点:
- https的技术门槛较高,多数个人或者私人网站难以支撑,CA机构颁发的证书都是需要年费的,此外对接Https协议也需要额外的技术支持;
- 目前来说大多数网站并不关心数据的安全性和保密性,其https最大的优点对它来说并不适用;
- https加重了服务端的负担,相比于http其需要更多的资源来支撑,同时也降低了用户的访问速度;
- 目前来说Http网站仍然大规模使用,在浏览器侧也没有特别大的差别,很多用户不关心的话根本不感知。
TCP v.s UDP(传输层)
七点区别
-
UDP 一般用于即时通信,比如: 语音、 视频 、直播等等。【DNS】
-
TCP 用于对传输准确性要求特别高的场景,比如文件传输、发送和接收邮件、远程登录等等。【FTP文件传输、HTTP/HTTPS】
TCP为什么可靠
- 校验和:通过检验和的方式,接收端可以检测出来数据是否有差错和异常,假如有差错就会直接丢弃TCP段,重新发送。
- 序列号:每一个连接都拥有不同的序列号。序列号的作用不仅可以应答,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据。
- 确认应答:TCP 传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送 ACK 报文。这个 ACK 报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。
- 超时重传:发送方在发送完数据后等待一个时间,时间到达没有接收到 ACK 报文,那么对刚才发送的数据进行重新发送。
- 拥塞控制:当网络拥塞时,减少数据的发送。(慢启动机制:在启动初期以指数增长方式增长;设置一个慢启动的阈值,当以指数增长达到阈值时就停止指数增长,按照线性增长方式增加至拥塞窗口;线性增长达到网络拥塞时立即把拥塞窗口置回1,进行新一轮的“慢启动”,同时新一轮的阈值变为原来的一半。)
- 滑动窗口控制
三次握手(建立TCP连接)
- 三次握手干了啥?
- 在最开始的时候,客户端和服务端都处于 CLOSE 状态
- 一次握手:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 SYN_SENT 状态,等待服务器的确认;
- 二次握手:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 SYN_RECV 状态
- 三次握手:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务器端都进入ESTABLISHED 状态,完成TCP三次握手。
-
Linux怎么查看TCP状态?
netstat -napt
命令
-
三报文而不是两报文?四报文
-
阻止重复历史连接的初始化:为了防止已失效的连接请求报文段(历史连接)突然又传送到了TCP服务器,因而导致错误
-
不使用两次握手的原因:在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费
-
-
同步双方初始序列号
- 序列号的作用:
- 接收方可以去除重复的数据;
- 接收方可以根据数据包的序列号按序接收;
- 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);
- SYN+ACK一来一回,才能保证双方的初始序列号能被可靠的同步
- 四次握手:SYN(客)->ACK(服)->SYN(服)->ACK(客),二三步可以合并为一步,同时发送ACK和SYN,所以三次握手就够了
- 序列号的作用:
-
避免资源浪费:【不使用四次握手的原因】
-
-
为什么序列号是随机的?
- 一方面为了安全性(随机ISN能避免非同一网络的攻击),另一方面可以让通信双方能够根据序号将「不属于」本连接的报文段丢弃
四次挥手(断开TCP连接)
- 四次挥手干了啥?
- 在建立完连接之后,客户端和服务端双方都处于 ESTABLISHED 状态
- 第一次挥手 :客户端发送一个 FIN(SEQ=X) 标志的数据包->服务端,用来关闭客户端到服务器的数据传送。然后,客户端进入 FIN-WAIT-1 状态。【我没啥要说的了】
- 第二次挥手 :服务器收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (SEQ=X+1)标志的数据包->客户端 。然后,此时服务端进入CLOSE-WAIT状态,客户端进入FIN-WAIT-2状态。【我知道了,可能还会有要说的话】
- 第三次挥手 :服务端关闭与客户端的连接并发送一个 FIN (SEQ=y)标志的数据包->客户端请求关闭连接,然后,服务端进入LAST-ACK状态。【B 可能又balabala,最后说完了】
- 第四次挥手 :客户端发送 ACK (SEQ=y+1)标志的数据包->服务端并且进入TIME-WAIT状态,服务端在收到 ACK (SEQ=y+1)标志的数据包后进入 CLOSE 状态。此时,如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后,客户端也可以关闭连接了。【我知道了】
-
为啥需要四次?
- 关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。 - 服务端收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。 - 服务端通常需要等待完成数据的发送和处理,所以服务端的
ACK
和FIN
一般都会分开发送,因此是需要四次挥手。
- 关闭连接时,客户端向服务端发送
-
第一次挥手丢失会发生什么?
-
如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由
tcp_orphan_retries
参数控制。 -
当客户端重传 FIN 报文的次数超过
tcp_orphan_retries
后,就不再发送 FIN 报文,则会在等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第二次挥手,那么直接进入到close
状态。
-
-
第二次挥手丢失会发生什么?
- ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。达到最大重传次数后的处理和“第一次挥手丢失”相同。
-
第三次挥手丢失会发生什么?
-
服务端收到第一次挥手后,内核会自动回复ACK,同时处于
CLOSE_WAIT
状态,即等待应用进程调用close函数关闭连接 -
服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会第三次挥手,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。
-
这时第三次挥手丢了!迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由
tcp_orphan_retries
参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。
-
-
第四次挥手丢失会发生什么?
-
如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的
tcp_orphan_retries
参数控制。 -
举个例子,假设 tcp_orphan_retries 为 2,当第四次挥手一直丢失时,发生的过程如下:
- 客户端在收到第三次挥手后,就会进入 TIME_WAIT 状态,开启时长为 2MSL 的定时器,如果途中再次收到第三次挥手(FIN 报文)后,就会重置定时器,当等待 2MSL 时长后,客户端就会断开连接。
-
-
为什么 TIME_WAIT 等待的时间是 2MSL?
MSL
是 Maximum Segment Lifetime,报文最大生存时间,如果超过这个时间,这个TCP报文就会被丢弃。- TIME_WAIT状态为什么是2MSL的时长?因为客户端不知道服务端是否能收到ACK应答数据包,服务端如果没有收到ACK,会进行重传FIN
- 考虑最坏的一种情况:第四次挥手的ACK包的最大生存时长(MSL)+服务端重传的FIN包的最大生存时长(MSL)=2MSL
- 在 Linux 系统里
2MSL
默认是60
秒,那么一个MSL
也就是30
秒。
-
为什么需要 TIME_WAIT 状态?
-
谁有这个状态?主动发起关闭连接的一方,所以客户端和服务端都有可能!
-
为什么需要?(两点原因)
-
保证让迟来的TCP报文有足够的时间被识别并丢弃
- 如下图,
SEQ = 301
报文,被网络延迟了,接着,服务端以相同的四元组重新打开了新连接,前面被延迟的SEQ = 301
这时抵达了客户端,会发生数据错乱等问题
- 如下图,
-
保证「被动关闭连接」的一方,能被正确的关闭
-
如下图,四次挥手的最后⼀个 ACK 报文如果在网络中被丢失了,此时如果客户端 TIME-WAIT 过短或没有,则就直接进入了 CLOSED 状态了
-
按照 TCP 可靠性原则,服务端会重发 FIN 报文,发现对方关了,客户端收到FIN返回RST报文,解释为一个错误。
-
-
-
-
TIME_WAIT过多有什么危害?
- 两种危害:
- 占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;
- 占用端口资源,端口资源也是有限的,一般可以开启的端口为
32768~61000
,也可以通过net.ipv4.ip_local_port_range
参数指定范围。
- 客户端和服务端 TIME_WAIT 过多,造成的影响不同:
- 客户端TW过多:占用端口资源,占满所有端口资源,就无法对【目的IP+目的端口】都一样的服务端发起连接了
- 服务端TW过多:占用系统资源,不会导致端口资源受限,因为服务端只监听一个端口
- 两种危害:
-
服务器出现大量TIME_WAIT状态的原因?
- 为什么会出现大量TW:TIME_WAIT 状态是主动关闭连接方才会出现的状态,所以如果服务器出现大量的 TIME_WAIT 状态的 TCP 连接,就是说明服务器主动断开了很多 TCP 连接。
- 什么场景下服务端会主动断开连接:
- HTTP 没有使用长连接
- HTTP 长连接超时
- HTTP 长连接的请求数量达到上限
TCP Keepalive 和 HTTP Keep-Alive
- HTTP 的 Keep-Alive,是由应用层(用户态) 实现的,称为 HTTP 长连接;
- TCP 的 Keepalive,是由 TCP 层(内核态) 实现的,称为 TCP 保活机制;
DNS(域名-IP映射)
应用层协议,基于UDP协议之上
-
两种查询解析模式
-
迭代
主机->本地DNS->根服务器->TLD DNS->权威DNS->本地DNS->主机
-
递归
-
-
DNS在进行区域传输的时候使用TCP协议,其它时候则使用UDP协议
- 一些DNS事务,比如区域传输或其他附加查询,可能会产生大于512字节的数据包,因此使用TCP更加可靠,使用TCP会减少丢包和重新发包的情况,因此更加可靠与高效。
- DNS的规范规定了2种类型的DNS服务器,一个叫主DNS服务器,一个叫辅助DNS服务器。在一个区中主DNS服务器从自己本机的数据文件中读取该区的DNS数据信息,而辅助DNS服务器则从区的主DNS服务器中读取该区的DNS数据信息。当一个辅助DNS服务器启动时,它需要与主DNS服务器通信,并加载数据信息,这就叫做区传送(zone transfer)。
操作系统
内核态和用户态
-
什么是内核态和用户态?
-
用户态(User Mode) :
- 用户态运行的进程可以直接读取用户程序的数据,拥有较低的权限。
- 啥时候进入:当应用程序需要执行某些需要特殊权限的操作,例如读写磁盘、网络通信等,就需要向操作系统发起系统调用请求,进入用户态。
-
内核态(Kernel Mode):
- 内核态运行的进程几乎可以访问计算机的任何资源包括系统的内存空间、设备、驱动程序等,不受限制,拥有非常高的权限。
- 啥时候进入:当操作系统接收到进程的系统调用请求时,就会从用户态切换到内核态,执行相应的系统调用,并将结果返回给进程,最后再从内核态切换回用户态。
-
-
**为什么要设置这两个核态?**只有内核态不行吗?
- 在 CPU 的所有指令中,有一些指令是比较危险的比如内存分配、设置时钟、IO 处理等,如果所有的程序都能使用这些指令的话,会对系统的正常运行造成灾难性地影响。因此,我们需要限制这些危险指令只能内核态运行。这些只能由操作系统内核态执行的指令也被叫做 特权指令 。
- 如果计算机系统中只有一个内核态,那么所有程序或进程都必须共享系统资源,例如内存、CPU、硬盘等,这将导致系统资源的竞争和冲突,从而影响系统性能和效率。并且,这样也会让系统的安全性降低,毕竟所有程序或进程都具有相同的特权级别和访问权限。
- 同时具有用户态和内核态主要是为了保证计算机系统的安全性、稳定性和性能。
-
内核态和用户态如何切换?
- 系统调用:用户态主动切换到内核态的方式,比如读取磁盘资源时。
- 中断:外围设备完成用户请求–(向CPU发出中断信号)–>中断处理程序–>(假如之前是用户态程序,则)用户态转为内核态 (比如硬盘读写操作完成
- 异常:用户态程序–(发生不可预知的异常,比如缺页异常)–>内核态
-
如何降低两个核态切换开销?
-
为什么切换开销大?用户态转化为内核态时,需要执行系统调用,保存现场,也就是保存用户态的寄存器等, 然后去系统调用,并在内核态执行,最后恢复现场。并且由于内核对于用户的不信任, 因此内核需要对用户进行一些额外的检查,这就需要耗费更多的工作了。(保存现场+安全检查开销大)
-
降低进程通信的开销,可以用共享内存方式进行进程通信
- mmap函数:使用
mmap
对文件进行读写操作时可以减少内存拷贝的次数,并且可以减少系统调用的次数,从而提高对读写文件操作的效率。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
- mmap函数:使用
-
减少系统调用(System Calls): 系统调用是用户态和内核态之间的切换触发点。减少系统调用的次数可以减少内核态和用户态之间的切换开销。可以通过使用更高效的系统调用、批量操作和缓存数据等方式来减少系统调用次数。
使用轻量级的同步机制: 同步机制,如锁、信号量等,会导致内核态和用户态之间的切换。使用轻量级的同步机制可以减少内核态和用户态之间的切换次数。例如,使用非阻塞的自旋锁或无锁数据结构,避免频繁的阻塞和唤醒操作。
使用事件驱动模型: 事件驱动模型可以避免线程的阻塞和切换,从而减少内核态和用户态之间的切换开销。通过使用异步IO、回调函数或事件循环机制,可以使程序在一个线程内处理多个事件,减少线程的创建和销毁,从而降低切换开销。
使用线程池和协程: 使用线程池和协程可以减少线程的创建和销毁,从而减少内核态和用户态之间的切换次数。线程池可以复用线程,减少线程创建的开销,而协程则可以在一个线程内实现多个协程的切换,避免线程的切换开销。
-
进程和线程
进程通信7种方式
常用的ipc划横线了
-
管道/匿名管道(Pipes):用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
-
有名管道(Named Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循 先进先出(First In First Out) 。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
-
信号(Signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
-
消息队列(Message Queuing):消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。
-
信号量(Semaphores):信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
-
共享内存(Shared memory):使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。【开销最小】
-
套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
线程同步的4种方式
线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。
- 互斥锁(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的
synchronized
关键词和各种Lock
都是这种机制。 - 读写锁(reader-writer lock):允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
- 信号量(semaphore):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
- 屏障(Barrier):屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的
CyclicBarrier
是这种机制。 - 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
内存管理
虚拟内存管理方式
页式
段式
段页式
死锁
-
死锁:多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。
-
产生死锁的四个必要条件:
- 互斥:资源处于非共享模式
- 请求并保持:请求别的资源得不到,被阻塞,自己获得的资源也不放手
- 非抢占:资源不能被抢占。我自己的资源只能用完我自己释放
- 循环等待
-
解决死锁的方法:
- 预防:破坏四个必要条件之一(通常是请求并保持or循环等待)
- 破坏请求并保持:预先静态分配,一次申请完它需要的全部资源
- 破坏循环等待:顺序资源分配法,规定每个进程必须按编号递增的顺序请求资源
- 避免:银行家算法(试探性分配资源,安全性算法判断系统是否处于安全状态,不安全则作废,安全就分配)
- 检测:用资源分配图(有环路不一定死锁)
- 解除
- 结束所有进程,重启OS
- 撤销 all 死锁进程,解除后继续运行
- 逐个撤销死锁进程,回收资源直到解除
- 抢占资源
- 预防:破坏四个必要条件之一(通常是请求并保持or循环等待)
Spring
概述
- Spring4.x主要模块:
- 最重要:Spring-Core(主要提供 IoC 依赖注入功能的支持) 模块
- Data Access/Integration中spring-orm : 提供对 Hibernate、JPA 、iBatis 等 ORM 框架的支持。
-
Spring,Spring MVC,Spring Boot 之间什么关系?
-
Spring MVC是Spring中很重要的模块,核心思想是将业务逻辑、数据、显示分离来组织代码
- M:Model模型;V:View视图;C:Controller控制器
-
Spring Boot 简化了Spring配置,如果需要构建 MVC 架构的 Web 程序,还是需要使用 Spring MVC 作为 MVC 框架,只是说 Spring Boot 帮你简化了 Spring MVC 的很多配置,真正做到开箱即用!
-
IoC(控制反转)
IoC(Inversion of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。
为什么叫控制反转?
-
控制 :指的是对象创建(实例化、管理)的权力
-
反转 :控制权交给外部环境(Spring 框架、IoC 容器)
-
SpringIOC有两个核心思想就是IOC控制反转和DI依赖注入
-
IOC 控制反转的基本思想是,将原来的对象控制从使用者,有了spring之后可以把整个对象交给spring来帮我们进行管理。
-
DI 依赖注入,就是把对应的属性的值注入到具体的对象中。spring提供标签和@Autowired和@Resource注解等方式注入,注入方式本质上是AbstractAutowireCapableBeanFactory的populateBean() 方法先从beanDefinition 中取得设置的property值,*例如autowireByName方法会根据bean的名字注入;autowireByType方法根据bean的类型注入,完成属性值的注入(涉及bean初始化过程)。*对象会存储在map结构中,在spring使用Map结构的singletonObjects存放完整的bean对象(涉及三级缓存和循环依赖)。整个bean的生命周期,从创建到使用到销毁的过程全部都是由容器来管理(涉及bean的生命周期)。
IOC 容器底层就是对象工厂(BeanFactory 接口)。IOC的原理是基于xml解析、工厂设计模式、反射实现的。使用IOC可以降低代码的耦合度。
@Component 和 @Bean 的区别是什么?
@Component
注解作用于类,而@Bean
注解作用于方法。@Component
通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用@ComponentScan
注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean
注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean
告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。@Bean
注解比@Component
注解的自定义性更强,而且很多地方我们只能通过@Bean
注解来注册 bean。比如当我们引用第三方库中的类需要装配到Spring
容器时,则只能通过@Bean
来实现。
@Autowired 和 @Resource 的区别是什么?
@Autowired
是 Spring 提供的注解,@Resource
是 JDK 提供的注解。Autowired
默认的注入方式为byType
(根据类型进行匹配),@Resource
默认注入方式为byName
(根据名称进行匹配)。- 当一个接口存在多个实现类的情况下,
@Autowired
和@Resource
都需要通过名称才能正确匹配到对应的 Bean。Autowired
可以通过@Qualifier
注解来显式指定名称,@Resource
可以通过name
属性来显式指定名称。
Bean的生命周期
- Bean 容器找到配置文件中 Spring Bean 的定义。
- Bean 容器利用 Java Reflection API 创建一个 Bean 的实例。
- 如果涉及到一些属性值 利用
set()
方法设置一些属性值。 - 如果 Bean 实现了
BeanNameAware
接口,调用setBeanName()
方法,传入 Bean 的名字。 - 如果 Bean 实现了
BeanClassLoaderAware
接口,调用setBeanClassLoader()
方法,传入ClassLoader
对象的实例。 - 如果 Bean 实现了
BeanFactoryAware
接口,调用setBeanFactory()
方法,传入BeanFactory
对象的实例。 - 与上面的类似,如果实现了其他
*.Aware
接口,就调用相应的方法。 - 如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor
对象,执行postProcessBeforeInitialization()
方法 - 如果 Bean 实现了
InitializingBean
接口,执行afterPropertiesSet()
方法。 - 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
- 如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor
对象,执行postProcessAfterInitialization()
方法 - 当要销毁 Bean 的时候,如果 Bean 实现了
DisposableBean
接口,执行destroy()
方法。 - 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。
AoP(面向切面编程)
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
通俗描述:不通过修改源代码方式,在主干功能里面添加新功能
- 项目中怎么使用AOP?
- 【Spring5框架学习】AOP:权限判断模块
- AOP的三种实现方式
-
通过Spring API实现AOP
-
通过编写增强类来继承Spring API提供的接口
-
通过自定义类来实现(推荐)
-
使用注解实现
-
自动装配原理
自动装配可以简单理解为:通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能。
1、main方法中SpringApplication.run(HelloBoot.class,args)的执行流程中有refreshContext(context)。
2、而这个refreshContext(context)内部会解析,配置类上自动装配功能的注解@EnableAutoConfiguration中的,@EnableAutoConfiguration中的,引入类AutoConfigurationImportSelector。
3、AutoConfigurationImportSelector这个类中的方法SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()会读取jar包中的/项目中的META-INF/spring.factories文件。
4、spring.factories配置了自动装配的类,最后根据配置类的条件,自动装配Bean。
Sping中的设计模式
- Spring使用工厂模式可以通过BeanFactory或ApplicationContext创建bean对象。
- 单例模式:Spring中bean的默认作用域就是singleton。
- 代理设计模式:Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用JDK Proxy去进行代理了,这时候Spring AOP会使用Cglib,这时候Spring AOP会使用Cglib生成一个被代理对象的子类来作为代理。
- 模板方法设计模式:Spring中
jdbcTemplate
、hibernateTemplate
等以Template结尾的对数据库操作的类,它们就使用到模板模式。 - 适配器模式 : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配
Controller
。
注解
常用注解
Spring Boot 常用注解汇总 - 云天 - 博客园 (cnblogs.com)
事务 @Transactional
1. Spring事务介绍
我们系统的每个业务方法可能包括了多个原子性的数据库操作,比如下面的 savePerson()
方法中就有两个原子性的数据库操作。这些原子性的数据库操作是有依赖的,它们要么都执行,要不就都不执行。
public void savePerson() {
personDao.save(person);
personDetailDao.save(personDetail);
}
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键操作就是:
- 将小明的余额减少 1000 元。
- 将小红的余额增加 1000 元。
万一在这两个操作之间突然出现错误比如银行系统崩溃或者网络故障,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
2. spring事务管理方式
-
编程式事务管理(少用)
- 通过
TransactionTemplate
或者TransactionManager
手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。
- 通过
-
声明式事务管理(推荐)
-
推荐使用(代码侵入性最小),实际是通过 AOP 实现(基于
@Transactional
的全注解方式使用最多)。 -
使用
@Transactional
注解进行事务管理的示例代码如下:@Transactional(propagation = Propagation.REQUIRED) public void aMethod { //do something B b = new B(); C c = new C(); b.bMethod(); c.cMethod(); }
-
3. Spring事务管理接口
-
Spring 框架中,事务管理相关最重要的 3 个接口如下:
-
PlatformTransactionManager
:(平台)事务管理器,Spring 事务策略的核心。事务上层的管理者 -
TransactionDefinition
:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。 -
TransactionStatus
:事务运行状态。
-
-
PlatformTransactionManager:事务管理接口
-
Spring 并不直接管理事务,而是提供了多种事务管理器。
-
接口定义的三个方法:
package org.springframework.transaction; import org.springframework.lang.Nullable; public interface PlatformTransactionManager { //获得事务 TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException; //提交事务 void commit(TransactionStatus var1) throws TransactionException; //回滚事务 void rollback(TransactionStatus var1) throws TransactionException; }
-
-
TransactionDefinition:事务属性
-
事务管理器接口
PlatformTransactionManager
通过getTransaction(TransactionDefinition definition)
方法来得到一个事务,这个方法里面的参数是TransactionDefinition
类 ,这个类就定义了一些基本的事务属性。 -
事务属性:事务的一些基本配置,描述了事务策略如何应用到方法上。
-
事务属性包含5个方面:
- 隔离级别
- 传播行为
- 回滚规则
- 是否只读
- 事务超时
-
在
TransactionDefinition
接口中的实现:package org.springframework.transaction; import org.springframework.lang.Nullable; public interface TransactionDefinition { int PROPAGATION_REQUIRED = 0; int PROPAGATION_SUPPORTS = 1; int PROPAGATION_MANDATORY = 2; int PROPAGATION_REQUIRES_NEW = 3; int PROPAGATION_NOT_SUPPORTED = 4; int PROPAGATION_NEVER = 5; int PROPAGATION_NESTED = 6; int ISOLATION_DEFAULT = -1; int ISOLATION_READ_UNCOMMITTED = 1; int ISOLATION_READ_COMMITTED = 2; int ISOLATION_REPEATABLE_READ = 4; int ISOLATION_SERIALIZABLE = 8; int TIMEOUT_DEFAULT = -1; // 返回事务的传播行为,默认值为 REQUIRED。 int getPropagationBehavior(); //返回事务的隔离级别,默认值是 DEFAULT int getIsolationLevel(); // 返回事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。 int getTimeout(); // 返回是否为只读事务,默认值为 false boolean isReadOnly(); @Nullable String getName(); }
-
-
TransactionStatus:事务状态
-
TransactionStatus
接口用来记录事务的状态 该接口定义了一组方法,用来获取或判断事务的相应状态信息。 -
PlatformTransactionManager.getTransaction(…)
方法返回一个TransactionStatus
对象。public interface TransactionStatus{ boolean isNewTransaction(); // 是否是新的事务 boolean hasSavepoint(); // 是否有恢复点 void setRollbackOnly(); // 设置为只回滚 boolean isRollbackOnly(); // 是否为只回滚 boolean isCompleted; // 是否已完成 }
-
4. 事务属性
-
传播行为
-
事务传播行为是为了解决业务层方法之间互相调用的事务问题。
-
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
-
事务传播行为种类:
-
PROPAGATION_REQUIRED
:默认的,使用最多。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
举个例子:如果我们上面的
aMethod()
和bMethod()
使用的都是PROPAGATION_REQUIRED
传播行为的话,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。@Service Class A { @Autowired B b; @Transactional(propagation = Propagation.REQUIRED) public void aMethod { //do something b.bMethod(); } } @Service Class B { @Transactional(propagation = Propagation.REQUIRED) public void bMethod { //do something } }
-
PROPAGATION_REQUIRES_NEW
:创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW
修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。举个例子:如果我们上面的
bMethod()
使用PROPAGATION_REQUIRES_NEW
事务传播行为修饰,aMethod
还是用PROPAGATION_REQUIRED
修饰的话。如果aMethod()
发生异常回滚,bMethod()
不会跟着回滚,因为bMethod()
开启了独立的事务。但是,如果bMethod()
抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,aMethod()
同样也会回滚,因为这个异常被aMethod()
的事务管理机制检测到了。@Service Class A { @Autowired B b; @Transactional(propagation = Propagation.REQUIRED) public void aMethod { //do something b.bMethod(); } } @Service Class B { @Transactional(propagation = Propagation.REQUIRES_NEW) public void bMethod { //do something } }
-
PROPAGATION_NESTED
:如果当前存在事务,就在嵌套事务内执行;如果当前没有事务,就执行与TransactionDefinition.PROPAGATION_REQUIRED
类似的操作。简单举个例子:如果
bMethod()
回滚的话,aMethod()
不会回滚。如果aMethod()
回滚的话,bMethod()
会回滚。
@Service Class A { @Autowired B b; @Transactional(propagation = Propagation.REQUIRED) public void aMethod { //do something b.bMethod(); } } @Service Class B { @Transactional(propagation = Propagation.NESTED) public void bMethod { //do something } }
-
.PROPAGATION_MANDATORY
:很少用。如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
其他三种传播行为很少用。
-
-
项目中怎么用事务传播的?
- 注册方法中有添加积分的方法,添加积分执行失败回滚->不能使注册方法也回滚,注册方法回滚->添加积分方法也要回滚
- 注册方法:
@Transactional
;添加积分:@Transactional(propagation = Propagation.NESTED)
- 太难了~面试官让我结合案例讲讲自己对Spring事务传播行为的理解。 (qq.com)
-
-
隔离级别
同mysql
-
超时属性
一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。
-
只读属性
只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。
-
回滚规则
这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常(
RuntimeException
的子类)时才会回滚,Error
也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚。@Transactional(rollbackFor= MyException.class)
5. @Transactional注解使用
-
作用范围
-
方法:推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
-
类:如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
-
接口:不推荐在接口上使用。
-
-
常用配置参数
-
原理
面试中在问 AOP 的时候可能会被问到的一个问题。
@Transactional
的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。- 如果一个类或者一个类中的 public 方法上被标注
@Transactional
注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional
注解的 public 方法的时候,实际调用的是,TransactionInterceptor
类中的invoke()
方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
-
@Transactional
什么时候会失效? -
非public方法:注解标注方法修饰符为非public时,@Transactional注解将会不起作用
-
在类内部调用调用类内部@Transactional标注的方法:若同一类中的其他没有
@Transactional
注解的方法内部调用有@Transactional
注解的方法,有@Transactional
注解的方法的事务会失效。【AOP自调用问题】这是由于
Spring AOP
代理的原因造成的,因为只有当@Transactional
注解的方法在类以外被调用的时候,Spring 事务管理才生效。//MyService类中的method1()调用method2()就会导致method2()的事务失效。 @Service public class MyService { private void method1() { method2(); //...... } @Transactional public void method2() { //...... } }
3. **异常处理不当**:事务方法内部捕捉了异常,没有抛出新的异常,导致事务操作不会进行回滚。 4. **数据库不支持**:如 MySql 的 MyISAM 引擎
分布式
RPC
RPC(Remote Procedure Call) 即远程过程调用,用于解决分布式系统中服务之间的调用问题。
为什么要 RPC ? 因为,两个不同的服务器上的服务提供的方法不在一个内存空间,所以,需要通过网络编程才能传递方法调用所需要的参数。并且,方法调用的结果也需要通过网络编程来接收。
一言蔽之:RPC 的出现就是为了让你调用远程方法像调用本地方法一样简单。
RPC 框架包含三个最重要的组件,分别是客户端、服务端和注册中心。在一次 RPC 调用流程中,这三个组件是这样交互的:
-
服务端在启动后,会将它提供的服务列表发布到注册中心,客户端向注册中心订阅服务地址;
-
客户端会通过本地代理模块 Proxy 调用服务端,Proxy 模块收到负责将方法、参数等数据转化成网络字节流;
-
客户端从服务列表中选取其中一个的服务地址,并将数据通过网络发送给服务端;
-
服务端接收到数据后进行解码,得到请求信息;
-
服务端根据解码后的请求信息调用对应的服务,然后将调用结果返回给客户端。
-
注册中心 目前成熟的注册中心有Zookeeper,Nacos,Consul,Eureka,这里使用ZK作为注册中心,没有提供切换以及用户自定义注册中心的功能。
-
Zookeeper(分布式协调服务):
- ZooKeeper 可以被用作注册中心、分布式锁;
- ZooKeeper 是 Hadoop 生态系统的一员;
- 构建 ZooKeeper 集群的时候,使用的服务器最好是奇数台。
-
ZooKeeper 特点
- 顺序一致性: 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。
- 原子性: 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。
- 单一系统映像: 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。
- 可靠性: 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。
常见RPC框架
Dubbo
Apache Dubbo 是一款微服务框架,为大规模微服务实践提供高性能 RPC 通信、流量治理、可观测性等解决方案, 涵盖 Java、Golang 等多种语言 SDK 实现。
分布式锁
-
分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁 就诞生了。
-
一个最基本的分布式锁需要满足:
- 互斥:任意一个时刻,锁只能被一个线程持有;
- 高可用:锁服务是高可用的。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。
- 可重入:一个节点获取了锁之后,还可以再次获取锁。
基于Redis实现分布式锁
不论是本地锁还是分布式锁,核心都在于“互斥”。
在 Redis 中, SETNX
命令是可以帮助我们实现互斥。SETNX
即 SET if Not eXists (对应 Java 中的 setIfAbsent
方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX
啥也不做。
SETNX lockKey uniqueValue
(integer) 1
SETNX lockKey uniqueValue
(integer) 0
释放锁的话,直接通过 DEL
命令删除对应的 key 即可。
DEL lockKey
(integer) 1
为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。
选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。
// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。
基于ZooKeeper实现分布式锁
Redis 实现分布式锁性能较高,ZooKeeper 实现分布式锁可靠性更高。
ZooKeeper 分布式锁是基于 临时顺序节点 和 Watcher(事件监听器) 实现的。
Linux
常用命令
ls 显示文件或目录
-l 列出文件详细信息l(list)
-a 列出当前目录下所有文件及目录,包括隐藏的a(all)
mkdir 创建目录
-p 创建目录,若无父目录,则创建p(parent)
cd 切换目录
touch 创建空文件
echo 创建带有内容的文件。
cat 查看文件内容
cp 拷贝
mv 移动或重命名
rm 删除文件
-r 递归删除,可删除子目录及文件
-f 强制删除
find 在文件系统中搜索某文件
wc 统计文本中行数、字数、字符数
grep 在文本文件中查找某个字符串
rmdir 删除空目录
tree 树形结构显示目录,需要安装tree包
pwd 显示当前目录
ln 创建链接文件
more、less 分页显示文本文件内容
head、tail 显示文件头、尾内容
ctrl+alt+F1 命令行全屏模式
系统管理命令
stat 显示指定文件的详细信息,比ls更详细
who 显示在线登陆用户
whoami 显示当前操作用户
hostname 显示主机名
uname 显示系统信息
top 动态显示当前耗费资源最多进程信息
ps 显示瞬间进程状态 ps -aux
du 查看目录大小 du -h /home带有单位显示目录信息
df 查看磁盘大小 df -h 带有单位显示磁盘信息
ifconfig 查看网络情况
ping 测试网络连通
netstat 显示网络状态信息
man 命令不会用了,找男人 如:man ls
clear 清屏
alias 对命令重命名 如:alias showmeit="ps -aux" ,另外解除使用unaliax showmeit
kill 杀死进程,可以先用ps 或 top命令查看进程的id,然后再用kill命令杀死进程。
gzip:
bzip2:
tar: 打包压缩
-c 归档文件
-x 压缩文件
-z gzip压缩文件
-j bzip2压缩文件
-v 显示压缩或解压缩过程 v(view)
-f 使用档名
例:
tar -cvf /home/abc.tar /home/abc 只打包,不压缩
tar -zcvf /home/abc.tar.gz /home/abc 打包,并用gzip压缩
tar -jcvf /home/abc.tar.bz2 /home/abc 打包,并用bzip2压缩
当然,如果想解压缩,就直接替换上面的命令 tar -cvf / tar -zcvf / tar -jcvf 中的“c” 换成“x” 就可以了。
查询进程占用内存
ps 和 top 命令常用来查看Linux系统进程相关信息。
-
**ps命令:**可以查看进程的瞬间信息。
-
ps命令用于报告当前系统的进程状态。可以搭配kill指令随时中断、删除不必要的程序。ps命令是最基本同时也是非常强大的进程查看命令,使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵死、哪些进程占用了过多的资源等等,总之大部分信息都是可以通过执行该命令得到的。
-
使用场景:
ps -ef #显示所有当前进程 ps aux #显示所有当前进程 ps -ax #显示所有当前进程 ps -u pungki #根据用户过滤进程 ps -aux --sort -pcpu | less #根据 CPU 使用来升序排序 ps -aux --sort -pmem | less #根据用户过滤进程 ps -aux --sort -pcpu,+pmem | head -n 10 #查询全10个使用cpu和内存最高的应用 ps -C getty #通过进程名和PID过滤 ps -f -C getty #带格式显示的,通过进程名和PID过滤 ps -L 1213 #根据线程来过滤进程 ps -axjf(或pstree) #树形显示进程 ps -eo pid,user,args # 显示安全信息 ps -U root -u root u #格式化输出 root 用户(真实的或有效的UID)创建的进程
-
-
**top命令:**可以持续的监视进程的信息。
-
top命令用来显示执行中的程序进程,使用权限是所有用户。
-
-
第一行表示的项目依次为当前时间、系统启动时间、当前系统登录用户数目、平均负载。
-
第二行显示的是所有启动的进程、目前运行的、挂起(Sleeping)的和无用(Zombie)的进程。
-
第三行显示的是目前CPU的使用情况,包括系统占用的比例、用户使用比例、闲置(Idle)比例。
-
第四行显示物理内存的使用情况,包括总的可以使用的内存、已用内存、空闲内存、缓冲区占用的内存。
-
第五行显示交换分区使用情况,包括总的交换分区、使用的、空闲的和用于高速缓存的大小。
-
第六行显示的项目最多,下面列出了详细解释
PID 进程id USER 进程所有者 PR 进程优先级 NI nice值。负值表示高优先级,正值表示低优先级 VIRT 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES RES 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA SHR 共享内存大小,单位kb S 进程状态。D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程,N表示该进程优先值是负数。 %CPU 上次更新到现在的CPU时间占用百分比 %MEM 进程使用的物理内存百分比 TIME+ 进程使用的CPU时间总计,单位1/100秒 COMMAND 进程名称(命令名/命令行)
-
-
银行
-
为什么选择来银行?
银行是一个有稳定发展前景的行业,而且在当今的经济环境下,银行的角色非常重要。选择银行是因为希望能够在一个稳定的行业中工作,同时为社会做出一些贡献。银行是一个注重团队合作和个人成长的行业。我希望能够在一个团结、相互协作的环境中工作,学习和发展自己的能力,不断提高自己的技能和经验,为银行和自己的未来发展做出贡献。
我对银行的核心业务和金融知识有浓厚的兴趣。希望通过在银行的工作中,深入了解银行的业务和金融知识,并将其应用于日常工作中,以提高自己的专业技能和知识水平。
call(“get”,KEYS[1]) == ARGV[1] then
return redis.call(“del”,KEYS[1])
else
return 0
end
<img src="https://img-blog.csdnimg.cn/020d53f90ce74a76b0e1638cfdc1b49d.png" alt="在这里插入图片描述" style="zoom:80%;" />
这是一种**最简易的 Redis 分布式锁实现**,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。
### 基于ZooKeeper实现分布式锁
Redis 实现分布式锁性能较高,ZooKeeper 实现分布式锁可靠性更高。
ZooKeeper 分布式锁是基于 **临时顺序节点** 和 **Watcher(事件监听器)** 实现的。
# Linux
## 常用命令
```shell
ls 显示文件或目录
-l 列出文件详细信息l(list)
-a 列出当前目录下所有文件及目录,包括隐藏的a(all)
mkdir 创建目录
-p 创建目录,若无父目录,则创建p(parent)
cd 切换目录
touch 创建空文件
echo 创建带有内容的文件。
cat 查看文件内容
cp 拷贝
mv 移动或重命名
rm 删除文件
-r 递归删除,可删除子目录及文件
-f 强制删除
find 在文件系统中搜索某文件
wc 统计文本中行数、字数、字符数
grep 在文本文件中查找某个字符串
rmdir 删除空目录
tree 树形结构显示目录,需要安装tree包
pwd 显示当前目录
ln 创建链接文件
more、less 分页显示文本文件内容
head、tail 显示文件头、尾内容
ctrl+alt+F1 命令行全屏模式
系统管理命令
stat 显示指定文件的详细信息,比ls更详细
who 显示在线登陆用户
whoami 显示当前操作用户
hostname 显示主机名
uname 显示系统信息
top 动态显示当前耗费资源最多进程信息
ps 显示瞬间进程状态 ps -aux
du 查看目录大小 du -h /home带有单位显示目录信息
df 查看磁盘大小 df -h 带有单位显示磁盘信息
ifconfig 查看网络情况
ping 测试网络连通
netstat 显示网络状态信息
man 命令不会用了,找男人 如:man ls
clear 清屏
alias 对命令重命名 如:alias showmeit="ps -aux" ,另外解除使用unaliax showmeit
kill 杀死进程,可以先用ps 或 top命令查看进程的id,然后再用kill命令杀死进程。
gzip:
bzip2:
tar: 打包压缩
-c 归档文件
-x 压缩文件
-z gzip压缩文件
-j bzip2压缩文件
-v 显示压缩或解压缩过程 v(view)
-f 使用档名
例:
tar -cvf /home/abc.tar /home/abc 只打包,不压缩
tar -zcvf /home/abc.tar.gz /home/abc 打包,并用gzip压缩
tar -jcvf /home/abc.tar.bz2 /home/abc 打包,并用bzip2压缩
当然,如果想解压缩,就直接替换上面的命令 tar -cvf / tar -zcvf / tar -jcvf 中的“c” 换成“x” 就可以了。
查询进程占用内存
ps 和 top 命令常用来查看Linux系统进程相关信息。
-
**ps命令:**可以查看进程的瞬间信息。
-
ps命令用于报告当前系统的进程状态。可以搭配kill指令随时中断、删除不必要的程序。ps命令是最基本同时也是非常强大的进程查看命令,使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵死、哪些进程占用了过多的资源等等,总之大部分信息都是可以通过执行该命令得到的。
-
使用场景:
ps -ef #显示所有当前进程 ps aux #显示所有当前进程 ps -ax #显示所有当前进程 ps -u pungki #根据用户过滤进程 ps -aux --sort -pcpu | less #根据 CPU 使用来升序排序 ps -aux --sort -pmem | less #根据用户过滤进程 ps -aux --sort -pcpu,+pmem | head -n 10 #查询全10个使用cpu和内存最高的应用 ps -C getty #通过进程名和PID过滤 ps -f -C getty #带格式显示的,通过进程名和PID过滤 ps -L 1213 #根据线程来过滤进程 ps -axjf(或pstree) #树形显示进程 ps -eo pid,user,args # 显示安全信息 ps -U root -u root u #格式化输出 root 用户(真实的或有效的UID)创建的进程
-
-
**top命令:**可以持续的监视进程的信息。
-
top命令用来显示执行中的程序进程,使用权限是所有用户。
-
-
第一行表示的项目依次为当前时间、系统启动时间、当前系统登录用户数目、平均负载。
-
第二行显示的是所有启动的进程、目前运行的、挂起(Sleeping)的和无用(Zombie)的进程。
-
第三行显示的是目前CPU的使用情况,包括系统占用的比例、用户使用比例、闲置(Idle)比例。
-
第四行显示物理内存的使用情况,包括总的可以使用的内存、已用内存、空闲内存、缓冲区占用的内存。
-
第五行显示交换分区使用情况,包括总的交换分区、使用的、空闲的和用于高速缓存的大小。
-
第六行显示的项目最多,下面列出了详细解释
PID 进程id USER 进程所有者 PR 进程优先级 NI nice值。负值表示高优先级,正值表示低优先级 VIRT 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES RES 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA SHR 共享内存大小,单位kb S 进程状态。D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程,N表示该进程优先值是负数。 %CPU 上次更新到现在的CPU时间占用百分比 %MEM 进程使用的物理内存百分比 TIME+ 进程使用的CPU时间总计,单位1/100秒 COMMAND 进程名称(命令名/命令行)
-
-
银行
-
为什么选择来银行?
银行是一个有稳定发展前景的行业,而且在当今的经济环境下,银行的角色非常重要。选择银行是因为希望能够在一个稳定的行业中工作,同时为社会做出一些贡献。银行是一个注重团队合作和个人成长的行业。我希望能够在一个团结、相互协作的环境中工作,学习和发展自己的能力,不断提高自己的技能和经验,为银行和自己的未来发展做出贡献。
我对银行的核心业务和金融知识有浓厚的兴趣。希望通过在银行的工作中,深入了解银行的业务和金融知识,并将其应用于日常工作中,以提高自己的专业技能和知识水平。