Java 基础常见面试题

目录

Java 概述

变量、运算符、分支和循环

数组

面向对象

异常处理

常用类

IO 流

多线程

网路通信

集合框架

设计模式和反射

JVM 内存结构


汇总 JavaSE 的一些常见面试题,不定期更新。

Java 概述

1. Java 语言的特点

简单性、面向对象性、可移植性、分布性、安全性、健壮性、多线程性、高性能性和动态性。

2. JDK 和 JRE 的区别

JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。
JRE:Java Runtime Environment 的简称,java 运行环境,为 java 的运行提供了所需环境。
简单的来说 JDK 其实包含了 JRE,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分析的工具。简单来说:如果你需要运行 java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。

3. 包的作用是什么

  1. 把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。
  2. 可以更好得维护程序结构。
  3. 可以避免因为类名相同造成的冲突。
  4. 包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。

4.  什么是 Java 虚拟机

变量、运算符、分支和循环

1. 原始数据类型和引用数据类型的区别

1.1 定义方面的不同

int a = 9;
String b = "关为";

如图所示,a 是原始数据类型,值就直接保存在变量中。而 b 是引用类型,变量中保存的只是实际对象的地址。一般称这种变量为"引用",引用指向实际对象,实际对象中保存着内容。

1.2 赋值运算符(=)的区别

a = 8;
b = "关老师";

对于原始类型 a,赋值运算符会直接改变变量的值,原值被覆盖掉。对于引用类型 b,赋值运算符会改变引用中所保存的地址(0x0x-->0x11),原来的地址被覆盖掉。但是原来的对象不会被改变。如上图所示,"关为" 字符串对象没有被改变。但是需要注意的是如果没有其他变量引用这个对象,那么这个对象就会被 JVM 当成是垃圾,被自动清扫。 

1.3 原始类型和引用类型对于方法调用

int a = 8;
String b = "关为";

原始类型变量 a 没有方法,就是对一个值的指代,而引用类型 b 是一个类,可以有方法的调用。

1.4 形式参数传值问题

public void guanwei(int a,String b,User u){
    a = 1;
    b = "关为2";
    u.setName("关为2");
}

public static void main(String[] args){
    App app = new App();
    int a = 3;
    String b = "关为";
    User u = new User(1,"关为",'男');
    app.guanwei(a,b,u);
    System.out.println(a);
    System.out.println(b);
    System.out.println(u);
}

我们观察结果会发现,原始类型 a 的值并没有发送改变,这个好理解,因为原始类型是传值。

User 中的 name 值从"关为"--->"关为2",因为引用类型参数是传递引用。

但是 String 的内容也没有发生改变,String 难道不是引用类型么?这是因为 String 是常量字符串。值不可以发生改变。

2. 原始数据类型都有哪些

原始数据类型也叫基本数据类型,共有 8 种:byte、boolean、char、short、int、float、long 和 double。

3. &和&&之间的区别

  1. &是按位与,&&是逻辑与。
  2. &是二进制进行判断,&&是多个 boolean 类型值关系比较。
  3. &当两侧的表达式的结果均为真时,整个运算结果才为 true。&&当第一个表达式为 false 时,结果为 false,并且不再计算第二个表达式。

数组

1. 什么是数组

2. 数组中存放的是值还是引用

对于原始类型数组,地址就是内容,这里没有区别。对于引用类型,数组中存放的是引用。

3. 请简述冒泡排序法

4. 请简述二分查找法

面向对象

1. == 和 equals 的区别

原始类型没有方法,值和地址一致。引用类型 == 和 equals() 没有区别。因为这个方法在 Object 类中定义的原型是:

public boolean equals(Object obj) {
    return (this == obj);
}

可以看到 equals() 其实就是在比较地址。而我们可以重写这个方法,来按照我们自己的规则来进行关系比较,其中 String 类就是重写了这个方法,所以 String 类的 equals() 是比较字符串值是否相等。 

