1. 谈谈你对面向对象的理解。

面向对象和面向过程是有区别的,举个例子:

开车这件事情,面向过程就是将开车分解成多个步骤,按顺序执行:

  1. 打开车门;
  2. 坐上车;
  3. 发动引擎
  4. 踩油门。

面向对象注重有哪些参与者,比如这里就可以细化成两个对象,人和车:
人要做的事情:

  1. 打开车门;
  2. 坐上车;
  3. 发动引擎。

车要做的事情:

  1. 引擎运转;
  2. 中控系统开始工作。
  3. 接受人的指令。

那么可以看出来,面向过程直接高效,因为符合我们正常做事的流程,面向对象更加好维护,好扩展。要做的事情会根据不同的对象进行划分,而且将来要扩展人的功能或者车的功能不会互相影响。并且,车可以被多个人开,这就是面向对象的好处。

面向对象有三个特征:封装,继承,多态。
封装:
封装就是能通过一些关键字来限定类中成员或者方法是否能被外界所访问。举个例子:

比如说类中有个属性邮箱,我希望外界不能随便设置,而是要通过某些验证才可以,那么我可以将邮箱设置成private, 然后通过set方法来设置邮箱,在set方法中,我们通过正则表达式来确保符合邮箱格式。

此外,方法也可以设置成private, 相当于只给本类访问,不提供给外部。

继承:
关键字是extend, 表示子类可以去继承父类,这个主要是体现在合理共用公共方法以及拓展子类自己的功能:
比如说animal类,dog和cat都继承animal类,然后animal有通用方法,吃饭,子类都是同一个吃饭的方法。但是dog和cat都有自己的叫的方法,他们就可以重写该方法。这样可以增强代码复用,减少代码冗余。

多态:
多态离不开继承,因为是用父类的类型去引用子类的对象。比如说animal的例子,可以有:

Animal animal = new dog();
Animal animal = new cat();

animal.yell();

比如,将来我把dog改成cat, 因为有多态的存在,只需要修改这里new对象的部分就可以了。后边都是用的animal里共有的方法,因此逻辑不需要改动。 比如说调用yell方法,会从汪汪汪变成喵喵喵。

缺点就是不能调用子类特有的方法,因为该方法必须在父类中有。

2. JDK,JRE,JVM三者区别和联系

JDK包含JRE,JRE包含JVM,所以JVM最小。

JDK就是JAVA开发工具,如果是一个JAVA开发程序员,就需要安装JDK,它包含JVM和类库以及JAVA工具。比如JAVAC命令将java文件转化成.class文件,即字节码文件。

JRE是JAVA运行时环境,如果你只是想运行JAVA程序而不用开发JAVA,那么就安装JRE,它包含JVM和类库,不包含JAVA工具。

JVM是最小的,指的是java 虚拟机。内部结构包括堆,栈,程序计数器,方法区,本地方法栈。解释运行JAVA字节码文件,所以说JAVA之所以能一次编译到处是运行,是因为有不同版本的JVM,比如LInux和Windows版,能保证你在不同的操作系统上能运行同一份字节码文件。

3. ==和equals

==可以用来比较基础数据类型和对象,当比较基础数据类型时,比较的是值,比较对象时,比较的是对象的地址。

equals方法用来比较对象,在不重写的情况下,和==是一样的,底层就是采用==实现的。

但是我们通常会重写equals方法,比如String类中就重写了equals方法,当两个字符串的内容完全相等时,就会返回true, 而不是比较对象的地址。

因此:

public class StringTest {
    public static void main(String[] args) {
        String s1 = "HelloWorld"; // 常量池中
        String s2 = new String("HelloWorld"); // 堆中
        String s3 = s2;

        System.out.println(s1 == s2); // false
        System.out.println(s1 == s3); // false
        System.out.println(s2 == s3); // true
        System.out.println(s1.equals(s2)); // true
        System.out.println(s1.equals(s3)); // true
        System.out.println(s1.equals(s3)); // true
    }
}

4. Final

简述Final作用

  • 修饰类:该类不可被继承
  • 修饰变量:该变量一旦被赋值,不能被更改
  • 修饰方法:该方法不能被覆盖(重写),但是可以被重载。

