学习笔记:Java 并发编程④

news2024/11/27 1:31:12

若文章内容或图片失效,请留言反馈。

部分素材来自网络,若不小心影响到您的利益,请联系博主删除。


  • 视频链接https://www.bilibili.com/video/av81461839
  • 配套资料https://pan.baidu.com/s/1lSDty6-hzCWTXFYuqThRPw提取码5xiu

写这篇博客旨在制作笔记,方便个人在线阅览,巩固知识。无他用。

博客的内容主要来自视频内容和资料中提供的学习笔记。当然,在此基础之上也增删了一些内容。


参考书籍《实战 JAVA 高并发程序设计》 葛一鸣


系列目录


  • 学习笔记:Java 并发编程①_基础知识入门
  • 学习笔记:Java 并发编程②_共享模型之管程
  • 学习笔记:Java 并发编程③_共享模型之内存
  • 学习笔记:Java 并发编程④_共享模型之无锁
  • 学习笔记:Java 并发编程⑤_共享模型之不可变
  • 学习笔记:Java 并发编程⑥_共享模型之并发工具

本章内容CASvolatile原子整数原子引用原子累加器Unsafe


1.初步体验


1.1.提出问题


需求:保证 account.withdraw 取款方法的线程安全


Account.java

public interface Account {
    // 获取余额
    Integer getBalance();

    // 取款
    void withdraw(Integer amount);

    /**
     * 方法内会启动 1000 个线程,每个线程做 -10 元的操作
     * 如果初始余额是 10000,那么正确的结果应该是 0
     */

    static void demo(Account account) {
        List<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 1000; i++) {
            threads.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }

        long startTime = System.nanoTime();

        threads.forEach(Thread::start);
        threads.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        long endTime = System.nanoTime();

        System.out.println(account.getBalance() + " cost:" + (endTime - startTime) / 1000_000 + "ms");
    }
}

AccountUnsafe.java

public class AccountUnsafe implements Account {
    private Integer balance;

    public AccountUnsafe(Integer balance) {
        this.balance = balance;
    }

    @Override
    public Integer getBalance() {
        return this.balance;
    }

    @Override
    public void withdraw(Integer amount) {
        this.balance -= amount;
    }
}

测试类TestAccount.java

public class TestAccount {
    public static void main(String[] args) {
        Account accountUnsafe = new AccountUnsafe(10000);
        Account.demo(accountUnsafe);
    }
}

上面的代码肯定是达不到需求的。

因为余额(balance)是共享资源,多个线程对会对它就行读写操作,withdraw() 很明显是临界区。

控制台的输出结果也不是每次都是 0

320 cost:192ms

1.2.加锁实现


我们可以加锁来实现它的线程安全以达到需求。(最好是读写都加锁)

AccountUnsafe.java

在这里插入图片描述

0 cost:191ms

1.3.无锁实现


参考书籍《实战 JAVA 高并发程序设计》 葛一鸣

对于并发控制而言,锁是一种悲观的策略。它总是假设每一次的临界区操作会产生冲突。因此,必须对每次操作都小心翼翼。如果有多个线程同时需要访问临界区资源,就宁可牺牲性能让线程进行等待,所以说锁会阻塞线程执行。