2. 类和对象的关系

官方的说法是:类是对象的抽象,而对象是类的具体实例。简单的说,类是对象的分类,数据类型,类别,而对象可以看成是这个类型下的一个确切的值。

Person guanwei = new Person();

其中的 new Person(); 就是产生一个对象,guanwei 就是给对象起了一个名字,也就是变量名,而 Person 是 guanwei 变量的数据类型。 

3. 封装的好处是什么,为什么要用到封装

封装(Encapsulation)是面向对象方法的重要原则,就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。
封装是把过程和数据包围起来,对数据的访问只能通过已定义的接口。封装是一种信息隐藏技术。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。有如下一些好处:

  • 提高代码的安全性。
  • 提高代码的复用性。
  • “高内聚”:封装细节,便于修改内部代码,提高可维护性。
  • “低耦合”:简化外部调用,便于调用者使用,便于扩展和协作。

4. 重写和重载的区别

重写是子类重写父类的方法体,而重载是同一个类中多个方法具有相同的名称,不同的形式参数列表。

5. 抽象类必须要有抽象方法么

不一定,抽象类中可以不定义抽象方法,只需要将类声明为抽象类就行。

6. 抽象类的构造器有什么用

7. 如果父类是抽象类,子类是完整类,那么产生子类对象时,会调用父类构造器么

8. 抽象类和接口之间的区别

9. 两个对象的 hashCode()相同,则 equals()也一定为相同么

10. 修饰符有什么作用

Java 中有很多修饰符,比较出名的是 static、final 和 abstract 这三个,它们的作用就是修饰词。

晚上11点,班长在写代码,遇到一个bug,班长解决问题后,睡觉去了。
晚上11点,班长漫不经心地在写代码,遇到一个很简单的bug,班长稀里糊涂解决问题后,满不在乎地睡觉去了。
晚上11点,帅气的班长认认真真地在写代码,遇到一个超难的bug,班长呕心沥血解决问题后,眉开眼笑地睡觉去了。

11. final 修饰符有什么作用,它可以修饰哪些地方

12. final 修饰的变量一定要赋值么

不一定,final 可以修饰成员变量和局部变量,在修饰局部变量时可以不赋值,但只能赋予一次值。

final int a ;
a = 9;

13. 谈一谈你的 static 修饰符的理解 

异常处理

1. 什么是异常,什么是异常处理

异常是程序在运行过程中遇到的不正常现象,它会中断正在运行的程序。

异常处理就是对产生的异常进行处理,分为异常捕获和异常抛出。

2. 异常和错误的区别

3. final、finally 和 finalize() 的区别

4. 运行时异常和编译时异常的区别

5. 请罗列至少5种常见的异常

常用类

1. 如何将字符串反转

2. String str= new String("关为"); 这段代码产生了几个对象,分别是

两个对象,一个是字符串“关为”,另一个是 new 出来的对象 str。 

3. 字符串类都有哪些,它们之间的区别是

4. String 类的常用方法有哪些

IO 流

1. 什么是 IO 流

2. IO 流的分类都有哪些

3. 为什么需要缓冲流

多线程

1. 线程的状态有那些

新建状态:使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start()这个线程。
就绪状态:当线程对象调用了 start()方法之后,该线程就进入就绪状态,该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。就绪状态的线程处于就绪队列中,要等待 JVM 里线程调度器的调度。
运行状态:如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
阻塞状态:如果一个线程执行了 sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

  • 等待阻塞:运行状态中的线程执行 wait()方法,使线程进入到等待阻塞状态。
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中。
  • 其他阻塞:通过调用线程的 sleep()或 join()发出了 I/O 请求时,线程就会进入到阻塞状态。当 sleep()状态超时,join()等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。 

