Java常见面试题及答案详解

一、Java基础

1.JDK 和 JRE 有什么区别?

JRE:Java Runtime Environment(java运行时环境)。即java程序的运行时环境,包含了java虚拟机,java基础类库。
在这里插入图片描述
JDK:Java Development Kit(java开发工具包)。即java语言编写的程序所需的开发工具包。
JDK包含了JRE,同时还包括java源码的编译器javac、监控工具jconsole、分析工具jvisualvm等。
在这里插入图片描述

2.== 和 equals 的区别是什么?

一、理解”==“的含义

在java中,主要有两个作用。

1、基础数据类型:比较的是他们的值是否相等,比如两个int类型的变量,比较的是变量的值是否一样。

2、引用数据类型:比较的是引用的地址是否相同,比如说新建了两个User对象,比较的是两个User的地址是否一样。

OK。到这就注意了,你会发现,我在举引用的例子的时候,使用的是User对象,而不是String。别着急接着往下看。
在这里插入图片描述
从这个源码中你会发现,比较的是当前对象的引用和obj的引用是否相同,也就是说比较的默认就是地址。还记的在上面我们使用的是User而不是String嘛?在这里比较的是引用的地址,equals也是比较的是引用的地址,所以他们的效果在这里是一样的。
在这里插入图片描述
现在你会发现好像equals的作用和
没什么区别呀,那String类型那些乱七八糟的东西是什么呢?继续往下看马上揭晓。

三、重写equals

1、String中equals方法

看到这个标题相信你已经能找到答案里,Object对象里面的==和equals没有什么区别,这样一看equals方法存在的意义真的不大,不过后来String在Object的基础之上重写了equals,于是功能被大大的改变了。如何重写的呢?我们去String的源码中找寻答案:

二、理解equals的含义

先看看他的源码,equals方法是在Object中就有。注意了这里的源码是Object里面的equals。

