JUC第十六讲:JUC集合: CopyOnWriteArrayList详解

news2024/11/18 23:45:08

JUC第十六讲:JUC集合: CopyOnWriteArrayList详解

本文是JUC第十六讲,JUC集合: CopyOnWriteArrayList详解。CopyOnWriteArrayList是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的拷贝来实现的。COW模式的体现

文章目录

  • JUC第十六讲:JUC集合: CopyOnWriteArrayList详解
    • 1、带着BAT大厂的面试问题去理解
    • 2、CopyOnWriteArrayList 源码分析
      • 2.1、类的继承关系
      • 2.2、类的内部类
      • 2.3、类的属性
      • 2.4、类的构造函数
      • 2.5、核心函数分析
        • 1、copyOf函数
        • 2、add函数
        • 3、addIfAbsent方法
        • 4、set函数
        • 5、remove函数
    • 3、CopyOnWriteArrayList示例
    • 4、更深入理解
      • 4.1、CopyOnWriteArrayList 的缺陷和使用场景
      • 4.2、CopyOnWriteArrayList为什么并发安全且性能比Vector好?
    • 5、参考文章

1、带着BAT大厂的面试问题去理解

请带着这些问题继续后文,会很大程度上帮助你更好的理解相关知识点。

  • 请说说非并发集合中Fail-fast机制?
  • 为什么说ArrayList查询快而增删慢? 被面过
    • ArrayList底层是数组,所以查询的时候直接根据索引可以很快地找到对应地元素,改也是如此,找到index对应元素进行替换。而增加和删除就涉及到数组元素地移动,所以会比较慢。
  • 对比ArrayList说说CopyOnWriteArrayList的增删改查实现原理? COW基于拷贝
  • 说下弱一致性的迭代器原理是怎么样的? COWIterator<E>
  • CopyOnWriteArrayList为什么并发安全且性能比Vector好?
  • CopyOnWriteArrayList有何缺陷,说说其应用场景?

2、CopyOnWriteArrayList 源码分析

CopyOnWriteArrayList使用的数据结构是数组。结构如下

在这里插入图片描述

2.1、类的继承关系

CopyOnWriteArrayList 实现了List接口,List接口定义了对列表的基本操作;同时实现了RandomAccess接口,表示可以随机访问(数组具有随机访问的特性);同时实现了Cloneable接口,表示可克隆;同时也实现了Serializable接口,表示可被序列化。

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}

2.2、类的内部类

  • COWIterator类

COWIterator表示迭代器,其也有一个Object类型的数组作为CopyOnWriteArrayList数组的快照,这种快照风格的迭代器方法在创建迭代器时使用了对当时数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(remove、set 和 add)不受支持。这些方法将抛出 UnsupportedOperationException。

static final class COWIterator<E> implements ListIterator<E> {
    /** Snapshot of the array */
    // 快照
    private final Object[] snapshot;
    /** Index of element to be returned by subsequent call to next.  */
    // 游标
    private int cursor;
    // 构造函数
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }
    // 是否还有下一项
    public boolean hasNext() {
        return cursor < snapshot.length;
    }
    // 是否有上一项
    public boolean hasPrevious() {
        return cursor > 0;
    }
    // next项
    @SuppressWarnings("unchecked")
    public E next() {
        if (! hasNext()) // 不存在下一项,抛出异常
            throw new NoSuchElementException();
        // 返回下一项
        return (E) snapshot[cursor++];
    }

    @SuppressWarnings("unchecked")
    public E previous() {
        if (! hasPrevious())
            throw new NoSuchElementException();
        return (E) snapshot[--cursor];
    }
    
    // 下一项索引
    public int nextIndex() {
        return cursor;
    }
    
    // 上一项索引
    public int previousIndex() {
        return cursor-1;
    }

    /**
        * Not supported. Always throws UnsupportedOperationException.
        * @throws UnsupportedOperationException always; {@code remove}
        *         is not supported by this iterator.
        */
    // 不支持remove操作
    public void remove() {
        throw new UnsupportedOperationException();
    }

    /**
        * Not supported. Always throws UnsupportedOperationException.
        * @throws UnsupportedOperationException always; {@code set}
        *         is not supported by this iterator.
        */
    // 不支持set操作
    public void set(E e) {
        throw new UnsupportedOperationException();
    }

    /**
        * Not supported. Always throws UnsupportedOperationException.
        * @throws UnsupportedOperationException always; {@code add}
        *         is not supported by this iterator.
        */
    // 不支持add操作
    public void add(E e) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        Object[] elements = snapshot;
        final int size = elements.length;
        for (int i = cursor; i < size; i++) {
            @SuppressWarnings("unchecked") E e = (E) elements[i];
            action.accept(e);
        }
        cursor = size;
    }
}

