0
点赞
收藏
分享

微信扫一扫

shared_ptr 与 unique_ptr 的转换 笔记

Sikj_6590 2024-01-27 阅读 18
jvmjava

文章目录

1 原子性

问题:两个线程对初始值为 0 的静态变量 i 一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

i++产生JVM字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法
putstatic i // 将修改后的值存入静态变量i

i++产生JVM字节码指令:

getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 减法
putstatic i // 将修改后的值存入静态变量i

交错执行的可能导致结果可能为正,也可能为负,也可能为0,为正的情况如下:

// 假设i的初始值为0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1

显而易见,两个线程谁后对静态变量做赋值,另一方的赋值就被覆盖了

解决办法: 想要保证 i++ 和 i-- 代码的原子性,需使用 synchronized 对象锁

2 可见性

问题:main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止?

static boolean run = true;
public static void main(String[] args) throws InterruptedException {
	Thread t = new Thread(()->{
		while(run){
			// ....
		}
	});
	t.start();
    
	Thread.sleep(1000);
	run = false; // 线程t不会如预想的停下来
}

之前说过,JIT应用场景之一就有字段优化,可见于 回顾:JVM类加载

①初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存

②热点字段run渐被缓存至t线程自己的工作内存,以减少对主内存的访问

③main线程对run的更新虽然同步至主内存,但t线程的run永远都是旧值

解决办法: volatile(易变关键字),强制 使用到该变量的线程 到主存中获取它的值

关键字使用场景作用/特点
synchronized多个写线程既可以保证代码块的原子性,也同时保证代码块内变量的可见性,
属于重量级操作,性能相对更低
volatile多读一写可见性

3 有序性

int num = 0;
boolean ready = false;

// 线程1 执行此方法
public void actor1(I_Result r) {
	if(ready) {
		r.r1 = num + num;
	} else {
		r.r1 = 1;
	}
}

// 线程2 执行此方法
public void actor2(I_Result r) {
	num = 2;
	ready = true;
}

两个线程对r对象的r1属性值做修改,问能得到哪几种结果?

有序性理解:

double-checked locking 模式实现单例中的问题分析:

public final class Singleton {
	private Singleton() { }
	private static Singleton INSTANCE = null;
	public static Singleton getInstance() {
		// 实例没创建,才会进入内部的 synchronized代码块
		if (INSTANCE == null) {
			synchronized (Singleton.class) {
				// 也许有其它线程已经创建实例,所以再判断一次
				if (INSTANCE == null) {
					INSTANCE = new Singleton();
				}
			}
		}
		return INSTANCE;
	}
}	

虽然方法能懒惰实例化并加锁,但是多线程下还是有问题的, INSTANCE = new Singleton() 对应的字节码为:

0: new #2 // class cn/itcast/jvm/t4/Singleton
3: dup
4: invokespecial #3 // Method "<init>":()V
7: putstatic #4 // Field
INSTANCE:Lcn/itcast/jvm/t4/Singleton;

问题就在 4 和 7 两步,正常是对象初始化在将其地址赋值给静态变量,但是可能指令重排,先7后4, 导致的结果就是对象还未来得及执行初始化方法,其地址就先赋给了静态变量,此时另一个线程调用该方法,未初始化的对象直接通过静态变量return出去了,这有问题

时间1 t1 线程执行到 INSTANCE = new Singleton();
时间2 t1 线程分配空间,为Singleton对象生成了引用地址(0 处)
时间3 t1 线程将引用地址赋值给 INSTANCE,这时 INSTANCE != null7 处)
时间4 t2 线程进入getInstance() 方法,发现 INSTANCE != nullsynchronized块外),直接
返回 INSTANCE
时间5 t1 线程执行Singleton的构造方法(4 处)

解决办法:对 INSTANCE 使用 volatile 修饰

4 CAS

CAS 即 Compare and Swap ,它体现的一种乐观锁的思想(线程安全),比如多个线程要对一个共享的整型变量执行 +1 操作:

// 需要不断尝试
while(true) {
	int 旧值 = 共享变量 ; // 比如拿到了当前值 0
	int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1

    /*
	这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候
	compareAndSwap 返回 false,重新尝试,直到:
	compareAndSwap 返回 true,表示我本线程做修改的同时,别的线程没有干扰
	*/
	if( compareAndSwap ( 旧值, 结果 )) {
		// 成功,退出循环
	}
}