public boolean equals(Object anObject) {
        if (this == anObject) { //引用比较
            return true;
        }
        if (anObject instanceof String) { //判断是否为String类型
            String anotherString = (String)anObject;
            int n = value.length;
            //anotherString.value.length 相当于 anotherString.length()
            if (n == anotherString.value.length) { 
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i]) //比较每一个字符是否相等
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

从上面的源码,我们能够获取到的信息是:String中的equals方法其实比较的是字符串的内容是否一样。也就是说如果像String、Date这些重写equals的类,你可要小心了。使用的时候会和Object的不一样。
2、测试String
看看下面的代码:
在这里插入图片描述
在上面的代码中,定义了三个字符串,分别使用==和equals去比较。为什么会出现这样一个结果呢?还需要从内存的角度来解释一下。

3、内存解释

在java中我们一般把对象存放在堆区,把对象的引用放在栈区。因此在上面三个字符串的内存状态应该是下面这样的。
在这里插入图片描述
现在明白了吧。

(1)String str1 = "Hello"会在堆区存放一个字符串对象

(2)String str2 = new String(“Hello”)会在堆区再次存放一个字符串对象

(3)String str3 = str2这时候Str3和Str2是两个不同的引用,但是指向同一个对象。

根据这张图再来看上面的比较:

(1)str1 == str2嘛?意思是地址指向的是同一块地方吗?很明显不一样。

(2)str1 == str3嘛?意思是地址指向的是同一块地方吗?很明显不一样。

(3)str2 == str3嘛?意思是地址指向的是同一块地方吗?很明显内容一样,所以为true。

(4)str1.equals(str2)嘛?意思是地址指向的内容一样嘛?一样。

(4)str1.equals(str3)嘛?意思是地址指向的内容一样嘛?一样。

(4)str2.equals(str3)嘛?意思是地址指向的内容一样嘛?一样。

OK。现在不知道你能理解嘛?

4、总结:

(1)、基础类型比较

使用==比较值是否相等。

(2)、引用类型比较

①重写了equals方法,比如String。

第一种情况:使用==比较的是String的引用是否指向了同一块内存

第二种情况:使用equals比较的是String的引用的对象内用是否相等。

②没有重写equals方法,比如User等自定义类

==和equals比较的都是引用是否指向了同一块内存。
在这里插入图片描述

3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?

答案:不对
我们先看到Objec类中hashCode()方法源码

在这里插入图片描述
该方法是个native方法,因为native方法是由非Java语言实现的,所以这个方法的定义中也没有具体的实现。根据jdk文档,该方法的实现一般是通过将该对象的内部地址转换成一个整数来实现的,这个返回值就作为该对象的哈希码值返回。

再看equals源码,尤其要注意return的说明

在这里插入图片描述
hashCode值是从hash表中得来的,hash是一个函数,该函数的实现是一种算法,通过hash算法算出hash值,hash表就是hash值组成的,一共有8个位置。因此,hashCode相同的两个对象不一定equals()也为true。
在这里插入图片描述

4.final 在 java 中有什么作用?

final作为Java中的关键字可以用于三个地方。用于修饰类、类属性和类方法

特征:凡是引用final关键字的地方皆不可修改!

  • 修饰类:表示该类不能被继承
  • 修饰方法:表示方法不能被重写
  • 修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。
    在这里插入图片描述在这里插入图片描述在这里插入图片描述

5.java 中的 Math.round(-1.5) 等于多少?

Math.round(-1.5)的返回值是-1。四舍五入的原理是在参数上加0.5然后做向下取整。
我们可以通过大量实验看下结果
在这里插入图片描述

6.String 属于基础的数据类型吗?

  • 不属于。
  • Java 中 8 种基础的数据类型:byte、short、char、int、long、float、double、boolean
    但是 String 类型却是最常用到的引用类型。

7.java 中操作字符串都有哪些类?它们之间有什么区别?

答:String、StringBuffer、StringBuilder

区别:

  • String : final修饰,String类的方法都是返回new String。即对String对象的任何改变都不影响到原对象,对字符串的修改操作都会生成新的对象。
  • StringBuffer : 对字符串的操作的方法都加了synchronized保证线程安全
  • StringBuilder : 不保证线程安全,在方法体内需要进行字符串的修改操作,可以new StringBuilder对象,调用StringBuilder对象的append、replace、delete等方法修改字符串
  • StringBuffer的安全性能高,适合多线程使用;Stringbuider性能更低适合单线程操作

8.String str="i"与 String str=new String(“i”)一样吗?

  • 不一样,因为内存的分配方式不一样。String str="i"的方式,Java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中
    在这里插入图片描述

9.如何将字符串反转?

  • 使用 Java 的反转函数 reverse() 将字符串反转
    在这里插入图片描述

10.String 类的常用方法都有那些?

  • indexOf():返回指定字符的索引。
  • charAt():返回指定索引处的字符。
  • replace():字符串替换。
  • trim():去除字符串两端空白。
  • split():分割字符串,返回一个分割后的字符串数组。
  • getBytes():返回字符串的 byte 类型数组。
  • length():返回字符串长度。
  • toLowerCase():将字符串转成小写字母。
  • toUpperCase():将字符串转成大写字符。
  • substring():截取字符串。
  • equals():字符串比较。

11.抽象类必须要有抽象方法吗?

答案是:不必须
在这里插入图片描述
这道题考察的是抽象类的知识:

抽象类必须有关键字abstract来修饰。
抽象类可以不含有抽象方法
如果一个类包含抽象方法,则该类必须是抽象类

12.普通类和抽象类有哪些区别?

  • 抽象类不能被实例化,即不能使用new关键字来实例化对象
  • 抽象类可以有抽象方法,抽象方法只需申明,无需实现
  • 含有抽象方法的类必须申明为抽象类
  • 抽象的子类必须实现抽象类中所有抽象方法,否则这个子类也是抽象类
  • 抽象方法不能被声明为静态
  • 抽象方法不能用private修饰
  • 抽象方法不能用final修饰

13.抽象类能使用 final 修饰吗?

  • 不能,抽象类是被用于继承的final修饰代表不可修改、不可继承的

14.接口和抽象类有什么区别?

  • 抽象类要被子类继承,接口要被类实现
    在这里插入图片描述
  • 接口只能做方法声明,抽象类中可以作方法声明,也可以做方法实现
    在这里插入图片描述
  • 接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量
    在这里插入图片描述
  • 接口是设计的结果,抽象类是重构的结果
    在这里插入图片描述
  • 抽象类和接口都是用来抽象具体对象的,但是接口的抽象级别最高
    在这里插入图片描述
  • 抽象类可以有具体的方法和属性,接口只能有抽象方法和不可变常量
    在这里插入图片描述
  • 抽象类主要用来抽象类别,接口主要用来抽象功能
    在这里插入图片描述

15.java 中 IO 流分为几种?

这个题要从流的角度去划分:

按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的角色划分为节点流和处理流。

在这里插入图片描述
所有流的基类

InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

16.BIO、NIO、AIO 有什么区别?

什么是IO

  • Java中I/O是以流为基础进行数据的输入输出的,所有数据被串行化(所谓串行化就是数据要按顺序进行输入输出)写入输出流。简单来说就是java通过io流方式和外部设备进行交互
  • 在Java类库中,IO部分的内容是很庞大的,因为它涉及的领域很广泛:标准输入输出,文件的操作,网络上的数据传输流,字符串流,对象流等等等。
  • 比如程序从服务器上下载图片,就是通过流的方式从网络上以流的方式到程序中,在到硬盘中

在这里插入图片描述
什么是BIO

  • BIO:同步并阻塞,服务器实现一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,没处理完之前此线程不能做其他操作(如果是单线程的情况下,我传输的文件很大呢?),当然可以通过线程池机制改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解
    在这里插入图片描述

什么是NIO

  • NIO:同步非阻塞,服务器实现一个连接一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4之后开始支持。
    在这里插入图片描述在这里插入图片描述

什么是AIO

AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用操作系统参与并发操作,编程比较复杂,JDK1.7之后开始支持。.

AIO属于NIO包中的类实现,其实IO主要分为BIO和NIO,AIO只是附加品,解决IO不能异步的实现
在以前很少有Linux系统支持AIO,Windows的IOCP就是该AIO模型。但是现在的服务器一般都是支持AIO操作
在这里插入图片描述 BIO和NIO、AIO的区别

  • BIO是阻塞的,NIO是非阻塞的.
  • BIO是面向流的,只能单向读写,NIO是面向缓冲的, 可以双向读写
  • 使用BIO做Socket连接时,由于单向读写,当没有数据时,会挂起当前线程,阻塞等待,为防止影响其它连接,,需要为每个连接新建线程处理.,然而系统资源是有限的,,不能过多的新建线程,线程过多带来线程上下文的切换,从来带来更大的性能损耗,因此需要使用NIO进行BIO多路复用,使用一个线程来监听所有Socket连接,使用本线程或者其他线程处理连接
  • AIO是非阻塞 以异步方式发起 I/O 操作。当 I/O 操作进行时可以去做其他操作,由操作系统内核空间提醒IO操作已完成

17.Files的常用方法都有哪些?

  • Files.exists() 检测文件路径是否存在
  • Files.createFile()创建文件
  • Files.createDirectory()创建文件夹
  • Files.delete() 删除文件或者目录
  • Files.copy() 复制文件
  • Files.move() 移动文件
  • Files.size()查看文件个数
  • Files.read() 读取文件
  • Files.write()写入文件

二、容器

18.java 容器都有哪些?

Java容器:

数组,String,java.util下的集合容器

数组长度限制为 Integer.Integer.MAX_VALUE;

String的长度限制: 底层是char 数组 长度 Integer.MAX_VALUE 线程安全的
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

  • List:存放有序,列表存储,元素可重复
  • Set:无序,元素不可重复
  • Map:无序,元素可重复

19.Collection 和 Collections 有什么区别?

1、java.util.Collection 是一个集合接口它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。

List,Set,Queue接口都继承Collection
直接实现该接口的类只有AbstractCollection类,该类也只是一个抽象类,提供了对集合类操作的一些基本实现。List和Set的具体实现类基本上都直接或间接的继承了该类。

2、java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态方法(对集合的搜索、排序、线程安全化等),大多数方法都是用来处理线性表的。此类不能实例化,就像一个工具类,服务于Java的Collection框架。

20.List、Set、Map 之间的区别是什么?

List(对付顺序的好帮手)List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象
Set(注重独一无二的性质)不允许重复的集合。不会有多个元素引用相同的对象。
Map(用Key来搜索的专家)使用键值对存储。Map会维护与Key有关联的值。两个Key可以引用相同的对象,但Key不能重复,典型的Key是String类型,但也可以是任何对象。

21.HashMap 和 Hashtable 有什么区别?

HashMap可以接受null键值和值,而Hashtable则不能。

Hashtable是线程安全的,通过synchronized实现线程同步。而HashMap是非线程安全的,但是速度比Hashtable快。

这两个类有许多不同的地方,下面列出了一部分:

  • Hashtable 是 JDK 1 遗留下来的类,而 HashMap 是后来增加的。
  • Hashtable 是同步的,比较慢,但 HashMap 没有同步策略,所以会更快。
  • Hashtable 不允许有个空的 key,但是 HashMap 允许出现一个 null key。

22.如何决定使用 HashMap 还是 TreeMap?

23.说一下 HashMap 的实现原理?

  • HashMap 基于 Hash 算法实现,通过 put(key,value) 存储get(key) 来获取 value
  • 当传入 key 时,HashMap 会根据 key,调用 hash(Object key) 方法,计算出 hash 值,根据 hash值将 value 保存在 Node 对象里,Node 对象保存在数组里
  • 当计算出的 hash 值相同时,称之为 hash 冲突,HashMap 的做法是用链表和红黑树存储相同 hash 值的 value
  • 当 hash 冲突的个数:小于等于 8 使用链表;大于 8 时,使用红黑树解决链表查询慢的问题
    在这里插入图片描述

24.说一下 HashSet 的实现原理?

我们先看一下HashSet和TreeSet在整个集合框架中的位置。他们都实现了Set接口。他们之间的区别是HashSet不能保证元素的顺序,TreeSet中的元素可以按照某个顺序排列他们的元素都不能重复
在这里插入图片描述
先来看一下HashSet:

public static void main(String[] args) {
Set<String> set = new HashSet<String>();
set.add("张三");
set.add("李四");
set.add("王五");
System.out.println(set);

System.out.println(set.size());

System.out.println(set.contains("张三"));
}

打印输出的顺序是是: [李四, 张三, 王五]

可以看出和存进去的顺序不一致。

我们先看一下 Set set = new HashSet();

这行代码创建了一个HashSet,构造函数如下:

public HashSet() {
map = new HashMap<>();
}

可以看到实际上是创建了一个HashMap的对象。没错,HashSet底层就是一个HashMap.
在这里插入图片描述再来看一下这行代码:set.add(“张三”);

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

非常的简单,就是调用了一下HashMap的put方法对元素进行插入

这里的PERSENT是什么呢?继续顺藤摸瓜:

private static final Object PRESENT = new Object();
原来就是一个普通的Object对象前面用static final修饰说明是不可变的
在这里插入图片描述
继续添加:set.add(“李四”);
在这里插入图片描述
可以看出来HashMap的key分别为”张三”,”李四”,“王五”,
因为HashSet用不到value,他们的value都是一样的指向同一个地方。
继续往下看:System.out.println(set.size());

public int size() {
return map.size();
}

也是调用的HashMap的size方法。

System.out.println(set.contains(“张三”));

public boolean contains(Object o) {
return map.containsKey(o);
}

同样调用的HashMap的contains方法。

25.ArrayList 和 LinkedList 的区别是什么?

  • ArrayList和LinkedList可想从名字分析,它们一个是Array(动态数组)的数据结构,一个是Link(链表)的数据结构,此外,它们两个都是对List接口的实现。
    前者是数组队列,相当于动态数组;后者为双向链表结构,也可当作堆栈、队列、双端队列
  • 当随机访问List时(get和set操作),ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
  • 当对数据进行增加和删除的操作时(add和remove操作),LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。
  • 从利用效率来看,ArrayList自由性较低,因为它需要手动的设置固定大小的容量,但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;而LinkedList自由性较高,能够动态的随数据量的变化而变化,但是它不便于使用。
  • ArrayList主要控件开销在于需要在lList列表预留一定空间;而LinkList主要控件开销在于需要存储结点信息以及结点指针信息

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

数组转 List ,使用 JDK 中 java.util.Arrays 工具类的 asList 方法

public static void testArray2List() {
    String[] strs = new String[] {"aaa", "bbb", "ccc"};
    List<String> list = Arrays.asList(strs);
    for (String s : list) {
        System.out.println(s);
    }
}

List 转数组,使用 List 的toArray方法无参toArray方法返回Object数组传入初始化长度的数组对象,返回该对象数组

public static void testList2Array() {
    List<String> list = Arrays.asList("aaa", "bbb", "ccc");
    String[] array = list.toArray(new String[list.size()]);
    for (String s : array) {
        System.out.println(s);
    }
}

27.ArrayList 和 Vector 的区别是什么?

首先我们给出标准答案:

  • Vector是线程安全的,ArrayList不是线程安全的。
  • ArrayList在底层数组不够用时在原来的基础上扩展0.5倍,Vector是扩展1倍。
    看上图Vector和ArrayList一样,都继承自List,来看一下Vector的源码
    在这里插入图片描述
    实现了List接口,底层和ArrayList一样,都是数组来实现的。分别看一下这两个类的add方法,首先来看ArrayList的add源码
    在这里插入图片描述
    再看Vector的add源码
    在这里插入图片描述
    方法实现都一样,就是加了一个synchronized的关键字,再来看看其它方法,先看ArrayList的remove方法
    在这里插入图片描述
    再看Vector的remove方法
    在这里插入图片描述
    方法实现上也一样,就是多了一个synchronized关键字,再看看ArrayList的get方法
    在这里插入图片描述
    Vector的get方法
    在这里插入图片描述
    再看看Vector的其它方法
    在这里插入图片描述
    无一例外,只要是关键性的操作,方法前面都加了synchronized关键字来保证线程的安全性。当执行synchronized修饰的方法前,系统会对该方法加一把锁,方法执行完成后释放锁,加锁和释放锁的这个过程,在系统中是有开销的,因此,在单线程的环境中,Vector效率要差很多。(多线程环境不允许用ArrayList,需要做处理)。

28.Array 和 ArrayList 有何区别?

在这里插入图片描述
定义一个 Array 时,必须指定数组的数据类型及数组长度,即数组中存放的元素个数固定并且类型相同。
ArrayList 是动态数组,长度动态可变,会自动扩容。不使用泛型的时候,可以添加不同类型元素。
在这里插入图片描述

29.在 Queue 中 poll()和 remove()有什么区别?

队列是一个典型的先进先出(FIFO)的容器。即从容器的一端放入事物,从另一端取出,并且事物放入容器的顺序与取出的顺序是相同的。
队列的两种实现方式:
在这里插入图片描述
1、offer()和add()的区别

  • add()和offer()都是向队列中添加一个元素。但是如果想在一个满的队列中加入一个新元素,调用 add() 方法就会抛出一个unchecked 异常,而调用 offer() 方法会返回 false。可以据此在程序中进行有效的判断!

2、peek()和element()的区别

  • peek()和element()都将在不移除的情况下返回队头,但是peek()方法在队列为空时返回null,调用element()方法会抛出NoSuchElementException异常

3、poll()和remove()的区别

  • poll()和remove()都将移除并且返回对头,但是在poll()在队列为空时返回null,而remove()会抛出NoSuchElementException异常

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

  • Vector:就比Arraylist多了个同步化机制(线程安全)。
  • Hashtable:就比Hashmap多了个线程安全。
  • ConcurrentHashMap:是一种高效但是线程安全的集合。
  • Stack:栈,也是线程安全的,继承于Vector。

31.迭代器 Iterator 是什么?

在这里插入图片描述

32.Iterator 怎么使用?有什么特点?

java.lang.Iterable 接口被 java.util.Collection 接口继承,java.util.Collection 接口的 iterator() 方法返回一个 Iterator 对象
next() 方法获得集合中的下一个元素
hasNext() 检查集合中是否还有元素
remove() 方法将迭代器新返回的元素删除
forEachRemaining(Consumer<? super E> action) 方法,遍历所有元素

在这里插入图片描述
Iterator 的使用示例

public class TestIterator {
    
    static List<String> list = new ArrayList<String>();
    static {
        list.add("111");
        list.add("222");
        list.add("333");
    }
    public static void main(String[] args) {
        testIteratorNext();
        System.out.println();
        testForEachRemaining();
        System.out.println();
        testIteratorRemove();
    }
    //使用 hasNext 和 next遍历 
    public static void testIteratorNext() {
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String str = iterator.next();
            System.out.println(str);
        }
    }
    //使用 Iterator 删除元素 
    public static void testIteratorRemove() {
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            String str = iterator.next();
            if ("222".equals(str)) {
                iterator.remove();
            }
        }
        System.out.println(list);
    }
    //使用 forEachRemaining 遍历
    public static void testForEachRemaining() {
        final Iterator<String> iterator = list.iterator();
        iterator.forEachRemaining(new Consumer<String>() {
             public void accept(String t) {
                System.out.println(t);
            }
        });
    }
}

