volatile详解、原理

news2024/11/25 0:55:07

文章目录

  • 一、Volatile的定义和作用
    • 1.1 Volatile简介
    • 1.2 Volatile作用
  • 二、并发编程中的三个问题:可见性、原子性、有序性
  • 二、Java内存模型(JMM)
  • 三、volatile变量的特性
    • 3.1 线程可见性
    • 3.2 禁止重排序
      • 禁止重排序原理
      • 禁止重排序举例
    • 3.3 volatile特性
  • 四、volatile原理
  • 五、适用场景、不适用场景
    • 5.1 volatile适用场景
    • 5.2 volatile不适用的场景
      • 5.2.1 volatile不适用 复合操作。
      • 5.2.2 如何解决
  • 六、Volatile与Synchronized比较

一、Volatile的定义和作用

1.1 Volatile简介

Java允许线程访问共享变量。为了确保共享变量能被一致、可靠地更新,线程必须确保 它是排他性地使用此共享变量,通常都是获得对这些共享变量强制排他性的同步锁。Java编程语言提供了另一种机制,volatile域变量,对于某些场景的使用 这会更加的方便。可以把变量声明为volatile,以让Java内存模型来保证所有线程都能看到这个变量的同一个值。

volatile是Java提供的一种轻量级的同步机制。Java语言包含两种内在的同步机制:同步块(或同步方法) 和 volatile变量,相比于synchronized(synchronized通常被称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

注:Volatile只能修饰变量。

1.2 Volatile作用

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证线程间的可见性:当一个线程对共享变量进行了修改,另外的线程可以立即看到修改后的最新值。CPU都是有行缓存的,volatile能让行缓存无效,因此能读到内存中最新的值。

  • 禁止进行指令重排序:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果

二、并发编程中的三个问题:可见性、原子性、有序性

还记得并发编程中的可见性、原子性、有序性吗?如果忘记可以到这里重温复习【并发编程】1 synchronized底层实现原理、Java内存模型JMM;可重入、不可中断、monitor、CAS、乐观锁和悲观锁;对象内存结构、Mark Word、synchronized锁升级

  • 可见性:当一个线程对共享变量进行了修改,另外的线程并没有立即看到修改后的最新值。 ——> 解决:当多个线程访问同一个变量时,一个线程修改了该变量的值,其他线程能够立即看到修改后的最新值
  • 原子性:当一个线程对共享变量操作到一半时,另外的线程也有可能来操作共享变量,干扰了前一个线程的操作。 ——> 解决:在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行
  • 有序性:程序代码在执行过程中的先后顺序,由于Java在编译期以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序。 ——> 解决:程序执行的顺序按照代码的先后顺序执行

二、Java内存模型(JMM)

和Java内存结构不同,Java内存模型是一套规范、是标准化的,屏蔽掉了底层不同计算机的区别(Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则)。

JMM描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,Java内存模型是对共享数据的可见性、有序性、和原子性的规则和保障。细节如下。

  • 主内存:主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。
  • 工作内存:每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。

在这里插入图片描述

对于普通的共享变量x,线程1将其修改为某个值 发生在线程1的工作内存中,此时还未同步到主内存中去;而线程2已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程中的不可见问题,较粗暴的方式自然就是加锁,但此处使用synchronized或Lock这些方式 太重量级了,比较合理的方式是 volatile。

注意:JMM是抽象的内存模型,所谓的主内存、工作内存都是抽象概念,不一定就真实地对应cpu缓存、物理内存。

具体细节见【并发编程】1 synchronized底层实现原理、Java内存模型JMM;可重入、不可中断、monitor、CAS、乐观锁和悲观锁;对象内存结构、Mark Word、synchronized锁升级
在这里插入图片描述

三、volatile变量的特性

3.1 线程可见性

用 volatile 修饰共享变量,当一个线程对共享变量进行了修改,另外的线程可以立即看到修改后的最新值。CPU都是有行缓存的,volatile能让行缓存无效,因此能读到内存中最新的值。

  • 当某个线程写volatile变量、将修改的值同步回主内存时,JMM会把该线程工作内存中的变量强制刷新到主内存中去
  • JMM会把其他工作内存的值全部设置为失效,线程会重新读取共享内存的值

详解

当一个线程把主内存中的共享变量读取到自己的本地内存中,然后做了更新。在还没有把共享变量刷新的主内存的时候,另外一个线程是看不到的。如何把修改后的值刷新到主内存中?

现代的处理器使用写缓冲区临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,较少对内存总线的占用。但是什么时候写入到内存是不知道的。

在这里插入图片描述

所以就引入了volatile,volatile是如何保证可见性的呢?

  • 将当前处理器缓存行的数据写回到系统内存。
  • 这个写回内存的操作会使其他cpu里缓存了该内存地址的数据无效。
  • 如果声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的还是旧的,再执行操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

3.2 禁止重排序

用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果。

首先看下不加volatile的情况

1)修改pom.xml文件,添加依赖

