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)
Concurrent Programming —— JMM(Java Memory Model)
前言
本文将介绍乐观锁、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特点:
- CAS是基于乐观锁的思想:不怕别的线程来修改共享变量,
synchronized
是基于悲观锁的思想:防止别的线程来修改共享变量 - 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原子引用
原子引用有AtomicReference
,AtomicMarkableReference
和AtomicStampedReference
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原子数组
AtomicIntegerArray
,AtomicLongArray
,AtomicReferenceArray
可以保护数组内元素的线程安全,用法和AtomicInteger
基本一致
//返回数组长度
length()
//对数组中的元素进行自增
getAndIncrement(index)
4.4 原子字段更新器
原子字段更新器是用来保护对象的成员变量的线程安全,有AtomicReferenceFieldUpdater
、AtomicIntegerFieldUpdater
、AtomicLongFieldUpdater
这几个类
对象的成员属性必须被 volatile 修饰
AtomicReferenceFieldUpdater updater = new AtomicReferenceFieldUpdater(保护对象的class,成员变量的class,成员变量)
updater.compareAndSet(对象,初始值,期望值)
4.5原子累加器
原子累加器LongAdder,比原子类中的自增方法效率要高,在多个线程竞争时,CAS竞争激烈导致效率变低,原子累加器在有竞争时,设置多个累加单元,不同的线程累加不同的单元(Cell变量),减少了CAS重试失败,从而提升了效率
5. Unsafe对象
Unsafe
对象提供了非常底层的,操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得,是原子类的底层实现
Unsafe的CAS操作:
- 获取域的偏移地址
- 执行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)