对于并发控制而言,无锁是一种乐观的策略,它会假设访问是没有冲突的。既然没有冲突,自然不需要等待,所以所有的线程都可以在不停顿的状态下持续执行。那遇到冲突怎么办呢?无锁的策略使用一种叫做比较交换的技术(CAS,即 CompareAnd Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。

与锁相比,使用比较交换(下文简称 CAS)会使程序看起来更加复杂一些。但由于其非阻塞性,它对死锁问题天生免疫,并且,线程间的相互影响也远远比基于锁的方式要小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,它要比基于锁的方式拥有更优越的性能。

在硬件层面,大部分的现代处理器都已经支持原子化的 CAS 指令。在 JDK 5.0 以后,虚拟机便可以使用这个指令来实现并发操作和并发数据结构,并且,这种操作在虚拟机中可以说是无处不在。为了让 Java 程序员能够受益于 CASCPU 指令,JDK 并发包中有一个 atomic 包,里面实现了一些直接使用 CAS 操作的线程安全的类型。其中,最常用的一个类,应该就是 AtomicInteger。你可以把它看做是一个整数。但是与 Integer 不同,它是可变的,并且是线程安全的。对其进行修改等任何操作,都是用 CAS 指令进行的。


接下来,我们需要对上面的代码块做一些改动,通过无锁的方式来达到需求。

其中 Account.java 代码是无需改动的。


AccountCAS.java

public class AccountCAS implements Account_1 {
    private AtomicInteger balance;

    public AccountCAS(int balance) {
        this.balance = new AtomicInteger(balance);
    }

    @Override
    public Integer getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(Integer amount) {
        while (true) {
            // 获取余额的最新值
            int prev = balance.get();
            // 要修改的余额
            int next = prev - amount;
            // 真正意义上的修改
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

测试代码TestAccount.java

public class TestAccount {
    public static void main(String[] args) {
        Account_1 accountUnsafe = new AccountUnsafe(10000);
        Account_1.demo(accountUnsafe);

        Account_1 accountCAS = new AccountCAS(10000);
        Account_1.demo(accountCAS);
    }
}

输出结果

0 cost:76 ms
0 cost:69 ms

2.CAS


2.1.CAS 的工作方式


相关视频CAS 的工作方式

前面看到的 AtomicInteger 的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?

public void withdraw(Integer amount) {
    while (true) {
        // 需要不断尝试,直到成功为止
        while (true) {
            // 比如拿到了旧值 1000
            int prev = balance.get();
            // 在这个基础上 1000-10 = 990
            int next = prev - amount;
            
            /* 
             * CompareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
             *
             * [不一致] next 作废,返回 false 表示失败
             * * 比如,别的线程已经做了减法,当前值已经被减成了 990
             * * 那么本线程的这次 990 就作废了,进入 while 下次循环重试
             *
             * [一致] 以 next 设置为新值,返回 true 表示成功
             */
        }

        if (balance.compareAndSet(prev, next)) {
            break;
        }
    }
}

其中的关键是 compareAndSet,它的简称就是 CAS(也有 Compare And Swap 的说法),它必须是原子操作。

在这里插入图片描述

其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证 比较-交换 的原子性。

在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。


不过相较于视频,我个人觉得书中的分析更加容易理解(其实和上面那张图也差不了多少)

参考书籍《实战 JAVA 高并发程序设计》 葛一鸣

CAS 算法的过程是这样:它包含三个参数 CAS(V, E, N)V 表示要更新的变量,E 表示预期值,N 表示新值。仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。CAS 操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。

简单地说,CAS 需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了。


2.2.CAS 慢动作分析


这里基本就是用代码来演示上面的分析过程(如果理解了上面的分析的话,这个 2.2 小节的代码块就不用看了)

相关视频CAS 慢动作分析

SlowMotion.java

@Slf4j
public class SlowMotion {
    public static void main(String[] args) {
        AtomicInteger balance = new AtomicInteger(10000);
        int mainPrev = balance.get();
        log.debug("try get {}", mainPrev);
        new Thread(() -> {
            sleep(1000);
            int prev = balance.get();
            balance.compareAndSet(prev, 9000);
            log.debug(balance.toString());
        }, "t1").start();
        sleep(2000);
        log.debug("try set 8000...");
        boolean isSuccess = balance.compareAndSet(mainPrev, 8000);
        log.debug("is success ? {}", isSuccess);
        if (!isSuccess) {
            mainPrev = balance.get();
            log.debug("try set 8000...");
            isSuccess = balance.compareAndSet(mainPrev, 8000);
            log.debug("is success ? {}", isSuccess);
        }
    }

    private static void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

控制台输出

17:13:36.207 [main] DEBUG org.example.chapter06.SlowMotion - try get 10000
17:13:37.264 [t1] DEBUG org.example.chapter06.SlowMotion - 9000
17:13:38.263 [main] DEBUG org.example.chapter06.SlowMotion - try set 8000...
17:13:38.263 [main] DEBUG org.example.chapter06.SlowMotion - is success ? false
17:13:38.263 [main] DEBUG org.example.chapter06.SlowMotion - try set 8000...
17:13:38.263 [main] DEBUG org.example.chapter06.SlowMotion - is success ? true

2.3.CAS 与 volatile


获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

注意事项volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现 比较并交换 的效果

例:AtomicInteger 中的两个字段(java/util/concurrent/atomic/AtomicInteger.java

// AtomicInteger 当前的实际取值

private volatile int value;
// 保存着 value 字段在 AtomicInteger 对象中的偏移量

private static final long valueOffset;

2.4.为什么无锁效率高


  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇。
    synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
  • 打个比喻:线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等到它被唤醒时又得重新打火、启动、加速… 恢复到高速运行,这样做的代价是比较大的
  • 但是在无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。当线程数小于等于 CPU 核心数时,使用无锁方案是比较划算的,是有足够多的 CPU 让线程运行的;当线程数远多于 CPU 核心数时,无锁的效率相比于有锁的效率就没有太大的优势了,此时依旧会发生上下文切换。

在这里插入图片描述


2.5.CAS 特点总结


结合 CASvolatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试。
  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
  • CAS 体现的是无锁并发无阻塞并发。请仔细体会这句话的意思:
    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。

3.CAS 相关工具类


3.1.原子整数


J.U.C 并发包提供了:AtomicBooleanAtomicIntegerAtomicLong

此处介绍一下 AtomicInteger,相较于其他的原子类,操作都是类似的。


3.1.1.基本方法


下方的代码块是 AtomicInteger 的一些主要方法

java/util/concurrent/atomic/AtomicInteger.java

// 取得当前值
public final int get() { return value; } 

// 设置当前值
public final void set(int newValue) { value = newValue; } 

// 设置新值,并返回旧值
public final int getAndSet(int newValue) { 
	return unsafe.getAndSetInt(this, valueOffset, newValue); 
} 

// 若当前值为 except,则设置为 update
public final boolean compareAndSet(int expect, int update) { 
	return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 
}

// 当前值加 1,返回旧值
public final int getAndIncrement() { 
	return unsafe.getAndAddInt(this, valueOffset, 1); 
}

// 当前值减 1,返回旧值
public final int getAndDecrement() { 
	return unsafe.getAndAddInt(this, valueOffset, -1); 
}

// 当前值增加 delta,返回旧值
public final int getAndAdd(int delta) {
	return unsafe.getAndAddInt(this, valueOffset, delta);
}

// 当前值加 1,返回新值
public final int incrementAndGet() {
	return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// 当前值减 1,返回新值
public final int decrementAndGet() {
	return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}

// 当前值增加 delta,返回新值
public final int addAndGet(int delta) {
	return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

下方的代码块是 AtomicInteger 的两个重要字段

java/util/concurrent/atomic/AtomicInteger.java

// 该字段是 AtomicInteger 当前的实际取值
private volatile int value;

// 该字段保存着 value 字段在 AtomicInteger 对象中的偏移量
private static final long valueOffset;

测试代码-1

public class TestAtomicInteger_1 {
    public static void main(String[] args) {
        AtomicInteger i = new AtomicInteger(0);

        System.out.println("i.get():" + i.get());
        System.out.println("i.incrementAndGet():" + i.incrementAndGet()); // ++i
        System.out.println("i.getAndIncrement():" + i.getAndIncrement()); // i++
        System.out.println("i.get():" + i.get());

        System.out.println("i.addAndGet(5):" + i.addAndGet(5));
        System.out.println("i.getAndAdd(5):" + i.getAndAdd(5));
        System.out.println("i.get():" + i.get());
    }
}

输出结果-1

i.get()0
i.incrementAndGet()1
i.getAndIncrement()1
i.get()2
i.addAndGet(5)7
i.getAndAdd(5)7
i.get()12

3.1.2.复杂运算方法


此外,再介绍一个 AtomicInteger 中的方法:updateAndGet。该方法可以做一些复杂运算。

java/util/concurrent/atomic/AtomicInteger.java

public final int updateAndGet(IntUnaryOperator updateFunction) {
    int prev, next;
    do {
        prev = get();
        next = updateFunction.applyAsInt(prev);
    } while (!compareAndSet(prev, next));
    return next;
}

上述代码块中的方法中的 IntUnaryOperator 是一个函数式接口,而且其内部只包含了一个抽象方法。这就意味着该接口可以配合 Lambda 表达式来使用。在这个 Lambda 表达式中,参数代表的是我们要读取到的值,运算的结果就是将来我们要设置的值。

// 所谓的函数式接口,其实就是在这个接口里面,只能有一个抽象方法。

@FunctionalInterface // 该注解只能标记在 "有且仅有一个抽象方法" 的接口上,主要用于编译期的错误检查
public interface IntUnaryOperator {
	int applyAsInt(int operand);
	
	... ...
}

测试代码-2

public class TestAtomicInteger_2 {
    public static void main(String[] args) {
        AtomicInteger i = new AtomicInteger(1);
        
        int value_1 = i.updateAndGet(x -> x * 10);
        System.out.println("i.getAndUpdate(x -> x * 10):" + value_1);// 10
        System.out.println("i.get():" + i.get()); // 10

        int value_2 = i.getAndUpdate(y -> y * 10);
        System.out.println("i.getAndUpdate(y -> y * 10):" + value_2);// 10
        System.out.println("i.get():" + i.get()); // 100

		// updateAndGet(IntUnaryOperator updateFunction):更新后返回新值
		// getAndUpdate(IntUnaryOperator updateFunction):更新后返回旧值
    }
}

输出结果-2

i.getAndUpdate(x -> x * 10)10
i.get()10
i.getAndUpdate(y -> y * 10)10
i.get()100

updateAndGet 方法中,用函数式接口来作为参数,其目的是:一个方法,多种运算实现。

为了追求代码的通用性,要把计算的操作当成一个变化的参数传递进来(比如加减乘除),这里就用到了接口的思想。

IntUnaryOperator 中的 applyAsInt(int operand) 方法就是接受一个整数,返回一个整数。至于它中间做了什么操作,我们并不关心。我们只要给一个旧值,updateAndGet 方法给我们一个计算结果,之后我们再拿这两个值做 CAS 操作、做原子更新操作即可。

这里我们可以自己实现一下 java/util/concurrent/atomic/AtomicInteger.javaupdateAndGet 方法的逻辑

public class TestAtomicInteger_3 {
    public static void main(String[] args) {
        AtomicInteger i = new AtomicInteger(10);
        updateAndGet(i, oldValue -> oldValue / 2);
        System.out.println(i.get()); // 输出结果是 5
    }

    public static int updateAndGet(AtomicInteger i, IntUnaryOperator operator) {
        while (true) {
            int prev = i.get();
            int next = operator.applyAsInt(prev);
            if (i.compareAndSet(prev, next)) {
                return next;
            }
        }
    }
}

上方的代码块和源码中的方法实现,并没有什么太大的区别。诸位可以自行对比一下。


3.2.原子引用


在实际开发中,我们要保护的共享数据不一定是基本数据类型,也可能是 decimal 这种小数类型,此时我们就可以使用原子引用保证操作共享变量时的线程安全。原子引用类型有以下几种:AtomicReferenceAtomicMarkableReferenceAtomicStampedReference


3.2.1.AtomicReference


其实这个 AtomicReference 和上面介绍的 AtomicInteger 也并无太大的区别

下面的代码块就是对之前的案例的一个改造。


DecimalAccount.java

public interface DecimalAccount {
    BigDecimal getBalance(); // 获取余额

    void withdraw(BigDecimal amount); // 取款

    static void demo(DecimalAccount decimalAccount) {
        List<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 1000; i++) {
            threads.add(new Thread(() -> {
                decimalAccount.withdraw(BigDecimal.TEN);
            }));
        }

        threads.forEach(Thread::start);

        threads.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(decimalAccount.getBalance());
    }

}

DecimalAccountCAS.java

public class DecimalAccountCAS implements DecimalAccount {
    private AtomicReference<BigDecimal> balance;

    public DecimalAccountCAS(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

    @Override
    public BigDecimal getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(BigDecimal amount) {
        while (true) {
            BigDecimal prev = balance.get();
            BigDecimal next = prev.subtract(amount);
            if (balance.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

TestDecimalAccount.java

public class TestDecimalAccount {
    public static void main(String[] args) {
        DecimalAccount.demo(new DecimalAccountCAS(new BigDecimal("10000")));
    }
}

输出结果是 0,符合预期。


3.2.2.ABA 问题


先让我们来看看下方的代码

TestAtmoicReference.java

@Slf4j(topic = "c.TestAtomicReference")
public class TestAtmoicReference {
    static AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) throws InterruptedException {
        log.debug("Main start ...");
        
        // 获取值
        String prev = ref.get();
        
        other();
        sleep(1);
        
        // 尝试改为 C
        log.debug("change A->C:{}", ref.compareAndSet(prev, "C"));
    }

    private static void other() {
        new Thread(() -> {
            log.debug("change A->B:{}", ref.compareAndSet(ref.get(), "B"));
        }, "t1").start();

		sleep(0.5);
	
        new Thread(() -> {
            log.debug("change B->A:{}", ref.compareAndSet(ref.get(), "A"));
        }, "t2").start();
    }
}

控制台输出

22:43:22.354 [main] DEBUG c.TestAtomicReference - Main start ...
22:43:22.400 [t1] DEBUG c.TestAtomicReference - change A->Btrue
22:43:22.909 [t2] DEBUG c.TestAtomicReference - change B->Atrue
22:43:23.917 [main] DEBUG c.TestAtomicReference - change A->Ctrue

显然,当前线程无法正确判断 AtomicReference 类型的对象是否被其他线程修改过。

主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况。

如果主线程希望:只要有其它线程 动过了 共享变量,那么自己的 CAS 就算失败,这时,仅比较值是不够的,需要再加一个版本号。


参考书籍《实战 JAVA 高并发程序设计》 葛一鸣

AtomicReferenceAtomicInteger 非常类似,不同之处就在于 AtomicInteger 是对整数的封装,而 AtomicReference 则对应普通的对象引用。也就是它可以保证你在修改对象引用时的线程安全性。

在介绍 AtomicReference 的同时,我希望同时提出一个有关原子操作的逻辑上的不足。

之前我们说过,线程判断被修改对象是否可以正确写入的条件是对象的当前值和期望值是否一致。这个逻辑从一般意义上来说是正确的。但有可能出现一个小小的例外,就是当你获得对象当前数据后,在准备修改为新值前,对象的值被其他线程连续修改了两次,而经过这两次修改后,对象的值又恢复为旧值。这样,当前线程就无法正确判断这个对象究竟是否被修改过。

打一个比方,如果有一家蛋糕店,为了挽留客户,决定为贵宾卡里余额小于 20 元的客户一次性赠送 20 元,刺激消费者充值和消费。但条件是,每一位客户只能被赠送一次。此时,如果用户正好正在进行消费,就在赠予金额到账的同时,他进行了一次消费,使得总金额又小于 20 元,并且正好累计消费了 20 元。使得消费、赠予后的金额等于消费前、赠予前的金额。这时,后台的赠予进程就会误以为这个账户还没有赠予,所以,存在被多次赠予的可能。从这一段输出中,可以看到,这个账户被先后反复多次充值。其原因正是因为账户余额被反复修改,修改后的值等于原有的数值,使得 CAS 操作无法正确判断当前数据状态。


3.2.3.AtomicStampedReference


参考书籍《实战 JAVA 高并发程序设计》 葛一鸣

AtomicReference 无法解决上述问题的根本因为是对象在修改过程中,丢失了状态信息。对象值本身与状态被画上了等号。因此,我们只要能够记录对象在修改过程中的状态值,就可以很好地解决对象被反复修改导致线程无法正确判断对象状态的问题。

AtomicStampedReference 正是这么做的。它内部不仅维护了对象值,还维护了一个版本号(它可以使任何一个整数来表示状态值)。

AtomicStampedReference 对应的数值被修改时,除了更新数据本身外,还必须要更新时版本号。当 AtomicStampedReference 设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要版本号发生变化,就能防止不恰当的写入。


AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A -> B -> A -> C

通过 AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。

TestAtmoicStampedReference.java

@Slf4j(topic = "c.TestAtomicStampedReference")
public class TestAtmoicStampedReference {
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        log.debug("Main start ...");

        // 获取值
        String prev = ref.getReference();
        // 获取版本号
        int stamp = ref.getStamp();
        log.debug("版本号:{}", stamp);

        other();
        sleep(1);

        // 尝试改为 C
        log.debug("版本号:{}", stamp);
        log.debug("change A->C:{}", ref.compareAndSet(prev, "C", stamp, stamp + 1));

    }

    private static void other() {
        new Thread(() -> {
            int stamp = ref.getStamp();
            log.debug("版本号:{}", stamp);
            log.debug("change A->B:{}", ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1));
        }, "t1").start();

        sleep(0.5);

        new Thread(() -> {
            int stamp = ref.getStamp();
            log.debug("版本号:{}", stamp);
            log.debug("change B->A:{}", ref.compareAndSet(ref.getReference(), "A", stamp, stamp + 1));
        }, "t2").start();
    }
}

输出结果

22:45:44.452 [main] DEBUG c.TestAtomicStampedReference - Main start ...
22:45:44.464 [main] DEBUG c.TestAtomicStampedReference - 版本号:0
22:45:44.508 [t1] DEBUG c.TestAtomicStampedReference - 版本号:0
22:45:44.508 [t1] DEBUG c.TestAtomicStampedReference - change A->Btrue
22:45:45.015 [t2] DEBUG c.TestAtomicStampedReference - 版本号:1
22:45:45.015 [t2] DEBUG c.TestAtomicStampedReference - change B->Atrue
22:45:46.018 [main] DEBUG c.TestAtomicStampedReference - 版本号:0
22:45:46.018 [main] DEBUG c.TestAtomicStampedReference - change A->Cfalse

列举一下 AtomicStampedReference 的几个 API

// 比较设置。参数依次为:期望值、写入新值、期望版本号、新版本号
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) 

// 获得当前对象引用
public V getReference() 

// 获得当前版本号
public int getStamp()

// 设置当前对象引用和版本号
public void set(V newReference, int newStamp)

3.2.4.AtomicMarkableReference


但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了 AtomicMarkableReference

在这里插入图片描述

GarbageBag.java

public class GarbageBag {
    String desc;

    public GarbageBag(String desc) { this.desc = desc; }
    
    public void setDesc(String desc) { this.desc = desc; }
    
    @Override
    public String toString() {
        return super.toString() + " " + desc;
    }
}

TestAtomicMarkableReference.java

@Slf4j(topic = "c.TestAtomicMarkableReference")
public class TestAtomicMarkableReference {
    public static void main(String[] args) throws InterruptedException {
        GarbageBag bag = new GarbageBag("装满了垃圾");
        // 参数 2(mark)可以看作是一个标记,表示垃圾袋满了
        AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);

        log.debug("主线程 start...");
        
        GarbageBag prev = ref.getReference();
        log.debug(prev.toString());

        new Thread(() -> {
            log.debug("打扫卫生的线程 start...");
            bag.setDesc("空垃圾袋");
            while (!ref.compareAndSet(bag, bag, true, false)) { }
            log.debug(bag.toString());
        }).start();

        Thread.sleep(1000);
        
        log.debug("主线程想换一只新垃圾袋?");

        boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
        
        log.debug("换了么?" + success);
        log.debug(ref.getReference().toString());
    }
}

控制台输出

23:23:55.349 [main] DEBUG c.TestAtomicMarkableReference - 主线程 start...
23:23:55.360 [main] DEBUG c.TestAtomicMarkableReference - org.example.chapter06.GarbageBag@446cdf90 装满了垃圾
23:23:55.405 [Thread-0] DEBUG c.TestAtomicMarkableReference - 打扫卫生的线程 start...
23:23:55.405 [Thread-0] DEBUG c.TestAtomicMarkableReference - org.example.chapter06.GarbageBag@446cdf90 空垃圾袋
23:23:56.414 [main] DEBUG c.TestAtomicMarkableReference - 主线程想换一只新垃圾袋?
23:23:56.414 [main] DEBUG c.TestAtomicMarkableReference - 换了么?false
23:23:56.414 [main] DEBUG c.TestAtomicMarkableReference - org.example.chapter06.GarbageBag@446cdf90 空垃圾袋

3.3.原子数组


有时候我们想修改的不是引用本身,而是要修改引用对象里面的内容,典型的例子就是数组。

原子数组:AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray


下面的代码块涉及到了函数式接口,所以我们先于此回顾一下函数式接口的知识。

  • 函数式接口的定义:有且仅有一个抽象方法的接口。
  • @FunctionalInterface:用于编译期的错误检查。该注解只能标记在 “有且仅有一个抽象方法” 的接口上。

Java 8java.util.function 包下预定义了大量的函数式接口供我们使用
这里简单介绍一下其中的三个:SupplierConsumerFunction

  • Supplier 提供者。无中生有:()->结果
  • Function 函数,一个参数一个结果:(参数)->结果
    如果是两个参数一个结果的话,就是 BiFunction(参数1, 参数2) ->结果
  • Consumer 消费者。一个参数没结果:(参数1)->void
    如果是两个参数没结果的话,就是 BiConsumer (参数1, 参数2)->void

还有一个比较重要的接口是 Predicate 接口,它通常用于判断参数是否满足指定的条件。


问题代码

TestAtmoicArray.java

public class TestAtmoicArray {
    public static void main(String[] args) {
        demo(
                () -> new int[10],
                (array) -> array.length,
                (array, index) -> array[index]++,
                array -> System.out.println(Arrays.toString(array))
        );
    }

    private static <T> void demo(
            Supplier<T> arraySupplier, // 提供线程不安全/线程安全的数组
            Function<T, Integer> lengthFun, // 获取数组长度的方法
            BiConsumer<T, Integer> putConsumer, // 自增方法,回传 array 和 index
            Consumer<T> printConsumer // 打印数组的方法
    ) {
        List<Thread> threadList = new ArrayList<>();
        T array = arraySupplier.get();
        int length = lengthFun.apply(array);
        for (int i = 0; i < length; i++) {
            // 每个线程对数组做 10000 次操作
            threadList.add(new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    putConsumer.accept(array, j % length);
                }
            }));
        }
        threadList.forEach(t -> t.start()); // 启动所有线程
        threadList.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }); // 等所有线程结束
        printConsumer.accept(array);
    }
}

按照我们的预想,每个元素自增至 10000 次。但显然下面的输出结果不符合我们的预期。

[8341, 8381, 8347, 8370, 8372, 8346, 8358, 8363, 8353, 8365]

TestAtmoicArray.javademo() 中使用原子数组

demo(
        () -> new AtomicIntegerArray(10),
        (array) -> array.length(),// AtomicIntegerArray::length
        (array, index) -> array.getAndIncrement(index), // AtomicIntegerArray::getAndIncrement
        array -> System.out.println(array) // System.out::println
);

最终的输出结果符合预期

[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

3.4.字段更新器


字段更新器可以保护某个对象里的属性(成员变量)

AtomicReferenceFieldUpdaterAtomicIntegerFieldUpdaterAtomicLongFieldUpdater


  • 利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常。
Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type

Student.java

public class Student {
    public volatile String name;

    @Override
    public String toString() {
        return "Student{" + "name='" + name + '\'' + '}';
    }
}

TestAtomicFieldUpdater.java

public class TestAtomicFieldUpdater {
    public static void main(String[] args) {
        Student student = new Student();

        AtomicReferenceFieldUpdater updater =
                AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");

        System.out.println(updater.compareAndSet(student, null, "张三"));
        System.out.println(student);

    }
}

输出结果

true
Student{name='张三'}

若我们 DEBUG 这个程序,在这个过程中间修改字段内容:Student.name = "李四",那么最后的输出结果则是 “李四”

在这里插入图片描述


3.5.累加器性能比较


TestAdder.java

public class TestAdder {
    public static void main(String[] args) {
        System.out.println("---[AtomicInteger::getAndIncrement]---");
        for (int i = 0; i < 5; i++) {
            demo(
                    () -> new AtomicInteger(0),
                    (adder) -> adder.getAndIncrement() // AtomicInteger::getAndIncrement
            );
        }
        
        System.out.println("---[LongAdder::increment]---");
        for (int i = 0; i < 5; i++) {
            demo(
                    () -> new LongAdder(), // new LongAdder()
                    adder -> adder.increment() // LongAdder::increment
            );
        }
    }

    // 此处是用 Supplier 提供一个累加器对象,Consumer 执行一个累加操作
    private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
        T adder = adderSupplier.get();
        long start = System.nanoTime();
        List<Thread> ts = new ArrayList<>();

        // 4 个线程,每人累加 50 万
        for (int i = 0; i < 40; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    action.accept(adder);
                }
            }));
        }

        ts.forEach(t -> t.start()); // Thread::start
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        long end = System.nanoTime();
        System.out.println(adder + " cost:" + (end - start) / 1000_000);
    }
}