33.Iterator 和 ListIterator 有什么区别?

ListIterator 继承 Iterator
ListIterator 比 Iterator多方法

1) add(E e) 将指定的元素插入列表,插入位置为迭代器当前位置之前
2) set(E e) 迭代器返回的最后一个元素替换参数e
3) hasPrevious() 迭代器当前位置,反向遍历集合是否含有元素
4) previous() 迭代器当前位置,反向遍历集合,下一个元素
5) previousIndex() 迭代器当前位置,反向遍历集合,返回下一个元素的下标
6) nextIndex() 迭代器当前位置,返回下一个元素的下标

使用范围不同,Iterator可以迭代所有集合;ListIterator 只能用于List及其子类
ListIterator 有 add 方法,可以向 List 中添加对象;Iterator 不能
ListIterator 有 hasPrevious() 和 previous() 方法,可以实现逆向遍历;Iterator不可以
ListIterator 有 nextIndex() 和previousIndex() 方法,可定位当前索引的位置;Iterator不可以
ListIterator 有 set()方法,可以实现对 List 的修改;Iterator 仅能遍历,不能修改

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

可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java.lang.UnsupportedOperationException 异常。
在这里插入图片描述
同理:Collections包也提供了对list和set集合的方法。

Collections.unmodifiableList(List)