一些细节:

  • 修饰对象时,对象的引用虽然不可以变,但是对象内部值是可以变的。

为什么局部内部类和匿名内部类只能访问局部final变量?

因为当存在内部类的时候,会生成两个class文件,而不是一个,此时内部类和外部类级别是一样的。

会存在一个问题,外部类运行完以后,要垃圾回收,如果此时内部类引用了外部类的局部变量,会拷贝一份外部类的变量放进内部类中,已防止引用的是一个不存在的对象。但是这要导致了一个问题的出现,就是外部类的变量和内部类的变量不一致,可能被修改,因此JAVA做了妥协,不允许修改该局部变量,因此用final修饰。

5. String, StringBuilder, StringBuffer的区别

String用final修饰,不可变,每次改动会生成新的对象,String也是线程安全的。

StringBuilder和StringBuffuer的最大区别是,前者线程不安全,但是效率更高,后者线程安全但是效率比StringBuilder要低。

因此,当我们不需要频繁修改字符串的时候,采用String即可。

如果需要频繁修改字符串,优先使用StringBuilder,因为它效率更高。但是如果要多线程共享变量,就要使用StringBuffer来保证线程安全。

6. 重载和重写的区别

重载发生在同一个类中,说白了就是只有方法名一样的多个方法,他们的参数是不同的,这个不同包括参数的个数,类型,顺序,只要参数不是完全相同,那就是重载。但是要特别注意,重载和修饰符以及返回类型无关,也
就是说,如果只有修饰符或者返回类型不同,那么就会报错,不算重载。

public String add(int a, String b);
public int add(int a, String b);

上面不算重载,会报错。

重写发生在继承中,即子类重写父类的方法,以实现自己独有的功能。注意, private的方法由于子类访问不到,就不涉及到重写。此外,子类的返回值范围必须小于等于父类,比如,父类返回Object,子类返回String,抛出的异常范围小于等于父类。

7. 接口和抽象类的区别

  • 抽象类只能单继承,接口却可以多实现;
  • 抽象类中不但可以存在抽象方法,也可以存在普通成员方法;但是,接口中只能有抽象方法,而且必须是public abstract, 之所以必须是public,是为了保证实现了接口的对象实现该方法,如果是private那么就访问不到,更别提重写了。
  • 抽象类中的成员变量可以是多种类型的,但是接口中的变量只能是public static final的, 即常量;

扩展:非初级程序员必考
抽象类,实际上是一个is a 的关系,比如dog is a animal
接口,是has a 或者like a的关系,比如鸟和飞行器,本质上鸟并不是飞行器,但是鸟有飞的功能,飞行器也有飞的功能,那么就可以用接口。

所以,当关注的是类的本质,用抽象类,如果关注的是操作,用接口。

8. List和Set的区别

List是允许重复,而且有序的,会按照你放进去的顺序进行排序。

Set不允许重复,而且不保证顺序。因此Set常常用来做去重。

此外,List可以通过iterator或者get(i)下标取出元素,但是Set只能通过Iterator取出元素。Set有add, remove, contains方法,但是没有get方法。

9. hashCode和equals方法

  • 如果重写了equals方法,那么hashCode也必须重写;
  • 如果两个对象相等,hashCode一定相同
  • 如果两个对象相等,equals返回true
  • 如果两个对象不相等,hashCode也可能相同。

为什么需要hashCode方法?
拿HashSet去重举例,首先,对对象进行hashCode方法,得到哈希索引,如果哈希索引上已经有值了,接着调用equals方法判断是否为同一个对象,如果是同一个对象,则不添加,否则散列到其他位置。

如果没有hashCode方法,那么需要对所有对象做equals来判断是否重复,那么效率非常低,所以hashCode的存在提高了效率。

10. ArrayList和LinkedList的区别

ArrayList底层是基于动态数组,优势是读取速度快,因为是连续内存地址,而且List中的数据类型相同,可以通过很快的访问到指定索引的元素。缺点就是当涉及到插入操作时,需要扩容以及元素的移动。扩容问题可以通过提前设定较大的数组长度来解决(但是太大了也不好,浪费空间)。关于元素的移动,如果都是尾插,那么就不存在移动问题。因此,提前设定大点的容量 + 尾插法可以极大的提升插入性能,甚至比LinkedList还快。

