4.原子操作类:AtomicLong、LongAdderLong、Accumulator

news2024/9/16 15:28:08

JUC包中有AtomicInteger、AtomicLong和AtomicBoolean等原子性操作类,它们原理类似,下面以AtomicLong为例进行讲解。

AtomicLong

底层的操作自增自减都用Unsafe类中的getAndAddLong方法(获取本类内存偏移值)实现的,getAndAddLong底层用Unsafe类中的CAS方法,大量线程竞争只有一个线程成功,会导致大量的自旋尝试

public class AtomicLong extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 1927816293512124184L;
    // 获取Unsafe实例:AtomicLong类是通过BootStarp类加载器加载的所以可以拿到Unsafe类的实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // 存放变量value的偏移量
    private static final long valueOffset;
	// 判断JVM是否支持Long类型的无锁CAS
    static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();
    private static native boolean VMSupportsCS8();
    static {
        try {
            // 获取value在AtomicLong类中的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    // 实际变量值 volatile是为了多线程下保证内存的可见性 
    private volatile long value;
    public AtomicLong(long initialValue) {
        value = initialValue;
    }
    ....
}

递增和递减操作代码

public final long getAndIncrement() {
    return unsafe.getAndAddLong(this, valueOffset, 1L);
}
public final long getAndDecrement() {
    return unsafe.getAndAddLong(this, valueOffset, -1L);
}
public final long incrementAndGet() {
    return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
public final long decrementAndGet() {
    return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
}
// Unsafe类中的getAndAddLong方法  
// 参数1: AtomicLong实例的引用
// 参数2: value变量在AtomicLong中的偏移值
// 参数3: 要设置的第二个变量的值
public final long getAndAddLong(Object var1, long var2, long var4) {
    long var6;
    //CAS操作设置var1对象偏移为var2处的值增加var4
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
    return var6;
}

上述代码中,valueOffset为AtomicLong在static语句块中进行初始化时通过Unsafe类获得的本类中value属性的内存偏移值

可以看到,上述四个方法都是基于Unsafe类中的getAndAddLong方法(原子性操作)实现的。

compareAndSet方法

public final boolean compareAndSet(long expect, long update) {
    return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}

内部还是调用了Unsafe类中的CAS方法。如果原子变量中的value值等于expect,则使用update值更新该值并返回true,否则false。

AtomicLong使用示例

public class AtomicLongDemo {
    private static AtomicLong al = new AtomicLong(0);
    public static long addNext() {
        return al.getAndIncrement();
    }
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread() {
                @Override
                public void run() {
                    AtomicLongDemo.addNext();
                }
            }.start();
        }

        // 等待线程运行完
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("final result is " + AtomicLongDemo.addNext());
    }
}

AtomicLong使用CAS非阻塞算法,性能比使用synchronized等的阻塞算法实现同步好很多。但在高并发下,大量线程会同时去竞争更新同一个原子变量,由于同时只有一个线程的CAS会成功,会造成大量的自旋尝试,十分浪费CPU资源。因此,JDK8中新增了原子操作类LongAdder。

LongAdder

JDK8中新增的原子操作类LongAdder。

