冒泡
选择 选最小的放前面
插入 (插扑克牌)
归并 部分让其有序 再通过外排的方式 合并 O(NlogN)
工程中的综合排序
样本量小时 用复杂度高的算法比如插入排序(O(N2)) 因为常数项低
基础类型 用快排 (稳定性考虑)
自定义的类型 用归并 (稳定性考虑)
1.声明接口:注册需要被监听行为名称
2.接口实现类: 扮演被监控的类,负责被监听方法实现细节
3.InvocationHanler接口实现类:
1.次要业务/增强业务
2.将次要业务与被监听方法绑定执行
4.代理监控对象:
被监控类内存地址,被监控类实现的接口,
InvocationHandler实现类的实例对象
Spring AOP:简化代理模式实现步骤
1.声明接口:注册需要被监听行为名称
2.接口实现类: 扮演被监控的类,负责被监听方法实现细节
3.次要业务/增强业务
Spring AOP 通知种类:设置次要业务与(被监听方法)绑定执行顺序问题
1.前置通知:
1)切面:次要业务方法
2) 执行切入点:被拦截的主要业务方法
2.后置通知:
1)执行切入点:被拦截的主要业务方法
2)切面:次要业务方法
3.环绕通知:
1)切面1:次要业务方法
2) 执行切入点:被拦截的主要业务方法
3)切面2:次要业务方法
4.异常通知:
try{
执行切入点:被拦截的主要业务方法
}catch(Exception ){
切面
}
Spring AOP Advice接口:只能对当前接口下所有的实现类进行次要业务绑定执行,无法动态指定
Spring AOP Advisor:(顾问)
1.一种织入方式
2.实际上Adivce封装版。
3.可以动态的将切面指定对应切入点
@Resource默认按照名称方式(by name)进行bean匹配,(这个注解属于J2EE的),默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行安装名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。
@Resource装配顺序
1. 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常
2. 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常
3. 如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常
4. 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为按类型进行匹配,如果匹配则自动装配;
@Autowired默认按照类型方式(by type)进行bean匹配 按类型装配(这个注解是属于spring的),默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用。
@Autowired 的实现:先调用AutowiredAnnotationBeanPostProcessor的determineCandidateConstructors方法
总结:
@Autowired//默认按type注入,只能按type注入
@Qualifier//一般作为@Autowired()的修饰用,用来指定注入bean的名称,来实现按名称注入的功能
@Resource//默认按name注入,可以通过name和type属性进行选择性注入
动态代理可以分为两种:JDK动态代理和CGLIB动态代理。当目标类有接口的时候才会使用JDK动态代理,其实是因为JDK动态代理无法代理一个没有接口的类。JDK动态代理是利用反射机制生成一个实现代理接口的匿名类;而CGLIB是针对类实现代理,主要是对指定的类生成一个子类,并且覆盖其中的方法。
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
// 1.config.isOptimize()是否使用优化的代理策略,目前使用与CGLIB
// config.isProxyTargetClass() 是否目标类本身被代理而不是目标类的接口
// hasNoUserSuppliedProxyInterfaces()是否存在代理接口
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
// 2.如果目标类是接口或者是代理类,则直接使用JDKproxy
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 3.其他情况则使用CGLIBproxy
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
@EnableAsync使异步调用@Async注解生效,未设置TaskExecutor时,默认是使用SimpleAsyncTaskExecutor这个线程池,但此线程不是真正意义上的线程池,因为线程不重用,每次调用都会创建一个新的线程。可通过控制台日志输出可以看出,每次输出线程名都是递增的。所以最好我们来自定义一个线程池。
调用的异步方法,不能为同一个类的方法(包括同一个类的内部类),简单来说,因为Spring在启动扫描时会为其创建一个代理类,而同类调用时,还是调用本身的代理类的,所以和平常调用是一样的。
其他的注解如@Cache等也是一样的道理,说白了,就是Spring的代理机制造成的。所以在开发中,最好把异步服务单独抽出一个类来管理。
示例:
什么情况下会导致@Async异步方法会失效?
在spring中像@Async和@Transactional、cache等注解本质使用的是动态代理,其实Spring容器在初始化的时候Spring容器会将含有AOP注解的类对象“替换”为代理对象(简单这么理解),那么注解失效的原因就很明显了,就是因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器,那么解决方法也会沿着这个思路来解决。
解决:
将要异步执行的方法单独抽取成一个类,原理就是当你把执行异步的方法单独抽取成一个类的时候,这个类肯定是被Spring管理的,其他Spring组件需要调用的时候肯定会注入进去,这时候实际上注入进去的就是代理类了。
其实我们的注入对象都是从Spring容器中给当前Spring组件进行成员变量的赋值,由于某些类使用了AOP注解,那么实际上在Spring容器中实际存在的是它的代理对象。那么我们就可以通过上下文获取自己的代理对象调用异步方法。
//同一个类中 这样调用同类下的异步方法是不起作用的
//this.testAsyncTask();
//通过上下文获取自己的代理对象调用异步方法 可以生效
EmailController emailController = (EmailController)applicationContext.getBean(EmailController.class);
emailController.testAsyncTask();
调用的是静态(static )方法
调用(private)私有化方法
同理:在同一个类中,一个方法调用另外一个有注解(比如@Async,@Transational)的方法,注解是不会生效的,@Transational是声明式的事务,是生成代理类对其做了事务的增强
【解释:调用的方法不带注解,因此代理类不开事务,而是直接调用目标对象的方法。当进入目标对象的方法后,执行的上下文已经变成目标对象本身了,因为目标对象的代码是我们自己写的,和事务没有半毛钱关系,此时你再调用带注解的方法,照样没有事务,只是一个普通的方法调用而已。】
待总结:https://segmentfault.com/a/1190000013341344
三级缓存解决了Bean之间的循环依赖(set注入的依赖也是属性的循环依赖)
Spring中循环依赖场景有:
解决:(属性的循环依赖)
singletonObjects:第一级缓存,里面放置的是实例化好的单例对象; 缓存单例Bean
earlySingletonObjects:第二级缓存,里面存放的是提前曝光的单例对象;缓存正在创建,还未创建完成的单例Bean
singletonFactories:第三级缓存,里面存放的是要被实例化的对象的对象工厂;缓存单例bean的工厂
创建bean的时候Spring首先从一级缓存singletonObjects中获取。如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取,如果还是获取不到就从三级缓存singletonFactories中取(Bean调用构造函数进行实例化后,即使属性还未填充,就可以通过三级缓存向外提前暴露依赖的引用值(提前曝光),根据对象引用能定位到堆中的对象,其原理是基于Java的引用传递),取到后从三级缓存移动到了二级缓存完全初始化之后将自己放入到一级缓存中供其他使用,
因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决。
构造类型的依赖的另外解法:(@lazy)
在构造函数中使用@Lazy注解延迟加载。在注入依赖时,先注入代理对象,当首次使用时再创建对象说明:
一种互斥的关系而非层次递进的关系,故称为三个Map而非三级缓存的缘由 完成注入
Spring构造器注入循环依赖的解决方案是@Lazy,其基本思路是:对于强依赖的对象,一开始并不注入对象本身,而是注入其代理对象,以便顺利完成实例的构造,形成一个完成的对象,这样与其它应用层对象就不会形成互相依赖的关系;当需要调用真实对象的方法时,通过TargetSouce去拿到真实的对象[DefaultListableBeanFactory#doResolveDependency],然后通过反射完成调用
@SpringBootApplication注解是Spring Boot的核心注解:
@SpringBootApplication =
(默认属性)
@Configuration (里面可以写@Bean) ----------------》等价于XML文件,@Configuration的注解类标识这个类可以使用Spring IoC容器作为bean定义的来源。 @Bean注解告诉Spring,一个带有@Bean的注解方法将返回一个对象,该对象应该被注册为在Spring应用程序上下文中的bean。
@ComponentScan ----------------》@ComponentScan这个注解在Spring中很重要,它对应XML配置中的元素,@ComponentScan的功能其实就是自动扫描并加载符合条件的组件(比如@Component和@Repository等)或者bean定义,
最终将这些bean定义加载到IoC容器中。我们可以通过basePackages等属性来细粒度的定制@ComponentScan自动扫描的范围,如果不指定,则默认Spring框架实现会从声明@ComponentScan所在类的package进行扫描。
@EnableAutoConfiguration(最重要) ----------------》借助@Import的支持,收集和注册特定场景相关的bean定义(将所有符合自动配置条件的bean定义加载到IoC容器,仅此而已) SpringFactoriesLoader(读取META-INF/spring.factories)
@EnableAutoConfiguration自动配置的魔法骑士就变成了:从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射(Java Refletion)实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总为一个并加载到IoC容器。
回顾整体流程,Springboot的启动,主要创建了配置环境(environment)、事件监听(listeners)、应用上下文(applicationContext),并基于以上条件,在容器中开始实例化我们需要的Bean,至此,通过SpringBoot启动的程序已经构造完成
一个对象分为3个区域:
对象头:主要是包括两部分,
<1> markwod 存储自身的运行时数据比如hash码,分代年龄(4个bit位置 所以默认15次垃圾回收之后,新生代会进入老年代),锁标记等(但是不是绝对哦,锁状态如果是偏向锁,轻量级锁,是没有hash码的.是不固定的)
<2> klasspoint 指向类的元数据指针。还有可能存在第三部分,那就是数组类型,会多一块记录数组的长度(因为数组的长度是jvm判断不出来的,jvm只有元数据信息)
实例数据:会根据虚拟机分配策略来定,分配策略中,会把相同大小的类型放在一起,并按照定义顺序排列(父类的变量也会在哦)
对齐填充:这个意义不是很大,主要在虚拟机规范中对象必须是8字节的整数,所以当对象不满足这个情况时,就会用占位符填充,所以new一个空对象是8个字节。
引用计数(可能有循环引用的问题)
可达性分析 在java中主要是可达性分析,从GC ROOTS出发,去找是否能到达对象的链路。
GC ROOTS可以的对象有:
虚拟机栈(局部变量表)中引用的对象
方法区中常量引用的对象
方法区中类静态属性引用的对象
本地方法栈中JNI引用的对象
(1):强(内存泄露主因)
(2):软(只有软引用的话,空间不足将被回收),适合缓存用
(3):弱(只,GC会回收)
(4):虚引用(用于跟踪GC状态)用于管理堆外内存
逃逸分析:(只在当前方法内使用的对象不逃逸,在栈上分配,及时清理,减少GC,发生逃逸的对象在堆上申请内存)
Java中的对象都是在堆中分配吗?
逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。
逃逸分析的 JVM 参数如下:
开启逃逸分析:-XX:+DoEscapeAnalysis
关闭逃逸分析:-XX:-DoEscapeAnalysis
显示分析结果:-XX:+PrintEscapeAnalysis
对象逃逸状态
我们了解了 Java 中的逃逸分析技术,再来了解下一个对象的逃逸状态。
1、全局逃逸(GlobalEscape)
即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:
对象是一个静态变量
对象是一个已经发生逃逸的对象
对象作为当前方法的返回值
2、参数逃逸(ArgEscape)
即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。
3、没有逃逸
即方法中的对象没有发生逃逸。
句柄:使用句柄方式访问兑对象时,需要在堆中划出一部分内存作为句柄池,句柄池中存放各个对象的句柄,句柄包含了队形实例数据和对象类型数据的具体地址信息,而reference中存储的则 是对象的句柄地址。reference:存的是稳定的句柄指针地址,对象发生移动时只会改变句柄指向实例数据的指针。
直接指针:使用直接方式访问对象时,堆中对象存放的是对象的实例数据和指向对象类型数据的指针,reference则存储的是对象地址。
注:一次指针定位速度快
Integer缓存的范围是-128~127
可以通过设置:
XX:AutoBoxCacheMax=或 -Djava.lang.Integer.IntegerCache.high=
导致fullGC的原因
(1):老年代空间不足
(2):永久代(方法区)空间不足
(3):显式调用system.gc()
finalize方法:
finalize()是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它finalize()方法,让此对象处理它生前的最后事情(这个对象可以趁这个时机挣脱死亡的命运)。要明白这个问题,先看一下虚拟机是如何判断一个对象该死的。(上文的对象存活判断)
最后的救赎 上面提到了判断死亡的依据,但被判断死亡后,还有生还的机会。 如何自我救赎:
1.对象覆写了finalize()方法(这样在被判死后才会调用此方法,才有机会做最后的救赎);
2.在finalize()方法中重新引用到"GC Roots"链上(如把当前对象的引用this赋值给某对象的类变量/成员变量,重新建立可达的引用).
需要注意:
finalize()只会在对象内存回收前被调用一次(The finalize method is never invoked more
than once by a Java virtual machine for any given object. );
finalize()的调用具有不确定行,只保证方法会调用,但不保证方法里的任务会被执行完(比如一个对象手脚不够利索,磨磨叽叽,还在自救的过程中,被杀死回收了)。finalize()的作用 虽然以上以对象救赎举例,但finalize()的作用往往被认为是用来做最后的资源回收。
基于在自我救赎中的表现来看,此方法有很大的不确定性(不保证方法中的任务执行完)而且运行代价较高。所以用来回收资源也不会有什么好的表现。综上:finalize()方法并没有什么鸟用。
至于为什么会存在这样一个鸡肋的方法:书中说“它不是C/C++中的析构函数,而是Java刚诞生时为了使C/C++程序员更容易接受它所做出的一个妥协”。
Minor GC ,Full GC 触发条件??
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法去空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
CMS:Concurrent Mark Sweep
• 低延时的系统
• 不进⾏Compact
• ⽤于⽼年代
• 配合Serial/ParNew使⽤
初始标记-》并发标记-》重新标记-》并发清理
CMS收集器之所以能够做到并发,根本原因在于采用基于“标记-清除”的算法并对算法过程进行了细粒度的分解。前面篇章介绍过标记-清除算法将产生大量的内存碎片这对新生代来说是难以接受的,因此新生代的收集器并未提供CMS版本
G1:软实时、低延时、可设定⽬标
• JDK9+的默认GC(JEP248)
• 适⽤于较⼤的堆(>4~6G)
• ⽤于替代CMS
• ⽆需回收整个堆,⽽是选择⼀个Collection Set (CS)
• 两种GC:
• Fully young GC
• Mixed GC
• 估计每个Region中的垃圾⽐例,优先回收垃圾多的Region
G1的几个概念:
几个概念:
分区 Region: 将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存 -XX:G1HeapRegionSize=n 默认将整堆划分为2048个分区
卡片 Card:
在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RemeberSet)。每次对内存的回收,都是对指定分区的卡片进行处理。
已记忆集合 Remember Set (RSet)
在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。然而G1为了避免STW式的整堆扫描,在每个分区记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区内对象的卡片索引。当要回收该分区时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。
事实上,并非所有的引用都需要记录在RSet中,如果一个分区确定需要扫描,那么无
需RSet也可以无遗漏的得到引用关系。那么引用源自本分区的对象,当然不用落入
RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对
象,也不需要在RSet中记录。最后只有老年代的分区可能会有RSet记录,这些分区
称为拥有RSet分区(an RSet’s owning region)。
收集集合 CollectionSet
收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。
候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。
年轻代收集集合 CSet of Young Collection
应用线程不断活动后,年轻代空间会被逐渐填满。当JVM分配对象到Eden区域失败(Eden区已满)时,便会触发一次STW式的年轻代收集。在年轻代收集中,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据任期阈值(tenuring threshold)分别晋升到PLAB中,新的survivor分区和老年代分区。而原有的年轻代分区将被整体回收掉。
同时,年轻代收集还负责维护对象的年龄(存活次数),辅助判断老化(tenuring)对象晋
升的时候是到Survivor分区还是到老年代分区。年轻代收集首先先将晋升对象尺寸总
和、对象年龄信息维护到年龄表中,再根据年龄表、Survivor尺寸、Survivor填充容
量-XX:TargetSurvivorRatio(默认50%)、最大任期阈值-XX:MaxTenuringThreshold(默
认15),计算出一个恰当的任期阈值,凡是超过任期阈值的对象都会被晋升到老代。
混合收集集合 CSet of Mixed Collection
年轻代收集不断活动后,老年代的空间也会被逐渐填充。当老年代占用空间超过整堆
比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会启动一次混
合垃圾收集周期。为了满足暂停目标,G1可能不能一口气将所有的候选分区收集
掉,因此G1可能会产生连续多次的混合收集与应用线程交替执行,每次STW的混合
收集与年轻代收集过程相类似。
为了确定包含到年轻代收集集合CSet的老年代分区,JVM通过参数混合周期的最大总次数-XX:G1MixedGCCountTarget(默认8)、堆废物百分比-XX:G1HeapWastePercent(默认5%)。通过候选老年代分区总数与混合周期最大总次数,确定每次包含到CSet的最小分区数量;根据堆废物百分比,当收集达到参数时,不再启动新的混合收集。而每次添加到CSet的分区,则通过计算得到的GC效率进行安排。
保证线程间可见和禁止指令重排
volatile可见性是有指令原子性保证的,在jmm中定义了8类原子性指令,比如write,store,read,load。而volatile就要求write-store,load-read成为一个原子性操作,这样子可以确保在读取的时候都是从主内存读入,写入的时候会同步到主内存中(准确来说也是内存屏障)
指令重排则是由内存屏障来保证的,由两个内存屏障:
一个是编译器屏障:阻止编译器重排,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行。
第二个是cpu屏障:sfence保证写入,lfence保证读取,lock类似于锁的方式。java多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个lock指令,就是增加一个完全的内存屏障指令。
Volatile:
有点像现代计算机的内存模型 cpu 和 主内存之间 加高速缓存,一致性问题 ,MESI协议,
总线风暴:
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。
所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。
并发编程下指令重排序会带来一些安全隐患:如指令重排序导致的多个线程操作之间的不可见性。
禁止指令重排序(as-if-serial)—>通过内存屏障(StoreStore屏障 LoadStore屏障)
happens-before:如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系
volatile域的写操作,happens-before于任意线程后续对这个volatile域的读保证可见性
synchronized(o)
monitorenter moniterexit
package com.mashibing.insidesync;
import org.openjdk.jol.info.ClassLayout;
public class T01_Sync1 {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
com.mashibing.insidesync.T01_Sync1$Lock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 49 ce 00 20 (01001001 11001110 00000000 00100000) (536923721)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
com.mashibing.insidesync.T02_Sync2$Lock object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 90 2e 1e (00000101 10010000 00101110 00011110) (506368005)
4 4 (object header) 1b 02 00 00 (00011011 00000010 00000000 00000000) (539)
8 4 (object header) 49 ce 00 20 (01001001 11001110 00000000 00100000) (536923721)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes tota
InterpreterRuntime:: monitorenter方法
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
if (PrintBiasedLockingStatistics) {
Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
}
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
"must be NULL or an object");
if (UseBiasedLocking) {
// Retry fast entry if bias is revoked to avoid unnecessary inflation
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
"must be NULL or an object");
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
synchronizer.cpp
revoke_and_rebias
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
assert(!attempt_rebias, "can not rebias toward VM thread");
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}
slow_enter (obj, lock, THREAD) ;
}
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
assert(!mark->has_bias_pattern(), "should not see bias pattern here");
if (mark->is_neutral()) {
// Anticipate successful CAS -- the ST of the displaced mark must
// be visible <= the ST performed by the CAS.
lock->set_displaced_header(mark);
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
TEVENT (slow_enter: release stacklock) ;
return ;
}
// Fall through to inflate() ...
} else
if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
assert(lock != mark->locker(), "must not re-lock the same lock");
assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
lock->set_displaced_header(NULL);
return;
}
#if 0
// The following optimization isn't particularly useful.
if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
lock->set_displaced_header (NULL) ;
return ;
}
#endif
// The object header will never be displaced to this lock,
// so it does not matter what the value is, except that it
// must be non-zero to avoid looking like a re-entrant lock,
// and must not look locked either.
lock->set_displaced_header(markOopDesc::unused_mark());
ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}
inflate方法:膨胀为重量级锁
无锁 - 偏向锁 - 轻量级锁 (自旋锁,自适应自旋)- 重量级锁
synchronized优化的过程和markword息息相关
用markword中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位
Object o = new Object()
锁 = 0 01 无锁态
o.hashCode()
001 + hashcode
00000001 10101101 00110100 00110110
01011001 00000000 00000000 00000000
little endian big endian
00000000 00000000 00000000 01011001 00110110 00110100 10101101 00000000
默认synchronized(o)
00 -> 轻量级锁
默认情况 偏向锁有个时延,默认是4秒
why? 因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
-XX:BiasedLockingStartupDelay=0
如果设定上述参数
new Object () - > 101 偏向锁 ->线程ID为0 -> Anonymous BiasedLock
打开偏向锁,new出来的对象,默认就是一个可偏向匿名对象101
如果有线程上锁
上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程
偏向锁不可重偏向 批量偏向 批量撤销
如果有线程竞争
撤销偏向锁,升级轻量级锁
线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁
如果竞争加剧
竞争加剧:有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半, 1.6之后,加入自适应自旋 Adapative Self Spinning , JVM自己控制
升级重量级锁:-> 向操作系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间
(以上实验环境是JDK11,打开就是偏向锁,而JDK8默认对象头是无锁)
偏向锁默认是打开的,但是有一个时延,如果要观察到偏向锁,应该设定参数
没错,我就是厕所所长
加锁,指的是锁定对象
锁升级的过程
JDK较早的版本 OS的资源 互斥量 用户态 -> 内核态的转换 重量级 效率比较低
现代版本进行了优化
无锁 - 偏向锁 -轻量级锁(自旋锁)-重量级锁
偏向锁 - markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁,偏向加锁的第一个线程 。hashCode备份在线程栈上 线程销毁,锁降级为无锁
有争用 - 锁升级为轻量级锁 - 每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁
自旋超过10次,升级为重量级锁 - 如果太多线程自旋 CPU消耗过大,不如升级为重量级锁,进入等待队列(不消耗CPU)-XX:PreBlockSpin
自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
偏向锁由于有锁撤销的过程revoke,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁。
public class T {
static volatile int i = 0;
public static void n() {
i++; }
public static synchronized void m() {
}
publics static void main(String[] args) {
for(int j=0; j<1000_000; j++) {
m();
n();
}
}
}
java -XX:+UnlockDiagonositicVMOptions -XX:+PrintAssembly T
C1 Compile Level 1 (一级优化)
C2 Compile Level 2 (二级优化)
找到m() n()方法的汇编码,会看到 lock comxchg …指令
在高争用 高耗时的环境下synchronized效率更高
在低争用 低耗时的环境下CAS效率更高
synchronized到重量级之后是等待队列(不消耗CPU)
CAS(等待期间消耗CPU)
一切以实测为准
public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。
public String test(String str){
int i = 0;
StringBuffer sb = new StringBuffer():
while(i < 100){
sb.append(str);
i++;
}
return sb.toString():
}
JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。
https://www.zhihu.com/question/63859501
其实,只被VMThread访问,降级也就没啥意义了。所以可以简单认为锁降级不存在!
一个ALU + 两组Registers + PC
http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
互斥锁:
自旋锁:
自旋锁与互斥锁使用层面比较相似,但实现层面上完全不同:当加锁失败时,
互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对。
互斥锁和自旋锁都是最基本的锁,读写锁可以根据场景来选择这两种锁其中的
一个进行实现。
读写锁
读优先锁
读优先锁期望的是,读锁能被更多的线程持有,以便提高读线程的并发性,它的
工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被
阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直
到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取读锁
写优先锁
写优先锁是优先服务写线程,其工作方式是:当读线程 A 先持有了读锁,写线
程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C
获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线
程 A 释放读锁后,写线程 B 就可以成功获取读锁。
公平读写锁
读优先锁对于读线程并发性更好,但也不是没有问题。我们试想一下,如果一直有读
线程获取读锁,那么写线程将永远获取不到写锁,这就造成了写线程「饥饿」的现象。
写优先锁可以保证写线程不会饿死,但是如果一直有写线程获取写锁,读线程也会被
「饿死」。既然不管优先读锁还是写锁,对方可能会出现饿死问题,那么我们就不偏
袒任何一方搞个「公平读写锁」。
公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是
读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥
饿」的现象。
互斥锁、自旋锁、读写锁,都是属于悲观锁
悲观锁:认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁
乐观锁:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
双亲委派机制缺陷?
(1):双亲委派核心是越基础的类由越上层的加载器进行加载, 基础的类总是作为被调用代码调用的API,无法实现基础类调用用户的代码
(2):JNDI服务它的代码由启动类加载器去加载,但是他需要调独立厂商实现的应用程序,如何解决? 线程上下文件类加载器(Thread Context ClassLoader), JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC
如何打破双亲委派模型?
(1):自定义类加载器,继承ClassLoader类重写loadClass方法;
(2):SPI机制
tomcat是如何打破双亲委派模型:
tomcat有着特殊性,它需要容纳多个应用,需要做到应用级别的隔离,而且需要减少重复性加载,所以划分为:/common 容器和应用共享的类信息,/server容器本身的类信息,/share应用通用的类信息,/WEB-INF/lib应用级别的类信息。整体可以分为:boostrapClassLoader->ExtensionClassLoader->ApplicationClassLoader->CommonClassLoader->CatalinaClassLoader(容器本身的加载器)/ShareClassLoader(共享的)->WebAppClassLoader。虽然第一眼是满足双亲委派模型的,但是不是的,因为双亲委派模型是要先提交给父类装载,而tomcat是优先判断是否是自己负责的文件位置,进行加载的。
SPI:(Service Provider interface)
(1):服务提供接口(服务发现机制):
(2):通过加载ClassPath下META_INF/services,自动加载文件里所定义的类
(3):通过ServiceLoader.load/Service.providers方法通过反射拿到实现类的实例
dubbo对Java的SPI机制进行了改进,可以根据key获取到特定实现类。
SPI应用?
(1):应用于JDBC获取数据库驱动连接过程就是应用这一机制
(2):apache最早提供的common-logging只有接口.没有实现…发现日志的提供商通过SPI来具体找到日志提供商实现类
线程池的任务队列:
相同:
LinkedBlockingQueue和ArrayBlockingQueue都是可阻塞的队列;
内部都是使用ReentrantLock和Condition来保证生产和消费的同步;
当队列为空,消费者线程被阻塞;当队列装满,生产者线程被阻塞;
使用Condition的方法来同步和通信:await()和signal()
不同:
1、锁机制不同
LinkedBlockingQueue中的锁是分离的,生产者的锁PutLock,消费者的锁takeLock
而ArrayBlockingQueue生产者和消费者使用的是同一把锁;
2、底层实现机制也不同
LinkedBlockingQueue内部维护的是一个链表结构。
在生产和消费的时候,需要创建Node对象进行插入或移除,大批量数据的系统中,其对于GC的压力会比较大。
而ArrayBlockingQueue内部维护了一个数组
在生产和消费的时候,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例。
3、构造时候的区别
LinkedBlockingQueue有默认的容量大小为:Integer.MAX_VALUE,当然也可以传入指定的容量大小
ArrayBlockingQueue在初始化的时候,必须传入一个容量大小的值
4、执行clear()方法
LinkedBlockingQueue执行clear方法时,会加上两把锁
5、统计元素的个数
LinkedBlockingQueue中使用了一个AtomicInteger对象来统计元素的个数,ArrayBlockingQueue则使用int类型来统计元素。
注意:
FixedThreadPool和SingleThreadExecutor => 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而引起OOM异常
CachedThreadPool =>允许创建的线程数为Integer.MAX_VALUE,可能会创建大量的线程,从而引起OOM异常
这就是为什么禁止使用Executors去创建线程池,而是推荐自己去创建ThreadPoolExecutor的原因
Compare And Swap (Compare And Exchange) / 自旋 / 自旋锁 / 无锁
因为经常配合循环操作,直到完成为止,所以泛指一类操作
cas(v, a, b) ,变量v,期待值a, 修改值b
ABA问题,你的女朋友在离开你的这段儿时间经历了别的人,自旋就是你空转等待,一直等到她接纳你为止
解决办法(版本号 AtomicStampedReference),基础类型简单值不需要版本号
AtomicInteger:
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
Unsafe:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
运用:
package com.mashibing.jol;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class T02_TestUnsafe {
int i = 0;
private static T02_TestUnsafe t = new T02_TestUnsafe();
public static void main(String[] args) throws Exception {
//Unsafe unsafe = Unsafe.getUnsafe();
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
Field f = T02_TestUnsafe.class.getDeclaredField("i");
long offset = unsafe.objectFieldOffset(f);
System.out.println(offset);
boolean success = unsafe.compareAndSwapInt(t, offset, 0, 1);
System.out.println(success);
System.out.println(t.i);
//unsafe.compareAndSwapInt()
}
}
jdk8u: unsafe.cpp:
cmpxchg = compare and exchange
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
jdk8u: atomic_linux_x86.inline.hpp
is_MP = Multi Processor
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
jdk8u: os.hpp is_MP()
static inline bool is_MP() {
// During bootstrap if _processor_count is not yet initialized
// we claim to be MP as that is safest. If any platform has a
// stub generator that might be triggered in this phase and for
// which being declared MP when in fact not, is a problem - then
// the bootstrap routine for the stub generator needs to check
// the processor count directly and leave the bootstrap routine
// in place until called after initialization has ocurred.
return (_processor_count != 1) || AssumeMP;
}
jdk8u: atomic_linux_x86.inline.hpp
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
最终实现:
cmpxchg = cas修改变量值
lock cmpxchg 指令
硬件:
lock指令在执行后面指令的时候锁定一个北桥信号
(不采用锁总线的方式)
(基于AQS)是调用countdown方法计数到达0时唤醒所有await 的地方,CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
是await到达设定值时候唤醒所有await 的地方。
CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。它采用采用空间来换取时间的方式,解决多线程中相同变量的访问冲突问题。
线程Thread–》ThreadLocalMap
Entry
ThreadLocal 本身并不存储值,它只是作为一个 key保存到ThreadLocalMap
中,但是这里要注意的是它作为一个key用的是弱引用,因为没有强引用链,弱
引用在GC的时候可能会被回收。这样就会在ThreadLocalMap中存在一些key为
null的键值对(Entry)。因为key变成null了,我们是没法访问这些Entry的,但
是这些Entry本身是不会被清除的,为什么呢?因为存在一条强引用链。即线程
本身->ThreadLocalMap->Entry也就是说,恰恰我们在使用线程池的时候,线程
使用完了是会放回到线程池循环使用的。由于ThreadLocalMap的生命周期和线
程一样长,如果没有手动删除对应key就会导致这块内存即不会回收也无法访
问,也就是内存泄漏。
内存泄漏归根结底是由于ThreadLocalMap的生命周期跟Thread一样长
1)如何避免内存泄漏
调用ThreadLocal的get()、set()方法后再调用remove方法,将Entry节点和Map的
引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC
的时候就可以被回收。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发
生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得
调用remove方法。
在不使用线程池的前提下,即使不调用remove方法,线程的"变量副本"也会被gc
回收,即不会造成内存泄漏的情况。
解决线程本地变量在父子线程中传递的问题
1、首先
InheritableThreadLocal继承自ThreadLocal,重写了getMap()、createMap()方法非常重要,操作的是线程t.inheritableThreadLocals,这两个函数在调用set(T value)和get()函数的时候都有被用到,起到了非常重要的作用;
2、传递给子线程,采用默认方式产生子线程(new Thread()),ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)来将父线程的inheritableThreadLocals传入并创建子线程的inheritableThreadLocals
3、然后将父线程中inheritableThreadLocals对应的ThreadLocalMap中的所有的Entry全部复制到一个新的ThreadLocalMap中,最后将这个ThreadLocalMap赋值给了子线程的inheritableThreadLocals
参考链接:https://blog.csdn.net/hbtj_1216/article/details/100511851?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.edu_weight&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.edu_weight
Java是值传递
值传递还是引用传递参考:https://mp.weixin.qq.com/s/Wc9rJ0rJ22jTEyCmKDBEsQ
值传递 当一个参数按照值的方式在两个方法之间传递时,调用者和被调用者其实是用的两个不同的变量——被调用者中的变量(原始值)是调用者中变量的一份拷贝,对它们当中的任何一个变量修改都不会影响到另外一个变量。
引用传递 当一个参数按照引用传递的方式在两个方法之间传递时,调用者和被调用者其实用的是同一个变量,当该变量被修改时,双方都是可见的。
java启动参数共分为三类
其一是标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
其二是非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;
其三是非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用
调优
生成dump文件:
jmap -dump:format=b,file=20200604.dump pid(例:58300)
jmap是Java自带的工具,
jmap -heap [pid] 查看整个JVM内存状态
jstack是sunJDK自带的工具
查看JVM中线程的运行状况,包括锁等待
jstack [pid] 线程的所有堆栈信息
使用:
通过jps找到pid 比如21711(进程)
找到进程中 耗时多的线程 top -Hp pid 例如top -Hp 21711 (或者Ps -mp 21711 -o THREAD,tid,time)
得到第一条的线程id 例如21742
将线程号转为16进制 printf “%x\n” 21742 (结果:54ee)
输出堆栈信息 jstack -l 21711 >> 123.txt (进程号) 在结果中找54ee相关的
拓展:可以去排查cpu飙高的问题 利用top找出cpu飙高的进程
jstat(JVM统计监测工具)
jstat -gc [pid]
copy-on-write:https://mp.weixin.qq.com/s?__biz=MzI4Njg5MDA5NA==&mid=2247484364&idx=1&sn=60b00b2188047267e5c46c09ae248ca8&chksm=ebd742cddca0cbdbcf40710dec04757208ee8e64473966b28a01cc87352515e45f9ec237c0a3&scene=21###wechat_redirect
集合:
Fast-Fail java.util包下 并发修改异常
ConcurrentModificationException modCount 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。
Safe-Fail java.util.concurrent包下 无此异常 是对集合的拷贝进行修改 但是在迭代期间不可见这种修改
Hashtable 是不允许键或值为 null 的,HashMap 的键值则都可以为 null,Hashtable在我们put 空值的时候会直接抛空指针异常,但是HashMap却做了特殊处理
这是因为Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。
如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理.
ArrayList不会自动缩小容积,那么如果我们需要缩小容积怎么办呢?
其实ArrayList里面有一个方法可以缩小容积,trimToSize()
这个ArrayList本来就是删除元素比较慢了,你再缩容,另外创建一个新的数组进行数组复制,一个是当下会占用较大空间,第二是,它主要用于查询,一般使用的话,基本操作都是存数据,然后取数据,频繁的删除修改并不多。现在常见的语言的可扩容数组容器都不会自动缩容,但是会提供手动缩容。自动缩容并不合理,首先你不知道什么时候缩,缩了万一又要加一大堆数据又要重新分配,多耗的那点内存要比频繁的重新分配耗的时间划算多了。
以Java1.8版本为例子,源码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(h = key.hashCode()) ^ (h >>> 16):
由于和(length-1)运算,length 绝大多数情况小于2的16次方。所以始终是hashcode 的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列。
所以这样高16位是用不到的,如何让高16也参与运算呢。所以才有hash(Object key)方法。让他的hashCode()和自己的高16位^运算。所以(h >>> 16)得到他的高16位与hashCode()进行^运算。
HashMap1.7并发下死循环是头插法引起的 8->5 扩容后 5->8
ConcurrentHashMap1.7和1.8的区别??
JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树。
与存储引擎无关 在存储引擎的上层写入binlog,binlog先于redo log被记录
只在每次事务提交的时候一次性写入缓存中的日志"文件"(对于非事务表的操作,则是每次执行语句成功后就直接写入
二进制日志 (binlog) 有 3 种不同的格式可选:Mixed,Statement(默认),Row。
MySQL Replication 复制可以是基于一条语句 (Statement Level) ,也可以是基于一条记录 (Row Level),可以在 MySQL 的配置参数中设定这个复制级别,不同复制级别的设置会影响到 Master 端的 bin-log 日志格式。
ROW
ROW格式会记录每行记录修改的记录,这样可能会产生大量的日志内容,比如一条update语句修改了100条记录,那么这100条记录的修改都会被记录在binlog日志中,这样造成binlog日志量会很大,这种日志格式会占用大量的系统资源,mysql5.7和myslq8.0安装后默认就是这种格式。
STATEMENT
记录每一条修改数据的SQL语句(批量修改时,记录的不是单条SQL语句,而是批量修改的SQL语句事件)所以大大减少了binlog日志量,节约磁盘IO,提高性能,看上面的图解可以很好的理解row和statement
两种模式的区别。但是STATEMENT对一些特殊功能的复制效果不是很好,比如:函数、存储过程的复制。由于row是基于每一行的变化来记录的,所以不会出现类似问题
MIXED
实际上就是前两种模式的结合。在Mixed模式下,MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种。
undo log有两个作用:提供回滚和多个行版本控制(MVCC)
数据库主从复制原理(bin log)
(1):主库db的更新事件(update、insert、delete)被写到binlog
(2):主库创建一个binlog dump thread线程,把binlog内容发送到从库
(3):从库创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log.
(4):从库还会创建一个SQL线程,从relay log里面读取内容写入到slave的db.
在主从复制结构中,要保证事务的持久性和一致性,需要对日志相关变量设置为如下:
如果启用了二进制日志,则设置sync_binlog=1,即每提交一次事务同步写到磁盘中。
总是设置innodb_flush_log_at_trx_commit=1,即每提交一次事务都写到磁盘中。
innodb存储引擎存储数据的单元是页(和SQL Server中一样),所以redo log也是基于页的格式来记录的
**undo log是采用段(segment)**的方式来记录的,每个undo操作在记录的时候占用一个undo log segment。
另外,undo log也会产生redo log,因为undo log也要实现持久性保护。
undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。
mysql写入数据的时候,是先把数据写到缓冲区,然后再flush到磁盘的,如何在flush过程中发生了宕机,数据如何恢复?(redo log刷盘策略)
https://www.cnblogs.com/haohaozhang/p/10093182.html
数据库事务是如何实现的?
(1)通过预写日志(WAL)方式实现的,redo和undo机制是数据库实现事务的基础
(2)redo日志用来在断电/数据库崩溃等状况发生时重演一次刷数据的过程,把redo日志里的数据刷到数据库里,保证了事务的持久性(Durability)
(3)undo日志是在事务执行失败的时候撤销对数据库的操作,保证了事务的原子性
MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复这个ReadView就好了。
链接:https://blog.csdn.net/qq_38538733/article/details/88902979
读未提交 (脏读问题)
读已提交 (不可重复读问题)
可重复读 (幻读问题)
串行化
Mysql间隙锁GAP(解决幻读):https://mp.weixin.qq.com/s?__biz=MzI4Njg5MDA5NA==&mid=2247484721&idx=1&sn=410dea1863ba823bec802769e1e6fe8a&chksm=ebd74430dca0cd265a9a91dcb2059e368f43a25f3de578c9dbb105e1fba0947e1fd0b9c2f4ef&token=1676899695&lang=zh_CN###rd
当我们用范围条件检索数据而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合范围条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”。InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。(只会在
Repeatableread隔离级别下使用) InnoDB 实现的RR通过next-key lock机制避免了幻读现象。
next-key lock是行锁的一种,实现相当于record lock(记录锁) + gap lock(间隙锁)
MySQL 的覆盖索引与回表
回表问题:非聚集索引—》主键—》行记录 两次扫描
可以建联合索引解决回表问题
当数据量不大的时候,mysql优化器分析觉得全表扫描的代价比走索引代价小的时候,会全表扫描不走索引,因为走索引,还会有个回表的过程。
innoDb支持 自适应哈希索引 为什么要自适应??
mysql存储引擎中
innoDb 只有.ibd文件 存放数据data和索引index
myisam 有MYD文件 存放数据data
有MYI文件 存放索引index 所以它们是分开存放
跟你存的数据的类型有关 一个页大小是16k,索引的key为int varchar text类型时,他们的大小不一样
一个页里面就能存更多数据,向下分叉就能分更多条,层数就低。(但是一般都是3、4层)
所以,索引越小越好。
单体的项目 推荐主键自增 每次都是往后面插 避免页分裂 减少合并和分裂的次数
若不使用主键自增 可能乱序 从中间页里面插 如果中间页满了 就涉及到页分裂
数据库内部的优化器
CBO 基于成本的优化
RBO 基于规则的优化
回表 辅助索引–》主键索引 再查行记录
索引覆盖 不需要回表 直接通过组合索引找到需要的值
最左匹配
索引下推(index condition pushdown )ICP
select * from table where name = ? and age = ?
数据存储在磁盘
mysql有自己的服务
mysql服务要跟磁盘发生交互
没有索引下推时:
先从存储引擎中拉取数据(根据name筛选的数据)
再mysql server 根据age进行数据的筛选
有索引下推时:
会在拉取数据的时候直接根据name、age来获取数据,不需要server做任何的数据筛选
在磁盘里面做了,避免加载到内存的数据量过大
索引下推唯一的缺点就是需要在磁盘上多做数据筛选,原来的筛选是放在内存中,现在放到了磁盘上查找数据的环节,
这样做看起来成本比较高,但是别忘了,数据是排序的,所有的数据是聚集存放,所以性能不会有影响,而且整体的io量
会大大减少,反而会提升性能。
MRR?
mult_range read 针对回表查找时 先对辅助索引找到的主键id,进行一个排序,然后类似范围
查找去主键里面找行记录,会占用一定的内存(id在内存中排序)
FIC
fast index create
在删除或者插入数据的时候,我们要修改对应的索引,我们会新增一张临时表,把新增或者删除的数据全部弄到临时表,
然后删除原始的表,再把原来的临时文件改一下名字就好。
而FIC会给当前表加一个share锁,不会有创建临时文件的资源消耗,还是在源文件中,但是此时如果
有人发起DML操作,很明显数据会不一致,所以添加share锁,读取时没有问题,但是DML会有问题。
https://blog.csdn.net/qq_41864967/article/details/100888244
left join(左联接) 返回包括左表中的所有记录和右表中联结字段相等的记录
right join(右联接) 返回包括右表中的所有记录和左表中联结字段相等的记录录
inner join(等值连接) 只返回两个表中联结字段相等的行
子查询比join慢:
select goods_id,goods_name from goods where goods_id = (select max(goods_id) from goods);
执行子查询时,MYSQL需要创建临时表,查询完毕后再删除这些临时表,所以,子查询的速度会受到一定的影响,这里多了一个创建和销毁临时表的过程。
分布式事务
跨库join、(设计的时候 弄字段冗余)
全局表
所谓全局表,就是有可能系统中所有模块都可能会依赖到的一些表。比较类似我们理解的“数据字典”。为了避免跨库join查询,我们可以将这类表在其他每个数据库中均保存一份。同时,这类数据通常也很少发生修改(甚至几乎不会),所以也不用太担心“一致性”问题。
字段冗余
这是一种典型的反范式设计,在互联网行业中比较常见,通常是为了性能来避免join查询。
举个电商业务中很简单的场景:
“订单表”中保存“卖家Id”的同时,将卖家的“Name”字段也冗余,这样查询订单详情的时候就不需要再去查询“卖家用户表”。
字段冗余能带来便利,是一种“空间换时间”的体现。但其适用场景也比较有限,比较
适合依赖字段较少的情况。最复杂的还是数据一致性问题,这点很难保证,可以借助
据库中的触发器或者在业务代码层面去保证。当然,也需要结合实际业务场景来看一
致性的要求。就像上面例子,如果卖家修改了Name之后,是否需要在订单信息中同
步更新呢?
count、
全局排序
改进1(排序字段是唯一索引)
思考之后改进思路如下:
首先第一页的查询不变
第二页及以后的查询,需要传入上一页排序字段的最后一个值,及排序方式。
根据排序方式,及这个值进行查询。如排序字段date,上一页最后值为3,排序方式降序。查询的时候sql为select … from table where date < 3 order by date desc limit
0,10。这样再讲几个表的结果合并排序即可。
改进2(排序字段不是唯一索引)
第一步不变
第二步在传入上一个的基础上,还需要传入能确定该行记录的唯一性字段
sql需要进行修改为select … from table where date = 3 order by date desc union select … from table where date < 3 order by date desc
limit 0,10。然后将结果合并之后排序。根据唯一性字段确定上一页最后一条记录,然后找出下面的分页记录。这里为何不用select …
from table where date <= 3 order by date desc limit
0,10呢,因为这样只能取到10条记录,如果某台节点上的满足等于3的节点为11条,那么就会漏掉一条数据,导致查询结果不正确
三次握手过程中可以携带数据么?
第一次、第二次不可以,第三次可以。
第一次握手如果可以携带数据的话,攻击者可以在SYN报文中写入大量垃圾数据,这样服务器就会话费很多时间和资源来处理这些垃圾数据。若是攻击者不停的发送SYN报文,这样就有可能导致服务器端资源耗尽,出现宕机。
而对于第三次连接,此时客户端已经处于ESTABLISHED状态,也就是说,对于客户端来说,它已经知道服务器端的接收和发送能力正常,此时携带数据就不会出现什么问题。
大端存储:高低大(是指将数据的低位(比如 1234 中的 34 就是低位)放在内存的高地址上,而数据的高位(比如 1234 中的 12 就是高位)放在内存的低地址上。这种存储模式有点儿类似于把数据当作字符串顺序处理,地址由小到大增加,而数据从高位往低位存放。)
小端存储:高高小(是指将数据的低位放在内存的低地址上,而数据的高位放在内存的高地址上。这种存储模式将地址的高低和数据的大小结合起来,高地址存放数值较大的部分,低地址存放数值较小的部分)
在HTTP传输中,就是采取大端传输的方式
操作系统三篇:
https://mp.weixin.qq.com/s/ldRDtdjmot81sFT_0H5s0g
https://mp.weixin.qq.com/s/bhAHnX5y5LIKe7ZC0mA92A
https://mp.weixin.qq.com/s/zkNwCRhy11OKJNT48PGRAg
String
embstr:<44字节
raw:>44字节
int:数字类型 long类型的值依然算字符串
list:
ziplist:
列表对象保存的所有字符串元素都<64字节
列表对象保存的元素数量小于512个
linkedlist
不满足上述两个条件
hash
ziplist
哈希对象保存的所有键和值的字符串长度都小于64字节
哈希对象保存的键值对数量小于512
hashtable
不满足上述两个条件
set
intset
集合对象保存的所有元素都是整数值
集合对象保存的所有元素数量不超过512
hashtable
不满足以上两个条件
sorted set
ziplist:
有序集合保存的元素数量小于128
有序集合保存的所有元素的长度都小于64字节
skiplist
不满足以上两个条件
redisDB 结构的 expires 字典保存了数据库中所有键的过期时间,该字典被称为过期字典:过期字典的键是一个指针,指向键空间中的某个键对象,过期字典的值是一个 long long 类型的整数,这个整数保存了键所指向的数据库键的过期时间。
惰性删除+定期删除
惰性删除流程
定期删除流程(简单而言,对指定个数个库的每一个库随机删除小于等于指定个数个过期key)
六种淘汰策略
redis集群思想:
https://www.cnblogs.com/pigonthetree/p/12410255.html
redis单机—》redis主从—》redis哨兵 实际都是一个节点保存了所有数据 只是做了高可用和数据备份
redis cluster 分布式存储 采用slot槽(共16384个槽) 对数据分片 每个节点持有一部分槽
缓存和数据库双写一致性
1.(更新的时候)先删除缓存 再改数据库
问题:
如果有个更新操作和查询操作一起过来,更新的操作先把缓存删了,但是还没修改数据库成功,此时查询的操作查缓存查不到去查数据库,但是数据库的数据还没修改过来,所以是错误的数据。
2.先更新数据库,更新成功后,再删除缓存
会解决方案一可能出现的问题。还是会存在并发时读脏数据,但方案二2比1出现脏数据的概率要低。
3.更新数据的时候只更新缓存,不更新数据库,然后根据异步方式批量更新数据库。
好处:数据的IO完全走缓存,不走数据。性能高,直接操作内存。缺点就是数据不是强一致的。
首先,Kafka 通过把 message 顺序写入磁盘来提高写入和读取效率。
其次,kafka 利用操作系统的页存储来提高写入效率
再次,Kafka 也通过 zero copy 技术来提高了数据的写入效率。
最后,Kafka 的 message 传输支持压缩技术。
总结一下:Kafka 速度的秘诀在于,它把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络 IO 损耗,通过 mmap 提高
IO 速度,写入数据的时候由于单个 partition 是末尾添加所以速度最优;读取数据的时候配合 sendfile 直接暴力输出。
Kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息。(也就是pull拉取模式)
若是push模式下,当broker推送的速率远大于consumer消费的速率时,consumer恐怕就要崩溃了。最终Kafka还是选取了传统的pull模式。
Pull模式的另外一个好处是consumer可以自主决定是否批量的从broker拉取数据。所以有@KafakListener。
mmapmmap内存文件映射:
简单描述其作用就是:将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件。
它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。
通过mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存),也不必关心内存的大小有虚拟内存为我们兜底。
使用这种方式可以获取很大的I/O提升,省去了用户空间到内核空间复制的开销。
mmap也有一个很明显的缺陷——不可靠,写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。Kafka提供了一个参数——producer.type来控制是不是主动flush;如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步(sync);写入mmap之后立即返回Producer不调用flush叫异步(async)
sendfile:消费者从读取消息时,调用transferTo transferTo调用的是系统函数sendfile
通过 sendfile 系统调用,提供了零拷贝。磁盘数据通过 DMA 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIC Buffer(socket buffer),无需 CPU 拷贝。
Java NIO对sendfile的支持就是FileChannel.transferTo()/transferFrom()。
fileChannel.transferTo( position, count, socketChannel);
把磁盘文件读取OS内核缓冲区后的fileChannel,直接转给socketChannel发送;底层就是sendfile。消费者从broker读取数据,就是由此实现
Kafka总结
总的来说Kafka快的原因:
1、partition顺序读写,充分利用磁盘特性,这是基础;
2、Producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入;
3、Customer从broker读取数据,采用sendfile,将磁盘文件读到OS内核缓冲区后,直接转到socket buffer进行网络发送。
mmap 和 sendfile总结
1、都是Linux内核提供、实现零拷贝的API;
2、sendfile 是将读到内核空间的数据,转到socket buffer,进行网络发送;
3、mmap将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上。
RocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。
Kafak的leader选举过程
最简单最直观的方案是,leader在zk上创建一个临时节点,所有Follower对此节点注册监听,当leader宕机时,此时ISR里的所有Follower都尝试创建该节点,而创建成功者(Zookeeper保证只有一个能创建成功)即是新的Leader,其它Replica即为Follower。
实际上的实现思路也是这样,只是优化了下,多了个代理控制管理类(controller)。引入的原因是,当kafka集群业务很多,partition达到成千上万时,当broker宕机时,造成集群内大量的调整,会造成大量Watch事件被触发,Zookeeper负载会过重。zk是不适合大量写操作的
BASE:全称:Basically Available(基本可用),Soft state(软状态),和 Eventually consistent(最终一致性)三个短语的缩写,来自 ebay 的架构师提出。
Base 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大型互联网分布式实践的总结,是基于 CAP 定理逐步演化而来的。其核心思想是:
既是无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
UUID 随机数
数据库特性
Redis 生成 ID
snowflake 雪花算法
时钟回拨问题===============》改进目标:解决雪花算法的时钟回拨问题;部分避免机器id重复时,号码冲突问题。
解决:
如果时间回拨时间较短,比如配置5ms以内,那么可以直接等待一定的时间,让机器的时间追上来。
如果时间的回拨时间较长,我们不能接受这么长的阻塞等待,那么又有两个策略:
(1). 直接拒绝,抛出异常,打日志,通知RD时钟回滚。
(2). 利用扩展位,上面我们讨论过不同业务场景位数可能用不到那么多,那么我们可以把扩展位数利用起来了,比如当这个时间回拨比较长的时候,我们可以不需要等待,直接在扩展位加1。
2位的扩展位允许我们有3次大的时钟回拨,一般来说就够了,如果其超过三次我们还是选择抛出异常,打日志。