LinkedList底层基于链表,优势是插入和删除代价很小,只需要改动指针指向即可,因此涉及频繁的插入删除时,可以使用LinkedList。但是缺点就是读取慢,每次需要从头开始遍历。

11. HashMap和HashTable的区别

这道题实际上已经比较老了,因为HashTable因为性能问题已经不怎么用了,目前用ConcurrentHashMap来代替。

区别:HashMap是线程不安全的,HashTable是线程安全的,因为每个方法都加了synchronized。

底层实现:
HashMap在1.7采用数组 + 链表组成,流程是:

  1. 计算key的hash值
  2. 取余得到下标 (实际上是与length - 1按位与)
  3. 如果没有Hash冲突,直接存入
  4. 如果有Hash冲突,进行equal比较,相同则覆盖,不相同则以链表形式进行头插

HashMap 1.8的修改:

  1. 当链表长度达到8, 并且数组长度达到64时,链表会转化成红黑树,长度低于6则转回链表。
  2. 采用尾插法;

数组扩容:
当达到阈值时,进行扩容,会将数组的长度变为两倍,并且重新计算下标:

  • 要么还在原来位置
  • 要么是原来位置 + 原数组长度的位置

扩容不单单是为了增大容量,还有将链表截断的功能。

12. ConcurrentHashMap原理及1.7,1.8的区别

TODO

13. 如何实现一个IOC容器 (需要再看)

考查对IOC容器的理解,讲思路。

  1. 通过配置文件来配置包扫描路径;
  2. 递归包扫描获取.class文件;
  3. 反射、确定需要交给IOC管理的类;
  4. 对需要注入的类进行依赖注入

注意事项:

  • 配置文件中指定需要扫描的包路径;
  • 定义一些注解,分别表示访问控制层,业务层,数据持久层,依赖注入注解、获取配置文件注解;
  • 从配置文件中获取需要扫描的包路径,获取当前路径下的文件信息和文件夹信息,将当前路径下所有.class结尾的文件添加到一个set集合中进行存储;
  • 遍历该set集合,获取类上有指定注解的类,交给IOC容器,定义一个安全的Map来存储这些对象;
  • 遍历IOC容器,获取到每一个类的实例,判断是否有依赖其他类的实例,然后进行递归注入。

14. 什么是字节码文件

考查JVM。
字节码文件就是.class文件,JVM可以理解这种代码。

一个.java文件:

  1. 首先通过javac命令,即通过编译器,将其转化成字节码文件,即.class文件
  2. 交给JVM解释执行,JVM将每一条要执行的字节码文件交给解释器;
  3. 解释器将其翻译成特定机器上的机器码,在指定的机器上运行;

优点:

  • 实现了跨平台,即字节码可以到处运行;
  • 一定程度上解决了传统型解释型语言执行效率低的问题,传统解释型语言解释一句,运行一句,而Java提前编译好,效率更高;

15. Java类加载器的种类

  1. Bootstrap类加载器,加载lib目录下的类文件;
  2. Extension类加载器,加载lib目录下,ext目录下的类文件;
  3. Application类加载器,加载程序员自己编写的类;
  4. 自定义加载器,可以自己指定加载路径。

16. 双亲委派模型

  1. 首先往上查找缓存;
  2. 如果找到了,直接返回;
  3. 如果没找到,从上往下尝试加载,查找加载路径,有则加载返回,无则继续向下查找。

好处:

  1. 安全性,比如被用户重写了String,如果没有双亲委派,就回去加载用户自己的String类,导致程序运行出错。
  2. 避免类的重复加载,因为JVM判断是否是同一个类,除了要求包名和类名相同以外,还要求是同一个类加载器加载。

17. Java中的异常体系

  • 最顶级的是Throwable
  • Throwable下边有两个子类,Exception和Error;
    • Exception是程序员可以处理的异常;
    • Error是处理不了的,比如OOM, 程序被迫停止运行;
  • Exception分两种,RunTImeException和CheckedException;
    • RunTimeException发生在程序运行期间,会导致程序当前线程执行失败;
    • CheckedException发生在程序编译过程中,导致程序编译无法通过;

