后端面试题

牛客上看到的后端面试,这里码一下。
在这里插入图片描述
面经解析:

  1. Java的数据类型
    Java的数据类型包括基本数据类型和引用数据类型:
    基本数据类型:byte, short, int, long, float, double, char, boolean。
    引用数据类型:class, interface, array。

  2. list和set实现类
    List实现类:ArrayList, LinkedList, Vector。
    Set实现类:HashSet, TreeSet, LinkedHashSet。

  3. ArrayList和LinkedList的区别
    ArrayList和LinkedList都是Java集合框架中的实现类,它们分别基于数组和链表的数据结构。以下是它们之间的一些主要区别:
    底层数据结构:
    ArrayList使用动态数组实现。它的内部是一个数组,当数组容量不足时,会自动进行扩容。
    LinkedList使用双向链表实现。每个元素都包含一个指向前一个元素和一个指向后一个元素的引用。
    随机访问性能:
    ArrayList支持快速的随机访问,因为它是基于数组的,可以通过索引直接访问元素。
    LinkedList在随机访问时性能较差,因为必须从链表的头部或尾部开始遍历,直到找到目标元素。
    插入和删除操作性能:
    ArrayList在中间插入或删除元素时性能较差,因为需要移动数组中的元素。
    LinkedList在插入和删除元素时性能较好,因为只需要改变相邻元素的引用。
    空间复杂度:
    ArrayList相对较省空间,因为它只需要存储元素值和数组容量。
    LinkedList相对较耗费空间,因为每个元素都需要额外的两个引用字段。
    迭代器性能:
    ArrayList上的迭代器性能较好,因为它可以通过索引直接访问元素。
    LinkedList上的迭代器性能较差,因为必须沿着链表一个一个地移动。
    适用场景:
    如果需要频繁进行随机访问,使用ArrayList更为合适。
    如果需要频繁进行插入和删除操作,特别是在集合的中间位置,使用LinkedList更为合适。
    选择使用哪个取决于具体的使用场景和操作需求。如果不确定,通常来说,ArrayList是一个更通用的选择,因为它在大多数常见的操作上都表现得很好。

  4. HashSet加入元素的过程
    HashSet 是基于哈希表实现的无序集合,它使用哈希算法来存储和检索元素。下面是向 HashSet 中加入元素的过程:

计算哈希码(Hash Code):

当你向 HashSet 中添加一个元素时,首先会调用该元素的 hashCode() 方法,得到元素的哈希码。
如果元素为 null,则它的哈希码为 0。
映射到桶位置(Bucket Position):

哈希码经过一系列的变换和运算,被映射到哈希表中的一个桶位置(bucket position)。
桶位置是一个数组索引,表示存储元素的位置。
处理哈希冲突:

哈希表可能存在冲突,即不同元素映射到相同的桶位置。为了解决冲突,HashSet 使用链表或红黑树(在JDK 8之后)来存储相同桶位置上的元素。
如果桶位置上已经有一个元素,新元素会被添加到链表或红黑树的末尾。
检查元素唯一性:

在添加元素的过程中,HashSet 会通过调用元素的 equals() 方法来检查元素的唯一性。
如果已经存在相同的元素(根据 equals() 判断),新元素不会被加入。
HashSet 的添加过程通过哈希码和哈希表的桶来实现,确保元素的快速存储和检索。因为哈希表的桶位置是通过哈希码计算得到的,所以元素的存储位置在理想情况下是均匀分布的。这有助于在大多数情况下实现 O(1) 时间复杂度的添加、删除和查找操作。

  1. HashMap线程安全吗?为什么不安全?
    HashMap 在多线程环境下不是线程安全的。这是因为 HashMap 的实现是基于哈希表的,而哈希表的操作涉及到多个步骤,包括计算哈希码、定位桶位置、插入或检索元素等。在多线程环境下,多个线程同时对 HashMap 进行修改操作可能导致数据不一致或者丢失。

以下是一些可能导致线程不安全的情况:

竞态条件(Race Condition): 多个线程同时尝试插入或删除元素时,可能导致竞态条件。两个线程可能同时检测到某个位置为空,然后都尝试插入元素,导致其中一个线程的操作被覆盖。

扩容操作: 当 HashMap 需要扩容时,会创建一个新的数组并将旧的元素重新分配到新数组中。在这个过程中,如果有其他线程同时对 HashMap 进行修改,可能会导致元素在扩容过程中丢失或者被重复添加。

