Java基础面试题(2025.03)

196 阅读23分钟

Java基础

== 和 equals() 的区别

== 对于基本类型和引用类型的作用效果是不同的: 对于基本数据类型来说,== 比较的是值。 对于引用数据类型来说,== 比较的是对象的内存地址。

equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。

String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
System.out.println(42 == 42.0);// true

String 中的 equals 方法是被重写过的,因为 Objectequals 方法是比较的对象的内存地址,而 Stringequals 方法比较的是对象的值。

当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

String s1 = new String("abc");这句话创建了几个字符串对象?

会创建 1 或 2 个字符串对象。

1.字符串常量池中不存在 "abc":会创建 2 个 字符串对象。一个在字符串常量池中,由 ldc 指令触发创建。一个在堆中,由 new String() 创建,并使用常量池中的 "abc" 进行初始化。

2.字符串常量池中已存在 "abc":会创建 1 个 字符串对象。该对象在堆中,由 new String() 创建,并使用常量池中的 "abc" 进行初始化。

字符串常量池的作用:是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

抽象类和接口的区别

抽象类:包含抽象方法的类,可以包含成员变量和方法,包含静态方法,静态变量以及常量,构造器。

接口:抽象类更高层次的抽象,是一种标准,一种规范,只允许有静态常量,抽象方法,java8引入default方法和静态方法,java9引入private方法。

/**
 * 消息处理抽象类
 *
 * @param <T>
 */
public interface BerryMqService<T> {

    /**
     * 保存消息
     */
    public abstract void save(String message);

    /**
     * 数据转换
     */
    default MqData<T> transformation(String message, Class<?> dataClass) {
        Type tp = TypeToken.getParameterized(MqData.class, dataClass).getType();
        return GsonUtil.gson().fromJson(message, tp);
    }
}

如何选择:如果有状态,有成员变量的情况下选择抽象类,或者需要控制构造器入参时。其他情况选用接口。

引用拷贝,浅拷贝,深拷贝的区别

image.png

  • 引用拷贝:两个变量指向同一个对象。
  • 浅拷贝:新对象独立,但子对象共享。
  • 深拷贝:新对象完全独立,包括所有子对象。
为什么重写 equals() 时必须重写 hashCode() 方法?

因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题?

HashMap不能正常工作

HashMap 依赖于 hashCode() 和 equals() 方法来存储和检索键值对:

  • 存储时HashMap 使用 hashCode() 确定键的存储位置(桶)。
  • 检索时HashMap 先通过 hashCode() 找到桶,再用 equals() 比较键是否相等。 如果没有重写 hashCode(),即使两个对象通过 equals() 判断相等,它们的 hashCode() 可能不同,导致:
  • 存储问题:相同的键可能被存储到不同的桶中。
  • 检索问题:无法通过键正确检索到对应的值。
class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age && name.equals(person.name);
    }

    // 没有重写 hashCode()
}

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("Alice", 25);
        Person p2 = new Person("Alice", 25);

        System.out.println(p1.equals(p2)); // true

        HashMap<Person, String> map = new HashMap<>();
        map.put(p1, "Alice's Data");

        System.out.println(map.get(p2)); // 输出: null
    }
}
finally一定会执行么,在finally里return会发生什么?

1.finally不一定会执行,比如finally之前虚拟机终止的话finally就不会被执行

try {
    System.out.println("Try to do something");
    throw new RuntimeException("RuntimeException");
} catch (Exception e) {
    System.out.println("Catch Exception -> " + e.getMessage());
    // 终止当前正在运行的Java虚拟机
    System.exit(1);
} finally {
    System.out.println("Finally");
}

输出:

Try to do something
Catch Exception -> RuntimeException

另外,在以下 2 种特殊情况下,finally 块的代码也不会被执行:程序所在的线程死亡。或者关闭 CPU。

2.不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。

public static void main(String[] args) {
    System.out.println(f(2));
}

public static int f(int value) {
    try {
        return value * value;
    } finally {
        if (value == 2) {
            return 0;
        }
    }
}
//输出
0
什么是泛型?

泛型是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Person> persons = new ArrayList<Person>() 这行代码就指明了该 ArrayList 对象只能传入 Person 对象,如果传入其他类型的对象就会报错。并且,原生 List 返回类型是 Object ,需要手动转换类型才能使用,使用泛型后编译器自动转换。

泛型分为三种:

1.泛型类

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{

    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}
//实例化泛型类
Generic<Integer> genericInteger = new Generic<Integer>(123456);

2.泛型接口

