3.JUC

1.Volatitle关键字

volatitle是虚拟机提供的轻量级的同步机制,JMM是需要满足三个特性:可见性/原子性/禁止指令重排,但volatitle只能保证两个,不能保证原子性,所以其是轻量型的同步机制!
    有三个特性:
        1.保证可见性
        2.不保证原子性
        3.禁止指令重排
 
1.JMM(java 内存模型:Java Memeory Model,简称JMM),本身是一种抽象的概念,并不存在,它描述的是一种规则或者规范,通过这种规范定义程序
  的各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式
  1.1 JMM关于同步的规定:
      1.线程加锁前,必须去除主内存的最新值到自己的工作内存
      2.线程解锁前,必须把共享变量的值刷回主内存
      3.加锁解锁必须是同一把锁
  1.2:
      由于java运行程序的主体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方叫做线程栈),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都处在主内存
      主内存是共享内存区,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在各自的工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,
      操作完成后再将变量写回主内存,不能直接操作主内存中的变量,每个线程中的工作内存中存储的主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存完成。
      如下图:
          图之重点:每个线程都有自己的线程栈(包含了自己独有的程序计数器,局部变量表,操作数栈,动态链接,方法出口等),这是线程自己私有的,线程间无法共享
          每个线程都会拿到局部变量的拷贝,维护在自己的线程栈中

问题:结合上图,理解java中创建的对象是存放在堆中还是栈中??
不能一概而论,需要分情况:
    1.在方法中声明(局部变量):在方法中声明,方法都是由线程去调用的,每个线程都有自己的线程栈,每个方法都有自己的栈帧
        1.1 声明的基本类型属性:int、double等
            基本类型的变量名和变量值都存储在栈内存中,进一步(线程栈-->栈帧--->局部变量表中),方法执行结束,就会释放该栈帧,所以局部变量就会失效,所以局部变量只能在方法中有效!
        1.2 声明的是引用类型对象:
            如果声明的是引用类型对象时,对象名称存储在栈中(存放的只是引用),对象存放在堆中
    2.在类中声明(全局变量/成员变量):  全局变量分两种:1.类变量(又叫静态变量)  2.示例变量(类属性,不用static修饰)
        2.1 基本类型:
            变量名和变量值都存放在堆中
        2.2 引用类型:
            存储在堆中,声明的变量会存储一个地址,该内存地址指向所引用的对象

String字符串的存储位置:
    字符串常量就存放在堆区中的字符串常量池某一位置(jdk版本号<=1.6字符串常量池在方法区,>=1.7在堆区),然后把这个字符串的地址返回给str
    1.
        public static void main(String[] args) {
            String str1 = "1234";
            String str2 = "12"+"34";
            System.out.println(str1==str2);
        }
        输出:true  因为jvm底层会进行优化,直接优化成str1的形式
    
    
    2.
        public static void main(String[] args) {
            String str1 = "1234";
            String str = "12";
            String str2 = str+"34";
            System.out.println(str1==str2);
        }
        输出false
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(“12”);
        stringBuilder.append(“34”);
        String str2 = stringBuilder.toString();
        此时str2的地址是在堆上任意位置重新new出来的一个内存,所以两个字符串地址不同,返回false

1.volatitle保证可见性