­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­JDK源码状态­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ ­­­­

  • NEW:新建状态
  • RUNNABLE:运行状态(就绪和运行状态)
  • BLOCKED:阻塞状态(锁阻塞)
  • WAITING:等待状态(等待­唤醒阻塞)
  • TIMED_WAITING:有限等待(超时等待阻塞)
  • TERMINATED:结束

2. 线程的阻塞事件有哪些?

   1). BLOCKED阻塞:获取同步锁的阻塞
­         - 当一个正在运行的线程视图获得某个同步锁时,如果获取失败,则该线程进入锁池中等待获得该同步锁。
­         - 当一个处于锁池中等待同步锁的线程获得该同步锁后,该线程就进入到就绪状态,等待获得CPU执行线程。
   2). WAITING等待阻塞:wait­-notify 等待唤醒
­         - 当一个正在运行的线程被其他线程调用了wait()方法后,该线程进入到等待状态,直到调用该线程的 notify() 或 notifyAll() 方法后,处于在状态下的等待线程才会被唤醒,进入就绪状态等待获得CPU资源。
   3). 限时等待阻塞:
­         - 当一个正在运行的线程被其他线程调用了 wait(long timeout) 方法后,该线程进入等待状态,直到等待时间到达或被其他线程调用了notify()或notifyAll()方法后,该线程被唤醒,进入就绪状态等待获得CPU资源。
­         - 当一个正在运行的线程调用了sleep(long millis)方法后,该线程会自动进入到休眠状态放弃CPU资源,当休眠时间到达后,该线程会自动苏醒,进入就绪状态等待获得CPU资源。
   4). IO阻塞:
­         - 当调用了IO操作时,线程会让出CPU资源等待IO操作结束后(IO操作无需获取CPU进行,进行IO操作的磁盘等设备都有独立的芯片,可以直接进行IO操作),重新回到就绪状态等待获取CPU资源。

3. sleep、wait、yield和join的区别

  • sleep:Thread 类的方法,必须带一个时间参数。会让当前线程休眠进入阻塞状态并释放 CPU,提供其他线程运行的机会且不考虑优先级,但如果有同步锁则 sleep 不会释放锁即其他线程无法获得同步锁。
  • yield:让出 CPU 调度,Thread 类的方法,类似 sleep 只是不能由用户指定暂停多长时间,并且 yield() 方法只能让同优先级的线程有执行的机会。yield() 只是使当前线程重新回到可执行状态,所以执行 yield() 的线程有可能在进入到可执行状态后马上又被执行。调用 yield() 方法只是一个建议,告诉线程调度器我的工作已经做的差不多了,可以让别的相同优先级的线程使用 CPU 了,没有任何机制保证采纳。
  • wait:Object 类的方法(notify()、notifyAll() 也是 Object 对象),必须放在同步代码块中,执行该方法的线程会释放锁,进入线程等待池中等待被再次唤醒(notify() 随机唤醒,notifyAll() 全部唤醒,线程结束自动唤醒)即放入锁池中竞争同步锁。
  • join:一种特殊的wait,当前运行线程调用另一个线程的 join 方法,当前线程进入阻塞状态直到另一个线程运行结束等待该线程终止。 注意该方法也需要捕捉异常。等待调用 join 方法的线程结束,再继续执行。

4. 线程安全的集合类有那些?

  • Vector:使用Synchronized锁实现。
  • CopyOnWriteArrayList:线程安全的List集合,内置使用写入复制算法避免快速失败,使用lock锁实现。
  • CopyOnWriteArraySet:线程安全的Set集合,内置使用写入复制算法避免快速失败,使用lock锁实现。
  • ConcurrentHashMap:线程安全的Map集合,内置使用写入复制算法避免快速失败,使用lock锁实现。

5. 什么是Lock锁?

        Lock 锁是比 Synchronized 更加灵活的一种锁机制,是对 Synchronized 的补充,但并不能完全替换 Synchronized 锁。不同的是,Lock 锁需要手动加锁和解锁,加锁通过 lock() 方法实现,解锁通过 unlock() 方法实现。Lock 是一个接口,它的实现主要有 ReentrantLock、ReentrantReadWriteLock 两个类。

