帮帮忙……buf【66】.substring(0,buf【66】.indexof怎么使用(“,”)) 这是我做的

  • 执行速度:程序响应速度总耗時是否足够短
  • 内存分配:内存分配是否合理,是否过多消耗内存或者存在泄漏
  • 启动时间:程序运行到可以正常处理业务需要的时间
  • CPU时间:函数或者线程占用CPU时间
  • 内存分配:程序在运行时占用内存空间
  • 磁盘吞吐量:描述I/O使用情况
  • 网络吞吐量:描述网络使用情况
  • 响应时间:系统對用户行为或者时间做出回应的时间

系统瓶颈相关计算机资源

  • 异常:对java来说,异常捕获和处理是非常消耗资源的
  • 锁竞争:高并发程序中锁的竞争对性能影响尤其重要,增加线程上下文切换开销而且这部分开销与应用需求五官,占用CPU资源
  • 内存:一般只要应用设计合理內存读写速度不会太低,出发程序进行高频率内存交换扫描
  • Amdahl定义了串行系统并行化后加速比的计算公式和理论上线
    • 加速比定义:加速比= 優化前系统耗时/优化后系统耗时
  • Amdahl定律给出了加速比与系统并行°和处理器数量的关系,加速比Speedup,串行化程序比重FCPU数量为N:
  • 总结:根据Amdahl定律,使用多核CPU对系统进行优化优化效果取决于CPU数量以及系统中串行程序比重,CPU数越多串行比重越低优化效果更好。
  • 用于长沙一个多小嘚具体实例可以确保系统中一个类只产生一个实例,能带来两大好处:
    • 对的频繁使用的对象可以省略创建对象花费的时间,这对于那些重量级的对象而言是非常可观的一笔系统开销
    • 由于new操作次数减少,因而对系统内存的使用频率也会降低这将减轻GC压力,缩短GC停顿时間
  • 单例模式必须有一个私有构造,只有构造方法是私有的才能保证他不会被实例化;其次instance和getInstance必须是static修饰的这样JVM加载单例类时候对象就會被穿件,才能全局使用
  • 以上这种单例实现方式简单,可靠但是缺点是我们无法做到延迟加载,因为static修饰比如JVM加载类时候被建立如果此时,这个单例类特别大或者在系统中海油其他角色,你们任何使用这个类的地方都会初始化这个单例变量而不管是否被用到,比洳单例类作为String工厂如下:
  • 如上可见,没有使用instance但是还是会新建,因此我们需要的是延迟加载机制使用时候才创建,如下实现
  • 如上instance初始值null,在jvm加载时候没有额外的负担需要调用getinstance()方法之后才会判断是否已经存在,不存在则加上锁来初始化此处不加锁在并发情况会有線程安全问题。
  • 以上案例用synchronized必然存在性能问题存在锁竞争问题,如下改造
  • 使用内部类来维护单例的实例当StaticSingleton被加载时,内部类并不会被初始化只有getInstance被调用才会加载SingletionHolder,从而初始化instance因为实例的建立是类加载完成,所以天生就对多线程友好
  • 代理模式的一个作用可作为延迟加载,例如当前并没有使用某个组件则不需要初始化他,使用一个代理对象替代它的原有位置只要在真正需要使用的时候,才对他进荇加载在实践周上分散系统压力,尤其在启动的过程
  • 生成动态代理的方法有多个:JDK自带的动态代理,CGLIBJavassist或者ASM,jdk的无需引入第三方jar功能比较弱,CGLIB和Javassist都是高级字节码生成库总体性能不jdk的优秀,而且功能强大ASM是低级字节码生成工具,使用ASM已近乎使用java bytecode编程对开发人员要求高。
  • 首先使用JDK动态代理生成代理对象JDK动态代理需要实现一个处理方法调用的Handler,用于实现代理方法的内部逻辑接着需要用这个Handler生成代悝对象。如下:
  • 以上代码生成了一个实现IDBQuery接口的代理类代理类的内部逻辑有JdkDBQueryHandler决定,生成代理类后由newProxyInstance方法放回该代理类的一个实例。
  • CGLIB的動态代理使用和JDK的类似如上代码。
  • Javassist的动态java代码创建代理过程和上面的有一些不同javassist内部可以通过动态java代码,生成字节码这种方式创建嘚动态代理可以非常灵活,甚至可以在运行时候生成业务逻辑很奇特的一种用法,如下代码:
  • 以上代码使用CtField.make()方法和CtNewMethod.make方法在运行时候苼成了代理类的字段和方法这些逻辑由于javassist的CtClass对象处理,将java代码转换为对应的字节码并生成动态代理的实例。
  • 享元模式核心思想在于:洳果一个系统存在多个相同对象你们只需要共享一份拷贝,不必每次都实例化对象享元模式对性能的提升有如下两点:
    • 节省重复创建對象的开销。因为被享元模式维护的对象只被创建一次当被对象创建消耗比较大实话,可以节省大量时间
    • 由于创建对象的数量减少,所以对系统内存的需求也减少这将GC的压力也降低,进而使得系统拥有一个健康的内存结构和更快的响应速度
  • 享元模式主要角色由享元笁厂,抽象享元具体享元类,主函数几部分组成各功能如下:
    • 享元工厂:用来创建具体享元类,维护相同享元对象他保证相同的享え对象可以被系统共享(内部使用类似单例模式的算法,当请求对象已经存在时候直接返回对象,不存在时候在创建对象)
    • 抽象享元:定义需共享的对象的业务接口,享元类被创建出来总是为了实现某些特定的业务逻辑而抽象享元就是定义这些逻辑的语义行为。
    • 具体享元类:实现抽象享元类的接口完成某一具体逻辑。
  • 如上类图所示通过FlyWeightFactory工厂方法来生成ConcreteFlyWeight这一类大对象,其中ConcreteFlyWeight可以代表多个不同的类泹是都有相同的属性,通过继承IFlyWeight接口这样都可以通过工厂方法来生成,如下案例:
  • 装饰者模式基本设计准则 合成/聚合复用原则的思想玳码复用,应该尽可能的使用委托而不是使用继承。因为继承是一种紧密耦合任何父类的改动都会影响子类,不利于系统维护而委託则是松耦合,只要接口不变委托类的改动并不会影响其上层对象。

  • 装饰者模式通过委托机制复用系统中各个组件,在运行时可以將这些功能进行叠加,从而构造一个超级对象使其拥有各个组件的功能。基本结构如下

  • 装饰者Decorator在被装饰者ConcreteComponent的基础上(拥有相同接口Component)添加自己的新功能被装饰者拥有的是系统核心组件,完成特定功能模板如下案例:

  • JDK的实现中也有不少组件用的装饰器模式,其中典型的僦是IO模型OutputStream,InputStream类族的实现如下OutputStream为核心的装饰者模型的实现:

  • 观察者模式在软件系统中非常常用,当一个对象的行为依赖另外一个对象的狀态的时候观察者模式就相当有用。在JDK内部就已经为开发人员准备了一套观察者模式的实现在java.util包中,包括java.util.Observable类和java.util.Observer如图:
  • 如下按以上UML图實现观察者模式:
 
 
  • 缓冲可以协调上层组件和下层组件的性能差异,当上层组件性能优于下层组件时可以有效减少上层组件的处理速度,從而提升系统整体性能缓冲最好的案例便是I/O中的读取速度。还是上面那个例子
  • 其中第二个构造可以指定缓冲区大小默认情况和BuffereWriter一样,緩冲区大小8K缓冲大小不宜过小,这样起不到缓冲作用也不宜过大,浪费内存增加GC负担我们使用默认缓冲大小测试,一个2毫秒一个34毫秒,性能差一个数量级
  • 除了性能优化,缓冲区还可以作为上下层组件的通讯工具从而做到上下层组件解耦,优化设计结构
  • 缓存也昰一个提升系统性能而开辟的内存空间。缓存主要作用暂存数据处理结果并提供下次使用,减少数据库访问减少计算从而减少CPU的占用,从而提升系统性能
  • 对象池是目前常见的系统优化技术,核心思想是一个类被频繁请求使用不必每次生成一个实例,可以将这个类的┅些实例保存在一个池中需要使用直接获取,类似享元模式
  • 对象池的使用最多的就是线程池和数据库连接池,线程池中保存可以被偅用的线程对象,当有任务提交到线程池时候系统不需要新建线程,而从池中获取一个可用线程执行即可。任务结束后也不用关闭线程而将他返回到池中,下次继续用因为线程创建销毁过程是费时的工作,因为线程池改善了系统性能
  • 数据库连接池也是一个特殊对潒池,他维护数据库连接的集合当系统需要访问数据库直接从池中获取,无需重新创建并连接使用完后也不会关闭,重新回到连接池Φ在这个过程节省了创建和销毁的重量级操作,因此大大节约了事件减少资源消耗。目前应用多的数据库连接池C3P0 和Hibernate一起发布
  • 处理线程池,数据库连接池对普通Java对象,必须要时候也可以池化对于大对象来说,池化不仅节省获取对象实例的成本还可以减轻GC频繁回收這些对象产生的系统压力。
    • GenericObjectPool:一个通用的对象池可以设定容量,也可以指定无对象可用时候的对象池行为(等待或者创新对象)还可鉯设置是否进行对象有效性检查。
    • SoftReferenceObjectPool使用ArrayList保存对象但是并不是直接保存对象的强引用,二手保存对象的软引用对对象数量没有限制,没囿可用对象时候新建对象内存紧张时候JVM可以自动回收具有软引用的对象。
  • 多CPU是的并行充分发挥CPU的潜能
  • 大型应用系统一般用多台服务器來同时提供服务,以此将请求尽可能均匀的分配到各个计算技上
  • 案例TOmcat集群,通过Apache服务器实现用Apache服务器作为负载均衡分配器,将请求转姠各个tomcat类似nginxtomcat集群,有两种基本Session共享模式
    • 黏性Session所有Session信息平均到各个tomcat上,实现负载均衡好处在于不同各个节点同步信息,每个节点固定幾个用户信息节省内存缺点是宕机后用户信息丢失,无法做到高可用
    • 复制Session每台Tomcat存储全量用户Session数据,当一个节点上Session修改广播到所有节點同时做更新操作,这样即使挂掉也能提供服务,坏处是更新操作占用网络资源影响效率
  • 解决方案分布式缓存框架Terracotta,在公线内存时候并不会进行全复制,仅仅传输变化的部分网络复制也比较低,因此效率远高于普通session复制
  • 通常用于嵌入式设备或者内存,硬盘空间不足的情况通过牺牲CPU的方式,获得原本需要更多内存或者硬盘才能完成的工作如下哪里,教皇ab两个变量值,一般是引入第三变量方法┅而方法二通过计算能避免第三变量引入节省资源消耗CPU

  • 使用更多的内存或者磁盘空间换取CPU资源或者网络资源等,通过增加系统内存消耗来加快程序的运行速度。比如使用缓存技术来缓存一部分计算后的结果信息避免重复计算。
  • java中String类的基本实现主要包含三部分:char数组偏移量,String的长度char数组表示String的内容,他是String对象锁标识字符串的超集String的真实内容还需要由偏移量和长度在这个Char数组中进行定位和截取。如丅图:
  • java设计对String对象进行了一部分优化主要体现在以下三个方面,同时也是String对象的3个特点:
  • String对象一旦生成则不能在对他进行修改,String的这個特性可以泛化成不变模式机一个对象的状态在对象被创建后不发送变化,主要作用在一个对象被多线程共享并发范问时候,可以沈畧同步和锁等待的时间因为不会被修改,因此不存在线程安全问题
  • 当两个String对象拥有相同的值时候,他们只引用常量池中同一个拷贝這样可以大幅节省内存空间,如下案例
  • 以上案例str1 和str2 引用了相同的地址,但是str3重新开辟了一块内存但是str3 最终在常量池的位置和str1 是一样的,虽然str3 重新分配了堆空间但是在常量池中的实体和str1 相同,intern方法返回Stromg对象在常量池中的引用如下图说明:
  • 如上java中对String类的定义,final类型的定義是一个重要特点说明String在系统中不存在子类,这是对系统安全性的保护同时,对JDK1.5 版本之前环境中使用final定义有助于虚拟机寻找机会,內敛所有final方法从而提升系统效率。
  • 内存泄露(leak of memory):指为一个对象分配内存后在对象已经不再使用时候未及时释放,导致一直占用内存單元是实际可用内存减少,就好像内存漏了
  • 内存溢出(out of memory):通俗说就是内存不够,比如在一个无线循环中不断建大对象很快就会引發内存溢出。
  • 如上是String类的一个方法这个方法在JDK6 和JDK7 是实现不同的,一下我们分别讨论:
  • 如上简单案例两个字符串变量strsub。sub字符串是有父字苻串str截取得到如果在jdk6 中运行,因为数组在内存空间分配是在堆上那么sub和str的内部char数组value是公用的同一个,也就是上述a~t这个char数组str和sub唯一的差别就是在数组中的beginindex和字符串长度count不同,我们吧str引用为空实际上是想释放str的空间这时候GC并不会回收,因为sub引用的还是那个char因此不会被回收虽然sub只截取一部分,但是str特别大时候那就会造成资源浪费
  • JDK1.7中改进了subString的实现,他实际上是为截取的字符创在堆中重新申请内存用来保存字符串的字符如下源码:
  • 可以发现copyOfRange中为子字符串创建了一个新的char数组去存储子字符串中的字符。这样子字符串和父字符串也就没有什麼必然联系父字符串引用失效时候,GC就会适当时候回收
  • substring1.6 这种方式采用了空间换时间的手段,他是一个包内的私有构造不被外界调用,因此时间使用不用太担心带来的麻烦但是仍然需要关注这些问题。
  • 最原始的字符串分割split函数原型提供强大字符串分割功能,参数可傳正则
  • 普通字符串分割可以选择性能更好的函数StringTokenizer
  • 可以用两个组合方法比如indexof怎么使用和subString之前提到,subString采用空间换时间速度比较快,我们有洳下实现:
  • 由于String对象是不可变的如果我们在做字符串修改操作时候总会生成新对象,这样性能差所以有这两个用来修改字符串工具方法
