周阳高并发面试题笔记记录

1.谈谈你对volatile的理解

volatile是Java虚拟机提供的轻量级的同步机制[就是乞丐版的synchonized]

保证可见性,不保证原子性,静止指令重排 [能保证JMM两个]

2.JMM(JAVA 内存模型)

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,(12生肖里的龙并不存在)它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.
JMM关于同步规定:
1.线程解锁前,必须把共享变量的值刷新回主内存
2.线程加锁前,必须读取主内存的最新值到自己的工作内存
3.加锁解锁是同一把锁
 
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:

只要有一个线程修改了主内存中的变量值,且把它写回到了主内存,就一定要通知其它线程,俗称可见性

2.1可见性

通过前面对JMM的介绍,我们知道
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的.
这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享比那里X对线程B来说并不不可见.这种工作内存与主内存同步延迟现象就造成了可见性问题.

2.2原子性   number++在多线程下是非线程安全的,如何不加synchronized解决?

why不能保证原子性?(方法加了synchonized可以解决,但是杀鸡焉用宰牛刀)
主内存空间的值为0,一个线程number++,没有synchonized,线程可以哄抢
虽然有volatile通知修改,但是太快了,正要通知就被挂起了,然后被其它线程的数据覆盖了,出现了丢失数据

2.3VolatileDemo代码演示可见性+原子性代码

class Mydata{ //MyData.java==>MyData.class==>JAVA字节码
    // int number=0;
    volatile int number=0;
    public void addTo60(){
        this.number=60;
    }
    //请注意,此时number是加了volatile修饰的
    public void addPlusPlus(){
        number++;//被分成了3个指令  执行getfield拿到了原始number;执行iadd进行加1操作;执行putfield写把累加后的值写回
    }
    AtomicInteger atomicInteger=new AtomicInteger();
    public void addMyAutomic(){
        atomicInteger.getAndIncrement();
    }
}
/**
 * 验证volatile的可见性
 * 1.1假如int number=0;number变量之前根本没有添加volatile关键字修饰,没有可见性
 * 1.2假如volatile int number=0,有可见性
 *
 * 验证volatile不保证原子性
 * 2.1原子性指的是什么意思?不可分割,完整性,也即某个线程正在做某个业务时,中间不可以被加塞或者分割,需要整体完整
 * 要么同时成功,要么同时失败.课堂签到,张三写了张字,被李四抢走,在给张三写了三
 * 我的操作不应该被打断
 * 2.2volatile不保证原子性
 *
 * 2.3why不能保证原子性?(方法加了synchonized可以解决,但是杀鸡焉用宰牛刀)
 * 主内存空间的值为0,一个线程number++,没有synchonized,线程可以哄抢
 * 虽然有volatile通知修改,但是太快了,正要通知就被挂起了,然后被其它线程的数据覆盖了,出现了丢失数据
 * 2.4如何解决原子性?
 * 2.4.1加sync-->杀鸡别用宰牛刀
 * 2.4.2使用JUC下的AtomicInteger
 */
public class VolatileDemo {
    public static void main(String[] args) {
        volatileAutomic();
    }
    //加了volatile关键字不保证原子性
    private static void volatileAutomic() {
        Mydata mydata=new Mydata();
        for(int i=0;i<20;i++){
            new Thread(()->{
                for(int j=0;j<1000;j++){
                    mydata.addPlusPlus();
                    mydata.addMyAutomic();
                }
            },String.valueOf(i)).start();
        }
        //需要等待上面20个线程计算完成后,再用main线程取得最终的计算结果
        while (Thread.activeCount()>2){
            Thread.yield();//礼让线程
        }
        //只要不是20000,volatile就是不保证原子性
        System.out.println("计算结果 int类型"+mydata.number);
        //保证原子性
        System.out.println("计算结果 atomicInteger类型"+mydata.atomicInteger);
    }

