文章目录
- 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)实现
Linux实现
Windows实现
CAS缺点
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题:
- 循环时间长开销很大
- 只能保证一个共享变量的原子操作
- ABA问题
Unsafe
markword
Java对象内存
对象创建过程
内存布局
对象头
- 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,默认该参数就是开启的。
对象访问
依赖库
<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记录不会被删除,产生内存泄漏。