String常量的累加操作

  • 两段代码分表指向5万次,方法一小盒0ms方法二消耗15ms,与预期相反反编译第一段代码:
  • 以上结果看出对于常量字符串累加,java的编译时候就做了优化对编译时候就能确定取值的字符串操作,在编译时候就进行了计算所以运行时候并没有生成大量String实例,而使用StringBuffer的代码反编译后的结果和源代码王完全一致可见运行StringBuffer对象和append方法都被如实调用,所以第一段代码效率才会这么快
  • 如果我们写如下玳码避开编译优化
  • 执行5万次平均耗时16ms,性能与StringBuilder几乎一样我们反编译一次得到如下
  • 可以看到对应字符串的累加,java也做了相应的优化操作實际上就是用的Stringbuilder对象来实现的字符串累加,所以性能几乎一致
构建超大的String对象
  • 有如下字符串修改方式对比:

  • 如上代码以及耗时与我们预期的方法一和三本相同的不一样,因为一与三本质都是使用的StringBuilder为什么差距这么大我们反编译一得到的如下:

  
  • 如上可看出,每次循环都重噺生成StringBuilder实例所以降低系统性能。这个也表明了String的加法操作虽然被优化但是编译器并不是万能的,因此少用为妙也可以得出StringBuilder > concat > +,+=这个字符串编辑效率的方法排名。
  • StringBuilder和StringBuffer初始化都有一个容量参数如下构造,不知道的情况默认16字节
  • 如上扩容方法策略将原有容量大小翻倍<< 1 移位运算,以新容量申请内存空间建新char数组,然后复制原有数据到新数组因此大对象的处理会涉及到N多次的内存复制扩容,如果能评估StringBuilder的大尛能避免中间的扩容复制操作,提高性能
  • ArrayList和Vector都使用数组,但是Vector增加了对多线程的支持是线程安全的,Vector绝大部分方法做了线程同步悝论上说ArrayList性能要好于Vector

  • LinkedList使用双向链表实现,每个元素都包含三个部分元素内容,前驱表项后驱表项

  • add方法的性能取决于ensureExplicitCapacity 方法中的grow扩容,当ArrayList足够大add效率是很高的无需扩容时候只需要执行以下两个步骤
  • 需要扩容时候,会将数组扩大到原来的1.5倍之后在进行数据的复制,复制的方法利用Arrays.copyOf方法
 
  • 如下last节点就是我们理解的指针,用来指向最后一个节点元素每次添加节点都添加到最后LinkedList使用链表结构,不需要维护容量夶小无需扩容,这个是对比ArrayList的优势但是每次需要新建Entry并且赋值,会有一定性能影响
  • 使用jvm参数是为了屏蔽GC对程序性能的干扰ArrayList耗时16ms,LinkedList耗時31ms不间断的生产新对象还是有一定资源损耗,若不用jvm参数区别更大,因为LinkedList会产生很多对象占用堆内存触发GC,因此LinkedList对对内存和GC要求高
  • List接口中有一个插入元素的方法
  • ArrayList和LinkedList在这个方法上存在非常大性能差异,ArrayList数组实现需要对数据重新排列,如下:
  • 可见每次插入都需要将index後面的数据整体后移,非常损耗性能并且index越靠前需要拷贝的数据越多,性能损耗越大
  • LinkedList优势比较大,如下源码:
  • 如上代码LinkedList插入随意位置与插入队尾区别在于需要遍历链表到指定index位置,无需数据复制而是对元素中前驱指针与后驱指针作修改
  • 删除与添加的逻辑基本一致,兩个的性能取决于需要操作的数据是在List的前中,后ArrayList需要复制数据,在头部时候效率低LinkedList需要遍历List在中间的时候效率更低,我们做如下測试得出以下结论:
List类型/删除位置
  • 遍历有三种方式forEach,Iteratori.for,三种方式在100万数据量下性能如下表:
  • 最简单的ForEach性能不如迭代器而用for循环的情況下ArrayList性能最优,LinkedList的在随机访问每次都需要一次列表的遍历操作性能会非常差,无法等到运行结束
  • ForEach 与 迭代器比较,反编译代码得到一些結果:

  • 由上看出仅仅是多了一次赋值操作,导致ForEach循环的性能更差一点
  • 将key做hash算法,将hash值映射到内存地址直接取得key对应的数据。HashMap的高性能保证以下几点:
    • hash算法必须高效的
    • hash值到内存地址(数组索引)的算法是快速的
    • 更具内存地址(数组索引)可以直接取得对应值
  • 首先第一点:hash算法的高效性我们用get方法中hash算法代码来解释
  • 先获取key的hash值,调用的Object中的HashCode方法,这个方法是native的实现比一般方法都要快,其他的操作都是基於位运算也是高效的
  • 注意:native方法快的原因是,他直接调用操作系统本地连接库的API
  • 第二点取得key的hash后续通过hash获取内存地址还是通过get方法中玳码:

  
  • 以上get方法中table是数组,将hash值和数组长度(n+1)按位与直接得到数组索引等效于取余,但是位操作性能更高这样得到数组下标取对应徝,
  • 第三点更具数组下标取值直接内存访问速度也快因此获取内存地址也是快速的
  • HashMap解决Hash冲突,首先结构上如下图结构HashMap内部维护一个数組,在1.7 中是Entry在1.8 中是Node,只是节点元素不同结构一样,此处按1.8 来讲如下Node源码

  
  • 如上,每个Node中包含四部分hash值,keyvalue,next指针当put操作时候如如仩图,新的Node n1会被放在对应的索引下标内并且替换原有值,同时为保证旧值不丢失将Node n1 的next 指向旧的Node n2 ,这样就实现了一个数组索引空间内存放多个值HashMap实际上就是一个链表的数组。
  • 并且在java1.8 时候还做了优化在链表长度达到7 的时候将链表转红黑数,提高读写效率
  • 容量参数对性能影响类似ArrayList和Vector,技术数组结构不可避免有空间不足进行扩展,扩容的过程也就会对性能消耗HashMap 有如下构造
    • 初始容量大小大于等于initialCapacity并且是2嘚指数次幂的最小整数作为内置数组的大小。
    • 负载因此叫填充比介于0~1 之间浮点数,决定了HashMap在扩容前其内部数组的填充度默认情况初始夶小16,负载因子0,75
    • 负载因子 = 元素个数/内部数组总大小
  • 负载因子越大说明HashMap中的hash冲突就越多,次数另外一个参数threshold 变量他被定义为氮气数组总嫆量和负载因子的乘积
    • threshold = 内部数组总容量*负载因子 = 确定元素总量一个数字
  • 依照上面的公式threshold 相当于HashMap的一个元素的阀值,更具各个参数来决定超过这个阀值会开始扩容,扩容代码如下:
  • 在HashMap基础上增加一个链表存放元素顺序相当于一个维护了元素顺序的HashMap,通过如下构造:

  
  • TreeMap功能上仳HashMap更强大他实现了SortedMap的功能,如上构造函数注入了一个Comparator使用一个实现了Comparable接口的key,对于TreeMap而言排序是必须进行的一个过程
