文章目录

  • CAS
  • 什么是CAS
  • CAS算法
  • 源码分析
  • Linux实现
  • Windows实现
  • CAS缺点
  • Unsafe
  • markword
  • Java对象内存
  • 对象创建过程
  • 内存布局
  • 对象头
  • 对齐填充
  • 对象头占用空间大小
  • 指针压缩
  • 什么是OOP?
  • 启用指针压缩
  • 对象访问
  • 依赖库
  • 查看对象内部信息
  • 查看对象外部信息,包括引用的对象
  • 查看对象占用空间总大小
  • 示例
  • synchronized的横切面详解
  • 锁升级过程
  • JDK8 markword实现表
  • synchronized底层实现
  • synchronized vs Lock(CAS)
  • 锁消除(lock eliminate)
  • 锁粗化(lock coarsening)
  • 锁降级(不重要)
  • 超线程
  • volatile
  • 线程可见性
  • 防止指令重排
  • 问题:DCL单例需不需要加volatile?
  • CPL的基础知识
  • 系统底层如何实现数据一致性
  • 系统底层如何保证有序性
  • volatile如何解决指令重排
  • Java的引用
  • ThreadLocal


CAS

什么是CAS

CAS:Compare and Swap(Compare and Exchange),即比较再交换,
jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

CAS算法

对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS比较与交换的伪代码可以表示为:

do{
    备份旧数据;
    基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))

源码分析

底层使用汇编直接调用硬件指令(lock cmpxchg)实现

JAVA多线程高并发教学 java多线程与高并发_JVM

Linux实现

JAVA多线程高并发教学 java多线程与高并发_JAVA多线程高并发教学_02

Windows实现

JAVA多线程高并发教学 java多线程与高并发_64位系统_03

CAS缺点

CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题:

  • 循环时间长开销很大
  • 只能保证一个共享变量的原子操作
  • ABA问题

Unsafe

markword

Java对象内存

对象创建过程

JAVA多线程高并发教学 java多线程与高并发_JVM_04

内存布局

JAVA多线程高并发教学 java多线程与高并发_JAVA多线程高并发教学_05


JAVA多线程高并发教学 java多线程与高并发_64位系统_06


JAVA多线程高并发教学 java多线程与高并发_JAVA多线程高并发教学_07


JAVA多线程高并发教学 java多线程与高并发_Java_08

JAVA多线程高并发教学 java多线程与高并发_JAVA多线程高并发教学_09


JAVA多线程高并发教学 java多线程与高并发_JAVA多线程高并发教学_10

对象头

  • Mark Word:包含一系列的标记位,比如轻量级锁的标记位,偏向锁标记位等等。在32位系统占4字节,在64位系统中占8字节;
  • Class Pointer:用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在32位系统占4字节,在64位系统中占8字节;
  • Length:如果是数组对象,还有一个保存数组长度的空间,占4个字节;
  • 对象实际数据
    对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是4个字节(64位系统中是8个字节)。

Primitive Type

Memory Required(bytes)

boolean

1

byte

1

short

2

char

2

int

4

float

4

long

8

double

8

对于reference类型来说,在32位系统上占用4bytes, 在64位系统上占用8bytes。

对齐填充

Java对象占用空间是8字节对齐的,即所有Java对象占用bytes数必须是8的倍数。例如,一个包含两个属性的对象:int和byte,这个对象需要占用8+4+1=13个字节,这时就需要加上大小为3字节的padding进行8字节对齐,最终占用大小为16个字节。

注意:以上对64位操作系统的描述是未开启指针压缩的情况,关于指针压缩会在下文中介绍。

对象头占用空间大小

这里说明一下32位系统和64位系统中对象所占用内存空间的大小:

  • 在32位系统下,存放Class Pointer的空间大小是4字节,MarkWord是4字节,对象头为8字节;
  • 在64位系统下,存放Class Pointer的空间大小是8字节,MarkWord是8字节,对象头为16字节;
  • 64位开启指针压缩的情况下,存放Class Pointer的空间大小是4字节,MarkWord是8字节,对象头为12字节;
  • 如果是数组对象,对象头的大小为:数组对象头8字节+数组长度4字节+对齐4字节=16字节。其中对象引用占4字节(未开启指针压缩的64位为8字节),数组MarkWord为4字节(64位未开启指针压缩的为8字节);
  • 静态属性不算在对象大小内。

指针压缩

