Java多线程(5)
CPU缓存一致性问题
因为缓存的出现,极大提高了CPU的吞吐能力,但同时也引入了缓存不一致的问题,比如i++操作
在程序运行过程,首先将主存中的数据复制一份存放到CPU Cache中,那么CPU寄存器进行数值计算的时候就直接到Cache中读取和写入,当整个运算过程完毕之后再讲Cache中的数据刷新到主存当中
具体如下:
读取主内存的i到cpu cache
对i进行+1操作
将结果写回到cpu cache中
将数据刷新到主内存
i++在单线程环境不会有什么问题,但在多线程下就会出现问题了
每个线程都有自己的工作内存,变量i会在多个线程的本地内存中都保存一个副本,如果同时两个线程执行i++操作,假设i的初始值为0,每一个线程都从主内存中获取i的值存入cpu cache,然后经过计算再写入主内存,很有可能i在经过了两次自增之后结果还是1,这就是典型的缓存不一致的问题
Java内存模型决定了一个线程对共享变量的写入何时对其他线程可见,Java内存模型定义了线程和主内存之间的抽象关系:
共享变量存储在主内存,每个线程都可以访问
每个线程都有私有的工作内存或者称为本地内存
工作内存只存储该线程对共享变量的副本
线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存
并发编程的三个重要特性
原子性
指在一次的操作或者多次操作中,要么所有操作全部得到执行并且没有受到任何因素干扰而中断,要么所有操作都不执行
比如银行转账的例子,A给B的账户转了100元,这个动作包含两个基本操作:
从A上的账户扣除100
给B的账户增加100
这两个操作必须符合原子操作,要么成功要么失败
可见性
指当一个线程对共享变量进行了修改,那么其他线性也应该可以立即看到修改后的最新值
有序性
指程序代码执行过程中的先后顺序,由于编译器的优化,导致代码的执行顺序未必就是你编写代码时候的顺序
比如:
int x = 10;
int y = 0;
x++;
y = 20;
上面这段代码从编写程序的角度看肯定是顺序执行下来的,但是JVM真正运行之后就未必是这样的顺序了,比如y=20语句可能在x++语句前面得到执行,这就是通常我们说的指令重排序
在单线程下,无论怎样的指令重排序都最终可以保证程序的执行结果以及代码顺序执行的结果是一致的。但在多线程下,如果有序性得不到保证,那么很有可能就会出现问题
JMM如何保证三大特性
JMM与原子性
- 赋值操作: x = 10;
x = 10的操作是原子性的,执行线程首先会将x=10写入工作内存,然后再将其写入主内存(有可能往主内存进行数值刷新的过程中其他线程也在对其进行刷新操作,比如另一个线程将其写为11,但最终结果肯定要么是10,要么是11)
- 赋值操作: y = x;
这个操作是非原子性的
执行线程先从主内存中读取x的值,然后将其存入当前线程的工作内存之中
在执行线程的工作内存中修改y地方值为x,然后将y的值写入主内存中
- 自增操作: y++;
这个操作是非原子性
执行步骤:
执行线程从主内存读取y值,然后将其存入当前线程的工作内存之中
执行线程工作内存中对y执行+1操作
将y值写入主内存
结论:
多个原子性操作在一起就不再是原子性操作了
简单的读取与赋值操作是原子性的,将一个变量赋值给另一个变量的操作不是原子性
JMM与可见性
多线程下,如果某个线程首次读取共享变量,则首先到主内存中获取该变量,然后存入工作内存,以后只需要在工作内存中读取该变量即可。同样如果对该变量执行了修改操作,则先将新的值写入工作内存,然后在刷新到主内存,但是什么时候最新的值会刷新到主内存是不确定的
Java提供三种方式保证可见性:
- 使用volatile关键字
当一个变量被volatile修饰后,对于共享资源的读操作会直接在主内存中进行(同样也会缓存到工作内存,当其他线程对该共享资源进行了修改,则会导致当前线程在工作内存中的共享资源失效,所以必须从主内存中再次获取),对于共享资源的写操作当然是先修改工作内存,但是修改结束后会立即将其刷新到主内存中
- 使用synchronize关键字
synchronize关键字能够保证同一时刻只有一个线程获取锁,然后执行同步方法,并且还会确保在锁释放之前,会将对变量的修改刷新到主内存
- JUC包下的显式锁
Lock的lock方法能够保证在同一时刻只有一个线程获得锁然后执行同步方法
JMM与有序性
Java提供三种方式保证有序性:
volatile关键字
synchronize关键字
显式锁Lock
volatile解析
volatile关键字的语义
被volatile修饰的实例变量或者类变量具备如下两层语义:
- 保证了不同线程之间对共享变量操作时的可见性,也就说当一个线程修改了volatile修饰的变量,另一个线程会立即看到最新的值
- 禁止对指令进行重排序操作
volatile保证有序性
volatile关键字直接禁止JVM和处理器对volatile关键字修饰的指令重排序,但是对于volatile前后无依赖关系的指令则可以随便排序
比如:
int x = 0;
int y = 1;
volatile int z = 20;
x++;
y--;
在语句volatile int z = 20之前,先执行x定义还是先执行y定义无所谓,只有能保证执行到z=20时候x=0,y=1
volatile不保证原子性
public class volatileTest {
// 创建10个线程,每个线程执行1000次对i的自增操作,但最终结果i肯定不是10000
private static volatile int i = 0;
private static final CountDownLatch COUNT_DOWN_LATCH = new CountDownLatch(10);
private static void inc(){
i++;
}
public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 10; j++) {
new Thread(
()->
{
for (int x = 0;x<1000;x++){
inc();
}
COUNT_DOWN_LATCH.countDown();
}
).start();
}
COUNT_DOWN_LATCH.await();
System.out.println(i);
}
}
原子类
JUC包提供了一系列的原子性操作,这些类都是使用非阻塞算法CAS实现的,相比使用锁实现原子性操作在性能上有很大提升
CAS操作
CAS的全称是Compare And Swap 即比较和交换,其算法核心思想如下
执行函数:CAS(V,E,N)
其包含3个参数
V表示需要读写的内存位置
E表示进行比较的预期原值
N表示打算写入的新值
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可
由于CAS操作属于乐观派,它总认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作,基于这样的原理,CAS操作即使没有锁,同样知道其他线程对共享资源操作影响,并执行相应的处理措施。同时从这点也可以看出,由于无锁操作中没有锁的存在,
因此不可能出现死锁的情况,也就是说无锁操作天生免疫死锁
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题:
- ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
- 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
- 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
实现一个简单的,基于 CAS 的线程安全的 value+1 方法
public class SimpleCAS {
private volatile int value;
public void addValue(){
int newVal = value + 1;
while (value != cas(value, newVal)){
newVal = value + 1;
}
}
private synchronized int cas(int expectVal, int newVal){
int curVal = value;
if (expectVal == curVal){
value = newVal;
}
return curVal;
}
// 线程首先读取 value 的值并加 1,如果此时有另一个线程更新了 value,则期望值和 value 不相等,更新失败。更新失败后,循环尝试,重新读取 value 的值,直到更新成功退出循环。
}
原子化基本数据类型
有三个实现类:AtomicBoolean、AtomicInteger、AtomicLong
以AtomicInteger为例:
构造函数:
AtomicInteger()
创建一个新的AtomicInteger,初始值为 0
AtomicInteger(int initialValue)
用给定的初始值创建一个新的AtomicInteger。
方法:
int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction)
使用将给定函数应用于当前值和给定值的结果原子更新当前值,返回更新后的值
int addAndGet(int delta)
将给定的值原子地添加到当前值
boolean compareAndSet(int expect, int update)
如果当前值 ==为预期值,则将该值原子设置为给定的更新值
int decrementAndGet()
原子减1当前值
double doubleValue()
返回此值 AtomicInteger为 double一个宽元转换后
float floatValue()
返回此值 AtomicInteger为 float一个宽元转换后
int get()
获取当前值
int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction)
使用给定函数应用给当前值和给定值的结果原子更新当前值,返回上一个值
int getAndAdd(int delta)
将给定的值原子地添加到当前值
int getAndDecrement()
原子减1当前值
int getAndIncrement()
原子上增加一个当前值
int getAndSet(int newValue)
将原子设置为给定值并返回旧值
int getAndUpdate(IntUnaryOperator updateFunction)
用应用给定函数的结果原子更新当前值,返回上一个值
int incrementAndGet()
原子上增加一个当前值
int intValue()
将 AtomicInteger的值作为 int
void lazySet(int newValue)
最终设定为给定值
long longValue()
返回此值 AtomicInteger为 long一个宽元转换后
void set(int newValue)
设置为给定值
String toString()
返回当前值的String表示形式
int updateAndGet(IntUnaryOperator updateFunction)
使用给定函数的结果原子更新当前值,返回更新的值
boolean weakCompareAndSet(int expect, int update)
如果当前值 ==为预期值,则将值设置为给定更新值
使用:
public class T2 {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(0);
System.out.println(atomicInteger.get());
System.out.println("_____________________________________________");
atomicInteger.addAndGet(100);
System.out.println(atomicInteger.get());
System.out.println("_____________________________________________");
atomicInteger.getAndIncrement();
System.out.println(atomicInteger.get());
System.out.println("_____________________________________________");
atomicInteger.getAndSet(500);
System.out.println(atomicInteger.get());
System.out.println("_____________________________________________");
}
}
原子化对象引用类型
实现类分别是:AtomicReference、AtomicStampedReference AtomicMarkableReference,其中后两个可以实现了解决 ABA 问题的方案。
以AtomicReference为例子:
构造函数:
AtomicReference()
使用null初始值创建新的AtomicReference
AtomicReference(V initialValue)
用给定的初始值创建一个新的AtomicReference
方法:
V accumulateAndGet(V x, BinaryOperator accumulatorFunction)
使用将给定函数应用于当前值和给定值的结果原子更新当前值,返回更新后的值
boolean compareAndSet(V expect, V update)
如果当前值 ==为预期值,则将值设置为给定的更新值
V get()
获取当前值
V getAndAccumulate(V x, BinaryOperator accumulatorFunction)
使用给定函数应用给当前值和给定值的结果原子更新当前值,返回上一个值
V getAndSet(V newValue)
将原子设置为给定值并返回旧值
V getAndUpdate(UnaryOperator updateFunction)
用应用给定函数的结果原子更新当前值,返回上一个值
void lazySet(V newValue)
最终设定为给定值
void set(V newValue)
设置为给定值
String toString()
返回当前值的String表示形式
V updateAndGet(UnaryOperator updateFunction)
使用给定函数的结果原子更新当前值,返回更新的值
boolean weakCompareAndSet(V expect, V update)
如果当前值为 == ,则将原值设置为给定的更新值
例子:
public class T1 {
public static void main(String[] args) {
String initialReference = "initial value referenced";
AtomicReference atomicStringReference =
new AtomicReference(initialReference);
String newReference = "new value referenced";
boolean exchanged = atomicStringReference.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);
System.out.println(atomicStringReference.get());
System.out.println("____________________________________");
exchanged = atomicStringReference.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);
System.out.println(atomicStringReference.get());
}
}