2.3、类的属性

属性中有一个可重入锁,用来保证线程安全访问,还有一个Object类型的数组,用来存放具体的元素。当然,也使用到了反射机制和CAS来保证原子性的修改lock域。

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 版本序列号
    private static final long serialVersionUID = 8673264195747942595L;
    // 可重入锁
    final transient ReentrantLock lock = new ReentrantLock();
    // 对象数组,用于存放元素
    private transient volatile Object[] array;
    // 反射机制
    private static final sun.misc.Unsafe UNSAFE;
    // lock域的内存偏移量
    private static final long lockOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = CopyOnWriteArrayList.class;
            lockOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("lock"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

2.4、类的构造函数

  • 默认构造函数
public CopyOnWriteArrayList() {
    // 设置数组
    setArray(new Object[0]);
}
  • CopyOnWriteArrayList(Collection<? extends E>)型构造函数  该构造函数用于创建一个按 collection 的迭代器返回元素的顺序包含指定 collection 元素的列表。
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class) // 类型相同
        // 获取c集合的数组
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else { // 类型不相同
        // 将c集合转化为数组并赋值给elements
        elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elements.getClass() != Object[].class) // elements类型不为Object[]类型
            // 将elements数组转化为Object[]类型的数组
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    // 设置数组
    setArray(elements);
}

该构造函数的处理流程如下

  • 判断传入的集合c的类型是否为CopyOnWriteArrayList类型,若是,则获取该集合类型的底层数组(Object[]),并且设置当前CopyOnWriteArrayList的数组(Object[]数组),进入步骤③;否则,进入步骤②

  • 将传入的集合转化为数组elements,判断elements的类型是否为Object[]类型(toArray方法可能不会返回Object类型的数组),若不是,则将elements转化为Object类型的数组。进入步骤③

  • 设置当前CopyOnWriteArrayList的Object[]为elements。

  • CopyOnWriteArrayList(E[])型构造函数

该构造函数用于创建一个保存给定数组的副本的列表。