<dependency>
    <groupId>org.openjdk.jcstress</groupId>
    <artifactId>jcstress-core</artifactId>
    <version>0.14</version>
</dependency>

2)编写代码

//为什么使用此工具,直接运行代码不行吗?  只有使用大量的线程去执行,才能测试出指令重排序的效果
@JCStressTest     //用并发压缩工具jcstress 测试类方法
@Outcome(id = {"0, 0", "1, 1", "0, 1"}, expect = Expect.ACCEPTABLE, desc = "ok")    //@Outcome 对输出结果进行处理, "0, 0", "1, 1", "0, 1"  打印ok
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger")
@State
public class TestOrdering {
    int x;
    int y;

    @Actor
    public void actor1() {   //写
        x = 1;
        y = 1;
    }

    @Actor
    public void actor2(II_Result r) {   //读
        r.r1 = y;
        r.r2 = x;
    }
}

mvn clean install,java -jar xxx\target\jcstress.jar

3)查看项目对应路径results目录下的html文件,用浏览器打开,观察运行结果

在这里插入图片描述

在这里插入图片描述

根据运行结果,出现了”1,0“这种情况,说明actor1()方法先给y赋值、再给x赋值,确实发生指令重排序(指令重排序是为了实现指令流水线,属于计算机组成原理的内容)

如何解决:在变量上添加volatile,禁止指令重排序,则可以解决问题(volatile原理就是加了一些屏障,使屏障后的代码一定不会比屏障前的代码先执行,从而实现有序性)

  • 给 y 加上 volatile,不会出现”1,0“,解决了指令重排序的问题。
//省略部分代码
public class TestOrdering {
    int x;
    volatile int y;
    
    //其他不变,省略
}

在这里插入图片描述

volatile的底层原理是,给共享变量加上不同的屏障,保证指令不会发生重排序

  • 把volatile加在x上面,无法禁止指令重排序
//省略部分代码
public class TestOrdering {
    int x;
    volatile int y;
    
    //其他不变,省略
}

在这里插入图片描述

写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下,但无法阻止下方的指令往上走,y=1可以往上走、r.r1=y可以往下走。

  • 可以给x、y都加上volatile,也能解决问题,但性能不高,不推荐

在这里插入图片描述

在这里插入图片描述

禁止重排序原理

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分为如下三种:

在这里插入图片描述

1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。

当变量声明为volatile时,Java编译器在生成指令序列时,会插入内存屏障指令,通过内存屏障指令来禁止重排序。

JMM内存屏障插入策略如下:

  • 在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个StoreLoad屏障。
  • 在每个volatile读操作后面插入一个LoadLoad,LoadStore屏障。

Volatile写插入内存屏障后生成指令序列示意图:

在这里插入图片描述

Volatile读插入内存屏障后生成指令序列示意图:

在这里插入图片描述

通过上面这些我们可以得出如下结论:编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序。

禁止重排序举例

从一个最经典的例子来分析重排序问题,以单例模式的双重检查锁实现(线程安全)为例

public class Singleton {
	public volatile static Singleton uniqueInstance;
    /**
    * 构造函数私有,禁止外部实例化
    */
    private Singleton() {}
    
    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

为什么使用volatile关键字修饰uniqueInstance实例变量?

因为uniqueInstance = new Singleton()这段代码执行时分为三步:

  1. 为uniqueInstance分配内存空间
  2. 初始化uniqueInstance
  3. 将uniqueInstance指向分配的内存地址

正常的执行顺序当然是 1–>2–>3,但由于JVM具有指令重排的特性,执行顺序可能变成 1–>3–>2。单线程环境时,指令重排没有什么问题;多线程环境下,会导致有些线程可能会获取到还没初始化的实例。如线程A只执行了1和3,此时线程B来调用getUniqueInstance(),发现 uniqueInstance 不为空,便获取 uniqueInstance 实例,但是其实此时的 uniqueInstance 还没有初始化。

解决方法就是 加一个 volatile 关键字修饰 uniqueInstancevolatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。

3.3 volatile特性