18. GC是如何判断哪些对象可以被回收

  • 引用计数法:没一个对象有一个引用计数的属性,每新增一个引用,计数器+1,释放一个引用,计数器-1,当为0的时候可以被回收, JVM中并没有采用该方法,因为可能会有循环引用导致无法回收;
  • 可达性分析:从GC Roots开始向下搜索,例如A中引用了B,B中引用了C,那么ABC都不能被回收;

深入GC Roots:

  • 栈中引用的对象,比如new User(), 那么该对象就是一个GC Root;
  • 方法区中的静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中Native方法引用的对象;

不可达就会立马被回收吗?
不是,第一次的时候发现没有被GC root引用,第二次则判断是否需要运行finalize方法进行自我拯救。
如果没有覆盖finalize方法,第二次就直接回收,如果覆盖了,就运行finalize方法,然后重新进行GC root的可达性分析。

我们通常不用该方法。

19. 线程的生命周期,或者有哪些状态?

创建,就绪,运行,阻塞,死亡

  1. 创建:此时new了一个线程对象,但是还未调用start方法。
  2. 就绪:指的是线程已经启动,但是还未实际运行,此时可以去争夺CPU,例如调用了start方法。
  3. 运行:获得了CPU,执行程序代码
  4. 阻塞:因为某种原因放弃了CPU的使用权,暂时停止运行。线程需要重新进入就绪状态,才有机会转到运行状态;
  5. 死亡:线程运行结束,或者碰到异常而死亡;

阻塞的三种情况:

  1. 等待阻塞:线程调用wait,进入阻塞状态,无法自动唤醒,JVM将线程放入“等待池”中。此时需要其他线程调用notify或者notifyAll才能被唤醒。
  2. 同步阻塞:即synchronized时,线程尝试去获得同步锁,但是发现锁被其他线程占用,那么该线程只能在锁外等着,JVM把该线程放入“锁池”中;
  3. 其他阻塞:执行sleep或者join方法,或者发出I/O请求,JVM会把该线程置为阻塞状态。

image.png

20. sleep()、wait()、join()、yield()的区别

需要先明白两个池的概念:

  1. 锁池

所有需要竞争同步锁的线程都会放在锁池中,想象一个同步代码块,某个线程获得了锁,其他线程在锁外等,他们都在锁池里等待。当前面的线程释放了锁以后,某个获得了锁的线程就会进入就绪状态,等待CPU的分配。其他线程仍然处于阻塞状态。

  1. 等待池

调用wait方法后,线程会被放到等待池中,等待池的线程不会去竞争同步锁。只有当其他线程调用了notify()或者notifyAll()以后,等待池的线程才开始去竞争锁。notify()是随机从等待池中选出一个线程放入锁池,而notifyAll()是将所有等待池的线程放入到锁池中。

区别:

  1. sleep是Thread类的静态本地方法,wait则是Object类的本地方法;
  2. sleep不会释放锁,而wait会释放锁;
  3. sleep不依赖同步器synchronized, 而wait则依赖synchronized;
  4. sleep不需要被唤醒,会自动醒来,而wait如果不指定时间,需要被唤醒;
  5. sleep主要用于当前线程休眠,wait则用于多线程之间的通信;
  6. sleep会让出CPU执行时间并强制进行上下文切换,而wait则不一定,wait后可能还是有机会竞争到锁继续执行。

yield()执行后马上释放CPU的执行权,进入就绪状态(不是阻塞状态),所以依然有可能再次重新获得CPU的使用权继续执行。

join()表示必须要等到另一个线程执行完毕,当前线程才能继续执行,例如:
A中调用了B.join(), 那么A线程就会被阻塞,直到B线程执行完毕以后,A线程才能继续执行。

21. 对线程安全的理解

因为在jVM里的堆是由多线程共享的,同时操作共享的对象就会出现线程安全的问题。

22. Thread和Runnable的区别

