Interview系列 - 07 Java | 集合的快速失败和安全失败机制 | 迭代器类源码 | CopyOnWriteArrayList

news2025/7/21 21:42:35

文章目录

      • 1. 集合的快速失败 (fail-fast)
        • 1. 使用增强for遍历集合并使用ArrayList的 remove() 方法删除集合元素
        • 2. 使用 forEach 遍历集合并使用ArrayList的 remove() 方法删除集合元素
        • 3. 使用迭代器遍历集合并使用ArrayList的 remove() 方法删除集合元素
        • 4. 使用迭代器遍历集合并使用迭代器的 remove() 方法删除元素
        • 5. 源码分析
      • 2. 同步容器
      • 3. 集合的安全失败机制 (fail-safe)
      • 4. CopyOnWriteArrayList
        • 1. CopyOnWriteArrayList的使用
        • 2. CopyOnWriteArrayList的原理
        • 3. CopyOnWriteArrayList源码分析
        • 4. CopyOnWriteArrayList的优点
        • 5. CopyOnWriteArrayList和ReentrantReadWriteLock的比较

请思考以下几个问题:
1、遍历一个 List 有哪些不同的方式?实现原理是什么?Java 中 List遍历的最佳实践是什么?
2、如何边遍历边删除集合元素?
3、什么是集合的快速失败机制?
4、多线程场景下如何使用 ArrayList?

1. 集合的快速失败 (fail-fast)

采用快速失败机制的集合容器,使用迭代器进行遍历集合时,除了通过迭代器自身的 remove() 方法之外,对集合进行任何其他方式的结构性修改,则会抛出 ConcurrentModificationException 异常。

1. 使用增强for遍历集合并使用ArrayList的 remove() 方法删除集合元素

public class Main {
    public static void main(String[] args) {
        List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
        List<String> list = new ArrayList(initList);
        for (String element : list) {
            if (element.startsWith("李")) {
                // 利用ArrayList的remove()方法对集合进行修改
                list.remove(element);
            }
        }
        System.out.println(list);
    }
}

在这里插入图片描述
抛出并发修改异常!其实,for(xx in xx) 就是增强的 for循环,即迭代器 Iterator 的加强实现,其内部是调用的 Iterator 的方法。

2. 使用 forEach 遍历集合并使用ArrayList的 remove() 方法删除集合元素

public class Main {
    public static void main(String[] args) {
        List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
        List<String> list = new ArrayList(initList);
        list.forEach((e) -> {
            if (e.contains("李")) {
                list.remove(e);
            }
        });
        System.out.println(list);
    }
}

在这里插入图片描述
forEach 方法的背后其实就是增强的 for 循环,底层即迭代器,所以使用 list.remove 同样抛出 ConcurrentModificationException 异常。

3. 使用迭代器遍历集合并使用ArrayList的 remove() 方法删除集合元素

public class Main {
    public static void main(String[] args) {
        List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
        List<String> list = new ArrayList(initList);
        for (Iterator<String> ite = list.iterator(); ite.hasNext(); ) {
            String str = ite.next();
            if (str.contains("李")) {
                // 利用ArrayList的remove()方法对集合进行修改
                list.remove(str);
            }
        }
        System.out.println(list);
    }
}

在这里插入图片描述
又是那个并发修改异常,这个示例虽然使用了 Iterator 循环,但删除的时候却使用了 list.remove 方法,同样是有问题的。

4. 使用迭代器遍历集合并使用迭代器的 remove() 方法删除元素

public class Main {
    public static void main(String[] args) {
        List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");
        List<String> list = new ArrayList(initList);
        for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {
            String str = iterator.next();
            if (str.contains("李")) {
                iterator.remove();
            }
        }
        System.out.println(list);
    }
}

结果输出正常,这是因为迭代器中的 remove 方法将期待修改的数量(expectedModCount)值进行了同步。

5. 源码分析

public class ArrayList<E> extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable{

    // 1、添加元素方法
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private void ensureExplicitCapacity(int minCapacity) {
        // 快速失败机制:迭代器内部维护了一个 modCount 变量,添加元素时,modCount++
        modCount++;

        if (elementData.length < minCapacity)
            grow(minCapacity);
    }

