JUC并发编程与源码分析笔记09-原子类操作之十八罗汉增强

news2024/12/21 16:40:40

在这里插入图片描述

基本类型原子类

AtomicIntegerAtomicBooleanAtomicLong
常用API:

public final int get();// 获取当前的值
public final int getAndSet(int newValue);// 获取当前值,并设置新值
public final int getAndIncrement();// 获取当前的值,并自增
public final int getAndDecremennt();// 获取当前的值,并自减
public final int getAndAdd(int delta);// 获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update);// 如果输入的数值等于预期值(expect),则以原子方式将该值设置为输入值(update)

一个例子:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    public static void main(String[] args) {
        MyNumber myNumber = new MyNumber();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myNumber.add();
                }
            }).start();
        }
        System.out.println("最终结果:" + myNumber.atomicInteger.get());
    }
}

class MyNumber {
    AtomicInteger atomicInteger = new AtomicInteger();

    public void add() {
        atomicInteger.getAndIncrement();
    }
}

多次执行,会发现每次的结果都不一样,这是因为main线程执行的太快了,子线程还没有运行完成,main线程就获取了原子对象的值。尝试在main线程获取结果之前,添加一个线程等待,可以看到main线程就可以获取到正确的结果了。
那么,就有一个问题了,这里线程等待多久呢?由此就引出countDownLatch

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    public static void main(String[] args) throws InterruptedException {
        int size = 50;
        MyNumber myNumber = new MyNumber();
        CountDownLatch countDownLatch = new CountDownLatch(size);
        for (int i = 0; i < size; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 1000; j++) {
                        myNumber.add();
                    }
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }
        countDownLatch.await();
        System.out.println("最终结果:" + myNumber.atomicInteger.get());
    }
}

class MyNumber {
    AtomicInteger atomicInteger = new AtomicInteger();

    public void add() {
        atomicInteger.getAndIncrement();
    }
}

数组类型原子类

了解了基本类型原子类,再理解数组类型原子类就很容易了:AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray

public class AtomicDemo {
    public static void main(String[] args) {
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
        // AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(5);
        // AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1,2,3,4,5});
        for (int i = 0; i < atomicIntegerArray.length(); i++) {
            System.out.println(atomicIntegerArray.get(i));
        }
        System.out.println();
        int value = 0;
        value = atomicIntegerArray.getAndSet(0, 12345);
        System.out.println(value + "\t" + atomicIntegerArray.get(0));
        value = atomicIntegerArray.getAndIncrement(0);
        System.out.println(value + "\t" + atomicIntegerArray.get(0));
    }
}

引用类型原子类

AtomicReferenceAtomicStampedReferenceAtomicMarkableReference

AtomicReference

在自旋锁SpinLock里介绍过。

AtomicStampedReference

携带版本号的引用类型原子类,可以解决ABA问题,解决修改过几次问题,修改过一次后,版本号加一。

AtomicMarkableReference

带有标记位的引用类型原子类,标记位的值有两个:true/false,如果修改过,标记位由false变为true,解决一次性问题。

import java.util.concurrent.atomic.AtomicMarkableReference;

public class AtomicDemo {
    static AtomicMarkableReference  atomicMarkableReference = new AtomicMarkableReference(100, false);
    public static void main(String[] args) {
        new Thread(() -> {
            boolean marked = atomicMarkableReference.isMarked();
            System.out.println(Thread.currentThread().getName() + "标识:" + marked);
            try {
                Thread.sleep(1000);// 为了线程2可以拿到和线程1同样的marked,都是false
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = atomicMarkableReference.compareAndSet(100, 1000, marked, !marked);
            System.out.println(Thread.currentThread().getName() + "CAS结果:" + b);
            System.out.println("结果:" + atomicMarkableReference.getReference());
        }, "thread1").start();
        new Thread(() -> {
            boolean marked = atomicMarkableReference.isMarked();
            System.out.println(Thread.currentThread().getName() + "标识:" + marked);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean b = atomicMarkableReference.compareAndSet(100, 2000, marked, !marked);
            System.out.println(Thread.currentThread().getName() + "CAS结果:" + b);
            System.out.println("结果:" + atomicMarkableReference.getReference());
        }, "thread2").start();
    }
}

对象的属性修改原子类

AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater
使用目的:以一种线程安全的方式,操作非线程安全对象内的某个字段。
使用要求:

  1. 更新对象属性必须使用public volatile修饰
  2. 因为对象属性修改类型原子类是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性

AtomicIntegerFieldUpdater的例子

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicDemo {
    public static void main(String[] args) throws InterruptedException {
        int size = 10;
        Account account = new Account();
        CountDownLatch countDownLatch = new CountDownLatch(size);
        for (int i = 0; i < size; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 1000; j++) {
                        // account.synchronizedAdd();// 使用synchronized方式保证程序正确性
                        account.atomicAdd(account);// 不使用synchronized,保证程序的正确性
                    }
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }
        countDownLatch.await();
        System.out.println("money=" + account.money);
    }
}

class Account {
    String name = "王劭阳";
    public volatile int money = 0;

    /**
     * 使用synchronized方式保证程序正确性
     */
    public synchronized void synchronizedAdd() {
        money++;
    }

    AtomicIntegerFieldUpdater<Account> accountAtomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Account.class, "money");

    /**
     * 不使用synchronized,保证程序的正确性
     */
    public void atomicAdd(Account account) {
        accountAtomicIntegerFieldUpdater.getAndIncrement(account);
    }
}

AtomicReferenceFieldUpdater的例子

AtomicIntegerFieldUpdater也有局限性,它只能处理Integer类型的字段,对于其他类型的字段,可以使用AtomicReferenceFieldUpdater。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class AtomicDemo {
    public static void main(String[] args) throws InterruptedException {
        int size = 10;
        CountDownLatch countDownLatch = new CountDownLatch(size);
        Account account = new Account();
        for (int i = 0; i < size; i++) {
            new Thread(() -> {
                try {
                    account.init(account);
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }
        countDownLatch.await();
    }
}

class Account {
    public volatile Boolean init = Boolean.FALSE;

    AtomicReferenceFieldUpdater<Account, Boolean> accountBooleanAtomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Account.class, Boolean.class, "init");

    public void init(Account account) {
        if (accountBooleanAtomicReferenceFieldUpdater.compareAndSet(account, Boolean.FALSE, Boolean.TRUE)) {
            System.out.println(Thread.currentThread().getName() + " start init");
            System.out.println(Thread.currentThread().getName() + " end init");
        } else {
            System.out.println(Thread.currentThread().getName() + " init fail,because there has been inited");
        }
    }
}

原子操作增强类原理深度解析

DoubleAccumulatorDoubleAdderLongAccumulatorLongAdder:这几个类是从Java8出现的。
在阿里巴巴Java开发手册上,推荐使用LongAdder,因为它比AtomicLong性能更好(减少乐观锁的重试次数)。
这里以LongAccumulatorLongAdder为例进行比较。
区别:LongAdder只能用来计算加法,且从零开始计算,LongAccumulator提供了自定义函数操作。

import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

public class AtomicDemo {
    public static void main(String[] args) {
        LongAdder longAdder = new LongAdder();
        longAdder.increment();
        longAdder.increment();
        System.out.println(longAdder.sum());
        LongAccumulator longAccumulator = new LongAccumulator(Long::sum, 0);
        longAccumulator.accumulate(1);
        longAccumulator.accumulate(3);
        System.out.println(longAccumulator.get());
    }
}

设计计数器,比较synchronized、AtomicLong、LongAdder、LongAccumulator

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;

public class AtomicDemo {
    public static void main(String[] args) throws InterruptedException {
        long startTime, endTime;
        int size = 100;
        ClickNumber clickNumber = new ClickNumber();
        CountDownLatch countDownLatch1 = new CountDownLatch(size);
        CountDownLatch countDownLatch2 = new CountDownLatch(size);
        CountDownLatch countDownLatch3 = new CountDownLatch(size);
        CountDownLatch countDownLatch4 = new CountDownLatch(size);

        startTime = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 100 * 10000; j++) {
                        clickNumber.bySynchronized();
                    }
                } finally {
                    countDownLatch1.countDown();
                }
            }).start();
        }
        countDownLatch1.await();
        endTime = System.currentTimeMillis();
        System.out.println("bySynchronized:" + (endTime - startTime) + "\tsum=" + clickNumber.number);

        startTime = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 100 * 10000; j++) {
                        clickNumber.byAtomicLong();
                    }
                } finally {
                    countDownLatch2.countDown();
                }
            }).start();
        }
        countDownLatch2.await();
        endTime = System.currentTimeMillis();
        System.out.println("byAtomicLong:" + (endTime - startTime) + "\tsum=" + clickNumber.atomicLong.longValue());

        startTime = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 100 * 10000; j++) {
                        clickNumber.byLongAdder();
                    }
                } finally {
                    countDownLatch3.countDown();
                }
            }).start();
        }
        countDownLatch3.await();
        endTime = System.currentTimeMillis();
        System.out.println("byLongAdder:" + (endTime - startTime) + "\tsum=" + clickNumber.longAdder.sum());

        startTime = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 100 * 10000; j++) {
                        clickNumber.byLongAccumulator();
                    }
                } finally {
                    countDownLatch4.countDown();
                }
            }).start();
        }
        countDownLatch4.await();
        endTime = System.currentTimeMillis();
        System.out.println("byLongAccumulator:" + (endTime - startTime) + "\tsum=" + clickNumber.longAccumulator.longValue());
    }
}