实际上不能对比,硬要对比就是使用上的对比。
Thread是一个类,使用方法就是

  1. 继承Thread,
  2. 重写run方法,
  3. new一个对象,
  4. 调用start方法。 ```java class MyThread extends Thread { @Override public void run() {
     for (int i = 0; i < 10; i++) {
         System.out.println("新开的线程:" + i);
     }
    
    } }

public class ThreadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); } }

Runable是一个接口,可以多实现,使用方法:

1. 实现Runable接口
1. 重写run方法
1. new一个runable对象
1. new一个线程,将runable对象作为new的参数传入
1. 线程调用start方法。
```java
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("新线程");
    }
}

public class ThreadTest1 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread myThread = new Thread(myRunnable);

        myThread.start();
    }
}

23. 说说你对守护线程的理解

守护线程就是为非守护线程提供服务的线程。

注意,守护线程不是某一个线程的守护线程,而是整个JVM中所有非守护线程的保姆。

守护线程的作用?
最经典的是GC,当系统中所有的线程都退出了,也就不会产生垃圾了,那么垃圾收集器也没有存在的必要了,就会自动离开。

注意:

  1. 在守护线程中产生的新线程也是守护线程
  2. Java自带的多线程框架,比如ExecutorService,会将守护线程转化为用户线程,所以如果要使用守护线程就不能使用Java的线程池。

24. ThreadLocal的原理和使用场景

在Thread中,有一个ThreadLocals属性,该属性为一个ThreadLocalMap,其中的key就是我们的ThreadLocal,value就是我们这个ThreadLocal.set设置的值。

一对key和value组成一个Entry对象。

ThreadLocal在不同线程中是隔离的,因此不会互相影响。

public class ThreadLocalTest {
    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    public static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(threadLocal.get());
            threadLocal.set(1);;
            System.out.println(threadLocal.get());


            System.out.println(threadLocal1.get());
            threadLocal1.set("hello");
            System.out.println(threadLocal1.get());
        });

        Thread t2 = new Thread(() -> {
            System.out.println(threadLocal.get());
            threadLocal.set(2);
            System.out.println(threadLocal.get());

            System.out.println(threadLocal1.get());
            threadLocal1.set("world");
            System.out.println(threadLocal1.get());
        });

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

    }
}

以上代码用图来表示:
image.png

注意:

  • Entry对象的key是一个弱引用,这也就意味着当没有指向key的强引用以后,该key会被垃圾回收器给收集了。
  • 当执行set方法时,会先去获取当前的线程对象,然后获取当前线程的ThreadLocalMap对象,然后设置key和value, key就是ThreadLocal的变量名,value就是设置的值;
  • 当执行get方法时,类似set方法;

使用场景:

  1. 对象跨层传递时,可以使用ThreadLocal避免多次传递,打破层次间的约束,例如一个对象从controller层传到service层传到entity层。
  2. 线程间数据隔离
  3. 进行事物操作
  4. 数据库连接,Session会话管理。

25. ThreadLocal内存泄漏原因,如何避免

内存泄漏:不再会被使用的对象或变量理应被回收,但是却没有被回收,就是内存泄漏。

内存泄漏和内存溢出不是一个概念,内存溢出指的是想要申请内存空间,却内存不够,无法申请到了。内存泄漏如果不解决,就可能会导致内存溢出。

养成好习惯,在用完以后用ThreadLocal.remove()清除掉value值,而Key因为是软引用的,在GC的时候没有硬引用指向它以后会被回收掉。

26. 并发、并行

并行:指两个任务在同一时刻互不干扰的进行,例如多核CPU同时执行两个不同的任务;
并发:在某一时刻下,两个任务不能同时进行,而是交替运行。看起来像是在同时运行。

27. 并发的三大特性

并发的三大特性为原子性、可见性、有序性。这三个特性并不是说并发与生俱来就有的,而是说我们必须通过编程保证这三个特性,才能确保没有线程安全的问题。

  1. 原子性

指的是在一个操作中,CPU不可以中途暂停然后再调度,要么执行完成,要么不执行。注意,并不是指一条语句,可能是很多条语句,我们需要保证原子性才能不出错。例如A给B转钱,那么A减去1000元,B加上1000元,必须保证这两个要么不执行,要么同时都执行完。

