2023年Java核心技术面试第六篇(篇篇万字精讲)

简介: 2023年Java核心技术面试第六篇(篇篇万字精讲)

十 一. 如何保证集合是线程安全的?



11.1 典型回答


11.1.1 Java提供了不同层面的线程安全支持。



除了Hashtable等容器,还提供了同步包装器(synchronized Wrapper),我们可以调用 Collections 工具类提供的包装的方法,进行获取一个同步的包装容器(如Collections.synchronizedMap),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能较低。


普遍选择利用并发包提供的线程安全容器类。

Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());

同步包装器是Java中用于实现线程安全的容器类的一种机制。它通过在底层容器上添加同步操作来确保多个线程之间的数据访问同步。


Java提供了Collections工具类来创建各种类型的同步包装器,如synchronizedList、synchronizedSet、synchronizedMap等。这些同步包装器可以将非线程安全的集合或映射转换为线程安全的。


11.1.1.1 例子:


同步包装器采用了较为粗粒度的同步方式,即在对整个容器对象进行同步操作。当一个线程获取容器对象的锁时,其他线程需要等待该线程释放锁才能进行操作。这样做的好处是简单直接,不需要额外的复杂的同步逻辑。然而,在高并发情况下,由于所有操作都需要竞争同一个锁,可能会导致性能下降。


import java.util.*;
public class SynchronizedWrapperExample {
    public static void main(String[] args) {
        // 创建一个非线程安全的ArrayList
        List<String> list = new ArrayList<>();
        // 使用同步包装器将其转换为线程安全的
        List<String> synchronizedList = Collections.synchronizedList(list);
        // 创建并启动多个线程同时对同一个列表进行操作
        for (int i = 0; i < 5; i++) {
            int threadNumber = i;
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    synchronizedList.add("Thread " + threadNumber);
                }
            }).start();
        }
        // 等待所有线程执行完毕
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 输出列表元素个数(预期为500)
        System.out.println(synchronizedList.size());
    }
}


在上面的示例中,我们首先创建一个非线程安全的ArrayList对象,并使用Collections.synchronizedList()方法将其转换为线程安全的同步包装器。然后,我们创建了5个线程,每个线程向列表中添加100个元素。由于使用了同步包装器,多个线程对列表的操作会依次进行同步,确保线程安全。最后,我们输出列表的元素个数,预期结果为500。  


11.1.2 并发容器:


Java提供了ConcurrentHashMap和CopyOnWriteArrayList等并发容器,它们采用了更加高效的并发实现方式,在高并发情况下性能表现更好。


ConcurrentHashMap,CopyOnWriteArrayList。

ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();
CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList<>();


11.1.2.1  ConcurrentHashMap


我们创建了一个ConcurrentHashMap对象,并使用多个线程同时向映射中添加元素。由于ConcurrentHashMap采用了分段锁的机制,不同的线程可以同时访问和修改不同的段,从而提高了并发性能。最后,我们输出映射的大小。

import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        // 创建并启动多个线程同时对同一个映射进行操作
        for (int i = 0; i < 5; i++) {
            int threadNumber = i;
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    map.put("Key" + j, threadNumber);
                }
            }).start();
        }
        // 等待所有线程执行完毕
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 输出映射的大小
        System.out.println(map.size());
    }
}


11.1.2.2  CopyOnWriteArrayList


我们创建了一个CopyOnWriteArrayList对象,并使用多个线程同时向列表中添加元素。CopyOnWriteArrayList通过实现写时复制(Copy-On-Write)机制,在写操作时创建并复制一个新的数组,从而避免了线程冲突和数据不一致的问题。这使得在高并发读取的情况下,CopyOnWriteArrayList具有很好的性能表现。最后,我们输出列表的大小。


import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListExample {
    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        // 创建并启动多个线程同时对同一个列表进行操作
        for (int i = 0; i < 5; i++) {
            int threadNumber = i;
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    list.add(threadNumber);
                }
            }).start();
        }
        // 等待所有线程执行完毕
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 输出列表的大小
        System.out.println(list.size());
    }
}


11.1.2.3 注意:


虽然ConcurrentHashMap和CopyOnWriteArrayList在高并发场景下性能较好,但它们也有自己的适用场景和限制。在选择并发容器时,应根据具体的需求和并发访问模式来选择合适的容器。