public class Ticket {
    private int num = 100;
    //创建Lock对象
    Lock lock = new ReentrantLock(false);
    public void sale(){
        lock.lock();//加锁
        try {
            if(num > 0){
                System.out.println(Thread.currentThread().getName()+":销售第"+num­­+"张票....");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            lock.unlock();//解锁
        }
    }
}

6. synchronized和Lock的区别?

  • Synchronized 是 Java 内置的关键字,是由 JVM 控制的,Lock 是 Java 中的一个接口提供了丰富 API 的方便使用。
  • Synchronized 无法判断锁的状态,Lock 可以判断锁的状态。
  • Synchronized 会自动释放锁,Lock 需要手动释放锁。
  • Synchronized 是可重入锁,不可以中断,属于非公平锁,Lock 是可重入锁,可以判断锁,可以设置公平或非公平锁。
  • Synchronized 适合少量的代码同步问题,Lock 适合大量的同步代码。

7. Fail-Fast是什么?为什么会出现Fail-Fast?

        fail­-fast 称为快速失败机制,是 Java 集合中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。例如:当某一个线程A通过迭代器去遍历某集合的过程中,若该集合的内容被其他线程所改变了。那么线程A访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。原因是通过集合对象调用其 remove(xxx) 等方法进行修改时,会导致 modCount 和 expectedModCount 的值不一致。然后在循环执行 hashNext() 方法时,就会检测到这种不一致然后抛异常。

8. 如何取消正在执行的线程?

  • 当 run 方法完成后线程正常退出。
  • 使用 interrupt() 方法中断线程。
  • 使用 stop() 方法强行终止(不推荐使用该方法,目前该方法已作废)。

9. 什么是ReadWriteLock?

        ReadWriteLock 是一个读写锁接口, ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

//创建读写锁对象
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
写方法,在写操作时只允许一个线程进行
使用读写锁中的写锁加锁
*/
public void write(){
    readWriteLock.writeLock().lock();//使用写锁­加锁
    try{
        //写操作业务代码....
    }catch(Exception e){
        e.printStackTrace();
    }finally{
        readWriteLock.writeLock().unlock();//解锁
    }
}
/**
读方法,在读操作时可以有多个线程同时访问
使用读写锁中的读锁加锁
*/
public void write(){
    readWriteLock.readLock().lock();//使用读锁­加锁
    try{
        //读操作业务代码....
    }catch(Exception e){
        e.printStackTrace();
    }finally{
        readWriteLock.readLock().unlock();//解锁
    }
}

10. 有一个千万级的计算任务,如何高效处理?

        使用分支合并计算(ForkJoin),以提高计算效率 ­。将计算任务拆分成若干个小任务进行计算,最后在将各个小任务的计算结果进行合并 。Java 的并发包中提供了 “ForkJoinPool(分支合并线程池)” 进行分支合并计算,它是将一个任务拆分为多个子任务,每个子任务分配给一个线程执行,这样就可以保证多个子任务由多个线程并行计算,最后在将各个线程的计算结果进行 “join” 合并。

/**
 * 创建递归任务类,该类需继承RecursiveTask类
 */
public class ForkJoinDemo1 extends RecursiveTask<Long> {
    private Long start;//计算的起始值
    private Long end;//计算的结束值
    private Long critical;//分支计算的临界值(分割点)

    public ForkJoinDemo1(Long start, Long end, Long critical) {
        this.start = start;
        this.end = end;
        this.critical = critical;
    }

    public static void main(String[] args) throws ExecutionException,
            InterruptedException {
        // 创建分支合并线程池对象
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //创建基于递归计算的ForkJoinTask任务对象
        ForkJoinDemo1 forkJoinDemo1 = new ForkJoinDemo1(0l, 10_0000_0000l, 10000l);
        //将线程任务提交给线程池
        ForkJoinTask<Long> forkJoinTask = forkJoinPool.submit(forkJoinDemo1);
        //获得计算结果
        Long sum = forkJoinTask.get();
        System.out.println(sum);
    }