举个例子,对于静态变量进行i++操作,包含四步:

  • 将i从主存中读到工作内存中;
  • 进行+1的操作;
  • 将结果写入到工作内存,现在工作内存中的i为0 + 1 = 1
  • 将工作内存的值刷回主存。

如果不保证原子性,有可能两个线程都将i = 0 读到工作内存中,然后 + 1, 然后写回主存,最后主存里的i为1, 但实际上我们希望它进行两次加法操作,希望得到的值为2,那么就出现了线程安全问题。

因此我们可以用synchronized关键字进行加锁,保证同步代码块或者同步方法的原子性。

  1. 可见性

一个线程对共享变量值的修改,能够及时被其他线程所看到。

  1. 有序性

JVM会对我们写的代码进行指令重排优化,它优化的前提条件是保证单线程下不会出错,但是这可能会导致多线程出现错误。我们通常使用Volatile来保证有序性,防止进行指令重排。

思考,为什么有synchronized了,还要在单例懒汉模式中使用volatile?

 class LazySingleTon {
    private  volatile static LazySingleTon instance;

    private LazySingleTon(){

    }

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

因为,new一个对象的过程是:

  1. 分配内存空间;
  2. 初始化该对象;
  3. 将该对象的地址赋值给instance变量;

然而,可能发生指令重排,导致2和3互换,那么当3执行完毕时,2可能还没有执行完,也就是该对象还没有初始化完全。

那么当另一个线程过来的时候,在第9行进行判断

if (instance == null)

此时为false,那么就会直接返回该instance,但是因为该instance是不完整的,结果就是导致出错,因此我们需要加上volatile关键字。

总结:

  1. synchronized能保证原子性,但是不能防止指令重排;
  2. 唯一能防止指令重排的就是volatile;
  3. 此外,volatile和synchronized都能保证可见性。

28. 为什么使用线程池,参数解释

  1. 降低资源消耗:如果不使用线程池,就需要频繁的生成和销毁线程。而使用线程池,某个线程完成任务后又进入线程池,等到下一个线程使用它。这样可以提高线程的利用率,降低创建和销毁线程的损耗。
  2. 提高响应速度:任务过来了,有线程可以直接用,而不需要先创建,再使用;
  3. 提高线程的可管理性:线程是稀缺资源,使用线程池可以统一分配,调优监控

参数,先来看源码:

ExecutorService executor = Executors.newFixedThreadPool(5);
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

可以看到这5个参数:

  1. corePoolSize: 核心线程数是数量,为正常情况下创建的工作的线程数,一经创建并不会消除,是一种常驻线程,例如设置为5;
  2. maximumPoolSize: 最大线程数,表示最大的允许被创建的线程数,例如设置为10,就有5个核心的线程,以及还可以再创建5个非核心线程。例如当前任务数太多,核心线程都用完了,新任务再来的时候无法满足需求时,就会创建新的线程。
  3. keepAliveTime, unit: 当某个线程(不是核心线程)空闲时间达到该值时,被回收。unit是单位
  4. wordQueue:用来存放待执行的任务,假设核心线程都被使用了,还有任务过来,就放入队列中进行等待。如果队列满了,才会去创建核心线程以外的线程。如果所有的线程都被使用了,并且队列也满了,就必须设置拒绝策略。即后面要提到的handler

此外,还有一些参数:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }
  1. ThreadFactory: 线程工厂用来创建线程,例如设置名字,设置线程参数,不传就是使用默认的工厂。
  2. Handler: 即任务拒绝策略,当4中的问题发生的时候,会来执行handler;或者当我们调用shutdown关闭线程池,再继续提交任务就会遭到拒绝。

29. 简述线程池的处理流程

image.pngimage.png

30. 线程池中阻塞队列的作用?为什么是先添加队列而不是先创建最大线程?

阻塞队列就是,当队列中为空或者满了的时候,都去调用wait方法进入阻塞状态,从而释放CPU资源:

  • 为空,阻塞住,直到有任务放进来;
  • 满了,阻塞住,保证新进来的任务不会被丢弃

