- 请谈谈你对volatile的理解
volatile是Java虚拟机提供的轻量级的同步机制:
三大特性:
保证可见性、不保证原子性、禁止指令重排
- 谈谈JMM
Java内存模型(Java Memory Model)本身是一种抽象的概念,并不真实存在,描述的是一组规范或规则,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
1、线程解锁前,必须把共享变量的值刷新回主内存
2、线程加锁前,必须读取主内存的最新值到自己的工作内存
3、加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存
,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存
,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图·:
可见性、原子性、有序性
Volatile不保证原子性案例分析
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 1 验证volatile可见性
* 1.1 假设int num = 10; num变量之前未添加volatile关键字修饰,即没有可见性;
* 1.2 添加了volatile,可以解决可见性问题。
*
* 2 验证volatile不保证原子性
* 2.1 原子性指的是什么含义?不可分割,完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割(需要整体完整)。
* 要么同时成功,要么同时失败
* 2.2 是否可以保证原子性?
* 2.3 why?
* 2.4 如何解决原子性?
* 加sync
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
myData.addAtomic();
}
}, String.valueOf(i)).start();
}
// 需要上述20个线程全部计算完成后,看main线程最终取得的结果
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "\t int type, finally num value" + myData.num);
System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type, finally num value" + myData.atomicInteger);
}
// volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改
private static void keepVisibility() {
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
TimeUnit.SECONDS.sleep(3);
}catch (InterruptedException e){
e.printStackTrace();
}
myData.changeValue();
System.out.println(Thread.currentThread().getName() + "\t update num value: " + myData.num);
}, "AAA").start();
//第二个线程就是我们main线程
while (myData.num == 10){
// main线程一直等待循环,直到num的值不再等于零
}
System.out.println(Thread.currentThread().getName() + "\t mission is over");
}
}
class MyData{
// int num = 10;
volatile int num = 10;
public void changeValue(){
this.num = 20;
}
// 此时num前加volatile关键字修饰的,即不保证原子性
public void addPlusPlus(){
num++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndIncrement();
}
}
指令做重排分类
源代码==>编译器优化重排==》指令并行的重排==》内存系统的重排==》最终执行的指令
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致;
处理器在进行重排序时必须考虑指令之间的数据依赖性;
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性时无法确定的,结果无法预测;
禁止指令重排总结:
public class ReSortSeqDemo {
int a = 0;
boolean flag = false;
public void method01(){
a = 1;
flag = true;
}
// 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性时无法确定的,结果无法预测;
public void method02(){
if (flag){
a = a + 5;
System.out.println("*****retValue: " + a);
}
}
}
- 实际工作或者学习中在哪些地方用到过volatile?
// 单例模式
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName() + "\t 构造方法");
}
// DCL -> Double Check Lock(双端检锁机制)
public static SingletonDemo getInstance(){
if (instance == null){
synchronized (SingletonDemo.class){
if (instance == null){
instance = new SingletonDemo();
}
}
}
return instance;
}
}
但是指令重排只会保证串行语义的执行的一致性(单线程),并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
CAS你知道吗?
比较并交换
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author smartaotao
* @version 1.0.0
* @Description 比较并交换
* @createTime 2022-11-30 21:31:00
*/
public class CasDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
// main do things
System.out.println(
atomicInteger.compareAndSet(5, 2021) + "\t current data: " + atomicInteger.get());
System.out.println(
atomicInteger.compareAndSet(5, 2022) + "\t current data: " + atomicInteger.get());
}
}
CAS底层原理?如果知道,谈谈你对UnSafe的理解
自旋锁=CAS原理 + UnSafe类
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
1、UnSafe类是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native) 方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意Unsafe 类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用作系统底层资源执行相应务
2、变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
3、变量value用volatile修饰,保证了多线程之间的内存可见性。
CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
CAS缺点:
1.循环时间长开销很大:如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销;
2.只能保证一个共享变量的原子操作;
3.ABA问题;
原子类AtomicInteger的ABA【狸猫换太子】问题谈谈?原子更新引用知道吗?
CAS会导致“ABA问题”
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
class User{
private String userName;
private int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User user01 = new User("王飞", 18);
User user02 = new User("李白", 29);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user01);
System.out.println(atomicReference.compareAndSet(user01, user02) + "\t" + atomicReference.get().toString());
}
}