控制台输出

---[AtomicInteger::getAndIncrement]---
20000000 cost:395
20000000 cost:378
20000000 cost:329
20000000 cost:380
20000000 cost:374
---[LongAdder::increment]---
20000000 cost:55
20000000 cost:34
20000000 cost:49
20000000 cost:35
20000000 cost:39

性能提升的原因很简单,就是在有竞争时,设置多个累加单元。Therad-0 累加 Cell[0]Thread-1 累加 Cell[1] … … 最后再将结果汇总。这样它们在累加时,操作的是不同的共享变量(Cell,累加单元),因此减少了 CAS 重试失败,从而提高性能。


3.6.LongAdder 源码


LongAdder 是并发大师 @author Doug Lea 的作品,设计的非常精巧

LongAdder 类有几个关键域

// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;

// 基础值, 如果没有竞争, 则用 CAS 累加这个域
transient volatile long base;

// 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy;

补充知识:transient 修饰的变量是不能被序列化的


3.6.1.CAS 锁


演示用的代码(CAS 用于生产锁的一个原理)

/**
 * 这段代码仅仅用于演示,实际情况还请不要使用,实际操作远比这复杂
 * 比如这段代码块中的 while(true) 循环对性能来说就是一个不小的影响
 */
@Slf4j(topic = "c.LockCAS")
public class LockCAS {
    // 此处设定:0 表示没加锁状态,1 表示加锁状态
    private AtomicInteger state = new AtomicInteger(0);