Collections.unmodifiableSet(Set)

拓展:

我们很容易想到用final关键字进行修饰,我们都知道
final关键字可以修饰类,方法,成员变量,final修饰的类不能被继承,final修饰的方法不能被重写,
final修饰的成员变量必须初始化值,如果这个成员变量是基本数据类型,表示这个变量的值是不可改变的,
如果说这个成员变量是引用类型,则表示这个引用的地址值是不能改变的,
但是这个引用所指向的对象里面的内容还是可以改变的。

三、多线程

35.并行和并发有什么区别?
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。

并发的多个任务之间是互相抢占资源的。
并行的多个任务之间是不互相抢占资源的、

只有在多CPU的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
在这里插入图片描述就像上面这张图,只有一个咖啡机的时候,一台咖啡机其实是在并发被使用的。而有多个咖啡机的时候,多个咖啡机之间才是并行被使用的。

36.线程和进程的区别?

  • 一个进程必定属于也只能属于一个进程,而一个进程可以拥有多个线程,并且至少拥有一个线程
  • 属于一个进程的所有线程共享该线程的所有资源
  • 线程被称为轻量级进程,线程间切换代价小,进程间切换代价大
  • 进程是程序的一次执行,线程可以理解为程序中的一个程序片段的执行
  • 每个进程都有独立的内存空间,而线程共享其所属进程的内存空间

总之,一个程序至少有一个进程,一个进程至少有一个线程
在这里插入图片描述

37.守护线程是什么?

守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程

   专门用于服务其他的线程,如果其他的线程(即用户自定义线程)都执行完毕,连main线程也执行完毕,
   那么jvm就会退出(即停止运行),此时连jvm都停止运行了,守护线程当然也就停止执行了。

   换一种通俗的说法,如果有用户自定义线程存在的话,jvm就不会退出,
   此时守护线程也不能退出,也就是它还要运行,为什么呢,就是为了执行垃圾回收的任务。

38.创建线程有哪几种方式?

1.继承Thread类型重写run 方法
在这里插入图片描述
2.实现Runnable接口
在这里插入图片描述
3.实现Callable接口
在这里插入图片描述

39.说一下 runnable 和 callable 有什么区别?

主要区别

  • Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,支持泛型
  • Runnable 接口 run 方法只能抛出运行时异常且无法捕获处理;Callable 接口 call
    方法允许抛出异常,可以获取异常信息

    在这里插入图片描述在这里插入图片描述

40.线程有哪些状态?

Java中线程的状态分为6种

  • 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  • 运行(RUNNABLE):Java线程中将就绪(ready)和**运行中(running)**两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  • 阻塞(BLOCKED):表示线程阻塞于锁。
  • 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  • 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  • 终止(TERMINATED):表示该线程已经执行完毕。

线程的状态图
在这里插入图片描述1. 初始状态

  • 实现Runnable接口继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

2.1. 就绪状态

  • 就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态
  • 调用线程的start()方法,此线程进入就绪状态
  • 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
  • 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态
  • 锁池里的线程拿到对象锁后,进入就绪状态

2.2. 运行中状态

  • 线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

3. 阻塞状态

  • 阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

4. 等待

  • 处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

5. 超时等待

  • 处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒

6. 终止状态

  • 线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生
  • 在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

41.sleep() 和 wait() 有什么区别?

sleep()方法:

功能

  • sleep ( )方法是Thread类的方法,线程通过调用该方法,进入休眠状态主动让出CPU,从而CPU可以执行其他的线程。经过sleep指定的时间后,CPU回到这个线程上继续往下执行
  • 如果当前线程进入了同步锁,sleep()方法并不会释放锁。即使当前线程使用sleep方法让出了cpu,但其他被同步锁挡住了的线程也无法得到执行

使用场合

  • 线程的调度执行是按照其优先级的高低顺序进行的,当高级别的线程未死亡时,低级别的线程没有机会获得CPU资源。有时优先级高的线程需要优先级低的线程完成一些辅助工作或者优先级高的线程需要完成一些比较费时的工作,此时优先级高的线程应该让出CPU资源,使得优先级低的线程有机会执行。为了达到这个目的,优先级高的线程可以在自己的run()方法中调用sleep方法来使自己放弃CPU资源,休眠一段时间。
    在这里插入图片描述
    wait()方法
    功能:
  • wait()方法可以中断线程的运行,使本线程等待,暂时让出CPU的使用权,并允许其他线程使用这个同步方法。其他线程如果在使用这个同步方法时不需要等待,那么它使用完这个方法的同时,应该用notifyAll()方法通知所有由于使用了这个同步方法而处于等待的线程结束等待,曾中断的线程就会从刚才中断处继续执行这个同步方法(并不是立马执行,而是结束等待),并遵循“先中断先继续”的原则。
  • wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。如果notify方法后面的代码还有很多,需要这些代码执行完后才会释放锁,可以在notfiy方法后增加一个等待和一些代码,看看效果)

使用场合:

  • 当一个线程使用的同步方法中用到某个变量,而此变量又需要启动线程修改后才能符合本线程的需要,那么可以在同步方法中使用wait()方法。
    在这里插入图片描述 代码示例
package thread;
 
