线程安全之可见性(Volatile)和原子性
线程之可见性
可见性原理分析
可见性问题:让一个线程对共享变量的修改,能够及时的被其他线程看到。
JAVA 内存模型规定:
对volatile变量v的写入,与所有其他线程后续对v的读同步
要满足这些条件,所以volatile 关键字就有以下的功能:
- 禁止缓存;
volatile 变量访问控制符会加个ACC_VOLATILE
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2 - 对volatile 变量相关的指令不做重排序。
共享变量的定义
可以在线程之间共享的内存称为共享内存或堆内存。
所有实例字段,静态字段和数组元素都存在在堆内存中,这些字段和数组都是共享变量
冲突发生:如果至少有一个访问是写操作,那么对同一个变量的两次访问是冲突的。
这些能被多个线程访问的共享变量时内存模型规范的对象。
线程间操作的定义
1.线程间的操作是指:一个程序执行的操作可被其他线程感知或被其他线程直接影响。
2. Java 内存模型只描述线程间操作,不描述线程内操作,线程内操作按照线程内语义执行。线程间的操作有:
read 一般读操作,非volatile读
write 一般写操作,非volatile写
volatile read
volatile write
Lock.(锁monitor)、Unlock
线程的第一个和最后一个操作
外部操作
所有线程间的操作都存在可见性问题,JMM需要对其进行规范
同步的规则定义
对volatile变量v的写入,与所有其他线程后续对v的读同步。
对于监视器m的解锁与所有后续操作对于m的加锁同步。
Happens-before 先行发生原则
happens before 用于描述两个有冲突的动作之间的顺序,如果有一个action happends before 另一个action,则第一个操作被第二个操作可见,JVM 需要实现如下happens-before 规则:
某个线程中的每个动作都happens-before 该线程后面的动作。
某个管程上的unlock 动作和happens-before 同一个管程上后续的lock动作。
对某个volatile字段的写操作happens-before 每个后续对该volatile字段的读操作。
在某个线程对象上调用start() happens-before 被启动线程中的任意动作。
如果在线程t1 中成功执行了t2.join(),则t2 中的所有操作对t1可见。
如果某个动作a happens-before动作b,且b happens-before 动作c,则有 a happens-before c。
当程序包含两个没有被happens-before关系排序的冲突访问时,就称存在数据竞争,遵守了这个原则,也就意味着有些代码不能进行重排序,有些数据不能缓存!
Final 修饰符
final 在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的final 字段的正确构造版本。伪代码示例: f = new FinalDemo(); 读取到的f.x 一定是最新的值,x 为final 修饰的字段。
如果在构造函数中设置字段后发生读取,则会看到该final 字段分配的值,否则它将看到默认值:伪代码示例:pulbic finalDemo(){x=1;y=x} y会等于1;
Word Tearing 字节处理
有些处理器(尤其是早起的Alphas处理器)没有提供些单个字节的功能,在这样的处理器上更新byte数组,若只是简单的读取整个内容,更新对象的字节,然后将这个内容再写回内存,将是不合法的。
这个问题有时被分为“字分裂(word tearing)”更新单个字节有难度的处理器,就需要寻求其它方式来解决问题。因此在开发找那个尽量不要对byte[] 中的元素进行重新赋值,更不要在多线程程序这样做。
线程可见性总结
保证变量可见性的方式:
-
final变量
-
synchronized 关键字:
synchronized 语义规范:
1) 进入同步代码块前,先清空工作内存中的所有共享变量,从主内存中重新加载。
2) 解锁前必须把修改的共享变量同步回主内存。
注意:如果线程竞争两把不同的锁则不能保证
如何保证线程安全:
1) 锁机制保护共享资源,只有获得锁的线程才可操作共享资源。
2) Synchronized 语义规范保证了修改共享资源后,会同步主内存,就做到了线程安全。 -
用volatile修饰
1) 使用volatile变量时,必须重新从主内存加载,并且read,load是连续的。
2) 修改volatile变量时,必须立马同步回主内存,并且store,write是连续的。
线程安全之原子性
原子操作
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
将整个操作视为一个整体,资源在该次操作中保持一致,这是原子性的核心特性。
存在的问题及分析原因
示例代码:
/**
* 实现累加的功能
*/
public class Counter {
volatile int i = 0;
public void add() {
i++;
}
}
/**
* 累加器的测试类
*/
public class CounterTest {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
for (int i = 0; i < 8; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter.add();
}
System.out.println("done...");
}).start();
}
Thread.sleep(6000L);
//期望的结果是:i*j 为循环次数 = 80000
System.out.println(counter.i);
}
}
输出的结果:
done...
done...
done...
done...
done...
done...
done...
done...
27661
存在的问题
输出结果最终是: 27661 并不是我们所期望的80000。再多运行几次呢,输出的结果也是每次都不同,但都不是我们想要的值。那么我们想到可能线程并发的问题,我们将累加器加上锁或者加上synchronized 关键字试试看:
1) 使用synchronized
关键字实现的Counter :
/**
* 加synchronized 的 Counter
*/
public class Counter {
volatile int i = 0;
// 已加上同步关键字
public synchronized void add() {
i++;
}
}
2) 使用 ReentrantLock
实现的Counter
/**
* 加锁的Counter
*/
public class Counter {
volatile int i = 0;
Lock lock = new ReentrantLock();
//已加上锁
public void add() {
lock.lock();
i++;
lock.unlock();
}
}
再使用上面的CounterTest 分别调用不同写法的Counter 类,发现运行结果都是我们期望的80000:
done...
done...
done...
done...
done...
done...
done...
done...
80000
这就说明以上我们的猜测是正确的,但是为什么会出现不同的结果呢?接下来看以下分析:
首先我们还是将Counter 类还原到最初的状态(不加锁也不加synchronized):
然后进入到Counter.class 所在的目录打开终端执行(反编译查看字节码):
javap -v -p Counter.class
字节码显示:
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
将以上的指令码对应内存分析,假如现在有t1 线程,按照从上到下的指令码执行,无论加多少次,结果是我们所期望的。
以上是只有一个线程的情况,现在又加入了一个线程t2, 按照同样的指令码从上到下执行,t1,t2 执行完一次后 i 的结果是1而不是2,前面讲到原子操作,是一个不可中断的操作。 按照下图可以知道,对于t1线程来说,从上到下执行的指令码应为一个原子操作,不可让其它线程同时执行。由于前面提到可以使用锁机制来解决这一系列问题,但是现有另一种实现反方式!
解决办法
1) 借助sun.misc.Unsafe 类:
public class CounterUnsafe {
volatile int i = 0;
private static Unsafe unsafe = null;
private static long valueOffset;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
Field declaredField_i = CounterUnsafe.class.getDeclaredField("i");
valueOffset = unsafe.objectFieldOffset(declaredField_i);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 自旋
*/
public void add() {
for (; ; ) {
int current = unsafe.getIntVolatile(this, valueOffset);
if (unsafe.compareAndSwapInt(this, valueOffset, current, current + 1)) {
break;//如果修改不成功就自旋,修改成功就退出循环
}
}
}
使用CounterTest 类来创建CounterUnsafe 对象作为counter 来调用add():
public class Demo1_CounterTest {
public static void main(String[] args) throws InterruptedException {
//创建CounterUnsafe 对象:
CounterUnsafe counter = new CounterUnsafe();
for (int i = 0; i < 8; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter.add();
}
System.out.println("done...");
}).start();
}
Thread.sleep(6000L);
//最终输出的结果是我们期望的结果:80000
System.out.println(counter.i);
}
}
if (unsafe.compareAndSwapInt(this, valueOffset, current, current + 1)) {...}
如果您觉得以上条件判断眼熟,那么您一定了解过:java.util.concurrent.atomic (并发原子类的包路径)
2)使用 CAS(Compare and swap)
Compare and swap 比较和交换。属于硬件同步原语,处理器提供了基本内存操作的原子性保证。
CAS 操作需要输入两个值,一个旧值A(期望操作前的值)和一个新值B, 在操作期间先对旧值进行比较,若没有发生变化,才交换成新值,发生了变化则不交换。
JAVA 中的sun.misc.Unsafe
类,提供了compareAndSwapInt()
和compareAndSwapLong()
等几个方法实现CAS。
CAS 原理
同时有多个操作去修改同一个值时,先比较内存中期望的值是否为CAS中的第一个参数,如果是则用新值替换旧值,如果不是则不修改(自旋)。
CAS 操作的缺点
- 循环+cas ,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗。
- 仅针对单个变量的操作,不能用于多个变量来实现原子操作。
- ABA 问题。
ABA 问题
线程1,线程2同时执行CAS(0,1)操作,将i 的值由0 改为1
假设线程1操作成功,线程2操作失败
紧接着线程1执行了CAS(1,0),将i 的值改回0
ABA 问题的解决办法
在每次CAS操作中带上版本号,作为每次的操作的区分。AtomicStampedReference 中的源码,initialStamp为初始化的版本号:
此处使用 AtomicInteger
实现累加的具体示例:
累加器:
public class CounterAtomic {
AtomicInteger i = new AtomicInteger(0);
public void add() {
i.incrementAndGet();
}
}
测试类:
public class Demo1_CounterTest {
public static void main(String[] args) throws InterruptedException {
CounterAtomic counter = new CounterAtomic();
for (int i = 0; i < 8; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter.add();
}
System.out.println("done...");
}).start();
}
Thread.sleep(6000L);
System.out.println(counter.i);
}
}