减少循环中相同嘚操作,比如循环中的size等
  • 虽然java提供了基于流的IO实现InputStreamOutputStream这种实现以字节为单位处理数据,并且非常容易建立各种过滤器但是还是基于传统嘚IO凡是,是系统性能的瓶颈
  • NIO是New IO的简称,有以下特点:
    • 为所有原始类型提供Buffer缓存支持
  • 增加通道Channel对象作为新的原始IO抽象
  • 支持锁和内存映射攵件的文件访问接口
  • 与IO不同的是NIO基于Block块的,以块为基本单位处理数据NIO中最重要两个组件Buffer,Channel缓冲是一块连续的内存块,是NIO读写数据的中轉地通过标识缓存数据的源头或者目的地,用于向缓冲读写如下图:
  • NIO中和Buffer配合使用的是Channel,相当于一个双向通道读与写如下一个读文件案例:

  
    • 位置:position,写的时候当前缓冲区的位置,从position的下一个位置开始写入读取的时候当前缓冲区读取的位置从此位置后读取数据
    • 容量:capacit,读写的时候缓冲区总容量上线
    • 上限:limit,写入时候缓冲区实际上线一般小于等于总容量,通常和总容量相等读取的时候代表可读取的总容量,和上次写入的数据量相等
  • 一下案例解析三个参数:
  • 第一步申请15个字节大小缓冲区,初始阶段如下图

  • flip操作重置position,将buffer重写模式转为读并且limit设置到当前position位置作为读取的界限位置:

  • 五次读操作,与写操作一样重置position到当前位置指定已经读取的位置
  • 以上三个函数类姒功能,都重置buyffer这里说明的重置只是重置了标志位,并不是清空buffer数据还是存储在buffer中。分别看一下源码:
  • rewind 将position设置零同时清除标志位mark,莋用在于为提取Buffer的有效数据做准备
  • 标志mark缓冲区类似书签一样的功能数据处理过程中随时纪律当前位置,然后任意时刻回到这个位置,加快或者简化数据处理流程如下源码
  • mark用来记录当前位置,reset用于恢复到mark所在的位置如下使用方法:
  • 以原有缓冲区为基础,生成一个完全┅样的新缓冲区
  • 特点在于新生成的缓冲区与原缓冲区共享同一内存数据所以数据修改互相可见,但两者又独立维护个字positionlimit,remark使得操作哽加灵活。
  • 使用slice方法将现有缓冲区中创建新的子缓冲区字缓冲区和父缓冲区共享数据,这个方法有助于将系统模块化当需要处理一个Buffer嘚一个片段时候,可以使用slice方法取得一个子缓冲区然后单独处理。如下案例
  • 上例子中分出的子缓冲区如图所示:
  • asReadOnlyBuffer()方法获取与当前缓冲区┅直的并且内存共享的只读缓冲区,保证数据安全性
  • 当只读缓冲区被尝试修改的时候回抛出异常
  • NIO提供将文件映射到内存的方法进行IO操莋,比常规IO流快很多由FileChannel.map()实现,如下案例

  
  • 将文件0~最后一位byte字节映射到对应MapperByteBuffer内存中之后直接操作内存中数据
  • NIO提供的结构化数据处理:散射(Scattering),聚集(Gathering)散射指将数据读入一组Buffer中。聚集指将数据写入一组Buffer中接口实现如下
  • ScatteringByteChannel用法通道一次填充每个缓冲区,甜蜜一个后开始填充下一个,类似缓冲区数组的一个大缓冲区:

  
  • 如果需要创建指定格式的文件只需要先构造好大小合适的Buffer对象,使用聚集写的方式可鉯很快的创建出文件

  

  
  • 以上代码建两个ByteBuffer,分别存储书名和作者信息,构造ByteBuffer数组使用文件通道将数组写入文件

  
  • 同样散射读的方式根据长度精確构造buffer通过文件通道散射读将文件信息装载到对应的Buffer中,之后直接从buffer中读取信息
  • 以上两个案例可看到,通过和通道的配合使用可以簡化Buffer对于结构化数据处理的难度。
  • 分别用传统IO基于Buffer的IO,基于内存映射的MappedByteBuffer的I三种IO模型下的效率我们用如下案例
 
  • 此处指给出内存映射的方式文件读写,基于上几章节案例对比本次耗时写入109ms,读文件耗时61ms大大优于基于Buffer的IO,此外基于Buffer的IO大大优于基于传统的IO总结如下表格
  • 与ByteBuffer鈈同的是,普通ByteBuffer仍然在JVM的堆内存上分配空间最大内存收到最大堆内存限制,DirectBuffer直接分配在物理内存中不占用堆内存空间(重要)
  • 普通ByteBuffer访問,系统会使用一个“内核缓冲区”进行间接操作而DirectBuffer所处的位置,就相当于这个“内核缓冲区”使用DirectBuffer是一种更接近系统底层的方法。仳普通ByteBuffer更快


  • 测试二中DirectBuffer的GC信息简单,因为GC只记录堆空间内存回收由于DirectBuffer占用内存并不在堆内存中,因此对操作更少日志自然少,但是DirectBuffer对潒本身还是在堆空间分配只是他指向的内存地址不在对内存空间范围内而已。
  • 引用类型分四种: 强引用软引用,弱引用虚引用
  • java引用類似指针,通过引用可以对堆中的对象进行操作如下
  • 以上代码,局部变量str分配在栈上对象StringBuilder实例分配在堆内存,str会执行StringBuffer所在堆内存空间通过str操作改实例,str1 也同时指向StringBuffer实例内存也就是有两个引用,str与str1,
  • 如上两个引用都是强引用有如下特点
    • 强引用可以直接访问目标对象
    • 强引用所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出OOM异常也不会回收强引用指向的对象
    • 强引用可能导致内存泄露
  • 软引用是通过java.lang.ref.SoftReference使用軟引用一个持有软引用的对象,不会被jvm回收jvm会判断当堆内存使用临近阀值时候。才会去回收软引用的对象只有内存足够,理论上存活相当长的时间
  • 基于软引用特点,我们可以利用软引用来实现对内存敏感的Cache
  • 弱引用比软引用更弱在系统GC时,只要发现弱引用不管系統堆空间是否足够,都会讲对象回收但是GC的线程通常优先级不高,因此并不一定能很快发现持有弱引用的对象。一旦弱引用对象被GC回收便会加入到一个注册引用队列中。
  • 软引用弱引用都非常适合来保存那些可有可无的缓存数据,如果这么做当系统内存不足时候,這些缓存数据会被回收不会导致内存溢出。而当内存资源充足时候这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用
  • 虚引用是引用中最弱一个,和没有引用几乎一样随时可能被GC回收。当通过虚引用的get()方法去的强引用对象时候总是失败的并且,虚引鼡必须和引用队列一起使用.
  • 虚引用的作用在于跟踪垃圾回收过程清理被销毁对象的相关资源。通常对象不被使用时候,重载该类的finalize方法可以回收对象的资源但是,如果finalize方法使用不慎可能导致回收失效对象复活的情况如下案例:
  • 如上案例输出显示obj对象两次gc后还是存活狀态,我们在第二次gc前加上obj=null得到结果才能回收obj对象通过obj=null去除强引用,由此可看出在复杂应用系统中一旦finalize方法实现有问题容易造成内存泄露。而虚引用则不会有这种情况因为他实际上是已经完成了对象的回收工作的。
  • WeakHashMap类在java.util包内他实现了Map接口,是HashMap的一种实现他时使用弱引用作为内部数据的存储方案,如下定义源码:
  • 用处:如果系统中需要一个很大的Map表Map中的表项作为缓存用,这种场景下使用WeakHashMap是比较匼适,因为WeakHashMap会在系统内存范围保存所有表项,而内存一旦不够GC清楚未被引用的表项避免内存泄露。

  • 因为限制堆内存来实验可以看到沒固定一段时间后就会触发Full GC,因为内存不够会将之前申请的所有key都释放,以维持内存需求避免OOM

  • 案例二中用的HashMap运行语段时间后直接OOM,因為5M堆内存显然不够用此处就体现出WeakHashMap的优势
 
  • 总结:WeakHashMap使用弱引用,可以自动释放以及被回收的key所在的表项但如果WeakHashMap的key都在系统内持有强引用,那么WeakHashMap就退化成了普通的HashMap因为所有的表项都无法被自动清理。
  • 调用方法传递的参数以及调用中创建的临时变量都保存在栈Stack中,速度较赽其他变量,如静态变量实例变量等,都在堆Heap中创建速度较慢
  • 所有运算中,位运算最为高效如下优化:

  • 看两段代码执行相同功能,苐一段普通运算耗时219ms第二段位运算耗时31ms

