Java八股文面试(细节决定一切)[持续更新中]

目录

第一章--Java基础篇

   1. 你是怎么理解OOP面向对象 

   2. 重载和重写的区别

   3. 接口与抽象类的区别

   4. 深拷贝与浅拷贝的理解

   5. sleep和wait区别

   6. 什么是自动装箱 int和Integer有什么区别

   7. == 和equals的区别

   8.String能被继承吗?为什么用final修饰

   9. String Buffer和String Builder区别

  10.final ,  finally , finalize 

  11. Object中有哪些方法

  12. 说一下集合体系

  13. ArrayList 和 LinkedList区别

  14. HashMap底层是 数组 + 链表 + 红黑树 为什么要用这几类结构

  15. HashMap和HashTable区别

  16. 线程的创建方式

  17. 线程的状态转换有什么 (生命周期)

  18: Java中有几种类型的流

  19.  请写出你最常见的5个RuntimeException

  20. 谈谈你对反射的理解

  21. 什么是java序列化 , 如何实现java序列化

  22. Http常见的状态码 

  23. GET和POST的区别 

  24. Cookie和Session的区别

第二章--Java高级篇

   1. HashMap底层源码

   2. JVM内存分为哪几个区,每个区的作用是什么

   3. Java中垃圾收集器的方法有哪些

   4. 如何判断一个对象是否存活( 或者GC对象的判定方法 )

   5. 什么情况下会产生StackOverflowError ( 栈溢出 ) 和OutOfMemoryError ( 堆溢出 ) 怎么排查

   6. 什么是线程池, 线程池有哪些 ( 创建 )

   7. 为什么要使用线程池

   8. 线程池底层工作原理

   9. ThreadPoolExecutor对象有哪些参数, 怎么设定核心线程数和最大线程数 拒绝策略有哪些

   10. 常见线程安全的并发容器有哪些

   11. Atomic原子类了解多少 原理是什么