volatitle只能修饰成员变量(类属性/静态变量static),不能修饰局部变量(方法中的变量)
什么是可见性:
    前提:硬件是怎么工作的呢??
        你在window操作系统上需要下载一个游戏(20M),就需要使用cpu和内存了,在这个过程中cpu负责计算,比如计算下载进度,统计下载完成一共需要多少时间等,
        内存为cpu提供数据的,负责保存游戏的所有信息,比如游戏的大小(20M)数据。在这个过程中,cpu从内存上取游戏大小这个数据,然后cpu去计算下载进度,
        把计算出的进度结果再写到内存,最终呈现到用户页面.看上去下载游戏这个过程分工明确,没有问题,但实际上cpu的计算速度比内存的存取速度高了不知道多少个数量级,这个过程cpu很空闲啊
        cpu你闲着没事干那就是浪费资源浪费钱啊,这是个问题,于是人们就想了个办法,在内存上面加个(高速)缓存,如果是一些常用信息,比如游戏大小这个数据,那就不用在内存取了,直接在缓存上拿(如图二)
        而缓存设计的存取速度是很快的,当然价格也更高,如果刚好缓存上有这个游戏大小数据,这个操作在计算机的世界叫做缓存命中,这样就解决了cpu很闲的问题。
        哈哈,还是举个简单例子吧,咱春节买票回家,尽管你的手速很快,但是还是一票难求,12306官网响应速度慢,没办法家还是要回的,那就找黄牛,虽然价格贵但是能解决你的痛点。
        这个例子中你,12306系统,黄牛分别对应cpu,内存和缓存,方便你理解。顺便说下,这个黄牛其实也是设计模式中的代理。

分析了硬件架构,再来理解Java内存模型(JMM),如鱼得水,JMM是根据硬件架构映射出来的,不是真实存在的,硬件模型是物理的,是真实存在的。
如下图所示,如果现在有两个线程AB需要同时将共享变量c的值加1,最终的程序运行的结果c的值可能是3,也可能是2
    1.程序初始化,线程AB将拷贝主内存的共享变量c到各自的工作内存,此时工作内存A,工作内存B的初始化值c值都为1,初始化结束.
      这里可以把线程A理解成cpu1,线程B理解成cpu2,工作内存理解成高速缓存。这个过程因为工作内存是线程私有的,因为每个高速缓存是属于不同CPU是不可见的,
      工作内存A看不见工作内存B的c值为1,相反工作内存B也看不到工作内存A的c值 
    2.当线程AB同时将共享变量c加1时,如果线程A先获取时间片,此时工作内存A的c值加1等于2,然后由工作内存A将变量c=2同步到主内存,此时主内存c变量为2了,线程A执行结束,释放时间片给线程B
    3.此时主内存会更新线程B的工作内存B,将c=2告诉线程B,并更新工作内存B的值c=2,此时B获取时间片,看到工作内存B值是c=2,加1,c=3,线程B将c=3写到主内存,此时主内存c的值就是3了,线程B执行结束,整个程序结束
    4.另外一种特殊情况:
        如果线程A执行结束后,将主内存的c值变为2,如果主内存c=2还没有同步更新到工作内存B呢?此时问题就来了,线程B获取时间片后发现自己的工作内存变量c还是1,然后加1,此时c=2,
        将c再更新到主内存,此时主内存的值还是2,主内存再同步c=2的值给线程B已经失去意义了,因为线程全部执行完毕
问题总结:
    这个程序执行过程中,其实导致线程安全的本质问题是主内存通知不及时才导致发生的(缓存不可见)
    这个案例中因为主内存不能及时将c=2的值更新到线程B的工作内存,导致线程B获取不到c已经更新为2了

1.volatile实现可见性:
    在JVM手册中,当多线程写被volatile修饰的共享变量时,有两层语义。
        1.该变量立即刷新到主内存
        2.使其他线程的共享变量立即失效。言外之意当其他线程需要的时候再从主内存取
        在上述案例中,如果c为一个布尔值并且被volatile修饰,那么当线程AB同时更新共享变量c时,此时c对于工作内存AB是可见的。


2.synchronized实现可见性
    在JVM手册中,synchronized可见性也有两层语义。
        1.在线程加锁时,必须从主内存获取值。
        2.在线程解锁时,必须把共享变量刷新到主内存。

2.volatitle不保证原子性