    public void lock() {
        while (true) {
            if (state.compareAndSet(0, 1)) {
                break;
            }
        }
    }

    public void unlock() {
        log.debug("unlock ... ...");
        state.set(0);
    }

    public static void main(String[] args) {
        LockCAS lock = new LockCAS();

        new Thread(() -> {
            log.debug("begin...");
            lock.lock();
            try {
                log.debug("lock...");
                sleep(1);
            } finally {
                lock.unlock();
            }
        },"t1").start();

        new Thread(() -> {
            log.debug("begin...");
            lock.lock();
            try {
                log.debug("lock...");
            } finally {
                lock.unlock();
            }
        },"t2").start();
    }
}

输出

14:27:23.827 [t1] DEBUG c.LockCAS - begin...
14:27:23.827 [t2] DEBUG c.LockCAS - begin...
14:27:23.837 [t1] DEBUG c.LockCAS - lock...
14:27:24.846 [t1] DEBUG c.LockCAS - unlock ... ...
14:27:24.846 [t2] DEBUG c.LockCAS - lock...
14:27:24.846 [t2] DEBUG c.LockCAS - unlock ... ...

LongAdder 源码中的字段 cellsBusy,就类似于上面讲的 CAS 锁。

cellsBusy 将来可以用来作为加锁的一个标记(0 表示未加锁,1 表示加锁),以此来保护对某些资源访问时的线程安全。

