万字长文详细搞懂 volatile 关键字

volatile 这个关键字大家都不陌生,这个关键字一般通常用于并发编程中,是 Java 虚拟机提供的轻量化同步机制,你可能知道 volatile 是干啥的,但是你未必能够清晰明了的知道 volatile 的实现机制,以及 volatile 解决了什么问题,这篇文章我就来带大家解析一波。

volatile 能够保证共享变量之间的 可见性,共享变量是存在堆区的,而堆区又与内存模型有关,所以我们要聊 volatile ,就需要首先了解一下 Java 内存模型。Java 中的内存模型是 JVM 提供的,而 JVM 又是和内存进行交互的,所以在聊 Java 内存模型前,我们还需要了解一下操作系统层面中内存模型的相关概念。

先从内存模型谈起

计算机在执行程序时,会从内存中读取数据,然后加载到 CPU 中运行。由于 CPU 执行指令的速度要比从内存中读取和写入的速度快的多,所以如果每条指令都要和内存交互的话,会大大降低 CPU 的运行速度,造成昂贵的 CPU 性能损耗,为了解决这种问题,设计了 CPU 高速缓存。有了 CPU 高速缓存后,CPU 就不再需要频繁的和内存交互了,有高速缓存就行了,而 CPU 高速缓存,就是我们经常说的 L1 、L2、L3 cache。

当程序在运行过程中,会将运算需要的数据从主存复制一份到 CPU 的高速缓存中,在 CPU 进行计算时就可以直接从它的高速缓存读写数据,当运算结束之后,再将高速缓存中的数据刷新到主存中。

就拿我们常说的

i = i + 1 

来举例子

当 CPU 执行这条语句时,会先从内存中读取 i 的值,复制一份到高速缓存当中,然后 CPU 执行指令对 i 进行加 1 操作,再将数据写入高速缓存,最后将高速缓存中 i 最新的值刷新到主存当中。

