并发的原理

说起并发的底层,不得不提volatile,CAS,AQS,本文就是揭露它们神秘的面纱

一.volatile

为了更好的理解volatile,我们需要知道以下几个概念

JMM (java内存模型)

  • 抽象的概念,并不真实存在,它描述的是一组规则或者规范
  • 规定了内存主要划分为主内存和工作内存 (与JVM是不同层面的划分)
  • (可以泛泛的理解为主内存就是堆,工作内存就是栈。堆:线程共享 栈:线程独有)

关于不同的线程操作共享资源的步骤如下:

比如 i++

  1. 将主内存的数据读到(拷贝)工作内存中
  2. 在工作内存中进行计算:+1
  3. 刷新主内存(将工作内存最新的值写入到主内存中)

并发的三大特征: (按照上图解释)

  • 可见性:(及时通知)一个线程改变共享数据后,刷新到主内存中,保证其他的线程马上知道
  • 原子性:不可分离的最小单位。要么一起成功,要么一起失败。 (比如A线程正在执行,这个过程不允许其他线程插入) (比如上面不同的线程操作共享数据的3个步骤,就不保证一气呵成,导致写覆盖)
  • 有序性:(避免指令重排)程序执行的顺序按照代码的先后顺序执行

有了前面的知识,我们来谈谈对volatile的理解?

  • volatile是java虚拟机提供的轻量级的同步机制。可以用在成员变量上,修饰共享资源
  • 保证可见性,有序性(避免指令重排),但是不保证原子性

关于避免指令重排?

  • 所谓的指令重排就是代码在编译期间进行优化,可能有些代码的执行流程不是按照我们写的顺序执行的。
  • 避免指令重排是操作系统级别的方法,通过一个内存屏障,保证不进行指令重排

并发的原理

如何解决volatile不保证原子性?

  • 使用java提供的原子性操作的对象:java.util.concurrent.atomic下的对象
  • 这类对象就是基本数据类型的原子性操作

为啥不用synchronized?

  • 效率低,杀鸡不用屠龙刀。不至于。

开发中什么时候用到volatile?

  • 单例模式的其中一种写法(关于单例模式,专门总结)
  • atomic的源码

适用场合?

  • 值得说明的是:对 volatile 变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证 i++这种操作的原子性,因为本质上 i++是读、写两次操作。
  • 对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean flag = true)
  • 该变量没有包含在具有其他变量的不变式中,也就是说不同的 volatile 变量之间,不能互相依赖。只有在状态真正独立于程序内其他内容时才能使用 volatile。

二.CAS(1)

  • CAS是一条并发原语,属于操作系统级别的命令。原语的执行必须是连续的,不允许中断,,也就是CAS是原子指令,不会造成数据不一致的问题。
  • 全称为Compare And Swap,比较并交换。
  • 它的功能是判断某个位置的值是否为预期值,如果是则改为新的值,这个过程是原子性的

  • (线程工作内存 读取 主内存值后,进行操作之后,准备写入主内存时,进行CAS判断,如果主内存中的值还是刚才复制之前的值,才刷新主内存的值,否则继续循环获得主内存的值)

  • CAS有三个操作数,分别是:
  • 期望值:通过对象地址可以获取
  • 真实值:通过对象地址,使用Unsafe类可以获取,写入主内存时的真实值
  • 更新值:期望值==真实值时,更新数据

应用:原子性操作对象保证原子性就是使用CAS思想。

AtomicInteger atomicInteger = new AtomicInteger(1);
    atomicInteger.getAndIncrement();//相当于 i++

上面代码追溯源码:

并发的原理

注意:

  • Unsafe:是实现CAS的核心类,该类的方法全部是native修饰,主要用于直接调用操作系统底层资源执行响应的任务。
  • 自旋锁不会一直死循环(就是比较不成功不会一直比较下去), 因为var2是volatile的,保证可见性。也会实时得到新的值。

