面试题之JUC

对于JUC方面的面试题,我们首先应该讲一下JMM

    • JMM(Java Memory Model) Java内存模型
    • 谈谈对volatile关键字的理解
    • 哪些地方用到过volatile?
      • 单例模式的安全问题
    • CAS问题
    • CAS底层原理
    • CAS缺点
    • ABA问题
      • AtomicReference
      • AtomicStampedReference和ABA问题的解决

JMM(Java Memory Model) Java内存模型

首先看一下java内存模型图
对于JMM 是有以下要求:
1.保持原子性
2.保持可见性
3.有序性

面试题之JUC_第1张图片
对于一些操作,分别对应在下面图
面试题之JUC_第2张图片
面试题之JUC_第3张图片

/**
 * 
 */
package com.matao.concurrent.atomic;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author MT
 *
 */
public class ThreandDemo {
	public static  boolean initFlag = true;
	
	public static void main(String[] args) {
		new Thread( new Runnable() {
			public void run() {
				System.out.println("thread1 is runing");
				
				while(initFlag){
				}
				System.out.println("thread1 ends");
			}
		}).start();
		
		//睡眠两秒钟
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				System.out.println("thread2 is runing");
				initFlag = false;
				
			}
		}).start();
		
		
	}
}

在这里插入图片描述
对于以上代码,我们会发现代码运行之后,会处于一直运行状态,但当我们加上了volatile关键字就会让程序正常结束

谈谈对volatile关键字的理解

  1. volatile是jvm提供的轻量级的同步机制
    volatile有以下特性:
    1)保持可见性
    2)不保证原子性
    3)禁止指令重排
    验证可见性的代码:
/**
 * 
 */
package com.matao.concurrent.atomic;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author MT
 *
 */
public class ThreandDemo {
	public static volatile boolean initFlag = true;
	
	public static void main(String[] args) {
		new Thread( new Runnable() {
			public void run() {
				System.out.println("thread1 is runing");
				
				while(initFlag){
				}
				System.out.println("thread1 ends");
			}
		}).start();
		
		//睡眠两秒钟
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				System.out.println("thread2 is runing");
				initFlag = false;
				
			}
		}).start();
		
		
	}
}

可见性就是指线程之间可以共享,比如线程2对initFlag的修改,线程1是可见的,还可以共享,
volatile并不能保证操作的原子性。这是因为,比如一条number++的操作,会形成3条指令。

getfield        //读
iconst_1	//++常量1
iadd		//加操作
putfield	//写操作

假设有3个线程,分别执行number++,都先从主内存中拿到最开始的值,number=0,然后三个线程分别进行操作。假设线程0执行完毕,number=1,也立刻通知到了其它线程,但是此时线程1、2已经拿到了number=0,所以结果就是写覆盖,线程1、2将number变成1。

解决的方式就是:

  1. addPlusPlus()方法加锁。
  2. 使用java.util.concurrent.AtomicInteger类。
/**
 * 
 */
package com.matao.concurrent.blockingqueue;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author MT
 *
 */
public class Test {
	public static void main(String[] args) {
		atomicDemo();
	}
	
	private static void atomicDemo() {
	    System.out.println("原子性测试");
	    MyData myData=new MyData();
	    for (int i = 1; i <= 20; i++) {
	        new Thread(()->{
	            for (int j = 0; j <1000 ; j++) {
	                myData.addPlusPlus();
	                myData.addAtomic();
	            }
	        },String.valueOf(i)).start();
	    }
	    while (Thread.activeCount()>2){
	        Thread.yield();
	    }
	    System.out.println(Thread.currentThread().getName()+"\t int type finally number value: "+myData.number);
	    System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type finally number value: "+myData.atomicInteger);
	}

}

class MyData{
   // int number=0;
    volatile int number=0;

    AtomicInteger atomicInteger=new AtomicInteger();
    public void setTo60(){
        this.number=60;
    }

    //此时number前面已经加了volatile,但是不保证原子性
    public void addPlusPlus(){
        number++;
    }

    public void addAtomic(){
        atomicInteger.getAndIncrement();
    }
}

结果:可见,由于volatile不能保证原子性,出现了线程重复写的问题,最终结果比20000小。而AtomicInteger可以保证原子性。
在这里插入图片描述
volatile可以保证有序性,也就是防止指令重排序。所谓指令重排序,就是出于优化考虑,CPU执行指令的顺序跟程序员自己编写的顺序不一致。就好比一份试卷,题号是老师规定的,是程序员规定的,但是考生(CPU)可以先做选择,也可以先做填空。

int x = 11; //语句1
int y = 12; //语句2
x = x + 5;  //语句3
y = x * x;  //语句4

以上例子,可能出现的执行顺序有1234、2134、1342,这三个都没有问题,最终结果都是x = 16,y=256。但是如果是4开头,就有问题了,y=0。这个时候就不需要指令重排序。