为了在多线程环境下保证线程安全,可以使用 ConcurrentHashMap 类,它提供了一些并发安全的操作。ConcurrentHashMap 使用分段锁的机制,将哈希表分成多个段,每个段上都有一个独立的锁,从而降低了锁的粒度,提高了并发性能。这样,不同的线程可以同时修改不同的段,避免了整个数据结构的锁竞争。

总的来说,如果需要在多线程环境中使用哈希表,推荐使用 ConcurrentHashMap 而不是 HashMap,以确保线程安全性。

  1. 如何做到让HashMap线程安全?
    在Java中,HashMap本身不是线程安全的,但可以通过以下几种方式来实现线程安全的HashMap:

使用Collections.synchronizedMap方法:

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

这将返回一个线程安全的Map,它在每个方法上都使用同步机制来确保线程安全。但请注意,虽然这确保了每个方法的原子性,但在多个操作之间,仍然可能需要额外的同步。

使用ConcurrentHashMap: ConcurrentHashMap是Java提供的线程安全的Map实现。它使用分段锁机制,每个段相当于一个小的HashMap,不同的段之间互不影响,这样可以提高并发性能。

Map<K, V> concurrentMap = new ConcurrentHashMap<K, V>();

使用Collections.synchronizedMap包装HashMap的迭代器: 如果你使用Collections.synchronizedMap来创建线程安全的HashMap,当你迭代Map时,仍然需要手动同步。你可以通过在迭代器上使用synchronized块来实现:


Map<K, V> synchronizedMap = Collections.synchronizedMap(new HashMap<K, V>());
Set<K> keySet = synchronizedMap.keySet();
synchronized (keySet) {
    Iterator<K> iterator = keySet.iterator();
    while (iterator.hasNext()) {
        K key = iterator.next();
        // 在此处执行操作
    }
}

如果需要线程安全的HashMap,推荐使用ConcurrentHashMap,因为它在并发场景下性能更好。根据具体的需求,选择适合的方法来保证线程安全。

  1. ConcurrentHashMap怎么保证线程安全的?
    ConcurrentHashMap是Java集合框架中的线程安全的Map实现。它采用了一些策略来确保在多线程环境中的安全性:

分段锁(Segmentation): ConcurrentHashMap将整个数据结构分割成多个独立的段(segments),每个段独立地管理一部分数据。每个段都类似于一个小的HashMap,有自己的锁。这样,不同段的数据可以在不同的锁上进行操作,提高了并发度。当一个线程在一个段上进行操作时,其他线程可以同时在其他段上进行操作,减小了竞争范围。

精细化的锁策略: 在ConcurrentHashMap中,只有在读写冲突的时候才会使用锁,而且只锁定与冲突相关的段,而不是整个Map。这种细粒度的锁策略减小了锁的争用,提高了并发性能。

读操作的无锁支持: ConcurrentHashMap对于读操作提供了无锁支持,允许多个线程同时进行读取操作,不会阻塞。只有在写操作发生时才需要加锁,确保写操作的原子性和可见性。

CAS(Compare and Swap)操作: ConcurrentHashMap使用CAS操作来确保对数据的原子更新。CAS是一种无锁算法,它比传统的锁机制更轻量级。通过CAS,ConcurrentHashMap可以在不加锁的情况下完成一些简单的操作。

适应性自动调整: ConcurrentHashMap在运行时会根据负载因子、并发度等参数进行自动调整。这使得它在不同的负载和并发情况下都能够保持高效。

ConcurrentHashMap通过使用分段锁、细粒度的锁策略、无锁的读操作和CAS操作等技术,以及适应性自动调整,来保证在多线程环境中的高并发性能和线程安全。这些特性使得ConcurrentHashMap成为处理高并发情况下Map操作的理想选择。

  1. 手撕生产者消费者模型
// 生产者
class Producer implements Runnable {
    private BlockingQueue<Integer> queue;

    public Producer(BlockingQueue<Integer> queue) {
        this.queue = queue;
    }
    
