zoukankan      html  css  js  c++  java
  • 详解 volatile关键字 与 CAS算法

    (请观看本人博文 —— 《详解 多线程》



    在讲解本篇博文的知识点之前,本人先来给出一个例子:

    package edu.youzg.about_synchronized.core;
    
    public class Test {
    
        public static void main(String[] args) {
            TestRunnable myRunnable = new TestRunnable();
            new Thread(myRunnable).start();
            while (true){
                if (TestRunnable.getFlag()) {
                    System.out.println("进来了");
                    break;
                }
            }
        }
    }
    
    class TestRunnable implements Runnable{
        static boolean flag = false;
        public static boolean getFlag() {
            return flag;
        }
    
        @Override
        public void run() {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag=true;
            System.out.println("flag的值是" + getFlag());
        }
    }
    

    那么,现在本人来展示下运行结果
    在这里插入图片描述
    可以看到,在上图中出现了这样的错误:
    程序一直在运行!
    那么,我们不是已经在run()中把flag设置为true了吗,我们让线程跑起来之后,再调用getFlag()方法,理应返回的是true啊。
    但是,运行结果明显表明:返回的是false。
    这是为什么呢?

    对于上述问题,我们有一个专门的称呼 —— 内存可见性问题

    那么,现在,本人就来讲解下 内存可见性问题

    内存可见性问题

    首先,本人来讲解下内存模型

    内存模型

    Java内存模型规定了 :
    所有的 变量 都存储在 主内存中;
    每条线程中还有自己的工作内存
    线程的工作内存中保存了被该线程所使用到的变量
    (这些变量是从主内存中拷贝而来)。
    线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。
    不同线程之间也无法直接访问对方工作内存中的变量
    线程间变量值的传递均需要通过主内存来完成。

    而对于上面出现的问题,本人来通过几张图来展示下原因:

    首先,每个线程都会有一个独立的工作线程
    假设子线程先读入了主存中的flag值,并将工作内存中的flag改为了true:
    在这里插入图片描述
    这时,主线程竞争到了CPU,读入了主存中的flag的值:
    在这里插入图片描述
    这时,不论再竞争多少轮,只要子线程竞争到CPU,就会把工作线程中的flag的值返给主存:
    在这里插入图片描述
    但是,由于这时的主线程在循环中,循环的运行速度非常快,就导致主线程无暇去主存中读入新的flag的值,进而导致循环一直在进行,并且主线程的工作线程中的flag一直都是false,所以就一直循环,导致程序无法结束!

    其实,对于以上问题,我们用之前博文所讲的“synchronized锁”的知识,也完全可以去解决这个问题:
    本人现在来展示下加锁解决的代码:

    package edu.youzg.about_synchronized.core;
    
    public class Test {
    
        public static void main(String[] args) {
            TestRunnable myRunnable = new TestRunnable();
            new Thread(myRunnable).start();
            while (true){
                synchronized (myRunnable) {
                    if (TestRunnable.getFlag()) {
                        System.out.println("进来了");
                        break;
                    }
                }
            }
        }
    }
    
    class TestRunnable implements Runnable{
        static boolean flag = false;
        public static boolean getFlag() {
            return flag;
        }
    
        @Override
        public void run() {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag=true;
            System.out.println("flag的值是" + getFlag());
        }
    }
    

    那么,本人再来展示下运行结果
    在这里插入图片描述
    但是,本人之前说过,加锁有一个极大的缺点 —— 会降低运行效率
    本人上图展示的仅是一个子线程和一个主线程.
    那么,若是我们将来所做的app,同时去处理成百上千的线程呢?
    产生的结果想想就觉得可怕... ...

    那么,有没有办法能够 既解决内存可见性问题,又不会降低运行效率呢?
    针对这种需求,Java有一种自己的解决办法 —— volatile关键字


    volatile关键字

    概述

    当多个线程进行操作共享数据时,可以保证内存中的数据可见
    相较于 synchronized 是一种较为轻量级同步策略
    也可以将volatile看作是一个轻量级锁

    那么,对于上述解释,相信有的同学已经搞不懂了
    那么,本人再来讲解 volatile关键字和 synchronized锁 的区别:

    区别

    volatile 对于多线程,不是一种互斥关系
    volatile 不能保证变量状态的“原子性操作

    那么,现在,本人来展示下用 volatile关键字 来解决上述问题:

    package edu.youzg.about_synchronized.core;
    
    public class Test {
    
        public static void main(String[] args) {
            TestRunnable myRunnable = new TestRunnable();
            new Thread(myRunnable).start();
            while (true){
                if (myRunnable.getFlag()) {
                    System.out.println("进来了");
                    break;
                }
            }
    
        }
    }
    
    
    
    class TestRunnable implements Runnable{
        volatile boolean flag=false;
        public boolean getFlag() {
            return flag;
        }
    
        @Override
        public void run() {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag=true;
            System.out.println("flag的值是"+getFlag());
        }
    }
    

    本人再来展示下运行结果:
    在这里插入图片描述


    现在,本人再来给出一个有问题的代码:

    package edu.youzg.about_synchronized.core;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class Test {
    
        public static void main(String[] args) {
            IRunnable iRunnable = new IRunnable();
            for (int i = 0; i < 10; i++) {
                new Thread(iRunnable).start();
            }
        }
    }
    
    class IRunnable implements Runnable{
        static int i = 0;
    
        @Override
        public void run() {
            while (true){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":" + i++);
            }
        }
    }
    

    由于本人设置的是死循环,所以展示运行中的一个片段:
    在这里插入图片描述
    可以看到,本人设置的static(静态)的变量的值,明明每次执行的是i++操作,却有相同的输出结果,这是为什么呢?

    相信看过本人之前博文的同学已经知道了:
    由于i++不是原子型操作,所以就会出现一个线程对i的 读、改、写三步操作还未完成,就被另一个线程占据了CPU执行权,就导致了这种现象的出现。

    本人在之前的博文中,对于这种问题的解决,用的是 “synchronized 锁”的知识点:
    但是, “synchronized 锁”会使得程序的效率大大降低
    所以,针对这种 原子性问题,Java也同样有一种解决方案 —— CAS算法


    CAS算法:

    概述

    CAS (Compare-And-Swap,即:比较并交换) 是一种硬件对并发的支持
    针对多处理器操作而设计的处理器中的一种特殊指令
    用于管理对共享数据的并发访问

    实现原理

    CAS是一种无锁算法
    CAS有3个操作数内存值V旧的预期值A要修改的新值B
    当且仅当预期值A和内存值V相同时,将内存值V修改为B
    否则什么都不做


    CAS算法的伪代码可以表示为:
    do {
    备份旧数据;
    基于旧数据构造新数据;
    } while (!CAS( 内存地址,备份的旧数据,新数据 ))

    本人再来用几张图片来展示下这个算法的基本原理
    首先,假设线程1争取到了CPU:
    在这里插入图片描述
    然后,假设线程2 争取到了CPU:
    在这里插入图片描述
    这时,又轮到了线程1争取到了CPU,线程1发现A1和V值相同,于是将B1的值传回给主存,将V的值改为value2:
    在这里插入图片描述
    假设现在,线程2 又抢到了CPU的运行权,它发现A2和V 不一样,于是放弃了写入的操作,并将之前的A2、B2全部清空:
    在这里插入图片描述

    以上,就是CAS算法的基本流程,至于怎么实现,本人拿它的实现类来展示下相关代码:
    在这里插入图片描述
    在这里插入图片描述


    现在,本人再来说明一点:

    jdk5增加了并发包java.util.concurrent.*,
    其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁
    JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁

    缺点

    1. 对资源的开销大
    2. 只能保证 变量的原子性 而不能保证 代码块的原子性

    运用CAS算法实现的 原子操作的常用类

    AtomicBoolean 、 AtomicInteger 、 AtomicLong 、 AtomicReference
    AtomicIntegerArray 、 AtomicLongArray
    AtomicMarkableReference
    AtomicReferenceArray


    那么,本人就拿 AtomicInteger来解决下上面的问题:

    package edu.youzg.about_synchronized.core;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class Test {
    
        public static void main(String[] args) {
            //CAS 算法:比较并交换
            //是一种硬件支持
            IRunnable iRunnable = new IRunnable();
            for (int i = 0; i < 10; i++) {
                new Thread(iRunnable).start();
            }
        }
    }
    
    class IRunnable implements Runnable{
        // int i=0;
        //把普通变量换成原子变量,
        AtomicInteger num=new AtomicInteger(1);
        @Override
        public void run() {
            while (true){
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":" + num.getAndIncrement());
            }
        }
    }
    

    本人还是来截取一段运行结果来展示:
    在这里插入图片描述
    可以看到,并没有出现重复值的问题!


    那么,说到 CAS算法,本人就不得不为同学们来扩展下知识点了:

    扩展 —— 乐观锁 与 悲观锁:

    悲观锁:

    概述

    总是假设最坏的情况
    每次去拿数据的时候都认为别人会修改
    所以每次在拿数据的时候都会上锁
    这样别人想拿这个数据就会 阻塞 直到别人拿到锁

    举例

    Java里面的同步原语 synchronized 关键字的实现就是悲观锁

    优点

    • 充分保证线程安全性

    缺点

    • 降低运行效率

    乐观锁:

    概述

    总是假设最好的情况
    每次去拿数据的时候都认为别人不会修改
    所以不会上锁
    但是在更新时会 判断一下在此期间别人有没有去更新这个数据 (可以使用版本号等机制)

    举例

    乐观锁适用于 多读 的应用类型,这样可以提高吞吐量
    像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁
    在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 —— CAS算法 实现的

    优点

    • 非阻塞,效率高

    缺点

    • 会造成大量开销

    (本人 《详解 多线程》 博文 链接:https:////www.cnblogs.com/codderYouzg/p/12418935.html

  • 相关阅读:
    JAVA基础-抽象类和接口
    JAVA基础-多态
    JAVA基础-继承机制
    C++(二十七) — 深拷贝、浅拷贝、复制构造函数举例
    C++(二十六) — 构造函数、析构函数、对象数组、复制构造函数
    C++(二十五) — 类的封装、实现、设计
    C++(二十四) — 指向字符的指针为什么可以用字符串来初始化,而不是字符地址?
    C++(二十三) — 内存泄漏及指针悬挂
    C++(二十二) — 指针变量、函数指针、void指针
    C++(二十一) — 引用概念及本质
  • 原文地址:https://www.cnblogs.com/codderYouzg/p/12418977.html
Copyright © 2011-2022 走看看