提取表达式避免重复计算

  • 位运算速度高于算数运算,但是条件判断时候 布尔运算速度高于位运算
  • java对布尔运算做了充分优化,比如a,bc布尔运算a&&b&&c,更具逻辑与操作只有一个false立刻返回false,因此a为false立刻返回false,当bc运算需要消耗大量系統资源时候,这种处理方式效果最大化
  • 同理计算a||b||c逻辑或时候,一个未true返回true。
  • 位运算:虽然位运算效率也搞但是位运算只能将所有子表达式全部计算完成后,在给出最终结果因此从这个角度说使用位运算替代布尔运算可能会有额外消耗。如下案例对比
  • 数组赋值功能JDK提供的高效API
  • System.arraycopy()函数时native函数,通常native函数性能要优于普通的函数仅处于性能考虑,在软件开发时候应尽可能调用native函数。
  • 处理NIO外使用java进行IO操莋有两种基础方式
  • 两种方式都应该合理配合使用缓冲,能有效提高IO性能以文件IO为例如下图组件

  
  • java中新建对象一般new关键字,但是如果对象构慥函数过于复杂会导致对象实例化变得十分耗时且消耗资源,此时Object.clone方法会更好的选择
  • 原因在于Object.clone()方法可以绕过对象构造方法,快速复制┅个对象实例跳过了构造函数对性能的影响
  • 使用static字段修饰的方法是静态方法,java中由于实例方法需要维护一张类似虚拟函数表的结构以實现对多台的支持。与今天方法相比实例方法的调用需要更多资源。工具方法没有对其进行重载的必要更适合于用作为静态方法。
  • 简單说就是一个异步获取的流程在main函数中利用Future提交一个任务线程task,但是这个任务执行是有耗时的并不会立刻返回,此时main函数无需等待或鍺阻塞可以继续main函数之后的逻辑,等结果是必要因素的时候通过Future.get来阻塞等待获取对应信息如下图解

  • 如下源码,自己实现的一个Future模式实現

  • 以上代码中FutureData实现了一个快速返回的RealData包装他只是一个包装,或者说是一个RealData的虚拟实现并且设置FutureData的getReault的时候回被wait阻塞,等待RealData被注入才能返囙
  • Future模式在JDK的并发包中内置了一个实现比以上Demo更加复杂,如下类图

  • 我们将上一步骤中自己的Demo用JDK的Future实现如下改进的代码变更简单,直接通過RealData构造FutureTask将其作为单独线程运行通过FutureTask.get()方法获取结果。
  • Future模式核心优势在于去除了调用方的等待时间,并使得原本需要等待的时间段可以用于处悝其他的业务逻辑从而充分利用计算机资源。
  • 核心思想:两部分一个Master进程和一个Worker进程,Master负责接收分配任务Worker负责处理任务,Worker处理完后結果返回给Master由Master进程做归纳和汇总,从而得到最终结果
  • 优点:能够将大任务切分执行,提高系统吞吐量每个子任务都是异步处理,无需Client等待如下流程:
  • 维护一个Worker进程队列不断处理新任务,由master进程维护Worker队列类似线程池如图:
  • 保护暂停模式,当服务进程准备好时候才提供服务如下场景,服务器吞吐量有限的情况下客户端不断的请求,可能会超时丢失请求次数,我们将客户端请求进入队列服务端┅个一个处理。如下工作流图:

  • ResquestQueue队列用来缓存请求使这种模式可以确保系统仅在有能力处理任务时候才获取队列这任务执行其他时间则等待。类似MQ提供的生产者消费者模式如上模式并没有返回值的获取,有一定缺陷如下改进流程图:

  • 如上添加了Data类型,其实是参照了Future模式代码一样此模式环境系统压力,将系统负载再时间轴上均匀分布可以做到流量削峰的效果。
  • 为了在多线程环境下对象读写的线程安铨性如果新建的对象中属性是不可更改的,这样即使在高并发情况下也可以做到天生的线程安全并且这中模式实现也简单,满足如下偠求:
    • 去除setter方法以及所有修改自身属性方法
    • 将所有属性设置私有用final标记,确保不可修改
    • 确保没有子类可以重载修改它的行为
    • 有一个可以創建完整对象的构造函数
  • 不变模式在JDK中使用比较多,比如基本数据类型和String类型中都是定义的final但是不变模式是通过回避问题而不是解决問题的态度来处理多线程并发访问控制,可以在需求允许的情况下提高系统并发性能和并发量
  • 经典的多线程设计模式该模式中有两类线程,若干个生产者线程和若干消费者线程在两者中间共享内存缓冲区,生产者和消费者互相不直接通信通过内存缓冲区来交换信息,這样即使生产消费的速度不一致也不会影响业务的正常进行
  • 生产者消费者模式能很好对生产者线程和消费者现在进行解耦,优化了系统整体结构同事由于缓冲区作业,运行
  • JDK提供了用于多线程管理的线程池
  • 多线程的确可以最大限度的利用多核处理器的计算能了但同时线程的创建销毁时有系统开销的,需要消耗内存占用CPU资源,当线程创建无限制时候反而会耗尽CPU和内存导致正常业务无法进行。
  • 实际生产環境中线程数必须得到控制,盲目地大量创建线程对系统性能是有伤害的
  • 线程池作用在于在多线程环境下避免线程不断创建和销毁所帶来的额外开销。有线程池的存在当系统需要一个线程时候,并不立刻创建线程而是先去线程池查找是否有空余,若有直接使用,若没有则将任务放入等待队列或者创建新线程任务完成后将线程放回线程池。

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
  • 以上用线程池做1000 个任务与直接创建1000个线程做对比线程池婲费218 ms, 普通453ms简单线程池的实现看起来效率更高,
  • 其实JDK中提供了一套Executor框架帮助开发人员有效的进行线程控制,如下ExecutorsUML图

  • 运行的世界和上面線程池Demo近似265msExecutors工厂类还有如下方法
  • 以上工厂方法分表返回具有不同工种特性的线程池
    • newFixedThreadPool()方法:返回一个固定线程数量的线程池,改线程池中線程数不变当有一个新任务提交,线程池有空闲则只需没有则添加到等待队列,等空闲之后才处理在队列中的任务
    • newSingleThreadExecutor():返回一个只有一個线程的线程池多余任务提交放入一个FIFO(先入先出)的顺序队列的任务中
    • newCacheThreadPool():返回一个可根据实际情况调整线程数量的线程池,线程池数量不确定有空闲则复用,没有则会建新线程处理任务所有线程执行完放回线程池中
  • 如没有特殊要求,一般使用JDK内置线程池不自己实現
    • corePoolSize:指定线程池中核心线程数量
    • keepAliveTime:当线程池数超过corePoolSize,多余的空闲线程存活时间也就是超过corePoolSize的空闲线程在多久空闲时间后会被销毁
    • workQueue:任务隊列,被提及尚未被执行的任务
    • threadFactory:线程工厂用于创建线程,一般用默认即可
    • handler:拒绝策略任务太多来不及处理,如何拒绝任务
    • 直接提茭的队列:这个由SynchronousQueue对象提供,他是一个特殊BolckingQueue没有容量,每次insert操作都需要等待delete操作反之也是同样。SynchronousQueue不保存任务只是将任务提交给线程執行,没有空闲线程则创建直到达到最大线程数,执行拒绝策略
      • 当使用有界队列时候线程池中的情况:
      • 有新任务需要执行,如果线程池现有线程数<corePoolsize则新建线程
      • 如果线程池现有线程数>=corePoolSize,将任务加入等待队列
      • 如果等待队列满则无法加入,在总线程< maximumPoolSize前提下新建线程执行任務
    • 无界队列:LinkedBlockingQueue无上限的一个队列,除非耗尽了系统资源否则无界队列不存在无法入队列情况但是按照上面的规则,线程数量一直都会昰corePoolSize的数量
    • 优先队列:带有执行优先级的队列,通过PriorityBlockingQueue实现可控制任务的先后顺序,特殊的无界队列总是确保最高优先级的任务先执行。
  • 依据上面的规则我们在来看JDK中几个线程池的实现
  • newCacheThreadPool方法,corePoolSize为0 ,maximumPoolSize看成无穷大意味着任务提交时候回用空闲线程执行,如果没有空闲则加叺SynchronousQueue队列,这种队列的特定是无容量的也就是说没有空闲线程也不会入队列,而是直接创建线程这中线程池也存在资源耗尽的危险。
  • ThreadPoolExecutor最後一个参数指定拒绝策略当任务超过系统承载的处理策略,JDK内置了四种策略
    • AborPolicy策略:抛出异常,组织系统正常工作这个也是默认策略
    • CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程运行当前被丢弃的任务
    • DiscardOledestPolicy:丢弃最老的一个请求,也就是即将被执行的一个任务并尝试洅次提交当前任务
    • Discardpolicy:直接丢弃无法处理的任务,不做任务处理
  • 以上策略都实现了RejectedExecutionHandler,如果这几个都不满足业务我们可以自己实现
  • 以下用優先级队列的Demo

  
  • 调度中,线程以999,998~0 的顺序加入以上是一个输出片段,可以看到在省略号之前都是按照我们预期的顺序执行的之后是不规则順序,那是因为我们设置了初始线程数量是100也就是刚开始的时候并不会直接放入队列中,而是用现有的线程执行
  • 由此可见,使用自定義线程池可以提供更灵活的任务处理和电镀方式在JDK内置线程池无法满足应用需求的时候,则可以考虑使用自定义线程池
  • 对线程池的大尛设置需要考虑CPU数量,内存JDBC链接等因素,在并发编程实战的数字有如下公式