    // 2、删除元素方法
    public E remove(int index) {
        rangeCheck(index);

        // 快速失败机制:迭代器内部维护了一个 modCount 变量,添加元素时,modCount++
        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

    // 3、迭代器方法
    public Iterator<E> iterator() {
        return new Itr();
    }

    // 4、ArrayList中的内部类
    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            // 每次调动迭代器的next()方法都会检查集合元素是否修改,如果修改了modCount的值会改变
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            // 每次调动迭代器的remove()方法都会检查集合元素是否修改,如果修改了modCount的值会改变
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                // 使用迭代器的remove()方法,内部在调用ArrayList的remove()方法后,会同步修改expectedModCount,所以不会发生并发修改异常!
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

        final void checkForComodification() {
            // 当检测到 modCount != expectedmodCount 时,抛出异常
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }
}

迭代器在遍历时直接访问集合的内容时,集合中的内容在遍历的过程中无法被修改。为了保证不被修改,迭代器内部维护了一个 modCount 变量 ,当集合结构改变(添加、删除),就会改变 modCount 的值。每当迭代器使用 next() 方法遍历下一个元素时,都会检查 modCount 的值是否等于 expectedmodCount 的值,当检测到 modCount != expectedmodCount 时,抛出 ConcurrentModificationException 异常,反之继续遍历。

2. 同步容器

根据上面的分析可见,ArrayList集合单线程下效率相对较高,多线程环境下,线程不安全。 java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改。

如果想要保证线程安全,可以使用线程安全的同步容器,即使用sychronized关键字来实现线程安全的容器,比如 Vector,Collections工具类中的同步方法。

3. 集合的安全失败机制 (fail-safe)

采用安全失败机制的集合容器,使用迭代器进行遍历时不是直接在集合内容上访问的,而是将原有集合内容进行拷贝,在拷贝的集合上进行遍历。

迭代器在遍历时访问的是拷贝的集合,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 ConcurrentModificationException 异常。

优缺点:

(1) 由于对集合进行了拷贝,避免了 ConcurrentModificationException 异常,但拷贝时产生大量的无效对象,开销大。

(2) 无法保证读取到的数据是原集合中最新的数据,即迭代器进行遍历的是拷贝的集合,在遍历期间原集合发生的修改,迭代器是检测不到的。

(3) java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

4. CopyOnWriteArrayList

1. CopyOnWriteArrayList的使用

Collections可以将基础容器包装为线程安全的同步容器,但是这些同步容器包装类在进行元素迭代时并不能进行元素添加操作。

public class CopyOnWriteArrayListTest {
    //并发操作的执行目标
    public static class CocurrentTarget implements Runnable {
        //并发操作的目标队列
        List<String> list = null;
        public CocurrentTarget(List<String> targetList) {
            this.list = targetList;
        }

        @Override
        public void run() {
            Iterator<String> iterator = list.iterator();
            //迭代操作
            while (iterator.hasNext()) {
                // 在迭代操作时,进行列表的修改
                String threadName = Thread.currentThread().getName();
                System.out.println("开始往同步队列加入线程名称:" + threadName);
                list.add(threadName);
            }
        }
    }

    //测试同步队列:在迭代操作时,进行列表的修改
    public static void main(String[] args) throws InterruptedException {
        List<String> notSafeList = Arrays.asList("a", "b", "c");
        List<String> synList = Collections.synchronizedList(notSafeList);
        //创建一个执行目标
        CocurrentTarget synchronizedListListDemo = new CocurrentTarget(synList);
        //10个线程并发
        for (int i = 0; i < 10; i++) {
            new Thread(synchronizedListListDemo, "线程" + i).start();
        }
        //主线程等待
        Thread.sleep(1000);
    }
}

在这里插入图片描述
那么,该如何解决此问题呢?可使用CopyOnWriteArrayList替代Collections.synchronizedList同步包装实例:

public class CopyOnWriteArrayListTest {
    //并发操作的执行目标
    public static class CocurrentTarget implements Runnable {
        //并发操作的目标队列
        List<String> list = null;
        public CocurrentTarget(List<String> targetList) {
            this.list = targetList;
        }

        @Override
        public void run() {
            Iterator<String> iterator = list.iterator();
            //迭代操作
            while (iterator.hasNext()) {
                // 在迭代操作时,进行列表的修改
                String threadName = Thread.currentThread().getName();
                System.out.println("开始往同步队列加入线程名称:" + threadName);
                list.add(threadName);
            }
        }
    }