从上文的分析中可以看到,64位JVM消耗的内存会比32位的要多大约1.5倍,这是因为对象指针在64位JVM下有更宽的寻址。对于那些将要从32位平台移植到64位的应用来说,平白无辜多了1/2的内存占用,这是开发者不愿意看到的。

从JDK 1.6 update14开始,64位的JVM正式支持了 -XX:+UseCompressedOops 这个可以压缩指针,起到节约内存占用的新参数。

什么是OOP?

OOP的全称为:Ordinary Object Pointer,就是普通对象指针。启用CompressOops后,会压缩的对象:

  • 每个Class的属性指针(静态成员变量);
  • 每个对象的属性指针;
  • 普通对象数组的每个元素指针。

当然,压缩也不是所有的指针都会压缩,对一些特殊类型的指针,JVM是不会优化的,例如指向PermGen的Class对象指针、本地变量、堆栈元素、入参、返回值和NULL指针不会被压缩。

启用指针压缩

在Java程序启动时增加JVM参数:-XX:+UseCompressedOops来启用。

注意:32位HotSpot VM是不支持UseCompressedOops参数的,只有64位HotSpot VM才支持。

JDK 1.8,默认该参数就是开启的。

对象访问

JAVA多线程高并发教学 java多线程与高并发_JVM_11

依赖库

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>put-the-version-here</version>
</dependency>

查看对象内部信息

ClassLayout.parseInstance(obj).toPrintable()

查看对象外部信息,包括引用的对象

GraphLayout.parseInstance(obj).toPrintable()

查看对象占用空间总大小

GraphLayout.parseInstance(obj).totalSize()

示例

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
 
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphLayout;
 
public class JolDemo {
 
    static Object generate() {
        Map<String, Object> map = new HashMap<>();
        map.put("a", new Integer(1));
        map.put("b", "b");
        map.put("c", new Date());
 
        for (int i = 0; i < 10; i++) {
            map.put(String.valueOf(i), String.valueOf(i));
        }
        return map;
    }
 
    static void print(String message) {
        System.out.println(message);
        System.out.println("-------------------------");
    }
 
    public static void main(String[] args) {
        Object obj = generate();
 
        //查看对象内部信息
        print(ClassLayout.parseInstance(obj).toPrintable());
 
        //查看对象外部信息
        print(GraphLayout.parseInstance(obj).toPrintable());
 
        //获取对象总大小
        print("size : " + GraphLayout.parseInstance(obj).totalSize());
    }
}

synchronized的横切面详解

  • 源码层级:synchronized(o)
  • 字节码层级:monitorenter,monitorexist
  • JVM层级(Hotspot):

锁升级过程

JDK8 markword实现表

无锁 --> 偏向锁 --> 轻量级锁(自旋锁,无锁)–> 重量级锁

synchronized底层实现

  • 源代码层级:synchronized(o)
  • 字节码层级:monitorenter,monitorexit
  • JVM层级:执行过程中自动锁升级
  • CPU层级:lock comxchg

synchronized vs Lock(CAS)

锁消除(lock eliminate)

不需要加锁的地方,代码优化为取消加锁

锁粗化(lock coarsening)

上锁范围扩大

锁降级(不重要)

发生在GC阶段,进入回收阶段时,只能由GC访问,因此锁降级已经不重要了。

超线程

volatile

线程可见性

防止指令重排

问题:DCL单例需不需要加volatile?

CPL的基础知识

系统底层如何实现数据一致性

  • MESI如果能解决,就使用MESI
  • 如果不能,就锁总线

系统底层如何保证有序性

  • 内存屏障sfence mfence lfence等系统原语
  • 锁总线

volatile如何解决指令重排

  • 1.源码:volatile i
  • 2.字节码:ACC_VOLATILE
  • 3.JVM:JVM的内存屏障,屏障两边的指令不可以重排,保障有序
  • 4.hostpot实现:lock
  • bytecodeinterpreter.cpp
  • orderaccess_linux_x86.inline.hpp

Java的引用

强软弱虚

  • 强引用(普通引用)
  • 软引用:SoftReference,当内存不够用的时候,软引用将被回收,应用实例:缓存。
  • 弱引用:WeakReference,应用实例:ThreadLocal
  • 虚引用:PhantomReference,应用实例:堆外内存管理(DirectByteBuffer),回收由JVM特殊的GC线程进行处理。

ThreadLocal

  • new ThreadLocal
  • set
  • remove,确认不再使用ThreadLocal时必须手工remove,否则TreadLocalMap中的Entry记录不会被删除,产生内存泄漏。