CAS的缺点:

  • 循环时间开销很大(忙循环):如果一直预期值!=真实值 ,底层就一直循环得到主内存最新的值
  • 只能保证一个共享变量的原子性操作:比如一组代码的原子性操作是不能保证的,就需要使用synchronized
  • 引出来ABA问题

二.ABA(2)

简称:狸猫换太子

什么是ABA问题(ABA问题是怎么产生的)?

  • 主要因为CAS会在写入时,比较当前的主内存的值是否和复制前的值一样,一样就会更新数据。
  • 但是这个过程会产生”时间差”:比如A线程取出主内存的值进行操作,需要好长时间。这个时间内, B线程可能也会拿到主内存的值进行多次修改,比如1->2->1。
  • A线程执行完回来一判断,发现预期 值和真实值一样,A线程以为没被改过。但其实B线程”改过之后又改回来了”。那么这就存在问题了。尽管CAS操作成功,但是不代表这个过程就没有问题

ABA问题解决方案?

  • 采用时间戳原子引用

先解释什么是原子引用

  • 就是java不仅提供了一些基本数据类型的原子性操作对象,我们还可以使用new AtomicReference<>()对象,将我们自己的类转换为原子性操作类。

什么是时间戳原子引用?

  • 就是在原子引用的前提下,加了一个时间戳(版本号),就类似于乐观锁。
  • 每次进行CAS判断的时候, 不仅仅知识判断”真实值”是否等于”预期值”,还得加一个条件,判断”真实版本号”是否等于”预期版本号”。
  • 使用new AtomicStampedReference<>(资源初始值,版本初始值);
public class ABADemo {
    static AtomicReference 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);
        }, "Thread 1").start();

        new Thread(() -> {
            try {
                //保证线程1完成一次ABA操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
        }, "Thread 2").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第1次版本号" + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp());
        }, "Thread 3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);

            System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference());
        }, "Thread 4").start();
    }
}

三.AQS

  • 定义了一套多线程访问共享资源的同步器框架(就是一个抽象类),许多同步类实现都依赖于它, 如常用的ReentrantLock/Semaphore/CountDownLatch…。
  • volatile修饰的state标识状态+队列,至于线程的排队、等待、唤醒等,底层的AQS都已经实现好了,我们不用关心。
  • AQS定义两种资源共享方式
  • Exclusive (独占,只有一个线程能执行,如ReentrantLock)
  • Share (共享,多个线程可同时执行,如Semaphore/CountDownLatch)

源码讲解

  • Node:内部类,队列的节点,存放Thread。
  • state:0代表未锁定,N代表锁定了几个资源,比如CountDownLatch,每次-1,state-1.

  • 获得锁:

  • acquire():独占锁获得锁的入口
  • tryAcquire():尝试获得资源,失败返回false,成功直接执行任务。不公平锁的体现
  • addWaiter(Node):没有获得资源,当前线程加入到等待队列的队尾,并返回当前线程所在的结点
  • end(Node):上述方法没能成功,使用该方法直接放在队尾
  • acquireQueued(Node, int):进入等待状态休息,直到其他线程彻底释放资源后唤醒自己
  • 释放锁:
  • release(int):释放资源的入口
  • tryRelease(int):尝试释放指定量的资源
  • unparkSuccessor(Node):唤醒等待队列中最前边的那个未放弃线程

共享模式下acquireShared()和releaseShared()与上述一样

寄语:一定要努力,堵住那悠悠众口

