java并发编程之美第四章读书笔记

news2024/11/18 2:58:10

第四章java并发包中原子操作类原理剖析

JUC包提供了一系列的原子类操作,这些类都是使用非阻塞算法CAS实现的,相比使用锁实现原子操作在性能上有很大提高

本章只讲解最简单的AtomicLong类的实现原理以及JDK8中新增的LongAdder和LongAccumulator类的原理

原子变量的操作类

AtomicLong,AtomicInteger,AtomicBoolean等原子类操作类,内部使用Unsafe来实现

public class AtomicLongTest extends Number implements Serializable {


    private static final long serialVersionUID= 1927816293512124184L;

    private static final Unsafe unsafe =Unsafe.getUnsafe();


    private static final long valueOffset;

    static final boolean VM_SUPPORTS_LONG_CAS= VMSupportsCS8();
    private static native boolean VMSupportsCS8();
    
    static {
        try{
            valueOffset = unsafe.objectFieldOffset(AtomicLongTest.class.getDeclaredField("value"));
            
        }catch (Exception e){
            throw new Error(e);
        }
        
    }
    private volatile long value;
    private  AtomicLongTest(long initiaValue){
        value=initiaValue;
    }
    @Override
    public int intValue() {
        return 0;
    }


    @Override
    public long longValue() {
        return 0;
    }


    @Override
    public float floatValue() {
        return 0;
    }


    @Override
    public double doubleValue() {
        return 0;
    }
}

递增和递减代码操作

boolean compareAndSet(long expect,long update)

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

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

public class AtomicTest {
    private static AtomicLong atomicLong=new AtomicLong();
    private static Integer[] arrayOne=new Integer[]{0,1,2,3,4,5,6,7,56,0};
    private static Integer[] arrayTwo=new Integer[]{10,1,2,3,4,5,6,0,56,0};

    public static void main(String[] args) throws InterruptedException{
        Thread threadOne=new Thread(new Runnable() {
            @Override
            public void run() {
                int size = arrayOne.length;
                for(int i=0;i<size;i++){
                    if(arrayOne[i].intValue()==0){
                        atomicLong.incrementAndGet();
                    }
                }
            }
        });

        Thread threadTwo=new Thread(new Runnable() {
            @Override
            public void run() {
                int size = arrayTwo.length;
                for(int i=0;i<size;i++){
                    if(arrayTwo[i].intValue()==0){
                        atomicLong.incrementAndGet();
                    }
                }
            }
        });


        threadOne.start();
        threadTwo.start();
        threadOne.join();
        threadTwo.join();
        System.out.println("count 0:"+atomicLong.get());

        
    }
}

JDK8新增的原子操作类LongAdder()方法

简单介绍

前面提到过了AtomicLong是通过CAS提供的非阻塞的原子性操作,相比阻塞算法的同步器来说性能已经很好了,但是在高并发下大量线程同时去竞争同一个原子变量,由于同时只有一个线程凯跃操作成功,这样就造成了大量线程竞争失败后,会通过无限循环不断进行自选操作尝试CAS,白白浪费了CPU资源

使用LongAddr时候会在内部维护多个Cell变量,每个Cell里面有一个初始为零的long变量,在同等的并发量下,争夺单个变量更新操作的线程会减小,变相的减少了争夺共享资源的并发量

当多个线程在争夺同一个cell变量失败后,并不是在当前的cell变量上一直自旋CAS重试,而是在其他Cell变量上尝试进行CAS,这个改变增加了当前线程重试CAS的成功的可能性,最后在获取LongAdder的值的时候,是把所有的Cell变量的值累加再加上Base返回的

LongAdder 维护了一个延迟初始化的原子性更新数组 (默认情况下 Cell 数组是 nu和一个基值变量 base。由于 Cells 占用的内存是相对比较大的,所以一开始并不创建它而是在需要时创建,也就是惰性加载。

当一开始判断 Cell 数组是 null 并且并发线程较少时,所有的累加操作都是对 base变量进行的。保持 Cell 数组的大小为2的N次方,在初始化时 Cel 数组中的 Cel元素个数为2,数组里面的变量实体是 Cell 类型。Cell 类型是 AtomicLong 的一个改进,用来减少缓存的争用,也就是解决伪共享问题。

对于大多数孤立的多个原子操作进行字节填充是浪费的,因为原子性操作都是无规律地分散在内存中的(也就是说多个原子性变量的内存地址是不连续的),多个原子变量被放入同一个缓存行的可能性很小。但是原子性数组元素的内存地址是连续的,所以数组内的多个元素能经常共享缓存行,因此这里使用 @sun.misc.Contended 注解对 Cell类进行字节填充,这防止了数组中多个元素共享一个缓存行,在性能上是一个提升。

代码分析

  • (1)LongAdder 的结构是怎样的?
  • (2)当前线程应该访问 Cell 数组里面的哪一个 Cell 元素?
  • (3)如何初始化 Cell 数组?
  • (4) Cell 数组如何扩容?
  • (5) 线程访问分配的 Cel 元素有冲突后如何处理?
  • (6)如何保证线程操作被分配的 Cell 元素的原子性?

cell的构造

@sun.misc.Contended static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }

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

