Concurrent Programming —— JMM(Java Memory Model)

Concurrent Programming

Concurrent Programming —— Introduction
Concurrent Programming —— Pessimistic Lock and Monitor
Concurrent Programming —— JMM(Java Memory Model)
Concurrent Programming ——Thread Pool
Concurrent Programming —— JUC(java.util.concurrent)

前言

本文将介绍乐观锁、volatile、不可变、Unsafe、原子类和Java内存模型(JMM)以其特性
在这里插入图片描述

1. Java Memory Model

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等

JMM体现在以下方面:
原子性 - 保证指令不会受到线程上下文切换的影响
可见性 - 保证指令不会受到CPU缓存的影响
有序性 - 保证指令不会受CPU指令并行优化的影响

原子性:
原子性指操作不可分割,不会被其它的线程锁打断,使用了锁以后的临界区代码就是原子性的

可见性:

static boolean run = true;
Thread t = new Thread(()->{
	while(run){
		//执行任务
	}
});
t.start();
run = false;

这段代码应该在main线程设置了run为false后,t线程停止运行但是实际情况t线程并没有停止,这就导致了可见性问题

从内存的角度分析:
静态变量是共享的,run是存在主内存中,当t运行的时候会去主内存中读取run的值,因为要频繁的从主内存中读取run的值,所以JIT编译器就对其优化,将run的值存在自己线程私有的工作内存中,减少了对主内存的访问,提高了效率,当main线程修改了run的值后同步了主内存中run的值,但是t是从自己的工作内存中读取数据,永远都是旧值,没有再次从主内存读取更新后的值,导致t线程无法一直结束运行

使用volatile可以来解决可见性问题
volatile 可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查询变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存

一个线程对volatile变量的修改对另一个线程可见, 不能保证原子性,不能解决指令交错问题,两个线程还是能同时对共享变量进行修改,不能保证线程安全

volatile 适用于一个线程写,多个线程读的场景

加锁也可以解决可见性问题
synchronized 语句既可以保证代码块的原子性,也同时保证代码块内变量的可见性,缺点是synchronized 属于重量级操作,性能相对更低

有序性:
JVM在不影响正确性的前提下,可以调整语句的执行顺序,这种特性称之为指令重排,在多线程下指令重排会影响正确性

CPU就会将指令重排到达优化的作用
在这里插入图片描述在这里插入图片描述
指令重排前,指令只能按照顺序一条条处理,当重排后,可以用同一时间处理多条的指令

有序性问题:

int num = 0;
boolean run = false;
//线程1
//Result对象中有result属性用来保存结果
public void run1(Result r){
	if(run){
		r.result = num + num; 
	}
	else{
		r.result = 1;
	}
}

//线程2
public void run2(Result r){
	num = 2;
	run = true;
}

情况1: 线程1先执行,这时run为true,进入else结果为1

情况2: 线程2 先执行num=2,没有执行到run=true时,线程1执行,进入else分支,结果为1

情况3: 因为指令重排线程2先执行run=true时,线程1
执行,结果为0

情况4:线程2先执行,执行完run=true后,线程1
执行,结果为4

不同的顺序会导致不同的结果

通过volatile 修饰变量可以解决重排序问题,在volatile修饰的变量语句之前的顺序都不会被改变

2. volatile

volatile的底层实现原理是内存屏障,Memory Barrier
volatile 变量的写指令后加入屏障
volatile 变量的读指令前加入屏障

int num = 0;
volatile boolean run = false;

public void run1(Result r){
	//读屏障,读屏障之后的顺序也不会改变
	if(run){
		r.result = num + num; 
	}
	else{
		r.result = 1;
	}
}

//线程2
public void run2(Result r){
	num = 2;
	run = true;//加了写屏障,写屏障前的顺序不会改变
}

volatile 不能解决指令交错
写屏障只能保证之后的读能够读到最新的结果,但不能保证读跑到它前面去,有序的保证,也只是保证了本线程内相关代码不被重排序

3. 乐观锁

CAS:compare and set,比较最新值,并设置预期值,CAS的操作是原子性的

过程演示:
在这里插入图片描述
只有当本线程的值与最新值一样时,才能更新成功

CAS底层是lock cmpxchg指令(X86架构),在单核CPU和多核CPU下都能保证操作的原子性
CAS操作需要volatile的支持,保证获取共享变量的最新结果来实现【比较并交换】的效果

无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而synchronized会让线程在没有获得锁的时候,发生上下文切换,进入阻塞

无锁情况下,因为线程要保持运行,需要额外CPU的支持,如果线程没有分配到时间片,仍然会进入可运行状态,还是会导致上下文切换