public class MultiThread {
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Thread1()).start();
        //主动让出CPU,让CPU去执行其他的线程。在sleep指定的时间后,CPU回到这个线程上继续往下执行
        Thread.sleep(5000);
        new Thread(new Thread2()).start();
 
    }
}
  class Thread1 implements Runnable{
      @Override
      public void run() {
          synchronized (MultiThread.class){
              System.out.println("进入线程1");
              try{
                  System.out.println("线程1正在等待");
                  Thread.sleep(5000);
				//MultiThread.class.wait();
				//wait是指一个已经进入同步锁的线程内(此处指Thread1),让自己暂时让出同步锁,
				//以便其他在等待此锁的线程(此处指Thread2)可以得到同步锁并运行。
              }catch(Exception e){
                  System.out.println(e.getMessage());
                  e.printStackTrace();
              }
              System.out.println("线程1结束等待,继续执行");
              System.out.println("线程1执行结束");
          }
      }
  }
  class Thread2 implements Runnable{
      @Override
      public void run() {
          synchronized (MultiThread.class){
              System.out.println("进入线程2");
              System.out.println("线程2唤醒其他线程");
              //Thread2调用了notify()方法,但该方法不会释放对象锁,只是告诉调用wait方法的线程可以去
              //参与获得锁的竞争了。但不会马上得到锁,因为锁还在别人手里,别人还没有释放。
              //如果notify()后面的代码还有很多,需要执行完这些代码才会释放锁。
              MultiThread.class.notify();
              try {
                  Thread.sleep(5000);
              }
              catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("线程2继续执行");
              System.out.println("线程2执行结束");
          }
      }
  }

42.notify()和 notifyAll()有什么区别?

锁池:

  • 假设线程A已经拥有对象锁,线程B、C想要获取锁就会被阻塞,进入一个地方去等待锁的等待,这个地方就是该对象的锁池;

等待池:

  • 假设线程A调用某个对象的wait方法,线程A就会释放该对象锁,同时线程A进入该对象的等待池中,进入等待池中的线程不会去竞争该对象的锁。

notify和notifyAll的区别:

  1. notify只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会;
  2. notifyAll会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会;

43.线程的 run()和 start()有什么区别?

