导图社区 高薪java后端开发面试经验
6年一线互联网面试经验,汇集java/mysql/中间件(rockmq/kafka)/操作系统/算法等相关知识点、亦可以作为学习的指导方向,绝对物超所值。
编辑于2021-07-21 18:49:19面试
框架
持久框架
mybatis
hibernate
spring
事务

声明式事物
利用aop在方法的前后进行拦截,根据方法的执行结果进行提交或者回滚操作
bean和代理
每个bean一个代理
所有bean共享一个代理
使用拦截器
tx标签配置拦截器
注解配置
编程式事物
组成
DataSource
TransactionManager
代理机制
核心
ioc
aop
工作流程
面试题
谈谈 ioc
         
原始
车->车身->底盘->轮胎
如果给轮胎改变大小,构造方法传入参数,那么会影响上层的所有类
->依赖的意思
ioc
车<-车身<-底盘<-轮胎
如果给轮胎改变大小,只需要修改车类,即可,因为关系都是在高层管理的
好处
ioc方式,需要改轮胎的话,只需要在车的类里面修改即可,不影响其他类,而原始方法是从下往上都要影响
不需要关系创建对象的细节,生成对象时,ioc会先寻找对象间的关系,之后从下往上替你创建对象。
beanFactory和factoryBean
beanFactory
定义实现ioc的规范
职责
实例化对象
管理对象间的依赖关系
所有对象都由beanfactory产生
只是一个接口
XmlBeanFactory是实现中的一个
以xml方式描述对象间的关系
原始的factory,不包含aop功能和web功能等
applicationContext拓展了beanfactory
可以实现国际或
资源访问,如url和文件
factoryBean
以Bean结尾,表示它是一个Bean,不同于普通Bean的是:它是实现了FactoryBean<T>接口的Bean,根据该Bean的ID从BeanFactory中获取的实际上是FactoryBean的getObject()返回的对象,而不是FactoryBean本身,如果要获取FactoryBean对象,请在id前面加一个&符号来获取。
例如自己实现一个FactoryBean,功能:用来代理一个对象,对该对象的所有方法做一个拦截,在调用前后都输出一行LOG,模仿ProxyFactoryBean的功能。
循环依赖
初始化不完全的对象到第三级缓存中
springmvc
核心
DispatcherServlet
负责分发请求到不同组件
流程
1)客户端发送http请求,web应用服务器接收到这个请求,如果匹配DispatcherServlet的映射路径(在web.xml中配置),web容器将请求转交给DispatcherServlet处理;
2)DispatcherServlet将请求发送给Spring MVC控制器。DispatcherServlet查询处理器映射(Handler Mapping),处理器映射会根据请求的URL信息来决定由哪个控制器处理请求。
3)Controller进行业务逻辑处理后,返回一个ModelAndView给DispatcherServlet;
4)DispatcherServlet借由ViewResolver完成ModelAndView中逻辑视图名到真实视图对象View的解析工作;
5)DispatcherServlet根据ModelAndView中的数据模型对View对象进行视图渲染,最终客户端得到的响应消息可能是一个普通的html页面,也可能是一个xml或json串,甚至是一张图片或一个PDF文档等不同的媒体形式。
mybatis
一级缓存
一级缓存: 基于PerpetualCache 的 HashMap本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 Cache 就将清空。
二级缓存
1. 映射语句文件中的所有select语句将会被缓存。 2. 映射语句文件中的所有insert,update和delete语句会刷新缓存。 3. 缓存会使用Least Recently Used(LRU,最近最少使用的)算法来收回。 4. 缓存会根据指定的时间间隔来刷新。 5. 缓存会存储1024个对象 eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。
缓存更新机制
对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被clear。
shiro
http://blog.csdn.net/u013087513/article/details/75051134
功能
1.验证用户 2.对用户执行访问控制,如:判断用户是否具有角色admin,判断用户是否拥有访问的资源权限。 3.在任何环境下使用SessionAPI。例如C/S程序 4.可以使用多个用户数据源。例如一个是Oracle数据库,另外一个是MySQL数据库。 5.单点登录(SSO)功能 6.”Remember Me”服务,类似于购物车的功能,shiro官方建议开启。
4大组成部分
身份认证
Authentication
身份验证(身份认证),简称”登录”
授权
Authorization
给用户给用户分配角色或者权限资源
会话管理
Session Manager
用户Session管理器,可以让C/S程序也使用Session来控制权限
加密
Cryptography
将JDK中复杂的密码加密方式进行封装。 除了以上功能,shiro还提供很多扩展功能
拓展功能
Web Support
主要针对web应用提供一些常用功能
Caching
缓存可以使程序运行更有效率
Concurrency
多线程相关功能
Testing
帮助我们进行测试相关的功能
Run As
一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本时很有用
Remember Me
记住用户身份,提供类似购物车的功能
运行流程

Subject
主体,是与程序进行交互的对象,可以是人也可以是服务或其他程序,通常理解为用户。所有的Subject实例都必须绑定到一个
SecurityManager
我们与Subject交互,运行时shiro会自动转化为与SecurityManager交互的特定的subject的交互。
SecurityManager是shiro的核心,初始化时协调各个模块运行。然而,一旦SecurityManager协调完毕,SecurityManager会被单独留下,且我们只需要去操作Subject即可,无需操作SecurityManager。但是需要了解的是当我们与一个Subject进行交互时,实质上是SecurityManager在处理Subject的安全操作。
Realms
Realms在shiro中作为程序和安全数据之间的"桥梁"或"连接器"。它用于获取安全数据来判断subject是否能够登录,subject拥有什么权限。有点类似于DAO。在配置realms时,需要至少一个realm。而且shiro提供了一些常用的Realms来连接数据源,如LDAP数据源的JndiLdapRealm,JDBC数据源的JdbcRealm,ini文件数据源的iniRealm,Properties文件数据源的PropertiesRealm,等等,我们也可以插入自己的Realm实现来代表自定义的数据源。像其他组件一样,Realms也是由SecurityManager控制。
认证流程图

架构

Subject
(org.apache.shiro.subject.Subject) 即主体,简称用户,主体既可以是用户也可以是程序,主体访问系统,系统需要对主体进行认证、授权。外部应用与subject进行交互,Subject记录了当前操作用户 ,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。Subject在shiro中是一个接口,接口中定义了很多认证授权相关的方法,外部程序通过subject进行认证授权,而Subject是通过SecurityManager安全管理器进行认证授权。
SecurityManager
(org.apache.shiro.mgt.SecurityManager)如上所述,SecurityManager是shiro的核心,协调shiro的各个组件。SecurityManager就是安全管理器,负责对全部的subject进行安全管理。通过SecurityManager可以完成Subject的认证、授权等,实质上SecurityManager是通过Authenticator对主体进行认证,通过Authorizer对主体进行授权,通过SessionManager进行会话管理等等。SecurityManager是一个接口,继承了Authenticator,Authorizer,SessionManager这三个接口。
Authenticator
(org.apache.shiro.authc.Authenticator) 即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
Authorizer
(org.apache.shiro.authz.Authorizer)即授权器,用户在通过认证器认证通过后,在访问时需要通过授权器判断用户是否有此功能的操作权限。最终是通过认证器对主体进行授权的。
Realm
(org.apache.shiro.realm.Realm)Realm即领域,相当于DataSource数据源,通过Realm存取认证、授权相关数据。SecurityManager通过认证器对主体进行安全认证需要通过Realm获取用户身份数据,比如:如果用户身份数据在数据库,那么Realm就需要从数据库获取用户的身份信息。授权也是如此,也需要通过Realm取出授权相关信息。注意:不要将Realm理解成只是从数据源获取数据,在Realm中还有认证授权校验的相关代码
SessionManager
(org.apache.shiro.session.SessionManager)会话管理。web应用中一般是web容器对Session进行管理,shiro框架定义了一套会话管理,它不依赖于web容器的Session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点进行管理,此特性可使它实现单点登录。
SessionDAO:SessionDAO
SessionDAO即会话dao,是对Session会话操作的一套接口,比如要将Session存储到数据库,可以通过JDBC将会话存储到数据库。针对个性化的Session数据存储(存到数据库)需要使用SessionDAO。
CacheManager
(org.apahce.shiro.cache.CacheManager)缓存管理器,主要对Session和授权数据进行缓存,比如将授权数据通过cachemanager进行缓存管理,和ehcache整合对缓存数据进行管理,可以减少不必要的后台访问,提高应用效率,增加用户体验。
Cryptography
(org.apache.shiro.crypto.*)密码管理,提供了一套加密/解密组件,对JDK中的加密解密算法进行了封装,方便开发。比如提供常用的散列、加/解密等功能,比如MD5散列算法。
分布式框架
分布式事务
TCC
              
seata
tcc
使用场景
推荐不支持acid(事务)的数据库推荐使用
全局事务
使用场景
支持事务的数据库
springcloud
hytrix
信号量和线程池
https://www.cnblogs.com/ming-blogs/p/14596721.html
应用场景
当大多数人在使用Tomcat时,多个HTTP服务会共享一个线程池,假设其中一个HTTP服务访问的数据库响应非常慢,这将造成服务响应时间延迟增加,大多数线程阻塞等待数据响应返回,导致整个Tomcat线程池都被该服务占用,甚至拖垮整个Tomcat。因此,如果我们能把不同HTTP服务隔离到不同的线程池,则某个HTTP服务的线程池满了也不会对其他服务造成灾难性故障。这就需要线程隔离或者信号量隔离来实现了。
使用线程隔离或信号隔离的目的是为不同的服务分配一定的资源,当自己的资源用完,直接返回失败而不是占用别人的资源。
feign
优化
配置连接池
默认使用的URLConnection去发送请求,是没有连接池的
支持使用Apache的HTTPClient以及OKHTTP去发送请求,而Apache的HTTPClient以及OKHTTP都是支持连接池的
web项目
调优
前端
压缩源码和图片
JavaScript文件源代码可以采用混淆压缩的方式,CSS文件源代码进行普通压缩,JPG图片可以根据具体质量来压缩为50%到70%,PNG可以使用一些开源压缩软件来压缩,比如24色变成8色、去掉一些PNG格式信息等。
合并静态资源
包括CSS、JavaScript和小图片,减少HTTP请求。有很大一部分用户访问会因为这一条而取得最大受益
开启服务器端的Gzip压缩
这对文本资源非常有效,对图片资源则没那么大的压缩比率。
使用CDN
或者一些公开库使用第三方提供的静态资源地址(比如jQuery、normalize.css)。一方面增加并发下载量,另一方面能够和其他网站共享缓存。
延长静态资源缓存时间
把CSS放在页面头部,把JavaScript放在页面底部
后端
缓存
nginx静态缓存
redis数据缓存
缓存穿透
缓存血崩
避免数据库频繁链接
优化sql
负载均衡
分区,分表
图片服务器分离
cdn
读写分离
数据库乐观锁
消息队列
服务拆分
涉密设施
使用分布式
解决tomcat死的问题
session和cookies
作者:知乎用户 链接:https://www.zhihu.com/question/19786827/answer/28752144 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 1. 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session。2. 思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。3. Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办?这个信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。所以,总结一下:Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。
cookies
存在客户端
cookies中带有sessionid,如果禁用,那么在url中带上sessionId
可以被禁用
session
存在服务器端
session共享
存在cookies中
禁用cookies之后不能正常使用
数据库
影响数据库性能
memcache,redis
常用
大数据项目
日志分析
    
问题整理
dubbo
dubbo服务模式
长连接小数据量
大数据量短链接
分布式系统
rest和rpc的区别
链路追踪
优点
分布式系统如何进行测试
设计
如何设计一个分布式框架和系统
如何设计一个高并发网站
如何设置一个发短信,延迟在1m内
redis和数据库在同一个方法里更新,如何保证数据的一致性
redis分布式锁
setnx+守护线程延长锁时间+队列解决不断循环问题
权限设计
rbac
缺少的知识点
treeMap
红黑树特性和原理
面试目录
基础
基础数据结构的实现:linkedList arrayList hashmap treemap
hashmap 实现? 红黑树特性和原理? 复杂度?
HashMap在JDK8中如何解决死循环问题?
HashMap和HashSet关系
concurrentHashMap实现
常见的并发模块和类? -
volatile的作用和原理
ThreadPoolExecutor用法与关键参数关系?核心参数理解
进程间如何通讯,线程间如何通讯? 线程通讯:信号量、信号、锁
Threadlocal是怎样实现的? 注意些什么?
sleep()和wait()的区别,
进程间通信方式有哪些?各有什么优缺点,挑一两种说下使用场景
synchronize和重入锁的区别:
aqs 逻辑, 加锁和释放锁
重入锁下有几条队列?
cas理解、产生的问题、怎么解决?哪些类通过版本?
jvm内存结构?
栈溢出情况、新生代内存结构及其比例划分&为什么这么设计?
8*1+2*1=10 因为经过统计,每次gc会有90%的对象被回收,所以要预留空间去保存剩下的10%
IBM论文里说据他们统计95%的对象朝生夕死一样存活时间极短,为了保险默认实际使用了90%:
垃圾回收器类型和算法?G1 CMS使用场景?-
jvm常见参数? 常见分析指令? jps jmap jstack zprofile等?- 没怎么做个调优 Integer
类加载机制 双亲委派 为什么这么设计? 如何打破? Tomcat osgi jdbc如何做的? 其他语言 怎么做的额? node里的npm? -
自定义类加载器如何实现?注解的原理?如何实现自定义注解?
maven jar包 冲突解决方案? spring cloud 怎么做的? 版本代号-
如何排查类冲突?如何隔离冲突?如何自定义类加载器
spring aop的理解?用了哪些设计模型?动态代理理解? 有哪些实现方式?jdk cglib asm ?选型对比?
BeanFactory和ApplicationContext的差异?
spring 容器启动顺序和流程,扩展点BeanFactoryPostProcessor的加载流程?
springcontext 管理bean 加载插件 , 没看过
spring事务传播机制 ?异常处理?
Spring IOC的初始化和生命周期?
spring的ioc启动流程简述:refresh方法、解析xml文件或注释、BeanDefinition、初始化上下文ApplicationContext
相互依赖 死循环问题 - 三级缓存 提前曝光 还没初始化先放到缓存里
springboot 和spring 的区别?何自定义stater、spring.factories实现自动配置,加载所有需要加载的bean?
bean factory和factory bean的区别 - ok
BIO/NIO/AIO各自特点及对应场景
NIO的在netty中的封装的模型及底层操作系统epoll的实现; select实现对比?
粘包 拆包
DDD的理解? 解决什么问题? 遇到什么问题? 如何建模? 做个 用例分析没? 怎么确定领域边界?问题域的划分?
单点登录,cookie,session理解
seata实现原理
业务模型
https://zhuanlan.zhihu.com/p/136612886
项目管理
需求评审
如何判断需求是否合理
需求是否符合部门发展方向
代码评审
原则
在项目早期就能够发现代码中的BUG 帮助初级开发人员学习高级开发人员的经验,达到知识共享 避免开发人员犯一些很常见,很普通的错误 保证项目组人员的良好沟通 项目或产品的代码更容易维护 2 Code Review的前提 知道了Code Review的目的,我们就可以看看如何做Code Review了,但在做Code Review前我们还有事要做,所谓预则立,不预则废,就是说如果在进入Code Review之前我们不做些准备工作,Code Review很容易就变得没有意义或是流于形式,这在我们周围是有很多例子的啊。进入Code Review需要检查的条件如下: Code Review人员是否理解了Code Review的概念和Code Review将做什么 如果做Code Review的人员不能理解Code Review对项目成败和代码质量的重要程度,他们的做法可能就会是应付了事。 代码是否已经正确的build,build的目的使得代码已经不存在基本语法错误 我们总不希望高级开发人员或是主管将时间浪费在检查连编译都通不过的代码上吧。 代码执行时功能是否正确 Code Review人员也不负责检查代码的功能是否正确,也就是说,需要复查的代码必须由开发人员或质量人员负责该代码的功能的正确性。 Review人员是否理解了代码 做复查的人员需要对该代码有一个基本的了解,其功能是什么,是拿一方面的代码,涉及到数据库或是通讯,这样才能采取针对性的检查 开发人员是否对代码做了单元测试 这一点也是为了保证Code Review前一些语法和功能问题已经得到解决,Code Review人员可以将精力集中在代码的质量上。 3 Code Review需要做什么 好了,进入条件准备好了,有人在这些条件中看到Code Review这也不负责,那也不检查,不禁会问,Code Review到底做什么? 其实Code Review主要检查代码中是否存在以下方面问题:代码的一致性、编码风格、代码的安全问题、代码冗余、是否正确设计以满足需求(性能、功能)等等 下边我们一一道来。以下内容参考了《Software Quality Assurance: Documentation and Reviews》一文中的代码检查部分。 3.1 完整性检查(Completeness) 代码是否完全实现了设计文档中提出的功能需求 代码是否已按照设计文档进行了集成和Debug 代码是否已创建了需要的数据库,包括正确的初始化数据 代码中是否存在任何没有定义或没有引用到的变量、常数或数据类型 3.2 一致性检查(Consistency) 代码的逻辑是否符合设计文档 代码中使用的格式、符号、结构等风格是否保持一致 3.3 正确性检查(Correctness) 代码是否符合制定的标准 所有的变量都被正确定义和使用 所有的注释都是准确的 所有的程序调用都使用了正确的参数个数 3.4 可修改性检查(Modifiability) 代码涉及到的常量是否易于修改(如使用配置、定义为类常量、使用专门的常量类等) 代码中是否包含了交叉说明或数据字典,以描述程序是如何对变量和常量进行访问的 代码是否只有一个出口和一个入口(严重的异常处理除外) 3.5 可预测性检查(Predictability) 代码所用的开发语言是否具有定义良好的语法和语义 是否代码避免了依赖于开发语言缺省提供的功能 代码是否无意中陷入了死循环 代码是否是否避免了无穷递归 3.6 健壮性检查(Robustness) 代码是否采取措施避免运行时错误(如数组边界溢出、被零除、值越界、堆栈溢出等) 3.7 结构性检查(Structuredness) 程序的每个功能是否都作为一个可辩识的代码块存在 循环是否只有一个入口 3.8 可追溯性检查(Traceability) 代码是否对每个程序进行了唯一标识 是否有一个交叉引用的框架可以用来在代码和开发文档之间相互对应 代码是否包括一个修订历史记录,记录中对代码的修改和原因都有记录 是否所有的安全功能都有标识 3.9 可理解性检查(Understandability) 注释是否足够清晰的描述每个子程序 是否使用到不明确或不必要的复杂代码,它们是否被清楚的注释 使用一些统一的格式化技巧(如缩进、空白等)用来增强代码的清晰度 是否在定义命名规则时采用了便于记忆,反映类型等方法 每个变量都定义了合法的取值范围 代码中的算法是否符合开发文档中描述的数学模型 3.10 可验证性检查(Verifiability) 代码中的实现技术是否便于测试 二、Code Review经验检查项 以下是在实践中建立的检查列表(checklist),通过分类和有针对性的检查项,保证了Code Review可以有的放矢。 1 JAVA编码规范方面检查项 检查项参照JAVA编码规范执行,见《Java语言编码规范(Java Code Conventions)》 2 面向对象设计方面检查项 这几点的范围都很大,不可能在本文展开讨论,有专门的书籍介绍这方面问题,当然在Code Review中主要靠经验来判断。 类设计和抽象是否合适 是否符合面向接口编程的思想 是否采用合适的设计范式 3 性能方面检查项性能检查 在大多数代码中都是需要严重关注的方面,也是最容易出现问题的方面,常常有程序员写出了功能和语法没有丝毫问题的代码后,正式运行时却在性能上表现不佳,从而不得不做大量的返工,甚至是推倒重来。 在海量数据出现时,队列、表、文件,在传输、upload等方面是否会出现问题,有无控制,如分配的内存块大小、队列长度等控制参数 对hashtable,vector等集合类数据结构的选择和设置是否合适,如正确设置capacity、load factor等参数,数据结构的是否是同步的 有无滥用String对象的现象 是否采用通用的线程池、对象池模块等cache技术以提高性能 类的接口是否定义良好,如参数类型等、避免内部转换 是否采用内存或硬盘缓冲机制以提高效率 并发访问时的应对策略 I/O方面是否使用了合适的类或采用良好的方法以提高性能(如减少序列化、使用buffer类封装流等) 同步方法的使用是否得当,是否过度使用 递归方法中的叠代次数是否合适,应该保证在合理的栈空间范围内 如果调用了阻塞方法,是否考虑了保证性能的措施 避免过度优化,对性能要求高的代码是否使用profile工具,如Jprobe等 4 资源泄漏处理方面检查项 对于JAVA来说由于存在垃圾收集机制,所以内存泄漏不是太明显,但使用不当,仍然存在内存泄漏的问题。而对于其它的语言,如C++等在这方面就要严重关注了。当然数据库连接资源不释放的问题也是广大程序员最常见的,相信有很多的PM被这个问题折磨的死去活来。 分配的内存是否释放,尤其在错误处理路径上(对非JAVA类) 错误发生时是否所有的对象被释放,如数据库连接、Socket、文件等 是否同一个对象被释放多次(对非JAVA类) 代码是否保存准确的对象reference计数(对非JAVA类) 5 线程安全方面检查项 线程安全问题实际涉及两个方面,一个是性能,另一个是资源的一致性,我们需要在这两方面做个权衡,现在就是到了权衡利弊的时候了。 代码中所有的全局变量是否是线程安全的 需要被多个线程访问的对象是否线程安全,检查有无通过同步方法保护 ?同步对象上的锁是否按相同的顺序获得和释放以避免死锁,注意错误处理代码 ?是否存在可能的死锁或是竞争,当用到多个锁时,避免出现类似情况:线程A获得锁1、然后锁2、线程B获得锁2、然后锁1 在保证线程安全的同时,要注意避免过度使用同步,导致性能降低 6 程序流程方面检查项 循环结束条件是否准确 是否避免了死循环的产生 对循环的处理是否合适,如循环变量、局部对象、循环次数等能够考虑到性能方面的影响 7 数据库处理方面 很多Code Review人员在面对代码中涉及到的数据库可移植性和提高数据库性能方面的冲突时表现的无所适从,凡事很难两全其美的啊。 数据库设计或SQL语句是否便于移植(注意和性能方面会存在冲突) 数据库资源是否正常关闭和释放 数据库访问模块是否正确封装,便于管理和提高性能 是否采用合适的事务隔离级别 是否采用存储过程以提高性能 是否采用PreparedStatement以提高性能 8 通讯方面检查项 socket通讯是否存在长期阻塞问题 发送接收的数据流是否采用缓冲机制 socket超时处理,异常处理 数据传输的流量控制问题 9 JAVA对象处理方面检查项 这个检查项的基础是对JAVA对象有较深的理解,但现实是很多看过《Thinking in Java》的程序员,仍然在程序中无法区分传值和传引用,以及对象和reference的区别。这或许就是理论和实践难以结合的问题啊。正所谓知而不行,非真知也。 对象生命周期的处理,是否对象的reference已经失效,能够设置为null,并被回收 在对象的传值和传参方面有无问题,对象的clone方法使用是否过度 是否大量经常的创建临时对象 是否尽量使用局部对象(堆栈对象) 在只需要对象reference的地方是否创建了新的对象实例 10 异常处理方面检查项 JAVA中提供了方便的异常处理机制,但普遍存在的是异常被捕获,但并没有得到处理。我们可以打开一段代码,最常见的现象是进入某个方法后,一个大的try/catch将所有代码行括住,然后在catch中将异常打印到控制台,而且该异常是Exception对象。 每次当方法返回时是否正确处理了异常,如最简单的处理,记录日志到日志文件中 是否对数据的值和范围是否合法进行校验,包括采用断言(assertion) 在出错路径上是否所有的资源和内存都已经释放 所有抛出的异常都得到正确的处理,特别是对子方法抛出的异常,在整个调用栈中必须能够被捕捉并处理 当调用导致错误发生时,方法的调用者应该得到一个通知 不要忘了对错误处理部分的代码进行测试,很多代码在正常情况下执行良好,而一旦出错,整个系统就崩溃了 11 方法(函数)方面检查项 方法的参数是否都做了校验 数组类结构是否做了边界校验 变量在使用前是否做了初始化 返回堆对象的reference,不要返回栈对象的reference 方法API是否被良好定义,即是否尽量面向接口编程、便于维护和重构 12 安全方面检查项 对命令行执行的代码,需要详细检查命令行参数 web类程序检查是否对访问参数进行合法性验证 重要信息的保存是否选用合适的加密算法 通讯时考虑是否选用安全的通讯方式 13 其他 日志是否正常输出和控制 配置信息如何获得,是否有硬编码 三、总结 通过在项目中实施Code Review将为我们带来多方面的好处,表现在提高代码质量,保证项目或产品的稳定性,开发经验的积累等,具体的实施当然也要看项目的实际情况,因为 Code Review也是需要成本的,这方面属于Code Review过程的问题,将在其他文章中进行探讨。?
严谨性
拓展性
迪米特法则
  
