自学内容网 自学内容网

面经整理 八股 虾皮购物 Java后端开发 上

目录

1.java中的锁机制 什么是可重入锁

锁机制

可重入锁(Reentrant Lock)

特点:

示例代码:

总结

2.可重入锁的使用场景

1. 递归调用

2. 多线程资源共享

3. 复杂对象的状态管理

4. 保证顺序执行

总结

3.讲一下AQS

AQS的基本概念

AQS的主要方法

AQS的实现示例

使用AQS的场景

AQS的优势

总结

4.redis的相关数据结构

字符串(String)

哈希(Hash)

列表(List)

集合(Set)

有序集合(Sorted Set)

5.为什么每种数据类型一般都有两种数据结构空间和时间的权衡数据量小的时候使用更加紧凑的数据结构,节省内存数据量大的时候转换为普通数据结构,提高操作效率以Hash为例:当field-value长度较短且个数较少时,使用压缩列表当数据量增大时,转换为哈希表这种设计能够在内存使用和性能之间取得很好的平衡。

以Hash为例:

6.讲一下JVM相关内存结构

方法区(Method Area):

堆(Heap):

栈(Stack):

程序计数器(Program Counter Register):

本地方法栈(Native Method Stack):

7.HashMap的底层原理

1. 数据结构

2. 哈希函数

3. 添加元素

4. 获取元素

5. 扩容

6. 线程安全

总结


1.java中的锁机制 什么是可重入锁

在Java中,锁机制是用于控制对共享资源的访问,以确保线程安全的关键工具。Java提供了多种锁机制,其中可重入锁(Reentrant Lock)是一种重要的实现。

锁机制

  1. 同步方法和同步块

    • Java使用synchronized关键字来实现简单的锁机制。可以将其应用于方法或代码块,以限制对共享资源的访问。
    • 当一个线程访问被synchronized修饰的方法或代码块时,其他线程必须等待该线程释放锁。
  2. Lock接口

    • Java提供了java.util.concurrent.locks包,其中的Lock接口提供了比synchronized更灵活的锁机制。
    • Lock接口的实现类如ReentrantLock提供了额外的功能,比如尝试获得锁、定时锁等。

可重入锁(Reentrant Lock)

可重入锁是一种特殊类型的锁,允许同一个线程多次获得同一个锁而不会导致死锁。换句话说,如果一个线程已经持有了锁,它可以再次请求该锁而不会被阻塞。

特点:
  1. 重入性

    • 同一个线程可以多次获得同一个可重入锁,每次获得锁时,锁的计数器会增加;每次释放锁时,计数器会减少。当计数器为零时,锁才真正被释放。
  2. 公平性

    • 可重入锁可以选择公平性策略。在公平模式下,线程会按照请求锁的顺序获得锁;而在非公平模式下,线程可以打断等待队列,可能导致某些线程长时间无法获得锁。
  3. 锁的管理

    • 使用可重入锁时,开发者需要手动管理锁的获取和释放,这提供了更大的灵活性。