W/C = 等待时间与计算时间的比率
  • 为了保持处理器达到期望的使用率最优的线程池的大小由如下公式得出,在java中我们用二方法得到

  • ThreadPoolExecutor是一个可扩展的线程池提供了三个可扩展接口:
  • ThreadPoolExecutor实现中提供空的before和after方法,实际应用我们可以对他扩展实现对线程状态的跟踪,比如增加一些日志的输出等如下demo
  • 以上扩展方法带有日志输出功能,更好排查問题

  • 如上源码对比来看CopyOnWriteArrayList的get()方法并没有任何锁操作,而对比Vector的get()是用了Synchronized所有get操作之前必须取得对象锁才能进行。在高并发读的时候大量锁競争非常消耗系统性能我们可以得出,在读性能上CopyOnWriteArrayList是更优的一种方式。

  • 接下来看下两个的写入方式add的源码


 
  • 由上部分源码中CopyOnWriteArrayList 每次add方法都會是一次复制新建一个新数组,将老数组拷贝并在新数组加入新来的元素,这种开销还是挺大的
  • Vector中add方法其实就是一次synchronized之后的数组操作出发需要扩容才会出现数组复制的情况,因此绝大多数情况写入性能比CopyOnWriteArrayList更优秀
  • CopyOnWriteArraySet线程安全Set,实现了Set接口内部实现完全依赖CopyOnWriteArrayList,所以特性囷他一样适合读多写少的情况。如果需要并发写的时候用到Set我们可以用一下方式得到线程安全Set

  • JDK中有两套并发队列实现,ConcurrentLinkedQueue高性能队列BlockingQueue阻塞队列,都继承自Queue接口前者适用于高并发场景,通过无锁的凡是实现高并发状态的高性能性能高于BlockingQueue
    • ArrayBlockingQueue:基于数组的阻塞队列维护一个萣长数组,缓存队列中的数据对象还保存着两个整型变量,分表标识队列头部尾部在数组中位置,还可以控制内部锁是否采用公平锁默认非公平锁。
    • LinkedBlockingQueue:一个基于链表的阻塞队列生产者放入数据时候,将会缓存到一个链表中生产者立刻返回,只又当缓冲链表达到最夶容量才阻塞生产者,知道有消费者从链表获取数据生产者才被换醒
    • linkedList使用的链表实现,有利于扩容无需数组的复制,随机读取性能低
    • ArrayDeque使用数组实现拥有高效随机访问性能,更好的遍历性能不利于数据扩容
  • JDK中提供多种途径实现多线程的并发控制,例如:内部锁重叺锁,读写锁信号量等。
  • java每个线程有自己的工作内存并且还有另外一块内存是给所有线程共享的,线程自己的工作内存区域存放这共享内存区域中变量值的拷贝如下步骤以及图解
    • 当执行拷贝时候,线程会先锁定并清除自己的工作内存区这保证共享变量从共享内存区囸确拷贝到自己的工作内存区域
    • 装载到线程工作内存区域后,线程就可以操作对应的内存区域的数据应为内存拷贝之后对值修改是共享嘚,即修改了本地内存变量也就同时修改了共享内存中变量,所有只要保证当线程解锁是保证该工作内存区中变量值都已经写回到共享內存中
  • 共享内存中变量值一定是由他本身或者其他线程存储到变量中的值,即使他被多个线程操作也就是其中某个线程修改后的值,洏线程工作内存中的局部变量的是线程安全的只能自己线程访问
  • 如上图4.18所示描述了线程工作内存与主内存的一些操作,其中useassign很好理解,下面主内存和工作内存的操作
    • read操作与相匹配的load操作总是成对出现read读操作相,就想线程工作内存需要用一个局部变量去读取主内存中变量数据之后在通过load操作加载到工作内存
    • Store操作与write操作总是成对出现,store操作我需要将线程工作内存中的变量赋值给主内存变量的一个拷贝中可以看成是一个局部变量,接着write操作将这个拷贝后的值写入到主内存中变量完成修改操作。
  • Double和Long操作比较特殊因为只有他两是双精度64位,但是在32位操作系统中CPU最多一次性处理32位字长的数据这将导致Double,Long类型需要CPU处理两次才能完成对一个数据的操作,可能存在多线程下非原孓性的问题所以需要将其声明为volatile进行同步处理。
  • volatile关键字破事所有线程均读写主内存中的对应变量从而使得volatile变量在多线程之间可见
    • olatile变量鈳以做以下保证:
    • 其他线程对变量的修改可以及时反映在当前线程中
    • 确保当前线程对volatile变量的修改,能及时写会共享主内存中并被其他线程所见
    • 使用volatile申明的变量,编译器会保证其有序性
  • synchronized是java预约中最常见的同步方法,与其他同步方式比更简洁代码可读性和维护性更好。并苴从JDK1.6开始对其性能优化已经和非公平锁的差距缩小,并且不断优化中
  • synchronized最常用方法如下,当method方法被调用,调用线程首先必须获得当前对象嘚锁如果当前对象锁被其他线程持有,则需等待方法结束才会释放锁,接着获取synchronized中同步的代码越少越好,粒度越小越好
  • 还有如下凊况,用于static函数时候相当于Class对象上加锁所有对改方法调用都需要获取Class对象锁。
  • 为了实现多线程之间的交互还需要使用Object对象的wait和notify方法
    • wait可鉯让线程等待当前对象上的通知(notify被调用),在wait过程中线程会释放对象锁,将锁让给其他等待线程获取
    • notify将唤醒一个等待在当前对象上嘚线程,如果当前有多个线程等待则随机选择其中一个。
  • 比synchronized功能更强大可以中断,可定时
    • 公平锁,保证锁在等待队列中的各个线程昰可以公平获取的不存在插队,永远先进先出
    • 非公平锁:不保证申请锁的公平性,申请锁能差多
  • 公平锁实现代价更大,从性能上分析非公平锁性能好更多,无特殊需求选择非公平锁
  • 读写锁jdk5开始提供这种锁,读写锁允许多个线程同时读取但是如果有写操作在执行,其他操作需要等待写操作执行完后获取锁才能执行
  • 当读操作次数远远大于写操作,读写锁效率很高提升系统性能
  • await()方法使当前线程等待,同时释放当前锁当其他线程中使用signal()或者signalAll()方法时候,线程重新获得锁并执行或者当前线程被中断时,也能跳出等待
  • Singal()方法用于唤醒┅个等待中的线程,相对singalAll方法会唤醒所有在等待中的线程
 
 
 
 
 
 
 
  • 信号量可以指定多个线程同时访问某个资源,如下构造方法
  • 提供如下几个主要方法控制线程

  • ThreadLocal不提供锁使用空间换时间的方式,为每个线程提供变量的独立副本用这种方式解决线程安全的问题,因此他不是一种数據共享的解决方案
  • ThreadLocal并不具有绝对优势,只是并发的另外一种解决思想在高并发量或者锁竞争激烈的场合,使用ThreadLocal可以在一定程度上减少鎖竞争从而减少系统CPU损耗
  • 我们用如下demo来测试ThreadLocal中变量是线程安全的:
  • 以上没有输出,即使在并发时候也可以保证多个线程间localvar变量是本线程獨立的即使没有同步操作,单多个线程数据互不影响如下set源码解释本地线程安全性问题
  • 锁性能优化和常见思路:避免死锁, 减小锁粒喥锁分离
  • 多核时代,并发提高效率是因为多线程尽可能的利用了多核心的工作优势但是对比单线程方式也增加开销,系统需要维护多線程环境的上下文切换线程共享内存,局部变量信息线程调度等。
  • 死锁是多线程特有问题满足以下情况下出现死锁:
    • 互斥条件:一個资源每次只能被一个线程持有
    • 请求与保持条件:一个进程请求资源阻塞时候,对已获得的资源保持不放
    • 不剥夺条件:若干线程之前形成在未使用完之前,不能强行剥夺
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
  • 尽量减少持有锁的时间以减少线程间互斥的可能。
  • 例如在ConcurrentHashMap中增加一个新表项并不是将整个HashMap加锁,首先更具HashCode获取数据落到哪一个段(segment)上然后对该段加锁,并完成put操作在并发put的时候,只要被操作的不是同一个段就可以真正做到并行
  • 减少锁粒度缺点:当系统需要去全局锁时候其资源的消耗会更多,例洳ConcurrentHashMap里面我们需要获取每个Segment的锁后才能获取全局锁因此我们应该通过其他方式避免这种操作
  • 读写锁共享读,独占写可以有效提升系统并發能力。
  • 从读写锁中可以衍生出锁分离我们在LinkedBlockingQueue中take,put操作实现可以看出这种思想的实现因为LinkedBlockingQueue是链表,存取是不冲突的如果是全局所,伱们存取只能单个进行效率很低,锁竞争激烈JDK中用如下方法
 
 
 
 
  • 如上将take,put操作用两个锁来避开竞争关系削弱了竞争可能性,如下实现:
  • 內部锁重入锁功能上是重复的,使用上synchronized更简单jvm会自动释放,ReentrantLock需要在finally中自己释放
  • 功能上ReentrantLock功能更强大功能,如下:
  • 以上功能都有效避免迉锁的产生从而提高系统稳定性。而且重入锁中的Condition机制可以进行复杂的线程控制上面一节中已经有过说明。
  • 本来应该锁的粒度越小越恏但是在某些情况下有必要对锁粒度进行扩大化,比如我们连续不断请求释放同一个锁,这样JVM需要不断对同一个锁进行请求释放,慥成系统资源浪费这个时候其实JVM也会帮我们做优化,将连续锁操作合成一个
  • 自旋锁是JVM虚拟机多锁的优化,当多线程并发时候频繁的官气和恢复操作会有很大系统开销,当获取锁的资源仅仅需要一小段CPU时间时候锁的等待获取的时间可能就非常短,以至于比挂起和恢复嘚时间还要短这就是一个问题了
  • JVM引入的自旋锁就是为了解决上面问题,可以使线程在未获取到锁时候不被挂起,而作一个空循环(即所谓的自旋)若干个空循环后,线程如果获得了锁则只需,没有获取则挂起
  • 自旋后被挂起的几率变小,这种锁对那竞争不激烈并且鎖占用时间少的并发线程有利但是对竞争激烈,锁占用时间长大概率自旋后也大概率不能获取锁徒增了CPU时间的占用,得不偿失
  • JVM通过虛拟机参数-XX:PreBlockSpin参数来设置自旋等待次数
  • 锁消除是JVM在编译时候做的一个优化,当JVM对上下文进行解析时候会去除一些不可能存在共享资源竞爭的锁。也就是如果一个变量不可能存在多线程并发竞争的情况但是他却有加锁,JVM会编译时候讲锁去除以此避免不必要的锁获取消耗過程。
  • JDK的API中有一些线程安全的工具类StringBufferVector,在某些场合工具类内部同步方法是不必要的JVM在运行时基于逃逸分析技术,捕获这些不存在禁止卻有锁的代码消除这些锁,以此提高性能
  • JDK1.6开始提出的一种锁优化。核心思想:如果没有程序竞争则取消之前已经取得锁的线程同步操作。就是说如果一段时间内所有竞争这个锁资源的线程都是同一个那么我就在这段时间点取消锁竞争避免锁定获取释放过程
  • 缺点:偏姠锁在竞争激烈场合没有优化效果,因为竞争大导致有锁线程不停切换锁也很难保存在偏向模式,次数使用偏向锁大不大上面提到的性能优化还有损性能。
  • 非阻塞同步的线程安全方式
  • 基于锁方式存在他的弊端就是收核心限制线程竞争已经相互等待而非阻塞则避开了这個问题,类似ThreadLoadl还有一种更重要的基于比较交换的CAS(Compare And Swap)算法的无锁并发控制。
  • CAS优势非阻塞,无死锁情况没有锁竞争开销,没有线程调喥开销
  • CAS算法过程:CAS(V,EN)。V表示需要更新的变量E预期值,N新值类似乐观锁的思想,当V==E时候才将V值设置为N,如果V和E不同则标识其他线程已经修改则不作更新,CAS返回当前V的真实值更新失败并不挂起,而是被告知失败
  • 大部分处理器支持原子化CAS命令,JDK5 以后JVM可以使用這个指令实现并发操作和并发数据结构
  • 以getAndSet方法为例,看原因如何实现CAS算法工作流程:
  • 如上代码是一个循环循环退出条件是设置成功,並且返回原值此处并没有用到锁,只是不停尝试更新失败后继续获取当前值然后比较设置,继续循环而已并没有干扰其他线程的相關代码。
  • java.util.concurrent.atomic包中性类性能是非常优越的包中的原子类是基于无锁算法实现的,他们的性能远优于普通有锁操作
  • 无锁的操作实际将多线程並发冲突交给应用层,不仅提升系统性能还增加系统灵活性。相对的算法以及编码的复杂度也明显增加了。
  • 无锁算法缺点是需要应用層处理线程的冲突问题这增加开发难度和算法复杂度。现在Amino无锁框架提供了可用于线程安全的基于无锁算法的一些数据结构同事还内置了一些多线程调度如下优势:
  • 高并发下无锁竞争带来的性能开销
  • 可以轻易使用一些成熟无锁结构,无需自己研发
  • 反正就是性能比JDK搞的一個框架提供了基础数据例如List,Set这些
  • 进程— 线程— 协成这三个概念是逐步拆分的一个过程,进程是一种重量级的调度方式因为创建,調度上下文切换的成本太高,因此我们将进程拆分为多个线程将线程作为并发控制的基本单元,但是单并发在趋于激烈为了在进一步提升系统性能,我们对线程在进一步分割就是协程
  • 无论进程,线程协成在逻辑上都对应一个任务,执行一段逻辑代码当使用协程實现一个任务时候,协程并不完全占据一个线程当一个协程处于等待状态,CPU交给线程内的其他协程与线程相比,协程间的切换更轻便因此,具有更低的操作系统成本和更高的任务并发性
    • 协程的执行效率非常高,因为子进程切换不是线程切换而是由程序自身控制,洇此没有线程切换的开销,和多线程比较线程数量越多,协程的性能优势越明显
    • 协程不需要多线程的锁机制在协成中控制共享资源鈈加锁,只需要判断状态
  • 协程不被java语言原生支持,我们可以使用kilim框架通过较低成本开发引入协程

  • 协程没有锁,同步块等概念不会有哆线程程序的复杂度,但是与java其他框架不同的是开发后期需要通过kilim框架提供的织入(weaver)工具,将协程控制代码块织入原始代码已支持協程的正常工作。如下图

  • Task是协程的任务载体execute方法用于执行任务代码,fiber对象保存和管理任务执行堆栈以实现任务可以正常暂停和继续。Mailbox對象为协程间的通信载体用于数据共享和信息交流。

  • Task多谢负责执行代码完成任务kilim官网中状态图:
  • Task创建后处于Ready状态,调用execute方法后开始運行,期间可以被暂停也可以被唤醒。正常结束后Task对象成为完成状态
  • Fiber用来维护Task执行堆栈,Kilim框架对线程进行分割以支持更小的并行度,所以Kilim需要自己维护执行堆栈

  • Fiber主要成员如下:

    • pc:程序计数器记录当前执行位置
  • Fiber的up方法,down方法用于维护堆栈的生长和回退如下fiber状态机:

  • Kilim 框架中协程间的通讯和数据交换依靠Mailbox邮箱进行,就像管道多个Task间共享,以生产者消费者作类比生产者向Mailbox提交一个数据,消费者协程则想Mailbox中获取数据

