《深入理解java虚拟机》笔记

====================================

第二部分 自动内存管理机制

====================================

第2章 Java内存区域与内存溢出异常

1.运行时数据区域

  • 程序计数器 [线程独立]
  • 虚拟机栈、本地方法栈(有的虚拟机是合二为一) [线程独立]
  • 元数据区(类、方法、静态变量、常量) jdk1.7及之前称为方法区
  • 堆区
  • 直接内存

2.HotSpot虚拟机对象揭秘

对象创建过程:类加载检查 -> 分配内存 -> 初始化零值 -> 设置对象头 -> 执行<init>方法

创建对象的【分配方式】

  • 指针碰撞(有压缩整理功能的jvm)
  • 空闲列表

创建对象的【并发控制】

  • CAS配上失败重试保证原子性
  • 线程预先分配一块内存(线程本地分配缓冲TLAB) -XX:+/-UseTLAB

对象的内存布局

  • 对象头(哈希码、分代年龄、锁标记、GC标记等)
  • 实例数据
  • 对齐填充

对象的访问定位

  • 句柄引用(Java堆中划分一块作为句柄池,vmstack中的指针变量 -> 句柄池 -> 实例数据、类型数据)
  • 直接引用(vmstack中的指针变量 -> 实例数据 -> 类型数据(所以有的vm实例对象头中有类型指针、使用句柄引用则没有))

3.OutOfMemoryError异常

除程序计数器,vm其它几个运行时区域都会发生OOM

  • 堆:-Xms20m -Xmx20m(将最小、最大设置一样可避免堆自动扩展)
  • 栈:-Xss128k(单个栈容量设置,超过调用深度通常抛出StackOverflowError)
  • 栈:所有栈内存 约= 系统内存 - 堆内存Xmx - 方法区内存MaxPermSize - (其它忽略不计)【所以每个线程的Xss越大,创建线程时越容易OOM,即减少最大堆Xmx和减少栈容量Xss可换取更多的线程量】
  • 方法区:-XX:PermSize=10M -XX:MaxPermSize=10M ,动态产生类过多,jdk1.8没有方法区而是元数据区 -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=20M
  • 运行时常量:JDK1.7开始逐渐去永久代(String.intern()不再复制实例,而是存储首次出现实例的引用)
  • 直接内存:-XX:MaxDirectMemorySize=10M

第3章 垃圾收集器与内存分配策略

1.存活判断

  • 引用计数算法:很难解决对象相互循环引用的问题
  • 可达性分析算法:通过GCRoots向下搜索引用链(GCRoots:虚拟机栈中的引用对象、方法区类静态属性/常量引用对象、本地栈JNI引用对象)
  • 回收两步走:1可达性分析无GCRoots引用链相连;2是否有必要执行finalize(有覆盖该函数且未执行过)。避免使用finalize,代价高昂、不确定性大。
  • 回收方法区:回收性价比低、规范中未要求。1该类实例都被回收 && 2该类的classloader被回收 && 3该类的Class对象没有地方引用(用户自定义加载器可以被GC,所以用自定义类加载器加载的类,可以被卸载,而3个系统类加载器不会卸载,它们加载的系统类也不会被卸载)

2.引用类型

  • StrongReference强引用:只要引用存在,永不回收;
  • SoftReference软引用:OOM前会将软引用进行二次回收(本地缓存);
  • WeakReference弱引用:下一次GC回收(线程中的ThreadLocalMap-Entry的key(ThreadLocal)、util.logging.LogManager.addLogger只保留弱引用、mybatis中的WeakCache);
  • PhantomReference虚引用:当析构函数用来回收对象持有的资源,比如回收对象持有的IO;

ReferenceQueue引用队列:当程序需要在一个对象的可达性发生变化时得到通知,引用队列可以用来收集这些信息。即在创建软、弱、虚引用时,可以为其关联一个引用队列,当所引用对象被GC回收时,java虚拟机就会将该弱、软、幽灵引用添加到引用队列中。

3.垃圾收集算法

  • 标记-清除算法:效率不高、产生大量内存碎片;
  • 复制算法:(实现简单)部分内存浪费,比如等分两块时直接浪费一半内存;目前新生代使用1Eden80%+2Survivor10%(IBM调研发现新生代98%朝生夕灭);
  • 标记-整理算法:存活对象向一端移动,然后清理掉边界外内存;
  • 分代收集算法:新生代使用复制、老年代使用标记整理或标记清除;