class ClickNumber {
    long number;

    public synchronized void bySynchronized() {
        number++;
    }

    AtomicLong atomicLong = new AtomicLong();

    public void byAtomicLong() {
        atomicLong.getAndDecrement();
    }

    LongAdder longAdder = new LongAdder();

    public void byLongAdder() {
        longAdder.increment();
    }

    LongAccumulator longAccumulator = new LongAccumulator(Long::sum, 0);

    public void byLongAccumulator() {
        longAccumulator.accumulate(1);
    }
}

通过运行结果,可以看出来:LongAdderLongAccumulator的效率比AtomicLong要快。

源码、原理分析

架构

在这里插入图片描述

原理

当多个线程更新用于收集统计信息但不用于细粒度同步控制的目的的公共和时,LongAdder通常优于AtomicLong。在高争用的情况下,LongAdder的预期吞吐量明显更高,但代价是空间消耗更高。
为了分析其原理,需要先了解Striped64.javaCell.java类。
Cell.java是java.util.concurrent.atomic下Striped64.java的一个内部类。

/** Number of CPUS, to place bound on table size:CPU数量,即cells数组的最大长度 */
static final int NCPU = Runtime.getRuntime().availableProcessors();

/**
 * Table of cells. When non-null, size is a power of 2.
 * cells数组,为2的幂,方便位运算
 */
transient volatile Cell[] cells;

/**
 * Base value, used mainly when there is no contention, but also as
 * a fallback during table initialization races. Updated via CAS.
 * 基础value值,当并发较低时,只累加该值,主要用于没有竞争的情况,通过CAS更新
 */
transient volatile long base;

/**
 * Spinlock (locked via CAS) used when resizing and/or creating Cells.
 * 创建或扩容Cells数组时,使用自旋锁变量调整单元格大小(扩容),创建单元格时使用的锁
 */
transient volatile int cellsBusy;

Striped64.java中一些变量或方法定义:

  • base:类似于AtomicLong中全局value值。在没有竞争的情况下,数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
  • collide:表示扩容意向,false:一定不会扩容,true:可能会扩容
  • cellsBusy:初始化cells或者扩容cells需要获取锁,0:表示无锁状态,1:表示其他线程已经持有锁
  • casCellsBusy():通过CAS操作修改cellsBusy的值,CAS成功代表获取锁,返回true
  • NCPU:当前计算机CPU数量,Cell数组扩容时会使用到
  • getProbe():获取当前线程hash值
  • advanceProbe():重置当前线程hash值