LongAdder 的底层,当 cells 数组被创建,或者扩容的时候,就会用到 cellBusy 这个字段。


3.6.2.原理-缓存行伪共享


其中的 Cell 即为累加单元

java/util/concurrent/atomic/Striped64.java

@sun.misc.Contended // 该注解的作用是防止缓存行伪共享
static final class Cell {
    volatile long value;
    
    Cell(long x) { value = x; }

    // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
    final boolean cas(long prev, long next) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
    }
    
    // 省略不重要代码
}

要说缓存行的伪共享,那得从缓存说起。


我们于此处比较一下缓存与内存的速度。

在这里插入图片描述

从 CPU 到大约需要的时钟周期
寄存器1 cycle (4 GHz 的 CPU 约为 0.25 ns)
L1一级缓存3~4 cycle
L2二级缓存10~20 cycle
L3三级缓存40~45 cycle
内存120~240 cycle

因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。


缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)。

缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中。

CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效。

在这里插入图片描述

因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象。这样问题来了: 当 Core-0 要修改 Cell[0]Core-1 要修改 Cell[1] 时,无论谁修改成功,都会导致对方 Core 的缓存行失效。比如 Core-0Cell[0]=6000Cell[1]=8000 要做累加操作,最终变为 Cell[0]=6001Cell[1]=8000,这时 Core-1 的缓存行会失效,它又要重新去内存中找最新的值来同步。


