JVM内核学习笔记

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lchpersonal521/article/details/82531279

一、补充知识

  • 补码: 正数的补码是其本身,负数的补码是反码加1

    例如:-6

    原码:10000110
    反码:11111001
    补码:11111010
    
  • 为什么要有补码的存在?

    1. 无歧义的表示零:0; 0既不属于正数,也不属于负数

      用正数表示0:
      
          源码: 00000000
          反码: 01111111
          补码: 00000000
      
      用负数表示0:
      
          源码: 10000000
          反码: 11111111
          补码: 00000000
      
    2. 方便计算机计算,补码相加符号位可以直接参与运算

二、JVM运行机制

1. JVM启动流程

这里写图片描述

说明:

  • 装在配置:根据当前路径和系统版本寻找jvm.cfg
  • JVM.dll为JVM主要实现
  • JNIEnv为JVM接口,findClass等操作通过它实现

2. JVM基本结构

  • 运行时数据区:

    1. 方法区;

      • 保存装载的类信息
      • -类型的常量池
      • 字段,方法信息
      • 方法字节码
      • 通常和永久区(Perm)关联在一起
      • PS: JDK6时,String等常量信息置于方法区,JDK7时,已经移动到了堆,JDK8移除永久代,增加了元空间MetaSpace

        1. 本地方法栈;

        2. 堆是线程共享的,所有通过new关键字生成的对象,都在堆中。java GC主要针对的区域就是堆,GC算法跟堆算法密切相关,例如分代收集算法要求堆,必须是分代存放数据的。
        3. 线程私有

          • 栈由一系列帧组成(因此Java栈也叫做帧栈)
          • 帧保存一个方法的局部变量、操作数栈、常量池指针
          • 每一次方法调用创建一个帧,并压栈
        4. 栈上分配:

          1. 小对象(一般几十个bytes),在没有逃逸的情况下,可直接分配在栈上
          2. 直接分配栈上,可以自动回收,减轻GC压力(一个函数调用结束,栈帧自动回收)
          3. 大对象或者逃逸对象无法栈上分配
        5. PC寄存器

        6. 每个线程拥有一个PC寄存器
        7. 在线程创建时 创建
        8. 指向下一条指令的地址
        9. 执行本地方法时,PC的值为undefined

3. 内存模型

  1. 每一个线程有一个工作内存和主存独立
  2. 工作内存存放主存中变量的值的拷贝
  3. 当数据从主内存复制到工作存储时,必须出现两个动作

    1. 第一:由主内存执行的读(read)操作;
    2. 第二,由工作内存执行的相应的load操作;当数据从工作内存拷贝到主内存时,也出现两个操作:

      1. 第一个,由工作内存执行的存储(store)操作;
      2. 第二个,由主内存执行的相应的写(write)操作。每一个操作都是原子的,即执行期间不会被中断,对于普通变量,一个线程中更新的值,不能马上反应在其他变量中,如果需要在其他线程中立即可见,需要使用 volatile 关键字。
    3. 如下图所示:
      这里写图片描述

  4. 可见性

    1. 一个线程修改了变量,其他线程可以立即知道
    2. 保证可见性的方法
    3. volatile
    4. synchronized (unlock之前,写变量值回主存)
    5. final(一旦初始化完成,其他线程就可见)
  5. 有序性

    1. 在本线程内,操作都是有序的
    2. 在线程外观察,操作都是无序的。(指令重排 或 主内存同步延时)
  6. 指令重排

    1. 线程内串行语义
      • 写后读 a = 1;b = a; 写一个变量之后,再读这个位置。
      • 写后写 a = 1;a = 2; 写一个变量之后,再写这个变量。
      • 读后写 a = b;b = 1; 读一个变量之后,再写这个变量。
      • 以上语句不可重排
      • 编译器不考虑多线程间的语义
      • 可重排: a=1;b=2;
    2. 反例:
      这里写图片描述
      这里写图片描述
    3. 指令重排的基本原则
      1. 程序顺序原则:一个线程内保证语义的串行性
      2. volatile规则:volatile变量的写,先发生于读
      3. 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
      4. 传递性:A先于B,B先于C 那么A必然先于C
      5. 线程的start方法先于它的每一个动作
      6. 线程的所有操作先于线程的终结(Thread.join())
      7. 线程的中断(interrupt())先于被中断线程的代码
      8. 对象的构造函数执行结束先于finalize()方法