CAS特点:

  1. CAS是基于乐观锁的思想:不怕别的线程来修改共享变量,synchronized 是基于悲观锁的思想:防止别的线程来修改共享变量
  2. CAS体现的是无锁并发、无阻塞并发:
    没有使用synchronized,所以线程不会陷入阻塞,但如果竞争激烈,重试频繁发生,反而效率会受影响

4. 原子类

Java提供了原子类,原子类的方法都是原子性的且使用的是CAS的乐观锁,方而且成员变量也是由volatile所修饰的,保证类线程安全,这些类都是以Atomic开头的

4.1原子整数

//构造方法
AtomicInetger i = new AtomicInteger(value);
//自增方法
i.incrementAndGet()// 等同于i++
i.getAndIncrement()// 等同于++i
//获取值
i.get()
//加法
i.getAndAdd(value)
i.addAndGet(value)
//复杂运算
//IntUnaryOperator是函数接口,可以用lambda来写出运算方法
i.updateAndGet(IntUnaryOperator operation)
//乘以10
i.updateAndGet(x -> x*10)

4.2原子引用

原子引用有AtomicReferenceAtomicMarkableReferenceAtomicStampedReference
AtomicReference的用法:

//构造方法
AtomicReference<T> ref = new AtomicReference<>();
//获取值
ref.get();
//更新值
ref.compareAndSet(预期值,新值)
//赋值
ref.set(新值);

CAS会带来ABA问题

ABA问题:
初始变量为A,在CAS前,其他线程把共享变量改为了B,另一个线程又把B改为了A,CAS无法感知共享是否被修改过,CAS只能判断共享变量是否与最初值是否相同

AtomicStampedReference通过给共享变量加了版本号的属性,来判断共享变量是否被修改过,如果修改过CAS就会失败

//构造方法
AtomicStampedReference<T> ref = new AtomicStampedReference<>(T,版本号);
//后去对象
ref.getReference();
//要传入老版本与最新的版本号进行对比来判断是否更改过
ref.compareAndSet(原来对象,新对象,老版本号,新版本号);

AtomicMarkableReference只在意共享变量是否被更改过,不在意被更改了几次,只需要添加boolean来判断

//构造方法
AtomicMarkableReference<T> ref = new AtomicMarkableReference<>(T,true);
//后去对象
ref.getReference();
//要传入老版本与最新标记进行对比来判断是否更改过
ref.compareAndSet(原来对象,新对象,原来标记,新标记);

4.3原子数组

AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray可以保护数组内元素的线程安全,用法和AtomicInteger基本一致

//返回数组长度
length()
//对数组中的元素进行自增
getAndIncrement(index)

4.4 原子字段更新器

原子字段更新器是用来保护对象的成员变量的线程安全,有AtomicReferenceFieldUpdaterAtomicIntegerFieldUpdaterAtomicLongFieldUpdater这几个类

对象的成员属性必须被 volatile 修饰

AtomicReferenceFieldUpdater updater = new AtomicReferenceFieldUpdater(保护对象的class,成员变量的class,成员变量)
updater.compareAndSet(对象,初始值,期望值)

4.5原子累加器

原子累加器LongAdder,比原子类中的自增方法效率要高,在多个线程竞争时,CAS竞争激烈导致效率变低,原子累加器在有竞争时,设置多个累加单元,不同的线程累加不同的单元(Cell变量),减少了CAS重试失败,从而提升了效率

5. Unsafe对象

Unsafe对象提供了非常底层的,操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得,是原子类的底层实现

Unsafe的CAS操作:

  1. 获取域的偏移地址
  2. 执行CAS操作
//获取偏移值
long offset = unsafe.objectFieldOffSet(xxx.class.getDeclareField(变量名))
//执行CAS操作
unsafe.compareAndSwapInt(修改对象,域的offset,当前值,期望值)

不同类型的数据后缀不同,Object的CAS操作就是compareAndSwapObject

6. 不可变

最后一种保证线程安全的方法就是将类和成员变量都设计成不可变
不可变设计:
类、类中的所有属性都是final的
属性用final修饰保证了该属性是只读的,不能修改
类用final修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏了不可变性
保护性拷贝:通过创建副本对象来避免共享
保护性拷贝来保证数组内容不会被其它类改变

以Java中String类为例来讲解不可变:
String类是被final修饰的,无法被去继承String类从而破坏原本线程的方法,当创建一个String时,实际上是创建一个char数组,该数组也是被final所修饰的,因此创建后也无法更改,而String中修改字符串的方法都是创建一个新的char数组,而不是在原来的char数组上进行更改

Concurrent Programming

Concurrent Programming —— Introduction
Concurrent Programming —— Pessimistic Lock and Monitor
Concurrent Programming —— JMM(Java Memory Model)
Concurrent Programming ——Thread Pool
Concurrent Programming —— JUC(java.util.concurrent)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值