public CopyOnWriteArrayList(E[] toCopyIn) {
    // 将toCopyIn转化为Object[]类型数组,然后设置当前数组
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

2.5、核心函数分析

对于CopyOnWriteArrayList的函数分析,主要明白Arrays.copyOf方法即可理解CopyOnWriteArrayList其他函数的意义。

1、copyOf函数

该函数用于复制指定的数组,截取或用 null 填充(如有必要),以使副本具有指定的长度。

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    // 确定copy的类型(将newType转化为Object类型,将Object[].class转化为Object类型,判断两者是否相等,若相等,则生成指定长度的Object数组
    // 否则,生成指定长度的新类型的数组)
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    // 将original数组从下标0开始,复制长度为(original.length和newLength的较小者),复制到copy数组中(也从下标0开始)
    System.arraycopy(original, 0, copy, 0,
                        Math.min(original.length, newLength));
    return copy;
}
2、add函数
public boolean add(E e) {
    // 可重入锁
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 元素数组
        Object[] elements = getArray();
        // 数组长度
        int len = elements.length;
        // 复制数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 存放元素e
        newElements[len] = e;
        // 设置数组
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

此函数用于将指定元素添加到此列表的尾部,处理流程如下

  • 获取锁(保证多线程的安全访问),获取当前的Object数组,获取Object数组的长度为length,进入步骤②。
  • 根据Object数组复制一个长度为length+1的Object数组为newElements(此时,newElements[length]为null),进入下一步骤。
  • 将下标为length的数组元素newElements[length]设置为元素e,再设置当前Object[]为newElements,释放锁,返回。这样就完成了元素的添加。
3、addIfAbsent方法

该函数用于添加元素(如果数组中不存在,则添加;否则,不添加,直接返回),可以保证多线程环境下不会重复添加元素。

private boolean addIfAbsent(E e, Object[] snapshot) {
    // 重入锁
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 获取数组
        Object[] current = getArray();
        // 数组长度
        int len = current.length;
        if (snapshot != current) { // 快照不等于当前数组,对数组进行了修改
            // Optimize for lost race to another addXXX operation
            // 取较小者
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++) // 遍历
                if (current[i] != snapshot[i] && eq(e, current[i])) // 当前数组的元素与快照的元素不相等并且e与当前元素相等
                    // 表示在snapshot与current之间修改了数组,并且设置了数组某一元素为e,已经存在
                    // 返回
                    return false;
            if (indexOf(e, current, common, len) >= 0) // 在当前数组中找到e元素
                    // 返回
                    return false;
        }
        // 复制数组
        Object[] newElements = Arrays.copyOf(current, len + 1);
        // 对数组len索引的元素赋值为e
        newElements[len] = e;
        // 设置数组
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

该函数的流程如下:

① 获取锁,获取当前数组为current,current长度为len,判断数组之前的快照snapshot是否等于当前数组current,若不相等,则进入步骤②;否则,进入步骤④

② 不相等,表示在snapshot与current之间,对数组进行了修改(如进行了add、set、remove等操作),获取长度(snapshot与current之间的较小者),对current进行遍历操作,若遍历过程发现snapshot与current的元素不相等并且current的元素与指定元素相等(可能进行了set操作),进入步骤⑤,否则,进入步骤③

③ 在当前数组中索引指定元素,若能够找到,进入步骤⑤,否则,进入步骤④

④ 复制当前数组current为newElements,长度为len+1,此时newElements[len]为null。再设置newElements[len]为指定元素e,再设置数组,进入步骤⑤

⑤ 释放锁,返回。

4、set函数

此函数用于用指定的元素替代此列表指定位置上的元素,也是基于数组的复制来实现的。

public E set(int index, E element) {
    // 可重入锁
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 获取数组
        Object[] elements = getArray();
        // 获取index索引的元素
        E oldValue = get(elements, index);

        if (oldValue != element) { // 旧值等于element
            // 数组长度
            int len = elements.length;
            // 复制数组
            Object[] newElements = Arrays.copyOf(elements, len);
            // 重新赋值index索引的值
            newElements[index] = element;
            // 设置数组
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            // 设置数组
            setArray(elements);
        }
        // 返回旧值
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}
5、remove函数

此函数用于移除此列表指定位置上的元素。

public E remove(int index) {
    // 可重入锁
    final ReentrantLock lock = this.lock;
    // 获取锁
    lock.lock();
    try {
        // 获取数组
        Object[] elements = getArray();
        // 数组长度
        int len = elements.length;
        // 获取旧值
        E oldValue = get(elements, index);
        // 需要移动的元素个数
        int numMoved = len - index - 1;
        if (numMoved == 0) // 移动个数为0
            // 复制后设置数组
            setArray(Arrays.copyOf(elements, len - 1));
        else { // 移动个数不为0
            // 新生数组
            Object[] newElements = new Object[len - 1];
            // 复制index索引之前的元素
            System.arraycopy(elements, 0, newElements, 0, index);
            // 复制index索引之后的元素
            System.arraycopy(elements, index + 1, newElements, index,
                                numMoved);
            // 设置索引
            setArray(newElements);
        }
        // 返回旧值
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

处理流程如下

① 获取锁,获取数组elements,数组长度为length,获取索引的值elements[index],计算需要移动的元素个数(length - index - 1),若个数为0,则表示移除的是数组的最后一个元素,复制elements数组,复制长度为length-1,然后设置数组,进入步骤③;否则,进入步骤②

② 先复制index索引前的元素,再复制index索引后的元素,然后设置数组。

③ 释放锁,返回旧值。

3、CopyOnWriteArrayList示例

下面通过一个示例来了解CopyOnWriteArrayList的使用:

  • 在程序中,有一个PutThread线程会每隔50ms就向CopyOnWriteArrayList中添加一个元素,并且两次使用了迭代器,迭代器输出的内容都是生成迭代器时,CopyOnWriteArrayList的Object数组的快照的内容,在迭代的过程中,往CopyOnWriteArrayList中添加元素也不会抛出异常。
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

class PutThread extends Thread {
    private CopyOnWriteArrayList<Integer> cowal;

    public PutThread(CopyOnWriteArrayList<Integer> cowal) {
        this.cowal = cowal;
    }

    public void run() {
        try {
            for (int i = 100; i < 110; i++) {
                cowal.add(i);
                Thread.sleep(50);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class CopyOnWriteArrayListDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> cowal = new CopyOnWriteArrayList<Integer>();
        for (int i = 0; i < 10; i++) {
            cowal.add(i);
        }
        PutThread p1 = new PutThread(cowal);
        p1.start();
        Iterator<Integer> iterator = cowal.iterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + " ");
        }
        System.out.println();
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        iterator = cowal.iterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + " ");
        }
    }
}

运行结果(某一次)

0 1 2 3 4 5 6 7 8 9 100 
0 1 2 3 4 5 6 7 8 9 100 101 102 103 

CopyOnWriteArrayList 在项目中的使用?

  • todo

4、更深入理解

4.1、CopyOnWriteArrayList 的缺陷和使用场景

CopyOnWriteArrayList 有几个缺点:

  • 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc;
  • 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;

CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用

  • 因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

4.2、CopyOnWriteArrayList为什么并发安全且性能比Vector好?

Vector对单独的add,remove等方法都是在方法上加了synchronized;并且如果一个线程A调用size时,另一个线程B 执行了remove,然后size的值就不是最新的,然后线程A调用remove就会越界 (这时就需要再加一个Synchronized)。这样就导致有了双重锁,效率大大降低,何必呢。于是vector废弃了,要用就用CopyOnWriteArrayList 吧。

5、参考文章

  • 【JUC】JDK1.8源码分析之CopyOnWriteArrayList(六)
  • CopyOnWriteArrayList 面试集锦
  • ArrayList和CopyOnWriteArrayList面试题总结与源码分析

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

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

相关文章

linux系统中三个重要的结构体

第一​&#xff1a;struct inode结构体 struct inode { struct hlist_node i_hash; struct list_head i_list; /* backing dev IO list */ struct list_head i_sb_list;​ //主次设备号 dev_t i_rdev;​ struct list_head i_devices; //用联合体是因为该…

山西省行政村边界数据/乡镇街道边界数据/行政区划边界分布

山西&#xff08;简称&#xff1a;晋&#xff0c;别称&#xff1a;三晋&#xff0c;古称河东&#xff09;&#xff0c;中华人民共和国省级行政区&#xff0c;省会太原市&#xff0c;位于黄河中游东岸&#xff0c;华北平原西面的黄土高原上。东以太行山为界&#xff0c;与河北为…

019 基于Spring Boot的教务管理系统、学生管理系统、课表查询系统

基于Spring Boot的教务管理系统、学生管理系统、课表查询系统 一、系统介绍 本作品主要实现了一个课表查询系统&#xff0c;采用了SSM&#xff08;Spring SpringMVC MyBatis&#xff09;的基础架构。 二、使用技术 spring-bootspring-MVCthymeleafmybatis-plusdruidLombo…

Ae 效果:CC Lens

扭曲/CC Lens Distort/CC Lens CC Lens &#xff08;CC 镜头&#xff09;主要用于添加或移除摄像机镜头扭曲&#xff0c;比如桶形失真 Barrel、枕形失真 Pincushion以及鱼眼失真 Fisheye等。或者&#xff0c;用它来创建一些特殊的动画效果。 ◆ ◆ ◆ 效果属性说明 Center 中…

数据结构 2.1 单链表

1.单链表 线性表&#xff1a;1.有限的序列 2.序列中的每一个元素都有唯一的前驱和后继&#xff0c;除了开头和结尾的两个节点。 顺序表&#xff1a;分配一块连续的内存去存放这些元素&#xff0c;eg、数组 链表&#xff1a;内存是不连续的&#xff0c;元素会各自被分配一块内…

防抖和节流的实现

防抖和节流的实现 什么是防抖和节流实现防抖和节流防抖节流 防抖和节流的应用场景 什么是防抖和节流 防抖和节流是前端开发中常用的两种性能优化技术。 为什么需要防抖和节流呢&#xff1f; 两者目的都是为了防止某个时间段内操作频繁触发&#xff0c;造成性能消耗。 防抖&…

gin路由相关方法

c.Request.URL.Path 拿到请求的路径 package mainimport ( "fmt" "github.com/gin-gonic/gin" "net/http")//路由重定向&#xff0c;请求转发&#xff0c;ANY &#xff0c;NoRoute&#xff0c;路由组func main() { r : gin.Default() // -------…

Python 无废话-办公自动化Excel格式美化

设置字体 在使用openpyxl 处理excel 设置格式&#xff0c;需要导入Font类&#xff0c;设置Font初始化参数&#xff0c;常见参数如下&#xff1a; 关键字参数 数据类型 描述 name 字符串 字体名称&#xff0c;如Calibri或Times New Roman size 整型 大小点数 bold …

Spring注册Bean系列--方法5:@Import+ImportBeanDefinitionRegistrar

原文网址&#xff1a;Spring注册Bean系列--方法5&#xff1a;ImportImportBeanDefinitionRegistrar_IT利刃出鞘的博客-CSDN博客 简介 本文介绍Spring注册Bean的方法&#xff1a;ImportImportBeanDefinitionRegistrar。 注册Bean的方法我写了一个系列&#xff0c;见&#xff…

NAT模式和桥接模式的区别

NAT模式和桥接模式的区别 NAT模式和桥接模式都是虚拟机网络配置的两种方式&#xff0c;主要区别在于虚拟机与外部网络交互的方式不同。 NAT&#xff08;Network Address Translation&#xff0c;网络地址转换&#xff09;模式&#xff1a;在这种模式下&#xff0c;虚拟机和宿主…

SystemUI状态栏

SystemUI状态栏 1、日志开关2、相关属性配置3、Statusbar界面启动 android13-release 1、日志开关 Log.isLoggable通过设置属性&#xff0c;重启应用&#xff1b;Compile.IS_DEBUG日志开关是src-debug和src-release版本区别 frameworks\base\packages\SystemUI\src\com\android…

C++设计模式-外观(Facade)

目录 C设计模式-外观&#xff08;Facade&#xff09; 一、意图 二、适用性 三、结构 四、参与者 五、代码 C设计模式-外观&#xff08;Facade&#xff09; 一、意图 为子系统中的一组接口提供一个一致的界面&#xff0c;Facade模式定义了一个高层接口&#xff0c;这个接…

阿里云服务器Anolis OS龙蜥操作系统详细介绍

阿里云服务器Anolis OS镜像系统由龙蜥OpenAnolis社区推出&#xff0c;Anolis OS是CentOS 8 100%兼容替代版本&#xff0c;Anolis OS是完全开源、中立、开放的Linux发行版&#xff0c;具备企业级的稳定性、高性能、安全性和可靠性。目前阿里云服务器ECS可选的Anolis OS镜像系统版…

机器视觉行业最可怕的不是以量换价吗?而是买方市场的带量采购,量价挂钩

机器视觉行业其实有很多值得思考,人力成本(团队人数控制),硬件成本,售后成本,回款成本(收款成本)。那么我们今天谈谈带量采购,量价挂钩、以量换价。这个话题有很多争议,很多有趣的争论,也有值得我们后人评价。 什么是“以价换量”? 从经济学角度来看,“以价换量…

linux入门---信号的操作

目录标题 sigset_tsigset_t的操作函数sigprocmasksigpending信号的屏蔽测试sigaction sigset_t 为了能够让操作系统更好的使用信号&#xff0c;操作系统提供了sigset_t的数据类型&#xff0c;操作系统中存在pending表和block表&#xff0c;但是这两张表是内核数据结构&#xf…

数据结构-顺序存储二叉树

文章目录 目录 文章目录 前言 一 . 什么是顺序存储二叉树 二 . 模拟实现 前序遍历 总结 前言 大家好,今天给大家讲一下顺序存储二叉树 一 . 什么是顺序存储二叉树 顺序存储二叉树是一种将二叉树的节点按照从上到下、从左到右的顺序存储在数组中的方法。具体来说&#xff0c;顺…

Jackson 的 SNAKE_CASE 反序列化

最近项目中有关 JSON 的序列化和反序列化中&#xff0c;我们遇到了一个问题就是 category_id 我们在定义对象的时候使用的是 categoryId。 当程序进行反序列化的时候&#xff0c;我们获得的对象值为 NULL。 这是因为 jackson 提供了一个命名规则&#xff0c;如果你是希望进行…

FISCO BCOS(三十七)———FISCOBCOS应用开发,交易hash、区块高度的获取

这个需求怎么做? 交易hash的获取方式有很多,这里先介绍一种方式。 根据块高查询区块信息 https://webasedoc.readthedocs.io/zh_CN/latest/docs/WeBASE-Front/interface.html那我们如何知道现在的区块高度是多少? https://webasedoc.readthedocs.io/zh_CN/latest/docs/W…

谦卑篇(Be humble)//伟大是用卑微来换取的,任何时候都应该看清自己

1. 宇宙诞生138亿年&#xff0c;地球迄今经过了46亿个春秋&#xff0c;人生不过百年&#xff0c;//从生命科学的角度。 2. 蔡崇信&#xff1a;好的领导者是很谦卑的&#xff0c;他们会用谦卑雇佣来能力更强的人。// 从创业者的角度。 3. 《臣服实验》&#xff1a; 臣服生命之流…

新版Ai企业级系统去授权版本完美运行

Ai企联系统去授权版新鲜出炉 一款市面上新出的AI企联系统&#xff0c;一款市面上新出的AI企联系统 项目uniapp开发的&#xff0c;支持3.5 4.0 Mj 此套系统5端适配&#xff0c;WebH5微信小程序抖音小程序双端APP&#xff0c;支持流量主! 好像有能力的快手小程序那些也可以上…