这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了,因为每个 CPU 都可以运行一条线程,线程就是程序的顺序执行流,因此每个线程运行时有自己的高速缓存(对单核 CPU 来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核 CPU 为例来讲解说明。

比如同时有 2 个线程执行这段代码,假如初始时 i 的值为 0,那么我们希望两个线程执行完之后 i 的值变为 2,但是事实会是这样吗?

可能存在下面一种情况:初始时,两个线程分别读取 i 的值存入各自所在的 CPU 高速缓存中,然后线程 1 执行加 1 操作,把 i 的最新值 1 写入到内存。此时线程 2 的高速缓存当中 i 的值还是 0,进行加 1 操作之后,i 的值为 1,然后线程 2 把 i 的值写入内存。

最终结果 i 的值是 1,而不是 2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。

也就是说,如果一个变量在多个 CPU 中都存在缓存(一般在多线程编程时才会出现),就很可能存在缓存不一致的问题。

Java 内存模型

我们上面说到,共享变量会存在缓存不一致的问题,缓存不一致问题换种说法就是线程安全问题,那么共享变量在 Java 中是如何存在的呢?JVM 中有没有提供线程安全的变量或者数据呢?

这就要聊聊 Java 内存模型的问题了,图示如下

  • 虚拟机栈 : Java 虚拟机栈是线程私有的数据区,Java 虚拟机栈的生命周期与线程相同,虚拟机栈也是局部变量的存储位置。方法在执行过程中,会在虚拟机栈种创建一个 栈帧(stack frame)
  • 本地方法栈: 本地方法栈也是线程私有的数据区,本地方法栈存储的区域主要是 Java 中使用 native 关键字修饰的方法所存储的区域。
  • 程序计数器:程序计数器也是线程私有的数据区,这部分区域用于存储线程的指令地址,用于判断线程的分支、循环、跳转、异常、线程切换和恢复等功能,这些都通过程序计数器来完成。
  • 方法区:方法区是各个线程共享的内存区域,它用于存储虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据。
  • : 堆是线程共享的数据区,堆是 JVM 中最大的一块存储区域,所有的对象实例都会分配在堆上
  • 运行时常量池:运行时常量池又被称为 Runtime Constant Pool,这块区域是方法区的一部分,它的名字非常有意思,它并不要求常量一定只有在编译期才能产生,也就是并非编译期间将常量放在常量池中,运行期间也可以将新的常量放入常量池中,String 的 intern 方法就是一个典型的例子。

根据上面的描述可以看到,会产生缓存不一致问题(线程安全问题)的有堆区和方法区。而虚拟机栈、本地方法栈、程序计数器是线程私有,由线程封闭的原因,它们不存在线程安全问题。

针对线程安全问题,有没有解决办法呢?

一般情况下,Java 中解决缓存不一致的方法有两种,第一种就是 synchronized 使用的总线锁方式,也就是在总线上声言 LOCK# 信号;第二种就是著名的 MESI 协议。这两种都是硬件层面提供的解决方式。

我们先来说一下第一种总线锁的方式。通过在总线上声言 LOCK# 信号,能够有效的阻塞其他 CPU 对于总线的访问,从而使得只能有一个 CPU 访问变量所在的内存。在上面的 i = i + 1 代码示例中,在代码执行的过程中,声言了 LOCK# 信号后,那么只有等待 i = i + 1 的结果执行完毕并应用到内存后,总线锁才会解开,其他 CPU 才能够继续访问内存中的变量,再继续执行后面的代码,这样就解决了缓存不一致问题。

但是上面的方式会有一个问题,由于在锁住总线期间,其他 CPU 无法访问内存,导致效率低下。

在 JDK 1.6 之后,优化了 synchronized 声言 LOCK# 的方式,不再对总线进行锁定,转而采取了对 CPU 缓存行进行锁定,因为本篇文章不是介绍 synchronized 实现细节的文章,所以不再对这种方式进行详细介绍,读者只需要知道在优化之后,synchronized 的性能不再成为并发问题的瓶颈了。

MESI 协议就是缓存一致性协议,即 Modified(被修改)Exclusive(独占的) Shared(共享的) Or Invalid(无效的)。MESI 的基本思想就是如果发现 CPU 操作的是共享变量,其他 CPU 中也会出现这个共享变量的副本,在 CPU 执行代码期间,会发出信号通知其他 CPU 自己正在修改共享变量,其他 CPU 收到通知后就会把自己的共享变量置为无效状态。

并发编程中的三个主要问题

可见性问题

在单核 CPU 时代,所有的线程共用一个 CPU,CPU 缓存和内存的一致性问题容易解决,我们还拿上面的 i = 1 + 1 来举例,CPU 和 内存之间如果用图来表示的话我想会是下面这样。

在多核时代,每个核都能够独立的运行一个线程,每个 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程使用的是不同的 CPU 缓存。

因为 i 没有经过任何线程安全措施的保护,多个线程会并发修改 i 的值,所以我们认为 i 不是线程安全的,导致这种结果的出现是由于 aThread 和 bThread 中读取的 i 值彼此不可见,所以这是由于 可见性 导致的线程安全问题。

原子性问题

当两个线程开始运行后,每个线程都会把 i 的值读入到 CPU 缓存中,再执行 + 1 操作,然后把 + 1 之后的值写入内存。因为线程间都有各自的虚拟机栈和程序计数器,他们彼此之间没有数据交换,所以当 aThread 执行 + 1 操作后,会把数据写入到内存,同时 bThread 执行 + 1 操作后,也会把数据写入到内存,因为 CPU 时间片的执行周期是不确定的,所以会出现当 aThread 还没有把数据写入内存时,bThread 就会读取内存中的数据,然后执行 + 1操作,再写回内存,从而覆盖 i 的值。

有序性问题

在并发编程中还有带来让人非常头疼的 有序性 问题,有序性顾名思义就是顺序性,在计算机中指的就是指令的先后执行顺序。一个非常显而易见的例子就是 JVM 中的类加载

这是一个 JVM 加载类的过程图,也称为类的生命周期,类从加载到 JVM 到卸载一共会经历五个阶段 加载、连接、初始化、使用、卸载。这五个过程的执行顺序是一定的,但是在连接阶段,也会分为三个过程,即 验证、准备、解析 阶段,这三个阶段的执行顺序不是确定的,通常交叉进行,在一个阶段的执行过程中会激活另一个阶段。

在执行程序的过程中,为了提高性能,编译器和处理器通常会对指令进行重排序。重排序主要分为三类

  • 编译器优化的重排序:编译器在不改变单线程语义的情况下,会对执行语句进行重新排序。
  • 指令集重排序:现代操作系统中的处理器都是并行的,如果执行语句之间不存在数据依赖性,处理器可以改变语句的执行顺序
  • 内存重排序:由于处理器会使用读/写缓冲区,出于性能的原因,内存会对读/写进行重排序

也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

volatile 的实现原理

上面聊了这么多,你可能都要忘了这篇文章的故事主角了吧?主角永远存在于我们心中 …

其实上面聊的这些,都是在为 volatile 做铺垫。

在并发编程中,最需要处理的就是线程之间的通信和线程间的同步问题,上面的可见性、原子性、有序性也是这两个问题带来的。

可见性

而 volatile 就是为了解决这些问题而存在的。Java 语言规范对 volatile 下了定义:Java 语言为了确保能够安全的访问共享变量,提供了 volatile 这个关键字,volatile 是一种轻量级同步机制,它并不会对共享变量进行加锁,但在某些情况下要比加锁更加方便,如果一个字段被声明为 volatile,Java 线程内存模型能够确保所有线程访问这个变量的值都是一致的。

一旦共享变量被 volatile 修饰后,就具有了下面两种含义

  1. 保证了这个字段的可见性,也就是说所有线程都能够"看到"这个变量的值,如果某个 CPU 修改了这个变量的值之后,其他 CPU 也能够获得通知。
  2. 能够禁止指令的重排序

下面我们来看一段代码,这也是我们编写并发代码中经常会使用到的

boolean isStop = false;
while(!isStop){...
}isStop = true;

在这段代码中,如果线程一正在执行 while 循环,而线程二把 isStop 改为 true 之后,转而去做其他事情,因为线程一并不知道线程二把 isStop 改为 true ,所以线程一就会一直运行下去。

如果 isStop 用 volatile 修饰之后,那么事情就会变的不一样了。

使用 volatile 修饰了 isStop 之后,在线程二把 isStop 改为 true 之后,会强制将其写入内存,并且会把线程一中 isStop 的值置为无效(这个值实际上是在缓存在 CPU 中的缓存行里),当线程一继续执行代码的时候,会从内存中重新读取 isStop 的值,此时 isStop 的值就是正确的内存地址的值。

volatile 有下面两条实现原则,其实这两条原则我们在上面介绍的时候已经提过了,一种是总线锁的方式,我们后面说总线锁的方式开销比较大,所以后面设计人员做了优化,采用了锁缓存的方式。另外一种是 MESI 协议的方式。

  • 在 IA-32 架构软件开发者的手册中,有一种 Lock 前缀指令,这种指令能够声言 LOCK# 信号,在最近的处理器中,LOCK# 信号用于锁缓存,等到指令执行完毕后,会把缓存的内容写回内存,这种操作一般又被称为缓存锁定
  • 当缓存写回内存后,IA-32 和 IA-64 处理器会使用 MESI 协议控制内部缓存和其他处理器一致。IA-32 和 IA-64 处理器能够嗅探其他处理器访问系统内部缓存,当内存值修改后,处理器会从内存中重新读取内存值进行新的缓存行填充。

由此可见,volatile 能够保证线程的可见性。

那么 volatile 能够保证原子性吗?

原子性

我们还是以 i = i + 1 这个例子来说明一下,i = i + 1 分为三个操作

  • 读取 i 的值
  • 自增 i 的值
  • 把 i 的值写会内存

我们知道,volatile 能够保证修改 i 的值对其他线程可见,所以我们此时假设线程一执行 i 的读取操作,此时发生了线程切换,线程二读取到最新 i 的值是 0 ,然后线程再次发生切换,线程一把 i 的值改为 1,线程再次切换,因为此时 i 的值还没有应用到内存,所以线程 i 同样把 i 的值改为 1 后,线程再次发生切换,线程一把 i 的值写入内存后,再次发生切换,线程二再次把 i 的值写会内存,所以此时,虽然内存值改了两次,但是最后的结果却不是 2。

那么 volatile 不能保证原子性,那么该如何保证原子性呢?

在 JDK 5 的 java.util.concurrent.atomic 包下提供了一些原子操作类,例如 AtomicInteger、AtomicLong、AtomicBoolean,这些操作是原子性操作。它们是利用 CAS 来实现原子性操作的(Compare And Swap),CAS实际上是利用处理器提供的 CMPXCHG 指令实现的,而处理器执行 CMPXCHG 指令是一个原子性操作。

详情可以参考笔者的这篇文章 一场 Atomic XXX 的魔幻之旅。

那么 volatile 能不能保证有序性呢?

这里就需要和你聊一聊 volatile 对有序性的影响了

###有序性

上面提到过,重排序分为编译器重排序、处理器重排序和内存重排序。我们说的 volatile 会禁用指令重排序,实际上 volatile 禁用的是编译器重排序和处理器重排序。

下面是 volatile 禁用重排序的规则

从这个表中可以看出来,读写操作有四种,即不加任何修饰的普通读写和使用 volatile 修饰的读写。

从这个表中,我们可以得出下面这些结论

  • 只要第二个操作(这个操作就指的是代码执行指令)是 volatile 修饰的写操作,那么无论第一个操作是什么,都不能被重排序。
  • 当第一个操作是 volatile 读时,不管第二个操作是什么,都不能进行重排序。
  • 当第一个操作是 volatile 写之后,第二个操作是 volatile 读/写都不能重排序。

为了实现这种有序性,编译器会在生成字节码中,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

这里我们先来了解一下内存屏障的概念。

内存屏障也叫做栅栏,它是一种底层原语。它使得 CPU 或编译器在对内存进行操作的时候, 要严格按照一定的顺序来执行, 也就是说在 memory barrier 之前的指令和 memory barrier 之后的指令不会由于系统优化等原因而导致乱序。

内存屏障提供了两个功能。首先,它们通过确保从另一个 CPU 来看屏障的两边的所有指令都是正确的程序顺序;其次它们可以实现内存数据可见性,确保内存数据会同步到 CPU 缓存子系统。

不同计算机体系结构下面的内存屏障也不一样,通常需要认真研读硬件手册来确定,所以我们的主要研究对象是基于 x86 的内存屏障,通常情况下,硬件为我们提供了四种类型的内存屏障。

  • LoadLoad 屏障

它的执行顺序是 Load1 ; LoadLoad ;Load2 ,其中的 Load1 和 Load2 都是加载指令。LoadLoad 指令能够确保执行顺序是在 Load1 之后,Load2 之前,LoadLoad 指令是一个比较有效的防止看到旧数据的指令。

  • StoreStore 屏障

它的执行顺序是 Store1 ;StoreStore ;Store2 ,和上面的 LoadLoad 屏障的执行顺序相似,它也能够确保执行顺序是在 Store1 之后,Store2 之前。

  • LoadStore 屏障

它的执行顺序是 Load1 ; StoreLoad ; Store2 ,保证 Load1 的数据被加载在与这数据相关的 Store2 和之后的 store 指令之前。

  • StoreLoad 屏障

它的执行顺序是 Store1 ; StoreLoad ; Load2 ,保证 Store1 的数据被其他 CPU 看到,在数据被 Load2 和之后的 load 指令加载之前。也就是说,它有效的防止所有 barrier 之前的 stores 与所有 barrier 之后的 load 乱序。

JMM 采取了保守策略来实现内存屏障,JMM 使用的内存屏障如下

下面是一个使用内存屏障的示例

class MemoryBarrierTest {int a, b;volatile int v, u;void f() {int i, j;i = a;j = b;i = v;j = u;a = i;b = j;v = i;u = j;i = u;j = b;a = i;}
}

这段代码虽然比较简单,但是使用了不少变量,看起来有些乱,我们反编译一下来分析一下内存屏障对这段代码的影响。

从反编译的代码我们是看不到内存屏障的,因为内存屏障是一种硬件层面的指令,单凭字节码是肯定无法看到的。虽然无法看到内存屏障的硬件指令,但是 JSR-133 为我们说明了哪些字节码会出现内存屏障。

  • 普通的读类似 getfield 、getstatic 、 不加 volatile 修饰的数组 load 。
  • 普通的写类似 putfield 、 putstatic 、 不加 volatile 修饰的数组 store 。
  • volatile 读是可以被多个线程访问修饰的 getfield、 getstatic 字段。
  • volatile 写是可以被当个线程访问修饰的 putfield、 putstatic 字段。

这也就是说,只要是普通的读写加上了 volatile 关键字之后,就是 volatile 读写(呃呃呃,我好像说了一句废话),并没有其他特殊的 volatile 独有的指令。

根据这段描述,我们来继续分析一下上面的字节码。

a、b 是全局变量,也就是实例变量,不加 volatile 修饰,u、v 是 volatile 修饰的全局变量;i、j 是局部变量。

首先 i = a、j = b 只是把全局变量的值赋给了局部变量,由于是获取对象引用的操作,所以是字节码指令是 getfield 。

从官方手册就可以知晓原因了。

地址在 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html

由内存屏障的表格可知,第一个操作是普通读写的情况下,只有第二个操作是 volatile 写才会设置内存屏障。

继续向下分析,遇到了 i = v,这个是把 volatile 变量赋值给局部变量,是一种 volatile 读,同样的 j = u 也是一种 volatile 读,所以这两个操作之间会设置 LoadLoad 屏障。

下面遇到了 a = i ,这是为全局变量赋值操作,所以其对应的字节码是 putfield

地址在 https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html

所以在 j = u 和 a = i 之间会增加 LoadStore 屏障。然后 a = i 和 b = j 是两个普通写,所以这两个操作之间不需要有内存屏障。

继续往下面分析,第一个操作是 b = j ,第二个操作是 v = i 也就是 volatile 写,所以需要有 StoreStore 屏障;同样的,v = i 和 u = j 之间也需要有 StoreStore 屏障。

第一个操作是 u = j 和 第二个操作 i = u volatile 读之间需要 StoreLoad 屏障。

最后一点需要注意下,因为最后两个操作是普通读和普通写,所以最后需要插入两个内存屏障,防止 volatile 读和普通读/写重排序。

《Java 并发编程艺术》里面也提到了这个关键点。

从上面的分析可知,volatile 实现有序性是通过内存屏障来实现的。

关键概念

在 volatile 实现可见性和有序性的过程中,有一些关键概念,cxuan 这里重新给读者朋友们唠叨下。

  • 缓冲行:英文概念是 cache line,它是缓存中可以分配的最小存储单位。因为数据在内存中不是以独立的项进行存储的,而是以临近 64 字节的方式进行存储。

  • 缓存行填充:cache line fill,当 CPU 把内存的数据载入缓存时,会把临近的共 64 字节的数据一同放入同一个 Cache line,因为局部性原理:临近的数据在将来被访问的可能性大。

  • 缓存命中:cache hit,当 CPU 从内存地址中提取数据进行缓存行填充时,发现提取的位置仍然是上次访问的位置,此时 CPU 会选择从缓存中读取操作数,而不是从内存中取。

  • 写命中:write hit ,当处理器打算将操作数写回到内存时,首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器会将这个操作数写回到缓存,而不是写回到内存,这种方式被称为写命中。

  • 内存屏障:memory barriers,是一组硬件指令,是 volatile 实现有序性的基础。

  • 原子操作:atomic operations,是一组不可中断的一个或者一组操作。

如何正确的使用 volatile 变量

上面我们聊了这么多 volatile 的原理,下面我们就来谈一谈 volatile 的使用问题。

volatile 通常用来和 synchronized 锁进行比较,虽然它和锁都具有可见性,但是 volatile 不具有原子性,它不是真正意义上具有线程安全性的一种工具。

从程序代码简易性和可伸缩性角度来看,你可能更倾向于使用 volatile 而不是锁,因为 volatile 写起来更方便,并且 volatile 不会像锁那样造成线程阻塞,而且如果程序中的读操作的使用远远大于写操作的话,volatile 相对于锁还更加具有性能优势。

很多并发专家都推荐远离 volatile 变量,因为它们相对于锁更加容易出错,但是如果你谨慎的遵从一些模式,就能够安全的使用 volatile 变量,这里有一个 volatile 使用原则

只有在状态真正独立于程序内其他内容时才能使用 volatile

下面我们通过几段代码来感受一下这条规则的力量。

状态标志

一种最简单使用 volatile 的方式就是将 volatile 作为状态标志来使用。

volatile boolean shutdownRequested;public void shutdown() { shutdownRequested = true; }public void doWork() { while (!shutdownRequested) { // do stuff}
}

为了能够正确的调用 shutdown() 方法,你需要确保 shutdownRequested 的可见性。这种状态标志的一种特性就是通常只有一种状态转换:shutdownRequested 的标志从 false 转为 true,然后程序停止。这种模式可以相互来回转换。

双重检查锁

使用 volatile 和 synchronized 可以满足双重检查锁的单例模式。

class Singleton{private volatile static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if(instance == null) {synchronized (Singleton.class) {if(instance == null)instance = new Singleton();}}return instance;}
}

这里说下为什么要用两次检查,假如有两个线程,线程一在进入到 synchronized 同步代码块之后,在还没有生成 Singleton 对象前发生线程切换,此时线程二判断 instance == null 为 true,会发生线程切换,切换到线程一,然后退出同步代码块,线程切换,线程二进入同步代码块后,会再判断一下 instance 的值,这就是双重检查锁的必要所在。

读-写锁

这也是 volatile 和 synchronized 一起使用的示例,用于实现开销比较低的读-写锁。

public class ReadWriteLockTest {private volatile int value;public int getValue() { return value; }public synchronized int increment() {return value++;}
}

如果只使用 volatile 是不能安全实现计数器的,但是你能够在读操作中使用 volatile 保证可见性。如果你想要实现一种读写锁的话,必须进行外部加锁。

我自己肝了六本 PDF,全网传播超过10w+ ,你需要关注一下我的 CSDN 账号,私信回复 cxuan ,领取全部 PDF,这些 PDF 如下

下载链接 密码:7im6
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

李开复们混战AI,谁最有戏?

作者 | 王敏 编辑 | 金玙璠 2023年以来,AI“狂飙”。 ChatGPT一经问世,就掀起了新一轮AI革命。过去的几个月里,AI领域重磅“炸弹”一个接着一个。从业者们常常一觉醒来,就会因为硅谷发布新的AI产品而不得不快速更新认知。 伴随着热…

独家 前美团联合创始人王慧文“正在收购”国产AI框架OneFlow,光年之外欲添新大将

以ChatGPT为代表的AI大模型是2023年的科技C位。 2023年3月27日,ChatGPT引发的“抓马连续剧”,又有新剧更新。 前情提要: 前美团联合创始人、高级副总裁王慧文发文宣布进入AI领域,称将打造中国的OpenAI。 新闻标题一&#xff1…

笑死!这个插件太绝了;AI开发者如何稳赚这一波;MidJourney完完完全手册;零经验开发儿时3D游戏 | ShowMeAI日报

👀日报&周刊合集 | 🎡生产力工具与行业应用大全 | 🧡 点赞关注评论拜托啦! 🤖 『文心一言员工跳槽工资翻倍』猎头:百万年薪很正常 最近一段时间,百度文心大模型团队内的研发人员受到其他公司…

AI大爆发却找不到工作?因为学校里教的和它关系不大

Datawhale干货 最新:就业趋势,来源:新智元 【导读】猎聘大数据研究院重磅发布《AIGC就业趋势大数据报告2023》,招聘平均年薪已达40万,博士需求量同比增长超100%。 不用赘述,大家都知道,最近半年…

平替!0门槛克隆ChatGPT!30分钟训完,60亿参数性能堪比GPT-3.5

新智元报道 编辑:编辑部 【新智元导读】破解「CloseAI」,ChatGPT克隆羊问世!0门槛实现「自研」,从此大语言模型不再只是少数大公司的「金手指」。 此前,OpenAI不Open的事件,已经引发了坊间的诸多争议。 光…

当AI成为专家,要警惕话语权威的力量

来源:混沌巡洋舰 今年2月份,有一则颇具科幻色彩的新闻,美国最大的科幻杂志,由于收到太多由AI生成的稿件,导致编辑无法处理而暂停投稿通道。对此,必然的选项是由AI进行审稿。这一步一旦迈开,就意…

FastJSON巨坑01:Exception in thread OkHttp Dispatcher java.lang.IllegalStateException: closed

反复查代码,不知到底什么问题。 参考了安卓版的文章:https://blog.csdn.net/ucxiii/article/details/52447945 想不到,这个错误是由于response.body().string()调用了多次导致的,string()仅可调用一次!

消息推送unipush的简单使用

配置 manifest.json中选中push 配置push 点击uniPush下的配置 输入个人信息并点击授权并开通 填写信息后点击开通,选择配置管理应用配置,记录应用配置信息 前端 在app.vue中 onLaunch: function() {const clientInfo plus.push.getClientInfo()c…

win11 无法登录微软账户 终极解决方案

背景:win11突然无法登录微软账户,office无法激活,Edge里的微软账户也无法登录,反馈中心也无法打开等,有网络,浏览器可以访问微软并进行登录。 试过网上的网络配置(SSL及TLS协议勾选&#xff09…

Win11微软账户无法正常登录怎么回事?

Win11微软账户无法正常登录怎么回事?我们在使用电脑的时候,有时候需要登录到微软账户中来进行相关的操作。比如使用微软应用商店的时候,就需要去登录自己的平台账号。那么我们如何去进行账户的登录呢?接下来我们一起来看看详细的解…

微软账户登不上解决方案

步骤1: 步骤2: 步骤3: 步骤4: 步骤5: 步骤6: 保存就可以了~~使用愉快吖

Win11电脑上登录的微软账号怎么退出登录?

Win11电脑上登录的微软账号怎么退出登录?在电脑上登录微软账号的时候,很多用户都会去选择记住账号登录。那么记住登录状态之后,我们怎么去进行退出账号的登录状态呢?接下来我们一起来看看详细的微软账号退出方法分享吧。 操作方法…

快速登录微软账户(截图)

上一篇文章忘记截图,我还是补个截图吧: 再次强调,用电话号码,不用邮箱。

Win11微软账号登录不上?Win11登录Microsoft账户出错的解决方法

Win11微软账号登录不上?近期有部分Win11用户反映在登录微软账号会出现一直转圈,无法登录的情况,这样导致部分功能都不能正常使用了,为此十分令人头疼。那么对于这一情况,有没有什么方法可以有效的解决呢?下…

Win11系统在administered账户下无法登录微软账户显示:哎呀,出错了。解决方法。

昨天新安装了win 11系统,在安装好软件后发现无法登录微软账户,折腾一晚上网上大部分解决方式是新创建账户,然后在新创建账户中登录微软账户。再把administered账户使用管理员运行命令符:net user administrator /active:no&#x…

怎么更换电脑中已经登录的微软账号?

怎么更换电脑中已经登录的微软账号?有些用户在使用电脑的时候就想要把自己的电脑更换给其他用户使用,但是又想要保存所有的数据,今天小编就带着大家一起看看怎么操作吧! 操作方法: 可以点击图上的 账户信息&#xff0c…

CSS/CSS3常用Style

1、实现以下功能: 代码附上: width: 5px; height: 35px; border: 84px solid; border-color: #ffa002 transparent transparent transparent;2、如何实现“颜色渐变”这种功能效果图,如下图: 代码附上: background…

相约情人节| 让ChatGPT带你体验“人工智能”的浪漫!

“情人节不会写情书?” “不会写情人节贺卡?” 让ChatGPT来为你支招! 赶紧「扫码预约」直播课 微软工程师带你体验AI的浪漫 2月14日-3月7日每周二晚8点 4场线上直播课「扫码报名」啦! 扫描下方二维码,立即报名课程 扫描…

2023 谷歌I/O发布会新AI,PALM 2模型要反超GPT-4,一雪前耻!

文章目录 1 前言2 Google I/O 发布者大会3 PaLM 2模型3 Bard项目4 其他AI工具4.1 AI 图片编辑 Magic Editor4.2 Duet AI 办公4.3 Universal Translator 翻译工具4.4 Google 沉浸式导航4.5 Google 搜索引擎 5 讨论 1 前言 每年必看两大会,苹果发布会和谷歌发布会&am…

A/B实验避坑指南:为什么不建议开AABB实验

本文将针对日常开设 A/B 实验过程中一个不太合理的使用方法——AABB 实验进行详细的解释,告诉大家为什么不建议开 AABB 实验。 在开始之前,先来回顾一下“什么是 A/B 实验”,A/B 实验是针对想调研的问题,提供两种不同的备选解决方…