    /**
     * 递归计算的处理方法
     *
     * @return
     */
    @Override
    protected Long compute() {
        //计算的数据量小于临界值使用传统的方式计算
        if (end­start < critical) {
            long sum = 0;
            for (long i = start;i <= end;i++){
                sum += i;
            }
            return sum;
        }
        //数据量大于临界值使用分支合并计算
        //获得中间值
        long middle = (end + start) / 2;
        //拆分处理,创建任务对象
        ForkJoinDemo1 task1 = new ForkJoinDemo1(start, middle, 10000l);
        //拆分任务
        task1.fork();
        ForkJoinDemo1 task2 = new ForkJoinDemo1(middle + 1, end, 10000l);
        task2.fork();
        //结果汇总
        return task1.join() + task2.join();
    }
}

11. 线程池中的核心参数有那些

        ThreadPoolExecutor 自定义线程池的构造器中包含7个核心参数

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

­        参数1: int corePoolSize 核心线程数,核心线程在不使用时不会被释放。

        参数2: int maximumPoolSize 最大线程数,当核心线程和等待队列都被使用完时,再有新的线程任务提交,则线程池会自动创建新的线程(非核心线程或救急线程),但核心线程+非核心线程的总数不能超过最大线程数。

        参数3: long keepAliveTime 存活时间,由于核心线程不会被释放,所以存活时间指的是非核心线程的存活时间,当非核心线程空间时间大于存活时间,则这些非核心线程被释放。存活时间的时间单位由第四个参数设置。

        参数4: TimeUnit unit 时间单位,存活时间的时间单位。

        参数5:BlockingQueue workQueue 阻塞队列 ,当核心线程占满后,如果再有新的线程任务提交给线程池,则会将这些任务添加到阻塞队列中,当线程空闲时会从 阻塞队列中取出一个任务执行。为了避免向阻塞队列中添加过多的线程任务造成内存溢出,阻塞队列选用有界的队列实现­,当阻塞队列填充满后,还有新的任务提交,则线程池就会根据配置创建非核心线程执行这个新提交的任务。如果阻塞队列已满,非核心线程也已经到达上限,此时如果依然有新的线程任务提交到线程池,则线程池会根据约定好的"拒绝策略"对新提交的线程进行处理。

        参数6: ThreadFactory threadFactory 线程工厂­使用工厂模式创建线程对象。

        参数7: RejectedExecutionHandler handler 拒绝策略,当"核心线程"、"阻塞队列"和"非核心线程"都已到达上限,此时如果还有新的线程任务提交给线程池,则 线程池 会根据配置的"拒绝策略"来对新提交的线程任务进行处理。

12. 线程池的拒绝策略都有哪些?

        当"核心线程"、"阻塞队列"和"非核心线程"都已到达上限,此时如果还有新的线程任务提交给线程池,则线程池会根据"拒绝策略"来对新提交的线程任务进行处理,线程池中的"拒绝策略"包含以下四种策略:

  • ThreadPoolExecutor.AbortPolicy:异常策略,当有新的任务提交后,线程池就会抛出'RejectedExecutionException'异常。
  • ThreadPoolExecutor.CallerRunsPolicy:调用者执行策略,那个线程向线程池提交的任务就由那个线程自行执行该任务。
  • ThreadPoolExecutor.DiscardOldestPolicy:替换策略,使用新提交的线程任务将队列中等待时间最久的策略替换。
  • ThreadPoolExecutor.DiscardPolicy:丢弃策略,将新提交的线程任务丢弃。

13. 介绍一下你对 ForkJoinPool 的理解,与 ThreadPool 有什么区别?