4.字节码运行的两种方式 - 编译和解释运行

  1. 解释运行
    1. 解释执行以解释方式运行字节码
    2. 解释执行的意思是:读一句执行一句
  2. 编译运行(JIT– just in time)
    1. 将字节码编译成机器码
    2. 直接执行机器码
    3. 运行时编译
    4. 编译后性能有数量级的提升
  3. java代码->字节码->机器码
  4. 4.

常用JVM配置参数

Trace跟踪参数

  1. 打印GC的简要信息 -verbose:gc-XX:+printGC
    输出如下:

    [GC 4790K->374K(15872K), 0.0001606 secs]
    [GC 4790K->374K(15872K), 0.0001474 secs]
    [GC 4790K->374K(15872K), 0.0001563 secs]
    [GC 4790K->374K(15872K), 0.0001682 secs]

  2. 打印GC详细信息 : -XX:+PrintGCDetails
    打印CG发生的时间戳: -XX:+PrintGCTimeStamps

    例:

    [GC[DefNew: 4416K->0K(4928K), 0.0001897 secs] 4790K->374K(15872K), 0.0002232 secs]
    [Times: user=0.00 sys=0.00, real=0.00 secs] 

    PrintGCDetails的输出

    def new generation   total 13824K, used 11223K [0x27e80000, 0x28d80000, 0x28d80000)
    eden space 12288K,  91% used [0x27e80000, 0x28975f20, 0x28a80000)
    from space 1536K,   0% used [0x28a80000, 0x28a80000, 0x28c00000)
    to   space 1536K,   0% used [0x28c00000, 0x28c00000, 0x28d80000)
    tenured generation   total 5120K, used 0K [0x28d80000, 0x29280000, 0x34680000)
    the space 5120K,   0% used [0x28d80000, 0x28d80000, 0x28d80200, 0x29280000)
    compacting perm gen  total 12288K, used 142K [0x34680000, 0x35280000, 0x38680000)
    the space 12288K,   1% used [0x34680000, 0x346a3a90, 0x346a3c00, 0x35280000)
    ro space 10240K,  44% used [0x38680000, 0x38af73f0, 0x38af7400, 0x39080000)
    rw space 12288K,  52% used [0x39080000, 0x396cdd28, 0x396cde00, 0x39c80000)
    
  3. -Xloggc:log/gc.log

    • 指定GC log的位置,以文件输出
    • 帮助开发人员分析问题
  4. -XX:+PrintHeapAtGC

    • 每次一次GC后,都打印堆信息
  5. -XX:+TraceClassLoading
    监控类的加载

    [Loaded java.lang.Object from shared objects file]
    [Loaded java.io.Serializable from shared objects file]
    [Loaded java.lang.Comparable from shared objects file]
    [Loaded java.lang.CharSequence from shared objects file]
    [Loaded java.lang.String from shared objects file]
    [Loaded java.lang.reflect.GenericDeclaration from shared objects file]
    [Loaded java.lang.reflect.Type from shared objects file]
  6. -XX:+PrintClassHistogram

    • 按下Ctrl+Break后,打印类的信息:
    • 分别显示:序号、实例数量、总大小、类型