run()方法:

  • 是在主线程中执行方法,和调用普通方法一样;(按顺序执行同步执行

start()方法:

  • 创建了新的线程,在新的线程中执行;(异步执行
    在这里插入图片描述

问题

面试官:请问启动线程是start()还是run()方法,能谈谈吗?

应聘者:start()方法

当用start()开始一个线程后,线程就进入就绪状态,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。但是这并不意味着线程就会立即运行。只有当cpu分配时间片时,这个线程获得时间片时,才开始执行run()方法。start()是方法,它调用run()方法.而run()方法是你必须重写的. run()方法中包含的是线程的主体(真正的逻辑)。

在这里插入图片描述在这里插入图片描述在这里插入图片描述

44.创建线程池有哪几种方式?

6种

1. newFixedThreadPool

  • 定长线程池,每当提交一个任务创建一个线程,直到达到线程池的最大数量,这时线程数量不再变化,当线程发生错误结束时,线程池会补充一个新的线程
public class TestThreadPool {
      //定长线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,
      //这时线程数量不再变化,当线程发生错误结束时,线程池会补充一个新的线程
	static ExecutorService fixedExecutor = Executors.newFixedThreadPool(3);
	public static void main(String[] args) {
		testFixedExecutor();
	}
	//测试定长线程池,线程池的容量为3,提交6个任务,根据打印结果可以看出先执行前3个任务,
	//3个任务结束后再执行后面的任务
	private static void testFixedExecutor() {
		for (int i = 0; i < 6; i++) {
			final int index = i;
			fixedExecutor.execute(new Runnable() {
				public void run() {
					try {
						Thread.sleep(3000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + " index:" + index);
				}
			});
		}
		try {
			Thread.sleep(4000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("4秒后...");
		fixedExecutor.shutdown();
	}
}

在这里插入图片描述
2. newCachedThreadPool

  • 可缓存的线程池,如果线程池的容量超过了任务数自动回收空闲线程,任务增加时可以自动添加新线程,线程池的容量不限制

测试代码:

public class TestThreadPool {
    //可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程,
    //任务增加时可以自动添加新线程,线程池的容量不限制
	static ExecutorService cachedExecutor = Executors.newCachedThreadPool();
	public static void main(String[] args) {
		testCachedExecutor();
	}
	//测试可缓存线程池
	private static void testCachedExecutor() {
		for (int i = 0; i < 6; i++) {
			final int index = i;
			cachedExecutor.execute(new Runnable() {
				public void run() {
					try {
						Thread.sleep(3000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + " index:" + index);
				}
			});
		}
		try {
			Thread.sleep(4000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("4秒后...");
		cachedExecutor.shutdown();
	}
}

在这里插入图片描述
3. newScheduledThreadPool

  • 定长线程池,可执行周期性的任务

测试代码:

public class TestThreadPool {
    //定长线程池,可执行周期性的任务
	static ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(3);
	public static void main(String[] args) {
		testScheduledExecutor();
	}
   //测试定长、可周期执行的线程池
	private static void testScheduledExecutor() {
		for (int i = 0; i < 3; i++) {
			final int index = i;
			//scheduleWithFixedDelay 固定的延迟时间执行任务; scheduleAtFixedRate 固定的频率执行任务
			scheduledExecutor.scheduleWithFixedDelay(new Runnable() {
				public void run() {
					System.out.println(Thread.currentThread().getName() + " index:" + index);
				}
			}, 0, 3, TimeUnit.SECONDS);
		}
		try {
			Thread.sleep(4000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("4秒后...");
		scheduledExecutor.shutdown();
	}
}

在这里插入图片描述
4. newSingleThreadExecutor

  • 单线程的线程池,线程异常结束,会创建一个新的线程,能确保任务按提交顺序执行

测试代码:

public class TestThreadPool {
	
	//单线程的线程池,线程异常结束,会创建一个新的线程,能确保任务按提交顺序执行
	static ExecutorService singleExecutor = Executors.newSingleThreadExecutor();
	
	
	public static void main(String[] args) {
		testSingleExecutor();
	}
	
	//测试单线程的线程池
	private static void testSingleExecutor() {
		for (int i = 0; i < 3; i++) {
			final int index = i;
			singleExecutor.execute(new Runnable() {
				public void run() {
					try {
						Thread.sleep(3000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + " index:" + index);
				}
			});
		}
		
		try {
			Thread.sleep(4000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("4秒后...");
		
		singleExecutor.shutdown();
	}
	
}

在这里插入图片描述
5. newSingleThreadScheduledExecutor

  • 单线程可执行周期性任务的线程池

测试代码:

public class TestThreadPool {
	
	//单线程可执行周期性任务的线程池
	static ScheduledExecutorService singleScheduledExecutor =Executors.newSingleThreadScheduledExecutor();
	
	public static void main(String[] args) {
		testSingleScheduledExecutor();
	}
	
	//测试单线程可周期执行的线程池
	private static void testSingleScheduledExecutor() {
		for (int i = 0; i < 3; i++) {
			final int index = i;
			//scheduleWithFixedDelay 固定的延迟时间执行任务; scheduleAtFixedRate 固定的频率执行任务
			singleScheduledExecutor.scheduleAtFixedRate(new Runnable() {
				public void run() {
					System.out.println(Thread.currentThread().getName() + " index:" + index);
				}
			}, 0, 3, TimeUnit.SECONDS);
		}
		
		try {
			Thread.sleep(4000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("4秒后...");
		
		singleScheduledExecutor.shutdown();
	}	
}

在这里插入图片描述
6. newWorkStealingPool

  • 任务窃取线程池不保证执行顺序,适合任务耗时差异较大
  • 线程池中有多个线程队列,有的线程队列中有大量的比较耗时的任务堆积,而有的线程队列却是空的,就存在有的线程处于饥饿状态,当一个线程处于饥饿状态时,它就会去其它的线程队列中窃取任务。解决饥饿导致的效率问题
    默认创建的并行 level 是 CPU 的核数。主线程结束,即使线程池有任务也会立即停止。

测试代码:

public class TestThreadPool {
 
	//任务窃取线程池
	static ExecutorService workStealingExecutor = Executors.newWorkStealingPool();
	
	public static void main(String[] args) {
		testWorkStealingExecutor();
	}
	
	//测试任务窃取线程池
	private static void testWorkStealingExecutor() {
		for (int i = 0; i < 10; i++) {//本机 CPU 8核,这里创建10个任务进行测试
			final int index = i;
			workStealingExecutor.execute(new Runnable() {
				public void run() {
					try {
						Thread.sleep(3000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + " index:" + index);
				}
			});
		}
		try {
			Thread.sleep(4000);//这里主线程不休眠,不会有打印输出
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("4秒后...");	
//		workStealingExecutor.shutdown();
	}
}

在这里插入图片描述

45.线程池都有哪些状态?

  • 线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated
  • 线程池各个状态切换框架图:
    在这里插入图片描述

1、RUNNING

(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
(2) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0
在这里插入图片描述
2、 SHUTDOWN

(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务但能处理已添加的任务
(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN

3、STOP

(1) 状态说明:线程池处在STOP状态时,不接收新任务不处理已添加的任务,并且会中断正在处理的任务
(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP

4、TIDYING

(1) 状态说明:当所有的任务已终止,ctl记录的任务数量为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()terminated()ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING

5、 TERMINATED

(1) 状态说明:线程池彻底终止,就变成TERMINATED状态
(2) 状态切换:线程池处在TIDYING状态时,执行完**terminated()**之后,就会由 TIDYING -> TERMINATED

46.线程池中 submit()和 execute()方法有什么区别?

区别:

  • submit(Callable task)、submit(Runnable task, T result)、submit(Runnable task)
    归属于ExecutorService接口

  • execute(Runnable command)归属于Executor接口ExecutorService继承了Executor

  • submit()有返回值

  • execute没有返回值

public class ThreadPoolTest {
    private String taskName;
 
    public ThreadPoolTest(String taskName) {
        this.taskName = taskName;
    }
 
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("execute任务执行中");
            }
        });
        System.out.println("----分界线----");
        Future<String> future = executorService.submit(() -> {
            System.out.println("submit任务执行中");
            return "submit任务完成,这是执行结果";
        });
        try {
            //如果future.get()返回null,任务完成
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
            System.out.println("任务失败原因:" + e.getCause().getMessage());
        }
        executorService.shutdown();
    }
}
 
//输出:
----分界线----
execute任务执行中
submit任务执行中
submit任务完成,这是执行结果
  • submit()方便做异常处理。通过Future.get()可捕获异常。
public class ThreadPoolTest implements Runnable {
    private String taskName;
 
    public ThreadPoolTest(String taskName) {
        this.taskName = taskName;
    }
 
    @Override
    public void run() {
        throw new RuntimeException("此处" + this.taskName + "抛出异常。");
    }
 
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(new ThreadPoolTest("task1"));
        System.out.println("----分界线----");
        Future<?> future = executorService.submit(new ThreadPoolTest("task2"));
        try {
            future.get();//如果future.get()返回null,任务完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
            System.out.println("任务失败原因:" + e.getCause().getMessage());
        }
        executorService.shutdown();
    }
}

在这里插入图片描述

47.在 java 程序中怎么保证多线程的运行安全?

  1. 使用synchronied关键字,可以用于代码块,方法(静态方法,同步锁是当前字节码对象;实例方法,同步锁是实例对象)
  2. 使用volatile关键字,防止指令重排,被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量
  3. lock锁机制
    在这里插入图片描述
  4. 使用线程安全的类,比如Vector、HashTable、StringBuffer

48.多线程锁的升级原理是什么?

锁的级别从低到高:

  • 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

锁分级别原因:

  • 没有优化以前,synchronized是重量级锁悲观锁),使用 wait 和 notify、notifyAll
    来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 synchronized关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。

无锁

  • 没有对资源进行锁定所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功

偏向锁

  • 对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放
  • 偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁
  • 如果线程处于活动状态,升级为轻量级锁的状态。

轻量级锁

  • 轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B会通过自旋的形式尝试获取锁线程不会阻塞,从而提高性能
  • 当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时轻量级锁也会升级为重量级锁

重量级锁:

  • 指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态
  • 重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
    在这里插入图片描述

49.什么是死锁?

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:
在这里插入图片描述

50.怎么防止死锁?

产生死锁的四个必要条件(互请不循):

(1) 互斥条件:一个资源每次只能被一个进程使用。

(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

(4) 循环等待条件:若干进程之间形成一种头尾相接的【循环等待资源】关系。

在这里插入图片描述

51.ThreadLocal 是什么?有哪些使用场景?

ThreadLocal线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。
ThreadLocal 使用例子:

public class TestThreadLocal {
	
	//线程本地存储变量
	private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return 0;
		}
	};
    public static void main(String[] args) {
		for (int i = 0; i < 3; i++) {//启动三个线程
			Thread t = new Thread() {
				@Override
				public void run() {
					add10ByThreadLocal();
				}
			};
			t.start();
		}
	}
	
	/**
	 * 线程本地存储变量加 5
	 */
	private static void add10ByThreadLocal() {
		for (int i = 0; i < 5; i++) {
			Integer n = THREAD_LOCAL_NUM.get();
			n += 1;
			THREAD_LOCAL_NUM.set(n);
			System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
		}
	}
	
}

打印结果:启动了 3 个线程,每个线程最后都打印到 “ThreadLocal num=5”,而不是 num 一直在累加直到值等于 15
在这里插入图片描述
实现原理:

按照我们第一直觉,感觉 ThreadLocal 内部肯定是有个 Map 结构,key 存了 Thread,value 存了 本地变量 V 的值。每次通过 ThreadLocal 对象的 get() 和 set(T value) 方法获取当前线程里存的本地变量、设置当前线程里的本地变量

在这里插入图片描述
而 JDK 的实现里面这个 Map 是属于 Thread,而非属于 ThreadLocal。ThreadLocal 仅是一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在 Thread 里面。ThreadLocalMap 属于 Thread 也更加合理。

还有一个更加深层次的原因,这样设计不容易产生内存泄露。
ThreadLocal 持有的 Map 会持有 Thread 对象的引用,只要 ThreadLocal 对象存在,那么 Map 中的 Thread 对象就永远不会被回收。ThreadLocal 的生命周期往往比线程要长,所以这种设计方案很容易导致内存泄露。

JDK 的实现中 Thread 持有 ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。JDK 的这种实现方案复杂但更安全。
在这里插入图片描述

52.说一下 synchronized 底层实现原理?

在这里插入图片描述synchronizedjvm实现的一种互斥同步访问方式,底层是基于每个对象的监视器(monitor)来实现的被synchronized修饰的代码,在被编译器编译后在被修饰的代码前后加上了一组字节指令。

代码开始加入了monitorenter,在代码后面加入了monitorexit,这两个字节码指令配合完成了synchronized关键字修饰代码的互斥访问。

在虚拟机执行到monitorenter指令的时候,会请求获取对象的monitor锁,基于monitor锁又衍生出一个锁计数器的概念。
在这里插入图片描述Java并发锁

当执行monitorenter时,若对象未被锁定时,或者当前线程已经拥有了此对象的monitor锁,则锁计数器+1,该线程获取该对象锁

当执行monitorexit时,锁计数器-1当计数器为0时,此对象锁就被释放了。那么其他阻塞的线程则可以请求获取该monitor锁

53.synchronized 和 volatile 的区别是什么?

作用:

  • synchronized 表示只有一个线程可以获取作用对象的锁执行代码阻塞其他线程
  • volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序。

区别:

  • synchronized 可以作用于变量、方法、对象;volatile 只能作用于变量
  • synchronized 可以保证线程间的有序性(猜测是无法保证线程内的有序性,
    即线程内的代码可能被 CPU指令重排序)、原子性和可见性;
    volatile 只保证了可见性和有序性无法保证原子性
  • synchronized 线程阻塞,volatile 线程不阻塞

54.synchronized 和 Lock 有什么区别?

在这里插入图片描述

package com.cn.test.thread.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
    private Lock lock = new ReentrantLock();
    /*
     * 使用完毕释放后其他线程才能获取锁
     */
    public void lockTest(Thread thread) {
        lock.lock();//获取锁
        try {
            System.out.println("线程"+thread.getName() + "获取当前锁"); //打印当前锁的名称
            Thread.sleep(2000);//为看出执行效果,是线程此处休眠2秒
        } catch (Exception e) {
            System.out.println("线程"+thread.getName() + "发生了异常释放锁");
        }finally {
            System.out.println("线程"+thread.getName() + "执行完毕释放锁");
            lock.unlock(); //释放锁
        }
    }
    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        //声明一个线程 “线程一”
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                lockTest.lockTest(Thread.currentThread());
            }
        }, "thread1");
        //声明一个线程 “线程二”
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                lockTest.lockTest(Thread.currentThread());
            }
        }, "thread2");
        // 启动2个线程
        thread2.start();
        thread1.start();
    }
}

执行结果:
在这里插入图片描述

package com.cn.test.thread.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
    private Lock lock = new ReentrantLock();
    
    /*
     * 尝试获取锁 tryLock() 它表示用来尝试获取锁,如果获取成功,则返回true,
     * 如果获取失败(即锁已被其他线程获取),则返回false
     */
    public void tryLockTest(Thread thread) {
        if(lock.tryLock()) { //尝试获取锁
            try {
                System.out.println("线程"+thread.getName() + "获取当前锁"); //打印当前锁的名称
                Thread.sleep(2000);//为看出执行效果,是线程此处休眠2秒
            } catch (Exception e) {
                System.out.println("线程"+thread.getName() + "发生了异常释放锁");
            }finally {
                System.out.println("线程"+thread.getName() + "执行完毕释放锁");
                lock.unlock(); //释放锁
            }
        }else{
            System.out.println("我是线程"+Thread.currentThread().getName()+"当前锁被别人占用,我无法获取");
        }
    }
    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                lockTest.tryLockTest(Thread.currentThread());
            }
        }, "thread1");
        //声明一个线程 “线程二”
        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                lockTest.tryLockTest(Thread.currentThread());
            }
        }, "thread2");
        // 启动2个线程
        thread2.start();
        thread1.start();
    }
}