  • volatile仅能使用在变量级别
  • volatile仅能实现变量的修改可见性,不能保证原子性,volatile + cas 就实现了原子性,如atomic包下面的类。
  • volatile不会造成线程的阻塞
  • volatile标记的变量不会被编译器优化

volatile可以保证可见性、有序性,无法保证原子性,所以是线程不安全的(它对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性,因为本质上i++是读、写两次操作)。要保证多线程的原子性, 可以通过AtomicInteger或者Synchronized来实现,本质上就是CAS操作(见下文5.2)

【synchronized可以保证可见性、原子性、有序性,因为锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性】

java.util.concurrent.*里面的高级线程安全数据结构像ConcurrentHashMap以及java.util.concurrent.atomic.*等的实现都用到了volatile。可以多看看这些类的实现,以加深对volatile的理解和运用。

四、volatile原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用"内存屏障"来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

(2)它会强制将对缓存的修改操作立即写入主存

(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

计算机科学里面,为了解决复杂性,都会分层。正如一个名人所说:“计算机的任何问题都可以通过增加一个虚拟层来解决”(“All problems in computer science can be solved by another level of indirection”)。volatile虚拟机层引入的,解决语言层面的问题,那么它的实现,必然是靠下一层的支持,也就是需要汇编或者说处理器指令的支持来实现,volatile是靠内存屏障和**MESI**(缓存一致性协议)来达成的它的作用的。

内存屏障(Memory Barriers)是处理器提供的一组内存操作指令,它的作用是限制内存操作的顺序,也就是说内存屏障像一个栅栏一样,它前面的指令要在它后面的指令之前完成;还能强制把缓存写入到主存;再有的就是触发缓存一致性,就是当有写变量时,会把其他CPU核心的缓存变为无效。

五、适用场景、不适用场景

5.1 volatile适用场景

对变量可见性有要求、对读取顺序没要求的情况下。如 多线程情况下的标志位

  • 读操作,多于写操作
  • 写操作,不依赖于变量的当前值,即纯赋值操作
  • 只需要读取的值,不需要等待某一特定的值

5.2 volatile不适用的场景

5.2.1 volatile不适用 复合操作。

例如 number++ 不是一个原子性操作,由读取、加、赋值3步组成,下列程序可能返回多种结果,2677、4202、4722、4910、5000

public class Test02Atomicity {
    private volatile static int number = 0;

    public static void main(String[] args) throws InterruptedException {
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {
                number++;
            }
        };

        List<Thread> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            list.add(t);
        }

        for (Thread thread : list) {
            //让主线程等待自己创建的线程执行完毕,5个线程执行完 再取number的值
            thread.join();
        }

        System.out.println("number=" + number);  //值有多种可能,2677、4202、4722、4910、5000


    }
}

5.2.2 如何解决

1)采用synchronized

public class Test02Atomicity {
    private static int number = 0;
    private static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (obj) {  
                    number++; //synchronized保证 number++ 为原子操作。线程获取不到锁 就会等待
                }
            }
        };

        List<Thread> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            list.add(t);
        }

        for (Thread thread : list) {
            //让主线程等待自己创建的线程执行完毕
            thread.join();
        }

        System.out.println("number=" + number);  //5000

    }
}

2)采用Lock

public class Test02Atomicity {
    private static int number = 0;
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {

                try {
                    lock.lock();
                    number++;
                } finally {
                    lock.unlock();
                }

            }
        };

        List<Thread> list = new ArrayList<>();
        //使用5个线程来进行
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            list.add(t);
        }
        for (Thread thread : list) {
            //让主线程等待自己创建的线程执行完毕,5个线程执行完 再取number的值
            thread.join();
        }

        System.out.println("number=" + number);  //5000

    }
}

3)采用java并发包中的原子操作类,原子操作类是通过CAS的方式来保证其原子性的

public class Test02Atomicity {
    private static AtomicInteger atomicInteger = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {
                atomicInteger.incrementAndGet();   //AtomicInteger的incrementAndGet()可保证变量赋值的原子性
            }
        };

        List<Thread> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(increment);
            t.start();
            list.add(t);
        }

        for (Thread thread : list) {
            //让主线程等待自己创建的线程执行完毕
            thread.join();
        }

        System.out.println("atomicInteger=" + atomicInteger.get());  //5000

    }
}