简单说: 就是CAS用于检验共享变量的结果达到预期要求, 因此它配上 volatile 修饰变量保证该变量的可见性,可以实现无锁并发,效率提升,缺点就是不达要求不断重试,会争抢资源,效率反而下降,因此它适用于竞争不激烈、CPU多核的场景,不然还是稳妥起见选用synchronized悲观锁

原子操作类, 底层就是采用 CAS 技术 + volatile 来实现的。以 AtomicInteger 为例:

// 创建原子整数对象
private static AtomicInteger i = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
	Thread t1 = new Thread(() -> {
		for (int j = 0; j < 5000; j++) {
			i.getAndIncrement(); // 获取并且自增 i++
			// i.incrementAndGet(); // 自增并且获取 ++i
		}
	});
    
    Thread t2 = new Thread(() -> {
		for (int j = 0; j < 5000; j++) {
			i.getAndDecrement(); // 获取并且自减 i--
		}
	});

    t1.start();
	t2.start();
	t1.join();
	t2.join();
	System.out.println(i);
}

5 synchronized 优化

每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存储这个对象的 哈希码 、 分代年龄 ,当加锁时,这些信息就根据情况被替换为 标记位 、 线程锁记录指针 、 重量级锁指针 、 线程ID 等内容

反过来,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word

5.1 轻量级锁

假设有两个方法同步块,利用同一个对象加锁:

static Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
		// 同步块 
		method2();
	}
}

public static void method2() {
	synchronized( obj ) {
		// 同步块 B
	}
}
线程 1对象 Mark Word线程 2
访问同步块 A,把 Mark 复制到
线程 1 的锁记录
01(无锁)
CAS 修改 Mark 为线程 1 锁记录
地址
01(无锁)
成功(加锁)00(轻量锁)线程 1
锁记录地址
执行同步块 A00(轻量锁)线程 1
锁记录地址
访问同步块 B,把 Mark 复制到
线程 1 的锁记录
00(轻量锁)线程 1
锁记录地址
CAS 修改 Mark 为线程 1 锁记录
地址
00(轻量锁)线程 1
锁记录地址
失败(发现是自己的锁)00(轻量锁)线程 1
锁记录地址
锁重入00(轻量锁)线程 1
锁记录地址
执行同步块 B00(轻量锁)线程 1
锁记录地址
同步块 B 执行完毕00(轻量锁)线程 1
锁记录地址
同步块 A 执行完毕00(轻量锁)线程 1
锁记录地址
成功(解锁)01(无锁)
01(无锁)访问同步块 A,把 Mark 复制到
线程 2 的锁记录
01(无锁)CAS 修改 Mark 为线程 2 锁记录
地址
00(轻量锁)线程 2
锁记录地址
成功(加锁)

5.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

static Object obj = new Object();
public static void method1() {
	synchronized( obj ) {
		// 同步块
	}
}
线程 1对象 Mark Word线程 2
访问同步块,把 Mark 复制到 线程 1 的锁记录01(无锁)
CAS 修改 Mark 为线程 1 锁记录 地址01(无锁)
成功(加锁)00(轻量锁)线程 1 锁记录地址
执行同步块00(轻量锁)线程 1 锁记录地址
执行同步块00(轻量锁)线程 1 锁记录地址访问同步块,把 Mark 复制
到线程 2
执行同步块00(轻量锁)线程 1 锁记录地址CAS 修改 Mark 为线程 2 锁
记录地址
执行同步块00(轻量锁)线程 1 锁记录地址失败(发现别人已经占了
锁)
执行同步块00(轻量锁)线程 1 锁记录地址CAS 修改 Mark 为重量锁
执行同步块10(重量锁)重量锁指
阻塞中
执行完毕10(重量锁)重量锁指
阻塞中
失败(解锁)10(重量锁)重量锁指
阻塞中
释放重量锁,唤起阻塞线程竞争01(无锁)阻塞中
10(重量锁)竞争重量锁
10(重量锁)成功(加锁)

5.3 自旋

5.4 偏向锁

5.5 其他优化

优化操作
减少上锁时间同步代码块中尽量短
减少锁的粒度将一个锁拆分为多个锁提高并发度
锁粗化多次循环进入同步块不如同步块内多次循环
锁消除JVM 会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时候
就会被即时编译器忽略掉所有同步操作。
读写分离CopyOnWriteArrayList
ConyOnWriteSet
举报

相关推荐

0 条评论