Kilim示例以及性能评估

  • 协程拥有比线程更小粒度,所以理论上kilim的并发模型可以支持更高的并行度使用kilim可以让系统更低成本的支持更高的并行度

java虚拟机内存模型

  • 程序计数器:用于存放下一条运行的指令
  • 虚拟机栈,本地方法栈:用于存放函数用堆栈信息
  • java堆:存放程序运行时候需要的对象等数据方法区
  • 方法区:用于存放程序的类元数据信息
  • 是一块很小的空间当我们用多线程时候,其实一个CPU一个时刻呮能为一个现场提供服务所以线程数超过CPU个数的时候,线程质检轮询争夺CPU资源此时当一个线程被切换出去,为此需要程序计数器来记錄这个独立线程运行到哪一个步骤指令以便下一次轮询到继续执行的时候从这个步骤指令开始往下执行。
  • 各个线程质检的计数器互补影響独立工作,是一块线程私有的内存空间
  • 如果一个线程在执行java方法,程序计数器就在记录正在执行java字节码的地址如果是一个Native方法,程序计数器为空
  • 也是线程私有的内存空间,同java线程统一时刻创建保存:局部变量,部分结果并参与方法调用放回
  • 可用-Xss参数控制栈大尛,如下测试我们设置-Xss1M,之后设置-Xss2M
  • 栈存储结构是栈帧每个栈帧存放:

  • 每个方法调用就是一次入栈,出栈过程参数多,局部变量表就夶需要内存就大。如下图:

  • 如上可看出调用函数参数,局部变量越多占用栈内存越大,导致调用次数比无参数时候下降5544–>4167

  • 可见最大局部变量是13是这么算的,局部变量存放单位字32位(并非字节)long,double是64为两个字,所以三个参数三个局部变量都是long 6*2=12个字,还有一个非static方法虚拟机回将当前对象(this)最为参数通过局部变量传递给当前方法所以最终13字

  • 局部变量中的字对GC有一定影响,如果局部变量被保存在局部变量表GC根能引用到这个局部变量所指向的空间,可达性算法判断不可回收因此不会清除,如下:

  • 以上GC日志显示已经回收但是依據可达性分析算法是不能回收的,是GC做的其他优化后续解答(??)
  • 局部变量表中字可能会影响GC回收如果这个字没有被后续代码复鼡,那么他引用的对象不会被GC释放
  • 本地方法栈和虚拟机栈类似,只不过是用来管理本地方法调用本地方法比不是用java实现,而是C实现SUN嘚HotSpot虚拟机中不区分本地方法栈和虚拟机栈。因此和虚拟机栈一样会抛出StackOverFlowErrorOutOfMemoryError
  • java堆运行是内存,几乎所有对象和数组都在堆空间分配内存堆内存分为新生代存放刚产生对象和年轻对象,老年代存放新生代中一定时间内一直没有被回收的对象。如下

  • 如上图新生代分为三个部分:

    • eden:对象刚建立时候存放的位置
    • s0(surivivor space0 或者 from space),s1(survivor space1 或者通space):servivor意为幸存者空间也就是存放其中的对象至少经历一次GC并新村。并如果幸存区到指定年龄还没被回收则有机会进入老年代
  • 如下案例我用JVM参数:

  • 上面GC日志是注释掉GC代码后生成,可以看到进行了多次内存分配,我们算┅下先分配8.5M从fromspace使用率 92%,因为触发了GC将eden中的b1移动到了from空间,最后分配的8MB被分配到eden新生代,新生代设置12 并且按照8:1:1 分配所以清理掉的0.5 M可鉯存放在from中,如果from更小导致存放不下回直接到old区
  • 如上,调试来几次Xmn的大小得出的结果因为回更具对内存中年轻态的大小来决定GC之后存儲的位置,上日志看出FUllGC后新生代有一部分,老年态也有一部分我猜测是原来的b1 到老年态,二最好的b2的8M内存在新生态
  • 方法区最重要的昰类信息,常量池域信息,方法信息
    • 类信息:类的名称,父类名称类型修饰符(public/private/protected),类型的直接接口类表
    • 常量池:类方法域等信息引用的常量
    • 域信息:域名称,域类型域修饰符
    • 方法信息:方法名,返回类型方法参数,方法修饰符方法字节码,操作数栈方法棧帧的局部变量区大小以及异常表。
  • HotSpot中方法去也成为永久区GC对此部分也能回收,两点:
    • GC对永久区常量池的回收
    • 永久区对类元数据的回收
  • 洳下Demo测试常量池回收

  • 以上代码无限循环中向常量池中添加对象如果不清理必然OutOfMemoary,但是日上日志中得出GC在一段时间后清理,使程序正常運行
  • 类元数据回收需要构造N个类来测试对应GC回收情况如下DEMO用Javassist类库来生成类

  • 以上日志看出在一段时间后就回触发一次FullGC,之后并没有OOM由此鈳见GC能够保证方法区中类元数据的回收。
  • 用-Xmx指定最大堆内存最大堆指的是新生代+ 老年代大小只和的最大值。
  • 用-Xms设置最小对内存
  • 意义在于:Java启动优先满足-Xms的指定大小当指定内存不够才向操作系统申请,直到触及Xmx导致OOM
  • -Xms过小,JMV为保证系统尽量在指定范围内存工作只能频繁嘚GC操作来释放内存,间接导致MinorGC和FUllCc的次数如此啊测试

  • 以上demo控制循环次数越多FullGC的次数越多,20次循环中有两次FullGC修改JVM参数
  • 将最大堆,堆小堆都設置11M只触发一次FUllGC,由此可见:
    • JVM将系统内存尽量控制在-Xms大小内当内存触及-Xms时候,就Full GC我们将-Xms与-Xmx设置成一样可以在系统运行初期减少GC次数囷耗时。
  • 用-Xmn设置新生代的大小新生代也会堆GC有比较大影响,如上案例在继续增加如下:
  • 结果可以看的20次循环中又有2次FullGC
  • HotSpot虚拟机中-XX:NewSize设置新苼代初始大小,-XX:MaxNewSize设置新生代最大值只设置-Xmn等效同时设置这两个参数。
  • 用-XX:MaxPermSize,-XX:PermSize分别设置永久代最大,最小值永久代直接决定来系统可以支歭多少个类的定义,以及多少个产量合理设置有助于系统动态类的生成。
  • 以上JVM参数分别可以运行4142 9333 次,一般我们设置MaxPermSize=64M可以满足绝大部分不够的话可以增加128M,在不够就代优化代码了
  • 使用-Xss参数设置线程栈大小
  • 栈过小,将会导致线程运行时候没有足够空间分配聚币变量或鍺达不到足够深度的栈深度调用,导致StackOverFlowError栈空间过大,那么将导致开启线程所需要的内存成本上升系统所能支持线程总数下降。
  • 如下Demo测試固定栈空间下开设线程数量:
  • 很大可能是一直运行到结束并没有预期结果调大-Xss10M并且增加循环次数,最大可能是死机原因在于次数设置的是线程栈大小10M,也就是一个线程10M每次创建都会向系统内存申请10M的内存,导致系统内存不够从而电脑卡顿,死机都有可能而最终應该是OutOfMemoryError。
  • 之前提了设置堆最大最小新生代大小JVM参数配置,实际生产环境希望能对对空间进行比例分配,有如下参数:

  • 同类型参数-XX:NewRatio=老年玳/新生代继续添加参数测试:

 
  • 如上堆内存20M,新生代老年代1:2计算得到老年代12MB,和GC日志中13824 相符合
    • -Xms:堆内存初始大小
    • -Xmx:堆内存最大大小
    • -XX:MinHeapFreeRatio:設置堆空间最小空闲比例,堆空间内存小于这个JVM会扩展堆空间
    • -XX:MasHeapFreeRatio:设置堆空间最大空闲比例当对空间空闲内存大于这个,会压缩堆空间嘚到一个较小的堆
  • Java和C++最大区别就是C++需要手动回收分配的内存,但是Java使用了垃圾收集器替代C++的手工管理方式减少程序员负担减少出错几率。因此GC需解决一下问题:
  • 最古老的的GC算法对象A,只要有任何对象引用了A就计数器+1任何取消A引用-1,当引用为0A就可以被清除
  • 弊端在于无法解决相互引用的死局,例:AB相互引用,但是没有任何第三方引用根据算法是不可被回收,这种情况导致内存泄露
  • 不适合作为JVM的GC策畧
    • 标记:通过根节点(java虚拟机栈中对象的引用)标记从根节点开始可达对象,未被标记则是可回收对象
    • 清除:清除未被标记的对象
  • 弊端:清理后产生空间碎片空间是非连续空间,大对象分配时候不连续内存空间工作效率更低。
  • 基于标记算法将原内存分两块每次用其中┅块,GC时候将存活对象复制到另一块内存中,清楚原来内存块中所有对象然后交换角色。

  • 优势:如果系统中垃圾对象多复制算法需偠复制的就少,此时复制算法效率最高并且统一复制到某一块内存,不会有内存碎片

  • 弊端:内存折半,负载自然降低

  • 用处: Java新生代串荇垃圾回收器使用复制算法,Eden空间from,to空间三部分from = to空间,天生就有区域划分并且from与to空间角色互换。因此GC时候eden空间存活对象复制到survivor空間(假设是to),正在使用的survivor(from)中的年前对象复制到to大对象to放不下直接进入old区域,to满了其他放不下的直接去old区域,之后清理eden与from去

  • 体現复制算法优势在新生代的合理性,新生代存活对象比例小复制算法效果更佳

    • 标记:和之前一样的通过根节点可达性分析获取被引用的對象,做标记
    • 压缩:清理没标记对象并且将存活对象压缩到内存的一端接着清理边界外所有空间,避免碎片产生
  • 标记压缩避免碎片化嘚同时不需要进行内存空间减半的风险,因此性价比高
  • 老年代中存活对象多不使用与复制算法,因此可以用标记压缩方式
  • 为解决Stop the World状态提出的一种思想,GC时间长影响用户体验
  • 增量算法思想:一次性完成GC需要更多时间,我们让GC收集线程与应用线程交替执行每次收集一部汾区域,将对系统影响降到最低
  • 弊端:线程切换和上下文切换的消耗一定程度影响应用程序性能是系统吞吐量下降使GC总体成本上升
  • 主要思想:按每个GC算法不同给合适的区域使用对应的GC算法。
    • 年轻代:对象存活度低使用复制算法,经过N次后存活对象进入old区
    • 老年代:对象存活度高,使用标记压缩算法提高GC效率
  • 一下指标评价GC处理器好坏
    • 吞吐量:系统吞吐量 (系统总运行时间 = 应用程序耗时 + GC耗时)
    • 垃圾回收器負载:垃圾回收器耗时与系统运行总时间比值
  • 垃圾回收频率:通常增加对内存空间可以有效降低GC频率,但是会增加一次GC的耗时
  • 反应时间:標记为垃圾后多久能清理
  • 堆分配:不同垃圾收集器堆内存分配不同好的收集器应该有一个合理的分配方式。
  • JDK中最老的也是最基本的回收器有两个特点
    • 第一仅仅使用单线程进行垃圾回收
    • 第二他是独占式垃圾回收
  • 此垃圾回收器工作,java应用程序必须听着就是“Stop The World”,但是他是朂成熟的一个并且新能高,新生代中使用复制算法逻辑简单
  • JVM参数设置-XX:+UseSerialGC 来指定新生代中使用串行收集器,老年代中使用串行收集器
  • 老年玳不同在于使用标记压缩算法也是同样需要其他线程停顿,但是可以和多种新生代回收器配合用如下JVM参数
  • -XX:UseParallelGC:新生代用并行回收收集器老姩代使用串型收集器
  • 只是将串型收集器多线程化,回收策略算法都一样。
  • 多核CPU系统下更快的处理单核CPU系统下甚至比串型差
  • 一个很牛逼嘚收集器,和并行比较都是多线程,独占但是他有一些关键的参数可以设置,我们可以用一下参数启用:
    • -XX:+UseParallelGC:新生代用并行回收收集器咾年代用串型收集器
  • 用如下JVM参数设置关键值:
    • -XX:MaxGCPauseMillis:设置最大GC停顿时间,它自动调整java堆大小来控制GC的时间如果时间设置的很短,他会使用一个較小堆这样回收很快,但是回收频繁吞吐量就降低来
    • -XX:GCTimeRatio:这个设置吞吐量,比如设置N那么他自动调节系统话费不超过1*(1+n)

我要回帖

更多关于 indexof怎么使用 的文章

 

随机推荐