示例代码:
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void methodA() {
        lock.lock(); // 获取锁
        try {
            System.out.println("In method A");
            methodB(); // 可以在同一线程中再次调用
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public void methodB() {
        lock.lock(); // 再次获取锁
        try {
            System.out.println("In method B");
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();
        example.methodA();
    }
}

在上面的示例中,methodA获取了锁并调用methodB。由于ReentrantLock是可重入的,methodB可以再次获得同一个锁,而不会导致死锁。

总结

可重入锁是Java中非常有用的一个特性,能够帮助开发者更好地管理并发操作,避免死锁,并且在需要时提供灵活的锁控制。通过合理使用可重入锁,可以提高程序的并发性能和稳定性。

2.可重入锁的使用场景

重入锁(Reentrant Lock)在多线程编程中有许多实际应用场景。以下是一些具体应用场景及其示例:

1. 递归调用

可重入锁特别适合于需要递归调用的方法。在一个方法中如果需要调用自身或其他方法时,可以再次获取锁,而不会导致死锁。

import java.util.concurrent.locks.ReentrantLock;

public class RecursiveLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private int counter = 0;

    public void increment() {
        lock.lock();
        try {
            counter++;
            System.out.println("Counter: " + counter);
            if (counter < 5) {  // 控制递归次数
                increment();  // 递归调用
            }
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        RecursiveLockExample example = new RecursiveLockExample();
        example.increment();
    }
}

在这个例子中,increment方法递归调用自身,ReentrantLock允许同一线程多次获取锁。

2. 多线程资源共享

当多个线程需要访问共享资源(如一个计数器、集合等)时,使用可重入锁可以确保在访问这些资源时不会出现竞争条件。

import java.util.concurrent.locks.ReentrantLock;

public class SharedResource {
    private final ReentrantLock lock = new ReentrantLock();
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
            System.out.println("Count: " + count);
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        SharedResource resource = new SharedResource();
        
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                resource.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                resource.increment();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Final Count: " + resource.getCount());
    }
}

在这个例子中,两个线程同时访问SharedResource中的计数器,使用ReentrantLock确保线程安全。

3. 复杂对象的状态管理

在一些复杂对象的状态管理中,可能需要在多个方法中对同一状态进行操作。可重入锁可以避免在同一线程内反复请求锁的复杂性。

import java.util.concurrent.locks.ReentrantLock;

public class ComplexObject {
    private final ReentrantLock lock = new ReentrantLock();
    private String state = "";

    public void updateState(String newState) {
        lock.lock();
        try {
            state = newState;
            processState();  // 可以在同一线程内调用其他方法
        } finally {
            lock.unlock();
        }
    }

    private void processState() {
        // 处理状态
        System.out.println("Processing state: " + state);
    }

    public static void main(String[] args) {
        ComplexObject obj = new ComplexObject();
        obj.updateState("Updated State");
    }
}

在这个例子中,updateState方法安全地更新对象的状态并调用另一个方法进行处理,避免了在同一线程内的死锁问题。

4. 保证顺序执行

在一些情况下,可能需要保证某些方法的执行顺序,比如在初始化过程中。可重入锁可以帮助确保在初始化完成之前不会有其他线程执行。

import java.util.concurrent.locks.ReentrantLock;

public class SequentialExecution {
    private final ReentrantLock lock = new ReentrantLock();
    private boolean initialized = false;

    public void initialize() {
        lock.lock();
        try {
            if (!initialized) {
                // 执行初始化逻辑
                System.out.println("Initializing...");
                initialized = true;
            }
        } finally {
            lock.unlock();
        }
    }

    public void performTask() {
        lock.lock();
        try {
            if (!initialized) {
                initialize();  // 确保初始化完成
            }
            System.out.println("Performing task...");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        SequentialExecution seqExec = new SequentialExecution();
        seqExec.performTask(); // 第一次调用时会初始化
        seqExec.performTask(); // 第二次调用时直接执行任务
    }
}

在这个例子中,performTask方法在执行前会检查是否已经初始化,确保不会在多个线程中重复初始化。

总结

可重入锁在Java多线程编程中提供了灵活性和安全性,尤其在处理递归调用、共享资源、复杂对象状态和任务顺序时非常有效。合理使用可重入锁能够提高程序的稳定性和性能。

3.讲一下AQS

AbstractQueuedSynchronizer(AQS)是Java中的一个重要同步框架,它提供了一种基于队列的同步器基础类。AQS被设计用于实现锁和其他同步器(例如信号量、读写锁等),在Java并发包java.util.concurrent中被广泛使用。

AQS的基本概念

  1. 队列结构:AQS内部使用一个FIFO队列来管理等待获取锁的线程。每个线程在尝试获取锁时,如果获取失败,就会被加入到这个队列中。

  2. 状态管理:AQS通过一个整数值来表示同步状态(即锁的状态)。这个值可以被修改(例如,获取锁时状态加1,释放锁时状态减1)。

  3. 重入机制:AQS支持可重入的锁机制,允许同一线程多次获取锁而不会发生死锁。

  4. 模板方法模式:AQS使用模板方法模式来实现同步器的具体行为。开发者可以通过继承AQS并实现其中的抽象方法来定义具体的锁或其他同步器。

AQS的主要方法

AQS提供了一些重要的方法和机制,以下是一些关键点:

  • tryAcquire(int arg):尝试获取锁的操作。开发者需要实现这个方法,以定义如何获取锁。

  • tryRelease(int arg):尝试释放锁的操作。开发者需要实现这个方法,以定义如何释放锁。

  • acquire(int arg):获取锁的过程,它会调用tryAcquire,如果获取失败,则将当前线程加入到等待队列中。

  • release(int arg):释放锁的过程,它会调用tryRelease,如果释放成功,会唤醒等待队列中的其他线程。

  • getState()setState(int newState):获取和设置当前的状态。

AQS的实现示例

以下是一个简单的基于AQS的独占锁实现的示例:

import java.util.concurrent.AbstractQueuedSynchronizer;

public class MyLock {
    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            // 只允许一个线程获得锁
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }
    }

    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public void unlock() {
        sync.release(1);
    }
}