六、Volatile与Synchronized比较

  • Volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,因此Volatile性能更好。
  • Volatile只能修饰变量(类的成员变量、类的静态成员变量),synchronized可以修饰方法,静态方法,代码块。
  • Volatile对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性。而锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性。
  • 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
  • volatile是变量在多线程之间的可见性,synchronize是多线程之间访问资源的同步性。

参考 https://zhuanlan.zhihu.com/p/633426082、https://blog.csdn.net/u012723673/article/details/80682208、https://blog.csdn.net/xinghui_liu/article/details/124379221

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1671992.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

七、e2studio VS STM32CubeIDE之显示中文编码

目录 一、概述/目的 二、查看和修改文件编码 三、eclipse编码格式 3.1 优先级 3.1.1 全局workspace 3.1.2 工程 3.1.3 文件 3.1.4 全局文件的content type 二、STM32CubeIDE设置显示中文编码 二、e2studio设置显示中文编码 七、e2studio VS STM32CubeIDE之显示中文编…

【Linux】网络接口绑定和组合的操作实例

网络接口绑定和组合的操作实例 &#xff08;一&#xff09;网卡1. 增2. 查3. 激活——设置网络接口 &#xff08;二&#xff09;网络接口绑定1. 概述2. 实验操作3. 删除绑定 &#xff08;三&#xff09;网络接口组合1. 概述2. 实验操作 &#xff08;一&#xff09;网卡 1. 增 …

第十五篇:全面防护:构建不容侵犯的数据库安全策略与实战指南

全面防护&#xff1a;构建不容侵犯的数据库安全策略与实战指南 1. 引言&#xff1a;数据库安全的现代战略 1.1 简介&#xff1a;数据库安全在当今的数字化时代中的重要性 在数字化的浪潮中&#xff0c;数据已成为企业乃至国家的核心资产&#xff0c;其价值不亚于实体世界的黄…

【JavaEE初阶系列】——博客系统(编写服务器/前后端交互代码)

目录 &#x1f6a9;部署页面需求 &#x1f6a9;准备工作 &#x1f6a9;获取博客列表页 &#x1f6a9;博客详情页 &#x1f6a9;实现登录页面 &#x1f388;强制要求登录 &#x1f388;显示用户信息 &#x1f6a9;退出登录 &#x1f6a9;发布博客 &#x1f6a9;部署页面…

5.nginx常用命令和日志定时切割

一. nginx常用的相关命令介绍 1.强制关闭nginx: ./nginx -s stop 2.优雅的关闭nginx: ./nginx -s quit 3.检查配置文件是否正确&#xff1a; ./nginx -t 4.查看nginx版本&#xff1a; ./nginx -v 5.查看nginx版本相关的配置环境信息&#xff1a;./nginx -V 6.nginx帮助信…

Lists.partition用法详解

文章目录 一、引入依赖二、用法三、输出 一、引入依赖 依赖于谷歌的guava 包 <!-- guava --><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>28.1-jre</version></dependency&…

C++语法|volatile关键字!从CPU角度进行理解

个人认为C有着复杂、臃肿的语法系统&#xff0c;但是也正是因为这些特性&#xff0c;让我们在使用C时既能深入到操作系统级的控制&#xff0c;也能抽象出来完全专注于一些业务问题。 这里为大家推荐一本书和汇编代码阅读网站&#xff01; 《CPU眼里的C/C》 Compiler Explorer 我…

ssm+vue的公务用车管理智慧云服务监管平台查询统计(有报告)。Javaee项目,ssm vue前后端分离项目

演示视频&#xff1a; ssmvue的公务用车管理智慧云服务监管平台查询统计&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&…

GCP谷歌云有什么数据库类型,该怎么选择

GCP谷歌云提供的数据库类型主要包括&#xff1a; 关系型数据库&#xff1a;这类数据库适用于结构化数据&#xff0c;通常用于数据结构不经常发生变化的场合。在GCP中&#xff0c;关系型数据库选项包括Cloud SQL和Cloud Spanner。Cloud SQL提供托管的MySQL、PostgreSQL和SQL Se…

高效项目管理:如何利用zz-plan在线甘特图工具