问题:

  1. LongAdder结构是怎样的

    将一个原子性变量分解成多个原子性变量。内部维护多个Cell(初始值为0的long类型变量),如果在一个Cell原子变量失败了会尝试在其他Cell变量上进行CAS尝试。

    在获取LongAdder当前值时,是把所有的Cell变量值的value值累加再加上base返回的。

    延迟初始化原子更新数组(Cell数组默认为null),Cells占用内存比较大,在需要时创建,惰性加载,Cell数组null并且并发线程较少时,所有的累加都是对base变量进行的。

  2. 当前线程应该访问Cell数组里面的哪一个Cell元素

    getProbe() & m

    m是当前cells数组元素个数-1

    getProbe()用于获取当前线程中变量threadLocalRandomProbe的值

  3. 如何初始化Cell数组

    cellsBusy是一个标识,0:当前cells数组没有在被初始化或扩容也没有新建Cell元素;1:cells数组在被初始化或扩容或在新建Cell元素。通过CAS操作进行0、1状态切换使用casCellsBusy函数。

    初始化cells元素个数为2,h & 1 计算当前线程应该访问cells数组的哪个位置,即当前线程的threadLocalRandomProbe变量值 & (cells数组元素个数-1)

    标识cells数组已被初始化。

    最后对cellsBusy重置标记 线程安全因为cellsBusy是volatile保证了内存可见性,且没有其他地方修改cellsBusy的值。

        // 初始化cells数组(重点)
        // cellsBusy是一个标识,0:当前cells数组没有在被初始化或扩容也没有新建Cell元素;1:cells数组在被初始化或扩容或在新建Cell元素
        // 通过CAS操作进行0、1状态切换使用casCellsBusy函数。
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false;
            try {                           
                if (cells == as) {
                    // 初始化cells元素个数为2
                    Cell[] rs = new Cell[2];
                    // h & 1 计算当前线程应该访问cells数组的哪个位置,即当前线程的threadLocalRandomProbe变量值 & (cells数组元素个数-1)
                    rs[h & 1] = new Cell(x);
                    cells = rs;
                    // 标识cells数组已被初始化
                    init = true;
                }
            } finally {
                // 对cellsBusy重置标记 线程安全因为cellsBusy是volatile保证了内存可见性,且没有其他地方修改cellsBusy的值
                cellsBusy = 0;
            }
    
  4. Cell数组如何扩容

    扩容条件:当前cells元素个数小于CPU个数且有冲突(当前多个线程cells访问了cells中同一个元素)使其中一个线程CAS失败才会进行扩容。

    注意:每个CPU都运行一个线程时,也就是每个cell都使用一个CPU处理时性能才是最高的。

    扩容操作:

    1. 先通过CAS设置cellBusy为1(casCellsBusy),然后才能进行扩容

    2. 将容量扩充到之前的2倍,并复制Cell元素到扩容后的数组,扩容后的数组里除了包含复制过来的元素之外还包含其他新元素,新元素为null

      Cell[] rs = new Cell[n << 1];
      for (int i = 0; i < n; ++i)
         rs[i] = as[i];
      cells = rs;
      

    整个扩充逻辑:

    		 // 如果当前cells长度大于CPU个数,则不进行扩容。如果当前cells已经过时(其他线程对cells执行了扩容操作,改变了cells指向),也不会扩容。因为每个CPU都运行一个线程时,也就是每个cell都使用一个CPU处理时性能才是最高的。
                else if (n >= NCPU || cells != as)
                    collide = false; 
               // 是否有冲突,执行到此处说明a.cas()执行失败,即有冲突,将collide置为true,跳过扩容阶段,重新获取probe,到cells不同位置尝试cas,再次失败则扩容
                else if (!collide)
                    collide = true;  
                // 扩容(重点)
                // 如果当前元素个数没有达到CPU个数并且有冲突(当前多个线程访问了cells中的同一个元素,从而导致冲突使其中一个线程CAS失败)则扩容。
                // 通过CAS设置cellBusy为1
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        if (cells == as) {
                            // 将容量扩充到之前的2倍,并复制Cell元素到扩容后的数组,扩容后的数组里除了包含复制过来的元素之外还包含其他新元素,新元素为null
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i)
                                rs[i] = as[i];
                            cells = rs;
                        }
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue; // 扩容后再次尝试(扩容后cells长度改变,
                              // 根据(n - 1) & h计算当前线程在cells中对应元素下标会变化,减少再次冲突的可能性)
                }
                h = advanceProbe(h); // 重新计算线程probe,减小下次访问cells元素时的冲突机会
            }
    
  5. 线程访问分配的Cell元素有冲突后如何处理

    对CAS失败的线程重新计算当前线程的随机值threadLocalRandomProbe,以减少下次访问cells元素冲突的机会

    // 重新计算线程probe,减小下次访问cells元素时的冲突机会
                h = advanceProbe(h); 
    
  6. 如何保证线程操作被分配的Cell元素的原子性

    当前线程通过分配的Cell元素的cas函数来保证对Cell元素value值更新的原子性。

    a.cas(v = a.value, v + x)

由上可知,AtomicLong的性能瓶颈是多个线程同时去竞争一个变量的更新权导致的。而LongAdder通过将一个变量分解成多个变量,让同样多的线程去竞争多个资源解决了此问题。