    //测试同步队列:在迭代操作时,进行列表的修改
    public static void main(String[] args) throws InterruptedException {
        List<String> notSafeList = Arrays.asList("a", "b", "c");
        List<String> copyOnWriteArrayList = new CopyOnWriteArrayList(notSafeList);
        //创建一个执行目标
        CocurrentTarget cocurrentTarget = new CocurrentTarget(copyOnWriteArrayList);
        //10个线程并发
        for (int i = 0; i < 10; i++) {
            new Thread(cocurrentTarget, "线程" + i).start();
        }
        //主线程等待
        Thread.sleep(1000);
    }
}

使用CopyOnWriteArrayList容器可以在进行元素迭代的同时进行元素添加操作。那么CopyOnWriteArrayList是如何做到的呢?

2. CopyOnWriteArrayList的原理

CopyOnWrite(写时复制)就是在修改器对一块内存进行修改时,不直接在原有内存块上进行写操作,而是将内存复制一份,在新的内存中进行写操作,写完之后,再将原来的指针(或者引用)指向新的内存,原来的内存被回收。

CopyOnWriteArrayList是写时复制思想的一种典型实现,其含有一个指向操作内存的内部指针array,而可变操作(add、set等)是在array数组的副本上进行的。当元素需要被修改或者增加时,并不直接在array指向的原有数组上操作,而是首先对array进行一次复制,将修改的内容写入复制的副本中。写完之后,再将内部指针array指向新的副本,这样就可以确保修改操作不会影响访问器的读取操作。

在这里插入图片描述
CopyOnWriteArrayList是一个满足CopyOnWrite思想并使用Array数组存储数据的线程安全List。

3. CopyOnWriteArrayList源码分析

(1) CopyOnWriteArrayList的属性:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    /** 对所有的修改器方法进行保护,访问器方法并不需要保护 */
    final transient ReentrantLock lock = new ReentrantLock();

    /** 内部对象数组,通过 getArray/setArray方法访问 */
    private transient volatile Object[] array;

    //获取内部对象数组
    final Object[] getArray() {
        return array;
    }

    // 设置内部对象数组
    final void setArray(Object[] a) {
        array = a;
    }
    // 省略其他代码
}

(2) CopyOnWriteArrayList的读取操作:

访问器的读取操作没有任何同步控制和锁操作,理由是内部数组array不会发生修改,只会被另一个array替换,因此可以保证数据安全。

/** 操作内存的引用*/
private transient volatile Object[] array;

public E get(int index) {
    return get(getArray(), index);
}

//获取元素
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}

//返回操作内存
final Object[] getArray() {
    return array;
}

(3) CopyOnWriteArrayList的写入操作:

CopyOnWriteArrayList的写入操作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);  
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();  // 释放锁
    }
}

// 在集合的指定位置添加指定元素
public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        Object[] newElements;
        int numMoved = len - index;
        if (numMoved == 0)
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            newElements = new Object[len + 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        newElements[index] = element;
        setArray(newElements);
    } finally {
        lock.unlock();
    }
}

从add()操作可以看出,在每次进行添加操作时,CopyOnWriteArrayList底层都是重新复制一份数组,再往新的数组中添加新元素,待添加完了,再将新的array引用指向新的数组。当add()操作完成后,array的引用就已经指向另一个存储空间了。

既然每次添加元素的时候都会重新复制一份新的数组,那就带来了一个问题,就是增加了内存的开销,如果容器的写操作比较频繁,那么其开销就比较大。所以,在实际应用的时候,CopyOnWriteArrayList并不适合进行添加操作。但是在并发场景下,迭代操作比较频繁,CopyOnWriteArrayList就是一个不错的选择。

(4) CopyOnWriteArrayList的迭代器实现:

CopyOnWriteArray有自己的迭代器,该迭代器不会检查修改状态,也无须检查状态。为什么呢?因为被迭代的array数组可以说是只读的,不会有其他线程能够修改它。

//获取迭代器
public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

//返回操作内存
final Object[] getArray() {
    return array;
}

static final class COWIterator<E> implements ListIterator<E> {
    /**对象数组的快照(snapshot)*/
    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 E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }
}

4. CopyOnWriteArrayList的优点

CopyOnWriteArrayList有一个显著的优点,那就是读取、遍历操作不需要同步,速度会非常快。所以,CopyOnWriteArrayList适用于读操作多、写操作相对较少的场景(读多写少),比如可以在进行“黑名单”拦截时使用CopyOnWriteArrayList。

