美文网首页
第二章 java程序优化

第二章 java程序优化

作者: 农民工进城 | 来源:发表于2019-10-08 11:14 被阅读0次

本章主要知识点:

  • java程序中字符串的优化
  • 慎用正则表达式
  • java中集合的优化
  • java中IO优化
  • java中的引用类型和使用方法
  • 多线程优化

1. 字符串的优化处理

字符串的优化不容小视
字符串的主要特点:

  • 不变行:final类型,一旦生成不可改变

  • 针对常量池的优化:
    在Java中创建字符串的方式有两种:
    1)字符串常量,String str=“abc”;
    2)new 创建 String str=new String(“abc”)。
    第一种方式,JVM会首先检查常量池中是否存在该对象,尊在则返回该对象应用,否则则创建对象并放到常量池中;
    第二种方式,new 创建,JVM调用String的构造方法,同时引用常量池中的”abc”字符串,在堆内存中创建一个新对象,最后str引用该新对象

  • 利用String的intern方法,如果常量池中存在相同的值,则直接使用该对象,返回该对象的引用

  • 慎重使用 Split() 方法,能用 String.indexOf() 方法代替 Split() 方法尽量用indexOf(),因为用了正则表达式,而正则表达式,使用不当,会引起回溯问题

  • 大字符串拼接,用Stringbuilder

2. 慎用正则表达

慎用正则表达式,因为使用不当,可能会引起回溯问题
正则表达式的执行,需要正则表达式引擎执行,表达式引擎分为两种:DFA 自动机(Deterministic Final Automata 确定有限状态自动机)和NFA自动机(Non deterministic Finite Automaton 非确定有限状态自动机)。
对比来看,构造 DFA 自动机的代价远大于 NFA 自动机,但 DFA 自动机的执行效率高于NFA 自动机,

NFA 自动机会读取正则表达式的每一个字符,拿去和目标字符串匹配,匹配成功就换正则表达式的下一个字符,反之就继续和目标字符串的下一个字符进行匹配。

正则表达式三种模式
贪婪模式:如果单独使用+、?、*或者{min,max}等,正则表达式会匹配尽可能多的内容
懒惰模式:该模式下,正则表达式会尽可能少的重复匹配自负。
独占模式:独占模式仝贪婪模式一样,也会尽可能匹配更多的内容;不过在该模式下,匹配失败就会结束匹配,不会发生回溯问题

避免回溯的方法就是:使用懒惰模式和独占模式。

正则表达式的优化:
  • 少用贪婪模式,多用独占模式
  • 减少分支选择:
    分支选择”(X|Y|Z)”正则表达式会降低性能,应尽量减少使用。
    1)优先考虑将常用的选项放在前面,使较快的被匹配
    2)提取公共部分,”(abcd|abef)”—>”ab(cd|ef)”
    3)如果可以使用index来代替,则尽量使用index来代替
  • 减少捕获嵌套
    1) 捕获组是指把正则表达式中,子表达式匹配的内容保存到以数字编号或显式命名的数组,方便后面引用。一般一个 () 就是一个捕获组,捕获组可以进行嵌套。
    2) 非捕获组则是指参与匹配却不进行分组编号的捕获组,其表达式一般由(?:exp)组成。

3. 集合优化

3.1 ArrayList和LinkedList比较

1)ArrayList数组实现,内存空间连续;LinkedList 双向链表实现
2)ArrayList数组实现,随机访问便利较快,LinkedList不支持随机访问

  • 3.1.1 ArrayList 和 LinkedList 新增元素效率比较
    1> 从集合头部新增元素,LinkedList较快

原因:ArrayList是基于数组实现的,内存空间连续,如果要在头部新增元素,则需要重新复制以后的元素,从而进行平移操作,所以效率比较低;LinkedList是基于链表实现,在新增元素的时候,先确定需要插入的位置,如果是前半段则从前往后找;如果是后半段,从后往前找,找到对应位置,则插入。

2> 从集合中间新增元素,ArrayList较快
原因:中间位置效率都不太高,ArrayList需要复制数组平移,Linked需要寸照位置,遍历的元素最多
3> 从集合尾部新增元素,ArrayList较快
在容量两者效率都比较高,只不过LinkedList,需要new Node节点,所以ArrayList比较快,同样在容量不足的情况下,ArrayList效率会降低,而LinkedList不变

  • 2.ArrayList 和 LinkedList 删除元素效率比较
    1)从集合头部删除元素,LinkedList较快
    2)从集合中间删除元素,ArrayList较快
    3)从集合尾部删除元素,ArrayList较快

理由同新增元素

  • 3.ArrayList 和 LinkedList 遍历元素效率比较
    1)for循环,ArrayList较快
    内存连续
    2)迭代器循环,大致相同
3.2 合理使用 java8 中的Stream

我们通常还会将中间操作称为懒操作,也正是由这种懒操作结合终结操作、数据源构成的处理管道,实现了Stream的高效。
在循环迭代次数较少的情况下,常规的迭代方式性能反而更好;在单核 CPU 服务器配置中,也是常规迭代方式更有优势;而在大数据循环迭代中,如果服务器是多核 CPU 的情况下,Stream 的并行迭代优势明显。所以我们在平时处理大数据的集合时,应该尽量考虑将应用部署在多核 CPU 环境下,并且使用 Stream 的并行迭代方式进行处理。
用事实说话,我们看到其实使用 Stream 未必可以使系统性能更佳,还是要结合应用场景进行选择,也就是合理地使用 Stream。