可以看到,Cell 的构造很简单,其内部维护一个被声明为 volatile 的变量,这里声为volatile是因为线程操作 value 变量时没有使用锁,为了保证变量的内存可见性这里格声明为volatie的。另外cas 函数通过CAS 操作,保证了当前线程更新时被分配的Cell元素中value值的原子性。另外,Cell 类使用@sun.misc.Contended 修饰是为了避免伪共享

  • ·long sum() 返回当前的值,内部操作是累加所有 Cell内部的 value 值后再累加 bas例如下面的代码,由于计算总和时没有对 Cell 数组进行加锁,所以在累加过程可能有其他线程对Cell 中的值进行了修改,也有可能对数组进行了扩容,所以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;
}
  • void reset() 为重置操作,如下代码把 base 置为 0,如果 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 sumThenReset0是 sum 的改造版本,如下代码在使用sum累加对应的Cell值后把当前 Cell 的值重置为 0,base 重置为0。这样,当多线程调用该方法时会有问题比如考虑第一个调用线程清空 Cell 的值,则后一个线程调用时累加的都是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;
}

long longValue0等价于 sum0)。

  • 下面主要看下 add 方法的实现,从这个方法里面就可以找到其他问题的答案。
public void add(long x) {
    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);
    }
}
final boolean casBase(long cmp, long val){
    return UNSAFE.compareAndSwapLong(this,BASE,cmp, val);
}

代码 (1)首先看 cells 是否为null,如果为 null 则当前在基础变量 base 上进行累加,这时候就类似AtomicLong 的操作。

如果cells 不为 null 或者线程执行代码(1)的 CAS 操作失败了,则会去执行代码(2),代码(2)(3)决定当前线程应该访问 cells 数组里面的哪一个 Cell 元素,如果当前线程射的元素存在则执行代码 (4),使用CAS 操作去更新分配的 Cell 元素的 value 值,如当前线程映射的元素不存在或者存在但是 CAS 操作失败则执行代码(5)。其实将代码(2(3)(4)合起来看就是获取当前线程应该访问的 cells 数组的 Cell 元素,然后进行 CAS更新操作,只是在获取期间如果有些条件不满足则会跳转到代码(5) 执行。另外当前线程应该访问 cells数组的哪一个Cell元素是通过getProbe0)& m进行计算的,其中m是当cells 数组元素个数 -1,getProbe0 则用于获取当前线程中变量 threadLocalRandomProbe值,这个值一开始为0,在代码(5)里面会对其进行初始化。并且当前线程通过分配的Cell 元素的cas 函数来保证对 Cell 元素 value 值更新的原子性,到这里我们回答了问题2和问题6。

该代码为cells数组初始化和扩容的代码

final void longAccumulate(long x, LongBinaryOperator fn,
                          boolean wasUncontended) {
    int h;
    if ((h = getProbe()) == 0) {
        ThreadLocalRandom.current(); // force initialization
        h = getProbe();
        wasUncontended = true;
    }
    boolean collide = false;                // True if last slot nonempty
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        if ((as = cells) != null && (n = as.length) > 0) {
            if ((a = as[(n - 1) & h]) == null) {
                if (cellsBusy == 0) {       // Try to attach new Cell
                    Cell r = new Cell(x);   // Optimistically create
                    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;           // Slot is now non-empty
                    }
                }
                collide = false;
            }
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                         fn.applyAsLong(v, x))))
                break;
            else if (n >= NCPU || cells != as)
                collide = false;            // At max size or stale
            else if (!collide)
                collide = true;
            else if (cellsBusy == 0 && casCellsBusy()) {
                try {
                    if (cells == as) {      // Expand table unless stale
                        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;                   // Retry with expanded table
            }
            h = advanceProbe(h);
        }
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false;
            try {                           // Initialize table
                if (cells == as) {
                    Cell[] rs = new Cell[2];
                    rs[h & 1] = new Cell(x);
                    cells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        else if (casBase(v = base, ((fn == null) ? v + x :
                                    fn.applyAsLong(v, x))))
            break;                          // Fall back on using base
    }
}

在被初始化或者扩容,或者当前在创建新的 Cell 元素、通过 CAS 操作来进行0或 1状态的切换,这里使用 casCellsBusy 函数。假设当前线程通过 CAS 设置 ellsBusy 为1,则前线程开始初始化操作,那么这时候其他线程就不能进行扩容了。如代码 《14.1)初始化cells 数组元素个数为 2,然后使用 h&1 计算当前线程应该访 el 数组的哪个位置,也就是使用当前线程的 threadLocalRandomProbe 变量值& (cells 数组元素个数 -1),然后标示cels 数组已经被初始化,最后代码 (14.3) 重置了 ellsBusy 标记。显然这里没有使用CAS 操作,却是线程安全的,原因是 cellsBusy 是 volatile 类型的,这保证了变量的内存可见性,另外此时其他地方的代码没有机会修改 cellsBusy 的值。在这里初始化的 cells 数组里面的两个元素的值目前还是 null。这里回答了问题 3,知道了 cells 数组如何被初始化。