num instances bytes class name
1: 890617 470266000 [B
2: 890643 21375432 java.util.HashMap$Node
3: 890608 14249728 java.lang.Long
4: 13 8389712 [Ljava.util.HashMap$Node;
5: 2062 371680 [C
6: 463 41904 java.lang.Class

堆的分配参数

  1. -Xmx –Xms : 指定最大堆和最小堆

    • : -Xmx20m -Xms5m :

      /**代码*/
      System.out.print("Xmx=");
      System.out.println(Runtime.getRuntime().maxMemory()/1024.0/1024+"M");
      
      System.out.print("free mem=");
      System.out.println(Runtime.getRuntime().freeMemory()/1024.0/1024+"M");
      
      System.out.print("total mem=");
      System.out.println(Runtime.getRuntime().totalMemory()/1024.0/1024+"M");
      
      /**输出结果*/
      Xmx=19.375M
      free mem=4.342750549316406M
      total mem=4.875M
    • Java会尽可能维持在最小堆,所以free mem和total mem的值会随着内存使用的变化而变化

  2. -Xmn :设置新生代大小

  3. -XX:NewRatio
    • 新生代(eden+2*s)和老年代(不包含永久区)的比值
    • 4 表示 新生代:老年代=1:4,即年轻代占堆的1/5
  4. -XX:SurvivorRatio
    • 设置两个Survivor区和eden的比
    • 8表示 两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10
  5. -XX:+HeapDumpOnOutOfMemoryError
    • OOM时导出堆到文件
  6. -XX:+HeapDumpPath
    • 导出OOM的路径
  7. -XX:OnOutOfMemoryError

    • 在OOM时,执行一个脚本”-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p”
    • 当程序OOM时,在D:/a.txt中将会生成线程的dump
    • 可以在OOM时,发送邮件,甚至是重启程序
  8. -XX:PermSize -XX:MaxPermSize

    • 设置永久区的初始空间和最大空间
    • 他们表示,一个系统可以容纳多少个类型
    • 使用CGLIB等库的时候,可能会产生大量的类,这些类,有可能撑爆永久区导致OOM
根据实际事情调整新生代和幸存代的大小
官方推荐新生代占堆的3/8
幸存代占新生代的1/10
在OOM时,记得Dump出堆,确保可以排查现场问题

栈的分配参数

  1. -Xss
    • 通常只有几百K
    • 决定了函数调用的深度
    • 每个线程都有独立的栈空间
    • 局部变量、参数 分配在栈上

GC 算法与种类

GC的对象是堆空间和永久区

GC的算法

  1. 引用计数法

    • 老牌垃圾回收算法
    • 通过引用计算来回收垃圾
    • 引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。
    • 引用计数法的问题:
      • 引用和去引用伴随加法和减法,影响性能
      • 很难处理循环引用
    • 使用者:
      • COM
      • ActionScript3
      • Python
  2. 标记清除
    标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象
    这里写图片描述

  3. 标记压缩
    标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。
    这里写图片描述
  4. 复制算法
    • 与标记-清除算法相比,复制算法是一种相对高效的回收方法
    • 不适用于存活对象较多的场合 如老年代
    • 将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收
      这里写图片描述
      复制算法的最大问题是:空间浪费 整合标记清理思想
  5. 复制算法优化【分代收集思想】
    这里写图片描述

依据对象的存活周期进行分类,短命对象归为新生代,长命对象归为老年代。
根据不同代的特点,选取合适的收集算法:

  • 少量对象存活,适合复制算法
  • 大量对象存活,适合标记清理或者标记压缩

可触及性

  1. 可触及的
    • 从根节点可以触及到这个对象
  2. 可复活的
    • 一旦所有引用被释放,就是可复活状态
    • 因为在finalize()中可能复活该对象
  3. 不可触及的

    • 在finalize()后,可能会进入不可触及状态
    • 不可触及的对象不可能复活
    • 可以回收
  4. 经验

    • 避免使用finalize(),操作不慎可能导致错误。
    • 优先级低,何时被调用, 不确定
    • 何时发生GC不确定
    • 可以使用try-catch-finally来替代它

    • 栈中引用的对象
    • 方法区中静态成员或者常量引用的对象(全局对象)
    • JNI方法栈中引用对象

Stop-The-World

  • Java中一种全局暂停的现象
  • 全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
  • 多半由于GC引起
    1. Dump线程
    2. 死锁检查
    3. 堆Dump
  • GC时为什么会有全局停顿?
    类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。
  • 危害
    • 长时间服务停止,没有响应
    • 遇到HA系统,可能引起主备切换,严重危害生产环境。

GC参数

串行收集器

最古老,最稳定
效率高
可能会产生较长的停顿
-XX:+UseSerialGC
新生代、老年代使用串行回收
新生代复制算法
老年代标记-压缩

并行收集器

ParNew
-XX:+UseParNewGC
新生代并行
老年代串行
Serial收集器新生代的并行版本
复制算法
多线程,需要多核支持
-XX:ParallelGCThreads 限制线程数量

Parallel收集器
类似ParNew
新生代复制算法
老年代 标记-压缩
更加关注吞吐量
-XX:+UseParallelGC
使用Parallel收集器+ 老年代串行
-XX:+UseParallelOldGC
使用Parallel收集器+ 并行老年代

-XX:MaxGCPauseMills
最大停顿时间,单位毫秒
GC尽力保证回收时间不超过设定值
-XX:GCTimeRatio
0-100的取值范围
垃圾收集时间占总时间的比
默认99,即最大允许1%时间做GC
这两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优

CMS收集器

CMS收集器
Concurrent Mark Sweep 并发标记清除
标记-清除算法
与标记-压缩相比
并发阶段会降低吞吐量
老年代收集器(新生代使用ParNew)
-XX:+UseConcMarkSweepGC

CMS运行过程比较复杂,着重实现了标记的过程,可分为
初始标记
根可以直接关联到的对象
速度快
并发标记(和用户线程一起)
主要标记过程,标记全部对象
重新标记
由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
并发清除(和用户线程一起)
基于标记结果,直接清理对象

特点
尽可能降低停顿
会影响系统整体吞吐量和性能
比如,在用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半
清理不彻底
因为在清理阶段,用户线程还在运行,会产生新的垃圾,无法清理
因为和用户线程一起运行,不能在空间快满时再清理
-XX:CMSInitiatingOccupancyFraction设置触发GC的阈值
如果不幸内存预留空间不够,就会引起concurrent mode failure

有关碎片

标记-清除和标记-压缩

-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次整理
整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction
设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads
设定CMS的线程数量

GC参数整理

-XX:+UseSerialGC:在新生代和老年代使用串行收集器
-XX:SurvivorRatio:设置eden区大小和survivior区大小的比例
-XX:NewRatio:新生代和老年代的比
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParallelGC :新生代使用并行回收收集器
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:ParallelGCThreads:设置用于垃圾回收的线程数
-XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用CMS+串行收集器
-XX:ParallelCMSThreads:设定CMS的线程数量
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
-XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一百分比时,启动CMS回收
-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收

类加载器

class装载验证流程

  • 加载

    • 装载类的第一个阶段
    • 取得类的二进制流
    • 转为方法区数据结构
    • 在Java堆中生成对应的java.lang.Class对象
  • 链接

    • 验证

      • 目的:保证Class流的格式是正确的
        • 文件格式的验证
          • 是否以0xCAFEBABE开头
          • 版本号是否合理
        • 元数据验证
          • 是否有父类
          • 继承了final类?
          • 非抽象类实现了所有的抽象方法
        • 字节码验证 (很复杂)
          • 运行检查
          • 栈数据类型和操作码数据参数吻合
          • 跳转指令指定到合理的位置
        • 符号引用验证
          • 常量池中描述类是否存在
          • 访问的方法或字段是否存在且有足够的权限
    • 准备

    • 分配内存,并为类设置初始值 (方法区中)

      • public static int v=1;
      • 在准备阶段中,v会被设置为0
      • 在初始化的中才会被设置为1
      • 对于static final类型,在准备阶段就会被赋上正确的值
      • public static final int v=1;
    • 解析

      • 符号引用替换为直接引用
  • 初始化
    • 执行类构造器
      • static变量 赋值语句
      • static{}语句
      • 子类的调用前保证父类的被调用
      • 是线程安全的

什么是类装载器ClassLoader

  • 概念
    • ClassLoader是一个抽象类
    • ClassLoader的实例将读入Java字节码将类装载到JVM中
    • ClassLoader可以定制,满足不同的字节码流获取方式
    • ClassLoader负责类装载过程中的加载阶段

JDK中ClassLoader默认设计模式

  • ClassLoader的重要方法
/** 载入并返回一个Class*/
public Class<?> loadClass(String name) throws ClassNotFoundException;
/** 定义一个类,不公开调用*/
protected final Class<?> defineClass(byte[] b, int off, int len);
/**回调该方法,自定义ClassLoader的推荐做法*/
protected Class<?> findClass(String name) throws ClassNotFoundException loadClass;
/** 寻找已经加载的类*/
protected final Class<?> findLoadedClass(String name);
  • 分类
    • BootStrap ClassLoader (启动ClassLoader)[rt.jar /-Xbootclasspath]
    • Extension ClassLoader (扩展ClassLoader)[%JAVA_HOME%/lib/ext/*.jar]
    • App ClassLoader (应用ClassLoader/系统ClassLoader)[Classpath下]
    • Custom ClassLoader(自定义ClassLoader) [完全自定义路径]
    • 每个ClassLoader都有一个Parent作为父亲
  • 问题
    • 顶层ClassLoader,无法加载底层ClassLoader的类
    • Java框架(rt.jar)如何加载应用的类?
    • javax.xml.parsers包中定义了xml解析的类接口,Service Provider Interface SPI位于rt.jar ,即接口在启动ClassLoader中。而SPI的实现类,在AppLoader。
  • 解决
    • Thread. setContextClassLoader()
      • 上下文加载器
      • 是一个角色
      • 用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题
      • 基本思想是,在顶层ClassLoader中,传入底层ClassLoader的实例

打破常规模式

  • 双亲模式的破坏
    • 双亲模式是默认的模式,但不是必须这么做
    • Tomcat的WebappClassLoader 就会先加载自己的Class,找不到再委托parent
    • OSGi的ClassLoader形成网状结构,根据需要自由加载Class

热替换

  • 类被加载后,无需重启服务器,新加载的类即可直接使用

性能监控工具

  • 系统性能监控

    • 确定系统运行的整体状态,基本定位问题所在
      • uptime
        • 系统时间
        • 运行时间
        • 连接数
        • 1,5,15分钟内的系统平均负载
      • top
      • vmstat
      • pidstat
  • Java自带的工具

    • jps
      • 列出java进程,类似于ps命令
      • 参数-q可以指定jps只输出进程ID ,不输出类的短名称
      • 参数-m可以用于输出传递给Java进程(主函数)的参数
      • 参数-l可以用于输出主函数的完整路径
      • 参数-v可以显示传递给JVM的参数
    • jinfo
      • 可以用来查看正在运行的Java应用程序的扩展参数,甚至支持在运行时,修改部分参数
      • -flag :打印指定JVM的参数值
      • -flag [+|-]:设置指定JVM参数的布尔值
      • -flag =:设置指定JVM参数的值
    • jmap
      • 生成Java应用程序的堆快照和对象的统计信息
      • jmap -histo 2972 >c:\s.txt
      • Dump堆
        • jmap -dump:format=b,file=c:\heap.hprof 2972
    • jstack

      • 打印线程dump
      • -l 打印锁信息
      • -m 打印java和native的帧信息
      • -F 强制dump,当jstack没有响应时使用
    • JConsole

      • 图形化监控工具
      • 可以查看Java应用程序的运行概况,监控堆信息、永久区使用情况、类加载情况等
    • Visual VM
      • Visual VM是一个功能强大的多合一故障诊断和性能监控的可视化工具

Java堆分析

内存溢出(OOM)的原因

  • 可能发生内存溢出的内存空间
    • 永久区
      • 用cglib生成大量的类,类的元信息是保存在永久区的
    • 线程栈
      • 这里的栈溢出指,在创建线程的时候,需要为线程分配栈空间,这个栈空间是向操作系统请求的,如果操作系统无法给出足够的空间,就会抛出OOM
    • 直接内存
      • ByteBuffer.allocateDirect()无法从操作系统获得足够的空间

线程安全

  • 多线程网站统计访问人数
  • 使用锁,维护计数器的串行访问与安全性
  • 多线程访问ArrayList[两个线程同时往ArrayList中添加百万元素,可能会发生越界异常]

对象头Mark

  • Mark Word,对象头的标记,32位
  • 描述对象的hash、锁信息,垃圾回收标记,年龄
    • 指向锁记录的指针
    • 指向monitor的指针
    • GC标记
    • 偏向锁线程ID

偏向锁

  • 大部分情况是没有竞争的,所以可以通过偏向来提高性能
  • 所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程
  • 将对象头Mark的标记设置为偏向,并将线程ID写入对象头Mark
  • 只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步
  • 当其他线程请求相同的锁时,偏向模式结束
  • -XX:+UseBiasedLocking
    • 默认启用
  • 在竞争激烈的场合,偏向锁会增加系统负担
  • -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0-XX:-UseBiasedLocking

轻量级锁

  • BasicObjectLock
    • 嵌入在线程栈中的对象
  • 普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。
  • 如果对象没有被锁定
    • 将对象头的Mark指针保存到锁对象中
    • 将对象头设置为指向锁的指针(在线程栈空间中)
  • 如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁)
  • 在没有锁竞争的前提下,减少传统锁使用OS互斥量产生的性能损耗
  • 在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降

自旋锁

  • 当竞争存在时,如果线程可以很快获得锁,那么可以不在OS层挂起线程,让线程做几个空操作(自旋)
  • JDK1.6中-XX:+UseSpinning开启
  • JDK1.7中,去掉此参数,改为内置实现
  • 如果同步块很长,自旋失败,会降低系统性能
  • 如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能

几种锁的总结

  • 不是Java语言层面的锁优化方法
  • 内置于JVM中的获取锁的优化方法和获取锁的步骤
  • 偏向锁可用会先尝试偏向锁
  • 轻量级锁可用会先尝试轻量级锁
  • 以上都失败,尝试自旋锁
  • 再失败,尝试普通锁,使用OS互斥量在操作系统层挂起

减少锁持有时间

public synchronized void syncMethod(){
    othercode1();
    mutextMethod();
    othercode2();
}

改为:

public void syncMethod2(){
    othercode1();
    synchronized(this){
        mutextMethod();
    }
    othercode2();
}

减小锁粒度

  • 将大对象,拆成小对象,大大增加并行度,降低锁竞争
  • 偏向锁,轻量级锁成功率提高
  • ConcurrentHashMap
  • HashMap的同步实现
    • Collections.synchronizedMap(Map

锁分离

  • 根据功能进行锁分离
  • ReadWriteLock
  • 读多写少的情况,可以提高性能
类型 读锁 写锁
读锁 可访问 不可访问
写锁 不可访问 不可访问
  • 读写分离思想可以延伸,只要操作互不影响,锁就可以分离
  • LinkedBlockingQueue
    • 队列
    • 链表

锁粗化

  • 通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化
for(int i=0;i<CIRCLE;i++){
    synchronized(lock){

    }
}

改为:

synchronized(lock){
for(int i=0;i<CIRCLE;i++){

    }
}

锁消除

  • 在即时编译器时,如果发现不可能被共享的对象,则可以消除这些对象的锁操作
  • 例如StringBuffer为局部变量时,其内部的锁实现其实是不需要的
  • 参数
    • -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks 锁消除
    • -server -XX:+DoEscapeAnalysis -XX:-EliminateLocks 不使用锁消除

无锁

  • 锁是悲观的操作
  • 锁是乐观的操作
  • 无锁的一种实现方式
    • CAS(Compare And Swap)
    • 非阻塞的同步
    • CAS(V,E,N)
  • 在应用层面判断多线程的干扰,如果有干扰,则通知线程重试
  • java.util.concurrent.atomic.AtomicInteger
public final int getAndSet(int newValue) {
    for (;;) {
        int current = get();
        if (compareAndSet(current, newValue))
            return current;
    }
}

猜你喜欢

转载自blog.csdn.net/lchpersonal521/article/details/82531279