java原子类-Atomic

什么是原子类?

java 1.5引进原子类,具体在java.util.concurrent.atomic包下,atomic包里面一共提供了13个类,分为4种类型,分别是:

  • 原子更新基本类型
  • 原子更新数组
  • 原子更新引用
  • 原子更新属性。

原子类也是java实现同步的一套解决方案。

什么时候使用原子类?

当我们只是需要一个简单的、高效、线程安全的递增或者递减方案:

  1. 简单:操作简单,底层实现j简单
  2. 高效:占用资源少,操作速度快
  3. 安全:在高并发和多线程环境下要保证数据的正确性

当然这种情况可以使用synchronized关键字和lock可以实现,但是代码量会上去,而且性能也会降低一点,所以用原子类就比较方便一点

原子变量类简单介绍

原子变量类在java.util.concurrent.atomic包下,总体来看有这么多个:

基本类型:

  • AtomicBoolean:布尔型
  • AtomicInteger:整型
  • AtomicLong:长整型

数组:

  • AtomicIntegerArray:数组里的整型
  • AtomicLongArray:数组里的长整型
  • AtomicReferenceArray:数组里的引用类型

引用类型:

  • AtomicReference:引用类型
  • AtomicStampedReference:带有版本号的引用类型
  • AtomicMarkableReference:带有标记位的引用类型

对象的属性:

  • AtomicIntegerFieldUpdater:原子更新对象中int类型字段的值,基于反射的使用程序,可以对指定类的指定的volatile int字段进行原子更新
  • AtomicLongFieldUpdater:原子更新对象中Long类型字段的值,基于反射的使用程序,可以对指定类的指定的volatile long字段进行原子更新
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的值,基于反射的使用程序,可以对指定类的指定的volatile volatile引用进行原子更新

JDK8新增

Accumulator累加器

  • DoubleAccumulator、
  • LongAccumulator、

Adder累加器

  • DoubleAdder
  • LongAdder

是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效。

Atomic包里的类基本都是使用Unsafe实现的包装类。 从原理上来说就是:Atomic包的类的实现大多数都是调用的unsafe方法,而unsafe底层实际上是调用C代码,C代码调用汇编,最后生成出一条CPU指令cmpxchg,完成操作,这也是为什么CAS是原子性操作,因为是一条CPU指令,不会被打断。

原子变量类的使用

基本类型原子类(Atomic*)

这里以原子更新基本类型中的AtomicInteger类为例,介绍通用的API接口和使用方法。

常用的API:

public final int set() //设一个值
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果当前值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue) //最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
    AtomicInteger a = new AtomicInteger(0);for (int i = 1; i < 5; i++) {a.getAndIncrement(); // a 自增,相当于 a ++}//获取当前a的值System.out.println("AtomicInteger a从0自增4次结果为:"+ a.get());
​System.out.println("AtomicInteger 当前a为:"+ a.getAndDecrement() + ",并自减一次"); //a --
​//获取当前a的值,并更新a为8System.out.println("AtomicInteger a当前值为:"+a.getAndSet(8)+",并更新a为8");
​//获取当前a的值,并将a加6System.out.println("AtomicInteger a当前值为:"+a.getAndAdd(6)+",并将a加6");
​a.compareAndSet(12,9); //如果a=12,就把a更新为9,否则不进行操作System.out.println("AtomicInteger a当前值为:"+a.get());
​a.compareAndSet(14,9); //如果a=14,就把a更新为9,否则不进行操作System.out.println("AtomicInteger a当前值为:"+a.get());
​

AtomicInteger a从0自增4次结果为:4 AtomicInteger 当前a为:4,并自减一次 AtomicInteger a当前值为:3,并更新a为8 AtomicInteger a当前值为:8,并将a加6 AtomicInteger a当前值为:14 AtomicInteger a当前值为:9

数组类型原子类(Atomic*Array)

这里以AtomicIntegerArray 为例,介绍通用的API接口和使用方法。

常用API:

public final int get(int i) //获取 index=i 位置元素的值
public final int set(int i, int newValue) //为 index=i 位置元素设新值
public final int getAndSet(int i, int newValue) //返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果index=i 位置的值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue) //最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

基本使用:

         int[] a = {1,1,1,1};AtomicIntegerArray arr = new AtomicIntegerArray(a);System.out.println("arr数组初始值为:" +arr.toString());
​for (int i = 0; i < 4; i++) {arr.getAndIncrement(i); //index = i位置上的值arr[i]自增,相当于 a[i] ++}System.out.println("arr数组每个元素都自增1后为:" +arr.toString());//获取当前arr[1]的值System.out.println("arr[1]的值为:"+ arr.get(1));
​System.out.println("arr[2]当前值为:"+ arr.getAndDecrement(2) + ",并让arr[2]自减一次"); //a[2]--
​//获取当前a[2]的值,并更新a为8System.out.println("arr[2]当前值为:"+arr.getAndSet(2,8)+",并更新a[2]为8");
​//获取当前a的值,并将a加6System.out.println("arr[2]当前值为:"+arr.getAndAdd(2,6)+",并将a[2]加6");
​arr.compareAndSet(2,12,9); //如果a[2]=12,就把a[2]更新为9,否则不进行操作System.out.println("arr[2]当前值为:"+arr.get(2));
​arr.compareAndSet(2,14,9); //如果a[2]=14,就把a[2]更新为9,否则不进行操作System.out.println("arr[2]当前值为:"+arr.get(2));

arr数组初始值为:[1, 1, 1, 1] arr数组每个元素都自增1后为:[2, 2, 2, 2] arr[1]的值为:2 arr[2]当前值为:2,并让arr[2]自减一次 arr[2]当前值为:1,并更新a[2]为8 arr[2]当前值为:8,并将a[2]加6 arr[2]当前值为:14 arr[2]当前值为:9

引用类型原子类(Atomic*Reference)

User类:

package 原子类;
​
public class User {private String name;private int age;
​public User(String name, int age) {this.name = name;this.age = age;}
​public String getName() {return this.name;}
​public void setName(final String name) {this.name = name;}
​public int getAge() {return this.age;}
​public void setAge(final int age) {this.age = age;}
​@Overridepublic String toString() {return "User{" +"name='" + name + ''' +", age=" + age +'}';}
​
}

 package 原子类;


import java.util.concurrent.atomic.AtomicReference;
​
public class Demo3 {
​public static void main(String[] args) {AtomicReference<User> atu=new AtomicReference<>();
​User user1=new User("张三",10);User user2=new User("李四",16);User user3=new User("王五",19);atu.set(user1);//常用的API和前面的是差不多的System.out.println(atu.getAndSet(user2));System.out.println(atu.get());
​System.out.println(atu.compareAndSet(user2,user3));System.out.println(atu.get());}
//User{name='张三', age=10}
//User{name='李四', age=16}
//true
//User{name='王五', age=19}
​
​
}
什么是ABA问题?

ABA问题指在CAS操作过程中,当一个值从A变成B,又更新回A,普通CAS机制会误判通过检测。这时候就可能导致程序出现意外的结果。

在高并发场景下,使用CAS操作可能存在ABA问题,也就是在一个值被修改之前,先被其他线程修改为另外的值,然后再被修改回原值,此时CAS操作会认为这个值没有被修改过,导致数据不一致。

如何解决?

为了解决ABA问题,Java中提供了AtomicStampedReference类(原子标记参考),该类通过使用版本号的方式来解决ABA问题。每个共享变量都会关联一个版本号,CAS操作时需要同时检查值和版本号是否匹配。因此,如果共享变量的值被改变了,版本号也会发生变化,即使共享变量被改回原来的值,版本号也不同,因此CAS操作会失败。

AtomicStampedReference<V>:解决ABA问题

带版本号的引用类型原子类,可以解决ABA问题

AtomicStampedReference在构建的时候需要一个类似于版本号的int类型变量stamped,每一次针对共享数据的变化都会导致该 stamped 的变化(stamped 需要应用程序自身去负责,AtomicStampedReference并不提供,一般使用时间戳作为版本号),因此就可以避免ABA问题的出现,AtomicStampedReference的使用也是极其简单的,创建时我们不仅需要指定初始值,还需要设定stamped的初始值,在AtomicStampedReference内部会将这两个变量封装成Pair对象

package 原子类;
import 原子类.User;
import java.util.concurrent.atomic.AtomicStampedReference;
class ABADemo {public static void main(String[] args) {User zs = new User("张三",18);User ls = new User("李四",25);//创建对象带版本号的引用类型原子类,添加对象和初始化的版本号AtomicStampedReference<User> reference = new AtomicStampedReference<>(zs, 1);
​new Thread(() -> {int stamp = reference.getStamp();User referenceUser = reference.getReference();// 保证线程t2可以拿到版本号try {Thread.sleep(1000L);} catch (InterruptedException e) {e.printStackTrace();}// ASystem.out.println(Thread.currentThread().getName() + "版本号:" + stamp + " 对象" + referenceUser);// Bboolean compareAndSet = reference.compareAndSet(referenceUser, ls, stamp, stamp + 1);System.out.println(Thread.currentThread().getName() + "将数据设置" + (compareAndSet?"成功 ":"失败 ") + reference.getReference());// AcompareAndSet = reference.compareAndSet(reference.getReference(),zs,reference.getStamp(),reference.getStamp() + 1);System.out.println(Thread.currentThread().getName() + "将数据设置" + (compareAndSet?"成功 ":"失败 ") + reference.getReference());
​},"t1").start();
​new Thread(() -> {int stamp = reference.getStamp();User referenceUser = reference.getReference();// 保证线程t1发生完ABA问题try {Thread.sleep(5000L);} catch (InterruptedException e) {e.printStackTrace();}boolean compareAndSet = reference.compareAndSet(referenceUser,ls,stamp,stamp + 1);System.out.println(Thread.currentThread().getName() + "将数据设置" + (compareAndSet?"成功 ":"失败 ") + reference.getReference());
​},"t2").start();}
}

t1版本号:1 对象User{name='张三', age=18} t1将数据设置成功 User{name='李四', age=25} t1将数据设置成功 User{name='张三', age=18} t2将数据设置失败 User{name='张三', age=18}

常用API:

// 构造函数,初始化引用和版本号
public AtomicStampedReference(V initialRef, int initialStamp)// 以原子方式获取当前引用值
public V getReference()// 以原子方式获取当前版本号
public int getStamp()// 以原子方式获取当前引用值和版本号
public V get(int[] stampHolder)// 以原子的方式同时更新引用值和版本号
// 当期望引用值不等于当前引用值时,操作失败,返回false
// 当期望版本号不等于当前版本号时,操作失败,返回false
// 在期望引用值和期望版本号同时等于当前值的前提下
// 当新的引用值和新的版本号同时等于当前值时,不更新,直接返回true
// 当新的引用值和新的版本号不同时等于当前值时,同时设置新的引用值和新的版本号,返回true
public boolean weakCompareAndSet(V  expectedReference,V  newReference,int expectedStamp,int newStamp)// 以原子的方式同时更新引用值和版本号
// 当期望引用值不等于当前引用值时,操作失败,返回false
// 当期望版本号不等于当前版本号时,操作失败,返回false
// 在期望引用值和期望版本号同时等于当前值的前提下
// 当新的引用值和新的版本号同时等于当前值时,不更新,直接返回true
// 当新的引用值和新的版本号不同时等于当前值时,同时设置新的引用值和新的版本号,返回true
public boolean compareAndSet(V   expectedReference,V   newReference,int expectedStamp,int newStamp)// 以原子方式设置引用的当前值为新值newReference
// 同时,以原子方式设置版本号的当前值为新值newStamp
// 新引用值和新版本号只要有一个跟当前值不一样,就进行更新
public void set(V newReference, int newStamp)// 以原子方式设置版本号为新的值
// 前提:引用值保持不变
// 当期望的引用值与当前引用值不相同时,操作失败,返回fasle
// 当期望的引用值与当前引用值相同时,操作成功,返回true
public boolean attemptStamp(V expectedReference, int newStamp)// 使用`sun.misc.Unsafe`类原子地交换两个对象
private boolean casPair(Pair<V> cmp, Pair<V> val)
​
AtomicMarkableReference<V> 状态戳简化

AtomicMarkableReference与AtomicStampedReference的区别是Pair内部类维护的类型不同。

类似于上面的版本号,但是主要是解决一次性问题

解决是否修改过,它的定义就是将状态戳简化boolean也就是true或者falset,类似于一次性筷子

    static AtomicMarkableReference<Integer> markableReference = new AtomicMarkableReference<>(100,false);public static void main(String[] args) {new Thread(()->{boolean marked = markableReference.isMarked();System.out.println(Thread.currentThread().getName()+"\t"+"默认标识"+marked);//暂停1秒钟线程,等待后面的T2线程和我拿到一样的模式flag标识,都是falsetry {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}markableReference.compareAndSet(100, 1000, marked, !marked);},"t1").start();new Thread(()->{boolean marked = markableReference.isMarked();System.out.println(Thread.currentThread().getName()+"\t"+"默认标识"+marked);//这里停2秒,让t1先修改,然后t2试着修改try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}boolean t2Result = markableReference.compareAndSet(100, 1000, marked, !marked);System.out.println(Thread.currentThread().getName()+"\t"+"t2线程result--"+t2Result);System.out.println(Thread.currentThread().getName()+"\t"+markableReference.isMarked());System.out.println(Thread.currentThread().getName()+"\t"+markableReference.getReference());},"t2").start();}
}
​

运行结果:

//t1 默认标识false //t2 默认标识false //t2 t2线程result--false //t2 true //t2 1000

对象的属性修改原子类

使用要求:

  • 更新的对象属性必须使用public volatile修饰符
  • 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
  • 属性的修饰符(public/protected/default/private)要保证当前操作对该属性可以直接进行,比如当我们用private volatile int age 时就会报错,因为private修饰时,外部无法访问也无法修改。
  • 只能是实例变量,不能是类变量,也就是说不能加static关键字。
  • 只能是可修改变量,不能使final变量,因为final的语义就是不可修改。
  • 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段不能修改其包装类型(Integer/Long) 。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。

修改引用类型:

package 原子类;
​
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
​
class yingyongDemo {public static void main(String[] args) {MyVar myVar = new MyVar();for (int i = 1; i <= 5; i++) {new Thread(() -> {myVar.init();}, String.valueOf(i)).start();
​}}
​public static class MyVar {public volatile Boolean isInit = Boolean.FALSE;AtomicReferenceFieldUpdater<MyVar, Boolean> referenceFieldUpdater =AtomicReferenceFieldUpdater.newUpdater(MyVar.class, Boolean.class, "isInit");
​public void init() {if (referenceFieldUpdater.compareAndSet(this, Boolean.FALSE, Boolean.TRUE)) {System.out.println(Thread.currentThread().getName() + "\t" + "-----start init,needs 3 seconds");try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t" + "-----over init");} else {System.out.println(Thread.currentThread().getName() + "\t" + "抱歉,已经有其他线程进行了初始化");}}}
}

1 -----start init,needs 3 seconds 5 抱歉,已经有其他线程进行了初始化 4 抱歉,已经有其他线程进行了初始化 2 抱歉,已经有其他线程进行了初始化 3 抱歉,已经有其他线程进行了初始化 1 -----over init

Adder累加器

LogAdder比AtomicLog的区别

  • java8引入的,相比较是一个比较新的类

  • 高并发下LogAdder比AtomicLog效率高,不过本质是空间换时间

  • 竞争激烈的时候,LongAdder把不同线程对应到不同的Cell上进行修改,降低了冲突的概率,是多段锁的理念,提高了并发性

  • LongAdder适合的场景是统计求和计数的场景,而且LongAdder基本只提供了add方法,而AtomicLong还具有cas方法

    多线程下AtomicLong的性能,有20个线程对同一个AtomicLong累加(由于竞争很激烈,每一次加法,都要flush和refresh,导致很耗费资源)

  • 在内部,这个LongAdder的实现原理和AtomicLong是不同的,刚才的AtomicLong的实现原理是,每一次加法都需要做同步,所以在高并发的时候会导致冲突比较多,也就降低了效率

  • 而此时的LongAdder,每个线程都有一个计数器,仅用来在自己的线程内计算,这样一来就不会和其他线程的计数器干扰

  • 如下图,第一个线程的计数器的数值,也就是ctr’,为1的时候,可能线程2的计数器ctr’’的数值已经是3了,他们之间并不存在竞争关系,所以在加和的过程中,根本不需要同步机制,也不需要刚才的flush和reflush。这里没有一个公共的counter来给所有线程统一计数

  • LongAdder引入了分段累加的概念,内部有一个base变量和一个Cell[]数组共同参与计数:

    base变量:竞争不激烈,直接累加到该变量上

    Cell[]:竞争激烈,各个线程分散累加到自己的槽Cell[i]中,

总的来说: LongAdder的基本思路就是分散热点 ,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。sum源码:

 public long sum() {Cell[] as = cells; Cell a;long sum = base;if (as != null) {for (int i = 0; i < as.length; ++i) {if ((a = as[i]) != null)sum += a.value;}}return sum;}
​

sum()会将所有Cell数组中的value和base累加作为返回值, 核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点 。

一句话总结longAdder原理

LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零的做法,从空间换时间,用一个数组 ,将一个value拆分进这个数组cells。 多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和无竞争值base都加起来作为最终结果。 与AtomicLong对比

名称原理场景缺陷
AtomicLongCAS + 自旋低并发下的全局计算,AlomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题。可允许一些性能损耗,要求高精度时可使用。AtomicLong是多个线程针对单个热点值value进行原子操作高并发后性能急剧下降。(N个线程CAS操作修改线程的值,每次只有一个成功过,其它N-1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,占用大量CPU)
LongAdderCAS+Base+Cell数组分散,通过空间换时间分散了热点数据高并发下的全局计算,当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用。LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作sum求和后还有计算线程修改结果的话,最后结果不够准确

Accumulator累加器

public class LongAccumulatorDemo {public static void main(String[] args) {//需要传入累加的函数LongAccumulator accumulator = new LongAccumulator((x,y)->x+y,0);ExecutorService executor = Executors.newFixedThreadPool(8);IntStream.range(1,10).forEach(i->executor.submit(()->accumulator.accumulate(i)));executor.shutdown();while (!executor.isTerminated()){
​}System.out.println(accumulator.getThenReset()); //45}
}
​

 

使用场景:

① 适用于需要大量计算,并且需要并行计算的场景,如果不需要并行计算,可用for循环解决问题,用了Accumulator累加器可利用多核同时计算,提供效率

② 计算的顺序不能成为瓶颈,线程1可能在线程5之后运行,也可能在之前运行,不影响最终结果

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

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

相关文章

一次js请求一般情况下有哪些地方会有缓存处理

目录 前言 1. 浏览器缓存 1.1 强缓存 用法 代码 理解 1.2 协商缓存 用法 代码 理解 2. 服务端缓存 2.1 反向代理缓存 用法 代码 理解 2.2 应用层缓存 用法 代码 理解 3. CDN缓存 3.1 用法 3.2 理解 4. DNS缓存 4.1 用法 4.2 理解 5. AJAX请求缓存 5.…

【网络安全 --- 任意文件下载漏洞(1)】任意文件下载漏洞

一&#xff0c;环境&#xff0c;工具准备 1-1 VMVare 16 虚拟机及下载安装&#xff08;资源&#xff09; 请参考以下博客安装&#xff08;特详细&#xff09;&#xff1a;【网络安全 --- 工具安装】VMware 16.0 详细安装过程&#xff08;提供资源&#xff09;-CSDN博客【网络安…

Linux ln命令:建立链接文件

如果要想说清楚 ln 命令&#xff0c;则必须先解释下 ext 文件系统&#xff08;Linux 文件系统&#xff09;是如何工作的。我们在前面讲解了分区的格式化就是写入文件系统&#xff0c;而我们的 Linux 目前使用的是 ext4 文件系统。如果用一张示意图来描述 ext4 文件系统。 ext4 …

无需编程,小白也能建立个人网站

想要搭建一个属于自己的网站&#xff0c;但又不懂编程&#xff1f;别担心&#xff0c;现在有一个简单的方法可以帮助你轻松实现这个愿望。只需要几个简单的步骤&#xff0c;就可以让小白也能搭建出一个漂亮的网站。 首先&#xff0c;登录乔拓云账号&#xff0c;点击网站搭建进入…

2-多媒体数据压缩国际标准

文章目录 多媒体数据压缩编码的重要性和分类为什么要压缩?计算: 未压缩音频的数据率简答: 环绕声系统-作业题9(简述7.4.3全景声)计算: 未压缩图像的数据量-作业题10(估计尺寸及容量)计算: 未压缩视频的数据率 为什么能压缩?数据压缩编码的两大类无损压缩算法: LZ77-作业题6-(…

大数据Doris(十三):创建用户和创建数据库并赋予权限

文章目录 创建用户和创建数据库并赋予权限 一、创建用户

css-渐变色矩形

效果图&#xff1a; 代码&#xff1a; html: <!DOCTYPE html> <html><head><meta charset"utf-8"><meta name"viewport" content"initial-scale1.0, user-scalableno" /><title></title><link …

【多线程面试题 六】、 如何实现线程同步?

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a; 如何实现线程同步&…

吃豆人C语言开发—Day2 需求分析 流程图 原型图

目录 需求分析 流程图 原型图 主菜单&#xff1a; 设置界面&#xff1a; 地图选择&#xff1a; 游戏界面&#xff1a; 收集完成提示&#xff1a; 游戏胜利界面&#xff1a; 游戏失败界面 死亡提示&#xff1a; 这个项目是我和朋友们一起开发的&#xff0c;在此声明一下…

归并排序(java)

大家好我是苏麟 , 今天说说归并排序 . 归并排序 递归 正式学习归并排序之前&#xff0c;我们得先学习一下递归算法。 定义&#xff1a; 定义方法时&#xff0c;在方法内部调用方法本身&#xff0c;称之为递归. public void show(){System.out.println("aaaa")…

JavaScript_Pig Game切换当前玩家

const current0El document.getElementById(current--0); const current1El document.getElementById(current--1); if (dice ! 1) {currentScore dice;current0El.textContent currentScore;} else {} });这是我们上个文章写的代码&#xff0c;这个代码明显是有问题的&…

Spring Boot进阶(93):体验式教程:手把手教你整合Spring Boot和Zipkin

&#x1f4e3;前言 分布式系统开发中&#xff0c;服务治理是一个比较重要的问题。为了更好地实现服务治理&#xff0c;需要解决服务跟踪问题&#xff0c;即如何对分布式系统中的服务进行监控和追踪。本文将介绍如何使用Zipkin进行服务跟踪&#xff0c;并结合Spring Boot进行整合…

STM32F4X SDIO(一) SD卡介绍

STM32F4X SDIO&#xff08;一&#xff09; SD卡介绍 SD卡分类外观分类容量分类传输速度分类 在之前的章节中&#xff0c;讲过有关嵌入式的存储设备&#xff0c;有用I2C驱动的EEPROM、SPI驱动的FLASH和MCU内部的FLASH&#xff0c;这类存储设备的优点是操作简单&#xff0c;但是缺…

指针仪表读数YOLOV8NANO

指针仪表读数YOLOV8 NANO 采用YOLOV8 NANO训练&#xff0c;标记&#xff0c;然后判断角度&#xff0c;得出角度&#xff0c;可以通过角度&#xff0c;换算成数据

A星算法(A* A Star algorithm)原理以及代码实例,超详细,超简单,大白话谁都能看懂

本文以这篇博主的文章为基础【精选】A*算法&#xff08;超级详细讲解&#xff0c;附有举例的详细手写步骤&#xff09;-CSDN博客 这篇文章的博主做了一个UI界面&#xff0c;但我感觉&#xff0c;这样对新手关注算法和代码本身反而不利&#xff0c;会被界面的代码所干扰。所以笔…

C++多态(超级详细版)

目录 一、什么是多态 二、多态的定义及实现 1.多态构成条件 2.虚函数的重写和协变 虚函数重写的两个例外&#xff1a; 2.1协变 2.2析构函数的重写 &#xff08;析构函数名统一处理成destructor&#xff09; 3.重载、覆盖(重写)、隐藏(重定义)的对比 4.final 和 overr…

大模型在数据分析场景下的能力评测

“你们能对接国产大模型吗&#xff1f;” “开源的 LLaMA 能用吗&#xff0c;中文支持怎么样&#xff1f;” “私有化部署和在线服务哪个更合适&#xff1f;” 自 7 月 14 日发布 AI 数智助理 Kyligence Copilot 后&#xff0c;我们收到了很多类似上面的咨询&#xff0c;尤其…

编程实例:洗车店会员管理系统软件一卡多项目管理编程

编程实例&#xff1a;洗车店会员管理系统软件一卡多项目管理编程 编程系统化课程总目录及明细&#xff0c;点击进入了解详情。 https://blog.csdn.net/qq_29129627/article/details/134073098?spm1001.2014.3001.5502 1、会员可以直接用手机号&#xff0c;并可以绑定车牌号 2…

软考系统架构之案例篇(软件工程相关概念)

案例篇-软件工程相关概念 1. 流程图和数据流图之间的区别与联系2. 状态图和活动图的含义及其区别3. 活动图和流程图的区别4. 数据流图中所包含的基本元素及其作用5. 数据流图的平衡原则:6. 用例之间的关系7. 类之间的关系以及基本含义8. 对象模型、动态模型和功能模型的含义以及…

虚拟化 vs. 裸金属:K8s 部署环境架构与特性对比

伴随着 IT 云化转型的逐步推进&#xff0c;越来越多的用户加入应用容器化改造的行列&#xff0c;并使用 Kubernetes&#xff08;K8s&#xff09;进行容器部署管理。然而&#xff0c;令不少用户感到困惑的是&#xff0c;由于大部分应用此前都部署在虚拟化或超融合环境&#xff0…