为什么要先添加进队列,而不是先创建最大线程?
因为创建新线程需要获取全局锁,其他线程全部被阻塞,影响性能。举个例子:
厂里10个工人,有11个活要干,不会立马再去招1个临时工,而是让第11个活先等着,等有工人完成一个任务后,让该工人去接手第11个任务。毕竟招临时工花的钱更多。

31. 线程池复用的原理

即线程和任务是解耦的,回想我们使用线程池的流程:

  1. new一个线程池,里边有n个线程;
  2. new一个Runable对象,重写run方法;
  3. 线程池.submit(Runable对象);

说明线程是线程,任务是任务。摆脱了通过Thread创建线程时,任务也在线程里这一个问题。

核心原理:
线程池里的线程不断从阻塞队列中获取新任务,调用新任务的run方法!不是调用start方法,如果调用的是start方法就又会去创建一个子线程。通过这种方式使用固定线程来不停调用任务的run方法。

32. CopyOnWriteArrayList的底层原理

copyOnWriteArrayList是线程安全的ArrayList,如果我们通过多个线程对ArrayList添加元素,会抛出多线程修改异常

public class TestCopyOnWriteArrayList {
    public static void main(String[] args) {
        List<Integer> arrayList = new ArrayList<>();
        new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                arrayList.add(i);
            }
        }).start();

        new Thread(() -> {
            for (int i = 101; i <= 200; i++) {
                arrayList.add(i);
            }
        }).start();

        for (int i : arrayList) {
            System.out.println("i = " + i);
        }
    }
}

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
    at test.copyOnWriteArrayList.TestCopyOnWriteArrayList.main(TestCopyOnWriteArrayList.java:23)

此时换成CopyOnWriteArrayList即可解决这个问题:

public class TestCopyOnWriteArrayList {
    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> arrayList = new CopyOnWriteArrayList<>();
        new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                arrayList.add(i);
            }
        }).start();

        new Thread(() -> {
            for (int i = 101; i <= 200; i++) {
                arrayList.add(i);
            }
        }).start();

        for (int i : arrayList) {
            System.out.println("i = " + i);
        }
    }
}

// 成功输出1 ~ 200

原理:写前加ReentrantLock, 写后解锁。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 得到当前数组
        Object[] elements = getArray();
        int len = elements.length;

        // 复制一份新数组,长度为原数组 + 1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;

        // 将新数组复制会去
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

33. 深拷贝和浅拷贝

一个对象中既有基本数据类型,也有对实例对象的引用,当拷贝时

  • 浅拷贝拷贝基本数据类型,以及对象的引用,在Java堆中并没有拷贝出一个新的实例对象

如图,原对象和拷贝出来的对象都指向同一个user1:
image.png

  • 深拷贝拷贝基本数据类型,并且会重新复制一份Java堆中的实例对象

如图,原对象和拷贝出来的对象指向两个不同的实例对象。
image.png

如果我们仅仅实现Cloneable接口,并且在clone方法里仅仅调用super.clone(),就是浅拷贝:

public class Order implements Cloneable{
    User user;

    public Order(User user) {
        this.user = user;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

那么拷贝后,指向同一个user。

public class TestDeepCopy {
    public static void main(String[] args) throws CloneNotSupportedException {
        User user = new User();
        Order order = new Order(user);
        Order orderClone = (Order) order.clone();

        System.out.println(order.user);
        System.out.println(orderClone.user);
    }
}

// 输出为同一个对象
test.deepcopy.User@74a14482
test.deepcopy.User@74a14482

如果要深拷贝,就要重写clone方法,自己设置新的对象进去:

public class Order implements Cloneable {
    User user;

    public Order(User user) {
        this.user = user;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Order newOrder = (Order) super.clone();
        User newUser = (User) user.clone();
        newOrder.user = newUser;
        return newOrder;
    }
}

public class TestDeepCopy {
    public static void main(String[] args) throws CloneNotSupportedException {
        User user = new User();
        Order order = new Order(user);
        Order orderClone = (Order) order.clone();

        System.out.println(order.user);
        System.out.println(orderClone.user);
    }
}

// 输出的User为不同对象
test.deepcopy.User@74a14482
test.deepcopy.User@1540e19d