原子性:不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被分割和被加塞,要么同时成功,要么同时失败!
不保证原子性示例:初始值为0,创建20个线程,每个线程执行100次i++,预期结果是20000
代码如下:
    class Data {
        重点1:使用volatile修饰变量
        volatile int num = 0;
        重点2:创建方法num++
        public void addNum() {
            num++;
        }
    }
    public class HeapTest {
        public static void main(String[] args) {
            Data data = new Data();
            for (int i = 0; i < 20; i++) {
                new Thread(() -> {
                    for (int j = 0; j < 1000; j++) {
                        data.addNum();
                    }
                }, "线程" + i).start();
            }
            重点3:使用while循环判断:当前活跃线程数(最少包含两个:main线程和gc垃圾回收),等待所有线程执行完毕后输出下列值!
            while (Thread.activeCount() > 2) {
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName() + ":num=" + data.num);
        }
    }
    输出:等等,都小于预期的20000,这是为什么呢
        main:num=19855

如下图所示:这就是所加和小于预期值的原因!如果同时有线程拿到cpu资源开始计算就会出现这样的问题!
1.线程开始执行时从主内存中同步数据,执行完毕写回主内存(其余操作如线程内的操作,均在自己的线程栈中进行)
2.线程结束后将结果同步到主内存中

如何保障原子性呢?
    1.加sychronized(程度太重:有点杀鸡用牛刀的感觉,给一个i++操作加上synchronized太大)
        class Data {
            volatile int num = 0;
            public synchronized void addNum() {
                num++;
            }
        }
        也可以解决原子性问题!
    2.使用原子类(有对应的原子类可以用)
        class Data {
            重点1:使用对应的原子类:不传参默认是0
            AtomicInteger atomicInteger=new AtomicInteger();
            public void addAtomicInteger(){
                调用其i++操作
                atomicInteger.incrementAndGet();
            }
        }
        ...
        输出:20000预期值
        System.out.println(Thread.currentThread().getName() + ":num=" + data.atomicInteger);
        ...

3.volatile 禁止指令重排

指令重排:JVM对代码的执行顺序做优化,并不一定按照代码的顺序去执行,底层可能会重排顺序
    例如:
        1.int a=0;
        2.int b =0;
        3.a=a+1;
        4.b=a+1;
    在底层在不影响效果的前提下可能会按照1324的顺序执行,这就是指令重排!
    源代码-->编译器优化的重排--->指令并行的重排--->内存系统的重排--->最终执行的指令
但是多线程下,指令重排可能会发生风险!如下图..

你在哪里用过volatile

普通单例模式在并发下不会只有一个实例:
构造器私有
    1.懒汉式(使用时调用方法,方法里包含有实例化方法)
    2.饿汉式(直接在实例化)

1.DCL形式的单例模式(Double Check Lock双端检锁机制):
    public class SingleDemo {
        public static void main(String[] args) {
            for (int i = 0; i <20;i++){
                MySingle.getsingle();
            }
        }
    }
    class MySingle {
        private static MySingle single;
        private MySingle() {
            System.out.println(Thread.currentThread().getName() + "实例化单例模式");
        }
        //为什么不在方法上加上synchronized呢,这样锁住的是整个方法,太大太重了,而下述锁住的只是代码块,传入的对象是MySingle.class,锁住的是整个的类对象!
        public static MySingle getsingle() {
            //DCL模式:双端检锁模式
            if (single == null) {
                synchronized (MySingle.class) {
                    if (single == null) {
                        single = new MySingle();
                    }
                }
            }
            return single;
        }
    }
2.DCL模式下也是无法保证单例的线程安全问题,因为对象的实例化实际可以分为三步:
    以成员变量为例(对象名和实际对象都存储在堆中)
        1.在堆中创建对象名
        2.在堆中开辟空间存放new出来的具体对象
        3.对象名指向具体对象
在底层可能会发生指令重排,如下图:

如何解决上述的单例问题呢?可以加上volatile
代码如下:
    public class SingleDemo {
        public static void main(String[] args) {
            for (int i = 0; i <20;i++){
                MySingle.getsingle();
            }
        }
    }
    class MySingle {
        //重点1:在成员变量上加上volatile修饰
        private static volatile MySingle single;
        private MySingle() {
            System.out.println(Thread.currentThread().getName() + "实例化单例模式");
        }
        public static MySingle getsingle() {
            /重点2:DCL模式:双端检锁模式
            if (single == null) {
                synchronized (MySingle.class) {
                    if (single == null) {
                        single = new MySingle();
                    }
                }
            }
            return single;
        }
    }