Original: https://www.cnblogs.com/monkey-xuan/p/15864933.html
Author: 小猴子_X
Title: 并发的原理

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/579536/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 二. 手写SpringMVC框架

    1 新建DispatcherServlet 1.2 在src目录下,新建applicationContext.xml 1.3 在 Dispatcher S ervlet 的构造方法…

    Java 2023年6月16日
    049
  • 力扣|Q886可能的二分法PossibleBipartition

    Q886PossibleBipartition 题目简介 给定一组 n 人(编号为 1, 2, …, n), 我们想把每个人分进任意大小的两组。每个人都可能不喜欢其他人…

    Java 2023年6月8日
    078
  • JAVA 异常 基本知识

    异常 异常定义 异常是运行过程中出现的错误 人为错误:填写错误等 随机错误:网络中断、内存耗尽等 一个健壮的程序必须处理各种各样的错误 Java的异常是class Object T…

    Java 2023年6月9日
    068
  • 如何入门一个开源软件

    查看 conf下的默认配置,根据个人的需求尝试自定义配置 Original: https://www.cnblogs.com/kwanwoo/p/14702843.htmlAuth…

    Java 2023年6月8日
    071
  • MySQL八:读懂MVCC多版本并发控制

    转载~ mysql在并发的情况下,会引起脏读,幻读,不可重复读等一系列的问题,为解决这些问题,引入了mvcc的机制。本文就详细看看mvcc是怎么解决脏读,幻读等问题的。 1、 数据…

    Java 2023年6月8日
    084
  • Spring

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    Java 2023年6月15日
    069
  • 原型模式详解

    原型模式 1.1原型模式概述 1.1.1原型模式定义 原型模式(Prototype Pattern)指原型实例指定创建对象的种类,并且通过复制这些原型创建新的对象,属于创建型设计模…

    Java 2023年6月7日
    0111
  • JavaScript基础知识

    undefined window.alert() –写入警告框 document.write()—写入HTML输出 console.log()—写入浏览器控制台 aler…

    Java 2023年6月5日
    081
  • SpringBoot系列之使用Spring Task实现定时任务

    @ 一、前言介绍 二、Spring Task 2.1 SpringTask简介 2.2 实验环境准备 2.3 Enable Scheduling 2.4 单线程定时任务 2.5 线…

    Java 2023年5月30日
    080
  • 【转】【数学】矩阵的旋转

    1.简介 计算机图形学中的应用非常广泛的变换是一种称为仿射变换的特殊变换,在仿射变换中的基本变换包括平移、旋转、缩放、剪切这几种。本文以及接下来的几篇文章重点介绍一下关于旋转的变换…

    Java 2023年5月29日
    083
  • 有点长的博客:Redis不是只有get set那么简单

    我以前还没接触Redis的时候,听到大数据组的小伙伴在讨论Redis,觉得这东西好高端,要是哪天我们组也可以使用下Redis就好了,好长一段时间后,我们项目中终于引入了Redis这…

    Java 2023年6月5日
    078
  • WORD模板使用

    Date: 2012-12-03 13:05:55 中国标准时间 Author: csophys Org version 7.8.11 with Emacs version 24V…

    Java 2023年6月7日
    0112
  • Java中private、protected、public和default的区别

    public: 具有最大的访问权限,可以访问任何一个在classpath下的类、接口、异常等。它往往用于对外的情况,也就是对象或类对外的一种接口的形式。 protected: 主要…

    Java 2023年5月29日
    095
  • Spring核心之Ioc容器

    spring框架 Spring框架是由于软件开发的复杂性而创建的。Spring使用的是基本的JavaBean来完成以前只可能由EJB完成的事情。然而,Spring的用途不仅仅限于服…

    Java 2023年6月5日
    076
  • 研发过程中的文档管理与工具

    写文档也是技术活 01:实践 对于多数开发同学来说,很多时候即讨厌没有研发文档,但是自己又不愿意常写文档,痛且倔强着; 程序员该不该写文档,与争论哪种编程语言最好一样,想撕的嘴不留…

    Java 2023年6月15日
    073
  • virtual box入门使用+踩坑

    事实上我并不是完全安装上面步骤进行安装,在vagrant 使用上,我选择的是使用命令行 进行虚拟机配置。 安装完virtual后,记得把虚拟机存储路径变为非C盘。不然C盘分分钟爆炸…

    Java 2023年6月7日
    071
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球