4.HotSpot算法实现

  • 枚举跟节点:通过OopMap/GCmap(不同jvm实现和名称不一样)直接得知哪些地方存放着对象引用,快速定位GCRoot。1.对象的类型信息里有记录自己的OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据;2.每个方法可能会有好几个oopMap,就是把一个方法的代码分成几段,每一段代码一个oopMap,作用域自然也仅限于这一段代码。
  • SafePoint安全点:特定位置才会记录OopMap,避免每条指令都生成它导致大量空间浪费。比如方法调用、循环跳转、异常跳转等地。若发生GC,线程执行到最近一个安全点就会暂停下来,等待所有线程暂停后开始GC(主动式)。
  • SafeRegoin安全域:线程不执行的时候,如sleep、blocked等,安全域中引用不会发生变化,GC是安全的。
  • 记忆集RememberedSet:解决GC时对象跨分代/region引用情况(1.每个region或分代维护了一个RemenberedSet,2.在引用类型赋值的时候,会产生写屏障检查引用对象是否处于不同region,如果是便把相关引用信息记录到被引用对象的RememberedSet中,3.当GC时,跟节点枚举范围会加上RememberedSet来保证不用全堆扫描)
  • 卡表CARD_TABLE(卡表就是记忆集的一种具体实现,就是HashMap与Map的关系):CARD_TABLE中的元素标志的是内存区域中的一块内存,这一个内存被称为卡页(可以用字长/对象/卡页等精度,前两者维护成本高)。一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为0,称为这个元素变脏(Dirty),没有则标识为-1。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。聊聊虚拟机的垃圾回收算法细节问题-根节点枚举、安全点、安全区、记忆集与卡表、写屏障、并发可达性分析中的三色标记法_J3-西行的博客-CSDN博客 
  • 写屏障:有其他分代区域中对象引用了本区域对象时,其对应的卡表元素就应该变脏,变脏时间点原则上应该发生在引用类型字段赋值的那一刻。在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的,在引用对象赋值时会产生一个通知(post_write_barrier)。除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享”(False Sharing)问题。假设处理器的缓存行大小为64字节,由于一个卡表元素占1个字节,64个卡表元素将共享同一个缓存行。这64个卡表元素对应的卡页总的内存为32KB(64×512字节),也就是说如果不同线程更新的对象正好处于这32KB的内存区域内,就会导致更新卡表时正好写入同一个缓存行而影响性能。为了避免伪共享问题,一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表元素未被标记过时才将其标记为变脏。在JDK 7之后,HotSpot虚拟机增加了一个新的参数-XX:+UseCondCardMark,用来决定是否开启卡表更新的条件判断。开启会增加一次额外判断的开销,但能够避免伪共享问题,两者各有性能损耗,是否打开要根据应用实际运行情况来进行测试权衡。
  • 并发的可达性分析:三色标记,并发扫描产生 浮动垃圾和对象消失情况(指向白色对象的灰色对象断开引用 且一个黑色对象指向这个白色对象,则这个对象会被收集造成对象消失)。为了解决这个问题CMS使用增量更新(写后屏障记录并发时新增的引用关系),而G1使用的是原始快照(写前屏障记录并发时即将删除的引用关系)