11.1.2.4 ConcurrentHashMap和CopyOnWriteArrayList实现方式和场景精讲:


  1. 11.1.2.4.1 实现方式的区别:
  • ConcurrentHashMap:ConcurrentHashMap采用了分段锁(Segment Locking)的机制来实现并发访问。内部使用类似于哈希表的数据结构,将整个数据集分成多个段(Segment),每个段都拥有自己的锁,不同的线程可以同时访问和修改不同的段,从而提高了并发性能。
  • CopyOnWriteArrayList:CopyOnWriteArrayList采用写时复制(Copy-On-Write)的机制。在写操作时,会创建并复制一个新的数组,从而避免了读写冲突和数据不一致的问题。读操作不需要加锁,因此可以实现高并发的读取。


11.1.2.4.2 适用场景的区别:


  • ConcurrentHashMap:适用于多线程环境下需要频繁进行更新操作的情况。由于ConcurrentHashMap采用分段锁的机制,不同的线程可以同时对不同的段进行操作,因此在高并发的情况下,能够获得较好的性能表现。它通常用于替代Hashtable或使用synchronized关键字保护的传统HashMap。
  • CopyOnWriteArrayList:适用于多线程环境下以读操作为主、更新操作较少的情况。由于CopyOnWriteArrayList的读操作不需要加锁,因此在高并发的读取场景下,能够提供较好的性能。但是,写操作会创建并复制一个新的数组,因此写操作的性能相对较低。它通常用于代替同步的ArrayList,并且适用于读多写少的场景。


11.1.2.5 CopyOnWriteArrayList解析:


ConcurrentHashMap适用于频繁的更新操作和较高的并发度,通过分段锁实现线程安全和较高的并发性能;而CopyOnWriteArrayList适用于读操作较多、写操作较少的场景,通过写时复制机制实现线程安全和较好的读性能。选择合适的并发容器应根据具体需求和并发访问模式来决定。


在CopyOnWriteArrayList中,每次写操作都会创建一个新的数组,并将原数组保留为只读状态。因此,随着写操作的频繁发生,会产生多个不同版本的原数组。


对于读取操作,它会选择最近一次写操作完成后的原数组进行读取。也就是说,读取操作会选择最新版本的原数组来获取数据,以确保数据的一致性。


需要注意的是,即使有多个版本的原数组存在,读取操作仍然可以安全地进行。这是因为每个版本的原数组都是只读的,不会发生数据修改。而且,读取操作不需要加锁,可以并发地进行。


CopyOnWriteArrayList中,读取操作会选择最近一次写操作完成后的原数组进行读取,以保证数据的一致性。多个版本的原数组不会影响读取操作的进行


11.1.3 线程安全队列(Queue/Deque):


Java提供了多种线程安全的队列(Queue/Deque)实现,其中包括ArrayBlockingQueue和SynchronousQueue。这些线程安全队列可以在多线程环境下安全地进行元素的插入、删除和检索操作。


ArrayBlockingQueue,SynchronousQueue。

ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(10);
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();


11.1.3.1 ArrayBlockingQueue


我们创建了一个ArrayBlockingQueue对象,并使用多个线程同时向队列中添加元素。


ArrayBlockingQueue使用固定大小的数组作为底层数据结构,在写入元素时会阻塞直到有空间可用,从而保证线程安全和有界性。最后,我们输出队列的大小。