volatile底层是用CPU的内存屏障(Memory Barrier)指令来实现的,有两个作用,一个是保证特定操作的顺序性,二是保证变量的可见性。在指令之间插入一条Memory Barrier指令,告诉编译器和CPU,在Memory Barrier指令之间的指令不能被重排序。

哪些地方用到过volatile?

单例模式的安全问题

常见的DCL(Double Check Lock)模式虽然加了同步,但是在多线程下依然会有线程安全问题。

public class SingletonDemo {
    private static SingletonDemo singletonDemo=null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t 我是构造方法");
    }
    //DCL模式 Double Check Lock 双端检索机制:在加锁前后都进行判断
    public static SingletonDemo getInstance(){
        if (singletonDemo==null){
            synchronized (SingletonDemo.class){
                 if (singletonDemo==null){
                     singletonDemo=new SingletonDemo();
                 }
            }
        }
        return singletonDemo;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                SingletonDemo.getInstance();
            },String.valueOf(i+1)).start();
        }
    }
}

这个漏洞比较tricky,很难捕捉,但是是存在的。instance=new SingletonDemo();可以大致分为三步

memory = allocate();     //1.分配内存
instance(memory);	 //2.初始化对象
instance = memory;	 //3.设置引用地址

其中2、3没有数据依赖关系,可能发生重排。如果发生,此时内存已经分配,那么instance=memory不为null。如果此时线程挂起,instance(memory)还未执行,对象还未初始化。由于instance!=null,所以两次判断都跳过,最后返回的instance没有任何内容,还没初始化。

解决的方法就是对singletondemo对象添加上volatile关键字,禁止指令重排。

CAS问题

CAS是指Compare And Swap,比较并交换,是一种很重要的同步思想。如果主内存的值跟期望值一样,那么就进行修改,否则一直重试,直到一致为止。

public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger=new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data : "+ atomicInteger.get());
        //修改失败
        System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t current data : "+ atomicInteger.get());
    }
}

第一次修改,期望值为5,主内存也为5,修改成功,为2019。第二次修改,期望值为5,主内存为2019,修改失败。

查看AtomicInteger.getAndIncrement()方法,发现其没有加synchronized也实现了同步。这是为什么?

CAS底层原理

AtomicInteger内部维护了volatile int valueprivate static final Unsafe unsafe两个比较重要的参数。

public final int getAndIncrement(){
    return unsafe.getAndAddInt(this,valueOffset,1);
}

AtomicInteger.getAndIncrement()调用了Unsafe.getAndAddInt()方法。Unsafe类的大部分方法都是native的,用来像C语言一样从底层操作内存。

public final int getAnddAddInt(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和var2,就是根据对象偏移量得到在主内存的快照值var5。然后compareAndSwapInt方法通过var1和var2得到当前主内存的实际值。如果这个实际值快照值相等,那么就更新主内存的值为var5+var4。如果不等,那么就一直循环,一直获取快照,一直对比,直到实际值和快照值相等为止。

比如有A、B两个线程,一开始都从主内存中拷贝了原值为3,A线程执行到var5=this.getIntVolatile,即var5=3。此时A线程挂起,B修改原值为4,B线程执行完毕,由于加了volatile,所以这个修改是立即可见的。A线程被唤醒,执行this.compareAndSwapInt()方法,发现这个时候主内存的值不等于快照值3,所以继续循环,重新从主内存获取。

CAS缺点

CAS实际上是一种自旋锁,

  1. 一直循环,开销比较大。
  2. 只能保证一个变量的原子操作,多个变量依然要加锁。
  3. 引出了ABA问题

ABA问题

所谓ABA问题,就是比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题。比如线程T1将值从A改为B,然后又从B改为A。线程T2看到的就是A,但是却不知道这个A发生了更改。尽管线程T2 CAS操作成功,但不代表就没有问题。
有的需求,比如CAS,只注重头和尾,只要首尾一致就接受。但是有的需求,还看重过程,中间不能发生任何修改,这就引出了AtomicReference原子引用。

AtomicReference

AtomicInteger对整数进行原子操作,如果是一个POJO呢?可以用AtomicReference来包装这个POJO,使其操作原子化。

User user1 = new User("Jack",25);
User user2 = new User("Lucy",21);
AtomicReference atomicReference = new AtomicReference<>();
atomicReference.set(user1);
System.out.println(atomicReference.compareAndSet(user1,user2)); // true
System.out.println(atomicReference.compareAndSet(user1,user2)); //false

AtomicStampedReference和ABA问题的解决

使用AtomicStampedReference类可以解决ABA问题。这个类维护了一个“版本号”Stamp,在进行CAS操作的时候,不仅要比较当前值,还要比较版本号。只有两者都相等,才执行更新操作。

AtomicStampedReference.compareAndSet(expectedReference,newReference,oldStamp,newStamp);

你可能感兴趣的:(BAT面试)