原理

如图,LongAdder内部维护了多个Cell,每个Cell内部有一个初始值为0的long类型变量,这样,在同等并发下,对单个变量的争夺会变少。此外,多个线程争夺同一个变量失败时,会到另一个Cell上去尝试,增加了重试成功的可能性。当LongAdder要获取当前值时,将所有Cell的值于base相加返回即可

LongAdder维护了一个初始值为null的Cell数组和一个基值变量base。当一开始Cell数组为空且并发线程较少时,仅使用base进行累加当并发增大时,会动态地增加Cell数组的容量

Cell类中使用了**@sun.misc.Contented注解进行了字节填充**,解决了由于连续分布于数组中且被多个线程操作可能造成的伪共享问题(关于伪共享,可查看《伪共享(false sharing),并发编程无声的性能杀手》这篇文章)。

LongAdder

先看LongAdder的定义

public class LongAdder extends Striped64 implements Serializable

Striped64类中有如下三个变量:

transient volatile Cell[] cells;

transient volatile long base;
// 实现CAS自旋锁,状态只有0、1
transient volatile int cellsBusy;

cellsBusy用于实现自旋锁,状态值只有0和1,当创建Cell元素、扩容Cell数组或初始化Cell数组时,使用CAS操作该变量来保证同时只有一个变量可以进行其中之一的操作。

Cell

下面看Cell的定义

@sun.misc.Contended static final class Cell {
    volatile long value; // volatile确保内存可见性
    Cell(long x) { value = x; }
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val); // CAS操作保证value原子性
    }

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long valueOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

将value声明为volatile确保了内存可见性,CAS操作保证了value值的原子性,@sun.misc.Contented注解的使用解决了伪共享问题。

LongAdder方法

下面来看LongAdder中的几个方法:

  • long Sum():返回当前的值,内部操作是累加所有Cell内部的value值后再累加base。sum的结果并非一个精确值,因为计算总和时并没有对Cell数组加锁,累加过程中Cell的值可能被更改。
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;
}
  • void reset():将base和Cell数组中非空元素的值置为0。
public void reset() {
    Cell[] as = cells; Cell a;
    base = 0L;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                a.value = 0L;
        }
    }
}
  • long sumThenRest():sum的改造版本。使用sum累加对应的Cell值后,把当前Cell的值重置为0,base重置为0。
public long sumThenReset() {
    Cell[] as = cells; Cell a;
    long sum = base;
    base = 0L;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null) {
                sum += a.value;
                a.value = 0L;
            }
        }
    }
    return sum;
}
  • void add(long x):判断cells数组是否为空,非空则进入内层,否则尝试直接通过CAS操作在base上进行add。内层代码中,声明了一个uncontented变量来记录调用longAccumulate方法前在相应cell上是否进行了失败的CAS操作。当前线程通过分配Cell元素的cas函数来保证对Cell元素value值更新的原子性。
public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    // 判断cells是否为空,如果不为空则直接进入内层判断,
    // 否则尝试通过CAS在base上进行add操作,若CAS成功则结束,否则进入内层
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        // 记录cell上的CAS操作是否失败
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            // 计算当前线程应该访问cells数组的哪个元素:getProbe() & m (m是当前cells数组元素个数-1,getProbe()用于获取当前线程中变量threadLocalRandomProbe的值,这个值一开始为0)
            (a = as[getProbe() & m]) == null ||
            // 尝试通过CAS操作在对应cell上add
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended); // 当前线程中变量threadLocalRandomProbe的值,这个值一开始为0,如果是0就要初始化
    }
}

longAccumelate方法

longAccumulate时Striped64类中定义的,其中包含了初始化cells数组,改变cells数组长度,新建cell等逻辑。

判断cells是否为空或者长度为0:

  • 如果空或者长度为0则尝试进行cells数组初始化,初始化失败的话则尝试通过CAS操作在base上进行add,仍然失败则重走一次流程;

  • 如果cells不为空且长度大于0,则获取当前线程对应于cells中的元素,如果该元素为null则尝试创建,否则尝试通过CAS操作在上面进行add仍失败则扩容