        - ForkJoinPool 线程池最大的特点就是分叉(fork)合并(join),将一个大任务拆分成多个小任务,并行执行,再结合工作窃取模式(worksteal)提高整体的执行效率,充分利用 CPU 资源。
        - ForkJoinPool 线程池中为池中的每个线程都分配了一个队列,将每个线程自己的计算任务都压入到自己的队列中。当一个线程将自己队列中的任务执行结束后,如果其他线程中的任务还未执行结束,此时允许已执行结束的线程从其他线程的队列中获取任务执行(工作窃取),以充分利用 CPU 资源。

        - ForkJoinPool 与 ThreadPool 的区别:

        1). 相同点:ForkJoinPool 和 ThreadPoolExecutor 都实现了 Executor 和 ExecutorService 接口,都可以通过构造函数设置线程数。
        2). 不同点:ForkJoinPool 为线程池中的每个线程分配一个独立的队列,而且 ForJoinPool 使用工作窃取的方式, 可以从其他线程的队列中获取计算任务,而 ThreadPoolExecutor 的线程池中的任务队列只有一个,所有线程都从这个队列中获取计算任务。
        3). ForkJoinPool 是 ThreadPoolExecutor 线程池的一种补充,比较适合大数据量,大任务的处理。

14. 什么是工作窃取?

­        - 简单理解:自己的工作做完了,帮助其他人做工作。
­        - 专业理解:为了减少与原线程的竞争,使用双端队列存储任务,当一个线程将自己队列中的所有任务完成后,主动从其他未完成的线程的队列的尾部获取任务并计算。

15. 什么是双端队列?

        一般队列指从一端(头部)插入数据,从另一端(尾部)获取数据。双端队列指两端都可以进行插入和获取的操作。LinkedList 就是一种双端队列。

16. 什么是函数式接口,有什么好处?

        接口中只包含一个抽象方法的接口,我们称为函数式接口,如果在接口中使用'@FunctionalInterface'注解,表示强制规定当前接口为一个函数式接口。使用函数式接口可以简化编程模型(简化开发),可以通过'lambda'表达式进行简化。在框架中低层代码中大量使用函数式接口简化开发。

17. 什么是CAS?

        CAS(Compare and Swap) 比较并交换,使用 CAS 来保证线程对变量的原子操作,避免被其他线程所干扰。

CAS 的原理:CAS 中包含3个参数

  • 参数1:要修改的变量
  • 参数2:变量的预期值
  • 参数3:要更新的新值

        在更新数据时,CAS 先判断变量中的值与预期值是否一致,如果一致则更新为新值,如果变量的值与预期值不一致,表示当前操作被其他线程干扰,此时线程不会阻塞而是自旋等待(自旋锁),重复以上操作,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。CAS 在 Java 层面虽然没有使用锁保证原子性,但在底层使用汇编对“比较并替换”进行加锁保证其原子操作的。

CAS操作可能存在的问题

  1. CPU 开销较大:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给 CPU 带来很大的压力。可以通过设置自旋次数(默认10次)来减少 CPU 消耗。
  2. 不能保证代码块的原子性:CAS 机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用 Synchronized 了。
  3. ABA 问题

18. ABA 问题?

        在使用 CAS 进行变量的原子性操作时,先要获取一个预期值,然后再判断预期值与实际的值是否相同,如果相同则继续修改变量。由于CAS操作没有使用重量级锁,如果获得一个预期值后,实际的值被其他线程篡改为其他值,然后又被改回到原有的值,对于原线程来说,实际值和预期值是一样的,但实际上值已经被修改过。这样的问题就是 ABA 问题。

如:t1线程进行CAS操作,获取的预期值为"A",在t1线程进行"比较交换"前,t2线程进入并将变量的值由"A"改为"B",然后又重新改回到"A"并退出,此时对于t1线程来说,实际值和预期值都是"A",就可以做"比较并交换"操作,但实际上,实际值"A"已被修改过。