5. CopyOnWriteArrayList和ReentrantReadWriteLock的比较

CopyOnWriteArrayList和ReentrantReadWriteLock读写锁的思想非常类似,即读读共享、写写互斥、读写互斥、写读互斥。但是前者相比后者的更进一步:为了将读取的性能发挥到极致,CopyOnWriteArrayList读取是完全不用加锁的,而且写入也不会阻塞读取操作,只有写入和写入之间需要进行同步等待,读操作的性能得到大幅度提升。

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

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

相关文章

人脑脊液的代谢组学研究—标识恶性神经胶质瘤的特征

百趣代谢组学分享&#xff0c;脑疾病病人的脑脊液&#xff08;CSF&#xff09;通常用来诊断和监测研究&#xff0c;但是恶性胶质瘤病人脑脊液组成的变化很少被人们所知。该研究作者建立了靶向代谢组学分析方法&#xff0c;采用SRM监测模式&#xff0c;使用正负离子切换的方法在…

MySQL实战之深入浅出索引(上)

1.前言 提到数据库&#xff0c;大家肯定会想到数据库的索引&#xff0c;很多人都知道索引是为了提高查询效率的&#xff0c;那么今天我就给大家讲一下&#xff0c;什么是索引&#xff0c;索引的数据结构是什么&#xff0c;索引是如何工作的。 因为索引的内容比较多&#xff0…

大数据应用要经得起考验,不可盲目跟风_光点科技

一项大数据应用&#xff0c;如果不是经得起推敲的&#xff0c;那就值得怀疑它是不是优秀的大数据应用&#xff0c;是不是有可利用的价值&#xff0c;是不是值得将人力物力财力花费在其中。所以&#xff0c;必须对大数据应用进行必要的筛选&#xff0c;做一定的检验之后才可以做…

vegfr2药物|适应症|市场销售数据-上市药品前景分析

癌症作为人类身体健康的主要威胁&#xff0c;其高死亡率一直是人类死亡的主要原因。尽管人类为控制癌症付出了巨大的努力&#xff0c;然而癌症的发病率和死亡率还是在高速增长。而肺癌、结直肠癌、肝癌和乳腺癌等被认为是癌症死亡的主要因素。而根据科研人员发现&#xff0c;癌…

JavaScript HTML DOM 简介

文章目录JavaScript HTML DOM 简介HTML DOM (文档对象模型)HTML DOM 树查找 HTML 元素通过 id 查找 HTML 元素通过标签名查找 HTML 元素通过类名找到 HTML 元素下面我们将学到如下内容JavaScript HTML DOM 简介 通过 HTML DOM&#xff0c;可访问 JavaScript HTML 文档的所有元素…

高清无码的MP4如何采集?python带你保存~

前言 大家早好、午好、晚好吖 ❤ ~ 又是我,我又来采集小姐姐啦~ 这次我们采集的网站是(看下图): 本文所有模块\环境\源码\教程皆可点击文章下方名片获取此处跳转 话不多少,我们赶快开始吧~ 第三方模块: requests >>> pip install requests 如果安装python第三方模块…

刷题专练之数组移除元素

文章目录前言一、移除元素1.题目介绍2.思路&#xff1a;3.代码二、移动零1.题目介绍2.思路3.代码三、删除有序数组中的重复项1.题目介绍2.思想3.代码四、80. 删除有序数组中的重复项 II1.题目介绍2.思路3.代码4.推荐题解前言 我每个刷题篇的题目顺序都是特别安排的&#xff0c;…

FSM——squirrel状态机使用

FSM——squirrel状态机使用 1 FSM介绍 1.1 概念 FSM&#xff08;finite state machine&#xff09;:有限状态机 是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。核心内容&#xff1a;有限个状态、通过外部操作引起状态的转移。用来对状态的流转进行解耦&a…

C++031-C++日期模拟

文章目录C031-C日期模拟日期模拟题目描述 给出天数求月份日期题目描述 给出天数求月份日期-倒计时题目描述 求任意日期插值在线练习&#xff1a;总结C031-C日期模拟 在线练习&#xff1a; http://noi.openjudge.cn/ https://www.luogu.com.cn/ 日期模拟 题目描述 给出天数求月…

CSS 网页动画【快速掌握知识点】