执行结果:
在这里插入图片描述

package com.cn.test.thread.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
    private Lock lock = new ReentrantLock();
    public void tryLockParamTest(Thread thread) throws InterruptedException {
        if(lock.tryLock(3000, TimeUnit.MILLISECONDS)) { 
        //尝试获取锁 获取不到锁,就等3秒,如果3秒后还是获取不到就返回false  
            try {
                System.out.println("线程"+thread.getName() + "获取当前锁"); //打印当前锁的名称
                Thread.sleep(4000);//为看出执行效果,是线程此处休眠2秒
            } catch (Exception e) {
                System.out.println("线程"+thread.getName() + "发生了异常释放锁");
            }finally {
                System.out.println("线程"+thread.getName() + "执行完毕释放锁");
                lock.unlock(); //释放锁
            }
        }else{
            System.out.println("我是线程"+Thread.currentThread().getName()+"当前锁被别人占用,等待3s后仍无法获取,放弃");
        }
    }
    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lockTest.tryLockParamTest(Thread.currentThread());
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }, "thread1");
        //声明一个线程 “线程二”
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lockTest.tryLockParamTest(Thread.currentThread());
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }, "thread2");
        // 启动2个线程
        thread2.start();
        thread1.start();
    }
}

在这里插入图片描述

55.synchronized 和 ReentrantLock 区别是什么?

1、原始构成:

  • synchronized是关键字,属于JVM层面,底层是由一对monitorenter和monitorexit指令实现的。
  • ReentrantLock是一个具体类,是API层面的锁。

2、使用方法:

  • synchronized不需要用户手动释放锁,当synchronized代码块执行完成后,系统会自动让线程释放对锁的占用
  • ReentrantLock需要用户手动释放锁,若没有手动释放可能导致死锁现象。

3、等待是否可中断:

  • synchronized不可中断,除非抛出异常或者正常运行完成
  • ReentrantLock可中断

4、加锁是否公平:

  • synchronized非公平锁
  • ReentrantLock两者都可以默认是非公平锁

5、锁绑定多个条件Condition:

  • synchronized没有
  • ReentrantLock可用来分组唤醒需要唤醒的线程
    而不是像synchronized要么随机唤醒一个线程要么唤醒所有线程

56.说一下 atomic 的原理?

四、反射

57.什么是反射?

  • JAVA反射机制是在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法
  • 对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制
    在这里插入图片描述截图:
    在这里插入图片描述通过反射调用方法
      通过反射调用方法。详情见代码及注释:
import java.lang.reflect.Method;
public class InvokeTester
{
    public int add(int param1, int param2)
    {
        return param1 + param2;
    }
    public String echo(String message)
    {
        return "Hello: " + message;
    }
    public static void main(String[] args) throws Exception
    {
        // 以前的常规执行手段
        InvokeTester tester = new InvokeTester();
        System.out.println(tester.add(1, 2));
        System.out.println(tester.echo("Tom"));
        System.out.println("---------------------------");
        // 通过反射的方式
        // 第一步,获取Class对象
        // 前面用的方法是:Class.forName()方法获取
        // 这里用第二种方法,类名.class
        Class<?> classType = InvokeTester.class;
        // 生成新的对象:用newInstance()方法
        Object invokeTester = classType.newInstance();
        System.out.println(invokeTester instanceof InvokeTester); // 输出true
        // 通过反射调用方法
        // 首先需要获得与该方法对应的Method对象
        Method addMethod = classType.getMethod("add", new Class[] { int.class,  int.class });

        // 第一个参数是方法名,第二个参数是这个方法所需要的参数的Class对象的数组
        // 调用目标方法
        Object result = addMethod.invoke(invokeTester, new Object[] { 1, 2 });
        System.out.println(result); // 此时result是Integer类型

        //调用第二个方法
        Method echoMethod = classType.getDeclaredMethod("echo", new Class[]{String.class});
        Object result2 = echoMethod.invoke(invokeTester, new Object[]{"Tom"});
        System.out.println(result2);
    }
}