5.垃圾收集器

  • Serial/SerialOld:串行单线程,适合Client模式下的虚拟机。
  • ParNew:并行,Serial的多线程版本(除了多线程其它行为与Serial一样)。
  • ParallelScavenge/ParallelOld:(注重指令吞吐量、和cpu敏感的场合都可以优先考虑)
  • ParNew/CMS:ConcurrentMarkSweep并行标记清除,以获得最短时间停顿为目标。初始标记/1-并发标记/1-重新标记/n-并发清除/1(缺点:1并发占用一部分线程资源,会导致应用程序变慢;2并发清理导致浮动垃圾;3使用“标记-清除”算法会导致收集结束时产生大量空间碎片);
  • G1:(初始化标记/1-并发标记/1-最终标记/n-筛选回收/n)优势:1分代使用多个region,后台维护了一个优先列表跟踪每个region垃圾堆积的价值,每次收集根据允许的时间优先回收价值最大的region(这也就是它的名字 Garbage-First 的由来);2局部看region是复制算法/整体看是标记整理,所以不会产生内存碎片;3可预测的停顿时间,能让使用者明确指定在一个长度为 M 毫秒的时间段内垃圾回收消耗不得超过N毫秒。
  • ZGC:(初始标记-并发标记-再标记(若超过1ms则再次回到并发标记)-初始转移-并发转移)适用于大内存&停顿时间不超过10ms,与ZGC对比,G1的转移阶段完全STW,且停顿时间随存活对象的大小增加而增加。( ZGC通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。大致原理如下:应用线程访问对象将触发“读屏障”(读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。需要注意的是,仅“从堆中读取对象引用”才会触发这段代码),如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。那么,JVM是如何判断对象被移动过呢?就是利用对象引用的地址,即着色指针(着色指针是一种将信息存储在指针中的技术,ZGC仅支持64位系统,指针为64位,ZGC将对象存活信息存储在42~45位中,这与传统的垃圾回收并将对象存活信息放在对象头中完全不同)。新一代垃圾回收器ZGC的探索与实践 - 美团技术团队  )
  • 补充:Java与Go的GC对比 百度安全验证

6.内存分配策略与回收策略

部分收集 (Partial GC):

  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
  • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集-CMS,需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集-G1。

整堆收集 (Full GC):收集整个 Java 堆和方法区(Young/Old/Perm)。

  • 分配区域:主要分配在新生代Eden区、线程本地缓冲TLAB、少数直接分配到老年代等;
  • 对象优先在Eden分配:Eden空间不足将发起一次新生代GC(MinorGC/YoungGC);
  • 大对象直接进入老年代:比如byte数组,比遇到大对象更糟的是遇到朝生夕灭的大对象;
  • 长期存活的对象将进入老年代:熬过一次MinorGC年龄增加1岁,达到一定程度(-XX:MaxTenuringThreshold=15默认15岁)会晋升到老年代;
  • 动态年龄判断进入老年代:如果在Survivor中相同年龄对象大小总和大于Survivor空间的一半,年龄大于等于它们的对象就可以直接进入老年代;
  • 空间分配担保:发生MinorGC前,1老年代最大连续空间是否大于新生代所有对象空间(是安全的则进行MinorGC),2若不成立则查看参数是否允许担保失败,允许担保失败则继续查看老年代最大连续空间是否大于历次新生代晋升的平均大小,3大于则进行一次冒险的MinorGC,4不大于或不允许担保失败则直接进行FullGC。【JDK 6 Update 24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC】

====================================

第三部分 虚拟机执行子系统

====================================

第6章 类文件结构

1.Class类文件结构

  • 魔数 u4
  • 文件版本 u2+u2
  • 常量数量u2+常量表(存放字面量和符号引用:如类/接口全限定名、字段的名称描述符、方法的名称描述符)
  • 访问标志 u2(public、final、super、interface、abstract、synthetic非用户代码、enum、annotation)
  • 类索引u2+父类索引u2
  • 接口数量u2+接口索引表
  • 字段数量u2+字段表
  • 方法数量u2+方法表
  • 属性数量u2+属性表(Code、ConstantValue、Exceptions、Deprecated、LineNumberTable、SourceFile/name等)

2.字节码指令(一个字节存放指令,所以最多256个指令)

P197指令列表

第7章 虚拟机类加载机制

加载-验证-准备-解析-初始化-使用-卸载

1.类加载的【时机】

加载时机:jvm规范没有强制要求,不同虚拟机实现不同

初始化阶段:jvm规范严格规定<有且只有>5种情况,若类未初始化则必须立即进行初始化(而加载、验证等自然在这之前)

  • 遇到new、getstatic、putstatic、invokestatic这4条指令时;(对应new对象时、读写静态字段、调用类静态方法)
  • 使用java.lang.reflect包的方法对类进行反射调用时;
  • 当初始化一个类时,发现其父类没有初始化,则先触发父类的初始化;
  • 当虚拟机启动时,需要指定要执行的主类(包含main方法的类),虚拟机会先初始化这个主类;
  • 使用jdk1.7动态语言支持时,如果java.lang.invoke.MethodHandle解析的结果为REF_getStatic/REF_putStatic/REG_invokeStatic的方法句柄时;