这样就解决了多线程单例的线程安全问题,但并不是说这样就会只存在一个单实例了,通过反射等操作,是可以破解这个问题的!!!

CAS的理解

CAS:compareAndSet(比较并交换)
compareAndSet(预期值,更新值):每个线程会拷贝一份变量备份到自己的工作空间中(线程栈中),判断预期值和主内存中的值是否一致,如果一致,则更新主内存的值为更新值,并通知其他线程,可以判断期间是否有线程插队做了更改!
具体代码:
    public class  CasDemo {
        public static void main(String[] args) {
            AtomicInteger atomicInteger=new AtomicInteger(5);
            System.out.println(atomicInteger.compareAndSet(5, 2019)+"当前值:"+atomicInteger.get());
            System.out.println(atomicInteger.compareAndSet(5, 2021)+"当前值:"+atomicInteger.get());
        }
    }
    输出:
        true当前值:2019 --->主内存中的值和线程栈中的值一致,所以更新成功为2019
        false当前值:2019---->主内存中的值已被更新为2019,和预期值5不相同,所以不能跟新,还是原值2019

当多线程启动时,每个线程会将主内存中的变量同步到自己的线程栈中,compareAndSet会比对主内存的值和预期的值,如果一致,则证明期间没有其他线程插队更改,则更新为预期值并通知其他线程!

底层原理:
    1.原子的i++操作
        atomicInteger.getAndIncrement();
    2.底层原理:
        public final int getAndIncrement() {
            //this表示当前对象  valueOffset:内存地址,
            //引出一个对象Unsafe类 
            return unsafe.getAndAddInt(this, valueOffset, 1);
        }
    3.下探
        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;
        }
        
1.Unsafe
    是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地方法(native修饰,Unsafe类中有大量的native修饰的方法)来访问,Unsafe相当于一个后门
    基于该类直接操作特定内存中的数据,Unsafe类存在sun.misc包中,其内部方法操作可以像c的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法
    注意:
        Unsafe中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
        
2.变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
        public final int getAndIncrement() {
            //this表示当前对象  valueOffset:内存地址,
            //引出一个对象Unsafe类 
            return unsafe.getAndAddInt(this, valueOffset, 1);
        }

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

CAS的全称为Compare-And-Swap,它是一条CPU并发原语(cpu原语执行必须是连续的,过程不能被中断)
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

CAS并发原语体现在JAVA语言中sun.misc.Unsafe类中的各个方法。调用Unsafe类的Cas方法,JVM会帮我们实现出CAS汇编指令
这是一种完全依赖于硬件的功能,通过他实现了原子操作。
再次强调,
    1.由于CAS是一种系统原语,原语属于操作系统范畴,是由若干条指令组成的,用于完成某个功能的一个过程,
    2.并且原语的执行必须是连续的
    3.在执行过程中不允许被中断
   4.也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题
   
面试问题:
    原子类为什么可以保证原子性?
        1.原子类底层是Unsafe类进行操作(所有的方法都是native修饰)
        2.Unsafe内的CAS方法(Compare-And-Swap)JVM会帮我们实现出CAS汇编指令(CPU的并发原语)
            2.1.由于CAS是一种系统原语,原语属于操作系统范畴,是由若干条指令组成的,用于完成某个功能的一个过程
            2.2.并且原语的执行必须是连续的
            2.3.在执行过程中不允许被中断
            2.4.也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题
    为什么使用原子类而不是用synchronized?
        1.synchronized虽然保证了数据一致性,但是只能一个线程访问,并发性下降
        2.原子类底层用的是CAS,代码是do while的自旋锁,既保证了数据一致性,又保证了并发性
        
        
