synchronized & volatile

在Java内存模型中,保证并发安全的三个特性:原子性、有序性、可见性。

CAS
compare and swap
compare and exchange
比较在交换
乐观锁的思想,是一种无锁算法。
jdk5后增加的并发包下java.util.concurrent.*下面的类使用了CAS算法实现。
例如AtomicInteger中的一个方法:

15951497219631
15951497219631

自旋 do while

有三个参数,内存值A、旧的预期值B、新的预期值C,
当且仅当 A == B的时候,将A更新为C。

ABA问题:
当前线程在CAS过程中,进行比较的时候,发现值还是原来的值,但是是经过别的线程操作之后的值,并不是初始的值。
其他线程修改数次后,最终值与初始值相等。

举个不太恰当的栗子:
小明与女友分手了,半年后又和好了,但是在这半年内,虽然还是那个人,他并不知道他女友有没有跟其他人交往过。

解决:加版本号、时间戳、或者加Boolean类型标记。

CAS在硬件级指令
cmpxchg 前加lock(多个cpu)使这条指令为原子操作,即
lock cmpxchg 指令


描述对象在内存中的内存布局
JOL 是个工具
IDEA插件 :
https://plugins.jetbrains.com/plugin/10953-jol-java-object-layout

布局:
markword 锁的信息都在这里 8字节
类型指针 class pointer 指向属于哪个类 4字节
实例数据 instance data 成员变量所在
对齐 padding 整体字节数不能被8整除的时候,补齐
(loss due to the next object alignment)

Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());

执行上面代码,

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

新生成一个Object对象,占用的内存情况:
Mark Word 8字节
Class Pointer 4字节
Instance Data 0
对齐 4字节
总共16个字节。

UseCompressedClassPointer
class pointer 压缩指针
64位JVM,它的指针长度就是64位,8个字节
开启压缩指针后会压缩为4个字节、

UseCompressedOops
oops ordinary object pointers 普通对象指针,默认也是压缩的,4字节


锁升级过程

全部记录在markword中。
new - 偏向锁 - 轻量级锁(无锁、自旋锁、自适应锁)- 重量级锁

第一次加锁的时候,第一个线程,对象中的markword还没有被污染,所以第一次加的锁是偏向锁(54位指向当前线程的指针),不用去操作系统申请重量级锁。

发生任意竞争的时候,升级为轻量级锁,
首先撤销偏向锁状态,
每个线程的线程栈生成自己的锁记录,lock record
mark word中指向线程栈中的Lock Record的指针
使用自旋的方式去抢夺锁 CAS操作

竞争激烈的时候,
自旋锁太费CPU,锁升级为重量级锁
1、自旋超过多少次,或者是超过CPU的二分之一
2、自适应自旋,JVM自己控制
用户态、内核态
用户线程想要操作硬件的时候需要内核线程完成

重量级锁 队列

锁降级
在GC的情况下发生,没有意义

锁消除
StringBuffer append一堆,锁会消除

锁粗化


volatile

内存可见性和禁止指令重排序
看一个例子

	Boolean running = true;
	void m(){
		System.out.println(" m start ");
		while (running){
			// nothing
		}
		System.out.printf(" m end ");
	}

	public static void main(String[] args) {
		HelloVolatile t = new HelloVolatile();

		new Thread(t :: m).start();

		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}

		t.running = false;
	}

以上这个例子,本意是有一个变量running,初始值为true,然后启动了一个线程,while(running)循环,主线程在一秒后修改了这个running变量值为false,预期结果是我们新启动的线程也会停止掉。

结果显然不是这样的。因为每个线程都有自己的工作内存,当线程启动时,会将变量复制到自己的工作内存中,虽然我们在主线程中修改了变量值,但是对于新启动的线程的工作内存是不可见的。所以启动的新线程会一直运行。

将running变量修饰为volatile即可出现预期情况。
某个线程修改了变量值后,会通知到主内存该值已被修改,同时其他线程持有的该变量失效,重新从主内存中读取。

CPU三级缓存、主内存、线程的工作内存
8个操作

MESI缓存一致性协议

Modify
Exclusive
Shared
Invalid

如何禁止指令重排序

对象的创建过程
new
invokespecial -- init
astore_1
return

1、代码中变量用volatile修饰
2、在编译为字节码文件后,该属性的acc_flags 是ACC_VOLATILE
3、JVM的内存屏障,虚拟机看到ACC_VOLATILE后,屏障两边的指令不可以重排,保障有序

LoadLoad
StoreStore
LoadStore
StoreLoad

4、hotspot实现
lock

强软弱虚

强引用 Object o = new Object(); 当没有任何引用指向该对象内存的时候,才会被垃圾回收
软引用 适合做缓存,当内存不够用的时候,会优先回收软引用的对象。

弱引用 gc的时候会直接被回收,一次性,当加载的时候会用到
虚引用 get不到。作用:管理堆外内存。NIO中 DirectByreBuffer

ThreadLocal

每个线程独自拥有

DCL 单例模式

缓存行 cache line

举个栗子:

public static class T{
	private long a1,a2,a3,a4,a5,a6,a7;
	public volatile long x = 0L;
}
public static T[] arr = new T[2];

static {
	arr[0] = new T();
	arr[1] = new T();
}

public static void main(String[] args) throws InterruptedException {
	Thread t1 = new Thread(()->{
		for (int i = 0; i < 10000000; i++) {
			arr[0].x = i;
		}
	});

	Thread t2 = new Thread(()->{
		for (int i = 0; i < 10000000; i++) {
			arr[1].x = i;
		}
	});

	long start = System.currentTimeMillis();

	t1.start();
	t2.start();

	t1.join();
	t2.join();

	System.out.println(System.currentTimeMillis() - start);
}

类T中有7个long类型的变量是没有用到的,但是如果把这7个变量去掉,执行的时间会大大增加。

去掉后 执行时间 大概200多毫秒
没有去掉 执行时间 大概是 80多毫秒

这是因为缓存行的大小一般是64字节,CPU与内存交换数据中是以缓存行为单位的;多了7个long类型的变量,数组中的两个元素不在同一个缓存行上,两个线程就不用相互通知对方进行数据修改。


markword中记录了哪些信息?
4字节 分代年龄 最大15

CMS辣鸡回收器 年龄为6后转入老年代


面试题
Q:请描述synchronized 和reentrantlock的底层实现及重入的底层原理

Q:请描述锁的四种状态和升级过程

Q: CAS的ABA问题如何解决

Q:请谈一下你对volatile的理解

Q: volatile的可见性和禁止指令重排序是如何实现的

Q: CAS是什么

Q: 请描述一下对象的创建过程

Q:对象在内存中的内存布局

Q:DCL单例为什么要加volatile

Q: 解释一下锁的四种状态

Q: Object o = new Object()在内存中占了多少字节

Q:请描述synchronized和RenntrantLock的异同

Q:聊聊你对as-if-serial和happens-brfore语义的理解

Q:你了解ThreadLocal吗,你知道ThreadLocal如何解决内存泄露问题吗

Q:请描述下锁的分类以及JDK中的应用

HashTable和CurrentHashMap的线程安全实现的区别

2020/10/21 posted in  JVM