以上5种场景称为类主动引用,除此之外所有引用类的方式(被动引用)不会触发初始化,如:

  • 通过子类访问父类static变量时,不会初始化子类
  • new数组类
  • 类静态字符常量

2.类加载的【过程】

1)加载(完成以下3件事,读取位置未限制,可以从网络、jar、war、zip等地方读取)

  • 通过一个类的全限定名来获取定义此类的二进制字节流;
  • 将字节流转换为方法区的运行时数据结构;
  • 内存中生成一个代表这个类的java.lang.Class对象;

数组类

  • 如果数组的组件类型是引用类型,就递归采用类加载过程加载这个组件类型(数组将在加载该组件类型的类加载器类命名空间中被标识)
  • 如果数组的组件类型不是引用类型(如int[]数组),将会把数组标记为引导类加载器关联;
  • 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类,那数组类的可见类型为public;

2)验证

  • 1文件格式验证(验证字节流是否符合Class文件规范:魔数是否对、主次版本是否支持、常量类型是否支持、指向常量的索引值是否正确、Class文件是否有删除或增加等),通过后才会进入方法区存储,后面3个验证都基于方法区存储的结构进行;
  • 2元数据验证(语义分析是否符合语言规范:是否有父类(除了Object都应该有父类)、是否继承了不允许继承的final类、不是抽象类是否实现了接口或父类的抽象方法、是否出现了不合规的重载等)
  • 3字节码验证(方法体进行语义合法、符合逻辑校验:操作栈类型与指令类型匹配、跳转指令不会跳转到方法体之外、不会进行错误的类型赋值等)
  • 4符号引用验证(在<解析>阶段执行,对类自身以外的信息进行匹配校验:符号引用中通过字符串描述的全限定名是否能找到对应的类、方法、字段,以及他们的访问性能否被当前类访问到等)

3)准备

  • 正式为类变量分配内存并设置类变量初始值(0值和final值,非final值在<clinit>中)

4)解析(将常量池内的符号引用替换为直接引用,如果有了直接引用,那引用的目标必定已经在内存中存在)

  • 类或接口解析
  • 字段解析
  • 类方法解析
  • 接口方法解析

5)初始化clinit(此时才真正开始执行类中定义的java代码)

  • <clinit>是编译期自动收集类变量赋值、静态语句块(static{})的语句合并产生的,若没有则不生成该方法(收集顺序就是源代码出现顺序,静态语句块只能访问在它之前定义的变量,在它之后定义的变量只能赋值不能访问!)
  • <clinit>与类的构造函数<init>不同,不需要显示调用父类构造器,虚拟机会保证子类的<clinit>执行前,父类<clinit>已执行完毕;
  • 接口也会生成<clinit>用于初始化赋值操作,但不需要先执行父接口的<clinit>,只有父接口中定义的变量使用时父接口才会初始化;
  • 虚拟机会保证一个类的<clinit>执行时在多线程情况下正确的加锁、同步,所以如果一个类的<clinit>很长,可能会导致多个线程阻塞;

3.类加载器

  • 即使是同一个Class,若被不同类加载器加载,也会被认为是不同的类。
  • 双亲委派parents delegate:如果一个类加载器收到类加载请求,它会先委派给父类加载(其实不是继承/使用的组合模式),每层都会如此,只有当父类加载器反馈找不到时,子类才会自己去加载。(它很好的保证基础类的统一问题,比如不同类加载器加载Object时都会委派到最顶层的启动类加载器去加载,保证他们使用的Object都是同一个类)
  • 破坏双亲委派(涉及SPI的框架(如JNDI、JDBC等)设置线程上下文类加载器、OSGi类加载器使用复杂的网状结构)。自定义加载器的话,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

类加载器

  • Bootstrap ClassLoader(启动类加载器,$JAVA_HPOME/lib目录或-Xbootclasspath参数指定的路径 且能被虚拟机识别的文件名,不能识别的即使放在这个目录也不会加载。请求委派是传null即可使用它),除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。
  • Extension ClassLoader(扩展类加载器,$JAVA_HOME/lib/ext目录或java.ext.dirs系统变量指定的目录中的所有类库),父指针是null/即启动类加载器。
  • Application ClassLoader(系统类加载器,默认的加载器,加载用户类路径ClassPath上所指定的类库,getSystemClassLoader())
  • 用户自定义加载器(可以被GC,所以用自定义类加载器加载的类可以被卸载,而3个系统类加载器不会卸载,它们加载的系统类也不会被卸载)