    //volatile可以保证可见性,及时通知其它线程,主物理内存的值已经被修改
    private static void seeokByVolatile() {
        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.addTo60();
            System.out.println(Thread.currentThread().getName()+"\t update"+mydata.number);
        },"AAA").start();
        //如果一直等待,说明main线程没有感知到值的变化,不会输出misson is over
        while (mydata.number==0){
            //main线程就一直在这里等待循环,直到number值不在等于0
        }
        System.out.println(Thread.currentThread().getName()+"\t misson is over"+mydata.number);
    }
}

2.4有序性

计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下3种

源代码->编译器优化的重排->指令并行的重排->内存系统的重排->最终执行的指令

public void mySort(){
    int x=11;//语句1
    int y=12;//语句2
    x=x+5;//语句3
    y=x*x;//语句4
}
1234
2134
1324
问题:
请问语句4 可以重排后变成第一条码?
存在数据的依赖性 没办法排到第一个

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致.
处理器在进行重新排序是必须要考虑指令之间的数据依赖性[先有你爹才能有你]
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测

加了volatile就是禁止指令重排

3.你在哪些地方用到过volatile?

3.1单例模式DCL代码

public class SingletoDemo {
    private static SingletoDemo instance=null;
    private SingletoDemo(){
        System.out.println(Thread.currentThread().getName()+"我是构造方法");
    }
    public static SingletoDemo getInstance(){
        if(instance==null){
            instance=new SingletoDemo();
        }
        return instance;
    }
    public static void main(String[] args) {
        System.out.println(SingletoDemo.getInstance()==SingletoDemo.getInstance());
        System.out.println(SingletoDemo.getInstance()==SingletoDemo.getInstance());
        System.out.println(SingletoDemo.getInstance()==SingletoDemo.getInstance());
    }
}

 

3.2单例模式volatile分析

public class SingletoDemo {
    private static volatile SingletoDemo instance=null;
    private SingletoDemo(){
        System.out.println(Thread.currentThread().getName()+"我是构造方法");
    }
    //DCL(Double Check Lock 双端检锁机制,比如上厕所,上厕所之前看一下有没有人,进厕所把门锁上,再看看有没有人
    public static SingletoDemo getInstance(){
        if(instance==null){
            synchronized (SingletoDemo.class){
                if(instance==null){
                    instance=new SingletoDemo();
                }
            }
        }
        return instance;
    }
    public static void main(String[] args) {
        //单线程(main线程的操作动作)
       /* System.out.println(SingletoDemo.getInstance()==SingletoDemo.getInstance());
        System.out.println(SingletoDemo.getInstance()==SingletoDemo.getInstance());
        System.out.println(SingletoDemo.getInstance()==SingletoDemo.getInstance());*/
        //单例模式在多线程环境下可能存在安全问题
        for(int i=1;i<=10;i++){
            new Thread(()->{
                SingletoDemo.getInstance();
            },String.valueOf(i)).start();
        }
        //1.解决一:在getInstance方法上添加sync,但是太重了
        //2.解决二:DCL模式双重检测,可能出现指令重排,所以要加volatile
    }
}

DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排
  原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化.
instance=new SingletonDem(); 可以分为以下步骤(伪代码)
 
memory=allocate();//1.分配对象内存空间
instance(memory);//2.初始化对象
instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 
 
步骤2和步骤3不存在数据依赖关系.而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的.
memory=allocate();//1.分配对象内存空间
instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完.
instance(memory);//2.初始化对象
但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性
所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题.

1.解决一:在getInstance方法上添加sync,但是太重了
2.解决二:DCL模式双重检测,可能出现指令重排,所以要加volatile

4.CAS是什么

//this指的是当前对象
//valueOffset是内存偏移量,说白了就是内存地址,当前this对象的地址
public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

 

unsafe类是什么?

1.Unsafe

是CAS的核心类 由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问,UnSafe相当于一个后面,基于该类可以直接操作特额定的内存数据.UnSafe类在于sun.misc包中,其内部方法操作可以向C的指针一样直接操作内存,因为Java中CAS操作的助兴依赖于UNSafe类的方法.
注意UnSafe类中所有的方法都是native修饰的,也就是说UnSafe类中的方法都是直接调用操作底层资源执行响应的任务
2.变量ValueOffset,便是该变量在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的