19. 如何解决 ABA 问题?

        可以使用“原子引用”来解决 ABA 问题。“原子引用”是基于乐观锁的思想,为 CAS 操作添加一个版本号,每次执行操作时版本号进行+1操作,通过版本号的判断来判定数据是否被修改过,如果版本号一致才继续操作。不一致则表示变量被其他线程干扰,进行“自旋”。

        Java 中提供了 AtomicStampedReference 类来实现原子引用并可以设置版本号。 

20. Volatile 修饰符

Volatile 是 Java 中的一个修饰符,该修饰符只能在属性上使用。Volatile 的底层实现原理是内存屏障(Memory Barrier):

  • 对 Volatile 变量的写指令后会加入写屏障。
  • 对 Volatile 变量的读指令前会加入读屏障。
  • ­Volatile 的可见性和禁止指令重排都是通过“内存屏障”实现的。
  • Volatile 修饰的变量不能保证操作的原子性。
  • Volatile 是 JVM 提供的轻量级的同步机制。

Volatile 的作用:

  1. Volatile 可以保障多个线程操作同一变量的可见性,一个线程修改了变量的值,其他线程会马上感知到(获得最新的值)。
  2. Volatile 可以避免“指令重排”,由于 Java 程序在编译、执行的过程中可能会代码的顺序重新排列,而造成异常的结果,使用 volatile 修饰符就可以避免操作该变量的代码重排列,保证结果的正确性。

21. Volatile 能否保证线程安全?

Volatile 不能保证线程安全,要保证线程安全必须满足三个条件:

  1. 可见性:当一个线程修改了一个变量时,对其他线程是可见的。
  2. 原子性:一个线程内的代码是一个整体,运行期间不允许其他线程插队。
  3. 有序性:一个线程内的代码按顺序执行,不允许顺序重排。

Volatile 可以保证可见性和有序性,但不能保证原子性,所以 Volatile 不能保证线程安全。

22. 什么是指令重排?

        指令重排序是指编译器或 CPU 为了优化程序的执行性能而对指令进行重新排序的一种手段,重排序会带来可见性问题,所以在多线程开发中必须要关注并规避重排序。从源代码到最终运行的指令,会经过如下两个阶段的重排序:

        第一阶段(编译器重排序),就是在编译过程中,编译器根据上下文分析对指令进行重排序,目的是减少 CPU 和内存的交互,重排序之后尽可能保证 CPU 从寄存器或缓存行中读取数据。

        第二阶段(处理器重排序),处理器重排序分为两个部分:

  1. 并行指令集重排序,这是处理器优化的一种,处理器可以改变指令的执行顺序。
  2. 内存系统重排序,这是处理器引入 Store Buffer 缓冲区延时写入产生的指令执行顺序不一致的问题。

23. 什么是内存屏障?

        内存屏障是一条指令,该指令可以对编译器(软件)和处理器(硬件)的指令重排做出一定的限制。

24. 为什么需要内存屏障?

        编译器和处理器指令重排只能保证在单线程执行下逻辑正确,在多个线程同时读写多个变量的情况下,如果不对指令重排作出一定限制,代码的执行结果会根据指令重排后的顺序产生不同的结果。内存屏障能保证可见性和有序性。

可见性:

  1. 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中。
  2. 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据。

有序性:

  1. 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。
  2. 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。