final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
    // 初始化当前线程的变量threadLocalRandomProbe的值
    int h;
    if ((h = getProbe()) == 0) {
        ThreadLocalRandom.current();
        h = getProbe();
        // 标记执行longAccumulate前对相应cell的CAS操作是否失败,失败为false
        wasUncontended = true; 
    }
    // 是否冲突,如果当前线程尝试访问的cell元素与其他线程冲突,则为true
    boolean collide = false; 
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        // 当前cells不为空且元素个数大于0则进入内层,否则尝试初始化
        if ((as = cells) != null && (n = as.length) > 0) {
            // 当前线程调用add方法并根据当前线程的随机数threadLocalRandomProbe和cells元素个数计算要访问的Cell元素下标 如果发现下标元素的值为null
            if ((a = as[(n - 1) & h]) == null) {//
                // 尝试添加新的cell
                if (cellsBusy == 0) {   
                    // 新增一个Cell元素到cells数组,并且将其添加到cells数组之前要竞争 设置cellsBusy为1
                    Cell r = new Cell(x);
                    if (cellsBusy == 0 && casCellsBusy()) {
                        boolean created = false;
                        try {               // Recheck under lock
                            Cell[] rs; int m, j;
                            if ((rs = cells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                rs[j] = r;
                                created = true;
                            }
                        } finally {
                            cellsBusy = 0;
                        }
                        if (created)
                            break;
                        continue;
                    }
                }
                collide = false;
            }
             // 如果已经进行了失败的CAS操作
            else if (!wasUncontended) 
                // 则不调用下面的a.cas()函数(反正肯定是失败的),而是重新计算probe值来尝试
                wasUncontended = true; 
            // 当前Cell存在,则执行CAS设置
            else if (a.cas(v = a.value, ((fn == null) ? v + x :fn.applyAsLong(v, x))))
                break;
           // 如果当前cells长度大于CPU个数,则不进行扩容。如果当前cells已经过时(其他线程对cells执行了扩容操作,改变了cells指向),也不会扩容
           // 因为每个CPU都运行一个线程时,也就是每个cell都使用一个CPU处理时性能才是最高的。
            else if (n >= NCPU || cells != as)
                collide = false; 
           // 是否有冲突,执行到此处说明a.cas()执行失败,即有冲突,将collide置为true,跳过扩容阶段,重新获取probe,到cells不同位置尝试cas,再次失败则扩容
            else if (!collide)
                collide = true;  
            // 扩容(重点)
            // 如果当前元素个数没有达到CPU个数并且有冲突(当前多个线程访问了cells中的同一个元素,从而导致冲突使其中一个线程CAS失败)则扩容。
            // 通过CAS设置cellBusy为1
            else if (cellsBusy == 0 && casCellsBusy()) {
                try {
                    if (cells == as) {
                        // 将容量扩充到之前的2倍,并复制Cell元素到扩容后的数组,扩容后的数组里除了包含复制过来的元素之外还包含其他新元素,新元素为null
                        Cell[] rs = new Cell[n << 1];
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i];
                        cells = rs;
                    }
                } finally {
                    cellsBusy = 0;
                }
                collide = false;
                continue; // 扩容后再次尝试(扩容后cells长度改变,
                          // 根据(n - 1) & h计算当前线程在cells中对应元素下标会变化,减少再次冲突的可能性)
            }
            // 重新计算线程probe,减小下次访问cells元素时的冲突机会
            h = advanceProbe(h); 
        }
        // 初始化cells数组(重点)
        // cellsBusy是一个标识,0:当前cells数组没有在被初始化或扩容也没有新建Cell元素;1:cells数组在被初始化或扩容或在新建Cell元素
        // 通过CAS操作进行0、1状态切换使用casCellsBusy函数。
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false;
            try {                           
                if (cells == as) {
                    // 初始化cells元素个数为2
                    Cell[] rs = new Cell[2];
                    // h & 1 计算当前线程应该访问cells数组的哪个位置,即当前线程的threadLocalRandomProbe变量值 & (cells数组元素个数-1)
                    rs[h & 1] = new Cell(x);
                    cells = rs;
                    // 标识cells数组已被初始化
                    init = true;
                }
            } finally {
                // 对cellsBusy重置标记 线程安全因为cellsBusy是volatile保证了内存可见性,且没有其他地方修改cellsBusy的值
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        // 尝试通过base的CAS操作进行add,成功则结束当前函数,否则再次循环
        else if (casBase(v = base, ((fn == null) ? v + x :fn.applyAsLong(v, x))))
            break; 
    }
}