58.什么是 java 序列化?什么情况下需要序列化?

  • 序列化:将 Java 对象转换成字节流的过程。
  • 反序列化:将字节流转换成 Java 对象的过程。
  • Java 对象需要在网络上传输 或者 持久化存储到文件中时,就需要对 Java 对象进行序列化处理
  • 序列化的实现:类实现 Serializable 接口,这个接口没有需要实现的方法。实现 Serializable 接口是为了告诉 jvm这个类的对象可以被序列化

注意事项:

  • 某个类可以被序列化,则其子类也可以被序列化
  • 声明为 static 和 transient成员变量不能被序列化
  • static 成员变量是描述类级别的属性transient表示临时数据
  • 反序列化读取序列化对象的顺序要保持一致

具体使用

package constxiong.interview;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
 
/**
 * 测试序列化,反序列化
 * @author ConstXiong
 * @date 2019-06-17 09:31:22
 */
public class TestSerializable implements Serializable {
 
	private static final long serialVersionUID = 5887391604554532906L;
	
	private int id;
	
	private String name;
 
	public TestSerializable(int id, String name) {
		this.id = id;
		this.name = name;
	}
	
	@Override
	public String toString() {
		return "TestSerializable [id=" + id + ", name=" + name + "]";
	}
 
	@SuppressWarnings("resource")
	public static void main(String[] args) throws IOException, ClassNotFoundException {
		//序列化
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("TestSerializable.obj"));
		oos.writeObject("测试序列化");
		oos.writeObject(618);
		TestSerializable test = new TestSerializable(1, "ConstXiong");
		oos.writeObject(test);
		
		//反序列化
		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("TestSerializable.obj"));
		System.out.println((String)ois.readObject());
		System.out.println((Integer)ois.readObject());
		System.out.println((TestSerializable)ois.readObject());
	} 
}

在这里插入图片描述

59.动态代理是什么?有哪些应用?

答:动态代理:在运行时,创建目标类,可以调用和扩展目标类的方法

应用场景如:

  • 统计每个 api 的请求耗时
  • 统一的日志输出
  • 校验被调用的 api 是否已经登录和权限鉴定
  • Spring的 AOP 功能模块就是采用动态代理的机制来实现切面编程

60.怎么实现动态代理?

  • Java领域中,常用的动态代理实现方式有两种,一种是利用JDK反射机制生成代理
    另外一种是使用CGLIB代理
  • JDK代理必须要提供接口而CGLIB则不需要,可以直接代理类

1、JDK实现动态代理

  • 主要使用了Proxy.newProxyInstance()方法,该方法的官方解释为:返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。

下面分别举例说明。
在这里插入图片描述在这里插入图片描述在这里插入图片描述

2.CGLIB动态代理

  • 需要引入CGLIB相关Jar包
  • 使用JDK的Proxy实现动态代理,要求目标类与代理类实现相同的接口,若目标类不存在接口,则无法使用该方式实现。
  • 对于没有接口的类,要为其创建动态代理,就要使用CGLIB来实现。CGLIB动态代理的生成原理是生成目标类的子类,而子类是增强过的,这个子类对象就是代理对象。使用CGLIB生成代理类,要求目标类必须能被继承,因此不能是final类

在这里插入图片描述在这里插入图片描述

五、对象拷贝

61.为什么要使用克隆?

  • 想对一个对象进行处理,又想保留原有的数据进行接下来的操作,就需要克隆了。克隆分浅克隆和深克隆浅克隆后的对象中非基本对象和原对象指向同一块内存,因此对这些非基本对象的修改会同时更改克隆前后的对象。深克隆可以实现完全的克隆,可以用反射的方式序列化的方式实现。

62.如何实现对象克隆?

有两种方式:

  • 1). 实现Cloneable接口重写Object类中的clone()方法
  • 2). 实现Serializable接口,通过对象的序列化反序列化实现克隆,可以实现真正的深度克隆,代码如下。
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class MyUtil {
 
    private MyUtil() {
        throw new AssertionError();
    }
     public static <T> T clone(T obj) throws Exception {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bout);
        oos.writeObject(obj);
        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bin);
        return (T) ois.readObject();
        // 说明:调用ByteArrayInputStream或ByteArrayOutputStream对象的close方法没有任何意义
        // 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,
        //这一点不同于对外部资源(如文件流)的释放
    }
}

下面是测试代码:


import java.io.Serializable;
 
/**
 * 人类
 * @author 
 *
 */
class Person implements Serializable {
    private static final long serialVersionUID = -9102017020286042305L;
    private String name;    // 姓名
    private int age;        // 年龄
    private Car car;        // 座驾
    public Person(String name, int age, Car car) {
        this.name = name;
        this.age = age;
        this.car = car;
    }
    public String getName() {
        return name;
    }
     public void setName(String name) {
        this.name = name;
    }
     public int getAge() {
        return age;
    }
     public void setAge(int age) {
        this.age = age;
    }
      public Car getCar() {
        return car;
    }
      public void setCar(Car car) {
        this.car = car;
    }
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + ", car=" + car + "]";
    }
}

/**
 * 小汽车类
 * @author 
 *
 */
class Car implements Serializable {
    private static final long serialVersionUID = -5713945027627603702L;
    private String brand;       // 品牌
    private int maxSpeed;       // 最高时速
    public Car(String brand, int maxSpeed) {
        this.brand = brand;
        this.maxSpeed = maxSpeed;
    }
 
    public String getBrand() {
        return brand;
    }
 
    public void setBrand(String brand) {
        this.brand = brand;
    }
 
    public int getMaxSpeed() {
        return maxSpeed;
    }
 
    public void setMaxSpeed(int maxSpeed) {
        this.maxSpeed = maxSpeed;
    }
 
    @Override
    public String toString() {
        return "Car [brand=" + brand + ", maxSpeed=" + maxSpeed + "]";
    }
}
class CloneTest {
 public static void main(String[] args) {
        try {
            Person p1 = new Person("Hao LUO", 33, new Car("Benz", 300));
            Person p2 = MyUtil.clone(p1);   // 深度克隆
            p2.getCar().setBrand("BYD");
            // 修改克隆的Person对象p2关联的汽车对象的品牌属性
            // 原来的Person对象p1关联的汽车不会受到任何影响
            // 因为在克隆Person对象时其关联的汽车对象也被克隆了
            System.out.println(p1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

63.深拷贝和浅拷贝区别是什么?

答:复制一个 Java 对象

  • 浅拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针,不复制堆内存中的对象。
    在这里插入图片描述
  • 深拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量 和 变量指向堆内存中的对象的指针和堆内存中的对象。
    在这里插入图片描述假设B复制了A,当修改A时,看B是否会发生变化,
    如果B也跟着变了,说明这是浅拷贝,拿人手短;如果B没变,那就是深拷贝,自食其力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值