目录 前言 一、使用CSS3动画 二、使用CSS过渡 三、使用CSS变换&#xff1a; 前言 CSS是一种用于网页设计和排版的语言&#xff0c;也可以用它来制作网页动画。 一、使用CSS3动画 CSS3引入了动画属性&#xff0c;允许您为元素设置动画效果。您可以使用关键帧来定义动画的开始…

MVP简单模型搭建【架构】

MVP简介 MVP是一种项目架构设计模式&#xff08;说白了就是我们产品的一种设计方案&#xff09; 其实MVP本质 就是将View和Model完全隔离&#xff0c;通过Presenter统一调度管理&#xff08;Presenter扮演着中介的角色&#xff09;传统的设计思路是我们直接跟房东谈&#xff0…

聊聊动态线程池的9个场景(改进版)

线程池是一种基于 池化思想管理线程 的工具&#xff0c;使用线程池可以减少 创建销毁线程的开销&#xff0c;避免线程过多导致 系统资源耗尽。在 高并发以及大批量 的任务处理场景&#xff0c;线程池的使用是必不可少的。 如果有在项目中实际使用线程池&#xff0c;相信你可能…

数仓模型之维度建模

目录 1、数仓架构原则 2、如何搭建一个好的数仓 2.1 建模方法 2.2 建模解决的痛点 2.3 数仓系统满足的特性 2.4 数仓架构设计 3、维度建模 4、案例 5、问题讨论 今天我们来聊聊在数仓模型中举足轻重的维度建模。 简单而言&#xff0c;数据仓库的核心目标是为展现层提…

Hive学习——开窗函数精讲

目录 一、基于行的窗口函数——行的起点~行的终点 二、基于值的窗口函数——值的起点~值的终点 三、基于分区的窗口函数 四、基于缺省的窗口函数 五、lead与lag 六、first_value和last_value 七、排名函数——rank(113)、dense_rank(112)、row_number(123) 八、NTILE分…

nvm (node版本管理工具)安装的详细步骤,并解决安装过程中遇到的问题

1、下载NVM&#xff0c;跳转下载链接后&#xff0c;如下图&#xff0c;下载红框后解压文件 2、安装 注意&#xff1a;双击安装之后&#xff0c;会有两个地址选择&#xff0c; 1、地址中不能存在空格 2、不要放在C盘中&#xff0c;后面需要改个设置文件&#xff0c;安装到C盘的…

银行家算法

银行家算法 银行家算法是一种用来避免操作系统死锁出现的有效算法&#xff0c;所以在引入银行家算法的解释之前&#xff0c;有必要简单介绍一下死锁的概念。 一、死锁 死锁&#xff1a;是指两个或两个以上的进程在执行过程中&#xff0c;由于竞争资源或者由于彼此通信而造成…

Gitlab Linux 环境安装

环境检查是否已经安装了gitlab&#xff0c;安装了需要卸载# 检查当前环境是否已经安装了gitlab&#xff0c;并且查看版本 gitlab-rails console # 删除命令 yum remove git# 查找命令 rpm -qa | grep gitlab # 删除命令 rpm -e gitlab-ce-12.10.1-ce.0.el8.x86_64 # 查找命令f…

DS期末复习卷(八)

一、选择题(30分) 1.字符串的长度是指&#xff08; C &#xff09;。 (A) 串中不同字符的个数 (B) 串中不同字母的个数 (C ) 串中所含字符的个数 (D) 串中不同数字的个数 2.建立一个长度为n的有序单链表的时间复杂度为&#xff08; C &#xff09; (A) O(n) (B) O(1) © …

小米/红米手机数据恢复:从小米手机恢复已删除的数据

如果您不小心删除了小米手机上的数据&#xff0c;后来发现您需要它&#xff0c;那么本文适合您。我将向您介绍一些最可靠的小米恢复方法&#xff0c;以将您的数据恢复到您的设备上。无论您是否有备份&#xff0c;都可以处理。让我们开始吧&#xff01; 小米数据恢复 - 如何做&a…

一篇学习JVM

基础入门 1.JDK、JRE、JVM三者间的联系与区别 JDK JDK(Java SE Development Kit)&#xff0c;Java标准开发包&#xff0c;它提供了编译、运行Java程序所需的各种工具和资源&#xff0c;包括Java编译器、Java运行时环境&#xff0c;以及常用的Java类库等。 下图是JDK的安装目…