JUC(一)
一、请你谈谈对volatile的理解
Package java.util.concurrent
—> AtomicInteger
Lock
ReadWriteLock
1、volatile是java虚拟机提供的轻量级的同步机制
保证可见性、不保证原子性、禁止指令重排
-
保证可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值
当不添加volatile关键字时示例:
/** * 1验证volatile的可见性 * 1.1 如果int num = 0,number变量没有添加volatile关键字修饰 * 1.2 添加了volatile,可以解决可见性 */ public class VolatileDemo { public static void main(String[] args) { visibilityByVolatile();//验证volatile的可见性 } /** * volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改 */ public static void visibilityByVolatile() { MyData myData = new MyData(); //第一个线程 new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t come in"); try { //线程暂停3s TimeUnit.SECONDS.sleep(3); myData.addToSixty(); System.out.println(Thread.currentThread().getName() + "\t update value:" + myData.num); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } }, "thread1").start(); //第二个线程是main线程 while (myData.num == 0) { //如果myData的num一直为零,main线程一直在这里循环 } System.out.println(Thread.currentThread().getName() + "\t mission is over, num value is " + myData.num); } } class MyData { // int num = 0; volatile int num = 0; public void addToSixty() { this.num = 60; } }
输出结果:
thread1 come in thread1 update value:60 //线程进入死循环
当我们加上
volatile
关键字后,volatile int num = 0;
输出结果为:thread1 come in thread1 update value:60 main mission is over, num value is 60 //程序没有死循环,结束执行
-
不保证原子性
原子性:不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败
验证示例(变量添加volatile关键字,方法不添加synchronized):
/** * 1验证volatile的可见性 * 1.1 如果int num = 0,number变量没有添加volatile关键字修饰 * 1.2 添加了volatile,可以解决可见性 * * 2.验证volatile不保证原子性 * 2.1 原子性指的是什么 * 不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败 */ public class VolatileDemo { public static void main(String[] args) { // visibilityByVolatile();//验证volatile的可见性 atomicByVolatile();//验证volatile不保证原子性 } /** * volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改 */ //public static void visibilityByVolatile(){} /** * volatile不保证原子性 * 以及使用Atomic保证原子性 */ public static void atomicByVolatile(){ MyData myData = new MyData(); for(int i = 1; i <= 20; i++){ new Thread(() ->{ for(int j = 1; j <= 1000; j++){ myData.addSelf(); myData.atomicAddSelf(); } },"Thread "+i).start(); } //等待上面的线程都计算完成后,再用main线程取得最终结果值 try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } while (Thread.activeCount()>2){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+"\t finally num value is "+myData.num); System.out.println(Thread.currentThread().getName()+"\t finally atomicnum value is "+myData.atomicInteger); } } class MyData { // int num = 0; volatile int num = 0; public void addToSixty() { this.num = 60; } public void addSelf(){ num++; } AtomicInteger atomicInteger = new AtomicInteger(); public void atomicAddSelf(){ atomicInteger.getAndIncrement(); } }
执行三次结果为:
//1. main finally num value is 19580 main finally atomicnum value is 20000 //2. main finally num value is 19999 main finally atomicnum value is 20000 //3. main finally num value is 18375 main finally atomicnum value is 20000 //num并没有达到20000
-
禁止指令重排
有序性:在计算机执行程序时,为了提高性能,编译器和处理器常常会对指令做重拍,一般分以下三种
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排顺序是必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性时无法确定的,结果无法预测
重排代码实例:
声明变量:
int a,b,x,y=0
线程1 线程2 x = a; y = b; b = 1; a = 2; 结 果 x = 0 y=0 如果编译器对这段程序代码执行重排优化后,可能出现如下情况:
线程1 线程2 b = 1; a = 2; x= a; y = b; 结 果 x = 2 y=1 这个结果说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的
volatile实现禁止指令重排,从而避免了多线程环境下程序出现乱序执行的现象
内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,他的作用有两个:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化。如果在之间插入一Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排顺序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
graph TB subgraph bbbb["对Volatile变量进行读操作时,<br>回在读操作之前加入一条load屏障指令,<br>从内存中读取共享变量"] ids6[Volatile]-->red3[LoadLoad屏障] red3-->id7["禁止下边所有普通读操作<br>和上面的volatile读重排序"] red3-->red4[LoadStore屏障] red4-->id9["禁止下边所有普通写操作<br>和上面的volatile读重排序"] red4-->id8[普通读] id8-->普通写 end subgraph aaaa["对Volatile变量进行写操作时,<br>回在写操作后加入一条store屏障指令,<br>将工作内存中的共享变量值刷新回到主内存"] id1[普通读]-->id2[普通写] id2-->red1[StoreStore屏障] red1-->id3["禁止上面的普通写和<br>下面的volatile写重排序"] red1-->id4["Volatile写"] id4-->red2[StoreLoad屏障] red2-->id5["防止上面的volatile写和<br>下面可能有的volatile读写重排序"] end style red1 fill:#ff0000; style red2 fill:#ff0000; style red4 fill:#ff0000; style red3 fill:#ff0000; style aaaa fill:#ffff00; style bbbb fill:#ffff00;
2、JMM(java内存模型)
JMM(Java Memory Model)本身是一种抽象的概念,并不真实存在,他描述的时一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁时同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有的成为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在**主内存,主内存是贡献内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先概要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存的变量副本拷贝**,因此不同的线程件无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,期间要访问过程如下图:
- 可见性
- 原子性
- 有序性
3、你在哪些地方用过volatile
当普通单例模式在多线程情况下:
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 构造方法SingletonDemo()");
}
public static SingletonDemo getInstance() {
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
//构造方法只会被执行一次
// System.out.println(getInstance() == getInstance());
// System.out.println(getInstance() == getInstance());
// System.out.println(getInstance() == getInstance());
//并发多线程后,构造方法会在一些情况下执行多次
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, "Thread " + i).start();
}
}
}
其构造方法在一些情况下会被执行多次
解决方式:
-
单例模式DCL代码
DCL (Double Check Lock双端检锁机制)在加锁前和加锁后都进行一次判断
public static SingletonDemo getInstance() { if (instance == null) { synchronized (SingletonDemo.class) { if (instance == null) { instance = new SingletonDemo(); } } } return instance; }
大部分运行结果构造方法只会被执行一次,但指令重排机制会让程序很小的几率出现构造方法被执行多次
DCL(双端检锁)机制不一定线程安全,原因时有指令重排的存在,加入volatile可以禁止指令重排
原因是在某一个线程执行到第一次检测,读取到instance不为null时,instance的引用对象可能没有完成初始化。instance=new SingleDemo();可以被分为一下三步(伪代码):
memory = allocate();//1.分配对象内存空间 instance(memory); //2.初始化对象 instance = memory; //3.设置instance执行刚分配的内存地址,此时instance!=null
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化时允许的,如果3步骤提前于步骤2,但是instance还没有初始化完成
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance示例未必已初始化完成,也就造成了线程安全问题。
-
单例模式volatile代码
为解决以上问题,可以将SingletongDemo实例上加上volatile
private static volatile SingletonDemo instance = null;
二、CAS你知道吗
1、CompareAndSet----比较并交换
AtomicInteger.conpareAndSet(int expect, indt update)
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
第一个参数为拿到的期望值,如果期望值没有一致,进行update赋值,如果期望值不一致,证明数据被修改过,返回fasle,取消赋值
例子:
/**
* 1.CAS是什么?
* 1.1比较并交换
*/
public class CASDemo {
public static void main(String[] args) {
checkCAS();
}
public static void checkCAS(){
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data is " + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2014) + "\t current data is " + atomicInteger.get());
}
}
输出结果为:
true current data is 2019
false current data is 2019
2、CAS底层原理?对Unsafe的理解
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较知道主内存和工作内存中的值一直为止
-
atomicInteger.getAndIncrement();
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
-
Unsafe
-
是CAS核心类,由于Java方法无法直接访问地层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存数据。Unsafe类存在于
sun.misc
包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
-
变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存便宜地址获取数据的
-
变量value用volatile修饰,保证多线程之间的可见性
-
-
CAS是什么
CAS全称呼Compare-And-Swap,它是一条CPU并发原语
他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件的功能,通过他实现了原子操作。由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致问题。
//unsafe.getAndAddInt public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
var1 AtomicInteger对象本身
var2 该对象的引用地址
var4 需要变动的数据
var5 通过var1 var2找出的主内存中真实的值
用该对象前的值与var5比较;
如果相同,更新var5+var4并且返回true,
如果不同,继续去之然后再比较,直到更新完成
3、CAS缺点
-
** 循环时间长,开销大**
例如getAndAddInt方法执行,有个do while循环,如果CAS失败,一直会进行尝试,如果CAS长时间不成功,可能会给CPU带来很大的开销
-
只能保证一个共享变量的原子操作
对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性
-
ABA问题
三、原子类AtomicInteger的ABA问题?原子更新引用?
1、ABA如何产生
CAS算法实现一个重要前提需要去除内存中某个时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如线程1从内存位置V取出A,线程2同时也从内存取出A,并且线程2进行一些操作将值改为B,然后线程2又将V位置数据改成A,这时候线程1进行CAS操作发现内存中的值依然时A,然后线程1操作成功。
尽管线程1的CAS操作成功,但是不代表这个过程没有问题
2、如何解决?原子引用
示例代码:
public class AtomicRefrenceDemo {
public static void main(String[] args) {
User z3 = new User("张三", 22);
User l4 = new User("李四", 23);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
}
}
@Getter
@ToString
@AllArgsConstructor
class User {
String userName;
int age;
}
输出结果
true User(userName=李四, age=23)
false User(userName=李四, age=23)
3、时间戳的原子引用
新增机制,修改版本号
/**
* ABA问题解决
* AtomicStampedReference
*/
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("=====以下时ABA问题的产生=====");
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "Thread 1").start();
new Thread(() -> {
try {
//保证线程1完成一次ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "Thread 2").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("=====以下时ABA问题的解决=====");
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp());
}, "Thread 3").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference());
}, "Thread 4").start();
}
}
输出结果:
=====以下时ABA问题的产生=====
true 2019
=====以下时ABA问题的解决=====
Thread 3 第1次版本号1
Thread 4 第1次版本号1
Thread 3 第2次版本号2
Thread 3 第3次版本号3
Thread 4 修改是否成功false 当前最新实际版本号:3
Thread 4 当前最新实际值:100