    public void run() {
        try {
            while (true) {
                int value = produce(); // 生产数据
                queue.put(value); // 将数据放入队列
                Thread.sleep(1000); // 模拟生产过程
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    private int produce() {
        // 生产过程
        return 1;
    }
}

// 消费者
class Consumer implements Runnable {
    private BlockingQueue<Integer> queue;

    public Consumer(BlockingQueue<Integer> queue) {
        this.queue = queue;
    }
    
    public void run() {
        try {
            while (true) {
                int value = queue.take(); // 从队列中取出数据
                consume(value); // 消费数据
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    private void consume(int value) {
        // 消费过程
    }
}

  1. 手撕两个线程抢票代码,有没有其他方式保证线程安全?
// 使用synchronized关键字保证线程安全
class TicketSystem {
    private int tickets = 100;

    public synchronized void sellTicket() {
        if (tickets > 0) {
            System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余票数:" + --tickets);
        }
    }
}

// 或使用ReentrantLock
class TicketSystem {
    private int tickets = 100;
    private ReentrantLock lock = new ReentrantLock();

    public void sellTicket() {
        lock.lock();
        try {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余票数:" + --tickets);
            }
        } finally {
            lock.unlock();
        }
    }
}

  1. Volatile关键字的作用
    volatile是Java关键字之一,它主要用于保证多线程环境下变量的可见性和禁止指令重排序。volatile关键字的主要作用包括:

可见性(Visibility): 当一个变量被声明为volatile时,意味着这个变量可能会被多个线程同时访问,且不同线程之间的修改操作是可见的。具体来说,如果一个线程修改了一个volatile变量的值,这个修改对其他线程是可见的,其他线程会立即看到这个变量的最新值。

禁止指令重排序(Ordering): volatile关键字还有禁止指令重排序的作用。在不使用volatile的情况下,编译器和处理器可能会对指令进行重排序,这在多线程环境下可能导致意外的行为。通过将变量声明为volatile,可以防止编译器和处理器对其进行重排序,确保按照代码的顺序执行。

使用volatile的经典场景包括:

标志位: 在多线程环境中,一个线程设置一个volatile标志位,另一个线程检查这个标志位,以便在某个条件满足时通知其他线程停止执行或执行某个操作。

单例模式中的双检锁: 在双检锁机制中,为了避免指令重排序,需要将单例对象声明为volatile。

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

volatile不能保证复合操作的原子性。如果一个操作涉及到多个变量的读写,而且这些操作必须在一个原子步骤内完成,那么volatile就无法满足需求,此时可能需要使用其他的同步机制,例如使用java.util.concurrent包中的原子类。

  1. Atomic包用过吗?
    java.util.concurrent.atomic 包提供了一组用于在多线程环境中进行原子操作的类。这些类通过使用硬件级别的原子性操作或者利用 sun.misc.Unsafe 提供的 CAS(Compare-And-Swap)操作来确保对变量的操作是原子的。这些类大多数都是基于原始数据类型的,例如 int、long,还有一些是引用类型。

以下是 java.util.concurrent.atomic 包中一些主要的类以及它们的用途:

AtomicInteger: 用于对整数进行原子操作,支持原子的自增(incrementAndGet())、自减(decrementAndGet())等操作。

AtomicLong: 用于对长整型进行原子操作,同样支持原子的自增、自减等操作。

AtomicBoolean: 用于对布尔类型进行原子操作,支持原子的设置和获取操作。

AtomicReference: 用于对引用类型进行原子操作,支持原子的获取和设置引用对象。

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray: 用于对数组中的元素进行原子操作,提供了一些原子性的数组操作。

AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater: 用于对类的字段进行原子更新,允许在并发环境中对对象的字段进行原子性操作。

这些原子类提供了一种比使用 synchronized 关键字更轻量级的线程安全机制,特别适用于一些简单的计数器、状态标志等场景。在需要进行原子操作而又不需要全局的锁的情况下,这些类可以提供更好的性能。

虽然这些类提供了原子性的操作,但并不是所有的操作都可以用原子方式完成,因此在使用时仍然需要注意保证原子性的操作是否符合预期。

  1. 索引是什么?为什么能提高查询效率?
    索引是数据库中用于加速查询的一种数据结构,通过存储一定规则的索引信息,可以快速定位到符合条件的记录。
    索引提高查询效率的原因是它减少了需要扫描的数据量,使得数据库能够更快地定位到符合条件的数据。
    在这里插入图片描述
    原篇记录:https://www.nowcoder.com/discuss/567772285646446592
    希望对我找到实习有帮助。fighting~
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值