@sun.misc.Contended 就是用来解决这个问题的,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效。

在这里插入图片描述


3.6.3.源码-add


源代码:java/util/concurrent/atomic/LongAdder.java

public void increment() {
    add(1L);
}

源代码:java/util/concurrent/atomic/LongAdder.java

public void add(long x) {
	// // as 为累加单元数组、b 为基础值、x 为累加值
    Cell[] as; long b, v; int m; Cell a;

	/* 
	 * 进入 if 的两个条件
	 * * 1. as 有值, 表示已经发生过竞争, 进入 if
	 * * 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if
 	*/
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true; // uncontended 表示 cell 没有竞争
        
        if (
            	// as 还没有创建
                as == null || (m = as.length - 1) < 0 ||
                        // 当前线程对应的 cell 还没有
                        (a = as[getProbe() & m]) == null ||
                        // CAS 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell )
                        !(uncontended = a.cas(v = a.value, v + x))
        )
            // 进入 cell 数组创建、cell 创建的流程
            longAccumulate(x, null, uncontended);
    }
    
}

cells 数组是懒惰创建的。一开始没有竞争的时候,其为 null;只有竞争发生的时候,它才会创建。

add 流程图

在这里插入图片描述


3.6.4.源码-longAccumulate


java/util/concurrent/atomic/Striped64.java(部分代码有所省略)

final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
    int h;
    
    // 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell
    if ((h = getProbe()) == 0) {
        // 初始化 probe
        ThreadLocalRandom.current();
        // h 对应新的 probe 值, 用来对应 cell
        h = getProbe();
        wasUncontended = true;
    }
    
    // collide 为 true 表示需要扩容
    boolean collide = false;
    
    for (; ; ) {
        Cell[] as;
        Cell a;
        int n;
        long v;
        
        // 已经有了 cells
        if ((as = cells) != null && (n = as.length) > 0) {
            // 还没有 cell
            if ((a = as[(n - 1) & h]) == null) {
                // 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x
                // 成功则 break, 否则继续 continue 循环
            }
            // 有竞争, 改变线程对应的 cell 来重试 cas
            else if (!wasUncontended)
                wasUncontended = true;
            // cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null
            else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
                break;
            // 如果 cells 长度已经超过了最大长度, 或者已经扩容, 改变线程对应的 cell 来重试 cas
            else if (n >= NCPU || cells != as)
                collide = false;
            // 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了
            else if (!collide)
                collide = true;
            // 加锁
            else if (cellsBusy == 0 && casCellsBusy()) {
                // 加锁成功, 扩容
                continue;
            }
            // 改变线程对应的 cell
            h = advanceProbe(h);
        }
        // 还没有 cells, 尝试给 cellsBusy 加锁
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            // 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell
            // 成功则 break;
        }
        // 上两种情况失败, 尝试给 base 累加
        else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
            break;
    }
}
longAccumulate 流程图-1