LongAdder的基本思路就是分散热点,将value的值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
sum()方法会将Cell数组中的value和base累加并作为返回值,核心思想就是将之前的AtomicLong的一个value的更新压力分散到多个value中去,从而降级更新热点。
低并发的时候,就是对base变量进行CAS操作。高并发的时候,就是对Cell数组进行CAS操作,最后调用sum()。
v a l u e = b a s e + ∑ i = 0 n C e l l [ i ] value=base+\sum_{i=0}^{n}Cell[i] value=base+i=0nCell[i]
LongAdder在无竞争的情况下,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时,则采用化整为零分散热点的做法,用空间换时间,用一个数组Cell,将一个value拆分进这个数组Cell。多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash映射到这个数组Cell的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组Cell的所有值和base加起来作为最终结果。

源码解读深度分析

LongAdder的increment()方法:
increment()方法首先调用add(1L)方法,add()方法里,真正起作用的方法是longAccumulate()方法。

public void add(long x) {
	// as:Cells对象的引用
	// b:获取的base值
	// v:期望值
	// m:cells数组的长度
	// a:当前线程命中的cell单元格
    Cell[] as; long b, v; int m; Cell a;
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[getProbe() & m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

观察if判断条件,第一个条件(as = cells) != null的结果,第一次进来是false,我们继续看,会对base做一个cas操作,当线程少的时候,cas是足够的,因此casBase()的结果就是true,再取非,就是false了。这也是低并发时候,只需要操作base即可。
当线程增多,casBase()的返回值可能和想象中的不大一样了,因此,cas失败,!casBase()的值是false,所以会进第一层if。再继续往里看,第一个判断as == null即可判断出结果为true,再次进入if,接下来执行longAccumulate()方法。
当第一次来到longAccumulate()方法的时候,会执行Initialize table这块代码,初始化cells。
再次回到add方法,此时(as = cells) != null的结果为true,继续判断里面的if条件。as == null为false,继续判断(m = as.length - 1) < 0的值为false,继续判断(a = as[getProbe() & m]) == null,其中getProbe() & m可以类比HashMap的put()方法,在找数组下标时候的思想,通过位运算快速定位数组下标,判断as[i]这个地方有没有填充值,如果没有填充,那么运算结果为true,进入下面的longAccumulate()方法,如果运算结果为false,继续判断!(uncontended = a.cas(v = a.value, v + x))的值,也就是对非空的a值进行cas操作,操作成功后,uncontended是一个非零值,那么!uncontended就是一个零值,代表false,下面的longAccumulate()方法不会执行。继续观察!(uncontended = a.cas(v = a.value, v + x)),当cas失败的时候,!uncontended就为true,此时,也会进入longAccumulate()方法,这时候代表线程竞争更加激烈,现有的base+cell已经计算不过来了,需要对cell数组进行扩容了,扩容方法也在longAccumulate()里实现。
在这里插入图片描述
在这里插入图片描述
接下来查看longAccumulate()方法。
这个方法有3个参数:

  • long x:需要增加的值,一般是1
  • LongBinaryOperator fn:默认为null
  • boolean wasUncontentded:竞争标识,false表示存在竞争,只有cells初始化后,并且当前线程CAS修改失败,wasUncontentded的值才为false

查看getProbe()方法,大致可以理解成当前线程的hash值,用于确定进入哪个cells里。

final void longAccumulate(long x, LongBinaryOperator fn,
                          boolean wasUncontended) {
    int h;// 存储线程probe值
    if ((h = getProbe()) == 0) {// 说明probe未初始化
    	// 使用ThreadLocalRandom为当前线程重新计算一个hash值,强制初始化
        ThreadLocalRandom.current(); // force initialization
        h = getProbe();// 重新获取probe值,probe值被重置好比一个全新的线程一样
        wasUncontended = true;// 标记wasUncontended为true
    }
    boolean collide = false;                // True if last slot nonempty
    for (;;) {// 自旋,看代码的时候,先看case2,case3,再看case1
        Cell[] as; Cell a; int n; long v;
        // case1:cells已经被初始化了,可能存在扩容
        if ((as = cells) != null && (n = as.length) > 0) {
            if ((a = as[(n - 1) & h]) == null) {// 当前线程hash后映射的cells[i]为null,说明这个cells[i]可以使用
            	// 数组没有在扩容
                if (cellsBusy == 0) {       // Try to attach new Cell
                	// 新建一个Cell,填充value
                    Cell r = new Cell(x);   // Optimistically create
                    // 尝试加锁,成功后cellsBusy=1
                    if (cellsBusy == 0 && casCellsBusy()) {
                        boolean created = false;
                        try {               // Recheck under lock
                            Cell[] rs; int m, j;
                            // 双端加锁,将刚才带值的cell放到cells数组中
                            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;           // Slot is now non-empty
                    }
                }
                collide = false;
            }
            // wasUncontended表示初始化cells后,当前线程竞争失败,wasUncontended=false,重新设置wasUncontended为true,接着执行advanceProbe(h)重置当前hash值,重新循环
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            // 说明当前线程对应数组有数据,也重置过hash值,通过CAS操作尝试对当前cells[i]累加x,如果cas成功了,直接跳出循环
            else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                         fn.applyAsLong(v, x))))
                break;
            // 如果n≥CPU最大数量,就不能扩容,并通过下面的advanceProbe(h)再次计算hash值
            else if (n >= NCPU || cells != as)
                collide = false;            // At max size or stale
            // 如果collide为false,则修改它为true(可以扩容),重新计算hash值,如果当前数组已经≥CPU最大数量,还会把collide置为false(不能扩容)
            else if (!collide)
                collide = true;
            else if (cellsBusy == 0 && casCellsBusy()) {
                try {
                	// 当前数组和最先赋值的数组是同一个,代表没有被其他线程扩容过,当前线程对数组进行扩容
                    if (cells == as) {      // Expand table unless stale
                    	// 容量按位左移1位进行扩容
                        Cell[] rs = new Cell[n << 1];
                        // 扩容后的拷贝工作
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i];
                        // 将扩容后的数组指向cells
                        cells = rs;
                    }
                } finally {
                	// 释放锁
                    cellsBusy = 0;
                }
                // 设置扩容状态=不能扩容,继续循环
                collide = false;
                continue;                   // Retry with expanded table
            }
            h = advanceProbe(h);
        }
        // case2:cells没有加锁且没有初始化,尝试对它加锁并初始化cells数组
        // cellsBusy=0表示无锁,并通过casCellsBusy()获取锁,也就是修改cellsBusy的值
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false;
            try {                           // Initialize table
            	// double-check的目的:避免再次new一个cells数组,避免上一个线程中的数据被篡改
                if (cells == as) {
                	// 新建一个容量为2的数组
                    Cell[] rs = new Cell[2];
                    rs[h & 1] = new Cell(x);// 填充数据
                    cells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        // case3:cells正在初始化,尝试直接在基数base上进行累加操作
        // 兜底方法,上面的所有cas操作都失败了,那么操作数据就会更新到base上
        else if (casBase(v = base, ((fn == null) ? v + x :
                                    fn.applyAsLong(v, x))))
            break;                          // Fall back on using base
    }
}

在这里插入图片描述
接下来是sum()方法求和,会将Cell数组中value和base累加作为返回值,核心思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。

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()方法在执行的时候,并没有限制base和cells的更新,所以LongAdder不是强一致性的,它是最终一致性的。最终返回的sum是局部变量,初始化的时候sum=base,在累加cells[i]的时候,base可能被更新了,但是sum并不会重新读取base的值,所以会出现数据不准确的情况。

使用总结

AtomicLong:线程安全,可允许一些性能损耗,要求高精度时使用,保证精度,性能代价,AtomicLong是多个线程对单个热点值value进行原子操作。
LongAdder:需要在高并发下有较好的性能表现,对值精确度要求不高时候,可以使用,保证性能,精度代价,LongAdder是每个线程拥有自己的槽位,各个线程一般只对自己槽中的那个值进行CAS操作。

小总结

AtomicLong:

原理:CAS+自旋:incrementAndGet()
场景:低并发下全局计算,AtomicLong能保证并发下计数的准确性,内部通过CAS来解决并发安全性问题
缺陷:高并发后性能急剧下降,AtomicLong的自旋会成为瓶颈,N个线程进行CAS操作,每次只有一个线程成功,其他N-1个线程失败,失败后就不停的自旋直到成功,这样大量失败自旋的情况,CPU占用率就高了

LongAdder:

原理:CAS+Base+Cell数组,空间换时间分散热点数据
场景:高并发下全局计算
缺陷:如果在sum求和过程中,还有计算线程修改结果的话,会造成结果不准确

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

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

相关文章

2.25Maven的安装与配置

一.Mavenmaven是一个Java世界中,非常知名的"工程管理工具"/构建工具"核心功能:1.管理依赖在进行一个A 操作之前,要先进行一个B操作.依赖有的时候是很复杂的,而且是嵌套的2.构建/编译(也是在调用jdk)3. 打包把java代码给构建成jar或者warjar就是一个特殊的压缩包…

【基础算法】二分例题(我在哪?)

&#x1f339;作者:云小逸 &#x1f4dd;个人主页:云小逸的主页 &#x1f4dd;Github:云小逸的Github &#x1f91f;motto:要敢于一个人默默的面对自己&#xff0c;强大自己才是核心。不要等到什么都没有了&#xff0c;才下定决心去做。种一颗树&#xff0c;最好的时间是十年前…

元宇宙+教育,正在引发哪些剧烈变革?机会在哪里?丨圆桌实录

图片来源&#xff1a;由无界AI绘画工具生成2月23日&#xff0c;温州元宇宙创新中心为2023年第一批申请入驻的项目企业举办了签约仪式。温州临境网络科技有限公司、温州好玩文化产业有限公司、温州云兮科技有限公司&#xff08;筹&#xff09;等企业完成签约。这意味着&#xff…

Spring 事务管理详解及使用

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

Vue模态框的封装

一、模态框1、模态框&#xff1a;若对话框不关闭&#xff0c;不能操作其父窗口2、非模态框&#xff1a;对话框不关闭&#xff0c;可以操作其窗口二、Vue组件实现模态框的功能1、模态框是一个子组件2、显示和隐藏由父组件决定3、对话框的标题也是由父组件传递的4、对话框的主显示…

OpenAPI SDK组件之Spring Aop源码拓展

Spring Aop 看这个分享的应该都用过Spring Aop&#xff0c;这里就不再过多介绍了它是什么了。 我抽取了Spring Aop的部分源码&#xff0c;通过它实现请求参数可变拦截&#xff0c;同时apisdk离开Spring框架&#xff0c;仍然可以正常运行。 讲拦截也好&#xff0c;通知也罢&a…

[蓝桥杯 2022 国 B] 卡牌(贪心/二分)

题目传送门 该题第一思路是想去模拟题目中所描述的过程 这里我选择从大到小遍历可能凑出的牌套数&#xff0c;计算凑出它需要补的牌数以及判断是否会超出能补的牌数 #include<iostream> #include<climits> #include<vector> #include<algorithm> #def…

深拷贝与浅拷贝的理解

浅拷贝的理解浅拷贝的话只会拷贝基本数据类型&#xff0c;例如像string、Number等这些&#xff0c;类似&#xff1a;Object、Array 这类的话拷贝的就是对象的一个指针(通俗来讲就是拷贝一个引用地址&#xff0c;指向的是一个内存同一份数据)&#xff0c;也就是说当拷贝的对象数…

【人工智能 AI】What is RPA? 什么是机器人流程自动化?

目录 Introduction to RPA 机器人流程自动化简介 What is RPA? 什么是机器人流程自动化?

17、触发器

文章目录1 触发器概述2 触发器的创建2.1 创建触发器语法2.2 代码举例3 查看、删除触发器3.1 查看触发器3.2 删除触发器4 触发器的优缺点4.1 优点4.2 缺点4.3 注意点尚硅谷MySQL数据库教程-讲师&#xff1a;宋红康 我们缺乏的不是知识&#xff0c;而是学而不厌的态度 在实际开发…

数据结构:各种排序方法的综合比较

排序方法的选用应视具体场合而定。一般情况下考虑的原则有:(1)待排序的记录个数 n;(2)记录本身的大小;(3)关键字的分布情况:(4)对排序稳定性的要求等。 1.时间性能 (1) 按平均的时间性能来分,有三类排序方法: 时间复杂度为 O(nlogn)的方法有:快速排序、堆排序和归并排序,其中…

前端一面必会面试题(边面边更)

哪些情况会导致内存泄漏 以下四种情况会造成内存的泄漏&#xff1a; 意外的全局变量&#xff1a; 由于使用未声明的变量&#xff0c;而意外的创建了一个全局变量&#xff0c;而使这个变量一直留在内存中无法被回收。被遗忘的计时器或回调函数&#xff1a; 设置了 setInterval…

P1196 [NOI2002] 银河英雄传说 带权并查集

[NOI2002] 银河英雄传说 题目背景 公元 580158015801 年&#xff0c;地球居民迁至金牛座 α\alphaα 第二行星&#xff0c;在那里发表银河联邦创立宣言&#xff0c;同年改元为宇宙历元年&#xff0c;并开始向银河系深处拓展。 宇宙历 799799799 年&#xff0c;银河系的两大军…

使用Java编写Hive的UDF实现身份证号码校验及15位升级18位

使用Java编写Hive的UDF实现身份证号码校验及15位升级18位 背景 在数仓项目中&#xff0c;有时候会根据身份证信息做一些取数filter或者条件判断的相关运算进而获取到所需的信息。古人是用Oracle做数仓&#xff0c;理所当然是用SQL写UDF【虽然SQL写UDF给SQL用就像用鸡肉饲养肉…

Gin获取Response Body引发的OOM

有轮子尽量用轮子 &#x1f62d; &#x1f62d; &#x1f62d; &#x1f62d; &#x1f62d; &#x1f62d; 我们在开发中基于Gin开发了一个Api网关&#xff0c;但上线后发现内存会在短时间内暴涨&#xff0c;然后被OOM kill掉。具体内存走势如下图&#xff1a; 放大其中一次 在…

OllyDbg

本文通过吾爱破解论坛上提供的OllyDbg版本为例&#xff0c;讲解该软件的使用方法 F2对鼠标所处的位置打下断点&#xff0c;一般表现为鼠标所属地址位置背景变红F3加载一个可执行程序&#xff0c;进行调试分析&#xff0c;表现为弹出打开文件框F4执行程序到光标处F5缩小还原当前…

EF 框架的简介、发展历史;ORM框架概念

一、EF 框架简介EF 全称是 EntityFramework 。Entity Framework是ADO.NET 中的一套支持开发面向数据的软件应用程序的技术,是微软的一个ORM框架。ORM框架&#xff08;Object Relational Mapping&#xff09; 翻译过来就是对象关系映射。如果不用ORM框架&#xff0c;我们一般这样…

考虑交叉耦合因素的IPMSM无传感器改进线性自抗扰控制策略

考虑交叉耦合因素的IPMSM无传感器改进线性自抗扰控制策略一级目录二级目录三级目录控制原理ELADRC信号提取龙格贝尔观测器方波注入simulink仿真给定转速&#xff1a;转速环&#xff1a;电流环&#xff1a;一级目录 二级目录 三级目录 首先声明一下&#xff0c;本篇博客是复现…

分析 HTTP,TCP 的长连接和短连接以及 socket

1、HTTP 协议与 TCP/IP 协议的关系 HTTP 的长连接和短连接本质上是 TCP 长连接和短连接。HTTP 属于应用层协议&#xff0c;在传输层使用 TCP 协议&#xff0c;在网络层使用 IP 协议。IP 协议主要解决网络路由和寻址问题&#xff0c;TCP 协议主要解决如何在 IP 层之上可靠的传递…

Apache Hadoop生态部署-Flume采集节点安装

目录 Apache Hadoop生态-目录汇总-持续更新 一&#xff1a;安装包准备 二&#xff1a;安装与常用配置 2.1&#xff1a;下载解压安装包 2.2&#xff1a;解决guava版本问题 2.3&#xff1a;修改配置 三&#xff1a;修复Taildir问题 3.1&#xff1a;Taildir Source能断点续…