1.3Volatile&原子性、核心概述
Volatile关键字核心概念与应用
Volatile概念:Volatile关键字的主要作用是使变量在多个线程间可见。
作用:
- 在多线程间可以进行变量的变更,使得线程间进行数据的共享可见
- 阻止指令重排序,
happens-before
public class UseVolatile extends Thread {
private volatile boolean isRunning = true;
private void setRunning(boolean isRunning) {
this.isRunning = isRunning;
}
public void run(){
System.err.println("进入run方法...");
while(isRunning == true) {
//....
}
System.err.println("线程停止!");
}
public static void main(String[] args) throws InterruptedException {
UseVolatile uv = new UseVolatile();
uv.start();
Thread.sleep(2000);
//修改isRunning = false
uv.setRunning(false);
System.err.println("isRunning的值已经被设置成了false!");
}
}
//运行结果
进入run方法...
isRunning的值已经被设置成了false!
线程停止!
Volatile关键字内存模型分析
一个线程可以执行的操作有使用(use)
、赋值(assign)
、装载(load)
、存储(store)
、锁定(lock)
、解锁(unlock)
。
而主内存可以执行的操作有读(read)
、写(write)
、锁定(lock)
、解锁(unlock)
,每个操作都是原子的。
volatile
的作用就是强制线程到主内存(共享内存)里去读取变量,而不去线程工作内存区里去读取,从而实现了多个线程间的变量可见。也就是满足线程安全的可见性。
Volatile关键字happens-before与内存屏障概念、重排序
JVM中,所有的变量都存储在主内存中, 每一个线程都有一个私有的本地内存,本地内存中存储了该线程使用到的变量在主内存中拷贝。
线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量也不例外)
happens-before规则
- i = 1; // 操作 A
- j = i; // 操作 B
如果操作A happens-before 于操作B,那么就可以确定,操作B执行完之后,j 的值一定为 1;
因为happens-before关系可以向程序员保证:在操作B执行之前,操作A的执行后的影响[或者说结果](修改 i 的值)操作B是可以观察到的[或者说可见的]
换句话说,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,在这个例子就是A操作的结果要对B操作可见,那么必然存在A happens-before B
简而言之:使用happens-before的概念来阐述操作之间的内存可见性
- 程序顺序规则:
- 一个线程中的每个操作,happens-before于该线程中的任意后续操作(也就是说你写的操作,如果是单线程执行,那么前面的操作[程序逻辑上]就会happens-before于后面的操作)这里的影响指修改了 i 变量的值
- 监视器锁规则:
- 对一个锁的解锁,happens-before 于随后对这个锁的加锁
- volatile变量规则
- 对一个 volatile域的写,happens-before于任意后续对这个volatile域的读
- 传递性规则:
- 如果 A happens-before B,且 B happens-before C,那么A happens-before C。
指令重排
传递性在JDK中,JAVA语言为了维持顺序内部的顺序化语义,也就是为了保证程序的最终运行结果需要和在单线程严格意义的顺序化环境下执行的结果一致,程序指令的执行顺序有可能和代码的顺序不一致,这个过程就称之为指令的重排序。
指令重排序的意义在于:JVM能根据处理器的特性,充分利用多级缓存,多核等进行适当的指令重排序,使程序在保证业务运行的同时,充分利用CPU的执行特点,最大的发挥机器的性能。
Atomic类关键字
Atomic系列类封装了一系列的基础类型和对象操作,其主要目的就是为了实现原子性,
主要核心类如下:
- AtomicInteger
- AtomicLong
- AtomicBoolean
- AtomicIntegerArray
- AtomicLongArray
- AtomicReference
Atomic类实战
public class NoUseAtomic {
private static int count = 0;
public int add(){
return count += 10;
}
public static void main(String[] args) {
UseAtomic ua = new UseAtomic();
List<Thread> list = new ArrayList<>();
// 运行100次,每次+10,预期的最终结果应该是1000
for(int i =0; i < 100; i++) {
list.add(new Thread(new Runnable() {
@Override
public void run() {
System.err.println("累计结果:" + ua.add());
}
}));
}
for(Thread t : list) {
t.start();
}
}
}
// 运行结果
多次运行的话,会发生没有输出1000的情况出现,因为对变量count的操作并不是原子性的
public class UseAtomic {
// 使用 AtomicInteger 类
private static AtomicInteger count = new AtomicInteger(0);
public int add(){
// 相当于count+10
return count.addAndGet(10);
}
public static void main(String[] args) {
UseAtomic ua = new UseAtomic();
List<Thread> list = new ArrayList<>();
//如果使用atomicIntger 最终的结果 一定是: 1000
for(int i =0; i < 100; i++) {
list.add(new Thread(new Runnable() {
@Override
public void run() {
System.err.println("累计结果:" + ua.add());
}
}));
}
for(Thread t : list) {
t.start();
}
}
}
//运行结果
使用了AtomicInteger,保证了操作的原子性,因此输出结果中一定会有1000
Atomic类实战应用爬坑手记
在对Atomic类操作的时候,如果是有多个操作执行,那么就是非原子性的,需要加synchronized
进行修饰,保证Atomic类操作整体原子性!
public class UseAtomic {
private static AtomicInteger count = new AtomicInteger(0);
// add()方法,让count+10
public int add(){
count.addAndGet(3); // + 3
count.addAndGet(4); // + 4
count.addAndGet(2); // + 2
return count.addAndGet(1); // + 1
}
public static void main(String[] args) {
UseAtomic ua = new UseAtomic();
List<Thread> list = new ArrayList<>();
//如果使用atomicIntger 最终的结果 一定是: 1000
for(int i =0; i < 100; i++) {
list.add(new Thread(new Runnable() {
@Override
public void run() {
System.err.println("累计结果:" + ua.add());
}
}));
}
for(Thread t : list) {
t.start();
}
}
}
//运行结果
输出结果中仍有1000,但是多次运行发现,输出列表中有非10的倍数出现,如573 607等,与每次输出都是10的倍数的初衷不符合。
出现这样的原因是因为add方法并不是一个原子方法,需要加锁
public class UseAtomic {
private static AtomicInteger count = new AtomicInteger(0);
// 给add()方法添加 synchronized 关键字
public synchronized int add(){
count.addAndGet(3); // + 3
count.addAndGet(4); // + 4
count.addAndGet(2); // + 2
return count.addAndGet(1); // + 1
}
public static void main(String[] args) {
UseAtomic ua = new UseAtomic();
List<Thread> list = new ArrayList<>();
//如果使用atomicIntger 最终的结果 一定是: 1000
for(int i =0; i < 100; i++) {
list.add(new Thread(new Runnable() {
@Override
public void run() {
System.err.println("累计结果:" + ua.add());
}
}));
}
for(Thread t : list) {
t.start();
}
}
}
//运行结果
add方法加锁后,每次输出的都是10的倍数,并且一定会输出1000
JDK Unsafe类使用与CAS原子特性
java.util.concurrent.atomic包,其中大量的用到Unsafe这个类。
由于java不能直接访问操作系统底层,而是通过本地方法来访问。
Unsafe类提供了硬件级别的原子操作,主要提供了以下功能:
- 内存操作
- 字段的定位与修改
- 挂起与恢复
- CAS操作(乐观锁)
内存操作
类中提供的3个本地方法allocateMemory
、reallocateMemory
、freeMemory分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应
字段的定位与修改
可以定位对象某字段的内存位置也可以修改对象的字段值,即使它是私有的.
挂起与恢复
将一个线程进行挂起是通过park
方法实现的,调用 park
后,线程将一直阻塞直到超时或者中断等条件出现。
unpark
可以终止一个挂起的线程,使其恢复正常。
整个并发框架中对线程的挂起操作被封装在 LockSupport
类中,LockSupport
类中有各种版本pack
方法,但最终都调用了Unsafe.park()
方法
CAS操作(乐观锁)
首先介绍一下什么是Compare And Swap(CAS)?简单的说就是比较并交换!
CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
无论哪种情况,它都会在 CAS 指令之前返回该位置的值。
CAS 有效地说明了:
“我认为位置 V 应该包含值 A;
如果包含该值,则将 B 放到这个位置;
否则,不要更改该位置,只告诉我这个位置现在的值即可。”
Java并发包(java.util.concurrent)中大量使用了CAS操作,涉及到并发的地方都调用了
sun.misc.Unsafe类方法进行CAS操作,在Unsafe中是通过compareAndSwapXXX方法实现的。
底层方法