LongAccumulator

LongAdder是LongAccumulator的特例,两者都继承自Striped64。

看如下代码:

public LongAccumulator(LongBinaryOperator accumulatorFunction,
                        long identity) {
    this.function = accumulatorFunction;
    base = this.identity = identity;
}

public interface LongBinaryOperator {
    long applyAsLong(long left, long right);
}

LongAccumulator构造器允许传入一个双目运算符接口用于自定义加法规则,还允许传入一个初始值。

自定义的加法函数是如何被应用的呢?以上提到的longAccumulate()方法中有如下代码:

a.cas(v = a.value, ((fn == null) ? v + x :fn.applyAsLong(v, x)))

LongAdder的add()方法中调用longAccumulate()方法时传入的是null,而LongAccumulator的accumulate()方法传入的是this.function,即自定义的加法函数。

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

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

相关文章

UE4/5数字人Metahuman与Style3D的使用【二、布料模拟】

目录 鼠标点击布料模拟&#xff1a; 让布料模拟可以跟着动画序列&#xff1a; 有穿模情况&#xff1a; 多件衣服替换&#xff1a; 关卡序列中使用缓存&#xff1a; 效果&#xff1a; UE4/5数字人Metahuman与Style3D的使用【一、Style3DAtelier软件制作smd格式衣服并导入ue】…

Apikit 自学日记:保存、使用测试用例

API测试用例是SaaS版本企业版才能使用的功能&#xff0c;免费版用户可通过付费升级后使用。 API管理应用中的测试用例管理涉及到两个场景&#xff1a;单接口测试用例管理 和 多接口测试用例批量测试。 一、单接口测试用例管理 功能入口&#xff1a;API管理应用 / 选中某个项目…

基于Java+SSM+Vue的高校校园点餐系统设计与实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

lesson 8下 Zigbee单播通信理论相关概念原理(端点、簇)

目录 Zigbee单播通信理论相关概念原理 端点&#xff08;Endpoint&#xff09; 簇&#xff08;ClusterID&#xff09; 通信数据帧抓包分析 接收过程中的端点和簇&#xff08;接收模块&#xff09; 接收过程中的端点 接收过程中的簇 发送过程中的端点和簇&#xff08;发送…

java适配器模式

一、是什么&#xff1f; 定义: 将一个类的接口变成另外一个类所期待的另一个接口, 从而使因接口不匹配而无法一起工作的两个类能够一起工作 举个例子, 苹果手机想用type-c的充电器充电, 但充电接口不吻合, 所以就选哦一个转接头, 使type-c 能给苹果手机充电, 这就是适配器 …

物联网应用中的 Wi-Fi 6

近年来&#xff0c;设备智联在我们的日常生活中越来越常见。从智能家居设备到工业自动化系统&#xff0c;物联网技术正在改变我们与世界交互的方式。随着物联网设备的不断增多&#xff0c;对可靠、高容量和低功耗无线连接的需求变得尤为迫切。这就是 Wi-Fi 6&#xff08;即 802…

SpringBoot整合Mybatis-plus项目完成CRUD

一、准备阶段&#x1f349; 1.创建项目&#x1f95d; 2.引入依赖&#x1f95d; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-inst…

2023上半年软考系统分析师科目一整理-24

2023上半年软考系统分析师科目一整理-24 IEEE 802.1x是一种&#xff08; &#xff09;认证协议。 A.用户ID B.报文 C. MAC地址 D. SSID IEEE802.1X协议实现基于端口(MAC地址(的访问控制。认证系统对连接到链路对端的请求者进行认证。一般在用户接入设备上实现802.1X认证。在认证…

【MySQL】利用SQL短路,解决无数据表连接问题

系列文章 MySQL安装教程&#xff08;详细&#xff09; 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/126037520 MySQL卸载教程&#xff08;详细&#xff09; 本文链接&#xff1a;https://blog.csdn.net/youcheng_ge/article/details/129279265 …

代理服务器之 squid、lvs、nginx、haproxy之间的区别

