0 问题
- 1.synchronized 和ReentrantLock 底层实现&重入机制
- 2.锁的四种状态和升级过程
- 3.CAS 是什么,如何解决ABA问题
- 4.volatile的可见性和指令重排是如何实现的
- 5.java 一个对象创建的过程
- 6.对象在内存布局,Object o=new Object()在内存中占了多少字节
- 7.DCL单例为什么要加volatile
- 8.Java中的对象都是在堆上分配的吗?
…
如果你能够回答以上问题,后面文章你可以不用看了
1 CAS
CAS(compare and swap) 比较和更新,就是在无锁状态下可以保证多个线程对一个值得更新。
ABA是一个潜在问题,可能会产生问题,解决办法也比较简单就是加标记或者加个version。
CAS 底层是如何实现的,怎么保证更新原子性的? hostspot源码 底层使用lock cmpxchgl 指令,是硬件级别锁可以保证原子性。
AtomicInteger atomicInteger=new AtomicInteger();
atomicInteger.incrementAndGet();
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//比较和更新,如果和原始值一样就更新,可能会有ABA问题
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
//C/C++实现
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
2 java 对象内存布局
接下来实验室在64位java 虚拟机上,使用下面命令看下jvm 默认参数。
java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_291"
Java(TM) SE Runtime Environment (build 1.8.0_291-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.291-b10, mixed mode
UseCompressedOops:普通对象指针压缩,oops: ordinary object pointer ,类里面的引用变量
UseCompressedClassPointers:类指针压缩
JVM默认是开启类指针压缩和普通对象指针压缩,开启原因是为了减少内存空间开销。关闭压缩指针-XX:-UseCompressedClassPointers -XX:-UseCompressedOops,也就是将加号变成减号。
使用open jdk 工具来分析,maven 依赖下面给出。
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
public class LayoutDemo {
public static void main(String[] args) {
Object o=new Object();
Object [] arr=new Object[10];
System.out.println( ClassLayout.parseInstance(o).toPrintable() );
System.out.println( ClassLayout.parseInstance(arr).toPrintable() );
}
}
//普通java 对象
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
//对象头-标记位 8个字节
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
//对象头-类指针,压缩后4个字节,不压缩就是8个字节
8 4 (object header: class) 0xf80001e5
// 对齐
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
//数组对象
[Ljava.lang.Object; object internals:
OFF SZ TYPE DESCRIPTION VALUE
//对象头-标记位 8个字节
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
//对象头-类指针
8 4 (object header: class) 0xf800234d
//数组长度 四个字节
12 4 (array length) 10
// 对齐
12 4 (alignment/padding gap)
16 40 java.lang.Object Object;.<elements> N/A
Instance size: 56 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
从上面实验可以得出java 内存布局如下图所示,主要分为三个部分:对象头、数据部分、内存对齐。1)对象头中标记为固定8个字节,对象头中类指针在64位虚拟机情况要看有没有开启对应的压缩指针(默认是开启的)开启了占4个字节否则8个字节,对于数组的对象头会对一个数组长度部分占4个字节。2)对象实例数据部分,涉及引用对象部分也要看有没有开启相应的压缩指针
对象头中64位,记录了锁升级过程,如下图所示。无锁->偏向锁->轻量级锁(自旋锁)->重量级锁。第一次上锁上偏向锁,有竞争先上轻量级锁,竞争过于激烈就会升级为重量级锁。
3 synchronized
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:1)修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;2) 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象; 3) 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;4) 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
synchronized 在JVM 层面也做了很多优化,下面分析下其底层实现原理 ,从编译后的字节码可以看到加了监视器 MONITORENTER,MONITOREXIT,然后jvm在执行的过程进行相应的锁升级。
public class SynchronizeDemo {
public static void main(String[] args) {
Object o = new Object();
synchronized (o) {
System.out.println("test");
}
}
**编译后的字节码实现,加了MONITORENTER,MONITOREXIT**
MONITORENTER
L0
LINENUMBER 8 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "test"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L6
LINENUMBER 9 L6
ALOAD 2
MONITOREXIT
更底层实现lock cmpxchg 实现,可以通过-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,具体参考打印出汇编。总结下实现过程,synchronized->MONITORENTER,MONITOREXIT->执行过程锁升级->lock cmpxchg。
4 ReentrantLock
5 volatile 关键字作用原理
volatile 是java一个关键字,其主要有两个作用1)**保证线程可见性 :cpu 缓存MESI 协议。2)禁止指令重排,通过内存屏障禁止指定重排,**典型的案例:DCl 单例模式。下面就这个两个点展开说明一下。
什么是cpu 缓存MESI 协议,MESI(Modified Exclusive Shared Or Invalid)(也称为伊利诺斯协议,是因为该协议由伊利诺斯州立大学提出)是一种广泛使用的支持写回策略的缓存一致性协议,它了为了解决多核CPU 多级缓存一致性问题提出的。如下图所示,下面是X86 CPU的Cache结构图,从图中可以看出一个最简单的双核心 CPU,它有三级缓存,第一级 Cache 是指令和数据分开的,第二级 Cache 是独立于 CPU 核心的,第三级 Cache 是所有 CPU 核心共享的。值得说明是,缓存单位是按照缓存行设计的,一般大小为64个字节,CPU可以保证缓存行的一致性,linux 可以用如下命令查看缓存行大小。
cat /sys/devices/system/cpu/cpu1/cache/index0/coherency_line_size
这种 cpu缓存结构,会带来如下问题 1)多个核心CPU 1-2级别缓存一致性问题 2.cpu 的3级缓存和内存、显存等之间一致性问题。如下图所示给出了一个不一致例子,x值一初始值是200,在某一时刻cpu1修改x值为100,这个时cpu1 缓存值和内存值以及cpu2 缓存值是不一致的。
要解决缓存一致性问题,首先要解决的是多个 CPU 核心之间的数据传播问题,也就是写传播;最常见的一种解决方案呢,叫作总线嗅探(Bus Snooping)。其次还要解决就是事务的串行化(Transaction Serialization),事务串行化是说,我们在一个 CPU 核心里面的读取和写入,在其他的节点看起来,顺序是一样的,主要靠硬件锁机制。基于总线嗅探机制,其实还可以分成很多种不同的缓存一致性协议。不过其中最常用的,就是今天我们要讲的 MESI 协议。和很多现代的 CPU 技术一样,MESI 协议也是在 Pentium 时代,被引入到 Intel CPU 中的。
MESI 协议定义了 4 种基本状态:M、E、S、I,即修改(Modified)、独占(Exclusive)、共享(Shared)和无效(Invalid)。下面我结合示意图,给你解释一下这四种状态。
- M:Modified,当共享变量被某个cpu修改后,那么该cpu中的共享变量状态为【被修改】状态。其他读取了该变量的cpu中的状态为【无效】状态。
- E:Exclusive,当共享变量第一次被某一个cpu读取进缓存后,该变量的状态就被标记为【独占】状态,也就是说,当前cpu中的变量和主存中的完全一致。当其他cpu从主存中载入该变量数据时,此时该变量状态变为【共享】状态。
- S:Shared,共享变量被多个cpu载入时,变量状态变为【共享】状态。当cpu对该变量进行修改后,此时该变量在当前cpu的状态变为【被修改】状态,其他cpu中的状态变为【无效】状态。
- I:Invalid,当其他cpu修改了共享变量后,其他读取了该共享变量的cpu中的状态全部变为【无效】状态。【无效】状态的数据再下次使用之前时必须要从主存同步最新数据过来的。
什么是指令重排呢?
可以简单立理解就是实际执行的顺序可以与代码的顺序不一致,可能会乱序执行。但是值得说明的无论怎么排序,要满足 as-if-serial语义,单线程的运行结果不能改变。可以简单理解为指令重排序不会影响单线程,可能会影响多线程。这里也顺便提下happens-before语义,从JDK 5开始,Java使用新的JSR-133内存模型,提供了happens-before 原则,用来指定两个操作的之前的顺序,提供跨线程的可见性。两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行,happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。
为什么要指令重排?
提高程序执行性能。
public class Single {
// 加上 volatile
private static Single single;
private Single() {
}
public static Single getInstance() {
if (single == null) {
synchronized (Single.class) {
if (single == null) {
// 这个地方可能有问题
single = new Single();
}
}
}
return single;
}
}
如下面代码所示,3和4 在指令顺序是可以重排的,如果3和4对调,那么这个时候在并发场景,另一个线程看到single是没有初始化的对象。
LINENUMBER 14 L8
1. NEW com/hsc/java/practice/layout/Single
2. DUP
3. INVOKESPECIAL com/hsc/java/practice/layout/Single.<init> ()V
4. PUTSTATIC com/hsc/java/practice/layout/Single.single : Lcom/hsc/java/practice/layout/Single;
那么这个问题怎么解决呢?加下volatile,可以实现禁止instance指令重排,那么volatile怎么实现禁止指令重排?要回答这个问题我们先看内存屏障或者内存栅栏,屏障两边的指令不可重排,内存屏障的原语sfence、lfence、mfence等系统源语。在JVM 层面,JSR内存屏障:LoadLoad,StoreStore,LoadStore,StoreLoad。LoadLoad 就是load1和load2 不能换顺序。
对于volatile 修饰的变量,JVM在读写场景会加上相应屏障,禁止指令重排。
对于java 虚拟机层面使用 lock addl 指令。
inline void _OrderAccess_fence() {
// Always use locked addl since mfence is sometimes expensive
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
}
参考文献
[1] https://time.geekbang.org/column/article/109874
[2] https://time.geekbang.org/column/article/376711
[3] https://www.cnblogs.com/z00377750/p/9180644.html
[4]https://www.cnblogs.com/ITPower/p/13580691.html
[5]https://blog.csdn.net/byhook/article/details/87971081