使用AQS的场景

AQS被用于实现多种并发工具,包括:

  • 独占锁(ReentrantLock)
  • 共享锁(Semaphore)
  • 读写锁(ReentrantReadWriteLock)
  • CountDownLatch
  • CyclicBarrier

AQS的优势

  1. 性能:AQS使用了高效的队列和状态管理机制,能够处理大量线程的竞争。

  2. 灵活性:开发者可以根据需求扩展AQS,实现自定义的同步器。

  3. 支持多种同步策略:AQS可以实现独占锁和共享锁,适用于多种不同的场景。

总结

AbstractQueuedSynchronizer(AQS)是Java中一个强大而灵活的同步工具,它通过队列和状态管理为开发者提供了实现复杂同步器的基础。理解AQS的工作原理有助于更好地利用Java的并发特性,实现高效、安全的多线程程序。

4.redis的相关数据结构

  1. 字符串(String)

    • 简单动态字符串(SDS): Redis 使用 SDS 作为字符串的底层实现,具备可变长度、自动内存管理等特点。
    • 字符串对象: 包含 SDS 字符串和额外的元数据(如编码方式、长度等)。
  2. 哈希(Hash)

    • Ziplist: 适用于存储小哈希表,内存占用更少。
    • 哈希表(dict): 用于存储较大的哈希表,使用更复杂的哈希算法,提供更好的查找性能。
  3. 列表(List)

    • 压缩列表(Ziplist): 用于存储少量元素的列表,节省内存。
    • 链表(linked list): 当列表元素数量增加到一定程度后,Redis 会切换到链表结构,以支持高效的插入和删除操作。
  4. 集合(Set)

    • 压缩列表(Ziplist): 用于存储少量元素的集合。
    • 哈希表(dict): 当集合元素增多时,使用哈希表来提供快速的查找和操作性能。
  5. 有序集合(Sorted Set)

    • 压缩列表(Ziplist): 用于存储少量元素的有序集合。
    • 跳表(Skip List): 当元素数量较大时,使用跳表结构,以支持快速的范围查询和插入操作。

5.为什么每种数据类型一般都有两种数据结构

 

  • 空间和时间的权衡

  • 数据量小的时候使用更加紧凑的数据结构,节省内存

  • 数据量大的时候转换为普通数据结构,提高操作效率

以Hash为例:

  • 当field-value长度较短且个数较少时,使用压缩列表
  • 当数据量增大时,转换为哈希表

这种设计能够在内存使用和性能之间取得很好的平衡。

6.讲一下JVM相关内存结构