代理服务器之 squid、lvs、nginx、haproxy之间的区别 代理服务可简单的分为正向代理和反向代理 1、正向代理 正向代理服务器&#xff1a;squid 用于代理内部网络对 Internet 的连接请求(如 VPN/NAT)&#xff0c;客户端指定代理服务器,并将本来要直接发送给目标 Web 服务器的 HT…

Spring Boot 中的 ElasticsearchRepository 是什么,原理,如何使用

Spring Boot 中的 ElasticsearchRepository 是什么&#xff0c;原理&#xff0c;如何使用 简介 Elasticsearch 是一个开源的分布式搜索和分析引擎&#xff0c;可以通过 RESTful API 进行访问。Spring Data Elasticsearch 是 Spring Data 项目的一部分&#xff0c;提供了与 El…

Java线程等待唤醒的三种方法

线程等待唤醒的三种方法 需求&#xff1a;我们实现A线程等待B线程执行完在执行。 Object下面的wait()和notify() 使用Object中的wait()方法让线程等待&#xff0c;使用Object中的notify()方法唤醒线程 public static void main(String[] args) throws InterruptedException…

提高企业云服务性价比,亚马逊云科技为用户提供全面和深入的算力支持

6月27日至28日&#xff0c;2023亚马逊云科技中国峰会于上海顺利召开。在本次峰会上&#xff0c;似乎找寻到了云计算领域竞争对手均日渐成熟&#xff0c;而亚马逊云科技却能一直保持领先地位的原因——过去的十几年里&#xff0c;亚马逊云科技“基于客户需求&#xff0c;快速进行…

django-vue-admin ubuntu 20.04 环境准备 记录

django-vue-admin 运行记录 https://django-vue-admin.com/document/hjbs.html https://django-vue-admin.com/document/hjbs.html https://bbs.django-vue-admin.com/article/9.html https://gitee.com/liqianglog/django-vue-admin/tree/demo_project 1. 安装 ubuntu-20.04…

【八股】【C++】函数与类

这里写目录标题 形参与实参的区别函数调用过程指针和引用当函数参数引用作为函数参数有哪些好处回调函数友元函数重载匹配运算符重载直接初始化与拷贝初始化函数指针C中struct&#xff08;结构&#xff09;和class&#xff08;类&#xff09;的区别C有哪几种构造函数构造函数的…

中华太极图

python代码&#xff1a; import turtle turtle.circle(100) turtle.color(black,black) turtle.begin_fill() turtle.circle(50,180) turtle.circle(-50,180) turtle.right(180) turtle.circle(100,180) turtle.end_fill() turtle.penup() turtle.goto(0,25) turtle.pendown()…

06_pinctr子系统与gpio子系统

目录 pinctrl子系统简介 I.MX6ULL的pinctrl子系统驱动 PIN驱动程序讲解 设备树中添加pinctrl节点模板 gpio子系统简介 I.MX6ULL的gpio子系统驱动 GPIO驱动程序简介 gpio子系统API函数 设备树中添加gpio节点模板 与gpio相关的OF函数 LED实验 LED灯驱动程序编写 运行…

单片机中断

89C51/52的中断系统有5个中断源 &#xff0c;2个优先级&#xff0c;可实现二级中断嵌套 。 ( P3.2&#xff09;可由IT0(TCON.0)选择其为低电平有效还是下降沿有效。当CPU检测到P3.2引脚上出现有效的中断信号时&#xff0c;中断标志IE0(TCON.1)置1&#xff0c;向CPU申请中断。 &…

基于 unity 配置 adb

1.打开环境变量配置path的环境 2 找到自己的unity安装目录,找到对应路径 配置到 path 属性中 C:\~\Editor\2021.3.0f1c1\Editor\Data\PlaybackEngines\AndroidPlayer\SDK\platform-tools 3 应用保存即可

chatgpt赋能python:Win7下Python:轻松实现SEO优化

Win7下Python&#xff1a;轻松实现SEO优化 Python作为一门高级程序语言&#xff0c;不仅广泛应用于人工智能和数据科学领域&#xff0c;也在Web开发中扮演着重要角色。在SEO方面&#xff0c;Python也展现了强大的能力。本文将介绍如何在Win7下使用Python实现SEO优化。 什么是…