第8章 虚拟机字节码执行引擎

1.运行时栈帧结构

  • 局部变量表(非静态方法第0个变量槽/slot是this,对于64位类型long/double占用2个slot,局部变量表中slot是可以重用的,因为作用域不一定会覆盖整个方法体)
  • 操作栈(32位占一个栈容量、64占2个)
  • 动态链接(解析阶段就转换为直接引用的过程称为静态解析,不能转化的需要动态连接)
  • 返回地址
  • 其它附加信息:如调试信息,取决于虚拟机的实现

2.方法调用(invokestatic、invokespecial、invokevirtual、invokeinterface、invokedynamic)

解析(编译期就能确定方法,类加载解析阶段就会把符号引用替换为直接引用)

  • 只要是能被invokestatic和invokespecial指定调用的方法(静态方法、私有方法、实例构造器、父类方法)
  • invokevirtual调用的final方法

分派(多态特性)

  • 静态分派(方法重载——编译期根据静态类型确认调用的重载方法,如 Human h=new Man(); sayHello(Human)和sayHello(Man)会调用前者,Human是变量的静态类型)
  • 动态分派(方法重写——1找到this实际类型;2查找类中是否有调用方法的常量描述符-有则检查访问权限;3没找到则继续找父类的)
  • 单分派与多分派(Java语言是静态多分派(静态分派是有对象类型和参数类型两个宗量决定),动态单分派(java不支持动态类型,动态分派在编译期已确定参数的类型,仅与对象实际类型有关))
  • 虚拟机动态分派的实现(虚方法表vtable和接口方法表itable,在准备变量初始值时初始化)
  • 动态类型语言支持(使用java.lang.invoke包(MethodHandle方法句柄,模拟字节码层面的方法调用,reflect是模拟java代码层面的方法调用/包含的信息更多,前者更轻量化、性能更好);invokedynamic是给运行在jvm上的其它语言用的,同MethodHandle的设计目的一样是为了让用户可以自己进行动态方法分派)

====================================

第四部分 程序编译与代码优化

====================================

第10章 早期(编译期)优化

1.嵌入式注解处理器

定义自己的编译过程处理程序(如:命名规范检查、Hibernate中的标签正确性检查、Lombok代码生成)

2.java语法糖

  • 泛型(java用的伪泛型)——类型擦除
  • 自动装箱、拆箱——算术运算符时会自动拆箱(因为对象无法进行运算)、==一边是基础类型时另一边会拆箱、基础类型Integer/Long在-128~127之间有缓存(Integer上限可通过虚拟机参数配置)
  • 遍历循环实际使用的Iterable接口,所以要实现该接口的对象才能使用forearch

第11章 晚期(运行期)优化

1.解释器与编译器

1)现代虚拟机通常加入了编辑器来提升运行时效率

2)编译的对象

  • 被多次调用的方法
  • 被多次执行的循环体(所在的方法)

3)触发编译的条件

  • 基于采样的热点探测(周期性的检查栈顶方法)——简单、高效,但不精确和严谨/容易被阻塞情况误导
  • 基于计数器的热点探测(每个方法/甚至是代码块建立计数器,可配/1w+次触发)——麻烦,但精确严谨【Hotspot采用】

4)编译过程——次数触发后提交编译,本次继续解释执行,下次调用检查有本地代码版本则直接执行

2.编译优化技术