第一章--Java基础篇

   1. 你是怎么理解OOP面向对象 

         面相对象是利用语言对现实事物进行抽象,面向对象有以下特征: 

          1. 继承: 继承是从已有类得到继承信息创建新类的过程

          2. 封装: 封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口

          3. 多态: 多态是指允许不同子类类型的对象对同一消息做出不同的响应

   2. 重载和重写的区别

          1. 重载是发生在本类,重写是发生在父类与子类之间

          2. 重载的方法名必须相同,重写的方法名相同且返回值类型必须相同

          3. 重载的参数列表不同,重写的参数列表必须相同

          4. 重载的访问权限不能比父类被重写的方法的访问权限更低

          5. 构造方法不能被重写  

   3. 接口与抽象类的区别

          1. 抽象类要被子类继承,接口要被类实现

          2. 接口可以多继承接口,但类只能单继承

          3. 抽象类可以有构造器,接口不能有构造器

          4. 抽象类: 出来不能实例化抽象类之外,它和普通的java类没有任何区别

          5. 抽象类: 抽象方法可以有public,protected和default这些修饰符  接口: 接口只能是public

          6. 抽象类: 抽象类可以有成员变量  接口: 接口只能声明常量

   4. 深拷贝与浅拷贝的理解

           深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用.

          1. 浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象.

          2. 深拷贝是指,即会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,背部的类执行指向的不是同一个对象.

   5. sleep和wait区别

          1. sleep方法: sleep方法数据Thread类中的方法,释放CPU给其他线程,不释放锁资源

          2. wait方法: wait方法数据Object类中的方法,释放CPU给其他线程,同时释放锁资源

              wait(1000)等待超过1s被唤醒

              wait()一直等待需要通过notify或者notifyAll进行唤醒

              wait方法必须配合synchronized一起使用,不然运行时就会抛出IllegalMonitorStateException异常

    public static void main(String[] args) {
        Object o = new Object();
        Thread thread = new Thread(() -> {
            synchronized (o) {
                System.out.println("新线程获取锁的时间:" + LocalDateTime.now() + "  新线程名称:" + Thread.currentThread().getName());
                try {
                    // wait 释放CPU同事释放锁 线程进入等待状态
                    o.wait(2000);

                    // sleep 释放CPU不释放锁
                    // Thread.sleep(2000);

                    System.out.println("新线程获取释放锁锁时间:" + LocalDateTime.now() + "  新线程名称:" + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        // 启动新线程
        thread.start();

        try {
            // 休眠100毫秒
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        System.out.println("主线程获取锁时间" + LocalDateTime.now() + "  主线程名称:" + Thread.currentThread().getName());
        synchronized (o) {
            System.out.println("主线程获取释放锁锁时间" + LocalDateTime.now() + "  主线程名称:" + Thread.currentThread().getName());
        }
    }

   6. 什么是自动装箱 int和Integer有什么区别

          基本数据类型,如int,float,double,char,byte不具备对象的特征,不能调用对象.

          1. 装箱: 将基本数据类型转换为包装类对象

          2. 拆箱: 将包装类对象转换为基本数据类型值

    Java中为什么要引入自动装箱和自动拆箱的功能? 主要是用于Java集合中,List<Integer> list = new ArrayList<Integer>();

    list集合如果要放整数的话,只能放对象,不能放基本数据类型,因此需要将整数自动装箱成对象

    实现原理:javac编译器的语法糖,底层是通过Integer,valueOf()和Integer,intValue()方法实现的

    区别: 

          1. Integer是int的包装类,int则是java中的一种基本数据类型

          2. integer变量必须实例化之后才能使用,而int变量不需要

          3. Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象,而int则是直接存储数据值

          4. Integer的默认值是null,int默认是0

   7. == 和equals的区别

          1. == :

             如果比较的是基本数据类型,那么比较的就是变量的值

             如果比较的是引用数据类型,那么比较的就是地址值(两个对象是否指向同一块内存)

          2. equals:

             如果没重写equals方法比较的是两个对象的地址值

             如果重写了equals方法我们往往比较的是对象中的属性的内容

             equals方法是从Object类中继承的,默认的实现就是使用==

   8.String能被继承吗?为什么用final修饰

          1. String不能被继承,因为String类有final修饰符,而final修饰符的类是不能被继承的.

          2. String类是最常用的类之一,为了效率,禁止被重载和重写.

          3. 为了安全.String类中有native关键字修饰和调用系统级别的本类方法,调用了操作系统的API.如果方法可以重写,可能被植入恶意代码,破坏程序. Java的安全性也体现在这里.

   9. String Buffer和String Builder区别

          1. StringBuffer与StringBuilder中采用的方法和功能是完全等价的.

          2. 只是StringBuffer中的方法采用了synchronized关键字进行修饰,因此是线程安全的,而StringBuilder没有这个修饰,可以被认为是线程不安全的.

          3. 在单线程程序下,StringBuilder效率更快,因为它不需要加锁,不具备多线程安全,而StringBuffer则每次都需要判断锁,效率相对更低. 

  10.final ,  finally , finalize 

          1. final: 修饰符(关键字) 有三种用法: 修饰类 , 变量 , 方法 . 修饰类时 , 意味着他不能再派生出新的子类,既不能被继承,因此他和abstract是反义词 . 修饰变量时 , 该变量使用中不能被修改,必须在声明时给定初始值 , 在引用中只能读取不可修改 , 即为常量 . 修饰方法时 , 也同样只能使用 , 不能在子类中被重写.

          2. finally: 通常放在try...catch的后面构造最终执行代码块 , 这就意味着程序无论正常执行还是发生异常 , 这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中 . 

          3. finalize: Object类中定义的方法 , Java中允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作 . 这个方法是有垃圾收集器在销毁对象时调用的,通过重写finalize()方法可以调整系统资源或者执行其他清理工作 .

  11. Object中有哪些方法

          1. protected Object clone() --> 创建并返回此对象的一个副本.

          2. boolean equals(Object obj) --> 指示某个其他对象是否与此对象 "相等"

          3. protected void finalize() --> 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法 .

          4. Class<? extendsObject> getClass() --> 返回一个对象的运行时类.

          5. int hashCode() --> 返回一个对象的运行时类 .

          6. void notify() --> 唤醒在此对象监视器上等待的单个线程 .

          7. void notifyAll() --> 唤醒在此对象监视器上等待的所有线程 .

          8. String toString() --> 返回该对象的字符串表示. 

          9. void wait() --> 导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法

             void wait(long timeout) --> 导致当前的线程等待, 直到其他线程调用此对象的notify()或者notifyAll()方法,或者超过指定的时间量.

             void wait(long timeout, int nanos) --> 导致当前的线程等待, 直到其他线程调用此对象的notify().

  12. 说一下集合体系

  13. ArrayList 和 LinkedList区别

          1. ArrayList 是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构 .

          2. 对于随机访问get和set ,ArrayList效率优于LinkedList , 因为LinkedList 要移动指针 . 

          3. 对于新增和删除操作add和remove , LinkedList比较占优势,因为ArrayList要移动数据. 这一点要看实际情况 .  若只是对单条数据插入或删除 , ArrayList的速度反而要优先于LinkedList . 但若是批量随机的插入删除数据 , LinkedList的速度大大优于ArrayList , 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据 . 

  14. HashMap底层是 数组 + 链表 + 红黑树 为什么要用这几类结构

          1. 数据Node<K, V> table , 哈希表 , 根据对象的key的hash值进行在数组里面是哪个节点

          2. 链表的作用是解决hash冲突 , 将hash值取模之后的对象存在一个链表放在hash值对应的槽位

          3. 红黑树 JDK8 使用红黑树来替代超过8个节点的链表,主要是查询性能的提升,从原来的O(n)到O(logn)

          4. 通过hash碰撞 ,  让HashMap不断产生碰撞,那么相同的key的位置的链表就会不断增长,当对这个HashMap的相应位置进行查询的时候,就会循环遍历这个超级大的链表,性能就会下降,所以改用红黑树 

  15. HashMap和HashTable区别

          1. 线程安全性不同

              HashMap是线程不安全的,HashTable是线程安全的,其中的方法是Synchronized , 在多线程并发的情况下,可以直接使用HashTable , 但是使用HashMap时必须自己增加同步处理.

          2. 是否提供contains方法

              HashMap只有containsValue和containsKey方法;HashTable有contains , containsValue , containsKey三个方法,其中contains和containsValue方法功能相同 . 

          3. key和value是否允许为null值

              HashTable中 , key和value都不允许为null值, HashMap中 , null可以作为键 , 这样的键只能有一个 ; 可以有一个或多个键对应的值为null . 

          4. 数组初始化和扩容机制

              HashTable在不指定容量的情况下的默认容量为11, HashMap为16 , HashTable不要求底层数组的容量一定为2的整数次幂 , 而HashMap则要求一定为2的整数次幂.

              HashTable扩容时,将容量变为原来的2倍加1 ,而hashMap扩容时 , 将容量变为原来的2倍 . 

  16. 线程的创建方式

          1. 继承Thread类创建线程

          2. 实现Runnable接口创建线程

          3. 使用Callable和Future创建线程 有返回值

          4. 使用线程池创建线程

public class TestThread {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 继承Thread
        ThreadClass thread = new ThreadClass();
        // 启动线程
        thread.start();
        // 休眠100毫秒
        Thread.sleep(100);
        System.out.println("-----------------------------------");

        // 实现Runnable
        RunnableClass runnable = new RunnableClass();
        // 启动线程
        new Thread(runnable).start();
        // 休眠100毫秒
        Thread.sleep(100);
        System.out.println("-----------------------------------");

        // 实现Callable
        FutureTask futureTask = new FutureTask(new CallableClass());
        // 启动线程
        futureTask.run();
        System.out.println("callable返回值: " + futureTask.get());
        // 休眠100毫秒
        Thread.sleep(100);
        System.out.println("-----------------------------------");



        // 线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                1,
                1,
                2,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10)
        );
        threadPoolExecutor.execute(thread);
        threadPoolExecutor.shutdown();
        // 休眠100毫秒
        Thread.sleep(100);
        System.out.println("-----------------------------------");


        // 使用并发包Executors
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        executorService.execute(thread);
        executorService.shutdown();
    }

}

class ThreadClass extends Thread {
    @Override
    public void run() {
        System.out.println("我是继承Thread形式: " + Thread.currentThread().getName());
    }
}

class RunnableClass implements Runnable {
    @Override
    public void run() {
        System.out.println("我是实现Runnable形式: " + Thread.currentThread().getName());
    }
}

class CallableClass implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("我是实现Callable");
        return "我是返回值,可以通过get方法获取";
    }
}

  17. 线程的状态转换有什么 (生命周期)

          1. 新建状态(New) : 线程对象被创建后,就会进入新建状态.例如Thread thread = new Thread() .

          2. 就绪状态(Runnable) : 也可以称为"可执行状态". 线程对象被创建后 , 其他线程调用了该对象的start()方法,从而来启动该线程. 例如thread.start() . 处于就绪状态的线程,随时可能被CPU调度执行.

          3. 运行状态(Running) : 线程获取CPU权限进行执行. 需要注意的是,线程只能从就绪状态进入运行状态.

          4. 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行.直到线程进入就绪状态,才有机会赚到运行状态.主色的情况分三种:

             1. 等待阻塞 --- 通过调用线程的wait()方法,让线程等待某工作的完成.

             2. 同步阻塞 --- 线程在获取synchronized同步锁失败(因为锁被其他线程所占用) , 它会进入同步阻塞状态.

             3. 其他阻塞 --- 通过调用线程的sleep()或者join()或发出I/O请求时 , 线程会进入阻塞状态 . 当sleep()状态超时 , join()等待线程终止或者超时.或者I/O处理完毕时,线程重新转入就绪状态

          5. 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法 , 该线程结束生命周期. 

  18: Java中有几种类型的流

  19.  请写出你最常见的5个RuntimeException

         1. java.lang.NullPointerException

             空指针异常:

             出现原因: 调用未经初始化的对象或者不存在的对象

         2. java.lang.NumberFormatException

             指定的类找不到:

             出现原因:类的名称和路径加载错误; 通常是程序试图通过字符串来加载某个类时可能引发异常

         3. java.lang.IndexOutOfBoundsException

             字符串转换为数字异常;

             出现原因: 字符型数据中包含非数字型字符.

         4. java.lang.IllegalArgumentException

             方法传递参数错误

         5. java.lang.ClassCastException

             数据类型转换异常

  20. 谈谈你对反射的理解

         1. 反射机制

             所谓的反射机制就是Java语言在运行时拥有的一项自观的能力 . 通过这种能力可以彻底了解自身的情况为下一步的动作做准备. 

             Java的反射机制的实现要借助于4个类: class , Constructor , Field , Method ; 其中class代表的时类对象,Constructor = 类的构造器对象, Field - 类的属性对象 , Method - 类的方法对象 . 通过这四个对象我们可以粗略的看到一个类的各个组成部分 . 

         2. java反射的作用

             在Java运行时环境中,对于任意一个类,可以直到这个类有哪些属性和方法.对于任意一个对象,可以调用它的任意一个方法. 这种动态获取类的信息以及动态调用对象的方法的功能来自于Java语言的反射(Reflection)机制. 

         3. Java反射机制提供功能

            在运行时判断任意一个对象所属的类.

            在运行时构造任意一个类的对象 . 

            在运行时判断任意一个类所具有的成员变量和方法 . 

            在运行时调用任意一个对象的方法 . 

  21. 什么是java序列化 , 如何实现java序列化

         1. 序列化是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化. 可以对流化后的对象传输于网络之间. 序列化是为了解决在对对象流进行读写操作是所引发的问题. 

         2. 序列化的实现: 将需要被序列化的类实现Serializable接口,该接口没有需要实现的方法,implements Serializable 只是为了标注该对象时可被序列化的,然后使用输出流(咧: FileOutPutStream) 来构造一个ObjectOutputStream(对象流) 对象, 接着使用ObjectOutputStream对象的writeObjectStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话需要输入流.

  22. Http常见的状态码 

         1. 200 OK 客户端请求成功

         2. 301 Permanently Moved(永久移除) , 请求的URL已移走. Response中应该包含一个Location URL , 说明资源现在锁处的位置

         3. 302 Temporarily Moved 临时重定向

         4. 400 Bad Request 客户端请求有语法错误,不能被服务器所理解

         5. 401 Unauthorized 请求未经授权,这个状态码必须和 WWW-Authenticate报头域一起使用

         6. 403 Forbidden 服务器收到请求,但是拒绝提供服务

         7. 404 Not Found 请求资源不存在,eg: 输入了错误的URL

         8. 500 Internal Server Error 服务器发生补课预期的错误

         9. 503 Server Unavailable 服务器当前不能处理客户端的请求,一段时间后可能恢复正常

  23. GET和POST的区别 

         1. GET请求的数据回附在URL之后 ( 就是把数据放置在HTTP协议头中 ) , 以? 分割URL和传输数据,参数之间以&相连 , 如: login.action?name=zhangsan&password=zhang@123 , POST把提交的数据放置在HTTP包的包体中 .

         2. GET方法提交的数据最多只能是1024字节 , 理论上POST没有限制, 可传较大量的数据 .

             其实这样说是错误的 , 不准确的: "GET方法提交的数据最多只能是1024字节" , 因为GET是通过URL提交数据, 那么GET可提交的数据量就跟URL的长度有直接关系了.

             而实际上 , URL不存在参数上限的问题 , HTTP协议规范没有对URL长度进行限制 .

             这个限制是特定的浏览器及服务器对他的限制 . IE对URL长度的限制是2083字节 (2K+35).

             对于其他浏览器,如Netscape , FireFox等 , 理论上没有长度限制,其限制取决于操作系统的支持. 

         3. POST的安全性要比GET的安全性高 .

             注意:这里所说的安全性是上面get提到的 " 安全 " 不是同一个概念 .

            上面 " 安全 " 的含义仅仅是不做数据修改 .

            而这里安全的含义是真正的Security的含义 , 比如: 通过GET提交数据,用户名和密码将明文出现在URL上

         因为:

                 (1) 登录页面可能被浏览器缓存

                 (2) 其他人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码了,除此之外, 使用GET提交数据还可能回造成Cross-site request forgery攻击.

         4. GET是向服务器发索取数据的一种请求,而POST是向服务器提交数据的一种请求,

         5. 在FORM ( 表单 ) 中Method默认为 " GET " ,实质上, GET 和 POST 只是发送的机制不同,并不是一个取一个发 ! !

         详解可看: GET和POST区别

  24. Cookie和Session的区别

         1. Cookie是web服务器发送给浏览器的一块信息, 浏览器会在本地一个文件中给每一个Web服务器存储Cookie . 以后浏览器在给特定的web服务器发送请求时, 同时回发送所有为该服务器存储的Cookie. 

         2. Session是存储在web服务器端的一块信息,Session对象存储特定用户会话所需的属性及配置信息. 当用户在应用程序的Web页之间跳转时, 存储在Session对象的变量将不会丢失, 而是在整个用户会话中一直存在下去

         3. Cookie和Session的不同点

         无论客户端做怎样的设置 , Session都能正常工作. 当客户端禁用Cookie时将无法使用Cookie

         在存储的数据量方面: Session能够存储任意的Java对象, Cookie只能存储String类型的对象 .

         详解可看: Cookie和Session的区别

第二章--Java高级篇

   1. HashMap底层源码

         HashMap的底层结构在JDK1.7中由 数组+链表 实现 , 在JDK1.8中由 数组+链表+红黑树 实现,以数组+链表的结构为例.

          JDK1.8之后的put方法: 

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素(处理hash冲突)
    else {
        Node<K,V> e; K k;
        // 判断table[i]中的元素是否与插入的key一样,若相同那就直接使用插入的值p替换掉旧的值e。
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
        // 判断插入的是否是红黑树节点
        else if (p instanceof TreeNode)
            // 放入树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 不是红黑树节点则说明为链表结点
        else {
            // 在链表最末插入结点
            for (int binCount = 0; ; ++binCount) {
                // 到达链表的尾部
                if ((e = p.next) == null) {
                    // 在尾部插入新结点
                    p.next = newNode(hash, key, value, null);
                    // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
                    // 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
                    // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) {
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 修改的次数
    ++modCount;
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

          HashMap基于哈希表的Map接口实现, 是以key--value存储的形式存在,即主要用来存放键值对.HashMao的实现不是同步的,这意味着它不是线程安全的.它的key , value都可以为null. 此外 , HashMap的映射不是有序的. 

         JDK1.8之前HashMap由 数组+链表 组成的 , 数组是HashMap的主体 , 链表则主要为了解决哈希冲突 ( 两个对象调用的hashCode方法计算的哈希码是一致导致计算的数组索引值相同) 而存在的 ( " 拉链法" 解决冲突 ) JDK1.8以后在解决哈希冲突时有了较大的变化, 当链表的长度大于阈值 ( 或者红黑树的边界值 , 默认为8 ) 并且当前的数组的长度大于64时 , 此时此索引位置上的数据改为使用红黑树存储. 

         补充: 将链表转换红黑树前会判断,即使阈值大于8 , 但数组长度小于64, 此时并不会将链表转换为红黑树. 而是选择进行数组扩容.

         这样做的目的是因为数组比较小, 尽量避开红黑树结构,这种情况下变为红黑树结构,反而胡降低效率,因为红黑树需要进行左旋 , 右旋 , 变色这些操作来保持平衡 . 同时数组长度小于64时 , 搜索是将相对要快些 . 所以综上所述为了提高性能和减少搜索时间, 底层在阈值大于8并且数组长度大于64时,链表才会转换为红黑树. 具体可以参考treeifyBin方法.

         详细细节可参考百度进行了解

   2. JVM内存分为哪几个区,每个区的作用是什么

          Java虚拟机主要分为以下几个区

            1. 方法区

                1.  有时候也成为永久代,在该区很少发生垃圾回收, 但是并不代表不发生GC , 在这里进行GC主要是对方区里的敞亮吃和对类型的卸载

                2. 方法区主要用来存储已被虚拟机加载的类信息 , 常量 , 静态变量和即时编译后的代码等数据

                3. 该区域是被线程共享的

                4. 方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用. 该常量并不一定是编译时确定, 运行时生成的常量也会存在这个常量池中. 

           2. 虚拟机栈

               1. 虚拟机栈也就是我们平常所称的栈内存 , 它为Java方法服务 , 每个方法在执行的时候都会创建一个栈帧, 用于存储局部变量表 , 操作数栈 , 动态链接和方法出口等信息. 

               2. 虚拟机栈是线程私有的, 他的生命周期与线程相同. 

               3. 局部变量表里存储的是基本数据类型, returnAddress类型 ( 指向一条字节码指令的地址 ) 和对象的引用, 这个对象引用有可能是指向对象起始地址的一个指针,也有可能是代表对象的句柄或者与对象相关联的位置. 局部变量所需的内存空间在编译器间确定

               4. 操作数栈的作用主要用来存储运算结果以及运算的操作数, 它不同于局部变量表通过索引来访问, 而是压栈和出栈的方式

               5. 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用, 持有这个引用是为了支持方法调用过程中的动态连接 . 动态链接就是将常量池中的符号引用在运行期转化为直接引用.

           3. 本地方法栈

               本地方法栈和虚拟机栈类似,只不过本地方法栈为Native方法服务. 

           4.堆

               Java堆是所有线程所共享的一块内存 , 在虚拟机启动时创建, 几乎所有的对象实例都在这里创建 , 因此该区域经常发生垃圾回收操作.

           5. 程序计数器

               内存空间小,字节码解释器工作时通过改变这个计数器可以选取下一条需要执行的字节码指令 , 分区 , 循环 , 跳转 , 异常处理和线程恢复等功能都需要依赖这个计数器完成 . 该内存区域是唯一 一个Java虚拟机规范没有规定任何OOM情况的区域. 

         详解可看:深入理解Java虚拟机之JVM内存布局

   3. Java中垃圾收集器的方法有哪些

           采用分区分代回收思想:

           1. 复制算法: 年轻代中使用的是Minor GC , 这种GC算法采用的是复制算法( Copying )

              1) 效率高,缺点: 需要内存容量大, 比较耗内存

              2) 使用在占用空间较小, 刷新次数多的新生区

           2. 标记清除: 老年代一般是由标记清除或者是标记清除与标记整理的混合实现

              1) 效率比较低 , 会差生碎片

           3. 标记整理: 老年代一般是由标记清除或者是标记清除与标记整理的混合实现

              1) 效率低速度慢,需要移动对象,但不会产生碎片

   4. 如何判断一个对象是否存活( 或者GC对象的判定方法 )

           1. 引用计数法

              所谓引用计数法就是给每一个对象设置一个引用计数器 , 每当有一个地方引用这个对象时,就将计数器加一 , 引用失效时 , 计数器就减一 . 当一个对象的引用计数器为零时 , 说明此对象没有被引用 , 也就是 " 死对象 " , 将会被垃圾回收 . 

              引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B , 对象B又引用着对象A,那么此时A , B对象的引用计数器都不为零 , 也就造成无法完成垃圾回收 , 所以主流的虚拟机都没有采用这种算法. 

           2. 可达性算法( 引用链法 ) 

             1. 该算法的基本思路就是通过一些被称为引用链(GC Roots ) 的对象作为起点 , 从这些节点开始向下搜索 , 搜索走过的路径被称为 ( Reference Chain ) 当一个对象到GC Roots没有任何引用链相连时  ( 即从GC Roots节点到该节点不可达 ) , 则证明该对象时不可用的. 

             2. 在Java中可以作为GC Roots的对象有一下几种: 虚拟机栈中引用的对象 , 方法区类静态属性引用的对象 , 方法区常量池引用的对象 , 本地方法栈JNI引用的对象 . 

   5. 什么情况下会产生StackOverflowError ( 栈溢出 ) 和OutOfMemoryError ( 堆溢出 ) 怎么排查

           1. 引发StackOverflowError ( 栈溢出 ) 的常见原因有以下几种

              1. 无线递归循环使用 ( 最常见 )

              2. 执行了大量方法 , 导致线程栈空间耗尽

              3. 方法内声明了海量的局部变量

              4. native代码有栈上分配的逻辑,并要求的内存还不小 , 比如java.net.SocketInputStream.read0 会在栈上要求分配一个64KB的缓存 ( 64位Linux) 

           2. 引发OutOfMemoryError ( 堆溢出 ) 的常见原因有以下几种

              1. 内存中加载的数据量过于庞大, 如一次从数据库取出过多数据

              2. 集合类中由对 对象的引用 , 使用完后未清空, 使得JVM不能回收

              3. 代码中存在死循环或者循环产生过多重复的对象实体

              4. 启动参数内存值设定的过小

           3. 排查: 可以通过jvisualvm进行内存快找分析

           可参考: Java内存溢出OutOfMemoryError的产生与排查

           配置堆大小 -Xms1m -Xmx1m

           配置栈大小 -Xss256k

public class StackOverFlowTest {
    private static int count = 1;

/**
 * 程序入口主方法。
 * 本程序通过调用两个方法来演示不同的内存溢出情况。
 */
public static void main(String[] args) {
    // 模拟栈溢出
    // getDieCircle();

    // 模拟堆溢出
     getOutOfMem();
}

    /**
     * 模拟栈溢出
     */
    public static void getDieCircle() {
        System.out.println(count++);
        getDieCircle();
    }

    /**
     * 模拟堆溢出
     */
    public static void  getOutOfMem() {
        while (true) {
            Object o = new Object();
            System.out.println(o);
        }
    }

}

   6. 什么是线程池, 线程池有哪些 ( 创建 )

           线程池就是事先讲多个线程对象放到一个容器中, 当使用的时候就不用new线程,而是直接去池里拿线程即可,节省了开辟子线程的时间, 提高代码执行效率

           在JDK的java.util.concurrent.Executors中提供了生成多种线程的静态方法 .

           ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

           ExecutorService new FixedThreadPool = Executors.newFixedThreadPool(4);

           ScheduledExecutorService newScheduledExectorService = Executors.newScheduledThreadPool(4);

           ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();

           然后调用他们的 execute 方法即可.

           这4种线程池地城全部是ThreadPoolExecutor对象的实现, 阿里规范手册中规定线程池采用ThreadPoolExecutor自定义的,实际开发也是.

           1. newCachedThreadPool

           创建一个可缓存线程池 , 如果线程池长度超过处理需要, 可灵活回收空闲线程, 若无可回收,则新建线程. 这种类型的线程池特点是: 工作线程的创建数量几乎没有限制( 其实也有限制的, 数目为Interger.MAX_VALUE) , 这样可灵活的往线程池中添加线程. 

           如果长时间没有往线程池中提交任务, 即如果工作线程空闲了指定的时间 ( 默认为1分钟 ) , 则该工作线程将自动终止. 终止后, 如果你又提交了新的任务,则线程池重新创建一个工作线程. 

           2. newSingleThreadPoole

           创建一个指定工作线程数量的线程池. 每当提交一个任务就创建一个工作线程, 如果工作线程数量达到线程初始化的最大数, 则将提交的任务存入到池队列中. FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点.但是,在线程池空闲时, 即线程池中没有可运行任务时,他不会释放工作线程,还会占用一定的系统资源.

           3. newSingleThreadExecutor

           创建一个单线程化的Executor, 即只创建唯一的工作者线程来执行任务, 它只会用唯一的工作线程来执行任务, 但保证所有任务按照指定顺序 ( FIFO,LIFO,优先级 ) 执行. 如果这个线程异常结束,会有另一个取代它, 保证顺序执行. 单工作线程最大的特点是可保证顺序地执行各个任务, 并且在任意给定的时间不会有多个线程是活动的. 

           4. newScheduleThreadPool

           创建一个定长的线程池, 而且支持定时的以及周期性的任务执行. 例如延迟3秒执行. 

public class TestThreadPool {
    /**
     * 程序的入口点。
     * 本方法演示了如何获取可用处理器的数量,并通过调用不同的线程池示例方法来展示Java线程池的使用。
     */
    public static void main(String[] args) {
        // 获取当前运行时的处理器数量
        int count = Runtime.getRuntime().availableProcessors();
        System.out.println(count);

        // 注释掉的代码是示例中未使用的线程池类型展示
        // 以下为各种线程池的示例方法调用,本代码中已注释掉,可根据需要解除注释进行测试
        // 1. 可缓存线程池
        //newCachedThreadPoolClass();
        // 2. 创建固定大小的线程池
        //newFixedThreadPoolClass();
        // 3. 定长线程池, 支持定时及周期性任务执行
        //newScheduleThreadPoolClass();
        // 4. 单线程化的线程池
        //newSingleThreadPoolClass();

        // 测试方法,用于展示线程池的使用
        test();
    }


    /**
     * 创建一个缓存线程池,并提交10个任务给线程池执行。
     * 使用Executors.newCachedThreadPool()创建一个线程池,该线程池会根据需要创建新线程来执行任务。
     * 任务是匿名内部类,实现了Runnable接口,其run方法打印出任务的索引号。
     * 最后调用线程池的shutdown方法来关闭线程池。
     */
    public static void newCachedThreadPoolClass() {
        // 创建一个缓存线程池
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

        // 提交10个任务给线程池执行
        for (int i = 0; i < 10; ++i) {
            final int index = i;
            // 每个任务都是一个匿名内部类的实例,实现Runnable接口
            cachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    // 执行任务时,打印任务的索引号
                    System.out.println(index);
                }
            });
        }
        // 关闭线程池,不再接受新的任务
        cachedThreadPool.shutdown();
    }

    /**
     * 创建一个固定大小的线程池,并提交多个任务给线程池执行。
     * 这个方法演示了如何使用Executor框架来管理线程的生命周期,并简化多线程编程。
     * 线程池的大小被固定为3,这意味着同时最多只有3个线程会执行任务。
     * 新的任务会被排队等待,直到有线程可用。
     * 使用固定大小的线程池可以避免线程过多导致的资源消耗,同时保证系统的稳定性。
     */
    public static void newFixedThreadPoolClass() {
        // 创建一个固定大小为3的线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

        // 提交10个任务给线程池执行
        for (int i = 0; i < 100; ++i) {
            final int index = i;
            // 每个任务都是一个Runnable,它们在不同的线程中执行
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    // 输出当前任务的索引
                    System.out.println(index);
                }
            });
        }

        // 关闭线程池,不再接受新的任务
        fixedThreadPool.shutdown();
    }

    /**
     * 创建一个定时任务执行池,用于演示如何定时执行任务。
     * 该方法通过ScheduledExecutorService创建一个定长的线程池,用于定时执行一系列任务。
     * 每个任务仅打印其索引值。
     * 定时任务的延迟时间为10秒。
     * 注意:此示例仅用于演示定时任务的创建和执行,实际应用中可能需要更复杂的逻辑和错误处理。
     */
    public static void newScheduleThreadPoolClass() {
        // 创建一个定长为5的定时任务执行池
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

        // 循环提交100个任务给线程池
        for (int i = 0; i < 100; ++i) {
            final int index = i;
            // 提交一个任务,该任务会在延迟10秒后执行
            // 使用匿名内部类实现Runnable接口,定义任务的执行逻辑
            scheduledThreadPool.schedule(new Runnable() {
                @Override
                public void run() {
                    // 执行任务时,打印任务的索引号
                    System.out.println(index);
                }
            }, 10, TimeUnit.SECONDS);
        }

        // 关闭线程池,等待所有任务执行完毕
        scheduledThreadPool.shutdown();
    }

    /**
     * 创建一个单线程执行器,并提交100个任务给它执行。
     * 这个方法演示了如何使用Executor框架来管理线程和任务。
     * 特别是,它使用了newSingleThreadExecutor,确保所有任务都在同一个线程中顺序执行。
     * 这种方法适用于任务之间需要保持严格顺序的情况。
     */
    public static void newSingleThreadPoolClass() {
        // 创建一个单线程的执行器服务
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

        // 提交100个任务给执行器
        for (int i = 0; i < 100; ++i) {
            final int index = i;
            // 每个任务简单地打印它的索引
            singleThreadExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(index);
                }
            });
        }

        // 关闭执行器服务,防止资源泄漏
        singleThreadExecutor.shutdown();
    }

    /**
     * 测试线程执行中异常的处理方式。
     * 本方法演示了如何使用ExecutorService来执行任务,并处理这些任务中可能抛出的异常。
     * 特别是,它展示了如何处理Runnable和Callable任务中的异常。
     */
    public static void test() {
        // 创建一个单线程的执行服务,用于顺序执行任务。
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

        // 提交一个Runnable任务,该任务故意引发一个除零异常。
        // 这展示了如何在不返回结果的简单任务中处理异常。
        singleThreadExecutor.execute(new Runnable() {
            @Override
            public void run() {
                int i = 10 / 0;
            }
        });

        // 提交一个Callable任务,该任务同样故意引发一个除零异常。
        // 与Runnable任务不同,Callable任务可以返回结果,这允许捕获并处理异常。
        Future<Boolean> future = singleThreadExecutor.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                int i = 10 / 0;
                return true;
            }
        });

        // 尝试获取Callable任务的执行结果。
        // 这一过程会阻塞,直到任务完成,或者抛出任务执行中产生的异常。
        try {
            future.get();
        } catch (InterruptedException e) {
            // 如果线程在等待结果时被中断,将中断异常转换为运行时异常并抛出。
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            // 如果任务执行中抛出异常,将执行异常转换为运行时异常并抛出。
            throw new RuntimeException(e);
        }

        // 关闭执行服务,以防止资源泄漏。
        // 所有已提交的任务都将被执行,但不再接受新的任务。
        singleThreadExecutor.shutdown();
    }
}

           通过 execute 提交的任务,才能将它抛出的异常交给未捕获异常处理器,而通过 submit 提交的任务,无论是抛出的未检查异常还是已检查异常,都将被认为是任务返回状态的一部分。如果一个由 submit 提交的任务由于抛出了异常而结束,那么这个异常将被 future.get() 封装在ExecutionException 中重新抛出。

   7. 为什么要使用线程池

           1. 线程池做的工作主要是控制运行的线程数量, 处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超过数量的线程排队等待,等其他线程执行完毕, 再从队列中取出任务来执行.

           2. 主要特点: 线程服用; 控制最大并发数; 管理线程

           第一: 降低资源消耗. 通过重复利用已创建的线程减低线程创建和销毁造成的消耗.

           第二: 提高相应速度.当任务达到时, 任务可以不需要等到线程创建就能立刻执行.

           第三: 提高线程的可管理性. 线程是稀缺资源, 如果无限制的创建, 不仅会消耗系统资源, 还会降低系统的稳定性, 使用线程可以进行统一的分配, 调优和监控

   8. 线程池底层工作原理

           1. 第一步: 线程池刚创建的时候,里面没有任何线程, 等到有任务过来的时候才会创建线程. 当然也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程

           2. 第二步: 调用 execute() 提交一个任务时, 如果当前的工作线程数< corePoolSize , 直接创建新的线程执行这个任务

           3. 第三步: 如果当前工作线程数量 >= corePoolSize , 将会放入任务队列中缓存

           4. 第四步: 如果队列已满, 并且线程池中工作线程的数量< maximumPoolSize, 还是会创建线程执行这个任务

           5. 第五步: 如果队列已满, 并且线程池中线程已达到maximumPoolSize, 这个时候会执行拒绝策略, Java线程池默认的策略是AborPolicy, 即抛出RejectedExecutionException异常

   9. ThreadPoolExecutor对象有哪些参数, 怎么设定核心线程数和最大线程数 拒绝策略有哪些

           1. corePoolSize: 核心线程数

           在ThreadPoolExecutor中有一个与它相关的配置: allowCoreThreadTimeOut ( 默认为false ) , 当allowCoreThreadTimeOut为false时, 核心线程会一直存活, 哪怕是一直空闲着, 而当allowCoreThreadTimeOut为true时核心线程空闲时间超过keepAliveTIme时会被回收. 

           2.maximumPoolSize: 最大线程数

           线程池能容纳的最大线程数, 当线程池中的线程达到最大时, 此时添加任务将会采用拒绝策略, 默认的拒绝策略是抛出一个运行时错误 ( RejectedExecutionException ) . 值得一提的是, 当初始化时用的工作队列为 LinkedBlockingDeque 时, 这个值将无效.

           3.keepAliveTime: 存活时间

           当非核心空闲超过这个时间将会被回收, 同时空闲核心线程是否收受allowCoreThreadTimeOut影响

              1. unit: keepAliveTime的单位

           4. workQueue: 任务队列

           常用有三种队列, 即SynchronousQueue, LinkedBlockingDeque( 无界队列 ) , ArrayBlockingQueue( 有界队列 ).

           5. threadFactory: 线程工厂

           ThreadFactory是一个接口, 用来创建worker. 通过线程工厂可以对线程的一些属性进行定制. 默认直接新建线程

           6. RejectedExecutionHandler: 拒绝策略

           也是一个接口, 只有一个方法, 当线程池中的资源已经全部使用, 添加新线程被拒绝时, 会调用RejectedExecutionHandler的rejectedExecution法. 默认是抛出一个运行时异常.

           线程池大小设置

           1. 需要分析线程执行的任务的特性: CPU密集型还是IO密集型

           2. 每个任务执行的平均时长大概是多少, 这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系. 

           如果是CPU密集型, 主要是执行计算任务, 响应时间很快, CPU一直在运行, 这种任务CPU的利用率很高, 那么线程数的配置应该根据CPU核心数来决定, CPU核心数=最大同时执行线程数, 加入CPU核心数为4 , 那么服务器最多能同时执行4个线程. 过多的线程会导致上下文切换反而使得效率降低. 那么线程池的最大线程数可以配置为CPU核心数+1 如果是IO密集型, 主要是进行IO操作, 执行IO操作的时间较长, 这是CPU处于空闲状态, 导致CPU的利用率不高, 这种情况下可以增加线程池的大小. 这种情况下可以结合线程的等待时长来做判断, 等待时间越高, 那么线程数也相对越多. 一般可以配置CPU核心数的2倍.

           一个公式: 线程池设定最佳线程数目 =  ( ( 线程池设定的线程等待时间 + 线程CPU时间 ) / 线程CPU时间 ) *CPU数目

           这个公式的线程CPU时间是预估的程序单个线程在CPU上运行的时间 ( 通常使用 loadrunner 测试大量运行次数求出平均值 )

           拒绝策略

              1. AbortPolicy: 直接抛出异常, 默认策略; 

              2. CallerRunsPolicy: 用调用者所在的线程来执行任务; 

              3. DiscardOldesPolicy: 丢弃阻塞队列中靠最前的任务, 并执行当前任务;

              4. DiscardPolicy: 直接丢弃任务; 当然也可以根据应用场景实现RejectedExecutionHandler接口, 自定义饱和策略, 如纪录日志或持久化存储不能处理的任务

   10. 常见线程安全的并发容器有哪些

           1. CopyOnWriteArrayList, CopyOnWriteArraySet, ConcurrentHashMap

           2.CopyOnWriteArrayList, CopyOnWriteArraySet采用写时复制实现线程安全

           3. ConcurrentHashMap采用分段锁的方式实现线程安全

   11. Atomic原子类了解多少 原理是什么

           Java的原子类都存放在并发包java.util.concurrent.atomic下,如下图:

            基本类型

                   1. 使用原子的方法更新基本类型

                   2. AtomicInteger: 整形原子类

                   3. AtomicLong: 长整形原子类

                   4.AtomicLongArray: 长整型数组原子类

            引用类型

                   1.AtomicReference: 引用类型原子类

                   2. AtomicStampedReference: 原子更新带有标记位的引用类型

                   3. AtomicMarkableReference: 院子更新整形字段的更新器

                   4. AtomicIntegerFieldUpdater: 院子更新整形字段的更新器

                   5. AtomicLongFieldUpdater: 原子更新长整形字段的更新器

                   6. AtomicStampedReference: 原子更新带有版本号的引用类型. 该类将整数值与引用关联起来, 可用于解决原子的更新数据和数据的版本号, 以及解决使用CAS进行原子更新时可能出现的ABA问题

                   1. AtomicInteger 类利用CAS ( Compare and Swap ) + volatile + native 方法来保证原子操作, 从而避免synchronized 的高消耗, 执行效率大为提升.

                   2. CAS的原理, 是拿期望值和原本的值作比较, 如果相同, 则更新成新的值. UnSafe类的objectFieldOffset() 方法是个本地方法, 这个方法是用来拿 "原值" 的内存地址, 返回值是valueOffset; 另外, value 是一个 volatile 变量, 因此JVM总是可以保证任意时刻的任何线程总能拿到该变量的最新值.

                  

[更新中... ]

  • 21
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mortal-Han

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值