CAS的缺点:
    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;
    }
    1.循环时间长,开销大(如果CAS失败,会一直进行尝试。如果CAS长时间不成功,可能会给CPU带来很大的开销)
    2.只能保证一个共享变量的原子操作(对于多个共享变量的操作,循环CAS就无法保障操作的原子性,这时候就可以使用锁来保障原子性)
    3.引出ABA问题:

ABA问题

ABA问题:狸猫换太子
如下图:
    1.例如两个线程AB线程
    2.AB线程启动时,会将主内存中的数据拷贝一份到各自的工作空间
    3.A线程执行需要10秒
    4.B线程执行只需要2秒
    5.A线程执行时,B线程抢到资源,将主内存中的25改为20
    6.B线程又将主内存中的20改回为25
    7.A线程获得执行权限,发现主内存的25和自己的预期值相同,但并不知道期间B线程做了两次更新操作

原子引用

上述的都是基本类型包装类的原子引用,如何自定义对象的原子引用呢??AtomicReference原子引用类
        @AllArgsConstructor
        @NoArgsConstructor
        @Data
        @ToString
        class User{
            String username;
            int age;
        }
        public class AtomicReferenceDemo {
            public static void main(String[] args) {
                User z3 = new User("z3",18);
                User li4 = new User("li4",19);
                //创建泛型的原子引用类
                AtomicReference<User> atomicReference=new AtomicReference<User> ();
                //设置主内存的值
                atomicReference.set(z3);
                //从主内存中拿出对象和z3对比,是否一致,一致更新为li4
                System.out.println(atomicReference.compareAndSet(z3, li4)+":值:"+atomicReference.get().toString());
                System.out.println(atomicReference.compareAndSet(z3, li4)+":值:"+atomicReference.get().toString());
            }
        }


如何解决ABA问题呢?
    在原子引用基础上,新增一种机制,修改版本号(类似时间戳)

具体代码实现:使用AtomicStampedReference原子标记引用类
    public class ABADemo {
        //重点1:传入两个参数 1.主内存的原始值  2.版本号
        private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(50, 1);
    
        public static void main(String[] args) {
            new Thread(() -> {
                try {
                    //重点2:获取版本号
                    int stamp = atomicStampedReference.getStamp();
                    System.out.println(Thread.currentThread().getName() + "当前版本号:" + stamp);
                    Thread.sleep(1000);
                    //重点3:比较(预期值,更新值,预期版本号,更新版本号),会进行两方面的比较:版本号和数据值
                    atomicStampedReference.compareAndSet(50, 100, stamp, stamp++);
                    System.out.println(Thread.currentThread().getName() + "更改值为:" + atomicStampedReference.getReference() + " 版本号:" + atomicStampedReference.getStamp());
                    //重点3:将数值改回原来的50,但是版本号已经发生了变化为3
                    atomicStampedReference.compareAndSet(100, 50, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
                    System.out.println(Thread.currentThread().getName() + "更改值为:" + atomicStampedReference.getReference() + " 版本号:" + atomicStampedReference.getStamp());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "线程A").start();
            new Thread(() -> {
                try {
                    int stamp = atomicStampedReference.getStamp();
                    System.out.println(Thread.currentThread().getName() + "当前版本号:" + stamp);
                    Thread.sleep(3000);
                    //重点4:此处的数值虽然相同,但是版本号已经不同了,所以更改失败!
                    System.out.println(Thread.currentThread().getName() + "更改结果:" + atomicStampedReference.compareAndSet(50, 100, stamp, stamp++) + "当前值:" + atomicStampedReference.getReference());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "线程B").start();
        }
    }
    输出结果:
        线程A当前版本号:1
        线程B当前版本号:1
        线程A更改值为:100 版本号:1
        线程A更改值为:50 版本号:2
        线程B更改结果:false当前值:50

 

posted @ 2022-05-19 19:46  努力的达子  阅读(65)  评论(0编辑  收藏  举报