解释执行耗时除了解释字节码要额外耗时外,还因为代码的所有优化都集中在了即时编译器中。

  • 方法内联——去除调用成本(如建立栈帧),方法内联膨胀后便于后续能在更大范围优化。(invokespecial/invokestatic调用的私有方法、构造器、父类方法、静态方法、final方法才能在编译期内联,而其他方法java多态决定了默认为虚方法,需要运行时动态确认所以不能内联,但是虚拟机采用了激进方式——内联时遇到虚方法检查是否只有一个版本,若是则内联,在后续使用中若加载了导致继承关系变化的类,则抛弃已编译的代码退回解释执行或重新编译)
  • 冗余访问消除—— (a=obj.x; b=obj.x)  =>  (a=obj.x; b=a;)
  • 无用代码消除——(x=x;或 不会进入的分支等)
  • 公共子表达式消除—— (x=b*c-9; y=b*c+12)  =>  (E=b*c; x=E-9; y=E+12)
  • 数组边界检查消除——java是动态安全的语言,运行时会频繁进行动态检查,如每次数组元素的读写都伴有隐士的条件判断(i>=0&&i<arr.length);优化使一些明显不会越界的地方不用检测(如常量数组下标arr[3]编译时就检测是否越界;如循环变量取值永远在[0,arr.length)内则运行时不用判断)
  • 逃逸分析——栈上分配(对象不会逃逸出方法则直接栈上分配避免GC消耗)、同步消除(变量不会逃逸出线程则消除它的同步代码)、标量替换(对象不会被外部访问,且可被拆散成多个基础类型,则执行时不会创建对象,改为创建它的多个成员变量)

====================================

第五部分 高效并发

====================================

第12章 Java内存模型与线程

内存模型:指对各种内存、缓存进行读写访问的过程抽象。

Java 内存模型抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存储在主内存中。Java 内存模型主要目的是为了屏蔽系统和硬件的差异,避免一套代码在不同的平台下产生的效果不一致。

1.Java内存模型(围绕着原子性、可见性、有序性3个特征来建立的)

Java线程N - 线程工作区内存 - (Save/Load操作) - 主内存

2.基本操作规则

Java内存模型定义了8种主内存与工作内存的交互协议(每个操作都是原子的)——lock/unlock、read/load/use、assign/store/write

  • 不允许read和load、store和write操作之一单独出现,及不允许主内存读后工作内存不接受,写同理;
  • 不允许线程丢弃它最近的assign操作,即赋值后必须同步回主内存;
  • 不允许线程无原因的(未发生assign)把数据从工作内存同步会主内存;
  • 新变量只能在主内存中诞生,即一个变量实施use、store操作前,必须执行过assign和load (Obj ob; ob=method();提示使用未初始化的变量)
  • 一个变量只能同时被一个线程lock,可lock多次,必须执行相同次数unlock才能解锁
  • lock一个变量后会清空工作区内存,需要重新load才能使用
  • unlock一个变量前,必须把此变量同步会主内存
  • 一个变量没有被lock,不允许被unlock,一个线程不允许unlock其它线程lock住的变量

3.volatile的特殊性

  • 内存可见性
  • 禁止指令重排序/是指令不是java代码

volatile操作时满足的规则:

  • use时必须与load、read一起出现,保证看见其它线程修改后的值
  • assign时必须与store、write一起出现,保证其它线程可以看见自己对变量的修改
  • 线程1对变量X执行A1(assign)->F1(store)-P1(write),线程2对变量Y执行A2->F2-P2,如果A1先于A2,那么P1先于P2(即volatile修饰的变量不会被指令重排序)

4.先行发生原则(happens-before)

1)是什么

如果说操作A先行发生于操作B,也就是说发生操作B之前,操作A产生的影响能被操作B观察到(影响即:修改内存共享变量、发送消息、调用了方法等)

2)为什么需要先行发生原则

(是数据是否存在竞争、数据是否线程安全的主要依据)

——比如1多线程执行i++,由于没有先行发生原则,线程A可能看不到线程B对i的影响,从而导致安全问题

——比如2懒汉单例模式没有volatile时,new操作可能重排序,导致另一个线程获取到没有初始化的实例

3)JMM天然的8条先行发生原则

  • 程序次序规则:在一个线程内,按照代码逻辑顺序,前面的代码先行发生于后面(时间上)的代码。
  • 管程锁定规则:同一个锁的unlock先行发生于后面(时间上)的lock操作
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作
  • 线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作
  • 线程终止规则:线程的所有操作先生发生于对此线程的终止检查(如Thread.join(), Thread.isAlive())
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测(Thread.interrupted())到中断事件的发生
  • 对象终结规则:一个对象的初始化完成(构造函数结束)先行发生于它的finalize方法的开始
  • 传递性:如果操作A线程发生于操作B,B先行发生于C,那么A先行发生于C

5.Java线程