在这里插入图片描述

longAccumulate 流程图-2

在这里插入图片描述

longAccumulate 流程图-3

在这里插入图片描述


3.6.5.源码-sum


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;
}

4.Unsafe


Java\jdk\jre\lib\rt.jar!\sun\misc\Unsafe.class

public final class Unsafe {
    private static final Unsafe theUnsafe;
	... ...
}

显然,该类是单例模式,theUnsafe 是它的一个变量。


4.1.获取 Unsafe 对象


Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得

public class TestUnsafe {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        // 允许访问私有属性
        theUnsafe.setAccessible(true);
        // 该对象(unsafe)是静态的,静态成员变量从属于类,而不从属于对象,故此处传 null
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);

        System.out.println(unsafe);
    }
}

控制台输出

sun.misc.Unsafe@2f0e140b

4.2.CAS 相关应用


Teacher.java

@Data
public class Teacher {
    public volatile int id;
    public volatile String name;
}

TestUnsafe_2.java

public class TestUnsafe_2 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);

        // 1.获取域的偏移地址
        long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
        long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));

        Teacher t = new Teacher();

        // 2.执行 CAS 操作
        // 四个参数是:要操作的对象、要操作对象的的偏移量、已经获取得旧值、想修改成的新值
        unsafe.compareAndSwapInt(t, idOffset, 0, 1);
        unsafe.compareAndSwapObject(t, nameOffset, null, "张三");

        // 3.验证
        System.out.println(t);
    }
}

输出结果如下(显然,赋值成功)

Teacher(id=1, name=张三)

这里再提一下上述代码中出现的 offset 吧。
offset 是对象内的偏移量,其实就是一个字段到对象头部的偏移量,通过这个偏移量可以快速定位字段。


4.3.模拟实现原子整数


Account_1.java

public interface Account_1 {
    // 获取余额
    Integer getBalance();

    // 取款
    void withdraw(Integer amount);

    static void demo(Account_1 account_1) {
        List<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 1000; i++) {
            threads.add(new Thread(() -> {
                account_1.withdraw(10);
            }));
        }

        long startTime = System.nanoTime();

        threads.forEach(Thread::start);
        threads.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        long endTime = System.nanoTime();

        System.out.println(account_1.getBalance() + " cost:" + (endTime - startTime) / 1000_000 + " ms");
    }

}

UnsafeAccessor.java

public class UnsafeAccessor {
    static Unsafe unsafe;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
    }

    static Unsafe getUnsafe() {
        return unsafe;
    }
}

MyAtomicInteger.java

public class MyAtomicInteger implements Account_1 {
    private volatile int value;
    private static final long valueOffset;
    private static final Unsafe UNSAFE;