import java.util.concurrent.ArrayBlockingQueue;
public class ArrayBlockingQueueExample {
    public static void main(String[] args) {
        ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
        // 创建并启动多个线程同时向队列中添加元素
        for (int i = 0; i < 5; i++) {
            int threadNumber = i;
            new Thread(() -> {
                try {
                    for (int j = 0; j < 100; j++) {
                        queue.put(threadNumber);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        // 等待所有线程执行完毕
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 输出队列的大小
        System.out.println(queue.size());
    }
}


11.1.3.1.1 详细:


ArrayBlockingQueue是Java中的一个线程安全、有界的阻塞队列。它使用固定大小的数组作为底层数据结构,可以在写入元素时阻塞直到有空间可用,从而保证线程安全和有界性。


当我们创建一个ArrayBlockingQueue对象时,需要指定队列的容量大小。这个容量决定了队列中可以存放的元素数量的上限。


多个线程同时向ArrayBlockingQueue添加元素时,如果队列已满(即达到了容量上限),写入操作将会被阻塞,直到有空间可用。这意味着写入操作会等待其他线程从队列中取出元素或者扩容后再继续执行。这样可以确保线程安全,避免了多个线程同时写入导致的数据竞争和不一致性。


当线程成功地将元素添加到ArrayBlockingQueue中后,其他线程可以继续读取或写入元素,只要队列没有达到容量上限。读取操作不会被阻塞,只有当队列为空时,读取操作会被阻塞,直到有新的元素加入。


最后,输出队列的大小即为当前队列中的元素个数。注意,由于ArrayBlockingQueue具有有界性,队列的大小永远不会超过初始化时指定的容量。


ArrayBlockingQueue是一个线程安全、有界的阻塞队列,使用固定大小的数组作为底层数据结构。它可以保证在写入元素时阻塞直到有空间可用,从而实现了线程安全和有界性。通过输出队列的大小可以获取当前队列中的元素个数。


 11.1.3.2 SynchronousQueue


我们创建了一个SynchronousQueue对象,并使用多个线程同时向队列中添加和移除元素。SynchronousQueue是一个没有容量的队列,它要求插入操作必须等待另一个线程进行相应的删除操作,从而实现了线程之间的同步。每个插入操作都会阻塞直到有另一个线程进行相应的删除操作,反之亦然。

import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueExample {
    public static void main(String[] args) {
        SynchronousQueue<Integer> queue = new SynchronousQueue<>();
        // 创建并启动多个线程同时向队列中添加和移除元素
        for (int i = 0; i < 5; i++) {
            int threadNumber = i;
            new Thread(() -> {
                try {
                    queue.put(threadNumber); // 向队列中添加元素
                    int element = queue.take(); // 从队列中取出元素
                    System.out.println("Element: " + element);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}


11.1.3.2.1 详细小结:


ynchronousQueue是一个特殊的无容量队列,它实现了线程之间的同步。在SynchronousQueue中,每个插入操作都必须等待另一个线程进行相应的删除操作,反之亦然。这种机制确保了只有在有消费者线程等待接收元素时,生产者线程才能成功将元素插入到队列中,而且只有在有生产者线程插入元素时,消费者线程才能成功从队列中移除元素。


具体来说,当一个线程调用SynchronousQueue的put()方法尝试将一个元素插入队列时,如果此时没有其他线程正在等待删除操作,则插入操作会被阻塞,直到另一个线程调用take()方法来获取并删除该元素为止。同样地,当一个线程调用SynchronousQueue的take()方法尝试从队列中获取并删除一个元素时,如果此时没有其他线程正在等待插入操作,则获取操作也会被阻塞,直到另一个线程调用put()方法来插入一个元素为止。


由于SynchronousQueue没有容量限制,因此它不会保存任何元素,仅充当了一个传递数据的通道。这种特性使得SynchronousQueue非常适合于一些场景,例如在生产者和消费者线程之间进行高效的数据交换。


需要注意的是,SynchronousQueue对于多个生产者和消费者线程之间的同步是一对一的关系。也就是说,每个插入操作都必须等待一个相应的删除操作,反之亦然。如果存在多个生产者线程或多个消费者线程,它们之间的调度和顺序将由线程调度器来决定。


11.1.3.3 注意:

ArrayBlockingQueue适用于有界队列的场景,而SynchronousQueue适用于无界队列且对吞吐量有较高要求的场景。选择合适的线程安全队列应根据具体需求和并发访问模式来决定。


11.1.4 各种有序容器的线程安全版本:


Java提供了一些有序容器的线程安全版本,如ConcurrentSkipListMap和ConcurrentSkipListSet等。这些容器在保证线程安全的同时,能够维护元素的有序性。


ConcurrentSkipListMap<Integer, String> concurrentSkipListMap = new ConcurrentSkipListMap<>();
ConcurrentSkipListSet<Integer> concurrentSkipListSet = new ConcurrentSkipListSet<>();


11.1.4.1 详细解释:


Java中的ConcurrentSkipListMapConcurrentSkipListSet是线程安全的有序容器,它们在保证多线程环境下的安全性的同时,能够维护元素的有序性。


ConcurrentSkipListMap是基于跳表(Skip List)数据结构实现的有序映射,底层通过链表层级结构实现快速查找、插入和删除操作。它支持键值对的存储,并且根据键的自然顺序或自定义比较器进行排序。由于使用了跳表的特点,在并发情况下,ConcurrentSkipListMap可以提供较好的性能。


11.1.4.1.1 ConcurrentSkipListMap使用:


import java.util.concurrent.ConcurrentSkipListMap;
public class ConcurrentSkipListMapExample {
    public static void main(String[] args) {
        // 创建一个ConcurrentSkipListMap实例
        ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
        // 向map中插入键值对
        map.put(3, "Apple");
        map.put(1, "Banana");
        map.put(2, "Orange");
        map.put(4, "Grapes");
        // 遍历并输出map中的元素
        for (Integer key : map.keySet()) {
            System.out.println(key + ": " + map.get(key));
        }
    }
}


输出:

1. 1: Banana
2. 2: Orange
3. 3: Apple
4. 4: Grapes


11.1.4.1.2 ConcurrentSkipListSet使用:


ConcurrentSkipListSet是基于ConcurrentSkipListMap实现的有序集合,它使用了与ConcurrentSkipListMap相同的跳表数据结构。它可以存储不重复的元素,并且根据元素的自然顺序或自定义比较器进行排序。

import java.util.concurrent.ConcurrentSkipListSet;
public class ConcurrentSkipListSetExample {
    public static void main(String[] args) {
        // 创建一个ConcurrentSkipListSet实例
        ConcurrentSkipListSet<Integer> set = new ConcurrentSkipListSet<>();
        // 向set中添加元素
        set.add(3);
        set.add(1);
        set.add(2);
        set.add(4);
        // 遍历并输出set中的元素
        for (Integer element : set) {
            System.out.println(element);
        }
    }
}


输出:

1
2
3
4


ConcurrentSkipListMap和ConcurrentSkipListSet提供了线程安全的有序容器,能够在多线程环境下保证安全性,并且能够维护元素的有序性。这使得它们在需要同时满足线程安全和有序性的场景下非常有用。


具体保证线程安全的方式,包括从简单的synchronize方式,到基于更加细化,基于分离锁实现的ConcurrentHashMap等并发实现,具体选择要看开发的场景需求,并发包内提供的容器通用场景,远优于早期的简单同步实现


11.2 考点分析


线程安全和并发,是Java面试必考的点。

前面几篇重点解读了HashMap,ConcurrentHashMap,以及涉及的底层CAS这些机制


11.3 趣谈部分


11.3.1  为什么需要ConcurrentHashMap?


Hashtable 本身比较低效,因为它的实现基本上是将put,get,size等各种方法加上"synchronized"。


导致了所有并发操作都要竞争同一把锁,一个线程在进行同步操作的时候,其他线程只能等待,大大降低了并发操作的效率。


HashMap不是线程安全的,并发情况会导致CPU占用100%这些情况。


如果利用Collection提供的同步包装器来解决问题。


同步包装版本采用了更细粒度的锁机制。例如,Collections.synchronizedMap()方法返回的同步包装版本中,只对每个单独的操作方法进行了同步,不需要在整个对象上加锁。这样,多个线程可以同时对不同的键值对进行操作,提高了并发性能,并没有真正的改进。


11.3.2 Hashtable和同步包装器的底层源码部分解释:


从源码角度讲解区别。


11.3.2.1 Hashtable的put方法源码片段:


在Hashtable的put方法中,使用了synchronized关键字对方法进行同步。这意味着每次只能有一个线程访问put方法,其他线程必须等待。

public synchronized V put(K key, V value) {
    // 具体插入操作
    // ...
    return oldValue;
}


11.3.2.2 同步包装器的put方法源码片段:


在同步包装器中,使用了一个私有的辅助对象mutex作为锁。通过synchronized关键字对mutex进行同步,确保每个操作方法都是线程安全的。

public V put(K key, V value) {
    synchronized (mutex) {
        // 具体插入操作
        // ...
        return oldValue;
    }
}


11.3.2.2.1 同步包装器实现原理 例子:


import java.util.HashMap;
import java.util.Map;
public class SynchronizedWrapperExample {
    public static void main(String[] args) {
        // 创建一个非线程安全的HashMap对象
        Map<String, Integer> hashMap = new HashMap<>();
        // 使用同步包装器创建一个线程安全的Map对象
        Map<String, Integer> synchronizedMap = synchronizedMap(hashMap);
        // 创建多个线程对集合进行操作
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                synchronizedMap.put("Key" + i, i);
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                synchronizedMap.get("Key" + i);
            }
        });
        // 启动线程
        thread1.start();
        thread2.start();
        // 等待线程执行完毕
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 打印集合大小
        System.out.println(synchronizedMap.size());
    }
    // 同步包装器的实现
    public static <K, V> Map<K, V> synchronizedMap(Map<K, V> map) {
        return new SynchronizedMap<>(map);
    }
    // 自定义的同步包装器类
    private static class SynchronizedMap<K, V> implements Map<K, V> {
        private final Map<K, V> map; // 内部维护一个非线程安全的Map对象
        public SynchronizedMap(Map<K, V> map) {
            this.map = map;
        }
        // 对所有操作进行同步,使用this作为锁
        @Override
        public synchronized int size() {
            return map.size();
        }
        @Override
        public synchronized boolean isEmpty() {
            return map.isEmpty();
        }
        @Override
        public synchronized boolean containsKey(Object key) {
            return map.containsKey(key);
        }
        @Override
        public synchronized boolean containsValue(Object value) {
            return map.containsValue(value);
        }
        @Override
        public synchronized V get(Object key) {
            return map.get(key);
        }
        @Override
        public synchronized V put(K key, V value) {
            return map.put(key, value);
        }
        @Override
        public synchronized V remove(Object key) {
            return map.remove(key);
        }
        // 其他方法省略...
    }
}


在上面的代码中,我们创建了一个非线程安全的HashMap对象 hashMap,然后通过 synchronizedMap() 方法将其包装成线程安全的 synchronizedMap 对象。


自定义的 SynchronizedMap 类实现了 Map 接口,并对所有的操作方法进行了同步化处理,即使用 synchronized 关键字修饰方法,以保证在多线程环境下的线程安全性。


当多个线程同时对 synchronizedMap 进行操作时,只有一个线程能够获得锁并执行操作,其他线程需要等待。这样就确保了数据一致性和线程安全性。


需要注意的是,虽然同步包装器确保了线程安全性,但在高度并发的场景下,性能可能会受限。因为当一个线程持有锁进行操作时,其他线程需要等待,从而降低了整体的吞吐量。因此,在高并发场景中,可以考虑使用专门为高并发设计的集合类,如ConcurrentHashMap,以提供更好的性能。



Hashtable使用了粗粒度的锁,对整个对象进行同步,而同步包装器采用了细粒度的锁,只对具体的操作方法进行同步,并使用一个私有的辅助对象作为锁。这使得同步包装器在多线程环境下能够更高效地处理并发操作。


相关文章
|
6天前
|
XML Java 编译器
Java注解的底层源码剖析与技术认识
Java注解(Annotation)是Java 5引入的一种新特性,它提供了一种在代码中添加元数据(Metadata)的方式。注解本身并不是代码的一部分,它们不会直接影响代码的执行,但可以在编译、类加载和运行时被读取和处理。注解为开发者提供了一种以非侵入性的方式为代码提供额外信息的手段,这些信息可以用于生成文档、编译时检查、运行时处理等。
31 7
|
24天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
64 2
|
12天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
36 14
|
23天前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
29天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
6天前
|
JavaScript 安全 Java
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
|
17天前
|
Java 编译器 程序员
Java面试高频题:用最优解法算出2乘以8!
本文探讨了面试中一个看似简单的数学问题——如何高效计算2×8。从直接使用乘法、位运算优化、编译器优化、加法实现到大整数场景下的处理,全面解析了不同方法的原理和适用场景,帮助读者深入理解计算效率优化的重要性。
25 6
|
23天前
|
监控 前端开发 Java
【技术开发】接口管理平台要用什么技术栈?推荐:Java+Vue3+Docker+MySQL
该文档介绍了基于Java后端和Vue3前端构建的管理系统的技术栈及功能模块,涵盖管理后台的访问、登录、首页概览、API接口管理、接口权限设置、接口监控、计费管理、账号管理、应用管理、数据库配置、站点配置及管理员个人设置等内容,并提供了访问地址及操作指南。
|
24天前
|
存储 网络协议 安全
30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场
本文精选了 30 道初级网络工程师面试题,涵盖 OSI 模型、TCP/IP 协议栈、IP 地址、子网掩码、VLAN、STP、DHCP、DNS、防火墙、NAT、VPN 等基础知识和技术,帮助小白们充分准备面试,顺利踏入职场。
70 2
|
4月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。