ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

3.JUC

2022-05-19 20:04:26  阅读:109  来源: 互联网

标签:JUC 变量 int 原子 线程 内存 public


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

 

标签:JUC,变量,int,原子,线程,内存,public
来源: https://www.cnblogs.com/wmd-l/p/16289851.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有