3.变量value和volatile修饰,保证了多线程之间的可见性.

CAS是什么?

CAS的全称为Compare-And-Swap ,它是一条CPU并发原语.
它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的.
 
CAS并发原语提现在Java语言中就是sun.miscUnSaffe类中的各个方法.调用UnSafe类中的CAS方法,JVM会帮我实现CAS汇编指令.这是一种完全依赖于硬件 功能,通过它实现了原子操作,再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许中断,也即是说CAS是一条原子指令,不会造成所谓的数据不一致的问题.

所以不用加synchonized就可以解决number++在多线程操作环境下的线程安全问题atomicInteger.getAndIncrement();

this 就是var1,valueoffset 就是var2,var5就是获得var1变量的var2位置的值,

进入while循环,看var1变量的var2位置的值是否等于var5的值,如果相等,就改变值为var5+var4

如果修改成功,跳出循环

 

var1 AtomicInteger对象本身。
var2该对象值得引用地址。
var4需要变动的数量。
var5是用过var1 var2找出的主内存中真实的值。
用该对象当前的值与var5比较:
如果相同,更新var5+var4并且返回true,
如果不同,继续取值然后再比较,直到更新完成。
 

没有加锁,既解决了一致性,又提高了并发性

 假设线程A和线程B两个线程同时执行getAndAddInt操作(分别在不同的CPU上):

1.AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存.

2.线程A通过getIntVolatile(var1,var2) 拿到value值3,这是线程A被挂起.

3.线程B也通过getIntVolatile(var1,var2) 拿到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存中的值也是3 成功修改内存的值为4 线程B打完收工 一切OK.

4.这是线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的数值和内存中的数字4不一致,说明该值已经被其他线程抢先一步修改了,那A线程修改失败,只能重新来一遍了.

5.线程A重新获取value值,因为变量value是volatile修饰,所以其他线程对他的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法进行比较替换,直到成功.

CAS (CompareAndSwap)
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,
否则继续比较直到主内存和工作内存中的值一致为止.
CAS应用
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

CAS缺点:

1.循环时间长开销很大

2.只能保证一个共享变量的原子操作.对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性

3.引出来ABA问题?

Automic

CAS-->Unsafe-->CAS底层思想-->ABA-->原子引用更新-->如何规避ABA问题

5.ABA问题

原子类Automic的ABA问题谈谈?原子更新引用知道吗?
一句话:狸猫换太子

CAS会导致ABA问题

CAS算法实现一个重要前提是需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差里会导致数据地变化

比如说一个线程one从内存V中取出A,这时候另一个线程two也从内存中取出A,并且线程B进行了一系列操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作时发现内存中仍然是A,然后线程one操作成功.

尽管线程one的CAS操作成功,但是并不代表这个过程是没有问题的

如何解决ABA问题?  -->  原子引用-->时间戳原子引用
AtomicReference类是用来解决自己定义的类的CAS 
ABA问题的解决-->AtomicStampedReference
/**
 * ABA问题的解决  AtomicStampedReference
 */
public class ABADemo {
    static AtomicReference<Integer> atomicReference =new AtomicReference<>(100);
    static AtomicStampedReference 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);
        },"t1").start();
        new Thread(()->{
            //暂停1秒钟t2线程,保证上面的t1线程完成了ABA操作
            try{
                TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e){e.printStackTrace();}
            System.out.println(atomicReference.compareAndSet(100,2020)+"\t"+atomicReference.get());
        },"t2").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第一次版本号"+stamp);
            //暂停1秒钟t3线程
            try{TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e){e.printStackTrace();}
            atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t第二次版本号"+atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t第三次版本号"+atomicStampedReference.getStamp());
        },"t3").start();
        new Thread(()->{
            int stamp=atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t第一次版本号"+stamp);
      
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值