一、谈谈对volatile的理解
1.什么是可见性?
线程A修改了自己工作内存的数据时,线程B可以预见
public class VolatileDemo {
private static volatile int num = 0;
private static void changeNum() {
num = 60;
}
public static void main(String[] args) {
new Thread(() -> {
// 睡眠时间 模拟业务执行,营造main线程的工作内存,先获取到了主内存的num的值后,线程 A 在修改num的值
try { Thread.sleep(100);} catch (InterruptedException e) { e.printStackTrace(); }
changeNum();
System.out.println(Thread.currentThread().getName() + " num value:" + num);
}, "线程 A").start();
while (num == 0) {
// 如果没有保证可见性,main线程,没有读取到 线程 A 修改后的num的值,这里会一直死循环
}
System.out.println(Thread.currentThread().getName() + " num value:" + num);
}
}
2.不具有原子性
多个线程获取到初始值以后,在自己的工作内存修改了数据,准备将自己工作内存的数据赋值给主内存中的共享变量时,出现了重复操作,最终导致数据冲突
3.禁止指令重排
指令重排 - 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排(不影响最终执行结果的前提下,改变代码的执行顺序)
二、谈谈JMM是什么?
JMM(Java内存模型 Java Memory Model, 简称JMM)
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在自己的工作内存中进行
1.JMM关于同步的规定
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
三、如何解决volatile不具有的原子性问题?
使用 java.util.concurrent.atomic包下的原子类来演示可见性+原子性代码
public class AtomicDemo {
// 原子变量 保证JMM的原子性
private static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try { Thread.sleep(100);} catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(atomicInteger.incrementAndGet());
}, String.valueOf(i)).start();
}
}
}
// 注意:这里CAS算法解决了原子性的问题
四、你在哪些地方用到过volatile?
1.单例模式DCL代码
public class SigleDemo {
// 因为可能cup会指令重排,导致在对象还没完全被初始化好
private static volatile SigleDemo instance = null;
public SigleDemo() {
System.out.println(Thread.currentThread().getName() + " --- 实例化了。。。");
}
public static SigleDemo getInstance() {
if (instance == null) {
synchronized (SigleDemo.class) {
if (instance == null) {
instance = new SigleDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
getInstance();
}, String.valueOf(i)).start();
}
}
}
2.JUC包里大规模都有使用
五、CAS算法你知道吗?
1.比较并交换
CAS(Compare And Swap) ,他是一条CPU并发原语,作用判断内存某个位置的值是否为预期值,如果是则更改为更新的值,否则执行自旋操作,重新获取内存某个地址的值,直到内存某个位置的值是为预期值。
并且原语的执行必须是连续性的,在执行的过程中不允许被中断,也就是说CAS是CPU的一条原子指令,不会有数据不一致的问题
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
// true 修改成功 compareAndSet() 比较并交换
System.out.println(atomicInteger.compareAndSet(5, 2020));
// false 修改失败,因为上面已经将主内存的值修改为2020了,不再是预估的5
System.out.println(atomicInteger.compareAndSet(5, 2020));
}
2.CAS底层原理?如果知道,谈谈你对UnSafe的理解
CAS的底层原理是通过Unsafe类和自旋的方式实现原子性的
1)Unsafe类是什么?
Unsafe是CAS的核心类,由于Java方法无法直接访问底层系統,需要通过本地native修饰的方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在sun.misc包中,其内部方法操作可以像C的指针一祥直接操作内存,因カJava中CAS操作的抗行依赖于Unsafe类的方法。
注意:Unsafe中的所有方法都native修饰也就是说Unsafe类中的方法都是直接调用操作系统底层资源执行相应任务
2)CAS是什么?
CAS(Compare - And - Swap) 比较并交换,他是一条CPU并发原语,作用判断内存某个位置的值是否为预期值,如果是则更改为更新的值,否则执行自旋操作,重新获取内存某个地址的值,直到内存某个位置的值是为预期值。
并且原语的执行必须是连续性的,在执行的过程中不允许被中断,也就是说CAS是CPU的一条原子指令,不会有数据不一致的问题
CAS算法的自旋比较代码
3)为什么用CAS而不用Synchrnized?
Synchronized是只允许一个线程运行,虽然一致性保证了,但是降低并发性,而cas底层是unsafe类,并且不加锁,即保证一致性,也允许多个线程同时操作,并发量得到保障
区别:简单来说CAS适合(多读)的场景,synchronized比较通常多用于(多写)
3.CAS的缺点是什么?
可以看到getAndAddInt()一直在循环获取内存中的值和线程自身工作内存中的共享变量副本进行对比,如果CAS一直对比失败,会一直进行尝试,如果CAS长时间一直不成功,会给CPU带来很大开销
六、谈谈原子类AtomicInteger的ABA问题,原子更新引用知道吗?
1.ABA问题怎么产生的?
CAS会导致“ABA问题”。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B, 然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
2.原子引用 AtomicReference
/**
* ABA问题示例
*
* @Author mashanghaoyun
* @Date 2020/11/28 13:58
* @Version 1.0
**/
public class AtimicReferenceDmeo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
new Thread(() -> {
// 第一次修改为101
atomicReference.compareAndSet(100, 101);
// 第二次修改回原来的100
atomicReference.compareAndSet(101, 100);
}, "线程 1").start();
new Thread(() -> {
// 暂停一秒,保证上面的 线程完成一次ABA操作
try { Thread.sleep(1000);} catch (InterruptedException e) { e.printStackTrace(); }
boolean flag = atomicReference.compareAndSet(100, 2020); // true
System.out.println(flag);
}, "线程 2").start();
}
}
3.ABA问题的解决 - 时间戳(类似乐观锁)原子引用AtomicStampedReference
/**
* ABA问题的解决
*
* @Author mashanghaoyun
* @Date 2020/11/28 16:16
* @Version 1.0
**/
public class AtomicStampedReferenceDemo {
static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
new Thread(() -> {
int stamp = stampedReference.getStamp(); // 获取当前的版本号
Integer reference = stampedReference.getReference();// 预期的值
// 暂停一会,为了让线程B 与线程A 第一次获取同一个版本号
System.out.println(Thread.currentThread().getName() + " 第一次获取的版本号为:" + stamp);
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
// 参数: 预期的值 更新的值 预期的版本值 新的版本值
boolean flag = stampedReference.compareAndSet(reference, 101, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + " 第二次操作成功否:" + flag + " 最新的数据:" +
stampedReference.getReference() + "\t\t" + stampedReference.getStamp());
boolean flag2 = stampedReference.compareAndSet(
stampedReference.getReference(),
100,
stampedReference.getStamp(),
stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 第三次操作成功否:" + flag2 + " 最新的数据:" +
stampedReference.getReference() + "\t\t" + stampedReference.getStamp());
}, "线程 A").start();
new Thread(() -> {
int stamp = stampedReference.getStamp();
Integer reference = stampedReference.getReference();
System.out.println(Thread.currentThread().getName() + " 第一次获取的版本号:" + stamp);
// 暂停本线程的执行,为了让线程A 产生一个 "ABA"的问题
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
boolean flag = stampedReference.compareAndSet(reference, 2020, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + " 第二次操作版本号:" + stampedReference.getStamp() + "\t成功与否:" + flag);
}, "线程 B").start();
}
}