3.3 HashMap优化
  • 当查询操作较为频繁时,则可以适当地减少加载因子;如果对内存利用率要求比较高,则可以适当的增加加载因子。
  • 还可以在预知存储数据量的情况下,提前设置初始容量(初始容量 = 预知数据量 /加载因子)
    设置初始容量,一般得是 2 的整数次幂,减少hash冲突

4.IO 优化

  • 高并发用NIO
  • RPC序列化避免使用Java序列化
    Protobuf 的这种数据存储格式,不仅压缩存储数据的效果好, 在编码和解码的性能方面也很高效

5. 多线程性能优化

5.1 Synchronized 原理及优化
5.1.1 原理

Synchronized既可以修饰代码块也可以修饰方法。

  • 当修饰同步代码块时,是由 monitorenter 和 monitorexit 指令来实现同步的。进入 monitorenter 指令后,线程将持有Monitor对象,退出monitorexit指令后,线程将斥方该Monitor对象。
  • 当 Synchronized 修饰同步方法时,并没有发现 monitorenter 和 monitorexit 指令,而是出现一个ACC_SYNCHRONIZED 标志。
    因为 JVM 使用了 ACC_SYNCHRONIZED 访问标志来区分一个方法是否是同步方法。当方法调用时,调用指令将会检查该方法是否被设置ACC_SYNCHRONIZED访问标志。如果设置了该标志,执行线程将先持有Monitor对象,然后再执行方法。在该方法运行期间,其它线程将无法获取到该Mointor对象,当方法执行完成后,再释放该Monitor对象。

Monitor是依赖于底层的操作系统实现,存在用户态与内核态之间的切换,增加了性能开销

5.1.2 锁升级
image.png
  • 偏向锁
    主要用来优化同一线程多次申请同一个锁的竞争。
    偏向锁的作用就是,当一个线程在此访问这个同步代码块或者方法时,该线程只需要去对象头的MarkWord中去判断一下是否有偏向锁指向该线程的ID,不需要再进入Monitor去竞争对象了。
    当对象被当做同步锁并有一个线程抢到了锁时,锁标志位还是01,“是否偏向锁”标志位设置为1,并且记录抢到锁的线程ID,表示进入偏向锁状态。
    一旦出现其他线程竞争锁资源时,偏向锁就会被撤销。偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法,如果是,则升级锁,反之则被其他线程抢占。

  • 轻量级锁
    当有另外一个线程竞争获取偏向锁时,由于该锁已经是偏向锁,当发现对象头MarkWord中的线程ID不是自己线程的ID时,就会进行CAS操作获取锁,如果获取成功,直接替换MarkWord中的线程ID,该锁继续保持偏向锁;如果获取失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。
    轻量级锁适用于线程交替执行同步块的场景,绝大部分的锁在整个同步周期内都不存在长时间的竞争

  • 自旋锁与重量级锁
    轻量级锁CAS抢锁失败,线程会被挂起进入阻塞状态。如果正在持有锁的线程在很短的时间内释放锁资源,那么进入阻塞状态的线程无疑又要申请锁资源。
    自旋锁,就是一直不断的尝试,从而避免线程被挂起阻塞。一只长时间的占用CPU,自旋次数有JVM参数设置。
    在锁竞争不激烈且锁占用时间非常短的场景下,自旋锁可以提高系统性能。
    自旋锁重试失败后,同步锁就会升级至重量级锁。
    锁标志位改为10。在这个状态下,未抢到锁的线程都会进入Monitor,之后会被阻塞在_WaitSet队列中。

5.1.2 优化方法

减少锁竞争,是优化Synchronized同步锁的关键

  • 尽量使Synchronized同步锁处于轻量级锁或偏向锁状态,这样可以提高Synchronized同步锁性能
  • 减小锁粒度来降低锁竞争
  • 减少锁持有时间,避免锁升级
    Synchronized同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁
5.2 Lock 原理及优化
lock.jpg

从性能方面上来说,在并发量不高、竞争不激烈的情况下,Synchronized同步锁由于具有分级锁的优势,性能上与Lock锁差不多;但在高负载、高并发的情况下,Synchronized同步锁由于竞争激烈会升级到重量级锁,性能则没有Lock锁稳定。

5.2.1 Lock 原理

Lock锁是基于Java实现的锁,Lock是一个接口类,常用的实现类有ReentrantLock、ReentrantReadWriteLock,它们都是依赖AbstractQueuedSynchronizer(AQS)类实现的。
AQS类结构中包含一个基于链表实现的等待队列(CLH队列),用于存储所有阻塞的线程,AQS中还有一个state变量,该变量对ReentrantLock来说表示加锁状态。

5.2.2 Lock优化
  • 读写分离
  • StampedLock实现了乐观读锁、悲观读锁和写锁
    相比于RRW,StampedLock获取读锁只是使用与或操作进行检验,不涉及CAS操作,即使第一次乐观锁获取失败,也会马上升级至悲观锁,这样就可以避免一直进行CAS操作带来的CPU占用性能的问题,因此StampedLock的效率更高
5.2.3 合理设置线程池的线程数

线程数 =N(CPU 核数)*(1+WT(线程等待时间)/ST(线程时间运行时间))
具体情况具体分析,根据公式计算出一个大概的数值,再通过实际的性能测试,计算出一个合理的值

相关文章

网友评论

      本文标题:第二章 java程序优化

      本文链接:https://www.haomeiwen.com/subject/cngkqftx.html