cells数组的扩容是在代码(12)中进行的,对 cells 扩容是有条件的,也就是代码(10)(11)的条件都不满足的时候。具体就是当前 cells 的元素个数小于当前机器 CPU 个数并目当前多个线程访问了 cells 中同一个元素,从而导致冲突使其中一个线程 CAS 失败时才会进行扩容操作。这里为何要涉及 CPU 个数呢?其实在基础篇中已经讲过,只有当每个 CPU都运行一个线程时才会使多线程的效果最佳,也就是当 cells 数组元素个数与 CPU个数一致时,每个 Cell 都使用一个 CPU 进行处理,这时性能才是最佳的。代码(12)中的扩容操作也是先通过 CAS 设置 cellsBusy 为1,然后才能进行扩容。假设 CAS 成功则执行代码(12.1)将容量扩充为之前的 2 倍,并复制 Cell 元素到扩容后数组。另外,扩容后 cells 数组里面除了包含复制过来的元素外,还包含其他新元素,这些元素的值目前还是 null。这里回答了问题4。

在代码(7)(8)中,当前线程调用add 方法并根据当前线程的随机数threadLocalRandomProbe 和 cells 元素个数计算要访问的 Cell元素下标,然后如果发现对应下标元素的值为 null,则新增一个 Cell 元素到 cells 数组,并且在将其添加到 cells 数组之前要竞争设置 cellsBusy 为 1。

代码(13)对CAS失败的线程重新计算当前线程的随机值 threadLocalRandomProbe,以减少下次访问 cells 元素时的冲突机会。这里回答了问题 5。

小结

介绍了新增的JDK8中新增的LongAdder原子操作类,该类通过内部cells数组分担了高并发下多线程同时对一个原子变量进行更新时的竞争量,让多个线程可以同时对cells数组的元素进行操作,数组元素cell使用

@sun.misc.Contended注解进行修饰,这避免了cells数组内多个原子变量被放入同一个缓存行,也就避免了伪共享

LongAccumulator类原理探究

LongAdder 类是 LongAccumulator 的一个特例,LongAccumulator 比 LongAdder 的更强大。

其中accumulatorFunction 是一个双目运算器接口,其根输入的两个参数返回一个计算值,identity 则是 LongAccumulator 累加器的初始值

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

调用 LongAdder 就相于使用下面的方式调用 LongAccumulator:

调用 LongAdder 就相于使用下面的方式调用 LongAccumulator:

public interface LongBinaryOperator{
        long applyAsLong(long left,long right);
    }
    LongAdder adder new LongAdder();
    LongAccumulator accumulator  =new LongAccumulator(new LongBinaryOperator(){
        @Override
        public long applyhsLong(long left, long right) {
            return left + right;
        }
        
},0);

LongAccumulator 相比于 LongAdder,可以为累加器提供非 0的初始值,后者只能提供默认的0值。另外,前者还可以指定累加规则,比如不进行累加而进行相乘,只需要在构造LongAccumulator时传入自定义的双目运算器即可,后者则内置累加的规则。

从下面代码我们可以知道,LongAccumulator 相比于 LongAdder 的不同在于,在调用caseBase时,后者传递的是b+x,前者使用了r=function.ApplyAsLong(b=base,x)来计算

public void add(long x) {
    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);
    }
}
public void accumulate(long x) {
    Cell[] as; long b, v, r; int m; Cell a;
    if ((as = cells) != null ||
        (r = function.applyAsLong(b = base, x)) != b && !casBase(b, r)) {
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[getProbe() & m]) == null ||
            !(uncontended =
              (r = function.applyAsLong(v = a.value, x)) == v ||
              a.cas(v, r)))
            longAccumulate(x, function, uncontended);
    }
}