类(组件)和类之间不要跨级调用
依赖倒转原则
依赖于抽象而不是实现
单一职责原则
里氏替换原则
最少知道原则
接口隔离原则
依赖置换原则
开闭原则
可读性
规范性
性能
http://www.bjdcfy.com/html/04/7a/20763.html
是否采用通用的线程池、对象池模块等cache技术以提高性能
是否采用内存或硬盘缓冲机制以提高效率
并发访问时的应对策略
递归方法中的叠代次数是否合适,应该保证在合理的栈空间范围内
如果调用了阻塞方法,是否考虑了保证性能的措施
方法实现时,时间复杂度
for循环 o(n)
批量接口 o(1)
参数校验放到访问外部数据接口之前
线程安全方面检查项
代码中所有的全局变量是否是线程安全的
需要被多个线程访问的对象是否线程安全,检查有无通过同步方法保护
同步对象上的锁是否按相同的顺序获得和释放以避免死锁,注意错误处理代码
是否存在可能的死锁或是竞争,当用到多个锁时,避免出现类似情况:线程A获得锁1、然后锁2、线程B获得锁2、然后锁1
数据库处理方面
数据库设计或SQL语句是否便于移植(注意和性能方面会存在冲突)
数据库资源是否正常关闭和释放
是否采用合适的事务隔离级别
数据库设计是否符合范式
方法(函数)方面检查项
方法的参数是否都做了校验
数组类结构是否做了边界校验
变量在使用前是否做了初始化
方法注释是否齐全
其他
日志是否正常输出和控制
配置信息如何获得,是否有硬编码
架构设计
1000qps如何设计架构
普通写业务代码就行
技术达不到,需要调整业务需求
子主题
子主题
面试流程
自我介绍
你好,我叫丁旭,来自黑龙江哈尔滨,从事java行业6年多,有三年微服务开发经验,2年多团队管理经验,最近做的是大搜车平台下的易置换平台业务,主要做基于微服务的竞拍相关服务和车辆相关服务的开发。高峰期服务qps大约在300左右,主要在于竞拍服务的出价相关业务上,主要用到的微服务相关技术是,springcloud全家桶,包括eureka,feign,ribbon,zuul,appolo,自研的链路追踪系统,以及rocketmq,mysql等技术。 基本信息 工作单位 项目名称 业务使用技术 业务量
基础信息
工作经验
所处公司
近期项目
技术栈
精通项目
jdk
mysql
jvm相关
工作愿景
对工作和生活都有帮助的行业和方向
介绍项目
各个服务
车辆
订单
车源上拍服务
竞拍
搜索服务
客户服务
项目亮点
月初批量上拍
5000+场次同时拍卖
每场3qps,峰值15000qps
子主题
架构
微服务拆分原则
业务模型拆分
压力模型拆分
请求功能水平拆分
领域模型拆分
用户群体拆分
前台后台拆
为什么要微服务拆分
服务异构性
业务过于复杂
迭代/开发速度过慢
保持服务的单一性
用户
spark
Spark中的宽窄依赖
其实区分宽窄依赖主要就是看父RDD的一个Partition的流向,要是流向一个的话就是窄依赖,流向多个的话就是宽依赖。看图理解: 
窄依赖
窄依赖是子RDD的各个分片(partition)不依赖于其他分片,能够独立计算得到结果
宽依赖
宽依赖指子 RDD 的各个分片会依赖于父RDD 的多个分片,所以会造成父 RDD 的各个分片在集群中重新分片
Stage的划分
在DAG(add依赖图)中,在最后的action算子向前推移,到遇到的第一个shuffle算子,是一个stage,继续向前的另一个shuffle算子,是另一个stage
shuffle
洗牌
不同分区的数据进行交互,进行计算,这个过程就是shuffle
job
调用RDD的一个action,如count,即触发一个Job
DAG(有向无环图)
描述rdd之间的依赖关系
运行流程
Driver向Master申请资源;
Master让Worker给程序分配具体的Executor。
驱动driver端作用
创建SparkContext,读取配置文件
创建 RDD
以及进行 RDD 的转化操作和行动操作代码的执行
分析transform和action语句,生成DAG,在生成stage和task,分发到worker端的excutor执行,Executor进程启动后,会向Driver进程注册自己。因此,Driver进程就可以跟踪应用中所有的Executor节点的运行信息。
负责向集群管理器申请资源
向master注册信息
负责作业的解析
安排工作计划的
Driver内部的调度流程
经过dagscheduler和taskscheduler生成task
在Driver中,RDD首先交给DAGSchedule进行Stage的划分。
然后底层的调度器TaskScheduler就与Executor进行交互。
根据算子逻辑的依赖关系,DAGScheduler来划分成不同的Stage
cluster模式下
driver会在工作节点(worker)运行
client模式下
driver会运行在client端,提交的那个机器上(比如你自己的笔记本上)
将用户程序转为任务
执行器节点(Executor)
它们负责运行组成 Spark 应用的任务,并将结果返回给驱动器(driver)进程
它们通过自身的块管理器(Block Manager)为用户程序中要求缓存的 RDD 提供内存式存储。RDD 是直接缓存在Executor进程内的,因此任务可以在运行时充分利用缓存数据加速运算
master
算法
常见的排序方法
冒泡排序
/** * 冒泡法排序 * 比较相邻的元素。如果第一个比第二个大,就交换他们两个。 * 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。 * 针对所有的元素重复以上的步骤,除了最后一个。 * 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。 * * @param numbers * 需要排序的整型数组 */ public static void bubbleSort(int[] numbers) { int temp; // 记录临时中间值 int size = numbers.length; // 数组大小 for (int i = 0; i for (int j = i + 1; j if (numbers[i] temp = numbers[i]; numbers[i] = numbers[j]; numbers[j] = temp; } } } }
快速排序
           /** * 快速排序 * @author IT_ZJYANG */ public class QuickSort { /** * 将数组的某一段元素进行划分,小的在左边,大的在右边 * @param a * @param start * @param end * @return */ public static int divide(int[] a, int start, int end){ //每次都以最右边的元素作为基准值 int base = a[end]; //start一旦等于end,就说明左右两个指针合并到了同一位置,可以结束此轮循环。 while(start while(start //从左边开始遍历,如果比基准值小,就继续向右走 start++; //上面的while循环结束时,就说明当前的a的[start]值比基准值大,应与基准值进行交换 if(start //交换 int temp = a[start]; a[start] = a[end]; a[end] = temp; //交换后,此时的那个被调换的值也同时调到了正确的位置(基准值右边),因此右边也要同时向前移动一位 end--; } while(start = base) //从右边开始遍历,如果比基准值大,就继续向左走 end--; //上面的while循环结束时,就说明当前的a[end]的值比基准值小,应与基准值进行交换 if(start //交换 int temp = a[start]; a[start] = a[end]; a[end] = temp; //交换后,此时的那个被调换的值也同时调到了正确的位置(基准值左边),因此左边也要同时向后移动一位 start++; } } //这里返回start或者end皆可,此时的start和end都为基准值所在的位置 return end; } /** * 排序 * @param a * @param start * @param end */ public static void sort(int[] a, int start, int end){ if(start > end){ //如果只有一个元素,就不用再排下去了 return; } else{ //如果不止一个元素,继续划分两边递归排序下去 int partition = divide(a, start, end); sort(a, start, partition-1); sort(a, partition+1, end); } } } 测试 public static void main(String[] args) { int[] a = new int[]{2,7,4,5,10,1,9,3,8,6}; int[] b = new int[]{1,2,3,4,5,6,7,8,9,10}; int[] c = new int[]{10,9,8,7,6,5,4,3,2,1}; int[] d = new int[]{1,10,2,9,3,2,4,7,5,6}; sort(a, 0, a.length-1); System.out.println("排序后的结果:"); for(int x : a){ System.out.print(x+" "); } } 
选择排序
/** * 选择排序 * 在未排序序列中找到最小元素,存放到排序序列的起始位置 * 再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。 * 以此类推,直到所有元素均排序完毕。 * * @param numbers */ public static void selectSort(int[] numbers) { int size = numbers.length, temp; for (int i = 0; i int k = i; for (int j = size - 1; j >i; j--) { if (numbers[j] } temp = numbers[i]; numbers[i] = numbers[k]; numbers[k] = temp; } }
插入排序
/** * 插入排序 * * 从第一个元素开始,该元素可以认为已经被排序 * 取出下一个元素,在已经排序的元素序列中从后向前扫描 * 如果该元素(已排序)大于新元素,将该元素移到下一位置 * 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置 * 将新元素插入到该位置中 * 重复步骤2 * * * @param numbers */ public static void insertSort(int[] numbers) { int size = numbers.length, temp, j; for(int i=1; i temp = numbers[i]; for(j = i; j > 0 && temp numbers[j] = numbers[j-1]; numbers[j] = temp; } }
希尔排序
基数排序
归并排序
/** * 归并排序 * * 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列 * 设定两个指针,最初位置分别为两个已经排序序列的起始位置 * 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置 * 重复步骤3直到某一指针达到序列尾 * 将另一序列剩下的所有元素直接复制到合并序列尾 * * * @param numbers */ public static void mergeSort(int[] numbers, int left, int right) { int t = 1;// 每组元素个数 int size = right - left + 1; while (t int s = t;// 本次循环每组元素个数 t = 2 * s; int i = left; while (i + (t - 1) merge(numbers, i, i + (s - 1), i + (t - 1)); i += t; } if (i + (s - 1) merge(numbers, i, i + (s - 1), right); } } /** * 归并算法实现 * * @param data * @param p * @param q * @param r */ private static void merge(int[] data, int p, int q, int r) { int[] B = new int[data.length]; int s = p; int t = q + 1; int k = p; while (s if (data[s] B[k] = data[s]; s++; } else { B[k] = data[t]; t++; } k++; } if (s == q + 1) B[k++] = data[t++]; else B[k++] = data[s++]; for (int i = p; i data[i] = B[i]; }
数组反转
3.但是是效率可不可以更高一些呢,还有在同一个数组上如何进行翻转 例如数组:a,b,c,d,e,f,g 返回结果:g,f,e,d,c,b,a 分析:数组长度为 7 对应数组标记,数组从0开始,也就是0,1,2,3,4,5,6 当长度为单数时,中间位不需要互换,也就是d(4)不需要互换,也就是7/2取整结果3,3+1不需要互换,也就是length/2 +1不需要互换 当长度为双数时,不需要考虑,全部进行互换  总结:需要互换的数据为0~(length/2 -1) 与 length/2 ~ (length -1) 设置循环系统i=0,阈值为lenth/2 - 1,同时倒叙获取后面的参数进行互换。 public static void main(String[] args) { String[] num = {"1", "2", "3", "4", "5", "6"}; for (int i = 0; i String temp1 = num[i]; String temp2 = num[num.length - i - 1]; num[i] = temp2; num[num.length - i - 1] = temp1; } System.out.println(Arrays.asList(num).toString()); } 暂时只想到只想到这些,如果大家有什么更好的方式欢迎交流。 同时隐身知识点,java中的值传递与引用传递的区别。。。。 --------------------- 作者:debugmoney 来源:CSDN 原文:https://blog.csdn.net/kingo0/article/details/56488810/ 版权声明:本文为博主原创文章,转载请附上博文链接!
单向链表
      public class Node { protected Node next; //指针域 public int data;//数据域 public Node( int data) { this. data = data; } //显示此节点 public void display() { System. out.print( data + " "); } } public class LinkList { public Node first; // 定义一个头结点 private int pos = 0;// 节点的位置 public LinkList() { this.first = null; } // 插入一个头节点 public void addFirstNode(int data) { Node node = new Node(data); node.next = first; first = node; } // 删除一个头结点,并返回头结点 public Node deleteFirstNode() { Node tempNode = first; first = tempNode.next; return tempNode; } // 在任意位置插入节点 在index的后面插入 public void add(int index, int data) { Node node = new Node(data); Node current = first; Node previous = first; while (pos != index) { previous = current; current = current.next; pos++; } node.next = current; previous.next = node; pos = 0; } // 删除任意位置的节点 public Node deleteByPos(int index) { Node current = first; Node previous = first; while (pos != index) { pos++; previous = current; current = current.next; } if (current == first) { first = first.next; } else { pos = 0; previous.next = current.next; } return current; } // 根据节点的data删除节点(仅仅删除第一个) public Node deleteByData(int data) { Node current = first; Node previous = first; // 记住上一个节点 while (current.data != data) { if (current.next == null) { return null; } previous = current; current = current.next; } if (current == first) { first = first.next; } else { previous.next = current.next; } return current; } // 显示出所有的节点信息 public void displayAllNodes() { Node current = first; while (current != null) { current.display(); current = current.next; } System.out.println(); } // 根据位置查找节点信息 public Node findByPos(int index) { Node current = first; if (pos != index) { current = current.next; pos++; } return current; } // 根据数据查找节点信息 public Node findByData(int data) { Node current = first; while (current.data != data) { if (current.next == null) return null; current = current.next; } return current; } } public class TestLinkList { public static void main(String[] args) { LinkList linkList = new LinkList(); linkList.addFirstNode(20); linkList.addFirstNode(21); linkList.addFirstNode(19); //print19,21,20 linkList.add(1, 22); //print19,22,21,20 linkList.add(2, 23); //print19,22,23,21,20 linkList.add(3, 99); //print19,22,23,99,21,20 //调用此方法会print 19,22,23,99,21,20 linkList.displayAllNodes(); } }
单向链表反转

双向链表
双向链表反转
定义
Big O(大O标记法)
时间,针对于规模的变化规律
O(1)
不随问题的规模扩大而计算时间变化
例如查找数组固定位置的算法,只需要知道偏移量就可以
O(n)
随问题规模线性增长
例如求数组平均数,遍历数组,需要取出数组每个元素相加,计算时间随数组长度(规模)变化而变化
O(n2)
O(n3)
等等
常见
树
二叉树
定义
每个节点下边最多有两个子节点
二叉搜索树
定义
左叶子节点一定比父节点小,右节点一定比父节点大
时间复杂度
o(logN)
就是树的高度
平衡二叉树
定义
二叉平衡树要求左子树和右子树的层高差不大于1
红黑树
定义
特殊的二叉搜索树
变换种类
变色
左旋
右旋
时间复杂度
o(logN)
特点
根节点一定是黑色
新插入的节点一定是红色
1、具有二叉查找树的特点。
2、根节点是黑色的;
3、每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存数据。
4、任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的。
5、每个节点,从该节点到达其可达的叶子节点是所有路径,都包含相同数目的黑色节点。
数据结构的进化
链表->二叉树->二叉搜索树->平衡二叉树->红黑树
二叉平衡树要求左子树和右子树的层高差不大于1,很严格,每次插入数据几乎都会导致树的重新平衡,所有出现了红黑树
中间件
消息队列
kafka
使用流程
生产者发送消息到
场景
5 broker
5 分区
3副本
broker
一个kafka实例就是一个broker
数据存储位置
producer发送消息之后,随机获取一个broker,假设是2,那么之后的副本会在一次增加一的机器上,3,4
发送时根据key进行hash值取模,分配到对应的分区上,一条数据只能被一个group id的消费者消费数据一次,比如5个相同group id,一条数据只会被5个中的一个消费
面试题
如何保证宕机时数据不丢失?
多副本机制
多副本冗余的高可用机制
多副本之间数据如何同步?
分区中的副本间分follow和ledder
每个分区的follow向ledder副本发送命令拉取数据
ISR到底指的什么东西?
所有在线的ledder和follow副本总和
In-Sync Replicas”,也就是保持同步的副本
acks参数的含义?
0
意思就是我的KafkaProducer在客户端,只要把消息发送出去,不管那条数据有没有在哪怕Partition Leader上落到磁盘,我就不管他了,直接就认为这个消息发送成功了。
1
默认
意思就是说只要Partition Leader接收到消息而且写入本地磁盘了,就认为成功了,不管他其他的Follower有没有同步过去这条消息了。
all
Partition Leader接收到消息之后,还必须要求ISR列表里跟Leader保持同步的那些Follower都要把消息同步过去,才能认为这条消息是写入成功了。
producer发送的时候设置的
ack=all一定就不会丢失数据吗
如果副本个数为1的话,此时也会丢失数据
上面的总结
       
isr
所有在线的ledder和follow副本总和
In-Sync Replicas”,也就是保持同步的副本
HW
hight water 最高水位线,消费者可见的最大偏移量,选举leader之后,其他follwer同步数据时,采用多退少补原则。比leader大则砍掉,少则同步过来
led
last end
分区内最后的偏移量
ack
根据设置的值不同,可以产生最少一次消费,最多一次消费的结果
-1
至少消费一次,保证数据存储下来,但是不保证重复性
重复原因
producer 发送数据给leader,leader保存下来了,同步给follwer时候leader挂了,那么producer没有收到ack确认信息,那么就会重新发送,此时就是数据重复了
可以配合幂等配置来实现(在一次会话中,就是不重启情况下)精准消费一次,通过生产者传递的pid 与 sequese number(幂等性的实现)来实现
0
1
保证的是生产者这边数据不丢失的问题
分区分配策略
round robin
以组为划分
过程
消费者按字典名称排序,分区按照分区编号排序,然后计算每个消费者消费分区数量为组内分区数量/消费者数量=m,m取整,不整的话,第一个消费者分区加一
range
以topic划分
过程
先将topic和partition排序,拍完例如,t1p1,t1p2,t2p1,t2p2
之后轮询cosumer,进行分配,c0一个,然后c1一个,往复进行
每次增加消费者,都会触发重新分配partion->consumer的对应关系
客户端采取拉取数据方式
这种模式,客户端可以控制流量,避免推产生的生产速度大于消费速度从而产生处理不过来的
offset的管理
存储单位
是以(组+topic+partition)为单位来保存所有partition消费的offset,确保添加或者删除消费者后,组内的消费者从正确的offset位置消费
存储位置
zk
本地
zk在kafka中的作用
leader选举
集群broker上下线
分区副本分配
幂等性
开启幂等性配置后,ack默认-1
生产者链接broker后,会返回一个pid(单次会话唯一,后期会通过分布式事务来形成全局唯一的pid)
实现
pid+topic+partition+seqNumber(自增长)
当这4者相同时,认为是同一个数据,不做写入
问题
producer挂掉后,会重新生成pid,此时再传递pid+partition+seqNumber,数据就不一样了,会产生重复数据了
事务
kafka会保存一个TrasactionId,这个是客户端给的,可以和pid关联起来
当客户端挂了,再次链接时,事务管理器会通过TrasationId,找到原来的PID分给producer,这时候pid还是原来的,配合幂等性配置,可以实现跨分区会话的一致性问题。
解决跨分区跨会话的数据一致性问题,通过在producer发送全局唯一pid+partition+seqNumber来解决
为什么效率高
采用零拷贝
     
解决问题
数据从硬盘读取后到达内核缓存,之后从内核缓存复制到用户缓存中,浪费时间和空间,所以产生零拷贝
从内核缓存到用户缓存多次复制的问题
方案
采用虚拟内存方式
多个虚拟内存地址可以指向同一个硬盘位置,解决需要从内核缓存复制数据到用户缓存的问题
有多少个leader
每个topic下,都会从partition中选取一个leader
偏移量管理
自动
5s自动提交一次
手动
同步
提交完偏移量才拉取下个数据
异步
拉数据和提交偏移量同时进行
偏移量是保存在zk中的,消费者每次都从kafka中获取
leader选举
为什么高效
顺序写
比随机写入效率高
以追加的方式写入
零拷贝
去掉了内核态拷贝数据到用户态这一过程
使用虚拟内存技术,使得用户态和内核态的地址都对应到虚拟内存的同一地址中,减少了内核态到用户态的拷贝过程
分区
kafka使用分区策略,每个partition对应多个segement,每次都处理一个小文件,可以增加并行处理速度
批量发送
数据压缩
采用gzip或者snappy
RokcetMq
各组件角色说明
producer
consumer
nameserver
相当于eureka,管理broker地址
broker
实际存储消息的机器
集群
集群时,主节点存储数据,从节点读取数据
发送消息的几种方式
发送同步消息
同步等待发送结果
场景
应用在高可用,不丢失数据的场景
发送异步消息
不等待发送结果,通过回调函数接收发送结果
场景
应用于对性能要求超高的场景,可能会丢失数据
单向发送消息
sendOneWay
场景
不关心返回结果
发送逻辑
如何接收消息
接收逻辑
消费消息时的接收方式
broker主动推
客户端pull拉
消费消息模式
负载均衡(默认方式)
消费者将消息均衡的分散处理
广播模式
每个消费者接收到全量的数据
顺序性消息(集群有问题)
全局有序
局部有序
让需要有顺序的业务消息在同一个队列中即可
发送方
发送方使用重写队列选择器,将同一业务id发送到同一个队列中
接收方
使用有序消费器
事务性消息
发送消息-》提交事务-》没有提交的话mq server回调查询事务处理结果,决定提交事务还是回滚事务
没有提交事务前,消息处于半消息状态,根据事务的结果(提交,回滚)进行发送和撤销消息
发送方
两种回调方法
执行本地事务
可以执行消息的提交、回滚、和未知状态
消息状态回查(server未收到本地事务状态时,或者状态为unkonwn时调用)
高性能原因
顺序写
存储文件已经提前占好了存储空间,在连续的空间内写
零拷贝
1、应用程序中调用 read() 方法,这里会涉及到一次上下文切换(用户态->内核态),底层采用DMA(direct memory access)读取磁盘的文件,并把内容存储到内核地址空间的读取缓存区。
2、由于应用程序无法访问内核地址空间的数据,如果应用程序要操作这些数据,得把这些内容从读取缓冲区拷贝到用户缓冲区。 read() 调用的返回引发一次上下文切换(内核态->用户态),现在数据已经被拷贝到了用户地址空间缓冲区,如果有需要,可以操作修改这些内容。
3、我们最终目的是把这个文件内容通过Socket传到另一个服务中,调用Socket的 send()方法,又涉及到一次上下文切换(用户态->内核态),同时,文件内容被进行第三次拷贝,这次的缓冲区与目标套接字相关联,与读取缓冲区无关。
4、 send()调用返回,引发第四次的上下文切换,同时进行第四次拷贝,DMA把数据从目标套接字相关的缓存区传到协议引擎进行发送。
2,3是cpu拷贝
1.4是dma拷贝
如果程序不操作文件内容,那么可以直接使用dma拷贝靠内核态,使用文件描述符,dma直接从对应地址拷贝出来到目标位置
DMA(direct memory access)
缓存
redis
知识考点
穿透

为什么选用
数据类型丰富
支持持久化
支持主从
支持分片
支持100000+QPS
redis采用单线程实现,不会出现线程冲突,所有请求串行处理
在测试中,QPS峰值情况下也没有出现cpu跑满的情况,所以cpu不是制约redis性能的关键
说说你用过的redis的数据类型(5种)
String
最基本的数据类型,二进制安全
二进制安全指的可以包含任何数据,例如jpg,序列化的对象
set name dingxu
get name
set integer 1
incr integer 结果是2
使用场景
微博文章点赞数量统计
文章:文章编号=点击数量
id全局生成唯一标识
incrby orderid 1000批量生成id,拿到本地缓存中使用
分布式session
记录总量
防重复提交
key(操作类型id):value
有值就不能提交
hash
存储hash映射表
添加
hset hashTable name dingxu age 26
获取
hget hashTable name "dingxu"
修改
hset hashTable age 25
一般存储键值对的数据,例如json之类的数据
String元素组成的字典,适合用于存储对象
使用场景
对象缓存
购物车
归并同一类型数据
list
列表,按照String元素插入顺序排序
左插入
lpush
lpush testlist aaa
lpush testlist bbb
后进先出
右插入
rpush
rpush testlist ccc
取出
lrange
lrange testlist 0 10
1) "bbb"
2) "aaa"
3) "ccc"
从左往右
大约能存储40亿个数据
可实现最新消息查询功能
使用场景
推送通知
set
String元素组成的无序集合,通过哈希表实现,不许重复
操作
 插入成功返回1,失败0
可以实现交集、并集、差集的功能
sadd
smembers
使用场景
利用交集,差集,并集,实现查找共同项,不同项,总项,去重功能
sorted set
通过分数来为集合中的成员进行从小到大的排序
操作

添加
zadd
zadd myzset 3 abc

取出
zrangebyscore
通过分数从小到大排列取出的数据
排行榜计算
从海量数据里查询某一固定前缀的keys
不要使用keys
keys一次性全都查出来
keys partten
应该使用scan
一次查询近似count数量的数据
scan 游标 match parttern count 数量

分布式锁
需要解决的问题
互斥性
安全性
死锁
宕机时,没有释放锁
容错
解决方案
set key value [EX seconds] [PX milliseconds] [NX|XX]

java实现
 public class RedisTool { private static final String LOCK_SUCCESS = "OK"; //NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作; private static final String SET_IF_NOT_EXIST = "NX"; //PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。 private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } }
可以用用户编号作为key,防止同一时间多次借款
应用场景
控制分布式系统之间同步访问共享资源的一种方式
如何解决超时未完成的任务
锁续期
抢到锁后,启动新线程监控执行时间,5秒一次,如果被监控的线程还持有锁,则增加锁持续时间
红锁
大量key同时过期,导致redis卡顿
可以在过期时间上加或者减一个随机数,防止同时过期,导致redis卡顿
为什么性能高
I/O多路复用,而不使用传统阻塞式io模型
服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种: (1)同步阻塞IO(Blocking IO):即传统的IO模型。 (2)同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库。 (3)IO多路复用(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。 (4)异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。 同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。 阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。 另外,Richard Stevens 在《Unix 网络编程》卷1中提到的基于信号驱动的IO(Signal Driven IO)模型,由于该模型并不常用,本文不作涉及。接下来,我们详细分析四种常见的IO模型的实现原理。为了方便描述,我们统一使用IO的读操作作为示例。 一、同步阻塞IO 同步阻塞IO模型是最简单的IO模型,用户线程在内核进行IO操作时被阻塞。  图1 同步阻塞IO 如图1所示,用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。 用户线程使用同步阻塞IO模型的伪代码描述为: { read(socket, buffer); process(buffer); } 即用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。 二、同步非阻塞IO 同步非阻塞IO是在同步阻塞IO的基础上,将socket设置为NONBLOCK。这样做用户线程可以在发起IO请求后可以立即返回。  图2 同步非阻塞IO 如图2所示,由于socket是非阻塞的方式,因此用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。 用户线程使用同步非阻塞IO模型的伪代码描述为: { while(read(socket, buffer) != SUCCESS) ; process(buffer); } 即用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。 三、IO多路复用 IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。  图3 多路分离函数select 如图3所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。 从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。 用户线程使用select函数的伪代码描述为: { select(socket); while(1) { sockets = select(); for(socket in sockets) { if(can_read(socket)) { read(socket, buffer); process(buffer); } } } } 其中while循环前将socket添加到select监视中,然后在while内一直调用select获取被激活的socket,一旦socket可读,便调用read函数将socket中的数据读取出来。 然而,使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处理,则可以提高CPU的利用率。 IO多路复用模型使用了Reactor设计模式实现了这一机制。  图4 Reactor设计模式 如图4所示,EventHandler抽象类表示IO事件处理器,它拥有IO文件句柄Handle(通过get_handle获取),以及对Handle的操作handle_event(读/写等)。继承于EventHandler的子类可以对事件处理器的行为进行定制。Reactor类用于管理EventHandler(注册、删除等),并使用handle_events实现事件循环,不断调用同步事件多路分离器(一般是内核)的多路分离函数select,只要某个文件句柄被激活(可读/写等),select就返回(阻塞),handle_events就会调用与文件句柄关联的事件处理器的handle_event进行相关操作。  图5 IO多路复用 如图5所示,通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用IO多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达了,用户线程一定不会被阻塞。 用户线程使用IO多路复用模型的伪代码描述为: void UserEventHandler::handle_event() { if(can_read(socket)) { read(socket, buffer); process(buffer); } } { Reactor.register(new UserEventHandler(socket)); } 用户需要重写EventHandler的handle_event函数进行读取数据、处理数据的工作,用户线程只需要将自己的EventHandler注册到Reactor即可。Reactor中handle_events事件循环的伪代码大致如下。 Reactor::handle_events() { while(1) { sockets = select(); for(socket in sockets) { get_event_handler(socket).handle_event(); } } } 事件循环不断地调用select获取被激活的socket,然后根据获取socket对应的EventHandler,执行器handle_event函数即可。 IO多路复用是最常使用的IO模型,但是其异步程度还不够“彻底”,因为它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO,而非真正的异步IO。 四、异步IO “真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。 异步IO模型使用了Proactor设计模式实现了这一机制。  图6 Proactor设计模式 如图6,Proactor模式和Reactor模式在结构上比较相似,不过在用户(Client)使用方式上差别较大。Reactor模式中,用户线程通过向Reactor对象注册感兴趣的事件监听,然后事件触发时调用事件处理函数。而Proactor模式中,用户线程将AsynchronousOperation(读/写等)、Proactor以及操作完成时的CompletionHandler注册到AsynchronousOperationProcessor。AsynchronousOperationProcessor使用Facade模式提供了一组异步操作API(读/写等)供用户使用,当用户线程调用异步API后,便继续执行自己的任务。AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作,实现真正的异步。当异步IO操作完成时,AsynchronousOperationProcessor将用户线程与AsynchronousOperation一起注册的Proactor和CompletionHandler取出,然后将CompletionHandler与IO操作的结果数据一起转发给Proactor,Proactor负责回调每一个异步操作的事件完成处理函数handle_event。虽然Proactor模式中每个异步操作都可以绑定一个Proactor对象,但是一般在操作系统中,Proactor被实现为Singleton模式,以便于集中化分发操作完成事件。  图7 异步IO 如图7所示,异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核,然后操作系统开启独立的内核线程去处理IO操作。当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的CompletionHandler分发给内部Proactor,Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步IO。 用户线程使用异步IO模型的伪代码描述为: void UserCompletionHandler::handle_event(buffer) { process(buffer); } { aio_read(socket, new UserCompletionHandler); } 用户需要重写CompletionHandler的handle_event函数进行处理数据的工作,参数buffer表示Proactor已经准备好的数据,用户线程直接调用内核提供的异步IO API,并将重写的CompletionHandler注册即可。 相比于IO多路复用模型,异步IO并不十分常用,不少高性能并发服务程序使用IO多路复用模型+多线程任务处理的架构基本可以满足需求。况且目前操作系统对异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式(IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的缓冲区中)。Java7之后已经支持了异步IO,感兴趣的读者可以尝试使用。   
利用Linux 中 Selector
 select同时监控多个文件的fd的可读可写情况,监听的任务交给selector,此时程序可以执行其他任务,
监听多个fd,read,write,close,文件事件产生时,文件事件处理器就会回调Fd绑定的事件处理器,此时redis可以执行已经可用的文件
用户线程异步注册事件处理器,事件处理器使用selecor等待数据到达,查询到之后通知用户线程,用户线程开始读取文件
多路
指的是多个网络连接
复用
指的是复用同一个线程
采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)
多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。对于文件来讲,就是监听一堆文件,某个文件写满则返回。对于redis来讲,就是用了上述技术去监听多个连接,当连接完成写入的时候,会监听到并返回
纯内存操作
redis实现异步队列
不好的实现
使用List作为队列,RPUSH产生消息,LPOP消费消息
lpop
从队列左面一条一条取数据
blpop
阻塞,知道队列有消息或者超时
blpop testList 30
等待30秒
缺点
如果有多个消费者,只能被一个消费者消费
正确的实现
pub\sub,主题订阅者模式
 
可以实现一对多的消费队列
发送者pub发布消息
publish topicName 信息
订阅者sub接收消息
subscribe topicName
订阅者可以订阅任意数量的频道
缺点
即发即收型
如果发送者,发送消息时,订阅者不在线,那么这条消息就会丢失
持久化
RDB(快照方式)
save
在主线程中进行保存会阻塞redis,直到保存完成
bgsave
后台产生一个子进程进行保存
配置文件
save 900 1
900秒内有一次插入操作,则全量保存一次
save 60 10
60秒内有10次插入,则会进行全量保存
save ""
不使用RDB快照方式
redis启动时,如果有dump.rdb文件,则会载入文件
缺点
内存数据全量同步,会由于i/o而影响性能
根据配置规则进行保存,如果到达下一次保存的触发条件前,宕机了,则会丢失数据,可以采用AOF进行持久化处理
注意
保存的是数据
AOF(Append-Only-File) 保存写状态
注意
保存的是所有客户端除查询以外的指令,以append的方式追加到aof文件中
可以通过手动或自动调用aof-rewrite触发,将aof文件生成成新的,根据当前数据结果对应的aof文件,解决aof记录步骤太详细而没有用问题,只需要根据当前的数据产生对应的aof操作
三种保存间隔的参数
always
总是
seconds
一秒一次
no
交给系统管理,一般存满之后保存一次
保存客户端输入的写入的指令
子主题
redis同步机制
主从同步原理
全量同步
slave发送sync命令到master节点
master启动后台进程,将redis数据快照保存到rdb文件中
master将保存数据快照期间收到的写命令缓存起来
master完成写文件命令后,将文件发送给slave
然后slave将新的快照文件rdb替换旧的快照文件
slave完成读文件到缓存后master将写文件期间的增量命令发送给slave
增量操作
master接收到用户的操作,判断是否要传播到slave上
将记录追加到aof文件中
将缓存的数据发送给slave
缺点,
主节点就一个,会出现单点故障,可以采用redis sentinel解决,可以从从节点选取新master
写操作在master,读在slave
恢复流程
不开旗混合模式的情况下
如果存在aof则走aof,不存在则rdb
开启混合模式
先用rdb恢复,再用aof恢复

如何在海量数据中快速找到所需
分片
按照某种规则去划分数据,分散存储在多个节点上
例如对key进行hash之后对机器数量进行取模
缺点
当机器数量发生变化时无法找到数据的位置
解决方案,采用一致性hash算法可以解决
一致性hash算法 使用key的hash值对2的32次方进行取模,主机在hash圆环上的位置可以用主机名或者ip进行hash变换,确定环上的位置,key取模后可以确定其所属机器,例如计算出来的值在b和c之间,那么这个数据就会存到c节点上,也就是下一个节点上 
一致性hash配合虚拟节点可以解决数据分布不均匀
存储算法
hash槽16384(类似一致性hash算法,相当于多了可以在hash环上可以自定义移动位置功能)
          
可以解决(特点)
单调性
分散性
平衡性
子主题
缺点(一致性hash算法)
不保证数据的均匀性
虚拟节点可以解决
redis各个机器可以自定义槽的范围,相当于在hash环上任意移动(添加或减少hash槽来实现)
采用crc16 % 16384确定存储机器位置
如果采用hash和机器数量取余
一旦机器数量变化,那么所有节点的数据就会发生变化,一致性hash可以解决
为什么用16384个槽
主要是效率问题和带宽导致
节点间Ping point需要同步槽信息,报文头可以存储16bit数据=2的16次方=65535,这太浪费了
槽少,节点少,压缩率高,省带宽
相关信息
redis的命令是原子性的,不用考虑并发问题,例如Integer的incr进行增加命令
例如统计web网站中一个用户一天登录的次数,可以对userId+时间(20190101)这个key,进行Incr操作,进行原子性加一操作
高可用
集群+哨兵模式
数据迁移两种状态
 
迁移(migrating)
数据被标记为迁移中
入口(importing)
数据被标记为入口中
迁移过程中不会产生新增节点node的对应关系,数据迁移完成之后才会重新映射关系
删除策略
定时删除
惰性删除
内存淘汰机制
1. noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。
2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
3. allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。
4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。
5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
数据结构
zset
跳表
     
面试题
血崩与穿透解决方案
血崩
定义
缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都 去查询数据库了,而对数据库 CPU 和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列 连锁反应,造成整个系统崩溃
解决方案
1.一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
2.给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
3. 为 key 设置不同的缓存失效时间。
穿透
定义
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在 缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请 求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题
解决方案
1、布隆过滤器(节约内存空间)
一种算法,可以粗略估算整体集合中是否有这个元素,效率高
如果布隆过滤器告诉你数据存在,那么实际数据可能不存在,如果告诉你不存在,那么一定是不存在的。
原因是自定义的hash函数碰撞导致的。一个位上可能对应多个数据hash的结果(可能不同值计算出的hash是相同的)
2、缓存空结果,设置过期时间较短
解决超卖问题】
子主题
1. 首先定义一个 Redis 队列名为 sku:awards,里面的元素的值都是比如 1,只是用来代表一个商品,元素的个数则是供秒杀的商品总数。
2. 因为 Redis 是单线程的,所以可以将并发的请求串行化,而且 Redis List 的 pop 操作是原子性的。
3. 所有请求打到 Redis 上,都是从 sku:awards 队列上 pop 出一个元素:
防止用户重复买
使用set队列,key是商品编号+标识,value是用户id
首先,我们会将商品id和数量在活动开始前缓存到redis的list中,value用1表示,有多少个数量,就存多少个1,活动开始,针对这个队列使用leftpop命令进行出栈活动,直到没有值后,触发更新到db的操作
mysql和redis如何保证数据同步
如何解决redis大key问题
定义
Redis使用过程中经常会有各种大key的情况, 比如单个简单的key存储的value很大。
由于redis是单线程运行的,如果一次操作的value很大会对整个redis的响应时间造成负面影响,导致IO网络拥塞。
解决方案
将整存整取的大对象,分拆为多个小对象。可以尝试将对象分拆成几个key-value, 使用multiGet获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多个redis实例中,降低对单个redis的IO影响;
主要采用了分治思想,将大数据拆分成几个小部分处理
String:key,value
value太大的话就把数据分成n份,整体key+第几份数字为新key
value%n取余,确定数据存储在第几份
需要查全量数据的时候,使用key模糊查询
数据库
关系型数据库
binlog
主要考查方向
架构
答案
 数据库最主要的功能就是存储,那肯定要有文件存储系统,有了硬件存储之后就需要软件的支持,就需要存储管理,管理数据的逻辑与物理关系,还需要缓存机制(最近最少使用等方案) 提高效率,以及sql解析模块,日志管理,权限管理,出错之后还要有融在机制,索引管理,加快查询速度,和锁的管理
索引
为什么使用索引
避免全表扫描,提升查询效率
为什么存在
避免全表扫描
目录启发而出
什么样的信息能成为索引
能让数据有一定区分性的字段,都可以,例如主键,身份证号
索引的数据结构
B+树
mysql主流
Hash
myIsam及innodb不显示支持hash
BitMap
mysql不支持
树的层级越多,越消耗io,所以产生出b+树,尽量在每一层中加入尽量多的分支
索引内容都在树的节点上
密集索引和稀松索引的区别

定义
密集索引
一个表只能有一个聚集索引
聚集索引是指数据库表行中数据的物理顺序与键值的逻辑(索引)顺序相同。
所谓聚簇索引,就是将索引和数据放到一起,找到索引也就找到了数据,我们刚才看到的B+树索引就是一种聚簇索引
非聚集索引
非聚集索引是指数据库表行中数据的物理顺序与键值的逻辑(索引)顺序不相同。
因为MyISAM的主索引并非聚簇索引,那么他的数据的物理地址必然是凌乱的,拿到这些物理地址,按照合适的算法进行I/O读取,于是开始不停的寻道不停的旋转
而非聚簇索引就是将数据和索引分开,查找时需要先查找到索引,然后通过索引回表找到相应的数据。InnoDB有且只有一个聚簇索引,而MyISAM中都是非聚簇索引。
主键索引
innodb
存储的主键的信息以及对应行的内容信息
myisam
只存有主键信息,行信息在另外一个文件中
辅助键索引
就是除了主键外的索引信息
innodb
采用b+树,数据都在树上
主键索引采用密集索引

默认是主键
无主键是选用第一个unionkey
都没有的话会生成一个隐藏的主键
由于主键和表数据在同一个文件中,所以加载主键到内存中时,也加在了主键对应的行数据,简单说就是叶子节点上包含了主键和主键对应行内的数据信息
辅助键索引采用非密集索引(稀疏索引)
先在辅助键b+树上根据where条件查询到数据后到主键索引的树上查询对应信息
myisam
树的索引信息和数据信息是分开的
myisam主键索引和辅助键索引都是非密集索引
只包含主键的信息,查询本行数据还要单独查询数据存储
只包含键的信息,需要通过键去数据存储中查询
复合索引的建立方式
和单列索引基本相同
联合索引是首先使用多列索引的第一列构建的索引树
索引数据的整体大方向顺序是由第一列确定的
首先先对符合索引第一列进行排序,确定b+树的整体结构
在第一列相同的情况下再对第二列排序,如果再相同则对第三列排序,以此类推
https://blog.csdn.net/mu_wind/article/details/110128016
面试问题
为什么mysql采用b+tree
        
主要为磁盘等外存储设备设计的一种平衡查找树
磁盘io次数为所有树结构中最少
b tree高度一般为2-4,只需要2-4次io即可查到
其他树,二叉树,平衡二叉树,红黑树的高度不固定
根节点常驻内存中
所以查询一般要1-3次
存储数据量为16kb/(8b+8b)约等于1k,1k*1k*1k,三层的b+树可以存储10亿条记录
数据挂在叶子节点上,不需要每次读节点时加载数据,减少io次数
为什么不选择b树
b树特点是每个节点都带数据域,那么每次读取的时候都需要读取数据域,增加了io的次数
B-树(B类树)的特点就是每层节点数目非常多,层数很少,目的就是为了就少磁盘IO次数,当查询数据的时候,最好的情况就是很快找到目标索引,然后读取数据,使用B+树就能很好的完成这个目的,但是B-树的每个节点都有data域(指针),这无疑增大了节点大小,说白了增加了磁盘IO次数(磁盘IO一次读出的数据量大小是固定的,单个数据变大,每次读出的就少,IO次数增多,一次IO多耗时啊!),而B+树除了叶子节点其它节点并不存储数据,节点小,磁盘IO次数就少。这是优点之一。 另一个优点是什么,B+树所有的Data域在叶子节点,一般来说都会进行一个优化,就是将所有的叶子节点用指针串起来。这样遍历叶子节点就能获得全部数据,这样就能进行区间访问啦。
最左查询条件的产生
    
多列排序是基于什么原则的呢(重点)
实际上在MySQL中,联合索引的排序有这么一个原则,从左往右依次比较大小,就拿刚才建立的索引举例子,他会先去比较age的大小,如果age的大小相同,那么比较height的大小,如果height也无法比较大小, 那么就比较weight的大小,最终对这个索引进行排序。
索引条件下推5.6以及5.6版本以后存在
作用
减少非聚簇联合索引时,回表查询次数
定义
5.6之前
5.6以及之后
锁
innodb
默认用的行级锁,支持表级锁
当走索引,那么锁住的是当前行
关闭自动提交事务,且查询语句走索引(lock in share mode 共享锁),并锁住,锁的是当前行,其他update不能更新当前行
当不走索引,那么锁住的是整张表
关闭自动提交事务,且查询语句不走索引,并锁住,锁的是当前表,其他update不能更新当前表
MySQL的行锁是针对索引加的锁
myisam
默认用的表级锁,不支持行级锁
种类
读锁(共享锁)
lock in share mode
写锁(排它锁)
for update
间隙锁(范围锁)
https://zhuanlan.zhihu.com/p/48269420
产生条件
间隙锁只有在事务隔离级别 RR 中才会产生;
唯一索引只有锁住多条记录或者一条不存在的记录的时候,才会产生间隙锁,指定给某条存在的记录加锁的时候,只会加记录锁,不会产生间隙锁;
普通索引不管是锁住单条,还是多条记录,都会产生间隙锁;
间隙锁会封锁该条记录相邻两个键之间的空白区域,防止其它事务在这个区域内插入、修改、删除数据,这是为了防止出现 幻读 现象;
普通索引的间隙,优先以普通索引排序,然后再根据主键索引排序(多普通索引情况还未研究);
数据库开启间隙锁配置
定义
只有在Read Repeatable、Serializable隔离级别才有,就是锁定范围空间的数据,假设id有3,4,5,锁定id>3的数据,是指的4,5及后面的数字都会被锁定,因为此时如果不锁定没有的数据,例如当加入了新的数据id=6,就会出现幻读,间隙锁避免了幻读。
记录锁,行锁
子主题
mysql锁应用场景
            
https://blog.csdn.net/kevin_tech/article/details/108301875
锁和事务等级之间的关系
为什么要有读锁和写锁,事务等级解决不了么
面试题
如何加写锁,和释放锁
查询语句后加 for update
lock tables tableName read | write
在同一个session中 unlock tables
如何选择存储引擎
innodb
频繁的增删改
按索引执行的时候,增删改锁住行,效率高
可靠性高,要求事务的系统
myisam
大量count的时候选择
,myisam默认把count的总数存到了变量里,使用的时候直接取出,不必扫描表
频繁查询,少量增删改的时候
增删改要锁表
不需要支持事务的系统
锁的分类
悲观锁与乐观锁的实现
悲观锁
对数据加锁,innodb中 for update加写锁,或者lock in share mode 加读锁
乐观锁
表中加入版本号字段,提交update的时候加上先前获取的version作为条件进行更新,如果失败,则用户程序自行处理逻辑
mvcc
什么是mvcc
简介 MVCC(Multi-Version Concurrency Control)即多版本并发控制。 MySQL的大多数事务型(如InnoDB,Falcon等)存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,他们一般都同时实现了MVCC。当前不仅仅是MySQL,其它数据库系统(如Oracle,PostgreSQL)也都实现了MVCC。值得注意的是MVCC并没有一个统一的实现标准,所以不同的数据库,不同的存储引擎的实现都不尽相同。 作为MySQL中使用最广泛的存储引擎,本文主要讨论的是InnoDB的多版本并发控制的实现 MVCC优缺点 MVCC在大多数情况下代替了行锁,实现了对读的非阻塞,读不加锁,读写不冲突。缺点是每行记录都需要额外的存储空间,需要做更多的行维护和检查工作。 MVCC的实现原理 undo log 为了便于理解MVCC的实现原理,这里简单介绍一下undo log的工作过程 在不考虑redo log 的情况下利用undo log工作的简化过程为: 序号 动作 1 开始事务 2 记录数据行数据快照到undo log 3 更新数据 4 将undo log写到磁盘 5 将数据写到磁盘 6 提交事务 1)为了保证数据的持久性数据要在事务提交之前持久化 2)undo log的持久化必须在在数据持久化之前,这样才能保证系统崩溃时,可以用undo log来回滚事务 Innodb中的隐藏列 Innodb通过undo log保存了已更改行的旧版本的信息的快照。 InnoDB的内部实现中为每一行数据增加了三个隐藏列用于实现MVCC。 列名 长度(字节) 作用 DB_TRX_ID 6 插入或更新行的最后一个事务的事务标识符。(删除视为更新,将其标记为已删除) DB_ROLL_PTR 7 写入回滚段的撤消日志记录(若行已更新,则撤消日志记录包含在更新行之前重建行内容所需的信息) DB_ROW_ID 6 行标识(隐藏单调自增id) 结构 数据列 .. DB_ROW_ID DB_TRX_ID DB_ROLL_PTR MVCC工作过程 MVCC只在READ COMMITED 和 REPEATABLE READ 两个隔离级别下工作。READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE 则会对所有读取的行都加锁 SELECT InnoDB 会根据两个条件来检查每行记录: InnoDB只查找版本(DB_TRX_ID)早于当前事务版本的数据行(行的系统版本号 行的删除版本号(DB_ROLL_PTR)要么未定义(未更新过),要么大于当前事务版本号(在当前事务开始之后更新的)。这样可以确保事务读取到的行,在事务开始之前未被删除。 INSERT InnoDB为新插入的每一行保存当前系统版本号作为行版本号 DELETE InnoDB为删除的每一行保存当前的系统版本号作为行删除标识 UPDATE InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识 参考资料 《高性能MYSQL》 MySQL 5.7参考手册 MVCC实现方式 转载请注明出处: https://www.jianshu.com/p/a3d49f7507ff 作者:孙成酱子说 链接:https://www.jianshu.com/p/a3d49f7507ff 来源:简书 简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
并发版本控制
通过三个拓展字段来实现
事务编号
回滚记录
隐藏的行号
插入或者更新的时候会在事务编号这一列更新为新的当前事务编号,删除时标记为已删除
查询时,只查询事务编号比当前事务小的,和删除的版本号没有或者大于当前事务的
应用在两种隔离机制上
读已提交(rc)
读取版本号最大的数据
可重复度(rr)
读取版本号小于当前版本(已提交的最新数据)
删除版本号大于当前版本的(删除发生在当前版本之后)
mcvv只能解决基于快照读的幻读. 如果事务中使用了当前读, 则还会出现幻读的问题.
update,insert,select for update会使用当前读
语法
理论范式
第一范式
必须保证每列的原子性
第二范式
非主属性必须完全依赖主属性(主键|联合主键)
第三范式
非主属性必须完全直接依赖主属性
子主题
范式
定义
规则的意思
解决
数据冗余的问题
主从同步原理
执行流程
相关题
如何设计一个数据库
答案
 数据库最主要的功能就是存储,那肯定要有文件存储系统,有了硬件存储之后就需要软件的支持,就需要存储管理,管理数据的逻辑与物理关系,还需要缓存机制(最近最少使用等方案) 提高效率,以及sql解析模块,日志管理,权限管理,出错之后还要有融在机制,索引管理,加快查询速度,和锁的管理
如何定位并优化慢查询sql
根据慢日志定位慢查询sql
show variables like '%quer%'
slow_query_log
on
slow_query_log_file
文件存储路径
long_query_time
多长时间算慢查询
在my.ini中配置,永久生效,上面的重启就失效
show status like '%slow_queries%'
查询慢查询的条数
一次会话中,关闭连接重置
使用explain分析sql
   
explain主要列说明
type
访问类型
ALL、index(全索引扫描)、range、 ref、eq_ref、const、system、NULL(从左到右,性能从差到好)
all
全表扫描
index
根据索引的排序,取索引对应的行的数据
全索引扫描
Full Index Scan,index与ALL区别为index类型只遍历索引树
select code from t ; // 如果code 身上有索引, 那code就是索引列, 这个就是查询索引列, 并不是在where条件后面用索引, 效率会比all 类型快一些.
range
只检索给定范围的行,使用一个索引来选择行
ref
表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值
key
经过优化器评估最终使用的索引
id
在复合查找中,id越大,越先被调用
rows
要得到最终记录索要扫描经过的记录数
Extra
Using filesort
当Query 中包含 ORDER BY 操作,而且无法利用索引完成排序操作的时候
数据较少时从内存排序,否则从磁盘排序
Using index
从索引中就可以获得所需的全部数据
修改sql尽力走索引
数据库存储引擎有哪些
innodb
主要使用b+树
数据直接存储在树上
支持行锁,表锁,间隙锁,共享锁,排它锁
myisam
仅支持表锁
innodb和myisam文件存储上有什么区别
innodb
frm
存储表结构信息
idb
存储数据和索引
myisam
frm
存储表结构信息
myi
存储索引信息
myd
存储表数据
最左匹配原则(只是一个原则,实际情况还是要看sql优化器的选择,不理想时可以用force强制使用某些索引)
如果建立联合索引,mysql会一直从where条件从左匹配,直到遇到>,<,betwen,like就停止匹配

当使用联合索引中的某一个字段或者某几个字段查询时,只有包含最左面第一个字段的联合条件能用到索引
联合索引会对最左面第一个字段排序
根据第一个值建立索引关键字
当where中,没有出现最左面的字段时,且包含一部分复合索引的字段,那么此时type为index,意思是扫描了全部的索引,因为索引中包含where条件中的字段,但这些字段都不是最左的第一个字段,此时是无序的,所以就扫描全部的索引
有这样的a,b,c索引组合
当where 中 a=xx and b/c=xx时
ref
a=xx and b/c > yy
range
a=xx and d=yy
ref
只要包含了最左面的查询字段,那么这个查询中不论select 和where其他的所有字段
b=xx and d=yy
all
b>xx and d=yy
all
当where查询字段有范围查询(是符合索引中的某一个字段),且复合索引没有包括表中所有字段,那么此时select * 会是all的结果,如果select中只包含复合索引中的某几列,此时是Index
既要遍历所有索引(不包含最左字段)也要回表查询的话,这时候会放弃index方式,而采用all的方式
查询条件中,只要包含最左的字段,此时如果还包含了其他符合索引中的字段作为条件,符合索引a,b,c a=xx and b>yy 此时是range,或者a>xx and b>yy
当带有范围查询时,无论是否使用的哪个索引字段,只要select中出现了非索引字段,那么就会是all查询
查询条件中,不包含最左字段,而且select的字段是索引中的字段,此时是index,如果select中有不是索引字段的字段,那么是all
如果最左字段是范围查询,select中包含有非索引字段,此时是all,posibble key可选符合索引
索引越多越好吗
当数据量小的时候不需要建立索引
索引也要占空间,并且每次更新数据的时候也要更新索引
如何优化msql查询
创建索引
可以提高联表速度
在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
使用explain
https://segmentfault.com/a/1190000008131735            
type
子主题
system
表中只有一条数据. 这个类型是特殊的 const 类型.
const
针对主键或唯一索引的等值查询扫描
eq_ref
此类型通常出现在多表的 join 查询, 表示对于前表的每一个结果, 都只能匹配到后表的一行结果. 并且查询的比较操作通常是 =, 查询效率较高
ref
此类型通常出现在多表的 join 查询, 针对于非唯一或非主键索引, 或者是使用了 最左前缀 规则索引的查询.
index
由于不是唯一索引,所以索引扫描
index和ref的区别
作者:沈杰 链接:https://www.zhihu.com/question/36996520/answer/93256153 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 我来说一下吧: 这是你的表结构,有三个字段,分别是id,name,cid CREATE TABLE `student` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `cid` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `name_cid_INX` (`name`,`cid`), ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 索引方面:id是主键,(name,cid)是一个多列索引。 ----------------------------------------------------------------------------- 下面是你有疑问的两个查询: EXPLAIN SELECT * FROM student WHERE cid=1;  EXPLAIN SELECT * FROM student WHERE cid=1 AND name='小红';  你的疑问是:sql查询用到索引的条件是必须要遵守最左前缀原则,为什么上面两个查询还能用到索引? --------------------------------------------------------------------------------------------------------------------------- 讲上面问题之前,我先补充一些知识,因为我觉得你对索引理解是狭隘的: 上述你的两个查询的explain结果中显示用到索引的情况类型是不一样的。,可观察explain结果中的type字段。你的查询中分别是: 1. type: index 2. type: ref 解释: index:这种类型表示是mysql会对整个该索引进行扫描。要想用到这种类型的索引,对这个索引并无特别要求,只要是索引,或者某个复合索引的一部分,mysql都可能会采用index类型的方式扫描。但是呢,缺点是效率不高,mysql会从索引中的第一个数据一个个的查找到最后一个数据,直到找到符合判断条件的某个索引。 所以:对于你的第一条语句: EXPLAIN SELECT * FROM student WHERE cid=1; 判断条件是cid=1,而cid是(name,cid)复合索引的一部分,没有问题,可以进行index类型的索引扫描方式。explain显示结果使用到了索引,是index类型的方式。 --------------------------------------------------------------------------------------------------------------------------- ref:这种类型表示mysql会根据特定的算法快速查找到某个符合条件的索引,而不是会对索引中每一个数据都进行一 一的扫描判断,也就是所谓你平常理解的使用索引查询会更快的取出数据。而要想实现这种查找,索引却是有要求的,要实现这种能快速查找的算法,索引就要满足特定的数据结构。简单说,也就是索引字段的数据必须是有序的,才能实现这种类型的查找,才能利用到索引。 有些了解的人可能会问,索引不都是一个有序排列的数据结构么。不过答案说的还不够完善,那只是针对单个索引,而复合索引的情况有些同学可能就不太了解了。 下面就说下复合索引: 以该表的(name,cid)复合索引为例,它内部结构简单说就是下面这样排列的:  mysql创建复合索引的规则是首先会对复合索引的最左边的,也就是第一个name字段的数据进行排序,在第一个字段的排序基础上,然后再对后面第二个的cid字段进行排序。其实就相当于实现了类似 order by name cid这样一种排序规则。 所以:第一个name字段是绝对有序的,而第二字段就是无序的了。所以通常情况下,直接使用第二个cid字段进行条件判断是用不到索引的,当然,可能会出现上面的使用index类型的索引。这就是所谓的mysql为什么要强调最左前缀原则的原因。 那么什么时候才能用到呢? 当然是cid字段的索引数据也是有序的情况下才能使用咯,什么时候才是有序的呢?观察可知,当然是在name字段是等值匹配的情况下,cid才是有序的。发现没有,观察两个name名字为 c 的cid字段是不是有序的呢。从上往下分别是4 5。 这也就是mysql索引规则中要求复合索引要想使用第二个索引,必须先使用第一个索引的原因。(而且第一个索引必须是等值匹配)。 --------------------------------------------------------------------------------------------------------------------------- 所以对于你的这条sql查询: EXPLAIN SELECT * FROM student WHERE cid=1 AND name='小红'; 没有错,而且复合索引中的两个索引字段都能很好的利用到了!因为语句中最左面的name字段进行了等值匹配,所以cid是有序的,也可以利用到索引了。 你可能会问:我建的索引是(name,cid)。而我查询的语句是cid=1 AND name='小红'; 我是先查询cid,再查询name的,不是先从最左面查的呀? 好吧,我再解释一下这个问题:首先可以肯定的是把条件判断反过来变成这样 name='小红' and cid=1; 最后所查询的结果是一样的。 那么问题产生了?既然结果是一样的,到底以何种顺序的查询方式最好呢? 所以,而此时那就是我们的mysql查询优化器该登场了,mysql查询优化器会判断纠正这条sql语句该以什么样的顺序执行效率最高,最后才生成真正的执行计划。所以,当然是我们能尽量的利用到索引时的查询顺序效率最高咯,所以mysql查询优化器会最终以这种顺序进行查询执行。
注意
索引是已经排过序的
使用PROCEDURE ANALYSE() 来分析表中现有数据的数据类型是否合理,并且提出相应的意见
开启查询缓存
show variables like '%query_cache%';
vi /etc/my.cnf
[mysqld]中添加:
query_cache_size = 20M
query_cache_type = ON
关联查询时,确保on和using字段上有索引,而且是在关联的第二个表上建立的。
00 
尽量小,尽量固定
查询的时候尽量少字段
字段类型尽力固定大小
建立索引时如何确定组合索引的顺序
选择性
distince(字段)/count(*)
数值越大则选择性越高,越能过滤更多的字段
选择选择性最高的列放在前面
聚簇索引的优点和缺点
优点
缺点
为什么推荐使用自增长主键
可以保证数据是按顺序插入的。不需要重新计算和移动已经有的索引
避免使用随机主键,会使得插入变得完全随机
禁止隐式转换
查询where a='1',禁止使用a=1
禁止索引列参与函数计算
explain中key_len越长越好么
在不损失精度的情况下,越短越好
不损失精度指的是使用了全部的索引列
主从同步原理
主推送给从
索引相关问题
组成
页
默认16k
页目录
管理数据的目录
管理页的目录
目录页的本质也是页,普通页中存的数据是项目数据,而目录页中存的数据是普通页的地址。
索引会排序
页内数据区和多页模式本质上都是链表,那么的确可以采用相同的方式来对其进行优化,它就是目录页。
管理多页的目录级别是页,而页目录管理的级别是行。
索引很难么?带你从头到尾捋一遍MySQL索引结构,不信你学不会!_数据库_灵熙云的编程工作室-CSDN博客.rar
mysql为什么会有最左原则

在innodb的复合索引中,a,b,首先会对a列进行排序,此时a列绝对有序,其后对b进行排序,那么此时b就是相对于a列是相对有序的,所以只有a列绝对有序,使用查找时也就a列可以用到索引ref,单独使用其他复合索引列中的除最左的元素列都是index,触发的全索引扫描
100w和1000w,数据命中索引时,查询时间是否相同
mysql能承受多少并发,为什么
数据库连接池如何监控

可以使用druid自带的监控
子主题
事务隔离等级
对读锁和写锁的加锁时间不同,所产生不同的效果。从而产生了四种等级
(Read uncommitted)
操作同一条数据 事务1:--------------------------- | | 写 回滚 事务2:--------------------------- | 读 这时产生脏读
定义
读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。
问题
脏读
解决方案
那怎么解决脏读呢?Read committed!读提交,能解决脏读问题。
(Read committed)
操作同一条数据 事务1:------------- -------------- | | 写 写完 事务2:-------------- ------------- | | 加读锁,不可读取 此时可以读取 解决脏读,产生不可重复度问题 事务1:------------------------------- ----------------------- | | | 读 读完,事务结束。 扣款结束。 不加写(update)锁,扣款开始,事务开始。 事务2:------------------------- ----------- ----------------------- | 事务1读完,事务结束,但突然被事务二修改了数据 那么在事务1扣款时的数据和读取时的数据就不同,产生不可重复度的问题 新理解: 开启事务,并且事务一读取数据,期间事务二修改数据,事务二修改完之后,提交事务,此时事务一不提交事务,并且再次读取数据,发现数据变了,就是不可重复读的问题
定义
读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题
表现为同一个事物中相同的条件,两次读取出来的数据内容不一样,但数量相同
通过mvcc实现,读的永远是最近的版本号数据,所以会产生不可重复读的问题
解决了脏读
问题
不可重复读
解决方案
Repeatable read
Repeatable read(mysql默认级别)
事务1:------------------------------------------------------ | | | 读 读完,事务结束。 扣款结束。 加写锁(update锁),扣款开始,事务开始。 事务2:----------------------------------------------------------- | 事务1读完,事务结束,但突然被事务二修改了数据,但此时有写锁,不能修改,所以解决不可重复读的问题 但还会产生幻读问题 新理解: 事务一开始,按年龄30读取表中数据,读取出4条记录,事务二开始,向表中插入一条年龄为30的数据,并且提交,之后事务一更新表中年龄为30的数据,事务一发现更新成功了5条数据,这就是幻读,理论上市这样,但mysql使用mvcc模式巧妙的方式,使得在repetable read 级别下解决了这个问题 幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读。
定义
重复读,就是在开始读取数据(事务开启)时,不再允许修改操作
表现为更新数据的数量和期望的结果不一样
幻读是由插入或者删除引起的。
通过mvcc实现,读的是小于当前版本号的最新一条数据,所以一个事务中读到的数据相同
解决了不可重复读
简介
在一次事务里面,多次查询之后,结果集的个数不一致的情况叫做幻读。
而多出来或者少的哪一行被叫做 幻行
在同一个事物中,多次读取数据是相同的
问题
幻读是指当事务不是独立执行时发生的一种现象。 事务A读取与搜索条件相匹配的若干行。事务B以插入或删除行等方式来修改事务A的结果集,然后再提交。 幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读。
不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作
mvcc并不能彻底解决幻读问题
RR并不能解决幻读,
事务1:select ID>4 假设有两条数据 (for update)
事务2:insert id=5
事务2:提交
事务1:select ID>4 还是有两条,基于mvcc
事务1:update name='2' where id>4,insert update使用了当前读破坏了原先的mvcc
事务1:select ID>4,应该会有3条,会把事务2插入的数据查询出来 (如果事务1开始加了for update,则事务二被间隙锁锁住,不会插入成功,所以就不会有幻读的问题了)
事务1:提交
t2时间没有查到,倒是插入的时候不让插,幻读问题
配合排它锁for update(使用范围间隙锁,不让其他事务插入数据),在rr模式下解决了幻读的问题
解决方案
Serializable
rc和rr的区别
Serializable
事务1:------------------------------- ----------------------- | | | 读 读完,事务结束。 扣款结束。 加写锁,扣款开始,事务开始。 事务2:------------------------- ----------- ----------------------- | 事务1读完,事务结束,但突然被事务二修改了数据,但此时有写锁,不能修改,所以解决不可重复读的问题 但还会产生幻读问题
Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。
Mysql的默认隔离级别是Repeatable read。
大多数数据库默认的事务隔离级别是Read committed
产生问题
脏读
不可重复度
幻读
高等级事务产生的问题,低等级都会有,只是随着事务等级的提升,逐渐解决部分问题,直到最高等级事务串行,才没有问题,事务等级提高,伴随着性能的丧失
https://blog.csdn.net/sanyuesan0000/article/details/90235335
事务传播机制
@Transactional(propagation=Propagation.REQUIRED)
如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
@Transactional(propagation=Propagation.NOT_SUPPORTED)
容器不为这个方法开启事务
@Transactional(propagation=Propagation.REQUIRES_NEW)
不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
@Transactional(propagation=Propagation.MANDATORY)
必须在一个已有的事务中执行,否则抛出异常
@Transactional(propagation=Propagation.NEVER)
必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
@Transactional(propagation=Propagation.SUPPORTS)
如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
索引
如果有组合索引,那么在where时,单独使用其中一个字段的话,只有在组合索引的前面的字段可以使用到索引,除第一个索引字段的其他索引字段不能使用索引
索引是对记录集的多个字段进行排序的方法。在一张表中为一个字段创建一个索引,将创建另外一个数据结构,包含字段数值以及指向相关记录的指针,然后对这个索引结构进行排序,允许在该数据上进行二分法排序。
什么时候应该建
什么时候不应该建
频繁增删改的字段不建议建立,每次需要额外的时间和cpu,维护索引文件
表数据太少
数据分布平均的字段,如果字段有很多重复数据,那么建立索引意义不大,因为索引需要排序在查找,排完了一堆一样的,还是没用
两大功能
查找
排序
order by 导致file sort
事务
保持数据的一致性和完整性
存储引擎
innodb
myisam
定义
数据组织的形式
mysql读写分离
amoeba底层代理插件
分库分表
分库
连表查询方案
字段冗余
数据同步
广播表(全局表)
   
绑定表
有相同关联关系的数据都放到一个库里
使用es搜索,无论怎么分表,数据都同步给es,直接查es即可
对于没有走分表键的查询,那么只能轮询出来所有数据内存聚合分页或者使用es查询
分表
水平
取模
基于范围
枚举
hash
一致性哈希
hashCode(机器名称)%2^32,获取到机器在hash环中的位置,当新增机器扩容的时候,可以迁移很少的数据来进行升级。如果采用ip取余机器数的方式,那么数据需要全部进行重新rehash计算,影响范围大
节点和分表key按照统一规则计算出一个哈希值(虚拟值),以相同的对比规则按照大小方式进行排列,增加删除节点总是在某一个范围内进行小部分迁移,从而达到减少数据迁移量的目的。
建立索引规则
left join时候,将右表关联列添加索引
right join ,将左表关联列添加索引
性能分析
explain
select type
type
const
在普通查询中where后
使用查询字段的索引类型
主键或者唯一索引
eq_ref
在连表查询中
使用查询字段的索引类型
主键或者唯一索引
ref
针对的是普通索引,使用常量查询可能查出来很多行匹配
使用查询字段的索引类型
普通索引,非主键和唯一索引
ext
order by 是用不到索引就会产生User filesort
order by
使用索引的排序功能
产生using index 的情况
order by 符合最左匹配原则
where 子句和 order by 子句 组合符合最左匹配原则
如果发生了字段重合,where 和 order by 中有重合字段的情况,如果where和order by 字段做了并集(where -> a=,b> order by -> b,c)之后,满足最左匹配,那么就能用索引,并且没有filesort
where 中的字段和order by 或者 group by 中的字段如果和索引顺序一致,则可以使用索引进行查找和排序
索引 a,b,c,
where a = 1 order by b ,c
可以使用到a索引,并且可以使用bc进行排序,ext中是using where
where a = 1 order by c,b
a能用索引查找,但是c,b使用不到索引排序,ext中产生file sort
group by
使用索引的排序的功能
where 中的字段和order by 或者 group by 中的字段如果和索引顺序一致,则可以使用索引进行查找和排序
索引 a,b,c,
where a = 1 order by b ,c
可以使用到a索引,并且可以使用bc进行排序,ext中是using where
where a = 1 order by c,b
a能用索引查找,但是c,b使用不到索引排序,ext中产生file sort
where
使用到索引的查找功能
全值匹配我最爱,最左前缀要遵守;
带头大哥不能死,中间兄弟不能断;
索引列上少计算,范围之后全失效;
Like百分写最右,覆盖索引不写星;
不等空值还有or,索引失效要少用;
VAR引号不可丢,SQL高级也不难!
除了 ext字段(usering filesort,using temprery)以外,都是针对查找的分析
where 中的字段和order by 或者 group by 中的字段如果和索引顺序一致,则可以使用索引进行查找和排序
索引 a,b,c,
where a = 1 order by b ,c
可以使用到a索引,并且可以使用bc进行排序,ext中是using where
where a = 1 order by c,b
a能用索引查找,但是c,b使用不到索引排序,ext中产生file sort
show profiles
关键术语
间隙锁
共享锁
排它锁
自增锁
mvcc
事务等级
隔离级别
总结
事务隔离级别
mvcc
拓展字段
事务版本号
删除/修改版本号
作用
解决读取时,高并发情况下,锁竞争激烈的问题
mvcc主要就是为了让读写不冲突。
无法解决
MVCC并不是万灵药。大量的业务问题的关键点在于,你在提交一个事务那一刹那,你提交事务的所有修改依赖的读取是否都还有效,例如扣库存
共享锁
lock in share model
应用场景
排它锁
for update
应用场景
间隙锁
可重复读模式下
解决幻读问题
记录锁
优化
表设计时,适当的冗余查分,减少连表查询
web容器
tomcat
监控
probe
可以查看内存使用情况
线程数
tomcat manager
配置优化

docs/config/host.html
autoDeploy
docs/config/context.html
reloadable:false
docs/config/http.html
maxConnections
acceptCount
maxThreads
minspareThreads
都可以用probe监控后进行数字调节
enableLookups
req.getRmoteHost是否进行dns查询
conf/server.xml
protocol="org.apache.coyote.http11.Http11AprProtocol"
使用api连接器,增加并发量
分布式协调者
zookeeper
zab协议
1.领导者选举
2.2pc
3.过半机制
4.同步机制
socket
leader选举算法
大于半数投票机制
4台机器,4/2=2,>2,至少有三台参与选举,那么最多就只能挂掉1台
3台机器,3/2=1,>1,至少有两台参与选举,那么最多只能挂掉1台
这也是产生网上说集群数量为奇数的原因
myId
当zxid都相同时,myid最大的当leader
zxid
每有一条数据则zxid加一,投票比的就是zxid大小
数据同步算法
通过记录日志,持久化到dataTree中,服务弱挂掉后,重新启动读取日志
面试题
zk和eureka的区别
从cap理论下手
C - Consistent-一致性
A - Availability-可用性
P - Partition tolerance -分区容错性
系统设计
秒杀业务
业务特点
秒杀时网站的访问量大增;
假设一个页面200k,带图片,那么需要增加带宽
数据库压力增大
秒杀时购买的请求数量远小于库存,只有部分用户能够成功;
业务流程简单,根据先后顺序,下订单减库存;
子主题
解决方案
订单队列+锁
QueueController
package com.example.demo.dx.controller; import com.example.demo.dx.OrderDealThread; import com.example.demo.dx.OrderRequest; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; @RestController public class QueueController { private static ConcurrentLinkedQueue orderQueue = new ConcurrentLinkedQueue(); private static AtomicInteger totalprodocut; private static AtomicInteger totalQueSize; private ExecutorService excutorService = Executors.newCachedThreadPool(); public static ReentrantLock queueLock = new ReentrantLock(true); static { totalprodocut = new AtomicInteger(15); totalQueSize = new AtomicInteger(15); } @RequestMapping("/queque") public String queque(OrderRequest orderRequest) { try { // queueLock.lock(); if (totalprodocut.get() return "error"; } else { if (orderQueue.size() // System.out.println(orderRequest.getGood_id()+" 增加到待处理队列成功:" + orderQueue.size()); orderQueue.add(orderRequest); } else { return "error"; } } if (!OrderDealThread.dealLock.isLocked()) { OrderDealThread dealQueue = new OrderDealThread(orderQueue); excutorService.execute(dealQueue); } } catch (Exception e) { e.printStackTrace(); return "not done"; } finally { // queueLock.unlock(); } return "ok"; } }
OrderDealThread
package com.example.demo.dx; import java.util.Iterator; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; public class OrderDealThread implements Runnable { ConcurrentLinkedQueue orderQueue; private static AtomicInteger totalprodocut; public static ReentrantLock dealLock = new ReentrantLock(true); static { totalprodocut = new AtomicInteger(15); } public OrderDealThread() { } public OrderDealThread(ConcurrentLinkedQueue queque) { this.orderQueue = queque; } @Override public void run() { while (!orderQueue.isEmpty()) { try { dealLock.lock(); Iterator it = orderQueue.iterator(); while (it.hasNext()) { dealQueque(it.next()); } } catch (Exception e) { e.printStackTrace(); } finally { dealLock.unlock(); } } } void dealQueque(OrderRequest orderRequest) { if (orderRequest.getStatus() == 0) { int status = 2; if (totalprodocut.get() > 0) {//闇€鍐嶆鍒ゆ柇鏄惁杩樻湁鍟嗗搧锛屽姞閿佽寖鍥村唴 //TODO 下单处理其他逻辑 totalprodocut.decrementAndGet();// 减库存 status = 1; } if (status == 2) { // System.out.println(orderRequest.getUserId() + " deal er:" + Thread.currentThread().getName()); orderRequest.setStatus(2); } else { System.out.println(orderRequest.getUserId() + " deal ok:" + Thread.currentThread().getName()); orderRequest.setStatus(1); } } } }
OrderRequest
package com.example.demo.dx; import java.util.Random; public class OrderRequest { private int goodId = new Random().nextInt(100000);// 商品id private int userId = new Random().nextInt(100000);// 用户ID private int status;// 0:未处理;1:正常;2:异常 public int getGoodId() { return goodId; } public void setGoodId(int goodId) { this.goodId = goodId; } public int getUserId() { return userId; } public void setUserId(int userId) { this.userId = userId; } public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } }
redis缓存商品数量,利用“栈”:从头部放入,先进后出和“队列”:尾部进入,先进先出
使用redis队列,因为pop操作是原子的,即使有很多用户同时到达,也是依次执行.(mysql事务在高并发下性能下降很厉害,文件锁的方式也是).
高并发
高性能的实践方案

1、集群部署,通过负载均衡减轻单机压力。
2、多级缓存,包括静态数据使用CDN、本地缓存、分布式缓存等,以及对缓存场景中的热点key、缓存穿透、缓存并发、数据一致性等问题的处理。
3、分库分表和索引优化,以及借助搜索引擎解决复杂查询问题。
4、考虑NoSQL数据库的使用,比如HBase、TiDB等,但是团队必须熟悉这些组件,且有较强的运维能力。
5、异步化,将次要流程通过多线程、MQ、甚至延时任务进行异步处理。
6、限流,需要先考虑业务是否允许限流(比如秒杀场景是允许的),包括前端限流、Nginx接入层的限流、服务端的限流。
7、对流量进行削峰填谷,通过MQ承接流量。
8、并发处理,通过多线程将串行逻辑并行化。
9、预计算,比如抢红包场景,可以提前计算好红包金额缓存起来,发红包时直接使用即可。
10、缓存预热,通过异步任务提前预热数据到本地缓存或者分布式缓存中。
11、减少IO次数,比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。
12、减少IO时的数据包大小,包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。
13、程序逻辑优化,比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化,或者采用更高效的算法。
14、各种池化技术的使用和池大小的设置,包括HTTP请求池、线程池(考虑CPU密集型还是IO密集型设置核心参数)、数据库和Redis连接池等。
15、JVM优化,包括新生代和老年代的大小、GC算法的选择等,尽可能减少GC频率和耗时。
16、锁选择,读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。
上述方案无外乎从计算和 IO 两个维度考虑所有可能的优化点,需要有配套的监控系统实时了解当前的性能表现,并支撑你进行性能瓶颈分析,然后再遵循二八原则,抓主要矛盾进行优化。
高可用的实践方案

1、对等节点的故障转移,Nginx和服务治理框架均支持一个节点失败后访问另一个节点。
2、非对等节点的故障转移,通过心跳检测并实施主备切换(比如redis的哨兵模式或者集群模式、MySQL的主从切换等)。
3、接口层面的超时设置、重试策略和幂等设计。
4、降级处理:保证核心服务,牺牲非核心服务,必要时进行熔断;或者核心链路出问题时,有备选链路。
5、限流处理:对超过系统处理能力的请求直接拒绝或者返回错误码。
6、MQ场景的消息可靠性保证,包括producer端的重试机制、broker侧的持久化、consumer端的ack机制等。
7、灰度发布,能支持按机器维度进行小流量部署,观察系统日志和业务指标,等运行平稳后再推全量。
8、监控报警:全方位的监控体系,包括最基础的CPU、内存、磁盘、网络的监控,以及Web服务器、JVM、数据库、各类中间件的监控和业务指标的监控。
9、灾备演练:类似当前的“混沌工程”,对系统进行一些破坏性手段,观察局部故障是否会引起可用性问题。
高可用的方案主要从冗余、取舍、系统运维3个方向考虑,同时需要有配套的值班机制和故障处理流程,当出现线上问题时,可及时跟进处理。
高扩展的实践方案

1、合理的分层架构:比如上面谈到的互联网最常见的分层架构,另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层(但是需要评估性能,会存在网络多一跳的情况)。
2、存储层的拆分:按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分(分库分表)。
3、业务层的拆分:最常见的是按照业务维度拆(比如电商场景的商品服务、订单服务等),也可以按照核心接口和非核心接口拆,还可以按照请求源拆(比如To C和To B,APP和H5)。
linux
查找文件
find 路径 [参数] parttern
精确查找
find ~ -name test.java
模糊查找
find ~ name test*
不区分大小写
find ~ iname test*
文件检索
grep
grep '内容 ' 文件名
grep 'abc' test.txt
过滤符合匹配的数据
grep -o 'engine\[0-9a-z]*\'
管道符
|
只能传递标准输出,错误无法传递
只有支持管道的命令才能使用
sed
awk
grep
cut
head
top
less
more
wc
join
sort
split
awk,使用处理列
适用于结构化的表格数据
默认分隔符为空格,想要修改则用-F ‘,’指定
常用方式
显示文件第一列的数据
awk '{print $1 $4}' netstat.txt
按条件筛选复合条件的列

awk '$1=="tcp" && $2==1 {print $0}' netstat.txt
统计一列中某个数据出现的次数
sed,适用处理行

java
线上分析工具
mat
柱状图图标功能

shallow heap
只是对象本身占用空间,例如list里面放的user,这里只统计List的自己的,不包含user
retained headp
对象本身,包括本身内的所有内容,例如list里面放的user,这里只统计List的自己的以及包含user,总共占用数量
jstack
功能
查看线程状态
public static enum Thread.Stateextends Enum线程状态。线程可以处于下列状态之一: 1.NEW 至今尚未启动的线程的状态。 2.RUNNABLE 可运行线程的线程状态。处于可运行状态的某一线程正在 Java 虚拟机中运行,但它可能正在等待操作系统中的其他资源,比如处理器。 3.BLOCKED 受阻塞并且正在等待监视器锁的某一线程的线程状态。处于受阻塞状态的某一线程正在等待监视器锁,以便进入一个同步的块/方法,或者在调用 Object.wait 之后再次进入同步的块/方法。 4.WAITING 某一等待线程的线程状态。某一线程因为调用下列方法之一而处于等待状态: 不带超时值的 Object.wait 不带超时值的 Thread.join LockSupport.park 处于等待状态的线程正等待另一个线程,以执行特定操作。 例如,已经在某一对象上调用了 Object.wait() 的线程正等待另一个线程,以便在该对象上调用 Object.notify() 或 Object.notifyAll()。已经调用了 Thread.join() 的线程正在等待指定线程终止。 5.TIMED_WAITING具有指定等待时间的某一等待线程的线程状态。某一线程因为调用以下带有指定正等待时间的方法之一而处于定时等待状态: Thread.sleep 带有超时值的 Object.wait 带有超时值的 Thread.join LockSupport.parkNanos LockSupport.parkUntil 6.TERMINATED 已终止线程的线程状态。线程已经结束执行。
new
runnable
blocked
waiting
timed_waiting
terminated
查看线程的调用(堆栈)情况
查看线程对资源的锁定情况
使用步骤
ps -ef|grep 查看java进程id
jstack 进程id,最下面能看到死锁情况
top -p 进程id -H 查看进程中的线程
printf "%x" 数字 转换成16进制,在jstack中查找16进制的数字,能看到这个线程正在执行什么
一般配合top命令使用,分析cpu高,应用缓慢时,查看是否有死锁
jstat
jstat -gc
查看gc情况
https://www.cnblogs.com/boothsun/p/8127552.html
jstat -gcutil 182068
查看gc摘要
jstat -gcnew
查看对象在年轻代情况
查看年轻代对象活了多少次回收
TT:对象在新生代存活的次数
MTT:对象在新生代存活的最大次数
jinfo -flag
查看jvm参数
arthas
jad
jad 类型 在线反编译
trace
trace 跟踪方法执行时间
thread
thread 列出所有线程和cpu使用 thread 线程号查询线程干什么
thread -b 查找死锁
readDefine
将本地class文件,替换线上class文件
阿里出品
this指的是什么
this指的是当前对象的实例,例如 Person p = new Person,在p中调用this,那么这个this指的就是p引用的实例
list set map
面试题
ArrayList linkedList为什么线程不安全
源码中没有使用sychernized,或者cas技术
vector为什么效率低,不适合高并发场景
synchronized锁住了方法
equals和hashCode
treeset怎么实现排序
重写compareTo方法,进行排序
hashMap ,hashTable,ConcurrentHashMap
hashMap
转换为树的阈值
TREEFY_THRESHOLD常量定义的,为8
转回连表的阈值,小于
(UNTREEFY_THRESHOLD常量)=6
扩容因子
DEFAULT_LOAD_FACTOR常量
0.75f
数组默认16长度
数组扩容
新建一个数组,原值拷贝过去
子主题
实现
数组
链表
红黑树
为什么容量是2的倍数
方便使用位运算代替取余运算
当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。如果不是2的n次方,计算结果不相等
为什么自定义了hash的计算方式
  
使得进行hash计算时,分布均匀
按照经验值,数组长度一般不会超过2的16次方,那么就导致hashCode,的高16位数字在计算下标的时候不会参与计算(h&(length-1)),所以h的生成方式为上面的公式,使得,hashCode的高16位也参与计算,增加散列几率
不是线程安全,多线程会发生问题
put,get,resize,rehash,扩容时都可能出现线程安全问题
总结
采用红黑树
红黑树是二叉搜索树的一种变形,二叉搜索树会产生极端情况,退化成链表,变成时间复杂度o(n),红黑树通过变色,左旋,右旋,避免退化成链表
时间复杂度为O(logn)
负载因子为什么是0.75
出于对空间和时间的平衡考虑
首先,Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
结合负载因子的定义公式可知,threshold就是在此Load factor和length(数组长度)对应下允许的最大元素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。默认的负载因子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
如果空间足够,追求效率,那么负载因子可以降低,这样数组长度变长了,hashcode产生碰撞的几率就会减小,但是空间就占用的多了
主要属性
node数组(hash桶)
主属性
length*loadFactor=threshold
size
hashMap发生结构变化的次数
例如put会触发加一
为什么链表长度大于8要转换成红黑树
1. 在良好的hashCode算法下,根据柏松分布定律,链表长度达到8的概率为千万分之一,很小
2. 红黑树的平均查找长度,也就是时间复杂度是log(n),长度为8,查找长度为log(8)=3,因为2的3次方是8,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要,因为log(8)比8/2小,用树结构需要的查找步数更少;链表长度如果是小于等于6,6/2=3,2<log(6) <3 , 4/2=2, log(4)就是2,虽然树的速度也很快的,但是转化为树结构和生成树的时间并不会太短。所以最终选择8是从时间复杂度考虑的结果,从8开始用树效率更好。
hashTable
对this对象上锁,性能差
ConcurrentHashMap
采用cas 和 synchronized控制并发
早期
采用分段锁
将初始化的16个元素单独上锁,理论上速度提高了16倍
采用cas+syn技术
linkedHashMap
顺序map,按照存储的顺序遍历
treeMap
根据排序函数排序的map,按照排序之后的结果遍历,需要实现comparator接口
基础
hashSet无序
treeset有序
单例
懒汉,线程不安全
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } 这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。
懒汉,线程安全
public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } 这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
饿汉
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; } } 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
饿汉,变种
public class Singleton { private static Singleton instance = null; static { instance = new Singleton(); } private Singleton() { } public static Singleton getInstance() { return instance; } } 表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化instance。
静态内部类
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton() { } public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } } 这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。
枚举
public enum SomeThing { INSTANCE; private Resource instance; SomeThing() { instance = new Resource(); } public Resource getInstance() { return instance; } } 这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。
双重校验锁
//这样写虽然正确,但是粗暴地把getHelper锁住了,这样代价很大 class Foo { private Helper helper = null; public synchronized Helper getHelper() { if (helper == null) { helper = new Helper(); } return helper; } } 只有getHelper()的第一次调用需要同步创建对象,创建之后getHelper()只是简单的返回成员变量,而这里是无需同步的。 由于同步一个方法会降低100倍或更高的性能[2], 每次调用获取和释放锁的开销似乎是可以避免的:一旦初始化完成,获取和释放锁就显得很不必要。许多程序员一下面这种方式进行优化: 1、检查变量是否被初始化(不去获得锁),如果已被初始化立即返回这个变量。 2、获取锁 3、第二次检查变量是否已经被初始化:如果其他线程曾获取过锁,那么变量已被初始化,返回初始化的变量。 4、否则,初始化并返回变量。 //这种双重检查锁定就很好地解决问题,避免每次调用都要获取锁 class Foo { private volitier Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized(this) { if (helper == null) { helper = new Helper(); } } } return helper; } }
object下的方法
protected Object clone()
创建并返回此对象的一个副本
boolean equals(Object obj)
指示某个其他对象是否与此对象“相等”
protected void finalize()
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
Class<? extendsObject> getClass()
返回一个对象的运行时类。
int hashCode()
返回该对象的哈希码值。
void notify()
唤醒在此对象监视器上等待的单个线程。
void notifyAll()
唤醒在此对象监视器上等待的所有线程。
String toString()
返回该对象的字符串表示。
void wait()
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
void wait(long timeout)
导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。
void wait(long timeout, int nanos)
导致当前的线程等待,直到其他线程调用此对象的 notify()
java8

lambda
在1.8中没有方法区,也就是permgen,取而代之的是Metaspace
存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap
什么时候用str.intern()
metaspace村什么
jvm
并发与并行
并发与并行的区别 原创 2016年05月22日 23:17:31 标签:操作系统 /并发 /多线程 5553 学习多线程的时候会遇到一个名词:并发。这是属于操作系统中的词汇,需要了解并发和并行的区别,从网上搜集了几种说法帮助理解。 一: 并发是指一个处理器同时处理多个任务。 并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。 并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。 来个比喻:并发是一个人同时吃三个馒头,而并行是三个人同时吃三个馒头。  二: 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,一小时后,两个人各挖一个小一点的坑,要想挖两个大一点得坑,一定会用两个小时。  并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在,并发能够在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行)。 三: 当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态.这种方式我们称之为并发(Concurrent)。  当系统有一个以上CPU时,则线程的操作有可能非并发.当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
并发是指一个处理器同时处理多个任务。
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
java内存模型
 
主内存,工作内存和线程三者的交互关系
内存分布
堆(heap)
用来存储程序中的一些对象,比如你用new关键字创建的对象,它就会被存储在堆内存中,但是这个对象在堆内存中的首地址(对象引用)会存储在栈中。
定义
java堆是被所有线程共享的一块内存区域
虚拟机启动时创建
所有的对象实例以及数组都要在堆上分配
细分
新生代
eden空间
From survivorkongjian
To survivor空间
老年代
细分目的
更好的进行回收内存
虚拟机栈(stack)
栈:在jvm中栈用来存储一些对象的引用、局部变量以及计算过程的中间数据,在方法退出后那么这些变量也会被销毁。它的存储比堆快得多,只比CPU里的寄存器慢
线程私有的,他的生命周期与线程相同
描述的是java方法执行的内存模型
每个方法执行的时候都会创建一个栈帧(可以理解为栈中存的对象,成员变量是局部变量表,操作数,等等),用于存储局部变量表、操作数栈,动态链接,方法出口等
局部变量表
方法执行过程中的所有变量,int ,float,double,long等,方法输入输出参数
局部变量表所需要的内存空间在编译期间完成分配
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError
每一个 JVM 线程都拥有一个私有的 JVM 线程栈,用于存放当前线程的 JVM 栈帧(包括被调用函数的参数、局部变量和返回地址等)。如果某个线程的线程栈空间被耗尽,没有足够资源分配给新创建的栈帧,就会抛出 java.lang.StackOverflowError 错误。
引发 StackOverFlowError 的常见原因有以下几种:
无限递归循环调用(最常见)。
执行了大量方法,导致线程栈空间耗尽。
方法内声明了海量的局部变量。
native 代码有栈上分配的逻辑,并且要求的内存还不小,比如 java.net.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64位 Linux)。
如果拓展时无法申请到足够的内存,就会抛出OutOfMemoryError
-Xss 线程栈的大小,影响栈的深度和并发数,一般256K,当线程数量非常多的时候可能会出现outofMemoryError,原因是线程过多,每个线程分配256,总量大于
-xms -xmx,初始和最大堆(dui)的值一般设置成固定的值,以防内存抖动
栈中,存在多个栈帧,每个方法执行产生一个栈帧
本地方法栈
native方法
java8 非堆
    
定义
各个线程共享的内存区域
存储已被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据,属性,方法
主区域
类信息、常量、静态变量以及即时编译器编译后的代码等数据
常量池(也叫CLass文件常量池,编译成class文件之后,在class文件中,未加载到内存之前)
存放编译器生成的字面常量和符号引用
堆中有则调用intern时复制引用到常量池
没有则在常量池保存
字面量
String
int
static的常量
float
等基本变量
符号引用
符号引用,顾名思义,就是一个符号,符号引用被使用的时候,才会解析这个符号。如果熟悉linux或unix系统的,可以把这个符号引用看作一个文件的软链接,当使用这个软连接的时候,才会真正解析它,展开它找到实际的文件 对于符号引用,在类加载层面上讨论比较多,源码级别只是一个形式上的讨论。 当一个类被加载时,该类所用到的别的类的符号引用都会保存在常量池,实际代码执行的时候,首次遇到某个别的类时,JVM会对常量池的该类的符号引用展开,转为直接引用,这样下次再遇到同样的类型时,JVM就不再解析,而直接使用这个已经被解析过的直接引用。 除了上述的类加载过程的符号引用说法,对于源码级别来说,就是依照引用的解析过程来区别代码中某些数据属于符号引用还是直接引用,如,System.out.println("test" + "abc");//这里发生的效果相当于直接引用,而假设某个String s = "abc"; System.out.println("test" + s);//这里的发生的效果相当于符号引用,即把s展开解析,也就相当于s是"abc"的一个符号链接,也就是说在编译的时候,class文件并没有直接展看s,而把这个s看作一个符号,在实际的代码执行时,才会展开这个s
作用
用于保存编译时确定的数据
运行时常量池(程序运行起来之后)
存放
运行时常量池是方法区的一部分。CLass文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
作用
用于保存运行时动态加载的数据,以及常量池的数据
类加载后,常量池中的数据会在运行时常量池中存放!
1.8运行时常量池和字符串常量池被移动到了堆中
程序计数器
它可以看作是当前线程所执行的字节码的行号指示器
线程私有的
对java计数,如果方法是native的,则为undefined
内存结构图

堆区
yong
s1+s0
eden
old
非堆区(metaspace)
ccs
压缩累指针
短直针
堆里的每个对象都有指向自己class的指针,开启压缩累指针后对象所引用的类信息存在于ccs中,由64位变成32位
codeCache
各部分存储内容
程序计数器是jvm执行程序的流水线,存放一些跳转指令。
本地方法栈是jvm调用操作系统方法所使用的栈。
虚拟机栈是jvm执行java代码所使用的栈。
方法区存放了一些常量、静态变量、类信息等,可以理解成class文件在内存中的存放位置。
深入浅出java常量池 理论 jvm虚拟内存分布: 程序计数器是jvm执行程序的流水线,存放一些跳转指令。 本地方法栈是jvm调用操作系统方法所使用的栈。 虚拟机栈是jvm执行java代码所使用的栈。 方法区存放了一些常量、静态变量、类信息等,可以理解成class文件在内存中的存放位置。 虚拟机堆是jvm执行java代码所使用的堆。 Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。 所谓静态常量池,即*.class文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量: 类和接口的全限定名 字段名称和描述符 方法名称和描述符 而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。 运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。 String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。 常量池的好处 常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。 例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。 (1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。 (2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。 接下来我们引用一些网络上流行的常量池例子,然后借以讲解。 1 String s1 = "Hello"; 2 String s2 = "Hello"; 3 String s3 = "Hel" + "lo"; 4 String s4 = "Hel" + new String("lo"); 5 String s5 = new String("Hello"); 6 String s6 = s5.intern(); 7 String s7 = "H"; 8 String s8 = "ello"; 9 String s9 = s7 + s8; 10 11 System.out.println(s1 == s2); // true 12 System.out.println(s1 == s3); // true 13 System.out.println(s1 == s4); // false 14 System.out.println(s1 == s9); // false 15 System.out.println(s4 == s5); // false 16 System.out.println(s1 == s6); // true 首先说明一点,在java 中,直接使用==操作符,比较的是两个字符串的引用地址,并不是比较内容,比较内容请用String.equals()。 s1 == s2这个非常好理解,s1、s2在赋值时,均使用的字符串字面量,说白话点,就是直接把字符串写死,在编译期间,这种字面量会直接放入class文件的常量池中,从而实现复用,载入运行时常量池后,s1、s2指向的是同一个内存地址,所以相等。 s1 == s3这个地方有个坑,s3虽然是动态拼接出来的字符串,但是所有参与拼接的部分都是已知的字面量,在编译期间,这种拼接会被优化,编译器直接帮你拼好,因此String s3 = "Hel" + "lo";在class文件中被优化成String s3 = "Hello",所以s1 == s3成立。只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。 s1 == s4当然不相等,s4虽然也是拼接出来的,但new String("lo")这部分不是已知字面量,是一个不可预料的部分,编译器不会优化,必须等到运行时才可以确定结果,结合字符串不变定理,鬼知道s4被分配到哪去了,所以地址肯定不同。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中。 配上一张简图理清思路:  s1 == s9也不相等,道理差不多,虽然s7、s8在赋值的时候使用的字符串字面量,但是拼接成s9的时候,s7、s8作为两个变量,都是不可预料的,编译器毕竟是编译器,不可能当解释器用,不能在编译期被确定,所以不做优化,只能等到运行时,在堆中创建s7、s8拼接成的新字符串,在堆中地址不确定,不可能与方法区常量池中的s1地址相同。  s4 == s5已经不用解释了,绝对不相等,二者都在堆中,但地址不同。 s1 == s6这两个相等完全归功于intern方法,s5在堆中,内容为Hello ,intern方法会尝试将Hello字符串添加到常量池中,并返回其在常量池中的地址,因为常量池中已经有了Hello字符串,所以intern方法直接返回地址;而s1在编译期就已经指向常量池了,因此s1和s6指向同一地址,相等。 特例1 publicstaticfinal String A = "ab"; // 常量Apublicstaticfinal String B = "cd"; // 常量Bpublicstaticvoid main(String[] args) { String s = A + B; // 将两个常量用+连接对s进行初始化 String t = "abcd"; if (s == t) { System.out.println("s等于t,它们是同一个对象"); } else { System.out.println("s不等于t,它们不是同一个对象"); } } s等于t,它们是同一个对象 A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:String s=A+B; 等同于:String s="ab"+"cd"; 特例2 publicstaticfinal String A; // 常量Apublicstaticfinal String B; // 常量Bstatic { A = "ab"; B = "cd"; } publicstaticvoid main(String[] args) { // 将两个常量用+连接对s进行初始化 String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s等于t,它们是同一个对象"); } else { System.out.println("s不等于t,它们不是同一个对象"); } } s不等于t,它们不是同一个对象 A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。 至此,我们可以得出三个非常重要的结论: 必须要关注编译期的行为,才能更好的理解常量池。 运行时常量池中的常量,基本来源于各个class文件中的常量池。 程序运行时,除非手动向常量池中添加常量(比如调用intern方法),否则jvm不会自动添加常量到常量池。 以上所讲仅涉及字符串常量池,实际上还有整型常量池、浮点型常量池(java中基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean;两种浮点数类型的包装类Float,Double并没有实现常量池技术) 等等,但都大同小异,只不过数值类型的常量池不可以手动添加常量,程序启动时常量池中的常量就已经确定了,比如整型常量池中的常量范围:-128~127,(Byte,Short,Integer,Long,Character,Boolean)这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。 例如在自动装箱时,把int变成Integer的时候,是有规则的,当你的int的值在-128-IntegerCache.high(127) 时,返回的不是一个新new出来的Integer对象,而是一个已经缓存在堆 中的Integer对象,(我们可以这样理解,系统已经把-128到127之 间的Integer缓存到一个Integer数组中去了,如果你要把一个int变成一个Integer对象,首先去缓存中找,找到的话直接返回引用给你就 行了,不必再新new一个),如果不在-128-IntegerCache.high(127) 时会返回一个新new出来的Integer对象。 实践 说了这么多理论,接下来让我们触摸一下真正的常量池。 前文提到过,class文件中存在一个静态常量池,这个常量池是由编译器生成的,用来存储java源文件中的字面量(本文仅仅关注字面量),假设我们有如下java代码: 1 String s = "hi"; 为了方便起见,就这么简单,没错!将代码编译成class文件后,用winhex打开二进制格式的class文件。如图:  简单讲解一下class文件的结构,开头的4个字节是class文件魔数,用来标识这是一个class文件,说白话点就是文件头,既:CA FE BA BE。 紧接着4个字节是java的版本号,这里的版本号是34,因为笔者是用jdk8编译的,版本号的高低和jdk版本的高低相对应,高版本可以兼容低版本,但低版本无法执行高版本。所以,如果哪天读者想知道别人的class文件是用什么jdk版本编译的,就可以看这4个字节。 接下来就是常量池入口,入口处用2个字节标识常量池常量数量,本例中数值为00 1A,翻译成十进制是26,也就是有25个常量,其中第0个常量是特殊值,所以只有25个常量。 常量池中存放了各种类型的常量,他们都有自己的类型,并且都有自己的存储规范,本文只关注字符串常量,字符串常量以01开头(1个字节),接着用2个字节记录字符串长度,然后就是字符串实际内容。本例中为:01 00 02 68 69。 接下来再说说运行时常量池,由于运行时常量池在方法区中,我们可以通过jvm参数:-XX:PermSize、-XX:MaxPermSize来设置方法区大小,从而间接限制常量池大小。 假设jvm启动参数为:-XX:PermSize=2M -XX:MaxPermSize=2M,然后运行如下代码: 1 //保持引用,防止自动垃圾回收 2 List list = new ArrayList(); 3 4 int i = 0; 5 6 while(true){ 7 //通过intern方法向常量池中手动添加常量 8 list.add(String.valueOf(i++).intern()); 9 } 程序立刻会抛出:Exception in thread "main" java.lang.outOfMemoryError: PermGen space异常。PermGen space正是方法区,足以说明常量池在方法区中。 在jdk8中,移除了方法区,转而用Metaspace区域替代,所以我们需要使用新的jvm参数:-XX:MaxMetaspaceSize=2M,依然运行如上代码,抛出:java.lang.OutOfMemoryError: Metaspace异常。同理说明运行时常量池是划分在Metaspace区域中。具体关于Metaspace区域的知识,请自行搜索。
虚拟机堆是jvm执行java代码所使用的堆。
垃圾回收
算法
先用可达性分析,标记出可回收的对象
标记清除
 
复制

标记整理

分代收集算法
young
采用复制算法
俗称 minor GC
old
采用标记整理或者清除
old+young 称为full gc
垃圾回收器
一般来说,为特定的一代选择大小是这些考虑因素之间的一种权衡。例如,一个非常大的年轻一代可能会最大限度地提高吞吐量,但这样做的代价是占用空间、速度和暂停时间。年轻一代的停顿可以通过使用一个小的年轻一代来最小化,而代价是吞吐量。一个世代的大小不会影响另一个世代的收集频率和暂停时间。 没有一种正确的方式来选择一代人的年龄。最佳选择取决于应用程序使用内存的方式以及用户需求。因此,虚拟机对垃圾收集器的选择并不总是最优的,可能会被本节中描述的命令行选项覆盖。世世代代.
Parallel Collector(jdk8 默认)复制算法,标记整理算法 并行回收器(指垃圾回收线程之间并行)
参数
-XX:+UseParallelGC 启用
-XX:ParallelGCThreads=<N>
设置垃圾回收线程数量
不同垃圾回收线程会产生碎片反应,减少垃圾收集器线程的数量和增加持久代的大小将减少这种碎片效应。
默认情况下,当 CPU 数量小于8, ParallelGCThreads 的值等于 CPU 数量,当 CPU 数量大于 8 时,则使用公式:ParallelGCThreads = 8 + ((N - 8) * 5/8) = 3 +((5*CPU)/ 8)
默认堆增量20%,减小量5%,可以手动设置
-XX:YoungGenerationSizeIncrement=<Y>
年轻一代
-XX:TenuredGenerationSizeIncrement=<T>
终身的一代
-XX:AdaptiveSizeDecrementScaleFactor=<D>
如果增长增量是X百分比,那么收缩的减少是X/D百分比
使用场景
吞吐量优先
大内存
内存大可能会出现垃圾回收时间过长
自动调优方法(设置两个目标)
-XX:MaxGCPauseMillis=<N>
最大垃圾收集暂停时间
-XX:GCTimeRatio=<N>
吞吐量
默认值N 99
计算方式1/1+N
例如-XX:GCTimeRatio=19将目标设定为垃圾收集总时间的1/20或5%,那么吞吐量为95%
-Xmx<N>
使用选项-Xmx指定最大的堆占用空间。此外,收集器有一个隐含的目标,只要满足其他目标,就可以最小化堆的大小
目标的优先次序
1 最大暂停时间目标
减小堆
2 吞吐量目标
增大堆
3 最小堆目标
首先达到最大暂停时间目标。只有在达到它之后,才能解决吞吐量目标。同样,只有在实现了前两个目标之后,才考虑到足迹目标
servial,parallel,cms特点
年轻代收集使用单eden、双survivor进行复制算法;
年轻代、老年代是独立且连续的内存块;
老年代收集必须扫描整个老年代区域;
都是以尽可能少而块地执行GC为设计原则
算法
标记整理
G1(代替cms)
响应时间优先
可预测模型
可以社任职在m毫秒内垃圾回收时间不超过n毫秒
参数
-XX:G1HeapRegionSize=n
可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。
-XX:GCTimeRatio
G1默认为9,而CMS默认为99
整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间-XX:G1MaxNewSizePercent(默认60%)之间动态变化,且由参数目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩容的大小以及分区的已记忆集合(RSet)计算得到。当然,G1依然可以设置固定的年轻代大小(参数-XX:NewRatio、-Xmn),但同时暂停目标将失去意义。
-XX:MaxGCPauseMillis
最大停止时间
算法???
复制+标记整理
不再有eden s1,s2物理上的隔离,空闲的区域可随时转换
基础概念
术语
Rset
当前region中,被其他region引用的对象信息
Cset
分区
eden
s0
s1
old
huge
巨大对象储存区
>=0.5个regoin大小时
region
每个分块大小
1-32m之间
mixGc
g1中没有专门的老年代gc,只有年轻代和老年代一起的混合gc
回收过程
初次标记
标记gcRoot对象和其所在region位置(rootRegion)
会stw
扫描所有old区
找出并标识所有old区的rset中含有rootRegoin引用的区块
并发标记
扫描上一步标识出来的区域
范围比cms遍历范围缩小
重新标记
同cms,使用satb
会产生stw
复制清理
只选拉圾较少的region清理,省时间
使用场景
响应时间优先
衡量好坏的参数
吞吐量
等待时间
为什么有不同种回收期
适用于不同硬件设施和需求场景
cpu,内存大小
吞吐量和停顿时间
不能容忍停顿时间过长
衡量标准(不包括并发和g1收集器)
吞吐量
停顿时间
一个非常大的年轻一代可能会最大限度地提高吞吐量,但这样做的代价是占用空间、速度和暂停时间。年轻一代的停顿可以通过使用一个小的年轻一代来最小化,而代价是吞吐量
分代回收
GC Roots,枚举根节点,做可达性分析算法进行回收
可以成为gc root的对象

虚拟机栈中引用的对象,栈祯中的本地变量表
例如方法中,People p = new People ,这个new People就是root
方法区中,常量引用的对象
类中定义的常量(static final),常量保存的是某一个对象的地址,被保存的对象就是gc root
方法区中类静态属性引用的对象
a.虚拟机栈(栈桢中的本地变量表)中的引用的对象
b.方法区中的类静态属性引用的对象
c.方法区中的常量引用的对象
d.本地方法栈中JNI的引用的对象
jdk8之后,常量池,静态常量池,字符串常量池都在堆中
回收器选择
串行收集器
串行收集器只适用于小数据量的情况
并行收集器
吞吐量优先的并行收集器 如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。 典型配置: java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。 -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。 java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC -XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。 java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。 java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy -XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
吞吐量优先
ParallelGC
并发收集器
如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。 典型配置: java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。 -XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。 java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。 -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
并发收集器主要是保证系统的响应时间
ParNew
ConcMarkSweep(Cms)
执行过程
标记根节点(stw)
并发标记
修正标记(stw)
主要标记的是在并行标记阶段,已经标记过的,但是后来又存活下来的对象,不会标记在用户并行阶段又产生的拉圾
会产生浮动拉圾的问题
针对的是并发标记阶段已经标记出来的对象
清除(产生碎片)
产生的问题
1. 内存碎片(原因是采用了标记-清除算法)
2. 对 CPU 资源敏感(原因是并发时和用户线程一起抢占 CPU)
3. 浮动垃圾:在并发标记阶段产生了新垃圾不会被及时回收,而是只能等到下一次GC
1. 本来可达的对象,变得不可达了
2. 本来不可达的对象,变得可达了
3. 第一种会产生浮动拉圾,而第二种,会产生错误,比如根节点标记完成之后,线程新建个对象,如果没有修正过程,那么这个new的对象也会被回收。
4. 极端情况下cms会退化层seril old回收器
当年轻代剩余空间不够,需要晋升到老年代的时候,这时候老年代还处于gc情况,并且老年代剩余空间不足,那么cms会退化层seril old回收器,并进行full gc
条件
当年轻代剩余空间不够
需要晋升到老年代的时候
这时候老年代还处于gc情况
并且老年代剩余空间不足
标记清理算法
采用三色标记算法
https://www.jianshu.com/p/12544c0ad5c1           
定义
黑
本对象已访问过,而且本对象 引用到 的其他对象 也全部访问过了。
灰
本对象已访问过,但是本对象 引用到 的其他对象 尚未全部访问完。全部访问后,会转换为黑色。
白
尚未访问过。
标记过程示例
1. 初始时,所有对象都在 【白色集合】中;
2. 将GC Roots 直接引用到的对象 挪到 【灰色集合】中;
3. 从灰色集合中获取对象:
将本对象 引用到的 其他对象 全部挪到 【灰色集合】中;
将本对象 挪到 【黑色集合】里面。
4. 重复步骤3,直至【灰色集合】为空时结束。
5. 结束后,仍在【白色集合】的对象即为GC Roots 不可达,可以进行回收。
6. 7779607-eecbd09f81b721f8.webp
问题,解决方案
问题
漏标
后果
产生错误回收
解决方式
写屏障
增量记录
多标

后果
产生浮动拉圾
解决方式
没有解决
G1
对比CMS,有哪些不同?
1 region化的内存结构,采用复制清理的方式,避免了内存碎片。但是这种清理也造成了STW。
2 SATB速度更快。
3 初始标记,并发标记,重新标记,清理垃圾四个阶段很像,但是G1中有很多标记region的操作,并借助Rset进行了范围的缩小,提高了并发标记的速度。小结下就是初始标记和YGC的STW一起了,提高了效率;并发标记因为rset的设计,扫描范围缩小了,提高了效率;重新标记因为使用了SATB提高了效率;清理虽然造成了STW,但是复制使内存紧凑,避免了内存碎片。同时只清理垃圾较多的region,最大限度的降低了STW时间。
y,old对象分配规则
优先在eden分配
大对象超过设置的阈值直接进入老年代:-XX:PretenureSizeThreshold
长期存活对象进入老年代
-XX:MaxTenuringThreshold
对象在发生young gc后,活下来的对象年龄加一,到达设置的阈值后晋升为老年代
-XX:+PrintTenuringDistribution
发生young gc后打印年龄分布
-XX:TargetSurvivorRatio
设置百分比,当survivor区达到这个阈值后,进行统计当前区存活对象的平均值,和MaxTenuringThreshod的值进行比较,取小的,达到这个小的值晋升老年代
常用参数,默认参数
-Xmx -Xms
最大最小堆内存
-XX:NewSize -XX:MaxNewSize
最大新生代内存
-XX:NewRatio
new和old比例
-XX:SurvivorRadio
eden和suivivor比例
-XX:MetaspaceSize -XX:MaxMetaspaceSize
非堆内存
-XX:+UseCompressedClassPointers
是否启用压缩类指针
-XX:CompressedClassSpaceSize
压缩类空间大小
XX:MaxTenuringThreshold=1
新生代的对象经过几次垃圾回收后(如果还存活),进入老年代。如果该参数设置为0,这表示新生代的对象在垃圾回收后,不进入survivor区,直接进入老年代
-XX:+UseParallelGC
使用并行的垃圾收集器,新老同时生效
-XX:ParallelGCThread=4
设置并行垃圾回收器的线程为4个,该设置最好与处理器的数目相同
-XX:MaxGCPauseMillis=100
设置每次新生代每次收集器垃圾回收的最长时间,如果无法满足该时间,JVM会自动调整新生代区的大小,以满足该值
当不指定-XX:SurvivorRadio参数时,Eden与Survivor默认比例为8:1
堆
最大默认1/4
最小1/64
jvm调优

大内存拆分多个逻辑jvm,组成集群,解决收集时间过长的问题
垃圾回收器的选择
吞吐量优先
parllel
响应时间优先
cms
为什么要调优
尽量避免或减少full gc的产生,full gc 会触发stop world,全jvm内存进行回收,时间长
依据
1.首先估算应用程序最大负荷情况下的对象产生(占内存大小)情况
2.调整eden,s1,s2,以及old区的大小,让尽量多的对象在major gc中回收掉,使拉圾对象尽量少的晋升老年代
找次数和大小的平衡平衡
调优原则
-XX:metaspaceSize扩容,会导致fullgc,增加初始容量,减少fullgc次数
volital
功能
线程间变量可见性
内存屏障
ll
ls
sl
ss
happend before原则
8中原则,不允许改变程序顺序
面试题
堆和栈的区别
Person p = new Person
person的引用p存储在栈中,而对象存储在堆中
堆和栈的结构不一样
堆的大小一般比栈大很多
栈中数据使用完之后自动销毁,而堆中的对象使用完后,需要等下一次gc,才可能清除
堆和栈的内存分配
栈在方法入口前确定
堆动态分配
谈谈你熟悉的垃圾回收算法
参见java脑图
常用调优参数
常见的垃圾回收器
java脑图
finalize
gc发生时,如果发现对象没有与gc root相连接的引用链,并且对象重写了finalize方法,那么就会把对象放入F-queue队列中
由虚拟机产生的一个低优先级的线程调用队列中的对象的finalize方法
虚拟机不保证finalize方法一定会执行完
作用
给予对象最后一次重生的机会
强引用,软引用,弱引用,虚引用(精华答案)
Java四种引用包括强引用,软引用,弱引用,虚引用。 Java四种引用包括强引用,软引用,弱引用,虚引用。 强引用: 只要引用存在,垃圾回收器永远不会回收 Object obj = new Object(); //可直接通过obj取得对应的对象 如obj.equels(new Object()); 而这样 obj对象对后面new Object的一个强引用,只有当obj这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。 软引用: 非必须引用,内存溢出之前进行回收,可以通过以下代码实现 Object obj = new Object(); SoftReference sf = new SoftReference(obj); obj = null; sf.get();//有时候会返回null 这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null; 软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。 弱引用: 第二次垃圾回收时回收,可以通过如下代码实现 Object obj = new Object(); WeakReference wf = new WeakReference(obj); obj = null; wf.get();//有时候会返回null wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾 弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。 弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。 虚引用: 垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现 Object obj = new Object(); PhantomReference pf = new PhantomReference(obj); obj=null; pf.get();//永远返回null pf.isEnQueued();//返回是否从内存中已经删除 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。 虚引用主要用于检测对象是否已经从内存中删除。 最近在学习Java虚拟机,碰到引用的问题,在此借鉴总结一下: 原文地址:http://blog.csdn.net/coding_or_coded/article/details/6603549 对象的强、软、弱和虚引用 在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及(reachable)状态,程序才能使用它。从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。 ⑴强引用(StrongReference) 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 ps:强引用其实也就是我们平时A a = new A()这个意思。 ⑵软引用(SoftReference) 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。 ⑶弱引用(WeakReference) 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 ⑷虚引用(PhantomReference) “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。 ReferenceQueue queue = new ReferenceQueue (); PhantomReference pr = new PhantomReference (object, queue); 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 使用软引用构建敏感数据的缓存 1 为什么需要使用软引用 首先,我们看一个雇员信息查询系统的实例。我们将使用一个Java语言实现的雇员信息查询系统查询存储在磁盘文件或者数据库中的雇员人事档案信息。作为一个用户,我们完全有可能需要回头去查看几分钟甚至几秒钟前查看过的雇员档案信息(同样,我们在浏览WEB页面的时候也经常会使用“后退”按钮)。这时我们通常会有两种程序实现方式:一种是把过去查看过的雇员信息保存在内存中,每一个存储了雇员档案信息的Java对象的生命周期贯穿整个应用程序始终;另一种是当用户开始查看其他雇员的档案信息的时候,把存储了当前所查看的雇员档案信息的Java对象结束引用,使得垃圾收集线程可以回收其所占用的内存空间,当用户再次需要浏览该雇员的档案信息的时候,重新构建该雇员的信息。很显然,第一种实现方法将造成大量的内存浪费,而第二种实现的缺陷在于即使垃圾收集线程还没有进行垃圾收集,包含雇员档案信息的对象仍然完好地保存在内存中,应用程序也要重新构建一个对象。我们知道,访问磁盘文件、访问网络资源、查询数据库等操作都是影响应用程序执行性能的重要因素,如果能重新获取那些尚未被回收的Java对象的引用,必将减少不必要的访问,大大提高程序的运行速度。 2 如果使用软引用 SoftReference的特点是它的一个实例保存对一个Java对象的软引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。另外,一旦垃圾线程回收该Java对象之后,get()方法将返回null。 看下面代码: MyObject aRef = new MyObject(); SoftReference aSoftRef=new SoftReference(aRef); 此时,对于这个MyObject对象,有两个引用路径,一个是来自SoftReference对象的软引用,一个来自变量aReference的强引用,所以这个MyObject对象是强可及对象。 随即,我们可以结束aReference对这个MyObject实例的强引用: aRef = null; 此后,这个MyObject对象成为了软可及对象。如果垃圾收集线程进行内存垃圾收集,并不会因为有一个SoftReference对该对象的引用而始终保留该对象。Java虚拟机的垃圾收集线程对软可及对象和其他一般Java对象进行了区别对待:软可及对象的清理是由垃圾收集线程根据其特定算法按照内存需求决定的。也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软可及对象,而且虚拟机会尽可能优先回收长时间闲置不用的软可及对象,对那些刚刚构建的或刚刚使用过的“新”软可反对象会被虚拟机尽可能保留。在回收这些对象之前,我们可以通过: MyObject anotherRef=(MyObject)aSoftRef.get(); 重新获得对该实例的强引用。而回收之后,调用get()方法就只能得到null了。 3 使用ReferenceQueue清除失去了软引用对象的SoftReference 作为一个Java对象,SoftReference对象除了具有保存软引用的特殊性之外,也具有Java对象的一般性。所以,当软可及对象被回收之后,虽然这个SoftReference对象的get()方法返回null,但这个SoftReference对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量SoftReference对象带来的内存泄漏。在java.lang.ref包里还提供了ReferenceQueue。如果在创建SoftReference对象的时候,使用了一个ReferenceQueue对象作为参数提供给SoftReference的构造方法,如: ReferenceQueue queue = new ReferenceQueue(); SoftReference ref=new SoftReference(aMyObject, queue); 那么当这个SoftReference所软引用的aMyOhject被垃圾收集器回收的同时,ref所强引用的SoftReference对象被列入ReferenceQueue。也就是说,ReferenceQueue中保存的对象是Reference对象,而且是已经失去了它所软引用的对象的Reference对象。另外从ReferenceQueue这个名字也可以看出,它是一个队列,当我们调用它的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。 在任何时候,我们都可以调用ReferenceQueue的poll()方法来检查是否有它所关心的非强可及对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。利用这个方法,我们可以检查哪个SoftReference所软引用的对象已经被回收。于是我们可以把这些失去所软引用的对象的SoftReference对象清除掉。常用的方式为: SoftReference ref = null; while ((ref = (EmployeeRef) q.poll()) != null) { // 清除ref }
强引用
People p = new People() new 对象都是强引用对象,如果不是过了生命周期一般不会回收,例如方法结束了,p引用在栈中被删除,只剩下实例在堆中,那么可以被回收,如果大量的new 可能会出现内存溢出异常
软引用
Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj); obj = null; sf.get();//有时候会返回null ReferenceQueue queue = new ReferenceQueue(); SoftReference ref=new SoftReference(aMyObject, queue);
当在发生outOfMemery之前,gc会回收软引用防止溢出异常,正常内存充足时不会回收引用的对象,
配合引用队列(ReferenceQueue),可以在outOfMemery之前的gc把已经回收掉实例之后的软引用加入到队列中,稍后销毁软引用,不加队列的话可能会出现大量无效的软引用
弱引用
Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj); obj = null; wf.get();//有时候会返回null wf.isEnQueued();
当gc发生时,会回收引用的对象,配合Reference队列,可以回收弱引用
虚引用
ReferenceQueue queue = new ReferenceQueue (); PhantomReference pr = new PhantomReference (object, queue);
任何时候都获取不到对应的对象
配合ReferenceQueue可以监控gc的状态。
intern

String s = new String(“ss”);
s.intern();
同学好,第一行会在堆和常量池创建"ss",而s将堆引用保存在了栈中,s.intern()没有任何作用,因为ss已经在常量池存在;
String s = "ss" 是会在常量池中创建"ss"并返回对应的引用给s,保存在栈里
String s =new String(“ss”)+new String(“ss”);
上面只会在堆中创建ssss对象,调用intern,在常量池中创建ssss引用
1.8中,intern会在堆中找是否有需要的对象,有则在常量池中创建其引用,如果堆中没有,则在常量池中直接创建
各变量在内存中的存储位置
子主题
   
线程独占与共享
intern
jdk1.8中,String str = new String(“a”);
这行执行完,则堆中存有a,而且常量池中也有了,并且常量池中的不是引用地址,是这么理解吧?
而String str2 = new String(“a”)+new String(“a”);
这个执行完,只在堆中存有aa,常量池中没有aa,是咩?
同学好,是的,针对String str2 = new String(“a”)+new String(“a”); 常量池中只有a没有aa
new 对象时会产生堆冲突吗 会
使用tlab解决
线程独有的对空间,向堆申请的连续线程独占的空间
并发
理论知识
CAS算法
属于乐观锁
思想
不停的循环来进行重试,看实现中的for循环条件
实现
缺点
atomicStampedReference多增加了一个时间戳字段,时间戳和原始值联合判断是否应该更新
volitaile
happends-before
线程池
导致线程安全的原因
进程和线程的关系
一个进程可以包含多个线程
每一个java进程对应一个jvm实例,使用单独的堆,并且栈是线程独享的
如何中断线程
正常活动的线程,通过判断中断标志来自行处理停止的处理过程
sleep() &interrupt()
a线程sleep,b线程调用a的interrupt,则会直接停止sleep,并且抱interruptException,进入catch块
wait() &interrupt()
锁a进入wait状态,b线程执行a的interrupt,会立即唤醒wait的锁,尝试获取锁,当成功获取锁之后,会报interruptException,进入catch快
join() &interrupt()
在获取锁定之前,是无法抛出异常的。
汇总答案
1. sleep() &interrupt() 线程A正在使用sleep()暂停着: Thread.sleep(100000),如果要取消它的等待状态,可以在正在执行的线程里(比如这里是B)调用a.interrupt()[a是线程A对应到的Thread实例],令线程A放弃睡眠操作。即,在线程B中执行a.interrupt(),处于阻塞中的线程a将放弃睡眠操作。 当在sleep中时线程被调用interrupt()时,就马上会放弃暂停的状态并抛出InterruptedException。抛出异常的,是A线程。 2. wait() &interrupt() 线程A调用了wait()进入了等待状态,也可以用interrupt()取消。不过这时候要注意锁定的问题。线程在进入等待区,会把锁定解除,当对等待中的线程调用interrupt()时,会先重新获取锁定,再抛出异常。在获取锁定之前,是无法抛出异常的。 3. join() &interrupt() 当线程以join()等待其他线程结束时,当它被调用interrupt(),它与sleep()时一样,会马上跳到catch块里.。 注意,调用的interrupt()方法,一定是调用被阻塞线程的interrupt方法。如在线程a中调用线程t.interrupt()。
线程状态以及状态之间的转换
锁
面试题
synchronized和ReentrantLock的区别
公平锁实现消耗性能,降低吞吐量
syn是关键字,reentrantlock是类,有更多的方法可以精细操控锁
synchronized
理论知识
属于悲观锁
既保证了可见性,也保证了互斥性
锁定对象分类
实现原理
锁的实现主要由对象头中的Monitor实现
java中每个对象出生时都带有一把看不见的锁Monitor
Monitor由ObjectMonitor实现,jvm源码中的c文件编写:
如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态
ObjectMonitor
entryList锁池
处于等待锁block状态的线程,会被加入到该列表
waitset 等待池
处于wait状态的线程,会被加入到_WaitSet
每个对象的线程都会被封装成objectWaiter保存到
一句话回答
每个对象都有一个与之关联的Monitor,hot spot虚拟机中的实现是ObjectMonitor,当线程获取到了Monitor之后,会将owner设置成当前线程,并且count+1,如果调用wait方法,会进入waitSet(等待池)等待唤醒,owner变为null,count-1。如果当前线程执行完毕,也将释放monitor,并复原monitor
同步代码块的实现
synchronized字节码中,主要由monitorenter和monitorexit实现,monitorrenter可以重入(在同步快中,可以使用已经获取的锁,sync(this){sync(this)}),并且编译器会自动生成一个异常处理,发生异常时会调用monitorexit退出同步(图中有两个monitorexit),并且释放monitor,操作上面文本框中的内容
同步方法的实现
通过方法的acc_synchronized标记,有的话就先尝试获取Monitor,之后的逻辑和上上的文本框一样
使用javap -verbose查看源码
synchronized优化
自旋锁
当有线程需要获取锁,此时不放弃cpu,而是选择等待一会不断重试,这个过程叫做自旋
缺点
若其他线程锁占用时间过长,不断自旋等待,则会带来性能开销
优化
preblockbin
设置重试次数
当重试一定次数后,进行锁升级
自适应自旋锁
自旋次数不固定
由前一次在同一个锁上的自旋时间和次数决定
锁消除
jvm编译时,对上下文进行扫描,去除不可能存在的竞争锁
锁粗化
偏向锁
一个线程获取锁之后,锁进入偏向锁状态,当该线程再次请求锁的时候,不用做同步操作,此时使用cas来代替加锁。
轻量级锁
上面的自旋锁
总结
锁升级
         
无锁
偏向锁
利用threadId来实现锁
轻量级锁
利用cas技术实现锁
重量级锁
使用真正的syn来处理多线程
volitale和synchronized的区别
什么是java内存模型中的happends before原则
什么是java内存模型
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节
分为主内存和工作内存,描述内存之间共享变量的操作规则
与valitale和synch有关
由于方法内的变量引用与基本类型变量是存在于栈中的,所以是线程私有的
什么是CAS
处理并发的一种手段
取出内存位置v,预期值a,新值b,存入的时候比较a和v的值,一致则更新v的值,不一致重试,juc中的atomic包使用的就是cas
缺点
大量的重试导致开销大
AQS的实现原理
AbustactQueuedSynchronizer抽象队列同步器,框架
采用一个原子性的state作为锁标记,并结合cas技术操作同步状态
组成
cas
原子性的state
同步队列clh
双端队列,主要存储阻塞的线程,如果一个线程尝试获取一个锁,那么这个线程就会加入到队列的尾部
定义
AQS是指AbstractQueuedSynchronizer。它是一个笼统类,java并发包里的ReentrantLock、CountDownLatch和Semaphroe等重要的工具类都是基于AQS来实现的。
简短描述
AQS其实就是一个可以给我们实现锁的框架
内部实现的关键是:先进先出的队列、state状态
定义了内部类ConditionObject
拥有两种线程模式独占模式和共享模式。
独占
只有一个线程能执行,如ReentrantLock
共享
共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建,一般我们叫AQS为同步器。
非公平锁的实现原理
在acquire,和nonfairTryAcquire方法中多了两次不排队,直接cas获取锁的操作,如果没有获得的话,后面就和公平锁一样了
非公平锁和公平锁的两处不同:
非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
链接:https://www.jianshu.com/p/2ada27eee90b
lock锁重入逻辑
利用state和threadId来判断
基础知识
锁住同一个对象实例才会产生互斥结果
锁住同一个类才会产生互斥效果
互斥组合1
synchronized(this也就是当前对象的实例)
this指的当前对象,Person p = new Person ,p中有调用synchronized(this),这个this,指的就是p
synchronized(对象实例)
Object lock=newObject(),调用synchronized(lock)
synchronized(一般方法)
如果上面三个对应的同一个对象实例,则互斥
互斥组合2
synchronized(类)
synchronized(Person.class)
synchronized(静态方法)
person对象中,调用synchronized static 方法
组合1和2不互斥,类锁和对象锁互不干扰
共享数据的同步性
在同步代码块结束后,其他获取锁的代码块看到的数据一定是最新的,和volitale效果一样
线程池
面试题
自定义线程池
当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
任务等待队列的选择
线程池的线程数大小如何选定

cpu密集型
cpu数量或者+1
i/o密集型
((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
线程io时间越长(此时不耗费cpu),应该建立的线程数就应该越多,为了充分利用cpu
常用api,及区别
start和run的区别
start
调用native方法创建一个线程执行run方法
run
直接在主线程中执行run方法
runnable和Thread的区别
runnable是接口
因单一继承原则,推荐使用runnable接口
Thread实现了runnable接口,使得run支持多线程
如何实现线程处理的返回值
使用futureTask配合CallAble
使用线程池配合callable
sleep和wait的区别
最终两者都是调用native方法
Thread.sleep仅会让出cpu,不会改变锁的状态,可以在任何地方使用
Object.wait会让出cpu,也会改变锁的状态,而且只能在同步代码块中使用
一般锁.wait
Object lock = new Object();
lock.wait
等待
lock.notify
唤醒
lock.notifyall
唤醒所有
notify和notify all的区别
先清楚两个概念
锁池(entry list)
等待池(wait set)
这两个池和Object下的wait,notify,notifyall,以及synchcronized有关
yield
提示让出cpu,不会改变锁的状态,仅仅提示,不是一定
ThreadLocal的底层原理,以及内存溢出原因
原理
当调用set方法时,会初始化一个线程私有的ThreadLocalMap,数组+entry的模式实现map,其中entry是弱引用,来存储数据,key是(threadLocal对象引用,value是值)
当调用set,get的时候会清理掉key为null的数据,防止内存泄漏,但如果一直不调用,那么也会产生内存泄漏
内存溢出
子主题
由于entry(继承了弱引用的类型)的构造方法,初始化的时候将key作为引用,初始化了一个弱引用对象,所以当线程中的threadLocal对象=null的时候,引用消失,但是key还和threadLocal保持引用关系,所以在gc的时候会清理掉弱引用对象,最后成为了null,value的形式,直到下次调用set,get的时候会清楚null,value这个entry数据
threadLocal是一个new出来的对象
threadLocals是一个静态对象
Thread回归线程池之后,一直得不到使用,null,value,一直得不到清除,就会产生内存泄漏,多了就会产生oom
为什么entry是个弱引用类型
弱引用对象没有被强引用引用后,会被gc回收,形成null,value的形式
为了可以让threadLocal调用set,get方法时可以将不用的对象置为空,从而一定程度上避免oom
null,也是key,也可以被引用,null,value,这样的话,即使key弱引用被回收,value还是处于强引用的状态无法回收
引用链
Thread(threadLocalMaps)->ThreadLocal->ThreadLocalMap->entry(key),value
j.u.c
概览
tools
countDownLatch

cyclicBarrier

semaphore

exchange

阻塞队列
blockingQueue接口定义了放入、取出、初始化等行为,子类根据实现方式不同以及存取规则不同,一共分为7种(可阻塞的出队、入队操作)
参考java你脑图
Array基于数组,linked基于链表,都是先进先出
Linked设置了大小就有限,不设置就无限大
io/nio
模型
阻塞io

非阻塞io

多路复用io模型

信号驱动io模型

异步io模型

面试题
谈谈你对Java的理解
java分为如下几部
平台无关性
编译成class文件,之后由不同平台上的jvm翻译成当前机器可以识别的机器代码
GC
面向对象
类库
异常处理
jvm架构
jvm如何加载class文件
class loader只负责加载class文件
execution engine(执行引擎)负责按照规则解析,解析完之后提交到系统中执行
native interface解释
c++编写的一些代码
由于java没有c++效率高,有时需要加载c++的代码进行执行,class.forname就是native的方法
谈谈反射,如何调用

动态获取类的实例,并且可以调用所有方法,包括私有函数,利用class.forname(路径)
谈谈class loader
所有class文件都是由class loader加载进入系统,然后交给虚拟机执行初始化等操作
class loader种类不止一种
bootsrap class loader
加载系统核心jar包,java.*
c++编写
JRE/lib/rt.jar
ext class loader
加载拓展库javax.*
根据属性,java.ext.dirs的路径加载
java编写
JRE/lib/ext或者java.ext.dirs指向的目录
app class loader
加载属性,java.class.path路径下的文件
java编写
自定义class loader
继承classLoader,重写findclass方法,进行读取class文件,之后调用definClass。
java编写
为什么向runtime data area(运行时区)叫做java内存模型
jvm完全运行在内存中,模型的含义是固定的组成部分,那么模型上的这些部分就是对应堆、栈、方法区等,将这些抽象的部分称为内存模型
双亲委派模型
首先,先要知道什么是类加载器。简单说,类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。如果站在JVM的角度来看,只存在两种类加载器: 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。 其他类加载器:由Java语言实现,继承自抽象类ClassLoader。如: 扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。如此一层一层的查找,知道能加载为止
Java类伴随其类加载器具备了带有优先级的层次关系,确保了在各种加载环境的加载顺序。
保证了运行的安全性,防止不可信类扮演可信任的类。
双亲委派模型有效解决了以下问题:
每一个类都只会被加载一次,避免了重复加载
当调用类的方法时,先从父类的classloader中查找是否有,没有继续向上找,找到了就使用
有效避免了某些恶意类的加载(比如自定义了Java。lang.Object类,一般而言在双亲委派模型下会加载系统的Object类而不是自定义的Object类)
启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
每一个类都会被尽可能的加载(从引导类加载器往下,每个加载器都可能会根据优先次序尝试加载它)
类加载的方式
隐式
new
显式
  
loadClass
延迟加载,只把class文件读进来,没有进行连接,执行等操作。
spring大量使用此种方法延迟加载bean
class.forname
立即加载,表现为static静态块的执行
三大调优参数
如果拓展时无法申请到足够的内存,就会抛出OutOfMemoryError
-Xss 线程栈的大小,影响栈的深度和并发数,一般256K,当线程数量非常多的时候可能会出现outofMemoryError,原因是线程过多,每个线程分配256,总量大于
-xms -xmx,初始和最大堆(dui)的值一般设置成固定的值,以防内存抖动
jvm Metaspace等模型
参考脑图 java
io种类
       
jdk 1.4 bio(阻塞式io)
jdk1.4之后 nio(同步非阻塞式io)
同步是指java api调用时还是同步等待,非阻塞指的是系统内核读取io数据时,是采用的非阻塞形式
jdk 1.7 aio以及异步io模型
java api提供回调函数,不用等待请求,系统内核读取完数据后回调通知java结果
多路io复用指的是系统select函数,次函数封装了读取多个socket的操作,不断轮询各个socket,监控结果,这个模型称为多路复用
bio ,nio,aio的区别
Block-Io
InputStream
outputStream
Reader
writer
NonBlock-io
string,stringbuilder,string buffer之间的区别
String
每次给对象赋值都是新创建一个对象
String builder
可修改,都是用的同一个对象
线程不安全,速度快
buffer
error和exception
NotClassDefFoundError与ClassNotFoundException的区别
ClassNotFoundException
这类异常出现在对类进行加载时,该路径下找不到对应的class文件。有以下情况:
1.通过Class.forName()加载类。
2.通过类加载器ClassLoader加载loadClass(),或者findSystemClass()。
NotClassDefFoundError
exception的性能
java集合框架
分布式事务解决方案
两阶段提交
  
三阶段提交

三阶段和两阶段区别
对于协调者(Coordinator)和参与者(Cohort)都设置了超时机制(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到cohort的消息则默认失败)。在2PC的准备阶段和提交阶段之间,插入预提交阶段,使3PC拥有CanCommit、PreCommit、DoCommit三个阶段。PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的。
https://blog.csdn.net/kangkanglou/article/details/79743655
三阶段解决了两阶段不能解决的什么问题
  
二阶段只有协调者有超时机制,执行者没有,如果协调者挂了,执行者一直阻塞事务,三阶段两端都加入了超时机制,解决这个问题,超时后默认是提交,
三阶段分开,减少了阻塞时间,提高了并发量
TCC
            
解释
T
Try
尝试
C
Confirm
确认
C
Cancel
取消
思想
分三个阶段执行
Try
try,尝试将数据改为中间状态
Confirm
将数据的中间状态改为最终状态
Cancel
处理confirm失败后的回滚逻辑
产生的问题
悬挂

定义
当cencel操作在try操作执行完成执行,则try阶段后,会使资源一直处于try阶段,这种情况叫做悬挂
解决方案
事务增加执行状态
如果cancel执行过,则不允许执行try阶段
空回滚

try阶段可能因为各种原因没有执行
定义
try阶段因为网络原因或者故障,没有执行成功
故障或者网络恢复后,此时tm需要执行cancel操作,但实际上此时没有执行try操作,实际不需要执行cancle操作的内容,直接返回true就好
幂等

最终一致性
基于可靠消息队列的最终一致性
      
RocketMq,支持事务
2阶段事务性消息队列
发送消息到mq,mq将消息保存,不发送
回传ack标志,等待业务放发送业务处理成功信息,mq再真实发送消息到消费者
half
半消息,需要发送方确认提交或者回滚的消息
new String 和=初始化字符串区别
new String是在堆中创建
=是在常量池中创建
重写equals一定要重写hashCode吗
如果两个对象重写equals后判断相等了,并且不重写hashCode,那么放入hashSet的时候,首先会根据hash值确定位置,那么就可能出现hash计算出来不同,放入了不同位置,实际上在hashSet中是同一个元素,最终set中出现两个相同的person对象的情况
所以一定要求equals相同的情况下hashCode也要相同。
Integer、Long 缓存了-128-127之间的数字
范围内==比较是相等的
Integer i=100
Integer j=100
范围外都是new出来的,不想等
Integer i = 200
Integer j =200
string.intern()的存在意义
将new String("abc")的内容放入字符串常量池(在堆中),并返回其引用
以时间换空间,使用Intern后,效率会降低
如果不用intern,大量执行重复的new String(),会产生大量相同对象,导致内存占用过多,从而导致gc,而gc时间一般都很长。
new 一个对象的过程是什么
1:申请一个空间,成员变量为默认值
2:调用构造方法时初始化成员变量
3:引用和对象建立关联
dcl(双重检验单例) 需要配合volital关键字使用吗
需要
2.3步可能发生指令重排序,可能另一个线程可能拿着半初始化(使用默认值)的对象使用
ThreadLocal原理
功能
线程隔离存储变量
网络基础
tcp(传输层)
三次握手
恶意攻击
client下线攻击(syn flood)
第二次握手后,client端下线,server端(linux)会进行重试,一共5次,初始1秒,每次翻倍,最终花费63秒重试,会导致占满了连接数
解决方案
参数:源地址端口-目标地址端口-时间戳
为什么会有三次握手
同步client和server端的sequense number,图中的x、y,来保证数据的顺序性,保证不会乱序
基于全双工,并且请求-应答模型,请求,回复,收到
建立连接后,client出现故障怎么办
四次挥手
如果被动端没有收到ack,会触发重发fin,一来一回正好是2msl
为什么有四次挥手
服务器出现大量close_wait状态的连接的原因
使用netstate分析
报文头
滑动窗口
滑动窗口的长度由发送未确认+未发送长度来确定,确认后窗口进行移动
在TCP层,有个FLAGS字段
SYN表示建立连接,
FIN表示关闭连接,
ACK表示响应,
PSH表示有 DATA数据传输,
RST表示连接重置。
其中,ACK是可能与SYN,FIN等同时使用的,比如SYN和ACK可能同时为1,它表示的就是建立连接之后的响应,
其中,ACK是可能与SYN,FIN等同时使用的,比如SYN和ACK可能同时为1,它表示的就是建立连接之后的响应,
如果只是单个的一个SYN,它表示的只是建立连接。
TCP的几次握手就是通过这样的ACK表现出来的。
但SYN与FIN是不会同时为1的,因为前者表示的是建立连接,而后者表示的是断开连接。
udp
简介
tcp(有拆包)与udp(没有拆包)区别
面向连接 vs 无连接
tcp有三次连接的握手功能,面向连接的
udp无连接
可靠性
tcp
三次握手,确认和重传(ack,fin以及seq)保证消息的完整性和可达性
udp
不可靠,可能会丢失
有序性
tcp
利用seq,将传来的可能无序的数据重新排序
udp
无序
速度
tpc
需要保证可靠性,有序性,慢
udp
快
量级
tcp
头20字节,量级大
udp
头8字节,量级小
http(应用层)
cookies与session
cookies
服务器响应头中,响应set-cookies字段,设置cookies
客户端访问服务器的时候报文头中带有cookies字段,来传递cookies信息
session
实现方式
第一次http请求服务器时,服务器会在响应报文中的头部的set-cookies中和url中添加jsessionId字段,来存储sessionId
如果客户端支持cookies,那么之后的请求都是用cookies方式来保存jsessionId,如不,则采用url参数方式添加jsessionId方式
第二次访问服务器时的时候会在报文头的cookes中或者url中带上jessionId信息
状态码
数据结构
请求
请求方法:get、post等
协议版本:1.1、2.0、1.0
头部信息
content-type
content-language
响应
响应头
http与tcp关系
一个http连接,先建立传输层tpc连接,再进行应用层http。
面试题
http请求响应步骤
客户端和服务器建立tcp连接
客户端发送http请求
服务器响应http请求
释放tcp连接
客户端解析收到的html代码
在浏览器输入url后,按下回车后都经历了什么
1、dns解析出域名ip
浏览器会依照(浏览器>系统>路由器缓存>ips服务器缓存>根域名服务器缓存>顶级域名服务器缓存),查到了,就不再查询后面的缓存
2、根据ip:端口建立tcp连接(可以加上三次握手进行讲解)
客户端发送http请求
服务器响应处理http请求,并返回http报文
浏览器解析渲染页面
连接结束(结合4次挥手)
常见的http代码
说说cookies与session
cookies存在于客户端中
客户端访问服务器时
服务器会在返回报文头中加入set-cookies,设置jsessionId,存储服务器端session的id
get和post区别
http与https区别
临时
静态请求
在不同时间不同地点观看同一部电影的内容,一般是相同的。
动态请求
在不同时间不同地点查询天气的内容,一般是不同的。
在不同时间、不同地点发起的多次动态请求,返回的结果一般是不同的
高并发
读写分离
分库分表
数学
进制和符号数
java常用进制
2
8
16
任意进制转换10进制
正规计算方式
快速计算方式
8421码
有符号数
源码
反码
补码
位运算
种类与计算规则
按位与&,按位或|计算
按位异或^,按位取反~
左移<<,右移>>,无符号右移>>>
左移
右移
无符号右移
右移时,不考虑符号位(32位的第一位,0正数,1负数),都补零
程序设计原则
面向对象设计7原则
开闭原则
里氏代换原则
依赖倒置
单一性原则
设计模式
意义
优点
体系结构
分类
根据目的分类
创建型
工厂模式
作用
负责选择一个所需的类
创建某个类
设计方式
优点
缺点
结构型
行为型
根据范围分类
衡量设计模式好坏的方式
当需求发生改变时,模式是如何响应变化的
什么是模式
在特定环境中解决问题的一种方案
Floating Topic
Floating Topic
可能并发标记出垃圾了,但是后来又被引用了,所以会重新标记
名词解释
qps
query per second ,没秒内查询次数
文件描述符(fd)
file descriptor
在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号码,因此,在网络通信过程中稍不注意就有可能造成串话。