public interface Generator<T> {
    public T method();
}
//实现泛型接口,不指定类型:
class GeneratorImpl<T> implements Generator<T>{
    @Override
    public T method() {
        return null;
    }
}
//实现泛型接口,指定类型:
class GeneratorImpl implements Generator<String> {
    @Override
    public String method() {
        return "hello";
    }
}

3.泛型方法

   public static < E > void printArray( E[] inputArray )
   {
         for ( E element : inputArray ){
            System.out.printf( "%s ", element );
         }
         System.out.println();
    }
//使用
// 创建不同类型数组:Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray  );
printArray( stringArray  );
项目中哪里用到了泛型?

自定义统一返回类,可以根据具体的返回类型动态指定结果的数据类型。

1什么是反射?

反射赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性,被大量应用于框架之中。

项目哪里用到了反射?

枚举展示,提供一个对外暴露的接口,将想要在前端展示的枚举都继承一个抽象类,然后通过包路径反射出所有抽象类子类,再调用invoke()方法实现前后端统一枚举。

集合

1、HashMap

  • HashMap和HashSet的区别
  1. 作用上就不一样,HashSet存储的是无需的元素。HashMap存放的是键值对。
  2. HashSet是基于HashMap实现的,HashSet的元素就是HashMap中的Key
  • HashMap和TreeMap的区别
  1. 首先TreeMap有序``HashMap无序的,二者同样继承了AbstractMap类但TreeMap同样实现了NavigableMap接口,这个接口提供了很多搜索和操作键值对的方法,基于`红黑树实现。
  • HashMap的底层实现
  1. JDK1.8之前:数组+链表的方式组合,当一个元素putHashMap通过Key的 hashcode经过扰动函数处理过后得到hash值,然后通过二进制运算 数组长度-1 & hash得到数组位置,如果该位置没有元素,直接插入,如果有元素,判断该元素与存入元素得hash(调用hashCode())值和key相不相同(调用equals()),相同则替换,不相同则使用头插法将元素加入链表中(头插法目的:开发者认为后插入得元素使用概率会更大)。

  2. JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,判断数组长度是否小于64如果小于优先扩容,否则将链表转化为红黑树,以减少搜索时间。

  • 为什么hashMap总以2的幂作为hash表的大小
  1. 优化哈希函数:均匀分布键值对,减少哈希冲突,如果不取2的幂作为表大小,有些索引将永远不会得到利用,因为索引位置的计算公式是hash(key) & (length - 1) 这个(length - 1)在计算中起到掩码的作用,如果出现0将会导致0这位永远的得不到运算,位置只取决于非0位掩码的值。
  2. 提高计算效率:用与运算替代取模运算,提升性能,效率更高,只需要取hash值的低几位就可以了。
  3. 扩容优化:扩容时重新哈希的计算更加高效,因为扩容后的位置总是原来的位置,或者是原来的位置+原来表的大小。

并发编程

Java有几种创建线程的方式

Java创建线程有很多种方式啊,像实现Runnable、Callable接口、继承Thread类、创建线程池等等,不过这些方式并没有真正创建出线程,严格来说,Java就只有一种方式可以创建线程,那就是通过new Thread().start()创建。
而所谓的Runnable、Callable……对象,这仅仅只是线程体,也就是提供给线程执行的任务,并不属于真正的Java线程,它们的执行,最终还是需要依赖于new Thread()

说说线程得生命周期
  • NEW: 初始状态,线程被创建出来但没有被调用 start()

  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。

  • BLOCKED:阻塞状态,需要等待锁释放。

  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。

  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。

  • TERMINATED:终止状态,表示该线程已经运行完毕。

image.png 由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。

  • 当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。

  • TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。

  • 当线程进入 synchronized 方法/块或者调用 wait 后(被 notify)重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。

  • 线程在执行完了 run()方法之后将会进入到 TERMINATED(终止) 状态。

Thread.sleep()和Obejct.wait()有何异同、

同:

  • 两者都可以暂停线程的执行。

异:

  • sleep()没有释放锁,而wait()释放了锁。
  • sleep()通常用于暂停操作,wait()一般用于线程之间的交互/通信。
  • sleep()通常会自动苏醒,wait()则需要等待其他线程执行notify()或者notifyAll()来唤醒。
  • sleep()是Thread的本地方法,而wait()是Object的本地方法。
为什么wait()不在Thread类中

因为wait()方法的本质是释放线程所持有的对象锁,每个对象都会有一把对象锁,要操作的目标是对象而不是线程本身。

为什么 sleep() 方法定义在 Thread 中?

因为sleep()的目的是为了让当前线程休眠,不涉及对象,也不涉及到锁。

可以直接调用 Thread 类的 run 方法吗?

调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

什么是死锁?

两个线程A和B分别持有a资源和b资源,他们都需要等待对方持有的资源释放才能执行,就造成了死锁。

如何检测死锁?

通过jmap等命令查看。

如何避免死锁?

让AB两个线程去同时竞争一个资源,避免两个线程同时持资源。

什么是JMM,有什么作用?

JMM为Java内存模型,是Java为了屏蔽操作系统差异,简化多线程编程,自己推出的内存模型,也可以理解成Java定义的并发编程的规范,抽象出了主内存和本地内存之间的关系,也提出了Java并发编程的需要遵守的原则和规范。

什么是主内存,什么是本地内存,区分这些有什么好处?
  • 主内存:所有线程创建的实例都会放到主内存中,不管是成员变量,局部变量,还是静态变量,常量。
  • 本地内存:每个线程都拥有一个本地内存,这个本地内存存储了该线程读或者写主内存共享变量的副本。每个线程只能操作自己本地内存中的变量,不能访问其他线程的本地内存,如果线程需要通信,则需要通过主内存来进行。

优点:

  • 更好能模拟计算机内存,主内存相当于内存,工作内存相当于CPU寄存器,可以精准描述多线程下内存交互,使得效率更高,交互更安全。
解释一下volatile关键字?

volatile可以保证被修饰变量的可见性,在JMM模型中使用时总会在主存中进行读取,以此保证可见性。 还可以禁止指令重排,比如在单例模式中,就可以使用volatile + sychronazied防止指令重排保证线程安全。

实例代码:

//volatile + synchronized保证雪花id不重复
private volatile long lastTimestamp = -1L;
private volatile long sequence = 0L;

public synchronized long nextId() {
    long currentTimestamp = timestamp();

    if(currentTimestamp < lastTimestamp) {
        throw new IllegalStateException("Invalid System Clock!");
    }

    if (currentTimestamp == lastTimestamp) {
        sequence = (sequence + 1) & maxSequence;
        if(sequence == 0) {
            // Sequence Exhausted, wait till next millisecond.
            currentTimestamp = waitNextMillis(currentTimestamp);
        }
    } else {
        // reset sequence to start with zero for the next millisecond
        sequence = 0;
    }

    lastTimestamp = currentTimestamp;

    return currentTimestamp << (NODE_ID_BITS + SEQUENCE_BITS)
            | (nodeId << SEQUENCE_BITS)
            | sequence;
}
//不使用volatile会有指令重排导致对象初始化问题,和不可见,导致脏读问题。
volatile可以保证原子性么?

volatile可以保证变量的有序性和可见性,不能保证原子性。

示例代码:

public class VolatileAtomicityDemo {
    public volatile static int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        VolatileAtomicityDemo volatileAtomicityDemo = new VolatileAtomicityDemo();
        for (int i = 0; i < 5; i++) {
            threadPool.execute(() -> {
                for (int j = 0; j < 500; j++) {
                    volatileAtomicityDemo.increase();
                }
            });
        }
        // 等待1.5秒,保证上面程序执行完成
        Thread.sleep(1500);
        System.out.println(inc);
        threadPool.shutdown();
    }
}

正常情况下,运行上面的代码理应输出 2500。但你真正运行了上面的代码之后,你会发现每次输出结果都小于 2500。 很多人会误认为自增操作 inc++ 是原子性的,实际上,inc++ 其实是一个复合操作,包括三步

  1. 读取 inc 的值。
  2. 对 inc 加 1。
  3. 将 inc 的值写回内存。

导致A线程+1后还没写会内存,B线程读取了原来的值。

可以使用synchronized AtomicInteger ReentrantLock 来改造。

什么是悲观锁?

悲观锁总是假设最坏的情况,认为共享资源每次访问都会出现问题,所以每次获取资源时就会上锁。当一个线程想获取共享资源时只能等待上一个线程释放,期间一直阻塞。

sychronizedReenTrantLock等独占锁就是悲观锁;

缺点:高并发下悲观锁会有大量线程阻塞,导致系统上下文切换,增加系统开销。悲观锁可能会有死锁问题。

什么是乐观锁?

乐观锁总会假设最好的情况,认为共享资源每次访问不会出现问题,线程无需加索也无需等待,只是在提交修改时验证对应的共享资源是否被其他线程修改了

高并发场景下乐观锁比悲观锁不会存在锁竞争的情况,也不会出现死锁,在性能上会更好一些。但是如果写操作占比比较多,冲突非常多的情况下,也会影响性能。

选择:视具体情况而定,如果写比较多的情况可以使用悲观锁,读操作比较多可以使用乐观锁。 实现乐观锁的两种方式:1.版本号机制 2.CAS

CAS是什么?

比较并交换,变量修改前会将原变量值保存下来,在写入时比较原值和当前值,如果相等则不写入,不相等则写入失败。

可能导致的问题:

  • ABA问题:原变量之前是A后改成B再改成了A,中间有一次修改,A不是之前的A了。

如何避免:看情况,如果造成后果无关紧要则无需更改,如果有影响可以加入版本号或者时间戳来区分,JDK1.5有一个类AtomicStampedReference中的compareAndSet()就是通过这种思想解决ABA问题的。

synchronized是什么,有什么作用?

synchronized是同步的意思,在Java中被修饰的方法和代码块在任意时刻只会有一个线程会执行。

三种用法:

//修饰实例方法
synchronized void method(){
//code
}
//锁当前实例,线程想执行这段代码,需要获得当前实例的锁
//修饰静态方法
synchronized static void method(){
//code
}
//锁当前类,线程想执行只能获取当前类锁
  • synchronized(object) 表示进入同步代码库前要获得 给定对象的锁
  • synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁
//修饰代码块
synchronized(this) {
    //业务代码
}
锁升级的过程,四种状态?

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

synchronized和volatile有什么区别?
  • 两者都是为了线程同步,synchronized用来修饰方法,代码块,volatile用来修饰变量保证变量的可见性。
  • synchronized是重量级实现,volatile是轻量级实现,从性能上说volatile更好。
  • synchronized既保证可见性,也保证原子性,volatile只能保证可见性,不能保证原子性,所以组合操作复杂场景一般使用synchronizedvolatile一般做简单的标志位等。
ReentrantLock是什么?

ReentrantLock实现了Lock接口,是可重入且独占的锁,和synchronized关键字类似,但是比synchronized功能更多,更灵活,加入了一些超时tryLock(timeout),中断,公平以及非公平的机制。

synchronized和ReentreLock有什么区别?

synchronizedReentreLock都是可重入锁,ReentreLocksynchronized多了很多功能,比如超时、中断、公平非公平锁,synchronized适用于简单场景,是基于JVM的,ReentreLock是基于JavaAPI的,synchornized只能是非公平锁,ReentreLock可以是非公平锁,也可以是公平锁,默认为非公平。

ReentreLock下的Condition接口可以实现功能更丰富的await()notifyAll()功能,比如可以实现定向唤醒,ConditionsignalAll()功能可以只能唤醒注册在当前Condition下的线程。

ThreadLocal是什么,项目中有哪儿用到了?

ThreadLocal被设计用来存放每个线程自己专属的本地变量,来避免多个线程竞争一个资源的情况。 项目中:

public void pushTransportOrder(Long orderId) {
    SysDeptEntity dept = sysDeptDao.getObjectByDeptId(ThreadLocalUtil.get());
    String routingKey = dept.buildBloodCompanyConfig().getRoutingKey();
    if (StringUtils.isEmpty(routingKey))
        throw new RRException("未配置浆站所属MQ路由,请联系管理员添加");
    StoreTransportOrder order = orderMapper.queryById(orderId);
    order.updateCheck();
    executorService.execute(()->{
        try {
            ThreadLocalUtil.set(dept.getDeptId());
            ThreadLocalUtil.setSiteCode(dept.getSiteCode());
            mqService.bloodCompanyPub(getTransEntity(order,dept), routingKey);
        }catch (Exception e){
            log.error("浆站={},运输单={}推送失败,失败日志",dept.getDeptId(),orderId,e);
            orderMapper.update(StoreTransportOrder.buildUpdateFalse(orderId));
        }

    });
    order.setPushed(true);
    orderMapper.update(order);
}

A推送运输单到B系统,因为A系统是多租户模式,每个请求进入业务代码前先会被拦截器拦截,将token解析出的部门号写入ThreadLocal,在需要选择schema时将这个部门号拼接到sql中去。使用多线程时需要重新set一遍,避免取不到导致找不到schema。好处是避免了显示传参,优化了代码。

ThreadLocal的原理是什么?

Thread类中有一个变量为threadLocalMapTreadLocalgetset操作都是这个变量,可以说ThreadLocal只是threadLocalMap的封装,ThreadLocalMap的结构为一个Entry数组,EntryKey就是ThreadLocal,value就是setThreadLocal的对象。

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    private Entry[] table;
}
ThreadLocal可能会有什么问题?

内存泄漏,因为ThreadLocalMapEntry节点的key为弱引用,value为强引用,所以gc时会导致key被清理但是value还存在的情况,就导致了内存泄漏。 所以使用ThreadLocal后必须要调用remove()方法,最好使用try-catch-finly保证报错情况也会清理ThreadLocal中的值。

CountDownLatch用过么?

CountDownLatch:线程计数器,每当一个线程执行完可以调用countDown()方法,当所有线程执行完后,才能执行主线程,期间主线程调用await()方法等待子线程执行完。 项目中有查询统计,按月查询的需求,每个月启动一个线程,等所有线程执行完再return。

线程池

运用池化思想,提高资源利用率,当一个任务需要处理时,可以使用线程池中的一个线程,任务结束后该线程不会销毁,而是会等待下一个线程来使用,避免了线程创建和销毁时的性能浪费。还可以通过设置参数,更好的管理线程。

线程池的参数
/**
 * 用给定的初始参数创建一个新的ThreadPoolExecutor。
 */
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                          int maximumPoolSize,//线程池的最大线程数
                          long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                          TimeUnit unit,//时间单位
                          BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                          ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                          RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                           ) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
线程池处理任务的流程

任务提交后,线程池判断核心线程是否满了,如过没满,则会创建线程,如果满了,会判断阻塞队列是否满了,如果没满进入阻塞队列,如果满了,会判断最大线程数满了没,如果没满,会创建线程,如果满了会执行拒绝策略。

image.png

线程池的拒绝策略有哪些?
  • AbortPolicy:抛出异常来拒绝新任务。(默认策略)
  • CallerRunsPolicy:由调用线程来执行。如果调用线程已结束则会丢弃这个任务。(一般使用)
  • DiscardPolicy:不处理直接丢弃。
  • DiscardOldestPolicy:丢弃最早未处理的任务。
线程池的阻塞队列一般有哪几种?
  • linkedArrayBlockQueue
  • ArrayBlockQueue:由数组实现,容量一但确定就不会更改。
  • DelayBlockQueue
  • SynchronousQueue:同步队列,没有容量,由CacheThreadPool使用,可以无限创建线程。
线程池异常后线程是会继续复用还是销毁?

使用execute()执行时,未补货异常导致线程终止。线程池会创建新的线程替代。

使用submit()执行,线程会复用,错误信息会写在Future里

如何选择:execute()适用于不怎么关心执行结果的场景,submit()可以更灵活处理异常。

如何确定线程池大小?

看是cpu密集型还是IO密集型。

CPU:N+1

IO:2N

JVM

垃圾回收器
1.Serial、SerialOld

Serial是JVM最早最简单的单线程垃圾回收器,在新生代使用采用复制算法。 SerialOld 单线程垃圾回收器,在老年代使用,使用标记整理算法。 都采用stw机制回收垃圾。

2.ParNew

是Serial的多线程编程版本。别的都和Serial一致。

3.Parallel、ParallelOld

Parallel为新生代垃圾回收器、并行回收、stw机制、采用复制算法。

ParallelOld为代替SerialOld的老年代处理器,并行回收、stw机制、标记整理算法。

这俩为JDK1.8默认处理器。

Parallel和ParNew的区别:

Parallel更关心吞吐量、Parallel具有自适应调节策略。

4.CMS(Concurrent Mark Sweep)

为老年代垃圾回收器,实现了用户线程和GC线程同时执行。因为用户线程还在执行,所以只能采用标记清除算法。

CMS执行分为四个阶段。 1.初始标记:只标记与根节点相连的对象,会短暂STW。

2.并发标记:用户线程与GC标记线程同时执行,标记出剩余关联对象。

3.重新标记: 因为并发标记时,用户线程还在进行,会产生标记变动,重新标记就是为了修正这一部分对象,会STW。

4.并发清理:用户线程和GC清理线程会并发执行,因为没有移动对象所以不需要STW。

优点:第一款实现用户线程和GC线程并发的垃圾处理器,降低了用户线程等待时间;

缺点:1.无法处理浮动垃圾,因为用户线程还在执行,执行期间也是会产生垃圾的,所以只能留到下一次去清理;2.因为采用标记清理算法,所以会产生很多内存碎片。

5.G1(Garbage First)

G1将内存空间划分成不同的区域,并评估这些区域中的垃圾回收价值,会优先回收价值高的区域;

在垃圾回收步骤上,最后一步与CMS垃圾回收器有所不同,G1会stw,回收标记价值较高的区域,并将剩余对象复制到新区域,腾出空间。这种策略保证了G1回收效率更高,而且因为整理内存,也不会有内存碎片的问题。

image.png