前者调用的时候传递的是function,后者是null

当fn为null时候就使用v+x加法运算,这时候等价于LongAdder,当fn不为null时候则使用传递fn函数计算

else if (caseBase(v = base, ((fn = null) ? v + x : fn.applyAsLong(v, x))))
        break;
}

总结:本节简单介绍了LongAccumluator的原理,LongAdder是LongAccumluator的一个特例,只是后者提供更加强大的功能,可以让用户自定义累加规则

总结

本章介绍了并发包中的原子性操作类,这些类都是使用非阻塞算法 CAS 实现的,这相比使用锁实现原子性操作在性能上有很大提高。首先讲解了最简单的AtomicLong 类的实现原理,然后讲解了JDK 8中新增的 LongAdder 类和 LongAccumulator 类的原理。学习完本章后,希望读者在实际项目环境中能因地制宜地使用原子性操作类来提升系统性能。

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

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

相关文章

java并发编程之美第三章读书笔记

java并发包中ThreadLocalRandom类原理剖析 该ilei是JDK7在JUC包下面新增的随机数生成器吗,弥补了Random类在多线程下的缺陷 Random类及其局限性 public class RandomTest {public static void main(String[] args) {Random randomnew Random();for (int i0;i<10;i){Syste…

Facebook 用户量十分庞大,为什么还使用 MySQL 数据库?

Facebook是一个拥有超30亿用户的互联网公司&#xff0c;拥有海量数据&#xff0c;而且增长很快&#xff0c;单机数据库完全无法满足这种需求。 这时需要对数据进行分片&#xff0c;存储到多个数据库节点中&#xff0c;这个时候如果使用IOE作为其中一个节点&#xff0c;肯定可以…

MySQL Order by对各种排序算法的应用

通常我们实现的排序算法&#xff0c;都是在”纯内存“环境中进行。 MySQL 作为数据库难道是在先将所有要排序的数据加载到内存&#xff0c;再应用排序算法吗&#xff1f; 一、什么是内排序&#xff1f;什么是外排序&#xff1f; 内排序&#xff1a;全称为内部排序。内部排序是…

力扣题库刷题笔记647-回文子串

1、题目如下&#xff1a; 2、个人Python代码实现 思路如下&#xff1a; a、以切片的形式&#xff0c;判断每个子字符串是否为回文字符串。这里如何确定切片的起始下标就很重要了 b、首先需要知道的是字符串s&#xff0c;s[i,j]&#xff0c;指的是从下标i开始&#xff0c;到下标…

cout源码浅析

目录 cout源码浅析 那么对于没有定义在这之中的要怎么办呢&#xff1f; 实际使用 结语 首先来看我从cplusplus中截取的这张图&#xff1a; 注意最下面这一行字。cout其实是ostream的一个标准对象object。而上面则演示了一些继承关系。 好的&#xff0c;理解了之后&#xf…

算法DAY52 动态规划10 300.最长递增子序列 674. 最长连续递增序列 718. 最长重复子数组

300.最长递增子序列 五部曲&#xff1a; 1、dp数组的含义&#xff1a; dp[ i ] : 代表 截至到nums[i] (包括 nums[i]) 的序列中&#xff0c;以nums[i] 结尾的&#xff0c;最长递增子序列的长度。这里强调以nums[i] 结尾&#xff0c;是因为还要跟nums[j]做对比&#xff0c;确定…

ACG-crcme1(★★★)

运行程序 info exit 查壳 没壳 载入OD分析 刚载入OD发现要使用 ACG.key 搜一下字符串看看 发现这有貌似成功相关的字符串 进去看看 可以找到关键跳 爆破的话直接在这就可以完成 上面就该是算法了 算法分析 开始先判断文件存在和文件内容大小 读取文件内容&am…

微前端 qiankun@2.10.5 源码分析(二)

微前端 qiankun2.10.5 源码分析&#xff08;二&#xff09; 我们继续上一节的内容。 loadApp 方法 找到 src/loader.ts 文件的第 244 行&#xff1a; export async function loadApp<T extends ObjectType>(app: LoadableApp<T>,configuration: FrameworkConfi…

uniapp - 实现微信小程序电子签名板,横屏手写姓名签名专用写字画板(详细运行示例,一键复制开箱即用)

效果图 实现了在uniapp项目中,微信小程序平台流畅的写字签名板(也可以绘图)功能源码,复制粘贴,改改样式几分钟即可搞定! 支持自动横屏、持预览,真机运行测试非常流畅不卡顿。 基础模板 如下代码所示。 <template><view class=

vue3.2+vite+vant4+sass搭建笔记

1、确定node版本 1、下载nvm安装包 官方下载地址&#xff1a;https://github.com/coreybutler/nvm-windows/releases 双击安装 2、在node官网下载安装多个node 3、切换node 2、创建项目 1、安装依赖 pnpm i 2、启动项目 npm run dev 3、配置指向src import { defineC…

FAST协议解析2 FIX Fast Tutorial翻译【PMap、copy操作符】

FIX Fast Tutorial FIX Fast教程 &#xff08;译注&#xff1a;本篇是对https://jettekfix.com/education/fix-fast-tutorial/翻译和解释&#xff0c;除了文本的直接翻译外&#xff0c;我还针对各点按我的理解进行了说明和验证&#xff0c;所以可以看到译文下会有很多译注&am…

虹科方案 | HK-Edgility:将 SASE 带到边缘

通过上期的文章&#xff0c;我们了解到虹科HK-Edgility软件系统《面向未来的安全SD-WAN》的解决方案。本篇文章&#xff0c;我们将带您了解虹科系统在SASE的方案简介。 一、时代背景 向软件即服务 (SaaS) 和云原生应用程序的过渡&#xff0c;加上越来越多的远程用户生成和访问公…

快来参与:2023全国大数据与计算智能挑战赛正在报名中

全国大数据与计算智能挑战赛是由国防科技大学系统工程学院大数据与决策实验室组织的年度赛事活动&#xff0c;旨在深入挖掘大数据应用实践中亟需破解的能力生成难题、选拔汇聚数据领域优势团队、促进大数据领域的技术创新和面向需求的成果生成、推动形成“集智众筹、联合攻关、…

Spring项目的创建与使用

一、创建Spring项目 这里使用Maven方式创建Spring项目&#xff0c;分为以下三步&#xff1a; 创建一个普通的Maven项目添加spring框架支持添加启动类 注&#xff1a;这里创建的是一个spring的core项目&#xff0c;不是web项目&#xff0c;只需要main方法&#xff0c;不需要t…

Ubuntu显示美化 优化 常用插件

Ubuntu显示美化 优化 常用插件 1. 安装 Extension Manager2. 网速显示&#xff08;不显示总流量记得关掉&#xff09;3. 顶部透明度4. 左侧dock导航透明度5. 过渡动画2022-01-22 毛玻璃效果 和 程序启动背景墙效果2022-01-23 窗口预览&#xff08;类windos多窗口&#xff09;20…

C++11实现线程池

1.所有权的传递 适用移动语义可以将一个unique_lock赋值给另一个unique_lock,适用move实现。 void myThread1() {unique_lock<mutex> myUnique (testMutex1,std::defer_lock);unique_lock<mutex>myUnique1(std::move(myUnique));//myUnique 则实效 myUnique1 相当…

在Linux中进行Jenkins部署(maven-3.9.1+jdk11)

Jenkins部署在公网IP为x.x.x.x的服务器上 maven-3.9.1要安装在jdk11环境中 环境准备 第一步&#xff0c;下载jdk-11.0.19_linux-x64_bin.tar.gz安装包。 登录地址&#xff1a;Java Downloads | Oracle 下载jdk-11.0.19_linux-x64_bin.tar.gz安装包&#xff0c;然后使用Win…

电子温湿度记录仪

电子温湿度记录仪&#xff1a;实时监测环境温度和湿度电子温湿度记录仪是一种用于实时监测环境温度和湿度的设备。它广泛应用于医疗、制药、食品加工、仓储、博物馆、实验室等领域&#xff0c;以确保环境温湿度处于合适的范围内&#xff0c;以保持物品和设备的稳定性和安全性。…

信号的产生——tripuls函数

信号的产生——tripuls函数, 功能&#xff1a;产生非周期三角波信号&#xff0c;其调用格式如下&#xff1a; &#xff08;1&#xff09;ytripuls(t)&#xff0c; &#xff08;2&#xff09;ytripuls(t,w)&#xff0c; &#xff08;3&#xff09;ytripuls(t,w,s)&#xff0…

Java多线程入门到精通学习大全?深入了解线程:生命周期、状态和优先级!(第二篇:线程的基础知识学习)

本文详细介绍了线程的基础知识&#xff0c;包括什么是线程、线程的生命周期、线程的状态和线程优先级等。在了解这些知识后&#xff0c;我们能够更好地掌握线程的使用方式&#xff0c;提高程序的并发性和效率。如果您对线程有更深入的问题&#xff0c;也欢迎向我们提问。 1. 什…