JVM(Java虚拟机)的内存结构是Java程序运行时的关键部分,主要分为以下几个区域:

  1. 方法区(Method Area)

    • 存放类的结构信息,比如字段和方法的数据,以及常量池、静态变量等。
    • 在HotSpot JVM中,方法区通常与永久代(PermGen)有关,但在Java 8及以后,永久代被元空间(Metaspace)替代。
  2. 堆(Heap)

    • 用于存放对象实例,是JVM中最大的一块内存区域。
    • 堆是垃圾回收(GC)管理的主要区域,分为年轻代和老年代:
      • 年轻代(Young Generation):大部分新生对象分配在这里,包含三个部分: Eden区和两个Survivor区(S0和S1)。大多数对象在年轻代中存活时间较短。
      • 老年代(Old Generation):存放长期存活的对象,经过多次垃圾回收后,仍然存活的对象会被转移到老年代。
  3. 栈(Stack)

    • 每个线程都有自己的栈,存放局部变量、方法调用和返回地址等信息。
    • 栈是线程私有的,存储方法的调用信息,采用先进后出(LIFO)的结构。
  4. 程序计数器(Program Counter Register)

    • 每个线程都有一个独立的程序计数器,用于存放当前线程执行的字节码的地址。
    • 程序计数器是线程私有的,当线程切换时,可以通过计数器记录每个线程的执行位置。
  5. 本地方法栈(Native Method Stack)

    • 用于支持JVM调用本地方法(Native Methods),与Java栈类似,但主要用于调用非Java代码。

7.HashMap的底层原理

HashMap 是 Java 中一种常用的集合类,它基于哈希表实现,具有高效的插入、删除和查找操作。下面是 HashMap 的底层原理:

1. 数据结构

HashMap 的底层主要使用数组和链表(或红黑树)组合的方式来存储数据。

  • 数组:HashMap 使用一个数组来存储桶(bucket),每个桶可以存储多个键值对。
  • 链表:在每个桶中,如果发生哈希冲突(即不同的键经过哈希函数计算后得到了相同的数组索引),HashMap 会使用链表来存储这些冲突的元素。
  • 红黑树:当某个桶中的链表长度超过一定阈值(默认为 8),HashMap 会将链表转换为红黑树,以提高查找效率。

2. 哈希函数

HashMap 通过哈希函数将键映射到数组的索引上。哈希函数会对键调用 hashCode() 方法,生成一个哈希值。为了确保数组索引的有效性,HashMap 会对哈希值进行扰动处理,避免哈希碰撞和分布不均。

3. 添加元素

  • 当添加一个键值对时,首先计算该键的哈希值,并通过哈希函数找到应该存放该元素的数组索引。
  • 如果该索引处没有元素,则直接插入。
  • 如果该索引处已有元素(发生冲突),则会检查该元素的键是否与新插入的键相等。如果相等,更新值;如果不相等,将新元素添加到链表的末尾。

4. 获取元素

  • 通过键计算哈希值,找到对应的数组索引。
  • 如果该索引没有元素,返回 null。
  • 如果有元素,遍历链表(或红黑树),寻找与指定键匹配的元素,找到后返回对应的值。

5. 扩容

HashMap 默认的初始容量是 16,当元素个数超过负载因子(默认为 0.75,即 75%)时,会触发扩容操作。扩容的过程会将数组的大小翻倍,并重新计算所有键的索引位置,这样可以降低哈希冲突的概率。

6. 线程安全

HashMap 不是线程安全的。如果多个线程并发访问一个 HashMap,并且至少有一个线程对结构进行修改(例如添加或删除元素),则必须通过外部同步机制(例如使用 Collections.synchronizedMapConcurrentHashMap)来保证线程安全。

总结

HashMap 通过使用数组和链表(或红黑树)结合的方式,实现了高效的键值对存储和查找功能。了解其底层原理可以帮助更好地使用 HashMap,同时也能为性能优化提供思路。


原文地址:https://blog.csdn.net/qq_30500575/article/details/143083916

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!