作为项目管理人员&#xff0c;使用 zz-plan https://zz-plan.com/这样的在线甘特图协作软件可以极大地提高项目管理的效率和效果。以下是结合zz-plan特点的一些关键步骤&#xff1a; 1. 制定项目计划 在zz-plan上创建新的项目&#xff0c;定义项目目标、关键里程碑和最终期限。…

【数据可视化01】matplotlib实例介绍2

目录 一、引言二、实例介绍1.箱线图2.热力图3.线条形式 一、引言 接着上一文章【数据可视化01】matplotlib实例介绍1继续介绍matplotlib的实例。 二、实例介绍 在matplotlib中&#xff0c;常用的图形类型包括&#xff1a; 箱线图&#xff08;Box plot&#xff09;&#xff1…

d17(154-168)-勇敢开始Java,咖啡拯救人生

目录 方法递归 字符集 编码-解码 IO流 字节流 字节输入流 InputSream FileInputStream 字节输出流 OutputSream FileOutputSream 释放资源的方式 try-catch-finallly try-with-resource 字符流 字符输入流 Reader FileReader 文件字符输出流 Writer FileWriter …

(done) 什么是马尔可夫链?Markov Chain

参考视频&#xff1a;https://www.bilibili.com/video/BV1ko4y1P7Zv/?spm_id_from333.337.search-card.all.click&vd_source7a1a0bc74158c6993c7355c5490fc600 如下图所示&#xff0c;马尔可夫链条实际上就是 “状态机”&#xff0c;只不过状态机里不同状态之间的边上是 “…

李廉洋:5.13黄金原油美盘行情分析,必看策略。

黄金消息面分析&#xff1a;机构最新调查中的一些受访者表示&#xff0c;美国最大的科技股不仅是对创新行业的押注&#xff0c;而且可能是对冲通胀的工具。46%的受访者表示&#xff0c;数十年来一直是避险之选的黄金仍被视为抵御价格上涨风险的最佳保障。但近三分之一的人表示&…

【eclipse】如何在IDE里创建一个Java Web项目?

如何在eclipse中创建一个动态Web项目并成功运行&#xff1f; 一、 最终效果 懒得写那么多了…我也不知道该怎么写了&#xff0c;有点乱&#xff0c;有问题可以在评论里留言&#xff0c;我看到会解决的&#xff0c;在这个过程中也踩到了一些坑&#xff0c;但好在有CSDN帮助解决…

GEE数据集——东南亚区域油棕种种植分布(油棕榈树种植园的概率)数据集

森林数据伙伴关系围绕对全球商品驱动的森林砍伐、森林退化和恢复工作的全球监测&#xff0c;加强合作与应用。 世界各国政府和公司都承诺帮助制止砍伐森林和加快恢复&#xff0c;以避免气候变化带来的最坏影响&#xff0c;防止生物多样性丧失&#xff0c;保护森林对人类和自然…

JavaEE之线程(4)——线程安全、线程安全的原因,synchronized关键字

前言 在本栏的前面的内容中&#xff0c;我们介绍了线程的创建、Thread 类及常见方法、线程的状态&#xff0c;今天我们来介绍一下关于线程的另一个重点知识——线程安全。 一、线程安全 基本概念&#xff1a; 线程安全的确切定义是复杂的&#xff0c;但我们可以这样认为&…

微前端无界方案

微前端无界 无界 官方文档 主应用 1、引入 // 无框架时使用wujie import Wujie from wujie // 当结合框架时使用wujie-xxx // import Wujie from "wujie-vue2"; // import Wujie from "wujie-vue3"; // import Wujie from "wujie-react";cons…

想搭建AI知识库的企业看这篇就够了

企业要想在激烈的竞争中脱颖而出&#xff0c;有一套高效、智能的知识管理系统是非常重要的。搭建AI知识库能够帮助企业整合、分类、检索和应用知识&#xff0c;因此成为众多企业的第一选择。对于想要搭建AI知识库的企业来说&#xff0c;应该注意哪些方面呢&#xff1f;本文将从…

专业网站设计方案

当前互联网的快速发展和普及&#xff0c;使得网站设计成为了一个极其重要的环节。一个好的网站设计方案将能够吸引更多的访问者&#xff0c;提高用户体验&#xff0c;增强品牌形象。下面将为您介绍一个专业的网站设计方案。 首先&#xff0c;一个专业的网站设计方案应该具备清晰…