    static {
        UNSAFE = UnsafeAccessor.getUnsafe();
        try {
            valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    public int getValue() {
        return value;
    }

    public void decrement(int amount) {
        while (true) {
            int prev = this.value;
            int next = prev - amount;
            if (UNSAFE.compareAndSwapInt(this, valueOffset, prev, next)) {
                break;
            }
        }
    }

    public MyAtomicInteger(int value) {
        this.value = value;
    }

    @Override
    public Integer getBalance() {
        return getValue();
    }

    @Override
    public void withdraw(Integer amount) {
        decrement(amount);
    }
}

TestMyAtomicInteger.java

public class TestMyAtomicInteger {
    public static void main(String[] args) {
        Account_1.demo(new MyAtomicInteger(10000));
    }
}

输出结果

0 cost:66 ms

5.本章小结


  • CAS volatile
  • CAS 相关 API
    • 原子整数
    • 原子引用
    • 原子数组
    • 字段更新器
    • 原子累加器
  • Unsafe
  • 原理方面
    • LongAdder 源码
    • 伪共享

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

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

相关文章

CSS语法格式与三种引入方式

文章目录第一章——CSS简介1.1 CSS语法格式1.2 CSS 位置1.3 CSS引入方式1.3.1.行内样式表&#xff08;内联样式表&#xff09;1.3.2 外部样式表1.3.3 内部样式表第一章——CSS简介 1.1 CSS语法格式 CSS 规则由两个主要的部分构成&#xff1a;选择器以及一条或多条声明。 选择…

C语言全局变量和局部变量

局部变量定义在函数内部的变量称为局部变量&#xff08;Local Variable&#xff09;&#xff0c;它的作用域仅限于函数内部&#xff0c; 离开该函数后就是无效的&#xff0c;再使用就会报错。例如&#xff1a;intf1(int a){ int b,c;//a,b,c仅在函数f1()内有效 return abc; } i…

各种CV领域 Attention (原理+代码大全)

人类在处理信息时&#xff0c;天然会过滤掉不太关注的信息&#xff0c;着重于感兴趣信息&#xff0c;于是将这种处理信息的机制称为注意力机制。 注意力机制分类&#xff1a;软注意力机制&#xff08;全局注意&#xff09;、硬注意力机制&#xff08;局部注意&#xff09;、和…

打工人必知必会(三)——经济补偿金和赔偿金的那些事

目录 参考 一、经济补偿金&赔偿金-用人单位承担赔偿责任 1、月平均工资是税前还是税后工资&#xff1f; 3、经济补偿金是否要交个人所得税&#xff1f;如何交&#xff1f; 二、劳动者承担赔偿责任 三、劳动者需要特别注意 参考 《HR全程法律顾问&#xff1a;企业人力资…

Day12 XML配置AOP

1 前言前文我们已经介绍了AOP概念Day11 AOP介绍&#xff0c;并将其总结如下&#xff1a;2 AOP 标签和expression表达式学习<?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http://www.springframework.org/schema/beans"xmlns:x…

3.4只读存储器ROM

文章目录一、引子二、介绍1.MROM2.PROM3.EPROM4.Flash Memory5.SSD三、运行过程四、回顾一、引子 这一小节&#xff0c;我们学习只读存储器ROM。 上一小节&#xff0c;学习了两种RAM芯片&#xff0c;分别是SRAM和DRAM。详情请戳&#xff1a;3.3Sram和Dram RAM芯片可以支持随…

Pygame创建界面

今天开始对Python的外置包pygame进行学习&#xff0c;pygame是Python的游戏包&#xff0c;使用该包可以设计一些简单的小游戏。 前言 利用Python外置包创建一个简单界面&#xff0c;首先需要下载Python外置包pygame 使用语句&#xff1a;pip install pygame Display模块 创建…

红黑树知识点回顾

Rudolf Bayer 于1978年发明红黑树&#xff0c;在当时被称为对称二叉 B 树(symmetric binary B-trees)。后来&#xff0c;在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的红黑树。 红黑树具有良好的效率&#xff0c;它可在近似O(logN) 时间复杂度下完成插入、删除、…

实验五、任意N进制异步计数器设计

实验五 任意N进制异步计数器设计 实验目的 掌握任意N进制异步计数器设计的方法。 实验要求 一人一组&#xff0c;独立上机。在电脑上利用Multisim软件完成实验内容。 实验内容 说明任意N进制异步计数器的构成方法 设计过程 集成计数器一般都设有清零端和置数输入端&#xff…

3.7动态规划--图像压缩

3.6多边形游戏&#xff0c;多边形最优三角剖分类似&#xff0c;仅仅是最优子结构的性质不同&#xff0c;这个多边形游戏更加具有一般性。不想看了&#xff0c;跳过。 写在前面 明确数组含义&#xff1a; l: l[i]存放第i段长度, 表中各项均为8位长&#xff0c;限制了相同位数…

ElasticSearch - RestClient操作ES基本操作

目录 什么是RestClient hotel数据结构分析 初始化RestClient 创建索引库 删除索引库 判断索引库是否存在 小结 新增文档 查询文档 更新文档 删除文档 批量导入文档 小结 什么是RestClient ES官方提供了各种不同语言的客户端&#xff0c;用来操作ES这些客户端的本质…

Java基础语法——方法

目录 方法概述 方法定义及格式 方法重载 •方法重载概述 •方法重载特点 方法中基本数据类型和引用数据类型的传递 方法概述 ——假设有一个游戏程序&#xff0c;程序在运行过程中&#xff0c;要不断地发射炮弹(植物大战僵尸)。发射炮弹的动作需要编写100行的代码&…

五、在测试集上评估图像分类算法精度(Datawhale组队学习)

文章目录配置环境准备图像分类数据集和模型文件测试集图像分类预测结果表格A-测试集图像路径及标注表格B-测试集每张图像的图像分类预测结果&#xff0c;以及各类别置信度可视化测试集中被误判的图像测试集总体准确率评估指标常见评估指标混淆矩阵PR曲线绘制某一类别的PR曲线绘…

密码学的100个基本概念

密码学的100个基本概念一、密码学历史二、密码学基础三、分组密码四、序列密码五、哈希函数六、公钥密码七、数字签名八、密码协议九、密钥管理十、量子密码2022年主要完成了密码学专栏的编写&#xff0c;较为系统的介绍了从传统密码到现代密码&#xff0c;以及量子密码的相关概…

C语言函数声明以及函数原型

C语言代码由上到下依次执行&#xff0c;原则上函数定义要出现在函数调用之前&#xff0c;否则就会报错。但在实际开发中&#xff0c;经常会在函数定义之前使用它们&#xff0c;这个时候就需要提前声明。所谓声明&#xff08;Declaration&#xff09;&#xff0c;就是告诉编译器…

《网络编程实战》学习笔记 Day9

系列文章目录 这是本周期内系列打卡文章的所有文章的目录 《Go 并发数据结构和算法实践》学习笔记 Day 1《Go 并发数据结构和算法实践》学习笔记 Day 2《说透芯片》学习笔记 Day 3《深入浅出计算机组成原理》学习笔记 Day 4《编程高手必学的内存知识》学习笔记 Day 5NUMA内存知…

【论文翻译】Non-local Neural Networks

摘要 卷积运算和循环运算都是每次处理一个局部邻域的构建块。在本文中&#xff0c;我们将非局部操作作为一组用于捕获长期依赖关系的构建块。受计算机视觉中经典的非局部均值方法[4]的启发&#xff0c;我们的非局部运算将一个位置的响应计算为所有位置特征的加权和。这个构建块…

「自控原理」5.2 频域稳定判据、频域分析

本节介绍奈奎斯特稳定判据、对数稳定判据&#xff0c;并引入稳定裕度 本节介绍频率特性法分析系统性能 本节介绍通过开环频率特性得到闭环频率特性的方法 文章目录频域稳定判据奈奎斯特稳定判据ZP−2NZP-2NZP−2N奈奎斯特稳定判据的推导对数稳定判据容易判断出错的情况临界稳定…

第九层(4):STL之duque类

文章目录前情回顾deque类deque类的功能deque和vector的区别deque容器的内部图deque类内的构造函数deque类内的赋值操作deque类内的大小操作deque类内的插入操作deque类内的删除操作deque类内的单个访问下一座石碑&#x1f389;welcome&#x1f389; ✒️博主介绍&#xff1a;一…

设计模式 - 创建型模式_原型模式

文章目录创建型模式概述Case场景模拟⼯程Bad ImplBetter Impl &#xff08;原型模式重构代码&#xff09;创建型模式 创建型模式提供创建对象的机制&#xff0c; 能够提升已有代码的灵活性和可复⽤性。 类型实现要点工厂方法定义⼀个创建对象的接⼝&#xff0c;让其⼦类⾃⼰决…