java中的一些基础面试题

下面的面试题都是想起什么、遇到什么就记录下来,没有什么顺序,会比较跳跃,会持续更新(学又学不完,记又记不住),问题解答均由AI生成,仅供参数与记录

1.Java中有哪些异常类?

Java中的异常分为多种类型,以下是一些常见的异常:

  • 运行时异常(RuntimeException

    • ArithmeticException:算术条件异常,如整数除零等。
    • ArrayIndexOutOfBoundsException:数组索引越界异常,当对数组的索引值为负数或大于等于数组大小时抛出。
    • ClassCastException:类转换异常,试图将对象强制转换为不是其子类型的类时抛出。
    • IllegalArgumentException:非法参数异常,向方法传递了一个不合法或不正确的参数时抛出。
    • NullPointerException:空指针异常,在要求使用对象的地方使用了null时抛出。
    • NumberFormatException:数字格式异常,字符串转换为数字抛出的异常。
    • StringIndexOutOfBoundsException:字符串索引超出范围抛出的异常。
  • 检查型异常(Checked Exception

    • ClassNotFoundException:找不到类异常,试图根据字符串形式的类名构造类,而在遍历calsspath之后找不到对应名称的class文件时抛出。
    • CloneNotSupportedException:不支持克隆异常,当调用Object类中的clone方法克隆对象,但该对象的类无法实现Cloneable接口时抛出。
    • FileNotFoundException:文件未找到异常,试图访问一个不存在的文件时抛出。
    • IOException:操作输入流和输出流时可能出现的异常。
    • NoSuchFieldException:字段未找到异常。
    • NoSuchMethodException:方法未找到抛出的异常。
    • SQLException:操作数据库异常类。
  • 错误(Error

    • VirtualMachineError:虚拟机运行错误。
    • NoClassDefFoundError:类定义错误。
    • OutOfMemoryError:内存不足错误。
    • StackOverflowError:栈溢出错误。

2.java中怎么开启新事务?

在Java中,开启一个新的事务主要有以下几种方法:

  • 使用Spring框架的声明式事务管理

    • 配置事务管理器:首先需要在Spring的配置文件(如applicationContext.xml)中定义事务管理器bean。例如,使用DataSourceTransactionManager来管理基于JDBC的数据源事务。
    • 使用@Transactional注解:这是最常用的方式。将@Transactional注解添加到需要事务管理的方法上,Spring会自动为该方法开启和管理事务。当方法执行成功时,事务会自动提交;如果方法执行过程中出现异常,事务则会自动回滚。
      • 示例代码:
        import org.springframework.stereotype.Service;
        import org.springframework.transaction.annotation.Transactional;
        
        @Service
        public class MyService {
            @Autowired
            private MyRepository myRepository;
        
            @Transactional
            public void performTransactionalOperation() {
                //业务逻辑
                myRepository.save(new Entity());
                //更多数据库操作
            }
        }
        
    • 设置传播行为:通过设置@Transactional注解的propagation属性可以指定事务的传播行为,常见的传播行为包括:
      • REQUIRED:如果当前没有事务,则开启一个新事务;如果已经存在一个事务,则加入该事务。
      • REQUIRES_NEW:总是开启一个新事务,暂停当前事务(如果存在)。
      • NESTED:如果当前存在事务,则在嵌套事务中执行,否则行为与REQUIRED类似。
  • 使用Java EE的JTA(Java Transaction API)

    • 注入UserTransaction对象:在Java EE应用程序中,通常通过@Resource注解注入UserTransaction对象来管理事务。
    • 开启和提交事务:在需要的地方调用userTransaction.begin()开启一个新事务,执行完业务逻辑后调用userTransaction.commit()提交事务。如果发生异常,则调用userTransaction.rollback()回滚事务。
      • 示例代码:
        import javax.annotation.Resource;
        import javax.transaction.UserTransaction;
        
        public class MyService {
            @Resource
            private UserTransaction userTransaction;
        
            public void performTransactionalOperation() {
                try {
                    userTransaction.begin();
                    //业务逻辑
                    userTransaction.commit();
                } catch (Exception e) {
                    try {
                        userTransaction.rollback();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                    throw new RuntimeException(e);
                }
            }
        }
        
  • 使用JDBC手动管理事务

    • 获取数据库连接:通过DriverManager.getConnection或其他方式获取数据库连接。
    • 禁用自动提交:调用connection.setAutoCommit(false)禁用连接的自动提交模式。
    • 提交或回滚事务:在执行完业务逻辑后,如果没有异常发生则调用connection.commit()提交事务;如果出现异常则调用connection.rollback()回滚事务。
      • 示例代码:
        import java.sql.Connection;
        import java.sql.DriverManager;
        import java.sql.SQLException;
        
        public class MyService {
            public void performTransactionalOperation() {
                Connection connection = null;
                try {
                    connection = DriverManager.getConnection("jdbc:yourdatabaseurl", "username", "password");
                    connection.setAutoCommit(false); //禁用自动提交
                    //业务逻辑
                    //如:connection.prepareStatement("SQL语句").executeUpdate();
                    connection.commit(); //提交事务
                } catch (SQLException e) {
                    if (connection != null) {
                        try {
                            connection.rollback(); //回滚事务
                        } catch (SQLException ex) {
                            ex.printStackTrace();
                        }
                    }
                    e.printStackTrace();
                } finally {
                    if (connection != null) {
                        try {
                            connection.close(); //关闭连接
                        } catch (SQLException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        

3.java中操作文件的流有哪些?

在Java中,操作文件的流主要分为字节流和字符流两种,它们各自包含输入流和输出流。以下是对这些流的详细介绍:

  • 字节流

    • FileInputStream:从文件中读取字节数据。它是InputStream的子类,用于读取文件内容。
    • FileOutputStream:向文件中写入字节数据。它是OutputStream的子类,用于将数据写入文件。
    • BufferedInputStream:为FileInputStream提供缓冲功能,提高读取效率。它通过内部缓冲区来减少实际的磁盘I/O操作次数。
    • BufferedOutputStream:为FileOutputStream提供缓冲功能,提高写入效率。与BufferedInputStream类似,它也是通过内部缓冲区来优化写入性能。
    • DataInputStream:允许从一个输入流中读取基本数据类型(如intfloat等)和字符串。
    • DataOutputStream:允许向一个输出流中写入基本数据类型(如intfloat等)和字符串。
  • 字符流

    • FileReader:从文件中读取字符数据。它是Reader的子类,用于读取文件内容。
    • FileWriter:向文件中写入字符数据。它是Writer的子类,用于将数据写入文件。
    • BufferedReader:为FileReader提供缓冲功能,提高读取效率。它通过内部缓冲区来减少实际的磁盘I/O操作次数。
    • BufferedWriter:为FileWriter提供缓冲功能,提高写入效率。与BufferedReader类似,它也是通过内部缓冲区来优化写入性能。
    • InputStreamReader:将字节流转换为字符流。它使用指定的字符集将字节流解码为字符。
    • OutputStreamWriter:将字符流转换为字节流。它使用指定的字符集将字符编码为字节。

4.threadlocal在java 里的作用是什么?

ThreadLocal 是 Java 中提供的一种机制,用于在多线程环境中为每个线程提供独立的变量副本。它的主要作用是解决多线程环境下的变量共享问题,避免线程间的数据干扰。

  • 主要作用

    • 线程隔离:每个线程都有自己独立的变量副本,互不干扰。
    • 简化代码:避免了显式同步(如使用 synchronized)来保护共享变量,从而简化了并发编程。
    • 性能优化:减少了锁的使用,提高了程序的性能。
  • 示例
    以下是一个简单的示例,展示了如何使用 ThreadLocal 来存储每个线程独有的数据。

public class ThreadLocalExample {
    // 创建一个 ThreadLocal 对象,初始值为 null
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 创建两个线程
        Thread thread1 = new Thread(new MyRunnable(), "Thread-1");
        Thread thread2 = new Thread(new MyRunnable(), "Thread-2");

        thread1.start();
        thread2.start();
    }

    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            // 设置当前线程的 ThreadLocal 值
            threadLocal.set((int) (Math.random() * 100));

            // 获取当前线程的 ThreadLocal 值
            System.out.println(Thread.currentThread().getName() + " initial value: " + threadLocal.get());

            try {
                // 模拟一些工作
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 再次获取当前线程的 ThreadLocal 值
            System.out.println(Thread.currentThread().getName() + " final value: " + threadLocal.get());
        }
    }
}

在这个示例中,我们创建了一个 ThreadLocal 对象,并在两个不同的线程中分别设置和获取其值。每个线程都会拥有自己独立的 ThreadLocal 变量副本,因此不会相互干扰。

  • 注意事项
    • 内存泄漏:由于 ThreadLocal 变量是线程私有的,如果不及时清理,可能会导致内存泄漏。可以使用 ThreadLocal.remove() 方法手动清理。
    • 适用场景:适用于需要每个线程有自己独立实例的场景,例如数据库连接、会话信息等
  • 总结
    ThreadLocal 提供了一种简单而有效的方法来解决多线程环境下的变量共享问题,通过为每个线程提供独立的变量副本,避免了线程间的干扰,同时简化了并发编程的复杂性。

5.说说Spring IOC和AOP

Spring IoC(Inversion of Control,控制反转)和AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的两个核心概念,它们在软件开发中扮演着重要的角色。以下是对这两个概念的详细解释及示例:

  • Spring IoC(控制反转)

    • 概念
      IoC是一种设计原则,它将对象的创建权和管理权从应用程序本身反转到外部框架(如Spring容器)。这样,对象之间的依赖关系不再由对象自身来管理,而是由外部实体(如Spring容器)来负责。这种设计原则可以降低对象之间的耦合度,提高系统的可维护性和可扩展性。

    • 实现方式
      IoC主要通过依赖注入(Dependency Injection,简称DI)来实现。依赖注入有三种类型:构造器注入、Setter方法注入和注解注入。

    • 示例
      以下是一个使用注解实现IoC的简单示例:

      • 定义接口与实现类

        // 定义一个服务接口
        public interface UserService {
            void saveUser();
        }
        
        // 实现该接口
        @Component
        public class UserServiceImpl implements UserService {
            @Override
            public void saveUser() {
                System.out.println("用户数据保存成功!");
            }
        }
        
      • 使用@Autowired自动注入

        @Controller
        public class UserController {
            @Autowired
            private UserService userService;
        
            public void execute() {
                userService.saveUser();
            }
        }
        

    在这个示例中,UserController依赖于UserService,但通过使用@Autowired注解,Spring会自动将UserServiceImpl的实例注入到UserController中,从而实现了控制反转[1]。

  • Spring AOP(面向切面编程)

    • 概念
      AOP是一种编程范式,它允许开发者在不修改业务逻辑代码的情况下,为已有的方法增加额外的行为。这些额外的行为通常被称为“切面”(Aspect),它们可以在方法执行之前、之后或围绕方法执行时动态地织入到目标对象中。

    • 核心概念

      • 切面(Aspect):封装横切关注点的模块化单元。
      • 连接点(Join Point):程序执行过程中的某一点,如方法调用或异常抛出。
      • 切点(Pointcut):用于匹配连接点的表达式。
      • 通知(Advice):切面的具体逻辑,定义在切点处运行的操作。
      • 目标对象(Target Object):被织入切面的原始对象。
      • 代理对象(Proxy Object):通过动态代理技术生成的对象,用于在目标对象的方法调用前后添加额外的行为。
    • 示例
      以下是一个使用AspectJ注解实现日志记录功能的AOP示例:

      • 定义切面类

        @Aspect
        @Component
        public class LogAspect {
            // 定义一个切点,匹配com.example.service包下所有类的save方法
            @Pointcut("execution(* com.example.service.*.save(..))")
            public void logPointcut() {}
        
            // 在切点处添加前置通知,记录方法执行前的日志
            @Before("logPointcut()")
            public void doBefore() {
                System.out.println("执行前通知:方法开始执行");
            }
        
            // 在切点处添加后置通知,记录方法执行后的日志
            @After("logPointcut()")
            public void doAfter() {
                System.out.println("执行后通知:方法执行结束");
            }
        }
        
      • 应用切面
        将上述切面类添加到Spring容器的配置中(如果使用注解配置的话,确保组件扫描路径包含了切面类的包),Spring会自动将切面织入到匹配的目标对象中。当save方法被调用时,日志记录功能就会自动触发。

6.Spring Boot中有哪些常用注解

Spring Boot中有许多常用注解,这些注解在简化配置、快速构建应用程序方面发挥着重要作用。以下是一些常用的Spring Boot注解及其简要说明:

  • 核心注解

    • @SpringBootApplication:这是一个组合注解,通常用于主应用程序类,标志着这是Spring Boot应用程序的入口点。它包含了其他注解,如@Configuration@ComponentScan@EnableAutoConfiguration
    • @Configuration:用于标记一个类作为配置类,它通常用于定义Bean
    • @ComponentScan:用于指定Spring容器扫描组件的基本包路径。
  • Web注解

    • @Controller:标志着一个类是Spring MVC控制器,处理HTTP请求。
    • @RestController:结合了@Controller@ResponseBody,用于创建RESTful风格的控制器。
    • @RequestMapping:用于映射HTTP请求到控制器方法,并指定URL路径。
  • Bean管理注解

    • @Component:将一个类标识为Spring组件(Bean),可以被Spring容器自动检测和注册。
    • @Service:用于标记一个类作为业务逻辑的服务组件。
    • @Repository:用于标记一个类作为数据访问组件,通常用于持久层。
  • 依赖注入注解

    • @Autowired:用于自动装配Bean,通常与构造函数、Setter方法或字段一起使用。
    • @Resource:按名称自动注入依赖对象(也可以按类型,但默认按名称)。
    • @Qualifier:与@Autowired一起使用,用于指定要注入的Bean的名称。
  • 数据访问注解

    • @Repository:标志着一个类是Spring Data仓库,用于数据库访问。
    • @Entity:用于定义JPA实体类,映射到数据库表。
  • 其他常用注解

    • @Value:注入配置文件中的值到对应的变量中。
    • @Async:标识异步方法,用于告诉Spring在调用该方法时使用异步线程执行。
    • @Transactional:提供声明式事务管理,用于标识需要使用事务的方法或类。
    • @ExceptionHandler:处理异常情况,用于定义全局的异常处理方法。
    • @Scheduled:定时任务注解,用于标识定时任务的方法。
    • @Valid:开启数据验证功能,用于对请求参数进行校验。

7.java中常见锁

Java中的锁机制是多线程编程中确保线程安全的重要工具,以下是常见的锁类型及其特点:

  • 内置锁(synchronized 关键字)
    • 特点:基于对象监视器(Monitor),自动获取和释放锁,可重入,非公平锁。
    • 使用方式
      synchronized (object) {
          // 临界区代码
      }
      
    • 优化机制(JVM层面):
      • 偏向锁:减少无竞争时的开销,标记线程ID。
      • 轻量级锁:通过CAS自旋尝试获取锁,减少阻塞。
      • 重量级锁:竞争激烈时,升级为操作系统级别的互斥锁。

  • 显式锁(java.util.concurrent.locks.Lock 接口)

    • ReentrantLock(可重入锁)
    • 特点:可重入、支持公平/非公平模式、可中断、超时获取锁。
    • 示例
      Lock lock = new ReentrantLock();
      lock.lock();
      try {
          // 临界区代码
      } finally {
          lock.unlock();
      }
      
    • 公平性:公平锁按请求顺序分配,非公平锁允许插队,性能更高。
  • 读写锁(ReentrantReadWriteLock

    • 特点:读共享(允许多线程同时读),写独占(仅一个线程写)。
    • 使用场景:读多写少的场景(如缓存)。
    • 示例
      ReadWriteLock rwLock = new ReentrantReadWriteLock();
      Lock readLock = rwLock.readLock();   // 读锁
      Lock writeLock = rwLock.writeLock(); // 写锁
      
  • StampedLock(JDK8+)

    • 特点:支持三种模式(写锁、读锁、乐观读),乐观读不阻塞写操作。
    • 示例
      StampedLock stampedLock = new StampedLock();
      // 写锁
      long stamp = stampedLock.writeLock();
      try { /* 写操作 */ } finally { stampedLock.unlockWrite(stamp); }
      
      // 乐观读
      long stamp = stampedLock.tryOptimisticRead();
      if (!stampedLock.validate(stamp)) {
          stamp = stampedLock.readLock(); // 退化为悲观读
      }
      try { /* 读操作 */ } finally { stampedLock.unlockRead(stamp); }
      
  • 共享锁与排他锁

    • 排他锁(Exclusive Lock):如 synchronizedReentrantLock,同一时刻仅一个线程持有。
    • 共享锁(Shared Lock):如读锁(ReadWriteLock)、信号量(Semaphore),允许多线程并发访问。
  • 条件变量(Condition

    • 作用:与显式锁(如 ReentrantLock)配合,实现线程间协调(等待/通知)。
    • 示例
      Lock lock = new ReentrantLock();
      Condition condition = lock.newCondition();
      lock.lock();
      try {
          while (条件不满足) {
              condition.await(); // 释放锁并等待
          }
          // 条件满足后执行操作
          condition.signalAll(); // 唤醒其他线程
      } finally {
          lock.unlock();
      }
      
  • 其他同步工具

    • Semaphore(信号量):控制同时访问资源的线程数(如限流)。
    • CountDownLatch:等待多个线程完成操作后继续。
    • CyclicBarrier:线程到达屏障时等待,直到所有线程就绪。
  • 分布式锁(非Java内置)

    • 场景:跨JVM或分布式系统的锁(如Redis的RedLockZooKeeper)。
    • 实现:基于第三方中间件,确保全局唯一性。
  • 锁的选择建议

    • 简单场景:优先使用 synchronized(自动管理,避免死锁风险)。
    • 复杂需求:选择 ReentrantLock(可中断、超时、公平性等高级功能)。
    • 读多写少:使用 ReentrantReadWriteLockStampedLock
    • 高并发优化:结合锁状态(偏向/轻量级/重量级)和 StampedLock 的乐观读。

8.自定义线程池的拒绝策略有哪些

在Java中,自定义线程池的拒绝策略通过实现 RejectedExecutionHandler 接口定义。以下是常见的 内置拒绝策略自定义策略示例

  • 内置拒绝策略
    线程池(ThreadPoolExecutor)默认提供以下4种策略:

    • AbortPolicy(默认策略)

      • 行为:直接抛出 RejectedExecutionException 异常。
      • 适用场景:需严格确保任务不丢失,且允许通过异常处理机制捕获拒绝的任务。
      • 示例
        new ThreadPoolExecutor.AbortPolicy()
        
    • CallerRunsPolicy

      • 行为:由提交任务的线程(如主线程)直接执行被拒绝的任务。
      • 适用场景:降低任务提交速度,避免线程池过载(但可能阻塞主线程)。
      • 示例
        new ThreadPoolExecutor.CallerRunsPolicy()
        
    • DiscardPolicy

      • 行为:静默丢弃被拒绝的任务,不抛出异常也不执行。
      • 适用场景:允许任务丢弃且无需记录(如日志监控类非关键任务)。
      • 示例
        new ThreadPoolExecutor.DiscardPolicy()
        
    • DiscardOldestPolicy

      • 行为:丢弃队列中等待最久的任务(队首任务),然后重新尝试提交当前任务。
      • 适用场景:新任务优先级高于旧任务(如实时性要求高的场景)。
      • 示例
        new ThreadPoolExecutor.DiscardOldestPolicy()
        
  • 自定义拒绝策略
    通过实现 RejectedExecutionHandler 接口,可灵活扩展拒绝逻辑:

    • 示例:记录日志并持久化任务

      public class CustomRejectionPolicy implements RejectedExecutionHandler {
          @Override
          public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) {
              // 1. 记录任务信息(日志、数据库等)
              System.out.println("任务被拒绝: " + task);
              
              // 2. 持久化任务到磁盘或消息队列(异步重试)
              saveTaskToStorage(task);
              
              // 3. 可选:触发告警通知
              sendAlert("线程池过载,任务已持久化!");
          }
      
          private void saveTaskToStorage(Runnable task) {
              // 实现具体存储逻辑(如写入文件、数据库或消息队列)
          }
      
          private void sendAlert(String message) {
              // 发送告警通知(邮件、短信等)
          }
      }
      
    • 使用自定义策略

      ThreadPoolExecutor executor = new ThreadPoolExecutor(
          2,  // 核心线程数
          5,  // 最大线程数
          60, // 空闲线程存活时间
          TimeUnit.SECONDS,
          new LinkedBlockingQueue<>(10), // 任务队列容量
          new CustomRejectionPolicy()     // 自定义拒绝策略
      );
      

  • 拒绝策略的触发条件
    当线程池同时满足以下条件时,触发拒绝策略:

    • 线程池已关闭(调用 shutdown() 后继续提交任务)。
    • 线程池满负荷
      • 核心线程已满。
      • 任务队列已满。
      • 最大线程数已满。
  • 选择策略的建议

策略 适用场景 注意事项
AbortPolicy 需严格保证任务不丢失,允许通过异常处理 需捕获异常,避免程序崩溃
CallerRunsPolicy 限制任务提交速度,防止系统过载 可能阻塞主线程或上游服务
DiscardPolicy 允许静默丢弃非关键任务(如日志、监控) 需确保数据丢失不影响业务
DiscardOldestPolicy 新任务优先级高于旧任务(如实时数据处理) 可能丢失重要历史任务
自定义策略 需要扩展拒绝逻辑(如重试、持久化、降级) 实现复杂度较高,需处理异步逻辑和资源释放
  • 总结
    • 内置策略:快速应对常见场景(如抛异常、降级处理)。
    • 自定义策略:通过实现 RejectedExecutionHandler 满足业务定制需求(如任务重试、异步存储)。
    • 关键原则:根据任务重要性、系统容忍度和性能要求选择合适的策略。

9.解释一下CAS的原理

CAS(Compare And Swap,比较并交换)是一种 无锁并发控制 的核心技术,通过 硬件指令(如CPU原子操作) 实现多线程环境下的原子性操作。其原理可以用以下步骤概括:

  • 1. CAS的核心机制

    • 操作对象:内存位置(变量)的当前值(V)、预期原值(A)、新值(B)。
    • 执行逻辑
      1. 比较:检查内存位置的值是否等于预期原值(A)。
      2. 交换
        • 如果相等(V == A),将内存位置的值更新为新值(B)。
        • 如果不相等(V ≠ A),说明其他线程已修改过该值,本次操作失败。
    • 原子性保证:整个过程通过 一条CPU指令(如x86的CMPXCHG)完成,避免多线程竞争问题。
  • 示例说明
    以Java的AtomicInteger实现为例:

    AtomicInteger atomicInt = new AtomicInteger(0);
    atomicInt.compareAndSet(0, 1); // CAS操作
    
    • 内存值V:当前atomicInt的值(初始为0)。
    • 预期值A:0。
    • 新值B:1。
    • 结果:若此时V未被其他线程修改,则V会被更新为1,否则操作失败。
  • CAS的底层实现

    • 硬件支持:依赖CPU提供的原子指令(如x86的CMPXCHG,ARM的LDREX/STREX)。
    • JVM封装:通过sun.misc.Unsafe类的本地方法(如compareAndSwapInt)调用底层指令。

  • CAS的典型应用场景

    • 原子类(java.util.concurrent.atomic包)

      • AtomicIntegerAtomicLong等通过CAS实现无锁线程安全操作。
      • 示例:incrementAndGet()方法实现自增:
        public final int incrementAndGet() {
            return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
        }
        
    • 乐观锁

      • 数据库乐观锁、分布式锁(如版本号机制)均基于CAS思想。
    • 同步框架

      • AQS(AbstractQueuedSynchronizer)通过CAS实现锁状态(state)的原子更新。
  • CAS的优缺点

    • 优点

      • 无锁化:避免线程阻塞,减少上下文切换,提高并发性能。
      • 轻量级:适合简单原子操作(如计数器、标志位)。
    • 缺点

      • ABA问题:若变量值从A→B→A,CAS无法感知中间变化。
      • 解决方案:使用版本号(如AtomicStampedReference)。
      • 自旋开销:竞争激烈时,CAS可能长时间循环尝试(自旋),浪费CPU资源。
      • 单一变量限制:只能保证单个变量的原子性,无法直接支持复合操作。
  • ABA问题详解

    • 场景描述

      • 线程1读取变量值为A。
      • 线程2将变量修改为B,再改回A。
      • 线程1执行CAS操作,发现值仍为A,误认为未被修改。
    • 解决方案
      使用AtomicStampedReferenceAtomicMarkableReference,通过 版本号/标记 跟踪变量变化:

      AtomicStampedReference<Integer> atomicRef = 
          new AtomicStampedReference<>(100, 0); // 初始值100,版本号0
      
      int[] stampHolder = new int[1];
      int currentValue = atomicRef.get(stampHolder); // 获取值和当前版本号
      atomicRef.compareAndSet(currentValue, 200, stampHolder[0], stampHolder[0] + 1);
      
  • CAS与锁的对比

特性 CAS 锁(synchronized/Lock)
线程阻塞 无阻塞(自旋) 可能阻塞(等待锁释放)
适用场景 简单原子操作(如计数器) 复杂临界区逻辑
性能开销 低(无上下文切换) 高(上下文切换、锁竞争)
编程复杂度 高(需处理ABA问题、自旋逻辑) 低(自动管理临界区)
  • 总结
    CAS通过硬件支持的原子操作,实现了高效的无锁并发控制,是高性能并发编程的基石。但它需要开发者谨慎处理ABA问题和自旋开销,适合简单原子操作场景。在Java中,CAS被广泛应用于原子类、AQS框架及并发工具类(如ConcurrentHashMap)。

10.ConcurrentHashMap和HashMap有什么区别

ConcurrentHashMapHashMap是Java中两种常用的集合类,它们在多线程环境下的表现和性能有着显著的区别。以下是两者的详细对比:

  • 线程安全性

    1. HashMap:不是线程安全的,在多线程环境中,如果多个线程同时访问并修改HashMap,可能会导致数据不一致或产生并发修改异常(ConcurrentModificationException)。例如,在一个线程正在遍历HashMap的同时,另一个线程对其进行了修改操作,就可能会抛出该异常。
    2. ConcurrentHashMap:是线程安全的,它采用了多种机制来保证并发访问的正确性。在Java 7中,使用分段锁机制,将数据分成多个段,每个段都有自己的锁,不同线程可以同时访问不同的段,从而提高并发性能;在Java 8及以后,使用了CAS(Compare-And-Swap)操作和synchronized关键字的组合来实现更高效的并发控制,只有在需要对某个节点进行修改时,才会对该节点进行加锁,减少了锁的竞争。
  • 迭代器是否是fail-fast的

    1. HashMap:其迭代器实现了fail-fast机制,即在遍历过程中如果有其他线程修改了HashMap,可能会抛出ConcurrentModificationException异常,但这并不是绝对的,取决于具体的修改操作和遍历方式。
    2. ConcurrentHashMap:同样具有fail-fast的迭代器,但是在并发环境下,由于其内部机制的原因,遍历时的异常情况相对更复杂一些,不过总体上也是为了保证数据的一致性和正确性。
  • 并发性能

    1. HashMap:由于不是线程安全的,在单线程环境下,其操作无需额外的同步开销,因此性能相对较高。但在多线程环境中,如果不采取任何同步措施直接使用,会导致严重的线程安全问题,影响程序的正确性和稳定性。
    2. ConcurrentHashMap:在多线程环境下具有更好的并发性能,通过细粒度的锁机制或CAS操作,允许多个线程同时读取和写入不同的部分,从而提高了整体的吞吐量。尤其是在读多写少的场景下,ConcurrentHashMap能够充分利用多核CPU的资源,实现高效的并发访问。
  • 初始化和扩容机制

    1. HashMap:初始容量默认为16,负载因子默认为0.75。当元素数量超过容量与负载因子的乘积时,就会触发扩容操作,扩容时会创建一个新的数组,长度是原数组的两倍左右,然后将原数组中的元素重新计算哈希值并分配到新的数组中,这个过程涉及到大量的元素迁移,成本较高。
    2. ConcurrentHashMap:在Java 8中,默认初始容量为16,负载因子为0.75。与HashMap类似,当数据量超过容量时也会进行扩容,但ConcurrentHashMap的扩容过程更加复杂,因为它需要考虑分段锁或并发控制的问题,以确保在扩容过程中数据的一致性和线程安全。
  • 应用场景

    1. HashMap:适用于单线程环境或对线程安全要求不高的场景,例如在一个简单的桌面应用程序中,用于缓存一些临时数据,且不存在多线程并发访问的情况。
    2. ConcurrentHashMap:主要用于多线程环境,如服务器端的应用程序、并发编程中的缓存系统、实时数据处理等场景,能够保证在高并发情况下的数据一致性和系统的稳定运行。
  • 是否支持null键和null值

    1. HashMap:允许键和值为null,可以有一个或多个键值对的键或者值为null。
    2. ConcurrentHashMap:不支持键和值为null,若尝试插入null键或null值,则会抛出NullPointerException异常。
  • 获取方式

    1. HashMap:可以通过构造函数new HashMap()直接创建实例。
    2. ConcurrentHashMap:可以通过构造函数new ConcurrentHashMap()创建实例,也可以通过Collections.newConcurrentMap()方法创建带有初始容量的实例。

11.说说HashMap,HashTable,TreeMap,List,Set的区别

在Java集合框架中,HashMapHashTableTreeMapListSet是常用的数据结构,它们的区别主要体现在接口归属、数据结构特性、线程安全性、性能等方面。以下是它们的详细对比:

  • 1. 接口归属

    • List 和 Set:属于Collection接口的子接口,存储单列数据(单个元素)。
    • HashMap、HashTable、TreeMap:属于Map接口的实现类,存储键值对(双列数据)
  • 核心特性

类型 有序性 元素重复性 底层数据结构 典型实现类
List 按插入顺序(索引访问) 允许重复 动态数组/链表 ArrayList, LinkedList
Set 无序(或按排序规则) 不允许重复 哈希表/红黑树 HashSet, TreeSet
HashMap 不保证顺序(哈希散列) 键唯一,值可重复 数组+链表/红黑树(JDK8+) HashMap, LinkedHashMap
HashTable 不保证顺序 键唯一,值可重复 数组+链表 HashTable
TreeMap 按键的自然顺序或自定义排序 键唯一,值可重复 红黑树 TreeMap
  • 线程安全性
    • 线程安全HashTable(所有方法用synchronized修饰)。
    • 非线程安全List(如ArrayListLinkedList)、Set(如HashSetTreeSet)、HashMapTreeMap
      • 替代方案:使用Collections.synchronizedXXX()包装,或使用并发集合(如ConcurrentHashMap)。

12.hashMap、List的扩容原理是什么

在Java中,HashMapList(以ArrayList为例)的扩容机制是为了动态调整存储空间以适应元素数量的增长。以下是它们的扩容原理详解:

  • ArrayList的扩容原理
    ArrayList基于动态数组实现,当容量不足时自动扩容。

    • 核心步骤

      1. 触发条件

        • 当调用add()方法添加元素时,如果当前元素数量(size)等于数组容量(elementData.length),触发扩容。
      2. 扩容规则

        • 默认容量:初始容量为10(如果未指定)。
        • 扩容公式:新容量 = 旧容量 * 1.5(即增加50%)。
          int newCapacity = oldCapacity + (oldCapacity >> 1); // 位运算等价于 oldCapacity * 1.5
          
        • 最小保证:若手动指定扩容后的容量(如ensureCapacity()),会取用户指定值和默认计算值的较大者。
      3. 数据迁移

        • 创建一个新数组,将旧数组的元素复制到新数组中(Arrays.copyOf)。
        • 时间复杂度为O(n),频繁扩容会影响性能。
    • 示例

      // 初始容量为10
      ArrayList<Integer> list = new ArrayList<>();
      list.add(1); // 添加第1~10个元素时,无需扩容
      list.add(11); // 第11个元素触发扩容,新容量 = 10 * 1.5 = 15
      
    • 优化建议

      • 若已知元素数量,初始化时指定容量(如new ArrayList<>(100)),避免多次扩容。
  • HashMap的扩容原理
    HashMap基于哈希表(数组+链表/红黑树)实现,扩容是为了减少哈希冲突,保证性能。

    • 核心步骤

      1. 触发条件

        • 当元素数量(size)超过阈值(threshold = capacity * loadFactor)时,触发扩容。
        • 默认负载因子(loadFactor)为0.75,默认初始容量为16。
      2. 扩容规则

        • 容量翻倍:新容量 = 旧容量 * 2。
        • 阈值更新:新阈值 = 新容量 * loadFactor。
        • 数据迁移
          • 遍历旧数组的每个桶(bucket),重新计算每个键值对的哈希值,分配到新数组的位置。
          • 在Java 8+中,若链表长度超过8,链表会转为红黑树;迁移时会根据树的大小决定是否拆分为链表。
      3. 哈希重分布

        • 新位置计算:newIndex = hash(key) & (newCapacity - 1)
        • 由于容量是2的幂,扩容后元素的位置可能是原位置或原位置+旧容量(通过高位哈希位判断)。
    • 示例

      // 默认容量16,阈值 = 16 * 0.75 = 12
      HashMap<Integer, String> map = new HashMap<>();
      for (int i = 0; i < 12; i++) {
          map.put(i, "value"); // 第13个元素触发扩容,新容量 = 16 * 2 = 32
      }
      
    • 优化建议

      • 若已知键值对数量,初始化时指定容量(如new HashMap<>(64)),避免多次扩容。
      • 调整负载因子(如new HashMap<>(16, 0.5f))可以更早扩容,减少哈希冲突。
  • 对比总结

特性 ArrayList HashMap
初始容量 10 16
扩容触发条件 size == capacity size > threshold(capacity * loadFactor)
扩容倍数 1.5倍(旧容量 + 旧容量/2) 2倍(旧容量 << 1)
数据迁移成本 O(n)(全量复制) O(n)(重新哈希所有元素)
设计目标 快速随机访问,减少内存浪费 减少哈希冲突,保证查询效率
  • 注意事项
    • 性能影响:频繁扩容会导致性能下降,尽量预估容量。
    • 线程安全
      • ArrayListHashMap均非线程安全,扩容时并发操作可能引发异常。
      • 多线程场景使用CopyOnWriteArrayListConcurrentHashMap

13.HashMap在put时如何判断键是否重复

在Java的HashMap中,put(key, value)操作通过以下步骤判断键是否重复:

  • 核心判断逻辑
    HashMap通过**哈希值(hashCode对象相等性(equals)**共同判断键是否重复:

    • 哈希值相同 → 可能重复(哈希冲突)。
    • equals返回true → 确认重复。
  • put()方法的具体流程
    当调用map.put(key, value)时,流程如下:

    • 步骤1:计算键的哈希值

      int hash = hash(key); // 实际是调用key.hashCode()并经过扰动处理
      
      • 扰动函数:对key.hashCode()进行二次哈希(如异或高位),减少哈希冲突。
        static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
        
    • 步骤2:定位桶(Bucket)

      int index = (n - 1) & hash; // n是数组长度(2的幂)
      
      • 通过哈希值计算数组下标(类似取模运算)。
    • 步骤3:遍历桶内链表/红黑树

      • 如果桶为空 → 直接插入新节点。
      • 如果桶非空 → 遍历桶内所有节点:
        1. 比较哈希值:若哈希值不同 → 不重复,继续遍历。
        2. 比较对象引用或内容
          if (p.hash == hash && 
              ((k = p.key) == key || (key != null && key.equals(k))))
          
          • 若键的引用相同(==)或内容相等(equals)→ 判定重复,替换旧值。
          • 否则 → 继续遍历。
    • 步骤4:处理哈希冲突

      • 链表处理:若链表长度超过8且数组长度≥64 → 转为红黑树。
      • 树处理:若树节点少于6 → 退化为链表。
  • 关键点

    • ** hashCode()equals()的必要性**

      • hashCode():快速定位可能的桶,减少遍历次数。
      • equals():精确判断键是否重复(解决哈希冲突)。
    • (2) 自定义对象作为键
      若使用自定义类作为键,必须重写hashCode()equals()

      • 不重写:默认使用Object的实现(基于内存地址),可能导致逻辑错误。
      • 正确重写:示例:
        class Student {
            String name;
            int id;
        
            @Override
            public int hashCode() {
                return Objects.hash(name, id); // 基于name和id计算哈希值
            }
        
            @Override
            public boolean equals(Object o) {
                if (this == o) return true;
                if (o == null || getClass() != o.getClass()) return false;
                Student student = (Student) o;
                return id == student.id && Objects.equals(name, student.name);
            }
        }
        
    • (3) null键的特殊处理

      • HashMap允许一个null键,存储在数组下标0的位置。
      • 判断重复时,null键只需检查是否存在已有的null键。
  • 4. 示例演示

    HashMap<String, Integer> map = new HashMap<>();
    map.put("a", 1); // 哈希值计算后定位到桶1,插入新节点
    map.put("a", 2); // 哈希值相同 → 检查equals → 重复 → 替换值(1→2)
    map.put(new String("a"), 3); // 哈希值相同,且equals为true → 替换值(2→3)
    map.put(new String("a"), 4); // 同上
    
  • 5. 总结

    • 判断重复的步骤:哈希值 → 引用相等 → equals()内容相等。
    • 性能依赖:良好的hashCode()设计能减少哈希冲突,提升效率。
    • 设计原则:始终为自定义键类正确重写hashCode()equals()

14.final、finally和finalize三个不同的关键字的作用及用途

在Java中,finalfinallyfinalize是三个不同的关键字,它们有不同的作用和用途。以下是关于这三个关键字的详细解释:

  • final

    1. 基本概念final是一个关键字,用于修饰类、方法和变量,表示它们的定义不能被改变。
    2. 具体作用
      • 修饰类:当final修饰一个类时,这个类不能被继承。例如,public final class MyFinalClass {}定义了一个不能被继承的类。
      • 修饰方法:当final修饰一个方法时,这个方法不能被子类重写。例如,public class MyClass { public final void myFinalMethod() {} }定义了一个不能被子类重写的方法。
      • 修饰变量:当final修饰一个变量时,这个变量的值一旦被初始化后就不能被改变。例如,public static final String CONSTANT_STRING = "FINAL_STRING";定义了一个常量字符串。
  • finally

    1. 基本概念finally是一个关键字,用于定义一个代码块,该代码块中的代码在不论是否发生异常的情况下都会被执行。
    2. 具体作用
      • finally块通常与try-catch块一起使用,以确保在处理异常时执行一些必要的清理工作,例如关闭文件、释放资源等。
      • 无论try块中的代码是否抛出异常并被捕获,也无论catch块中的代码是否成功执行,finally块中的代码都会执行。
  • finalize

    1. 基本概念finalize是Object类中的一个方法,用于在垃圾回收器将对象从内存中清除之前通知对象进行清理操作。
    2. 具体作用
      • 当垃圾回收器确定没有其他对象引用当前对象时,会调用该对象的finalize()方法。
      • finalize()方法中,可以进行一些清理操作,如关闭文件、释放内存等。
      • 需要注意的是,finalize()方法的执行时间不确定,且不保证一定会被执行。因此,它不应该被用于关键的清理操作。

15.java中的String、StringBuffer、StringBuilder有什么区别

  1. 可变性

    • String:不可变。一旦创建了String对象,它的值就不能被改变。每次对String对象进行拼接、替换等操作时,实际上是创建了一个新的String对象。
    • StringBuffer:可变。它内部有一个可动态扩展的字符数组,对StringBuffer对象进行修改操作时,不会创建新的对象,而是直接在原对象的基础上进行修改。
    • StringBuilder:可变。和StringBuffer类似,它内部同样有一个可动态扩展的字符数组,对StringBuilder对象进行修改操作时,也是直接在原对象的基础上进行修改。
  2. 线程安全性

    • String:由于String是不可变的,所以它是线程安全的。多个线程可以同时访问同一个String对象,而不会出现数据不一致的问题。
    • StringBuffer:线程安全。它的大部分方法都使用了synchronized关键字进行同步,保证了在多线程环境下对StringBuffer对象的操作是线程安全的。
    • StringBuilder:非线程安全。它的方法没有使用synchronized关键字进行同步,因此在多线程环境下使用StringBuilder可能会出现数据不一致的问题。但在单线程环境下,StringBuilder的性能优于StringBuffer
  3. 性能

    • String:由于String是不可变的,每次对String对象进行修改操作时都需要创建新的对象,这会导致频繁的内存分配和垃圾回收,因此在频繁进行字符串修改操作时,String的性能较差。
    • StringBuffer:是线程安全的,但由于使用了synchronized关键字进行同步,会带来一定的性能开销。在单线程环境下,StringBuffer的性能不如StringBuilder
    • StringBuilder:非线程安全,没有同步开销,因此在单线程环境下,StringBuilder的性能是最好的,尤其是在频繁进行字符串拼接等修改操作时。
  4. 使用场景

    • String:当字符串内容不需要频繁修改,并且在多线程环境下使用时,建议使用String类。例如,存储一些常量字符串、文件名等。
    • StringBuffer:当需要在多线程环境下对字符串进行频繁修改时,建议使用StringBuffer类,以保证线程安全。
    • StringBuilder:当在单线程环境下对字符串进行频繁修改时,建议使用StringBuilder类,以获得更好的性能。

你可能感兴趣的:(java,开发语言,面试)