25. ThreadLocal 本地线程变量

        ThreadLocal 称为线程本地变量,实质上是为线程绑定变量。解决多线程并发时访问共享变量的问题。使用 ThreadLocal 可以为每个线程绑定一个变量,该变量被线程内部所共享,线程中的所有方法在需要时都可以使用该变量。但多个线程之间是隔离的,其他线程是无法访问的。

        ThreadLocal 的实现原理:

        每个线程对象都存在一个 ThreadLocalMap 属性,ThreadLocalMap 中包含一个 Entry 数组,我们在使用 ThreadLocal 为线程绑定变量时,是将 ThreadLocal 对象做为 key,变量做为 value,添加到 Thread 对象中的 ThreadLocalMap 属性的 Entry 数组中,也就是说在 Thread 对象中添加了一个属性 ThreadLocalMap。该属性属于某个线程对象的属性,所以 ThreadLocal 绑定的变量为线程变量,其他线程无法共享。

        但是需要注意的是,ThreadLocal会产生脏数据,在线程池环境下,核心线程不会被销毁,也就是说 ThreadLocalMap 也不会被释放,此时线程被反复使用,多个线程操作都使用 ThreadLocal 存储线程变量,这些线程变量在使用后没有被清除,下次再使用该线程时获得的数据就是之前的数据,造成脏数据的出现。

26. 并发、并行、串行之间的区别?

  • 串行指一个任务接着一个任务执行,有先后顺序。
  • 并行指在同一时刻,多个任务同时执行,多 CPU 或单 CPU 多核心情况下支持。
  • 并发指多个任务同时执行,但在同一时刻只能有一个任务执行,多个任务抢占 CPU 资源并发执行。

27. 什么是上下文切换?

        多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
­        简单说,当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
­        上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间。

28. JVM是加锁过程?

  1. 无锁:没有线程访问时的状态。
  2. 偏向锁:当有一个线程加锁,没有其他线程竞争,此时如果已开启偏向锁则将线程的 id 写入对象头 markword 中,使用偏向锁,此时如果未开启偏向锁则直接进入轻量级锁。
  3. 轻量级锁:当有其他线程竞争(多个线程加锁),偏向锁升级为轻量级锁,使用CAS维护原子性操作。如果CAS自旋不成功,竞争比较严重时,锁膨胀为重量级锁。
  4. 重量级锁:直接加锁,让未获得资源的线程进入到阻塞状态。

网路通信

集合框架

1. 集合的体系结构

集合框架可以分为:

List 线性结构、Set 非线性结构、Queue 队列结构、Map 键值对结构和 Collections 算法类这几个部分,又分别拥有各自的实现类。 

2. List 接口和 Set 接口的区别

List 接口是有序、可重复的规范,而 Set 接口是无序、不可重复的规范。

List 接口可以存放 null 值,而 Set 接口部分实现不允许存放 null ,部分只能存放一个 null 值。

List 接口是一个线性结构,可以通过下标索引获取单个元素,而 Set 是非线性结构(树型、Hash 型等),无法通过下标索引获取单个元素。

3. Vector、Stack、ArrayList 和 LinkedList 的区别

ArrayList、Vector 和 Stack 都是动态数组,它们查询速度很快,但是插入删除速度慢。

LinkedList 是双向链表结构,插入删除块,查询速度慢。

Vector 是线程安全的集合,Stack 是 Vector 的子类,它是一个先进后出的结构。有几个特有方法。

4. 如何实现数组和 List 之间的转换

数组转换成集合通过 Arrays.asList(数组)来实现。

集合转换成数组通过集合的 toArray(泛型)来实现。

String[] array = {"aa", "bb", "cc", "dd", "ee"};
// 将数组转换成集合,其中集合泛型是数据元素类型
List<String> list = Arrays.asList(array);
System.out.println(list);
// 将集合转换成数组,其中 new String[list.size()] 指代数组元素类型
String[] array2 = list.toArray(new String[list.size()]);

5. 说一下 HashSet 的实现原理

6. Array 和 ArrayList 有何区别

7. 在 Stack 中 peek()和 pop()有什么区别

8. 哪些集合类是线程安全的

9. 迭代器 Iterator 是什么,如何使用,有什么特点

10. HashMap、Hashtable、TreeMap 和 ConcurentHashMap 的区别

11. 怎么确保一个集合不能被修改

设计模式和反射

1. 勤汉单例和懒汉单例的区别

JVM 内存结构

1. 堆内存和占内存

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值