线程实现方式(Java在不同系统平台不同厂商使用方式不一样,如win/linux使用内核线程,Solaris有些厂商使用内核+用户)

  • 内核线程实现(又叫轻量级进程)——与系统内核线程1对1(调用代价高:线程操作如创建/析构都会由用户态切换内核态,线程量受内核资源限制)
  • 用户线程实现——用户态自己实现线程阻塞、调度等(实现复杂、部分接口系统未开放)
  • 内核线程+用户线程——M:N模式(如Golang)

Java线程状态:

  • New新建
  • Runable运行:对应操作系统的Running和Ready
  • Waiting无限期等待
  • TimedWaiting限期等待
  • Blocked阻塞
  • Terminated结束

6.补充

Disruptor: 

并发框架Disruptor译文 | 并发编程网 – ifeve.com

高性能队列——Disruptor - 美团技术团队

基础知识

  • 1.锁很耗时,耗时对比:锁>CAS>无锁(AtomLong——Java提供的CAS操作)
  • 2.内存屏障 (volatile——内存可见性 + 指令重排序,总线嗅探机制,写屏障/读屏障;volatile变量有时候不需要它的可见性保证来提升性能——使用sun.misc.Unsafe)
  • 3.伪共享false sharing(volatile 导致其他core缓存失效会带来一个问题。假设2个volatile变量位于一个缓存行中,那么一个core一直修改变量A,另一个core在使用变量B,A的不断更新导致在另一个core中处于同一个cache line中的B也会需要一直去内存拉最新的值,即使另一个完全不关心A的值。解决这个问题的方法就是Padding(缓存行填充),把volatile关键字的周围用其他的值来填充,保证volatile不会和其他的volatile共享一个cache line)
  • 4.缓存行填充(不同的cpu和系统大小不同,通常64B,未使用变量可能会被jvm优化掉,在 Java 8 的时候,官方给出了解决策略Contended 注解,并且在 JVM 启动参数中加入 -XX:-RestrictContended,这样 JVM 在运行时就会自动的添加合适大小的填充物(padding)来解决伪共享问题)

Disruptor为什么那么快

  • 1.数组:寻址可预测,相邻缓存,比链表快,不用重新分配和GC
  • 2.写入:使用二阶段提交(获取槽位+提交数据),无锁(CAS + volatile)
  • 3.读取:各自维护游标,无锁(volatile)
  • 4.缓存行填充,降低缓存失效率

从CPU到

大约需要的CPU周期

大约需要的时间

主存

-

约60-80ns

QPI 总线传输(between sockets, not drawn)

-

约20ns

L3 cache

约40-45 cycles

约15ns

L2 cache

约10 cycles

约3ns

L1 cache

约3-4 cycles

约1ns

寄存器

1 cycle

-

第13章 线程安全与锁优化

线程安全:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步或者其它协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

1.线程安全的实现方法

  • 互斥同步(synchonized:1.可重入2.会阻塞/涉及到用户态-内核态切换所以耗时;ReentrantLock重入锁加入更多功能:1.等待可中断2.可实现公平锁(按申请次序)3.锁可绑定多个条件)
  • 非阻塞同步(乐观锁)(TestAndSet、Get-and-Increment、Swap、Compare-and-Swap/CAS、Store-Conditional、Load-Linked)
  • 无同步方案(可重入代码、线程本地存储)

2.锁优化

  • 自旋锁和自适应自旋(自旋即多次重复获取锁不进入阻塞,避免等待造成切换线程开销,可通过jvm参数开关和次数,1.6后引入自适应次数)
  • 锁消除(判断是局部变量或线程安全则自动消除锁,如StringBuffer的append接口有synchonized,局部变量时会自动消除)
  • 锁粗化(连续多次同一对象的加锁操作,会被优化成一次大范围的加锁操作,如StringBuffer连续多次append)
  • 轻量级锁(数据依据:绝大部分锁,整个同步周期内都不存在竞争,所以轻量级锁使用CAS避免互斥开销,当出现竞争时会转为重量级锁/即互斥锁,然后竞争者进入阻塞状态)
  • 偏向锁(用于提升带有同步但无竞争的程序性能。CAS获取锁后,后续自己都是直接访问,不再进行同步操作,直到另一个线程尝试获取锁时宣布结束,取消偏向后执行重量级锁)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值