JAVA 终极面试题

目录标题

  • 一: JAVA 基础
    • 1.JDK和JRE有什么区别?
    • 2. 面向对象的特征(了解)
    • 3.== 和equals的区别是什么?
    • 4.两个对象的hashCode()相同,则equals()一定为true,对吗?
    • 5.final关键字在java中的作用
    • 6.java中的Math.round(-1.5)等于多少?
    • 7.Java八大数据类型
    • 8.java中操作字符串都有那些类?它们之间有什么区别?
    • 9.String str = "i"与 String str = new String("i")一样吗?
    • 10.如何将字符串反转?
    • 11.String类的常用方法有哪些?
    • 12.抽象类必须要有抽象方法吗?
    • 13.普通类和抽象类有哪些区别?
    • 14.抽象类能使用final修饰吗?
    • 15.接口和抽象类有什么区别?
    • 16.java中IO流分为几种
    • 17.BIO,NIO,AIO有什么区别?
    • 18. 重载和重写的区别(必会)
    • 19.Files的常用方法都有哪些?
    • 20. 什么是单例模式?怎么创造单例模式?
    • 21. 单例模式的优缺点
    • 22. 单例模式应用场景
    • 23. 手写冒泡排序?(必会)
    • 24. 常见的十种排序算法之前五种
    • 25. 子类继承和调用父类构造方法的执行顺序
    • 26. 自动装箱和自动拆箱
  • 二: 容器
    • 1. java容器都有哪些?
    • 2.collection和collections的区别
    • 3.List、Set和Map 之间的区别是什么?
    • 4.HashMap,HashTable,ConcurrentHashMap区别
    • 5.如何决定使用HashMap还是TreeMap?
    • 6.HashMap的实现底层原理
    • 7. HashMap的table的容量如何确定?loadFactor是什么?容量如何变化?变化会带来什么问题?
    • 8. hashmap是如何解决哈希冲突的?
    • 9.HashSet的实现原理
    • 10.ArrayList和LinkedList的区别与底层原理
        • 底层原理
    • 11.如何实现数组和List实体之间的转换?
    • 12.ArrayList和Vector(歪科特)的区别是什么?
    • 13.Array和ArrayList有何区别
    • 14.在Queue中poll()和remove()有什么区别?
    • 15.哪些集合类是线程安全的?
    • 16.迭代器Iterator是什么?
    • 17.Iterator 怎么使用?有什么特点?
    • 18.Iterator 和 ListIterator 有什么区别?
    • 19.怎么确保一个集合不能被修改?
    • 20. 遍历一一个List有哪些方式?每种方法的实现原理是什么? Java中List遍历的最佳实践是什么?
    • 21. java集合的快速失败机制“fail fast” ?
    • 22. HashSet如何检查重复? HashSet是如何保证数据不可重复的?
    • 23. hashCode ()与equals () 的相关规定
    • 24. BlockingQueue是什么?
    • 25. 在 Queue 中 poll()和 remove()有什么区别
    • 26. 能否使用任何类作为Map的key?
    • 27. 为什么HashMap中的String、Integer这样的包装类适合作为K
    • 28. 如果使用Object作为HashMap的Key,应该怎么办呢?
    • 29. HashMap为什么不直接使用hashCode处理后的哈希值直接作为table的下标?
    • 30. 如何决定使用HashMap还是TreeMap?
    • 31. comparable和comparator的区别?
    • 33. TreeMap和TreeSet在排序时如何比较元素? Collections工具类中的sort0方法如何比较元素?1
    • 34. JDK动态代理和cglib动态代理的区别和实现原理
  • 三: 多线程
    • 1.并行和并发有什么区别?
    • 2.线程和进程的区别
    • 3.守护线程是什么?
    • 4.创建线程有哪几种方式?
    • 5.说一下runnable和callable有什么区别?
    • 6.线程有哪些状态?
    • 7.sleep()和wait()有什么区别?
    • 8.notify()和notifyAll()有什么区别?
    • 9.线程的run()和start()有什么区别?
    • 10.在java程序中怎么保证多线程的运行安全?
    • 11.多线程锁的升级原理是什么?
    • 12.什么是死锁?
    • 13.怎么防止死锁?
    • 14.ThreadLocal是什么?有哪些使用场景?
    • 15.说一下synchronized底层实现原理
    • 16.synchronized和volatile的区别是什么?
    • 17.synchronized和Lock有什么区别?
    • 18.synchronized和ReentrantLock区别是什么?
    • 19.说一下atomic的原理
    • 20.synchronized的加锁流程
    • 21.同步锁、死锁、乐观锁、悲观锁 (高薪常问)
    • 22. 线程相关的基本方法?(必会)
  • 四: 线程池
    • 1.什么是线程池
    • 2. 为什么需要线程池(了解)
    • 3. 线程池的原理
    • 4. 拒绝策略(了解)
    • 5.线程池的优势
    • 6.创建线程池的方式
    • 7. 线程池有哪些状态
    • 8. 线程池的分类(高薪常问)
    • 9.线程池中submit()和execute()方法的区别
    • 10. 线程池的核心参数(高薪常问)
    • 11. 线程池的关闭(了解)
  • 五: 反射
    • 1.什么是反射?
    • 2.什么是java序列化?什么情况下需要序列化?
    • 3.动态代理是什么?有哪些应用?
    • 4.怎么实现动态代理?
  • 六: 迭代器
    • 1.什么是迭代器?
    • 2.for循环与迭代器的比较
    • 3.Iterator与ListIterator有什么区别?
    • 4. 增强for循环
  • 七: 锁
    • 1.大量失败请求-自旋锁
    • 2.锁重入问题--可重入锁
    • 3.读写锁
    • 4.分段锁
    • 5.死锁
  • 八: 对象拷贝
    • 1.为什么要使用克隆?
    • 2.如何实现对象克隆?
    • 3.深拷贝和浅拷贝的区别?
  • 九: JavaWeb
    • 1.jsp和servlet有什么区别?
    • 2.jsp有哪些内置区别?作用分别是什么?
    • 3.说一下jsp的四种作用域
    • 4.session和cookie有什么区别?
    • 5.session的工作原理
    • 6.如果客户端禁止cookie 能实现session还能用吗?
    • 7.Spring mvc 和struts的区别是什么?
    • 8.如何避免sql注入?
    • 9.什么是XSS攻击,如何避免?
    • 10.什么是CSRF攻击,如何避免?
  • 十: 异常
    • 1.throw 和 throws 的区别?
    • 2.final,finally和finalize有什么区别?
    • 3.try-catch-finally中哪个部分可以省略?
    • 4.try-catch-finally中,如果catch中return了,fially还会执行吗?
    • 5. RunTimeException和其他Exception的区别
    • 6. 常见的异常类有哪些?
  • 十一: 网络
    • 1.http响应码301和302代表的是什么?有什么区别?
    • 2.forward和redirect的区别?
    • TCP/IP协议分为哪四层,具体作用是什么?
    • 3.简述tcp和udp的区别?
    • 4.tcp为什么要三次握手,两次不行吗?为什么?
    • 5.说一下tcp粘包是怎么产生的?哪些情况下会发生粘包现象
    • 6.OSI的七层模型都有哪些?
    • 7.get和post的请求有那些区别?
    • 8.如何实现跨域?

一: JAVA 基础

1.JDK和JRE有什么区别?

JDK:java的开发工具包,是程序员使用java语言编写java程序所需的开发工具包,是提供给程序员使用的。最常用的是编译器和调试器。JDK包含JRE,javac,还包含了很多java调试和分析的工具,还包含了java程序编写所需的文档和demo。

JRE:java程序运行时的环境,其中包含了JVM虚拟机,java的基础类库。是使用java语言编写的程序运行时所需要的环境,是提供给想运行java程序的用户使用。在JRE中使用编译器来运行字节码文件。

JVM:JVM 虚拟机是一个抽象机器。它是一个规范,提供可以执行 java 字节码的运行时环境。是整个java 实现跨平台最核心的部分。源文件.java 在虚拟机中通过编译器编译成字节码文件,即class类文件(源文件同目录下),这种类文件可以在虚拟机上执行。但是要注意,因为经过编译后生成的class文件并不能直接在操作系统执行,而是经过虚拟机(相当于中间层)间接地与操作系统进行交互,由虚拟机将程序解释给本地系统执行。而在解释class的时候 JVM 需要调用解释器所需要的类库 lib,而 jre 包含它所需的 lib 类库。所以想要执行class文件需要 JVM 和 JRE同时存在。只有 JVM 并不能够完成class的执行。

2. 面向对象的特征(了解)

面向对象的特征:封装、继承、多态、抽象。

封装:就是把对象的属性和行为(数据)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节,就是把不想告诉或者不该告诉别人的东西隐藏起来,把可以告诉别人的公开,别人只能用我提供的功能实现需求,而不知道是如何实现的。增加安全性。

继承:子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提高了代码的复用性。

多态:指允许不同的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用)。封装和继承几乎都是为多态而准备的,在执行期间判断引用对象的实际类型,根据其实际的类型调用其相应的方法。

抽象: 表示对问题领域进行分析、设计中得出的抽象的概念是对一系列看上去不同, 但是本质上相同的具体概念的抽象。在 Java 中抽象用 abstract 关键字来修饰,用 abstract 修饰类时,此类就不能被实例化,从这里可以看出,抽象类(接口)就是为了继承而存在的。

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

:判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(当比较的是基本数据类型时 比较的是值,当比较的是引用数据类型时 == 比较的是内存地址)

equals:判断两个对象是否相等。但它一般有两种使用情况: 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容相等;若它们的内容相等,则返回 true (认为这两个对象相等)。

区别:==既能比较基本数据类型,也能比较对象。但equals方法只能比较对象,当equals比较对象时,比较的是对象的地址即引用。但是凡是有例外,对于String,Integer等包装类,则不关心对象引用地址是否一样,只关心里面的内容是否一样,如果一样则返回true.因为这些类重写了equals方法。

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

两个对象的hashCode相同,equals方法不一定需要为true。在Object的hashCode方法写了hashCode的几个约定,其中就有一条表示,equals方法返回false,不代表hashCode就不相等。言下之意,就是hashCode相等时,equals方法返回的可能是false。当然,要尽量为equals方法返回false的对象,返回不同的hashCode。

5.final关键字在java中的作用

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

(1)修饰类:表示该类不能被继承;

(2)修饰方法:表示方法不能被重写

(3)修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。

当final修饰的是一个基本数据类型数据时, 这个数据的值在初始化后将不能被改变; 当final修饰的是一个引用类型数据时, 也就是修饰一个对象时, 引用在初始化后将永远指向一个内存地址, 不可修改. 但是该内存地址中保存的对象信息, 是可以进行修改的.

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

Math的round方法是四舍五入,如果参数是负数,则往大的数 Math.round(-1.5)=-1

7.Java八大数据类型

Java八大数据类型:
(1)整数类型:byte、short、int、long
(2)小数类型:float、double
(3)字符类型:char
(4)布尔类型:boolea

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

操作字符串的类有:String、StringBuffer、StringBuilder

  1. String声明的是不可变的对象,每次操作都会生成新的String对象。然后将指针指向新的String对象,而StringBuffer、StringBuild可以在原有对象的基础上进行操作,所以在频繁修改字符串内容的情况下最好不要使用String。

  2. StringBuffer和StringBuilder最大的区别在于,StringBuffer是线程安全的,而StringBuilder是非线程安全的,但StringBuilder的性能却高于StringBuffer,所以在单线程环境下推荐使用StringBuilder,多线程环境下推荐使用StringBuffer。

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

不一样,使用String str=“i”,java虚拟机会把它分配到常量池中,而 String str=new String(“i”)创建了一个对象,会被分到堆内存中。

Java为了避免产生大量的String对象,设计了一个字符串常量池。工作原理是这样的,创建一个字符串时,JVM首先为检查字符串常量池中是否有值相等的字符串,如果有,则不再创建,直接返回该字符串的引用地址,若没有,则创建,然后放到字符串常量池中,并返回新创建的字符串的引用地址。所以,当你一使用String str="i"创建一个字符串时,str指向的是常量池中的这个字段。
String str=new String(“i”)使用的是标准的对象创建方式,Object obj会反映到java虚拟机栈的变量表中,作为一个引用类型数据出现,“new Object()”会反映到java堆中,在java堆上创建一个Object类型的实例数据值的结构化内存,这块内存的长度是不固定的。在java堆中还存放了能查到此对象类型数据(对象类型、父类、接口、方法等)的地址信息,这些信息存放在方法区中。

10.如何将字符串反转?

  1. 利用 StringBuffer或 StringBuilder 的 reverse 成员方法
  2. 利用 String 的 toCharArray 方法先将字符串转化为 char 类型数组,然后将各个字符进行重新拼接
  3. 利用 String 的 CharAt 方法取出字符串中的各个字符

11.String类的常用方法有哪些?

  1. 求字符串长度 public int length()//返回该字符串的长度
  2. 求字符串某一位置字符 public char charAt(int index)//返回字符串中指定位置的字符;注意字符串中第一个字符索引是0,最后一个是length()-1。
  3. 提取字符串: 用String类的substring方法可以提取字符串中的子串,该方法有两种常用参数:

​ 1)public String substring(int beginIndex)//该方法从beginIndex位置起,从当前字符串中取出剩余的字符作为一个新的字符串返回。

​ 2)public String substring(int beginIndex, int endIndex)//该方法从beginIndex位置起,从当前字符串中取出到endIndex-1位置的字符作为一个新的字符串返回。

  1. 字符串比较

​ 1)public int compareTo(String anotherString)//该方法是对字符串内容按字典顺序进行大小比较,通过返回的整数值指明当前字符串与参数字符串的大小关系。若当前对象比参数大则返回正整数,反之返回负整数,相等返回0。

​ 2)public int compareToIgnore(String anotherString)//与compareTo方法相似,但忽略大小写。

​ 3)public boolean equals(Object anotherObject)//比较当前字符串和参数字符串,在两个字符串相等的时候返回true,否则返回false。

​ 4)public boolean equalsIgnoreCase(String anotherString)//与equals方法相似,但忽略大小写。

  1. 字符串连接public String concat(String str)将参数中的字符串str连接到当前字符串的后面,效果等价于"+"。
  2. 字符串中单个字符查找

​ 1)public int indexOf(int ch/String str)//用于查找当前字符串中字符或子串,返回字符或子串在当前字符串中从左边起首次出现的位置,若没有出现则返回-1。

​ 2)public int indexOf(int ch/String str, int fromIndex)//改方法与第一种类似,区别在于该方法从fromIndex位置向后查找。

​ 3)public int lastIndexOf(int ch/String str)//该方法与第一种类似,区别在于该方法从字符串的末尾位置向前查找。

​ 4)public int lastIndexOf(int ch/String str, int fromIndex)//该方法与第二种方法类似,区别于该方法从fromIndex位置向前查找。

  1. 字符串中字符的大小与转换

​ 1)public String toLowerCase()//返回将当前字符串中所有字符转换成小写后的新串

​ 2)public String toUpperCase()//返回将当前字符串中所有字符转换成大写后的新串

  1. 字符串中字符的替换

​ 1)public String replace(char oldChar, char newChar)//用字符newChar替换当前字符串中所有的oldChar字符,并返回一个新的字符串。

​ 2)public String replaceFirst(String regex, String replacement)//该方法用字符replacement的内容替换当前字符串中遇到的第一个和字符串regex相匹配的子串,应将新的字符串返回。

​ 3)public String replaceAll(String regex, String replacement)//该方法用字符replacement的内容替换当前字符串中遇到的所有和字符串regex相匹配的子串,应将新的字符串返回。

  1. 其他类方法

​ 1)String trim()//截去字符串两端的空格,但对于中间的空格不处理。

​ 2)boolean statWith(String prefix)boolean endWith(String suffix)//用来比较当前字符串的起始字符或子字符串prefix和终止字符或子字符串suffix是否和当前字符串相同,重载方法中同时还可以指定比较的开始位置offset。

​ 3)regionMatches(boolean b, int firstStart, String other, int otherStart, int length)//从当前字符串的firstStart位置开始比较,取长度为length的一个子字符串,other字符串从otherStart位置开始,指定另外一个长度为length的字符串,两字符串比较,当b为true时字符串不区分大小写。

​ 4)contains(String str)//判断参数s是否被包含在字符串中,并返回一个布尔类型的值。

​ 5)String[] split(String str)//将str作为分隔符进行字符串分解,分解后的字字符串在字符串数组中返回。

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

不需要,抽象类不一定有抽象方法;但是包含一个抽象方法的类一定是抽象类。(有抽象方法就是抽象类,是抽象类可以没有抽象方法)

解释:

  1. 抽象方法:java中的抽象方法就是以abstract修饰的方法,这种方法只声明返回的数据类型、方法名称和所需的参数、没有方法体,也就是说抽象方法只需要声明而不需要实现。

  2. 抽象方法与抽象类:当一个方法为抽象方法时,意味着这个方法必须被子类的方法所重写,否则其子类的该方法仍然是abstract的,而这个子类也必须是抽象的,即声明为abstract。abstract抽象类不能用new实例化对象,abstract方法只允许声明不能实现。如果一个类中含有abstract方法,那么这个类必须用abstract来修饰,当然abstract类也可以没有abstract方法。 一个抽象类里面没有一个抽象方法可用来禁止产生这种类的对象。

  3. Java中的抽象类:abstract class 在 Java 语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。

在abstract class 中可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface中,只能够有静态的不能被修改的数据成员(也就是必须是static final的,不过在 interface中一般不定义数据成员),所有的成员方法都是abstract的。

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

1、抽象类的存在是为了被继承,不能实例化,而普通类存在是为了实例化一个对象
2、抽象类的子类必须重写抽象类中的抽象方法,而普通类可以选择重写父类的方法,也可以直接调用父类的方法
3、抽象类必须用abstract来修饰,普通类则不用
4、普通类和抽象类都可以含有普通成员属性和普通方法
5、普通类和抽象类都可以继承别的类或者被别的类继承
6、普通类和抽象类的属性和方法都可以通过子类对象来调用

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

不能

  1. 定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类
  2. final修饰的类不能被继承,没有子类。如果类中有抽象的方法也是没有意义的。
  3. abstract类为抽象类。即该类只关心子类具有的功能,而不是功能的具体实现。如果用final修饰方法,那么该方法则不能再被重写。final是不能修饰abstract所修饰的方法的。

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

实现:抽象类的子类使用 extends 来继承;接口必须使用implements来实现接口。

构造函数:抽象类可以有构造函数;接口不能有。

main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。

实现数量:类可以实现很多个接口;但是只能继承一个抽象类。

访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符

16.java中IO流分为几种

Java中的IO流按照流向分为输入流和输出流,按照操作数据类型分为字节流和字符流,共计四种类型:

  1. 字节输入流(InputStream):以字节为单位从输入流中读取数据,常用的实现类有FileInputStream、ByteArrayInputStream、ObjectInputStream等
  2. 字节输出流(OutputStream):以字节为单位将数据写入到输出流中,常用的实现类有FileOutputStream、ByteArrayOutputStream、ObjectOutputStream等。
  3. 字符输入流(Reader):以字符为单位从输入流中读取数据,常用的实现类有FileReader、InputStreamReader等。
  4. 字符输出流(Writer):以字符为单位将数据写入到输出流中,常用的实现类有FileWriter、OutputStreamWriter等。

在Java I/O库中,还有很多装饰器(Decorator)类,可以用于增加I/O流的功能和性能,如BufferedInputStream、BufferedWriter、DataInputStream、PrintWriter等。这些装饰器类可以组合使用,构成一个复杂的I/O操作链,从而实现更加灵活和高效的I/O操作。

17.BIO,NIO,AIO有什么区别?

1、BIO、NIO、AIO 有什么区别?
(1)同步阻塞BIO 一个连接一个线程。

JDK1.4之前,建立网络连接的时候采用BIO模式,先在启动服务端socket,然后启动客户端socket,对服务端通信,客户端发送请求后,先判断服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝请求,如果有的话会等待请求结束后才继续执行。

(2)同步非阻塞NIO 一个请求一个线程。

NIO主要是想解决BIO的大并发问题,BIO是每一个请求分配一个线程,当请求过多时,每个线程占用一定的内存空间,服务器瘫痪了。

JDK1.4开始支持NIO,适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中。

(3)异步非阻塞AIO 一个有效请求一个线程。

JDK1.7开始支持AIO,适用于连接数目多且连接比较长的结构,比如相册服务器,充分调用OS参与并发操作。

18. 重载和重写的区别(必会)

重载: 发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同, 方法返回值和访问修饰符可以不同,发生在编译时。

重写: 发生在父子类中,方法名和参数列表必须相同,返回值范围<=父类, 抛出的异常范围<=父类, 访问修饰符范围>=父类;如果父类方法访问修饰符为 private,则子类就不能重写该方法。

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

isExecutable:文件是否可以执行
isSameFile:是否同一个文件或目录
isReadable:是否可读
isDirectory:是否为目录
isHidden:是否隐藏
isWritable:是否可写
isRegularFile:是否为普通文件
getPosixFilePermissions:获取POSIX文件权限,windows系统调用此方法会报错
setPosixFilePermissions:设置POSIX文件权限
getOwner:获取文件所属人
setOwner:设置文件所属人
createFile:创建文件
newInputStream:打开新的输入流
newOutputStream:打开新的输出流
createDirectory:创建目录,当父目录不存在会报错
createDirectories:创建目录,当父目录不存在会自动创建
createTempFile:创建临时文件
newBufferedReader:打开或创建一个带缓存的字符输入流
probeContentType:探测文件的内容类型
list:目录中的文件、文件夹列表
find:查找文件
size:文件字节数
copy:文件复制
lines:读出文件中的所有行
move:移动文件位置
exists:文件是否存在
walk:遍历所有目录和文件
write:向一个文件写入字节
delete:删除文件
getFileStore:返回文件存储区
newByteChannel:打开或创建文件,返回一个字节通道来访问文件
readAllLines:从一个文件读取所有行字符串
setAttribute:设置文件属性的值
getAttribute:获取文件属性的值
newBufferedWriter:打开或创建一个带缓存的字符输出流
readAllBytes:从一个文件中读取所有字节
createTempDirectory:在特殊的目录中创建临时目录
deleteIfExists:如果文件存在删除文件
notExists:判断文件不存在
getLastModifiedTime:获取文件最后修改时间属性
setLastModifiedTime:更新文件最后修改时间属性
newDirectoryStream:打开目录,返回可迭代该目录下的目录流
walkFileTree:遍历文件树,可用来递归删除文件等操作

20. 什么是单例模式?怎么创造单例模式?

单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

一、标准饿汉式单例
在类加载的时候就创建单例对象,后续就只需要调用即可。
优点:

  1. 线程安全:无论如何多线程访问,保证唯一实例
  2. 调用、反应速度快:因为类加载时就已经创建,可以不加判断地直接调用。

缺点:

  1. 资源利用率低:有可能创建之后一直没有被调用,导致资源浪费。
  2. 加载速度慢:因为类加载时就创建单例对象,会影响加载速度。

二、标准懒汉式单例

调用实例时再加载。由于其在类加载时并不自行实例化,这种技术又被称为延迟加载(Lazy Load),即需要的时候再加载实例。

优点:线程安全

缺点:浪费内存空间

三、双重检查锁定(面试重点)

特点:性能高又保证线程安全

注意:其实双重检查模式就是对懒汉模式的优化,在new Singleton之前,加一个类锁。注意,成员变量singleton最好使用volatile修饰,否则若在无参构造中初始化一个其他的成员变量,会产生指令重排序,导致新创建的对象获取不到最新的成员变量值。

1.为啥双重选择?
如果只保留上面的if,将导致锁了跟没锁一样,当两个线程同时通过instance==null后,在锁那里等待,然后同样执行了两次new。即一个线程new,new后由于另一个线程已经有new的资格了,跟在后面new。

2.为啥volatile?

对于new而言,JVM可能自动做优化,其预期的执行顺序是:
1、为 instance 分配内存空间
2、初始化 instance
3、将 instance 指向分配的内存地址
JVM优化后可能导致变成1-》3-》2,此时,可能导致instance 还没有初始化完成,就已经被另一个线程调用发现非空,从而return了。即一个线程还没new完,另一个线程在return。
而volatile就可以阻止这种优化,当然不可避免的阻止了一些其他相关优化,因此可能导致效率上的降低。

四、枚举(最佳实践)

除了new以外,克隆、反射和反序列化都能产生新对象,其中有些会对以上三种实现方式产生破坏,于是大佬们想出了第四种,枚举。
利用JVM机制,保证了单例类不会被反射,并且构造函数只被执行一次。

21. 单例模式的优缺点

单例模式的优点

  1. 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
  2. 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)
  3. 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作
  4. 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理

单例模式的缺点

  1. 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象
  2. 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中

22. 单例模式应用场景

在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式

  1. 需要生成唯一序列号、计数的环境
  2. 在整个项目中需要一个共同访问资源
  3. 创建该对象时需要使用的资源比较多、或者大量访问了IO和数据库等资源
  4. 需要定义大量的静态函数及静态常量的类可以使用单例模式,当然,也可以直接定义成static

23. 手写冒泡排序?(必会)

冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

算法描述

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素作同样工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  3. 针对所有的元素重复以上的步骤,除了最后一个;
  4. 重复步骤1~3,直到排序完成。

JAVA 终极面试题_第1张图片

24. 常见的十种排序算法之前五种

冒泡排序

冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。原理如下:1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。2.对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。3.针对所有的元素重复以上的步骤,除了最后一个。4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

插入排序

插入排序算法是基于某序列已经有序排列的情况下,通过一次插入一个元素的方式按照原有排序方式增加元素。这种比较是从该有序序列的最末端开始执行,即要插入序列中的元素最先和有序序列中最大的元素比较,若其大于该最大元素,则可直接插入最大元素的后面即可,否则再向前一位比较查找直至找到应该插入的位置为止。插入排序的基本思想是,每次将1个待排序的记录按其关键字大小插入到前面已经排好序的子序列中,寻找最适当的位置,直至全部记录插入完毕。执行过程中,若遇到和插入元素相等的位置,则将要插入的元素放在该相等元素的后面,因此插入该元素后并未改变原序列的前后顺序。我们认为插入排序也是一种稳定的排序方法。插入排序分直接插入排序、折半插入排序和希尔排序3类。

选择排序

选择排序算法的基本思路是为每一个位置选择当前最小的元素。选择排序的基本思想是,基于直接选择排序和堆排序这两种基本的简单排序方法。首先从第1个位置开始对全部元素进行选择,选出全部元素中最小的给该位置,再对第2个位置进行选择,在剩余元素中选择最小的给该位置即可;以此类推,重复进行“最小元素”的选择,直至完成第(n-1)个位置的元素选择,则第n个位置就只剩唯一的最大元素,此时不需再进行选择。使用这种排序时,要注意其中一个不同于冒泡法的细节。举例说明:序列58539.我们知道第一遍选择第1个元素“5”会和元素“3”交换,那么原序列中的两个相同元素“5”之间的前后相对顺序就发生了改变。因此,我们说选择排序不是稳定的排序算法,它在计算过程中会破坏稳定性。

快速排序

快速排序的基本思想是:通过一趟排序算法把所需要排序的序列的元素分割成两大块,其中,一部分的元素都要小于或等于另外一部分的序列元素,然后仍根据该种方法对划分后的这两块序列的元素分别再次实行快速排序算法,排序实现的整个过程可以是递归的来进行调用,最终能够实现将所需排序的无序序列元素变为一个有序的序列。

归并排序

归并排序算法就是把序列递归划分成为一个个短序列,以其中只有1个元素的直接序列或者只有2个元素的序列作为短序列的递归出口,再将全部有序的短序列按照一定的规则进行排序为长序列。归并排序融合了分治策略,即将含有n个记录的初始序列中的每个记录均视为长度为1的子序列,再将这n个子序列两两合并得到n/2个长度为2(当凡为奇数时会出现长度为l的情况)的有序子序列;将上述步骤重复操作,直至得到1个长度为n的有序长序列。需要注意的是,在进行元素比较和交换时,若两个元素大小相等则不必刻意交换位置,因此该算法不会破坏序列的稳定性,即归并排序也是稳定的排序算法。

25. 子类继承和调用父类构造方法的执行顺序

  1. 当子类继承和调用父类构造方法时,执行顺序如下:

    1. 子类构造方法中会默认调用父类的无参构造方法,如果父类没有无参构造方法,则必须通过super关键字显式调用父类的构造方法。
    2. 如果子类的构造方法中没有显式调用父类的构造方法,则编译器会自动调用父类的无参构造方法。
    3. 如果父类没有无参构造方法,且子类的构造方法中没有显式调用父类的构造方法,则会编译错误。
    4. 子类中可以通过super关键字显式调用父类的构造方法,且必须在子类构造方法的第一行执行,否则会编译错误。
    5. 子类构造方法中可以通过this关键字调用本类的其他构造方法,但必须在第一行执行,否则会编译错误。
    6. 子类构造方法中可以同时使用super和this关键字,但必须按照规定的顺序执行。

    总之,子类继承和调用父类构造方法的执行顺序是先执行父类的构造方法,再执行子类的构造方法,且必须遵守一定的规定和顺序来编写代码。

26. 自动装箱和自动拆箱

自动装箱和自动拆箱是Java语言中的两个特性,用于将基本数据类型和对应的包装类型进行转换。下面分别介绍自动装箱和自动拆箱的概念和用法:

  1. 自动装箱:将基本数据类型转换为对应的包装类型。例如,将int类型的值转换为Integer类型的值
int i = 10;
Integer j = i; // 自动装箱

将int类型的值10赋值给i变量,然后将i变量赋值给Integer类型的变量j,这就是自动装箱的过程。

  1. 自动拆箱:将包装类型转换为对应的基本数据类型。例如,将Integer类型的值转换为int类型的值
Integer i = 10;
int j = i; // 自动拆箱

将Integer类型的值10赋值给i变量,然后将i变量赋值给int类型的变量j,这就是自动拆箱的过程。

总之,自动装箱和自动拆箱是Java语言中的两个特性,可以方便地将基本数据类型和对应的包装类型进行转换,提高了代码的可读性和简洁性。需要注意的是,自动装箱和自动拆箱可能会影响程序的性能和内存使用,需要根据具体的需求和场景进行选择和优化。

二: 容器

1. java容器都有哪些?

JAVA中的容器类主要分为两大类,一类是Map类,一类是Collection类,他们有一个共同的父接口Iterator,它提供基本的遍历,删除元素操作。Iterator还有一个子接口LinkIterator,它提供双向的遍历操作。

Collection是一个独立元素的序列,这些元素都服从一条或多条规则,它有三个子接口List,Set和Queue。其中List必须按照插入的顺序保存元素、Set不能有重复的元素、Queue按照排队规则来确定对象的产生顺序(通常也是和插入顺序相同)

Map是一组成对的值键对对象,允许用键来查找值。它允许我们使用一个对象来查找某个对象,也被称为关联数组,或者叫做字典。它主要包括HashMap类和TreeMap类。Map在实际开发中使用非常广,特别是HashMap,想象一下我们要保存一个对象中某些元素的值,如果我们在创建一个对象显得有点麻烦,这个时候我们就可以用上Map了,HashMap采用是散列函数所以查询的效率是比较高的,如果我们需要一个有序的我们就可以考虑使用TreeMap。

常用集合类有哪些?

  • Collection
    • List
      • ArrayList
      • LinkedList
      • Vector
      • Stack
    • Set
      • HashSet
      • LinkedHashSet
      • TreeSet
  • Map
    • HashMap
      • LinkedHashMap
    • TreeMap
    • ConcurrentHashMap
    • Hashtable
    • properties

2.collection和collections的区别

java.til.Collection是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口 方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。

Collections则是集合类的一个工具类帮助类,其中提供了-系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

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

一.排序

  1. List: 有序、可重复。
  2. Set: 无序、不可重复的集合。重复元素会覆盖掉。
  3. Map: 键值对,键唯一、值不唯一。Map 集合中存储的是键值对,键不能重复,值可以重复。

二.线程

  1. List: 通过索引查找快,增删速度慢 。

  2. Set: 检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。

  3. Map: 根据键得到值,对 map 集合遍历时先得到键的 set 集合,对 set 集合进行遍历,得到相应的值。

三.接口

  1. List: ArrayList、LinkedList、Vector

  2. Set: HashSet、TreeSet、LinkedHashSet

  3. Map: HashTable、TreeMap、HashMap

4.HashMap,HashTable,ConcurrentHashMap区别

区别对比一(HashMap 和 HashTable 区别):

1、HashMap 是非线程安全的,HashTable 是线程安全的。

2、HashMap 的键和值都允许有 null 值存在,而 HashTable 则不行。

3、因为线程安全的问题,HashMap 效率比 HashTable 的要高。

4、Hashtable 是同步的,而 HashMap 不是。因此,HashMap 更适合于单线程环境,而 Hashtable 适合于多线程环境。一般现在不建议用 HashTable, ① 是 HashTable 是遗留类,内部实现很多没优化和冗余。②即使在多线程环境下, 现在也有同步的 ConcurrentHashMap 替代,没有必要因为是多线程而用 HashTable。

区别对比二(HashTable 和 ConcurrentHashMap 区别):

  1. HashTable 使用的是 Synchronized 关键字修饰,ConcurrentHashMap 是 JDK1.7 使用了锁分段技术来保证线程安全的。JDK1.8ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。

  2. synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就 不会产生并发,效率又提升 N 倍。

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

对于在Map中插入、删除、定位一个元素这类操作,HashMap是最好的选择。 因为相对而言,HashMap插入更快. 但如果对一个key集合进行有序的遍历,那TreeMap是更好的选择。

6.HashMap的实现底层原理

HashMap 在 JDK1.8 之前的实现方式 数组+链表

但是在 JDK1.8 后对 HashMap 进行了底层优化,改为了由 数组+链表或者数值+红黑树 实现,主要的目的是提高查找效率

  1. Jdk1.8 数组+链表或者数组+红黑树实现,当链表中的元素超过了 8 个以后, 会 将链表转换为红黑树,当红黑树节点 小于 等于 6 时又会退化为链表。

  2. 当 new HashMap():底层没有创建数组,首次调用 put()方法示时,底层创建长度 为 16 的数组,jdk8 底层的数组是:Node[],而非 Entry[],用数组容量大小乘以加载因子得 到一个值,一旦数组中存储的元素个数超过该值就会调用 rehash 方法将数组容量增加到原 来的两倍,专业术语叫做扩容,在做扩容的时候会生成一个新的数组,原来的所有数据需要 重新计算哈希码值重新分配到新的数组,所以扩容的操作非常消耗性能. 默认的负载因子大小为 0.75,数组大小为 16。也就是说,默认情况下,那么当 HashMap 中元素个数超过 160.75=12 的时候,就把数组的大小扩展为 216=32,即扩大一倍

  3. 在我们 Java 中任何对象都有 hashcode,hash 算法就是通过 hashcode 与自己进 行向右位移 16 的异或运算。这样做是为了计算出来的 hash 值足够随机,足够分散,还有 产生的数组下标足够随机

    map.put(k,v)实现原理

    (1) 首先将 k,v 封装到 Node 对象当中(节点)。

    (2) 先调用 k 的 hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。

    (3) 下标位置上如果没有任何元素,就把 Node 添加到这个位置上。如果说下标对应的位置上有链表。此时,就会拿着 k 和链表上每个节点的 k 进行 equal。如果所有的 equals 方 法返回都是 false,那么这个新的节点将被添加到链表的末尾。如其中有一个 equals 返回了 true,那么这个节点的 value 将会被覆盖。

    map.get(k)实现原理

    (1) 先调用 k 的 hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。

    (2) 在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返 回 null。如果这个位置上有单向链表,那么它就会拿着参数 K 和单向链表上的每一个节点 的 K 进行 equals,如果所有 equals 方法都返回 false,则 get 方法返回 null。如果其中个节点的 K 和参数 K 进行 equals 返回 true,那么此时该节点的 value 就是我们要找的 value 了,get 方法最终返回这个要找的 value。

    1. Hash 冲突 不同的对象算出来的数组下标是相同的这样就会产生 hash 冲突,当单线链表达到一定长度 后效率会非常低。
    2. 在链表长度大于 8 的时候,将链表就会变成红黑树,提高查询的效率

    简单版本(HashMap是实现了map接口,在jdk1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,当储存无冲突的时候,就会存放到数组里面,当储存冲突的时候,就会存放到链表里面;当jdk1.8之后,hashmap的是由数组+链表+红黑树组成,当储存数据无冲突时,也是会直接储存到数组里面,当储存数据冲突时,就分情况了,如果链表的长度小于8时,就是用链表进行储存;如果链表的长度大于等于8时,此时的链表就会变成红黑树,成为红黑树期间,如果链表的长度小于6了就会变回链表; 之所以会引入红黑树,就是为了避免链表过长,从而影响到查询的效率,也提升了安全性。但是它还是非线程安全的,在多线程中会有数据丢失的情况,如果要线程安全则就要使用hashtable )

7. HashMap的table的容量如何确定?loadFactor是什么?容量如何变化?变化会带来什么问题?

初始容量为16,达到阈值扩容,阈值等于最大容量*负载因子,扩容每次2倍,总是2的n次方。

  1. table数组大小是由capacity这个参数确定的,默认是16, 也可以构造时传入,最大限制是1<< 30;
  2. loadFactor是装载因子,主要目的是用来确认table数组是否需要动态扩展 默认值是0.75,比如table数组大小 为16,装载因子为0.75时,threshold就是12,当table的实际大小超过12时,table就需要动态扩容;
  3. 扩容时,调用resize()方法将table长度变为原来的两倍(注意是table长度,而不是threshold)
  4. 如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。

8. hashmap是如何解决哈希冲突的?

HashMap
保存key-value形式的数据

Hash算法,就是把任意长度的输入,通过散列算法,变成固定长度的输出,这个输出结果是散列值。

Hash表又叫做“散列表”,它是通过key直接访问在内存存储位置的数据结构,在具体实现上,我们通过hash函数把key映射到表中的某个位置,来获取这个位置的数据,从而加快查找速度。

hash冲突
所谓hash冲突,是由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,所以总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。

如:保留的value通过hash算法产生的值相同,就出现的哈希冲突。

解决hash冲突:

  1. 开放定址法,也称为线性探测法,就是从发生冲突的那个位置开始,按照一定的次序从hash表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲位置中。
  2. 链式寻址法,这是一种非常常见的方法,简单理解就是把存在hash冲突的key,以单向链表的方式来存储,比如HashMap就是采用链式寻址法来实现的。
  3. 再hash法,就是当通过某个hash函数计算的key存在冲突时,再用另外一个hash函数对这个key做hash,一直运算直到不再产生冲突。这种方式会增加计算时间,性能影响较大。
  4. 建立公共溢出区, 就是把hash表分为基本表和溢出表两个部分,凡事存在冲突的元素,一律放入到溢出表中。

9.HashSet的实现原理

hashset是基于hashmap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75的hashmap。封装了一个hashmap 对象来存储所有的集合元素,所有放在 hashset中的集合元素实际上由 hashmap的key来保存,而 hashset中的 hashmap的 value则存储了一个PRESENT(present)的静态object对象

10.ArrayList和LinkedList的区别与底层原理

  1. ArrayList的实现是基于数组,LinkedList的实现是基于双向链表。

  2. 对于随机访问,ArrayList优于LinkedList,ArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问。而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)

  3. 对于插入和删除操作,LinkedList优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引。

  4. LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素

    底层原理
    1. ArrayList
      ArrayList(基于数组实现的)的底层是继承了AbstractList而AbstractList又继承了AbstractCollection并且实现了List接口 AbstractCollection又实现了Collection,Collection又继承了Iterable,然而Iterable本身就是泛型 并且在Iterable里返回了一个遍历T类元素的迭代器叫Iteratro 而T就是泛型的占位符。ArrayList同时还实现了RandomAccess、Cloneable、Serializable接口 所以ArrayList是支持快速访问、复制、序列化的 但是他不是线程安全的。 ArrayList的默认的初始容量是10 并且是通过static和final来修饰的,如果超出当前容量就会扩容原来的1.5倍,而且ArrayList的构造函数是指定使用Collection来构造的。
    2. LinkedList
      LinkedList(基于双向链表)的底层是继承了AbstractSequentialList并且实现了List, Deque, Cloneable, Serializable接口,而List, Deque(底层继承Queue 对列 ),实际上都是继承了Collection接口(Collection又继承了Iterable,然而Iterable本身就是的泛型并且在Iterable里返回了一个遍历T类元素的迭代器叫Iteratro而T就是泛型的占位符)并且LinkedList是线程安全的。LinkedList的指针默认一个指的是开头一个指的是结尾并且每个节点都用Node来表示,在进行遍历的时候首先会判断便利数组的长度是否大于size的一半如果大于他就会从后往前查找否则的话他就会从前往后查找, LinkedList是没有初始容量的,也没有扩容的机制,他的话就是一直在前面或者后面新增就好。

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

  1. List转数组 (1)使用for循环 (2)使用toArray()方法
  2. 数组转List (1)使用for循环 (2)使用asList() (3)使用Collections.addAll()

List list2 = new ArrayList(arrays.length);

Collections.addAll(list2, arrays);

12.ArrayList和Vector(歪科特)的区别是什么?

两个类都实现了 List 接口( List 接口继承了 Collection 接口), 他们都是有序集合 ,即存储在这两个集

合中的元素的位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引号取出某个元素,

并且其中的数据是允许重复的,这是与 HashSet 之类的集合的最大不同处, HashSet 之类的集合不可以

按索引号去检索其中的元素,也不允许有重复的元素。

ArrayList Vector 的区别主要包括两个方面:

同步性:

Vector 是线程安全的,也就是说是它的方法之间是线程同步的,而 ArrayList 是线程序不安全的,它

的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用 ArrayList ,因为它不考

虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用 Vector ,因为不需要我们自

己再去考虑和编写线程安全的代码。

数据增长:

ArrayList 与 Vector 都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需

要增加 ArrayList 与 Vector 的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增

加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。

Vector 默认增长为原来两倍,而 ArrayList 的增长策略在文档中没有明确规定(从源代码看到的是增

长为原来的 1.5 倍)。 ArrayList 与 Vector 都可以设置初始的空间大小, Vector 还可以设置增长的空

间大小,而 ArrayList 没有提供设置增长空间的方法。

总结:即 Vector 增长原来的一倍, ArrayList 增加原来的 0.5 倍。

13.Array和ArrayList有何区别

Array:

 数组(Array)是有序的元素序列。若将有限个类型相同的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。用于区分数组的各个元素的数字编号称为下标。数组是在程序设计中,为了处理方便, 把具有相同类型的若干元素按有序的形式组织起来的一种形式。这些有序排列的同类数据元素的集合称为数组。

数组是用于储存多个相同类型数据的集合

ArrayList:

ArrayList就是动态数组,用MSDN中的说法,就是Array的复杂版本,它提供了动态的增加和减少元素,实现了ICollection和IList接口,灵活的设置数组的大小等好处
  1. 如果列表的大小已经指定,大部分情况下是存储和遍历它们。 对于遍
  2. 历基本数据类型,尽管 Collections 使用自动装箱来减轻编码任务,在指定大小的基本类型的 列表上工作也会变得很慢。
  3. 如果你要使用多维数组,使用 [][] 比 List> 更容易。
  4. Array 类型的变量在声明的同时必须进行实例化(至少得初花数组的大小),而 ArrayList 可以只是先声明;
  5. Array 始终是连续存放的;而 ArrayList 的存放不一定连续;
  6. Array 对象的初始化必须指定大小,且创建后的数组大小是固定的;而 ArrayList 的大小可以动态指定,空间大小可以任意增加;
  7. Array 不能随意添加、删除;而 ArrayList 可以在任意位置插入和删除。

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

(1)offer()和add()区别:

增加新项时,如果队列满了,add会抛出异常,offer返回false。

(2)poll()和remove()区别:

poll()和remove()都是从队列中删除第一个元素,remove抛出异常,poll返回null。

(3)peek()和element()区别:

peek()和element()用于查询队列头部元素,为空时element抛出异常,peek返回null。

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

Vector、Hashtable、Stack 都是线程安全的,而像 HashMap 则是非线程安全的,不过在 JDK 1.5 之后随着 Java. util. concurrent 并发包的出现,它们也有了自己对应的线程安全类,比如 HashMap 对应的线程安全类就是 ConcurrentHashMap。

vector: 就比arraylist多 了个同步化机制(线程安全) 因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。

statck: 堆栈类,先进后出。

hashtable: 就比hashmap多 了个线程安全。

enumeration: 枚举,相当于迭代器。

16.迭代器Iterator是什么?

为了方便的处理集合中的元素,Java中出现了一个对象,该对象提供了一些方法专门处理集合中的元素.例如删除和获取集合中的元素.该对象就叫做迭代器(Iterator)。

Iterator接口提供遍历任何Collection的接口。我们可以在Collection中使用迭代器方法来获取迭代器实例。迭代器取代了Java集合框架中的 Enumeration。迭代器允许调用者在迭代过程中移除元素。

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

Iterator的使用

  1. Iterator()要求容器返回一个Iterator。Iterator将准备好返回序列的第一个元素。
  2. 使用next()获得序列中的下一个元素
  3. 使用hasNext()检查序列中是否还有元素。
  4. 使用remove()将迭代器新近返回的元素删除。

Iterator的特点

  1. Iterator遍历集合元素的过程中不允许线程对集合元素进行修改,否则会抛出ConcurrentModificationEception的异常。
  2. Iterator遍历集合元素的过程中可以通过remove方法来移除集合中的元素,删除的是上一次Iterator.next()方法返回的对象。
  3. Iterator必须依附于一个集合类对象而存在,Iterator本身不具有装载数据对象的功能。
  4. next()方法,该方法通过游标指向的形式返回Iterator下一个元素

18.Iterator 和 ListIterator 有什么区别?

(1)ListIterator 继承 Iterator

(2)ListIterator 比 Iterator多方法

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

Iterator和ListIterrator主要有如下几点区别:
1、使用范围不同,iterator可以应用于所有的集合,Set、List和Map以及这些集合的子类型。而ListIterator只能用于List及其子类型。

2、ListIterator有add方法,可以向List中添加对象,而Iterator不能。

3、ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,而且ListIterator有hasPrevious()和previous()方法,可以实现逆向遍历,但是iterator不可以。

4、ListIterator可以定位当前索引的位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。

5、都可以实现删除操作,但是ListIterator可以实现对象的修改,set()方法可以实现。Iterator仅能遍历,不能实现修改。

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

通过Collections工具类的静态方法unmodifiableCollection(list),该静态方法内部返回了Collections的静态内部类UnmodifiableCollection对象,该内部类又实现了Collection集合接口,也就是说内部类UnmodifiableCollection也是集合的一种。同样实现了Collection集合的方法,只不过在比如add、remove等修改的方法中直接抛出UnsupportedOperationException()异常,因此实现了集合不能修改的功能。

20. 遍历一一个List有哪些方式?每种方法的实现原理是什么? Java中List遍历的最佳实践是什么?

遍历方式有以下几种:

  1. for循环遍历,基于计数器。在集合外部维护一个计数 器,然后依次读取每一个位置的元素 ,当读取到最后一个元素后停止。
  2. 迭代器遍历,Iterator。 Iterator 是面向对象的一个设计模 式,目的是屏蔽不同数据集合的特点,统- -遍历集合的 接口。Java 在Collections中支持了Iterator 模式。
  3. foreach循环遍历。foreach 内部也是采用了Iterator 的方式实现,使用时不需要显式声明Iterator或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。

最佳实践: Java Collections框架中提供了一个RandomAccess接口,用来标记List实现是否支持 Random Access。如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为0(1),如ArrayList。如果没有实现该接口,表示不支持Random Access,如LinkedList。

推荐的做法就是,支持Random Access的列表可用for循环遍历,否则建议用Iterator或foreach遍历。

21. java集合的快速失败机制“fail fast” ?

快速失败机制是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。

例如:假设存在两个线程(线程1、线程2),线程l通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出ConcurrentModificationException异常,从而产牲fail-fast机制。

原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount量。集合在被遍历期 间如果内容发生变化,就会改变modCount的值。 每当迭代器使用hashNext()/next)遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;则抛出异常,终止遍历。

解决办法: 1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。 2.使用CopyOnWriteArrayList来替换ArrayList

22. HashSet如何检查重复? HashSet是如何保证数据不可重复的?

向HashSet中add 0元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles方法比较。

HashSet中的add 0方法会使用HashMap的put0方法。

HashMap的key是唯一的,由源码可以看出HashSet添加进去的值就是作为HashMap的key,組在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧 的V。所以不会重复( HashMap比较key是否相等是先比 较hashcode再眦较equals )

23. hashCode ()与equals () 的相关规定

  1. 如果两个对象相等,则hashcode- 定也是相同的
  2. 两个对象相等 对两个equals方法返回true
  3. 两个对象有相同的hashcode值,它们也不一 定是相等的

综上,equals方法被覆盖过,则hashCode方法也必须被覆盖

hashCode)的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

24. BlockingQueue是什么?

Java.util.concurrent.BlockingQueue是一个队列, 在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。

BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在

BlockingQueue的实现类中被处理了。Java提供 了集中BlockingQueue的实现,比如ArrayBlockingQueue、

LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。

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

在 Java 中,Queue 接口是一个用于队列的集合接口,常用的实现类有 LinkedList、PriorityQueue 和 ArrayDeque。Queue 接口中有两个方法 poll() 和 remove(),它们都是从队列中获取并删除队头元素的方法,它们的区别如下:

  1. 如果队列为空,调用 poll() 方法返回 null,而不是抛出异常;而调用 remove() 方法则会抛出 NoSuchElementException 异常。
  2. remove() 方法是 Queue 接口中定义的方法,而 poll() 方法是 Deque 接口中定义的方法,因此在实现 Deque 接口的类(如 LinkedList 和 ArrayDeque)中,poll() 方法和 remove() 方法的行为是相同的。

26. 能否使用任何类作为Map的key?

可以使用任何类作为Map的key,然而在使用之前,需要考虑以下几点:

如果类重写了equalsO) 方法,也应该重写hashCode(方法。

类的所有实例需要遵循与equals( 和hashCode()相关的规则。

如果一个类没有使用equals(),不应该在hashCode()中使砣。

用户自定义Key类最佳实践是使之为不可变的,这样hashCode()值可以被缓存起来,拥有更好的性能。可 变的类也可以确保hashCode()和equals() 在未来不会改变,这样就会解决与可变相关的问题了。

27. 为什么HashMap中的String、Integer这样的包装类适合作为K

String、Integer等 包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率

都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况

内部已重写了equals()、hashCode0)等方法, 遵守了

HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

28. 如果使用Object作为HashMap的Key,应该怎么办呢?

重写hashCode()和equals()方法

重写hashCode( )是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;

重写equals0方法,需要遵守自反性、对称性、传递性、 一致性以及对于任何非null的引用值x, xequal(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;

29. HashMap为什么不直接使用hashCode处理后的哈希值直接作为table的下标?

hashCode(方法返回的是int整数类型,其范围为-(2 ^ 31)~(2^31- 1),约有40亿个映射空间,而HashMap的容 量范围是在16 (初始化默认值) ~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;

那怎么解决呢?

HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;

在保证数组长度为2的幂次方的时候,使用hash()运算之 后的值与运算(&) (数组长度 - 1)来获取数组下标的.方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;

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

对于在Map中插入、删除和定位元素这类操作,

HashMap是最好的选择。然而,假如你需要对一个有序 的key集合进行遍历,TreeMap是 更好的选择。基纡你的 collection的大小,也许向HashMap中添加元素 会更快,将map换为TreeMap进行有key的遍历。

31. comparable和comparator的区别?

comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序

comparator接口实际上是出自java.util包,它有一个 compare(Object obj1, Object obj2)方法用来排序

-般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法,当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareIo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第_种代表我们只能使用两个参数版的Collections.sort().

33. TreeMap和TreeSet在排序时如何比较元素? Collections工具类中的sort0方法如何比较元素?1

TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插 入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。

Collections工具类的sort方法有两种重载的形式,

第一种要求传入的待排序容器中存放的对象比较实现Comparable接口以实现元素的比较;

第二种不强制性的要求容器中的元素必须可比较,但是 要求传入第二个参数,参数是Comparator 接口的子类型(需要重写compare方法实现元素的比较),相当于-个临时定义的排序规则,其实就是通过接口注入比较元 大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。

34. JDK动态代理和cglib动态代理的区别和实现原理

  1. Jdk动态代理:利用拦截器(必须实现InvocationHandler接口)加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理
  2. Cglib动态代理:利用ASM框架,对代理对象类生成的class文件加载进来,通过修改其字节码生成子类来进行代理

所以:

  • 如果想要实现JDK动态代理那么代理类必须实现接口,否则不能使用;
  • 如果想要使用CGlib动态代理,那么代理类不能使用final修饰类和方法;

还有: 在jdk6、jdk7、jdk8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理。

原理:

  1. JDK动态代理
    利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
  2. CGLiB动态代理
    利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

三: 多线程

1.并行和并发有什么区别?

  1. 并发和并行是即相似又有区别的两个概念,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。
  2. 在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单机处理系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。
  3. 若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,实现并行执行,即利用每个处理机来处理一个可并发执行的程序,这样,多个程序便可以同时执行。

2.线程和进程的区别

进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程

线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

3.守护线程是什么?

守护线程(Daemon Thread)也被称之为后台线程或服务线程,守护线程是为用户线程服务的,当程序中的用户线程全部执行结束之后,守护线程也会跟随结束。

守护线程的角色就像“服务员”,而用户线程的角色就像“顾客”,当“顾客”全部走了之后(全部执行结束),那“服务员”(守护线程)也就没有了存在的意义,所以当一个程序中的全部用户线程都结束执行之后,那么无论守护线程是否还在工作都会随着用户线程一块结束,整个程序也会随之结束运行。

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

方式一:继承于Thread类

步骤:
1.创建一个继承于Thread类的子类
2.重写Thread类的run() --> 将此线程执行的操作声明在run()中
3.创建Thread类的子类的对象
4.通过此对象调用start()执行线程

方式二:实现Runnable接口

步骤:
1.创建一个实现了Runnable接口的类
2.实现类去实现Runnable中的抽象方法:run()
3.创建实现类的对象
4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start()
① 启动线程
②调用当前线程的run()–>调用了Runnable类型的target的run()

方式一和方式二的比较:

  • 开发中优先选择实现Runnable接口的方式
  • 原因:
    (1)实现的方式没有类的单继承性的局限性
    (2)实现的方式更适合来处理多个线程有共享数据的情况
  • 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中

方式三:实现Callable接口

步骤:
1.创建一个实现Callable的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中
3.创建Callable接口实现类的对象
4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
6.获取Callable中call方法的返回值

实现Callable接口的方式创建线程的强大之处

  • call()可以有返回值的
  • call()可以抛出异常,被外面的操作捕获,获取异常的信息
  • Callable是支持泛型的

方式四:使用线程池

线程池好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理

步骤:
1.以方式二或方式三创建好实现了Runnable接口的类或实现Callable的实现类
2.实现run或call方法
3.创建线程池
4.调用线程池的execute方法执行某个线程,参数是之前实现Runnable或Callable接口的对象

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

相同点:

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start()启动线程

主要区别:
Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,但是此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

6.线程有哪些状态?

Java中线程的状态分为6种。

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

  3. 阻塞(BLOCKED):表示线程阻塞于锁。

  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

  6. 终止(TERMINATED):表示该线程已经执行完毕。

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

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

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

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

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

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

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

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

sleep() 是 Thread 类的静态本地方法;wait() 是Object类的成员本地方法
sleep() 方法可以在任何地方使用;wait() 方法则只能在同步方法或同步代码块中使用,否则抛出异常Exception in thread “Thread-0” java.lang.IllegalMonitorStateException
sleep() 会休眠当前线程,指定时间,释放 CPU 资源,不释放对象锁,休眠时间到自动苏醒继续执行;wait() 方法放弃持有的对象锁,进入等待队列,当该对象被调用 notify() / notifyAll() 方法后才有机会竞争获取对象锁,进入运行状态
JDK1.8 sleep() wait() 均需要捕获 InterruptedException 异常

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

notify() 和 notifyAll() 都是 Object 对象用于通知处在等待该对象的线程的方法。
void notify(): 唤醒一个正在等待该对象的线程。
void notifyAll(): 唤醒所有正在等待该对象的线程。
notify 可能会导致死锁,而 notifyAll 则不会
任何时候只有一个线程可以获得锁,也就是说只有一个线程可以运行 synchronized 中的代码
使用 notifyAll()可以唤醒所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一
个。

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

  1. start():
    用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体中的代码执行完毕而直接继续执行后续的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里的run()方法称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

  2. run():
    run()方法只是类的一个普通方法而已。run方法相当于线程的任务处理逻辑的入口方法,就是线程体,它由java虚拟机在运行相应线程时直接调用,而不是由代码进行调用。

  3. 总结:排队玩游戏机
    多线程原理相当于玩游戏机,只有一个游戏机(CPU),start是排队,等CPU轮到你,你就run。
    调用start后,线程会被放入到等待队列中,也就是上面说的就绪状态,等待CPU调用,并不是马上调用。然后通过JVM,线程thread会调用run方法,执行本线程的线程体,先调用start,后调用run。为什么不直接调用run呢?为了实现多线程的优点。

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

线程安全在三个方面体现:

[原子性]:提供互斥访问,同⼀时刻只能有⼀个线程对数据进行操作;
[可见性]:⼀个线程对主内存的修改可以及时地被其他线程看到;
[有序性]:⼀个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果⼀般杂乱无序(happens&before 原则)。

  1. 原子性指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
  2. 可见性指当一个线程修改了某一个共享变量的值,其他的线程是否能够立即知道这个修改。显然,对于串行程序来说,可见性问题是不存在的。因为我们在任何一个操作步骤中修改了某个变量,那么在后续的步骤中,读取这个变量一定是修改后的值。
  3. 有序性指对于一个线程的执行代码,我们习惯性的认为代码的执行是从前往后,依次执行的。但是在并发时,程序的执行可能会出现乱序。给人直观的感觉就是:写在前面的代码,可能会在后面执行。

保证安全的方法

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

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

多线程锁的升级原理是指当多个线程需要访问共享资源时,锁的状态会从低级别锁升级为高级别锁,以提高并发性能和保证数据一致性。

在多线程环境中,锁通常分为两种类型:共享锁和排他锁。共享锁允许多个线程同时读取共享资源,而排他锁则只允许一个线程访问共享资源。

当多个线程需要同时读取共享资源时,可以使用共享锁来提高并发性能。但是,当一个线程需要修改共享资源时,必须使用排他锁来保证数据一致性。为了实现这个目标,锁的状态会升级为更高级别的锁。

升级的过程可以通过以下步骤实现:

  1. 读锁升级为写锁:当一个线程持有共享锁并需要修改共享资源时,它会尝试将共享锁升级为排他锁。如果升级成功,那么该线程就可以修改共享资源。如果升级失败,该线程就必须释放共享锁并重新获取排他锁。
  2. 逐级升级:在某些情况下,需要将锁的级别逐级升级以满足更高的并发性能和更强的数据一致性要求。例如,可以将读锁升级为共享锁,然后再升级为排他锁。
  3. 锁的粒度:锁的升级还需要考虑锁的粒度,即锁所保护的资源的大小。锁的粒度过大会导致锁的竞争增加,降低并发性能;锁的粒度过小会增加锁的数量,也会降低并发性能。因此,需要根据实际情况选择适当的锁粒度。

总之,多线程锁的升级原理是通过升级锁的级别和粒度来提高并发性能和保证数据一致性。在实际应用中,需要根据实际情况选择适当的锁策略和粒度。

12.什么是死锁?

死锁是一组相互竞争资源的线程因为他们之间得到互相等待导致“永久“阻塞的现象; (你等我 我等你 你不放我也不放 就导致“永久“阻塞的现象)

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

当线程A持有独占锁a,并尝试去获取独占锁b的同时线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于互相特有对方需要的锁,而发生的阻塞现象,我们称为死锁。

13.怎么防止死锁?

发生死锁的原因

互斥条件 共享资源 X y 只能被一个线程占有
占用且等待 线程T1占用的共享资源X 他在等待共享Y的时候并不释放自己的X
不可抢占 其他线程不能去抢占t1线程占有的资源
循环等待 线程t1 等t2 占有的资源,线程t2 等t1占有的资源 循环等等

产生死锁的必要条件:
1、互斥条件:所谓互斥就是进程在某一时间内独占资源。
2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

如何避免死锁?
打破死锁的原因即可避免

1.加锁顺序(线程按照一定的顺序加锁)

当多个线程需要多把锁并且其中具有相同的一些锁,如果按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。

2.加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)

另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行。

3.死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

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

ThreadLocal是一种Java中的线程级别的变量,用于在多线程环境下提供线程本地变量,即为每个线程提供一个独立的变量副本,以解决线程安全问题。

ThreadLocal的使用场景如下:

  1. 线程安全性:在多线程环境下,使用ThreadLocal可以避免共享变量导致的线程安全问题。
  2. 上下文信息传递:在Web应用中,ThreadLocal可以用于在请求处理链中传递上下文信息,例如请求ID,用户信息等。
  3. 事务管理:在事务管理中,ThreadLocal可以用于存储事务上下文信息,例如数据库连接,事务隔离级别等。
  4. 缓存:ThreadLocal可以用于实现缓存,例如将频繁使用的对象或计算结果缓存到ThreadLocal变量中,以避免重复计算或频繁创建对象。
  5. 隐式参数传递:ThreadLocal可以用于实现隐式参数传递,即将参数值存储在ThreadLocal变量中,以避免在方法调用链中频繁传递参数。
  6. 并发控制:在某些情况下,ThreadLocal可以用于实现简单的并发控制,例如线程池中的任务执行次数统计等。

需要注意的是,ThreadLocal虽然可以避免线程安全问题,但是过多地使用ThreadLocal也可能导致内存泄漏和性能问题。因此,在使用ThreadLocal时需要注意内存管理和性能优化。

15.说一下synchronized底层实现原理

synchronized是Java中用于实现同步锁的关键字。它的底层实现原理可以分为两种:

  1. 基于对象监视器(Object Monitor)

Java中的每个对象都有一个与之相关联的监视器对象(Monitor Object),也称为锁对象。当一个线程想要执行被synchronized修饰的代码块时,它必须先获得锁对象,其他线程则必须等待该线程释放锁对象后才能继续执行。当线程执行完synchronized代码块后,它将自动释放锁对象,其他线程才有机会获得锁对象。

在Java虚拟机的内部实现中,每个对象都与一个Monitor对象相关联,Monitor对象中包含一个计数器和一个等待队列。当一个线程获取到锁对象时,计数器会自增1;当一个线程释放锁对象时,计数器会自减1。如果等待队列中有其他线程等待锁对象,则会从等待队列中选取一个线程获取锁对象。

  1. 基于字节码(Bytecode)指令

synchronized的底层实现还可以基于字节码指令,即monitorenter和monitorexit指令。monitorenter指令用于获取对象的锁,并将计数器加1;monitorexit指令用于释放对象的锁,并将计数器减1。在Java虚拟机中,synchronized关键字会被编译成monitorenter和monitorexit指令,以实现同步锁的功能。

需要注意的是,synchronized关键字只能用于同步方法或同步代码块,不能用于变量或普通方法。此外,在使用synchronized关键字时,需要注意避免死锁等问题,以确保程序的正确性和性能。

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

synchronized和volatile都是Java中用于实现线程安全的关键字,它们的区别如下:

  1. 作用范围不同:

synchronized关键字用于实现代码块或方法的同步,即确保在同一时刻只有一个线程可以访问某个共享资源。

volatile关键字用于修饰变量,用于保证变量的可见性和禁止指令重排。

  1. 实现方式不同:

synchronized关键字通过获取对象的锁来实现同步,其他线程必须等待锁被释放后才能继续执行。

volatile关键字通过保证变量在多线程之间的可见性和禁止指令重排来实现线程安全。

  1. 适用场景不同:

synchronized适用于需要确保同一时刻只有一个线程访问某个共享资源的场景,例如多线程操作共享变量。

volatile关键字适用于需要确保变量在多线程之间可见的场景,例如一个线程修改了共享变量的值,另一个线程需要立即看到该变量的新值。

总之,synchronized和volatile关键字都是用于实现线程安全的关键字,但是它们的作用范围、实现方式和适用场景都不同。在编写多线程程序时,需要根据具体的需求选择合适的关键字以确保程序的正确性和性能。

17.synchronized和Lock有什么区别?

synchronized和Lock都是Java中用于实现线程同步的关键字/接口,它们的区别如下:

  1. 锁的获取和释放方式不同:

synchronized是隐式锁,在进入同步代码块/方法时自动获取锁,在代码块/方法执行完成后自动释放锁。

Lock是显式锁,需要手动调用lock()方法获取锁,并手动调用unlock()方法释放锁。

  1. 支持性能和功能上的不同:

synchronized关键字在Java虚拟机层面实现,因此其性能相对较高,而且具有自动释放锁、可重入锁等功能。

Lock是Java的一个接口,提供了多种锁实现方式,例如ReentrantLock、ReentrantReadWriteLock等。Lock相对于synchronized关键字来说,更加灵活,支持超时获取锁、中断锁、公平锁、读写锁等特性。

  1. 可见性不同:

synchronized关键字不仅可以实现互斥同步,还可以保证共享变量的可见性。

Lock并不支持可见性。如果需要保证共享变量的可见性,还需要使用volatile关键字或Atomic类。

总之,synchronized和Lock都是用于实现线程同步的关键字/接口,它们在锁的获取和释放方式、性能和功能、可见性等方面有所不同。在编写多线程程序时,需要根据具体的需求选择合适的关键字/接口以确保程序的正确性和性能。

18.synchronized和ReentrantLock区别是什么?

synchronized和ReentrantLock都是Java中用于实现线程同步的关键字/类,它们的区别如下:

1.锁的获取和释放方式不同:

synchronized是隐式锁,在进入同步代码块/方法时自动获取锁,在代码块/方法执行完成后自动释放锁。ReentrantLock是显式锁,需要手动调用lock()方法获取锁,并手动调用unlock()方法释放锁。在获取锁时,ReentrantLock支持公平锁和非公平锁两种模式,而synchronized只能使用非公平锁。

2.支持性能和功能上的不同:

synchronized关键字在Java虚拟机层面实现,因此其性能相对较高,而且具有自动释放锁、可重入锁等功能。ReentrantLock 是Java的一个类,提供了多种锁实现方式,例如ReentrantLock、ReentrantReadWriteLock等。ReentrantLock相对 于synchronized关键字来说,更加灵活,支持超时获取锁、中断锁、公平锁、读写锁等特性。

3.可见性不同:

synchronized关键字不仅可以实现互斥同步,还可以保证共享变量的可见性。ReentrantLock并不支持可见性。如果需要保证共享变量的可见性,还需要使用volatile关键字或Atomic类。

4.其他差异:

synchronized关键字是Java语言的一部分,因此它可以自动地进行锁的释放和获取,而且它支持隐式锁的操作,因此比较方便。ReentrantLock是Java SE 5 中引入的新的锁机制,相对于synchronized来说,ReentrantLock提供了更高的灵活性,可以自定义锁的获取和释放,支持公平锁和非公平锁,同时也支持可重入锁。

总之,synchronized和ReentrantLock都是用于实现线程同步的关键字/类,它们在锁的获取和释放方式、性能和功能、可见性等方面有所不同。在编写多线程程序时,需要根据具体的需求选择合适的关键字/类以确保程序的正确性和性能

19.说一下atomic的原理

在多线程编程中,原子操作(atomic operation)是指一种不可被中断的操作,要么全部执行成功,要么全部不执行。在 C++ 中,使用 std::atomic 来实现原子操作。

std::atomic 的原理是使用硬件提供的原子操作指令,比如 x86 中的 LOCK 前缀指令,来保证操作的原子性。这些指令保证对内存的读写操作不会被其他线程打断,从而避免了竞态条件(race condition)的出现。

当多个线程尝试同时访问同一内存地址时,std::atomic 会通过使用硬件提供的原子操作指令来保证操作的原子性。这些指令能够在一个时钟周期内完成读写操作,并在此期间禁止其他线程对该内存地址进行访问。

在 C++11 中,std::atomic 还提供了一些内存模型(memory model)的概念,用于描述多线程环境下内存的可见性和同步问题。通过使用不同的内存模型,程序员可以灵活地控制线程之间的可见性和同步行为,从而实现高效的多线程编程。

20.synchronized的加锁流程

在Java中,synchronized有:【修饰实例方法】、【修饰静态方法】、【修饰代码块】三种使用方式,分别锁住不同的对象。这三种方式,获取不同的锁,锁定共享资源代码段,达到互斥效果,以此保证线程安全。
共享资源代码段又被称之为临界区,锁的作用就是保证临界区互斥,即同一时间临界区的只能有一个线程执行,其他线程阻塞等待,排队等待前一个线程释放锁。

1.1 修饰实例方法
作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
1.2 修饰静态方法
给当前类加锁,作用于当前类的所有对象实例,进入同步代码前要获得 当前 class 的锁。
当被static修饰时,表明被修饰的代码块或者变量是整个类的一个静态资源,属于类成员,
不属于任何一个实例对象,也就是说不管 new 了多少个对象,都只有一份.
1.3 修饰代码块
指定加锁对象,对给定对象/类加锁。
synchronized(this|object)表示进入同步代码库前要获得给定对象的锁
synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁

synchronized加锁时一般是会默认为偏向锁,可以通过jvm参数进行给关闭默认,如果默认为偏向锁就会生成一个无锁的mark work(),进行存储,cas操作的时候会有一个id用来设置头像中的markwork指向当前的线程,就会直接执行下一步的指令,如果没有就会判断是否重入,如果重入就进行添加null的锁记录,否则进行锁升级(成为重向及锁),执行下一条指令;
如果不禁用偏向时,此时会判断线程id是否和对象中线程相同,偏向锁会进行第二次获取,获取成功后会进行加锁执行下一条指令;通过epoch进行判断是否过期,如果没有过期就会进行匿名偏向,过期则进行重偏向,cas操作修改锁记录中mark偏向当前的线程,成功则加锁执行下一条指令,失败则进行匿名偏向,匿名偏向(Anonymously biased)成后就进行第一次加偏向锁,成功的将mark word线程id指向自己并加锁成功,然后指向下一条指令,当不进行匿名偏向时,就会进行偏向锁撤销,改为轻量锁,然后才会加锁成功,执行下一条指令,当成为为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

21.同步锁、死锁、乐观锁、悲观锁 (高薪常问)

同步锁: 当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。Java 中可以使用 synchronized 关键字来取得一个对象的同步锁。

死锁: 何为死锁,就是多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。

乐观锁: 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的似于 write_conditio 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 : CAS 实现的。

悲观锁: 总是假设最坏的情况,每次去拿数据时都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使 用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了 很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现

22. 线程相关的基本方法?(必会)

线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等

1.线程等待(wait) 调用该方法的线程进入 WAITING 状态,只有等待另外线程的通知或被中 断才会返回,需要注意的是调用 wait()方法后,会释放对象的锁。因此,wait 方 法一般用在同步方法或同步代码块中。

2.线程睡眠(sleep) sleep 导致当前线程休眠,与 wait 方法不同的是 sleep 不会释放当前占 有的锁,sleep(long)会导致线程进入 TIMED-WATING 状态,而 wait()方法 会导致当前线程进入 WATING 状态.

3.线程让步(yield) yield 会使当前线程让出 CPU 执行时间片,与其他线程一起重新竞争 CPU 时间片。一般情况下,优先级高的线程有更大的可能性成功竞争得到 CPU 时间片,但这又不是绝对的,有的操作系统对 线程优先级并不敏感。

4.线程中断(interrupt) 中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的 一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)

5.线程终止 (join),等待其他线程终止,在当前线程中调用一个线程的 join() 方 法,则当前线程转为阻塞状态,回到另一个线程结束,当前线程再由阻塞状态变 为就绪状态,等待 cpu 的宠幸.

6.线程唤醒(notify) Object 类中的 notify() 方法,唤醒在此对象监视器上等待的单个线程,如 果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的,并 在对实现做出决定时发生,线程通过调用其中一个 wait() 方法,在对象的监视 器上等待,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程, 被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。类 似的方法还有 notifyAll() ,唤醒再次监视器上等待的所有线程。

四: 线程池

1.什么是线程池

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处: 线程复⽤,控制最⼤并发数,管理线程

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

2. 为什么需要线程池(了解)

在实际使用中,线程是很占用系统资源的,如果对线程管理不完善的话很容易导致系统问题。因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主要有如下好处:

  1. 使用线程池可以重复利用已有的线程继续执行任务,避免线程在创建或销毁时造成的消耗
  2. 由于没有线程创建和销毁时的消耗,可以提高系统响应速度
  3. 通过线程池可以对线程进行合理的管理,根据系统的承受能力调整可运行线程数量的大小等

3. 线程池的原理

线程池工作原理: 主线程执行excute方法:

  1. 此时如果线程池的没有线程,就会创建线程来执行该任务
  2. 如果已经有线程,并且线程数小于核心线程数,会继续创建线程来执行新任务
  3. 如果线程数已经满核心线程数了,新的任务就会暂存到阻塞队列中等待执行
  4. 如果阻塞队列已经满了,则会创建新线程来执行新的任务
  5. 如果线程数已达到最大线程数,则会执行拒绝策略
  6. 拒绝策略有四种,会根据设置的来执行。

4. 拒绝策略(了解)

  1. ThreadPoolExecutor.AbortPolicy(系统默认): 丢弃任务并抛出 RejectedExecutionException 异常,让你感知到任务被拒绝了,我们可以根据业务逻辑选 择重试或者放弃提交等策略

  2. ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常,相对而 言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。

  3. ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执 行任务(重复此过程),通常是存活时间最长的任务,它也存在一定的数据丢失风险

  4. ThreadPoolExecutor.CallerRunsPolicy:既不抛弃任务也不抛出异常,而是将某些任务 回退到调用者,让调用者去执行它。

  5. 自定义拒绝策略: 通过实现RejectedExecutionHandler接口来自定义拒绝策略。

5.线程池的优势

线程池的优势主要体现在以下 4 点:

  1. 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  2. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  4. 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

6.创建线程池的方式

线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。.

池化思想在计算机的应用也比较广泛,比如以下这些:

  • 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
  • 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
  • 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。

线程池的创建方法总共有 7 种,但总体来说可分为 2 类:

  • 一类是通过 ThreadPoolExecutor 创建的线程池;(1 种是通过 ThreadPoolExecutor 创建的 核心)
  • 另一类是通过 Executors 创建的线程池。(6 种是通过 Executors 创建的)
  1. Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
  2. Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
  3. Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
  4. Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
  5. Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
  6. Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】
  7. ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置。

单线程池的意义
从以上代码可以看出 newSingleThreadExecutor 和 newSingleThreadScheduledExecutor 创建的都是单线程池,那么单线程池的意义是什么呢?
答:虽然是单线程池,但提供了工作队列,生命周期管理,工作线程维护等功能。

那接下来我们来看每种线程池创建的具体使用。

7. 线程池有哪些状态

五种状态:running(运行状态),shutdown(关闭状态),stop(停止状态),tidying(整顿状态),terminated(销毁状态)

  1. 线程处于running状态,会接收新来的任务,同时也会执行已添加的任务
  2. shutdown状态,调用threadpool.shutdown方法的时候会处于该状态,线程池处于该状态时,会拒绝新的任务,但会把已添加到对列中的任务执行完
  3. stop状态,调用threadPool.shutdownNow()方法时会处于该状态,线程池处于该状态,会拒绝新的任务进来,同时也会终止正在执行的任务,队列中的任务也不会执行
  4. tidying状态,当线程池调用了shutdown或者shutdownNow方法,线程池中的线程数变成0后,会进入该状态,即线程池中工作状态的线程数为0
  5. termenated状态, 线程池已经关闭,并且所有任务已经执行完毕。

状态转换如图

JAVA 终极面试题_第2张图片

8. 线程池的分类(高薪常问)

  1. newCachedThreadPool:创建一个可进行缓存重复利用的线程池
  2. newFixedThreadPool:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,线程池中的线程处于一定的量,可以很好的控制线程的并发量
  3. newSingleThreadExecutor : 创 建 一 个 使 用 单 个 worker 线 程 的 Executor ,以无界队列方式来运行该线程。线程池中最多执行一个线程,之后提交的线程 将会排在队列中以此执行
  4. newSingleThreadScheduledExecutor:创建一个单线程执行程序,安排在给定延迟后运行命令或者定期执行
  5. newScheduledThreadPool:创建一个线程池,它可安排在给定延迟后运行 命令或者定期的执行
  6. newWorkStealingPool:创建一个带并行级别的线程池,并行级别决定了 同一时刻最多有多少个线程在执行,如不传并行级别参数,将默认为当前系统的 CPU 个数

9.线程池中submit()和execute()方法的区别

  1. execute参数只能是Runnable的实现类 submit 参数既能是Runnable的实现类 可以是Callable的实现类
  2. execute 没有返回值 submit有返回值,并且可以通过 future 的get() 获取
  3. execute 无法处理异常 submit 可以处理异常,并且可以通过 future 的get() 打印异常堆栈信息

10. 线程池的核心参数(高薪常问)

corePoolSize:核心线程池的大小

maximumPoolSize:线程池能创建线程的最大个数

keepAliveTime:空闲线程存活时间

unit:时间单位,为 keepAliveTime 指定时间单位

workQueue:阻塞队列,用于保存任务的阻塞队列

threadFactory:创建线程的工程类

handler:饱和策略(拒绝策略)

11. 线程池的关闭(了解)

关闭线程池,可以通过 shutdown 和 shutdownNow 两个方法

原理:遍历线程池中的所有线程,然后依次中断

  1. shutdownNow 首先将线程池的状态设置为 STOP,然后尝试停止所有的正在执行和未执 行任务的线程,并返回等待执行任务的列表;
  2. shutdown 只是将线程池的状态设置为 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程

五: 反射

1.什么是反射?

  1. Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息,从而操作类或对象的属性和方法。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。
  2. Java属于先编译再运行的语言,程序中对象的类型在编译期就确定下来了,而当程序在运行时可能需要动态加载某些类,这些类因为之前用不到,所以没有被加载到JVM。通过反射,可以在运行时动态地创建对象并调用其属性,不需要提前在编译期知道运行的对象是谁。

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

序列化:将 Java 对象转换成字节流的过程。

反序列化:将字节流转换成 Java 对象的过程。

当 Java 对象需要在网络上传输 或者 持久化存储到文件中时,就需要对 Java 对象进行序列化处理。

序列化的实现:类实现 Serializable 接口,这个接口没有需要实现的方法。实现 Serializable 接口是为了告诉 jvm 这个类的对象可以被序列化。

注意事项:

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

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

动态代理:在运行时,创建目标类,可以调用和扩展目标类的方法。
Java 中实现动态的方式:JDK 中的动态代理 和 Java类库 CGLib。

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

4.怎么实现动态代理?

动态代理的实现
使用的模式:代理模式。

代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。类似租房的中介。

两种动态代理:
(1)jdk动态代理,jdk动态代理是由Java内部的反射机制来实现的,目标类基于统一的接口(InvocationHandler)
(2)cglib动态代理,cglib动态代理底层则是借助asm来实现的,cglib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。

六: 迭代器

1.什么是迭代器?

  1. Iterator对象称为迭代器,主要用于遍历Collection集合中的元素。
  2. 所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器。
  3. Iterator仅用于遍历集合,Iterator本身并不存放对象。
  4. 内部的方法:用来判断是否还有元素–hasNext(),获取集合中下一个元素,配合hashNext一起使用–next(),删除集合中的元素–remove

2.for循环与迭代器的比较

各有各的好处,就情况而论

ArrayList他的特点就是查询快,在for循环中使用get(),采用的就是随机访问,所以使用for循环更快

LinkedList底层主要是链表,链表的特点就是查询慢,是属于顺序访问,而Iterator的next方法就是顺序访问的,所以使用Iterator更快

3.Iterator与ListIterator有什么区别?

Iterator只能正向遍历集合,适用于获取移除元素。

ListIerator继承自Iterator,专门针对List,可以从两个方向遍历List,同时支持元素的修改

4. 增强for循环

增强for循环主要遍历集合和数组,它就等同于简化版本的Iterator迭代器,它的底层实现的就是Iterator迭代器

七: 锁

1.大量失败请求-自旋锁

对于用户对nginx发出的请求nginx会分配到相应的服务器如果这个服务器没加上锁那么他就会在一个while循环里一直进行(加锁,判断是否加上,等待一会,继续加锁操作)直到没有库存或者加上锁才会跳出这个循环

2.锁重入问题–可重入锁

我们都知道redis分布式锁是互斥的。假如我们对某个key加锁了,如果该key对应的锁还没失效,再用相同key去加锁,大概率会失败。

没错,大部分场景是没问题的。

为什么说是大部分场景呢?

因为还有这样的场景:

假设在某个请求中,需要获取一颗满足条件的菜单树或者分类树。我们以菜单为例,这就需要在接口中从根节点开始,递归遍历出所有满足条件的子节点,然后组装成一颗菜单树。

需要注意的是菜单不是一成不变的,在后台系统中运营同学可以动态添加、修改和删除菜单。为了保证在并发的情况下,每次都可能获取最新的数据,这里可以加redis分布式锁。

加redis分布式锁的思路是对的。但接下来问题来了,在递归方法中递归遍历多次,每次都是加的同一把锁。递归第一层当然是可以加锁成功的,但递归第二层、第三层…第N层,不就会加锁失败了?

3.读写锁

读与读是共享的,不互斥

读与写互斥

写与写互斥

4.分段锁

就是把大量请求分段就比如现在有100个请求但是呢只有5个商品这时的就会20个20个的分成五段每20个人抢一个商品就是分段锁

主从

就是当你第一台服务器加上锁之后redis突然宕机了这时候备用的redis就会启动成为主redis但是呢他里面并没有第一台服务器的数据第二台服务器就可以成功上锁解决方法就是使用zk (zookeeper) 或者使用redission 搭建五套主从redission同时向五套主从加锁两个宕机三个加上就是加锁成功四个宕机一个加上就是加锁失败 可以这样搞但是没必要太奢侈了

5.死锁

死锁是指两个或两个以上的进程在执行过程中由于相互竞争资源所造成的一种阻塞现象若无外力他们就会一直阻塞下去,这些永远相互等待的进程称之为死锁进程。

形成死锁的四个条件

  1. 互斥条件:线程对于分配到的资源具有排他性,即一个资源只能被一个线程占用,直到被该线程释放。(无法破环因为锁本来就是要让他们互斥的)

  2. 请求与保持条件:一个线程因请求被占用资源而发生阻塞时,对以获得资源保持不放。(一次性申请所有资源)

  3. 不剥夺条件:线程对于以获得的资源在未使用完之前,不能被其他线程剥夺,只能自己使用完后才会被释放。(主动释放)

  4. 循环等待条件:当发生死锁时,所等待的线程会形成一个还环路类似于死循环,会造成永久阻塞。(破坏循环等待条件)

八: 对象拷贝

1.为什么要使用克隆?

克隆的对象可能包含一些已经修改过的属性,而 new 出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的“状态”就靠克隆方法了

2.如何实现对象克隆?

有两种方式:
(1). 实现Cloneable接口并重写Object类中的clone()方法;
(2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆

3.深拷贝和浅拷贝的区别?

浅拷贝与深拷贝都可以实现在已有对象上再生出一份的作用。但是对象的实例是存储在堆内存中然后通过一个引用值去操作对象,由此拷贝的时候就存在两种情况了:拷贝引用和拷贝实例,这也是浅拷贝和深拷贝的区别。

  • 浅拷贝:浅拷贝是拷贝引用,拷贝后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响。
  • 深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。

九: JavaWeb

1.jsp和servlet有什么区别?

Servlet

一种服务器端的Java应用程序
由 Web 容器加载和管理
用于生成动态 Web 内容
负责处理客户端请求

Jsp

是 Servlet 的扩展,本质上还是 Servlet
每个 Jsp 页面就是一个 Servlet 实例
Jsp 页面会被 Web 容器编译成 Servlet,Servlet 再负责响应用户请求

区别

Servlet 适合动态输出 Web 数据和业务逻辑处理,对于 html 页面内容的修改非常不方便;Jsp 是在 Html 代码中嵌入 Java 代码,适合页面的显示
内置对象不同,获取内置对象的方式不同

2.jsp有哪些内置区别?作用分别是什么?

  1. application: application对象代表应用程序上下文,它允许JSP页面与包括在同一应用程序中的任何Web组件共享信息。

  2. config: Config对象允许将初始化数据传递给一个JSP页面。

  3. Exception: Exception对象含有只能由指定的JSP“错误处理页面”访问的异常数据。

  4. out: Out对象代表提供输出流的访问。

  5. page: Page对象代表JSP页面对应的Servlet类实例

  6. PageContext: PageContext对象是Jsp页面本身的上下文,它提供唯一一组方法来管理具有不同作用域的属性。

  7. request: Request对象提供对Http请求数据的访问,同时还提供用于加入特定请求数据的上下文

  8. response: Response对象允许直接访问HttpServletResponse对象

  9. session: Session对象可能是状态管理上下文中使用最多的对话

3.说一下jsp的四种作用域

Web交互的最基本单位为HTTP请求。每个用户从进入网站到离开网站这段过程称为一个HTTP会话,一个服务器的运行过程中会有多个用户访问,就是多个HTTP会话。作用域解释如下。

application 作用域

如果把变量放到application里,就说明它的作用域是application,它的有效范围是整个应用。 整个应用是指从应用启动,到应用结束。我们没有说“从服务器启动,到服务器关闭”,是因为一个服务器可能部署多个应用,当然你关闭了服务器,就会把上面所有的应用都关闭了。 application作用域里的变量,它们的存活时间是最长的,如果不进行手工删除,它们就一直可以使用。

application作用域上的信息传递是通过ServletContext实现的,它提供的主要方法如下所示:

Object getAttribute(String name) //从application中获取信息;

void setAttribute(String name, Object value) //向application作用域中设置信息。

session作用域

session作用域比较容易理解,同一浏览器对服务器进行多次访问,在这多次访问之间传递信息,就是session作用域的体现。如果把变量放到session里,就说明它的作用域是session,它的有效范围是当前会话。所谓当前会话,就是指从用户打开浏览器开始,到用户关闭浏览器这中间的过程。这个过程可能包含多个请求响应。也就是说,只要用户不关浏览器,服务器就有办法知道这些请求是一个人发起的,整个过程被称为一个会话(session),而放到会话中的变量,就可以在当前会话的所有请求里使用。
session是通过HttpSession接口实现的,它提供的主要方法如下所示:

Object HttpSession.getAttribute(String name) //从session中获取信息。

void HttpSession.setAttribute(String name, Object value)//向session中保存信息。

HttpSession HttpServletRequest.getSessio() //获取当前请求所在的session的对象。

session的开始时刻比较容易判断,它从浏览器发出第一个HTTP请求即可认为会话开始。但结束时刻就不好判断了,因为浏览器关闭时并不会通知服务器,所以只能通过如下这种方法判断:如果一定的时间内客户端没有反应,则认为会话结束。Tomcat的默认值为120分钟,但这个值也可以通过HttpSession的setMaxInactiveInterval()方法来设置:

void setMaxInactiveInterval(int interval)

如果想主动让会话结束,例如用户单击“注销”按钮的时候,可以使用 HttpSession 的 invalidate()方法,用于强制结束当前session:void invalidate()

request作用域

一个HTTP请求的处理可能需要多个Servlet合作,而这几个Servlet之间可以通过某种方式传递信息,但这个信息在请求结束后就无效了。request里的变量可以跨越forward前后的两页。但是只要刷新页面,它们就重新计算了。如果把变量放到request里,就说明它的作用域是request,它的有效范围是当前请求周期。 所谓请求周期,就是指从http请求发起,到服务器处理结束,返回响应的整个过程。在这个过程中可能使用forward的方式跳转了多个jsp页面,在这些页面里你都可以使用这个变量。

Servlet之间的信息共享是通过HttpServletRequest接口的两个方法来实现的:

void setAttribute(String name, Object value) //将对象value以name为名称保存到request作用域中。

Object getAttribute(String name) //从request作用域中取得指定名字的信息。

JSP中的doGet()、doPost()方法的第一个参数就是HttpServletRequest对象,使用这个对象的 setAttribute()方法即可传递信息。那么在设置好信息之后,要通过何种方式将信息传给其他的Servlet呢?这就要用到RequestDispatcher接口的forward()方法,通过它将请求转发给其他Servlet。

RequestDispatcher ServletContext.getRequestDispatcher(String path) //取得Dispatcher以便转发,path为转发的目的Servlet。

void RequestDispatcher.forward(ServletRequest request, ServletResponse response)//将request和response转发

因此,只需要在当前Servlet中先通过setAttribute()方法设置相应的属性,然后使用forward()方法进行跳转,最后在跳转到的Servlet中通过使用getAttribute()方法即可实现信息传递。

需要注意两点:

  1. 转发不是重定向,转发是在Web应用内部进行的。

  2. 转发对浏览器是透明的,也就是说,无论在服务器上如何转发,浏览器地址栏中显示的仍然是最初那个Servlet的地址。

page作用域

page对象的作用范围仅限于用户请求的当前页面,对于page对象的引用将在响应返回给客户端之后被释放,或者在请求被转发到其他地方后被释放。page里的变量只要页面跳转了,它们就不见了。如果把变量放到pageContext里,就说明它的作用域是page,它的有效范围只在当前jsp页面里。从把变量放到pageContext开始,到jsp页面结束,你都可以使用这个变量。

以上介绍的作用范围越来越小,request和page的生命周期都是短暂的,它们之间的区别:一个request可以包含多个page页(include,forward及filter)。

4.session和cookie有什么区别?

  1. 保存的位置不同: cookie保存在浏览器端,session保存在服务端。
  2. 使用方式不同: cookie如果在浏览器端对cookie进行设置对应的时间,则cookie保存在本地硬盘中,此时如果没有过期,则就可以使用,如果过期则就删除。如果没有对cookie设置时间,则默认关闭浏览器,则cookie就会删除。
    session:我们在请求中,如果发送的请求中存在sessionId,则就会找到对应的session对象,如果不存在sessionId,则在服务器端就会创建一个session对象,并且将sessionId返回给浏览器,可以将其放到cookie中,进行传输,如果浏览器不支持cookie,则应该将其通过encodeURL(sessionID)进行调用,然后放到url中。
  3. 存储内容不同:cookie只能存储字符串,而session存储结构类似于hashtable的结构,可以存放任何类型
  4. 存储大小:``cookie最多可以存放4k大小的内容,session则没有限制。
  5. session的安全性要高于cooKie
  6. cookie的session的应用场景:cookie可以用来保存用户的登陆信息,如果删除cookie则下一次用户仍需要重新登录
    session就类似于我们拿到钥匙去开锁,拿到的就是我们个人的信息,一般我们可以在session中存放个人的信息或者购物车的信息。
  7. session和cookie的弊端:cookie的大小受限制,cookie不安全,如果用户禁用cookie则无法使用cookie。如果过多的依赖session,当很多用户同时登陆的时候,此时服务器压力过大。sessionId存放在cookie中,此时如果对于一些浏览器不支持cookie,此时还需要改写代码,将sessionID放到url中,也是不安全。

5.session的工作原理

Session(会话)是指客户端与服务器之间的一次交互过程,通常包含多个请求和响应。在 Web 应用程序中,服务器端会为每个客户端会话创建一个 Session 对象,用于存储客户端的状态信息,如登录状态、购物车内容等。Session 的工作原理如下:

  1. 客户端向服务器发送请求,请求中包含一个唯一的 Session ID,该 ID 可以通过 Cookie、URL 参数或者隐藏表单域的形式发送。
  2. 服务器接收到请求后,根据 Session ID 查找或创建 Session 对象,并将该对象的状态信息保存在服务器端的内存或者持久化存储中,如 Redis、数据库等。
  3. 服务器将 Session ID 返回给客户端,通常使用 Cookie 的形式返回。客户端在之后的每个请求中都会自动携带该 Cookie。
  4. 客户端再次向服务器发送请求时,服务器从请求中获取 Session ID,查找相应的 Session 对象,从而获得客户端的状态信息,并根据该信息生成响应。同时,服务器也会更新 Session 对象中的状态信息,以便下次请求时使用。
  5. 当会话结束时,服务器将 Session 对象从内存或者持久化存储中删除,释放资源。如果使用 Cookie 存储 Session ID,客户端会在一定时间后自动删除该 Cookie。

总的来说,Session 的工作原理就是客户端与服务器之间传递一个唯一的标识符 Session ID,通过该标识符实现客户端的状态信息的持久化和共享。这种机制使得 Web 应用程序可以跟踪客户端的状态,提供个性化服务和定制化内容。

6.如果客户端禁止cookie 能实现session还能用吗?

如果客户端禁止了 Cookie,就无法通过 Cookie 来存储和传递 Session ID,但是仍然可以通过其他方式来实现 Session。

一种常见的方法是将 Session ID 添加到 URL 参数中,即每个请求中都包含一个类似于 sessionid=xxxx 的参数。这种方式可以确保服务器能够正确地识别客户端,并从服务器端的存储中获取对应的 Session 对象。不过这种方式有一些缺点,如增加了 URL 的长度,容易泄漏 Session ID 等敏感信息,同时也可能导致缓存和搜索引擎等问题。

另外一种方法是使用隐藏表单域(hidden form field)来存储 Session ID。当客户端发送表单请求时,可以将 Session ID 存储在表单的一个隐藏域中,从而传递给服务器。这种方式可以避免在 URL 中传递敏感信息,但是需要注意表单的安全性和可用性。

除此之外,还有一些其他的方法,如使用 HTTP 首部、IP 地址、SSL 客户端证书等来传递 Session ID,但是这些方法都需要考虑安全性和可用性的问题。

总的来说,虽然禁用 Cookie 会对 Session 的实现产生一些影响,但是仍然可以通过其他方式来实现 Session。但是需要考虑这些方式的安全性、可用性、扩展性等问题,以便为用户提供更好的体验和保护用户的隐私。

7.Spring mvc 和struts的区别是什么?

Spring MVC 和 Struts 是两个常用的 Java Web 框架,它们有以下主要区别:

  1. 架构设计:Spring MVC 是基于 Spring 框架的 MVC 模式,而 Struts 是基于 Apache Struts 框架的 MVC 模式。Spring MVC 是一个轻量级的框架,它可以使用 Spring 框架的所有特性,包括依赖注入和面向切面编程(AOP)。相比之下,Struts 是一个较重量级的框架,它的架构相对更为复杂。
  2. 配置方式:Spring MVC 配置相对简单,可以使用 Java 配置或 XML 配置文件。而 Struts 需要使用 XML 配置文件来定义 Action 和页面。
  3. 可扩展性:Spring MVC 框架提供了良好的扩展性,可以方便地与其他框架整合。而 Struts 的扩展性相对较差,需要使用一些第三方扩展来支持与其他框架的整合。
  4. 表单验证:Spring MVC 的表单验证功能是通过注解实现的,更加灵活。而 Struts 的表单验证需要使用 XML 配置文件来实现。
  5. 性能:由于 Spring MVC 是一个轻量级框架,相对于 Struts,其性能更好。但在大型应用程序中,这种性能差异可能并不明显。

总的来说,Spring MVC 更加灵活和可扩展,而 Struts 则相对更加稳定和成熟。选择哪一个框架取决于项目的需求和开发人员的经验。

8.如何避免sql注入?

SQL注入是一种常见的网络攻击方式,攻击者试图通过恶意的SQL代码将数据库操作转移到他们自己的控制下,从而获得敏感信息。以下是一些防止SQL注入的常用方法:

  1. 使用参数化查询:使用参数化查询语句可以防止SQL注入攻击。参数化查询是一种使用占位符(如?)来代替SQL查询中的变量的技术。在执行查询之前,占位符会被替换为安全的输入值。使用预编译语句可以大大降低SQL注入的风险。
  2. 对用户输入进行验证和过滤:对于任何从用户输入中获取的数据,都需要进行验证和过滤,以确保数据的完整性和安全性。例如,可以使用正则表达式或编写自定义验证器来验证输入数据。
  3. 不要使用动态SQL语句:避免使用动态生成的SQL语句,因为这会让应用程序变得更加容易受到SQL注入攻击。相反,尽可能使用静态SQL语句。
  4. 使用ORM框架:使用ORM框架(如Hibernate、MyBatis等)可以大大减少SQL注入的风险。ORM框架自动处理参数化查询和其他安全性问题。
  5. 限制数据库用户权限:在配置数据库用户时,应该尽可能限制其访问权限,以确保只能执行其需要的操作,这样可以降低SQL注入攻击的风险。
  6. 更新数据库软件和补丁:定期更新数据库软件和补丁,可以帮助防止SQL注入攻击。这些更新通常会修复数据库软件中发现的漏洞,包括可能被攻击者利用的漏洞。

总之,为了防止SQL注入攻击,应该采用多种方法,包括使用参数化查询、验证和过滤用户输入、限制数据库用户权限等等。

9.什么是XSS攻击,如何避免?

XSS攻击(跨站脚本攻击)是一种常见的Web攻击方式,攻击者通过注入恶意的JavaScript代码来窃取用户信息或者绕过安全策略。以下是一些避免XSS攻击的常用方法:

  1. 对用户输入进行过滤和验证:在应用程序接受用户输入时,需要进行过滤和验证,以防止恶意脚本被注入。可以使用编码或者过滤特殊字符来保护应用程序免受XSS攻击。
  2. 使用HTTPOnly标志:将HTTPOnly标志设置为true可以防止JavaScript访问cookie。这样,即使攻击者成功注入恶意脚本,也无法访问受害者的cookie。
  3. 在客户端对输入进行编码:在客户端对输入进行编码可以保护用户输入不被恶意脚本注入。例如,在使用富文本编辑器或Markdown编辑器时,需要在客户端对输入内容进行转义或过滤。
  4. 在服务器端对输出进行编码:在输出到Web页面时,需要确保将所有数据都进行编码,以防止XSS攻击。例如,在显示用户输入的评论时,需要将评论内容进行HTML编码,以防止注入恶意脚本。
  5. 防止DOM Based XSS攻击:DOM Based XSS攻击是一种特殊的XSS攻击方式,攻击者利用DOM操作直接修改了Web页面的结构和内容,绕过了传统的XSS防御策略。为了防止DOM Based XSS攻击,应该在客户端使用安全的DOM操作方法,同时在服务器端对输出进行编码。
  6. 使用Content Security Policy(CSP):Content Security Policy是一种安全机制,可以帮助防止XSS攻击。CSP定义了哪些内容可以被加载到Web页面上,从而限制了恶意脚本的执行。

总之,要防止XSS攻击,需要在客户端和服务器端都采取相应的防御措施,包括过滤和验证用户输入、在客户端和服务器端都对输出进行编码、使用HTTPOnly标志、防止DOM Based XSS攻击等。

10.什么是CSRF攻击,如何避免?

CSRF攻击(Cross-Site Request Forgery)是一种利用用户在已登录的网站的身份执行恶意操作的攻击。攻击者可以通过伪造请求让用户在不知情的情况下执行某些操作,比如更改密码、转账等。以下是一些避免CSRF攻击的常用方法:

  1. 使用CSRF Token:为了防止CSRF攻击,可以在Web页面中生成一个随机的Token,并在每次请求时将这个Token发送给服务器。这样,服务器可以检查这个Token是否与请求匹配,以确定请求是否来自合法的源。
  2. 使用SameSite Cookie:SameSite Cookie是一种新的Cookie属性,可以限制Cookie只在同一站点的请求中发送。将Cookie设置为SameSite可以有效防止CSRF攻击。
  3. 检查Referer头:Referer头是HTTP协议中的一个字段,用来表示请求的来源地址。服务器可以检查Referer头是否为当前站点的地址,以确定请求是否来自合法的源。
  4. 在关键操作中添加二次确认:为了防止误操作或者恶意操作,可以在关键操作中添加二次确认步骤,例如输入验证码或者输入密码等。
  5. 在请求中添加时间戳:为了防止CSRF攻击者重复发送请求,可以在请求中添加时间戳,如果请求的时间戳和服务器当前时间相差过大,则认为请求是非法的。

总之,要防止CSRF攻击,需要采取多种措施,包括使用CSRF Token、使用SameSite Cookie、检查Referer头、在关键操作中添加二次确认等。需要注意的是,这些措施不是绝对安全的,攻击者可以通过不断尝试绕过这些措施。因此,对于重要的安全操作,最好使用多种方法进行验证和授权,以确保用户的安全和数据的安全。

十: 异常

1.throw 和 throws 的区别?

1、throw代表动作,表示抛出一个异常的动作; throws代表一种状态,代表方法可能有异常抛出。
2、throw用在方法实现中,而throws用在方法声明中。
3、throw只能用于抛出一种异常,而throws可以抛出多个异常。

2.final,finally和finalize有什么区别?

final、finally和finalize是Java中的三个关键字,它们的含义和用法有所不同。

final是Java中的一个修饰符,可以用于类、方法和变量上。用于修饰一个类时,表示该类不能被继承;用于修饰一个方法时,表示该方法不能被重写;用于修饰一个变量时,表示该变量只能被赋值一次。final修饰的变量必须在声明时或者在构造函数中进行初始化,而且不能被修改。

finally是Java中的一个关键字,用于定义在try语句块和catch语句块之后执行的代码块。无论try语句块中是否发生异常,finally语句块都会被执行。通常在finally块中释放资源,比如关闭文件或者数据库连接等。

finalize是Java中的一个方法,是由垃圾回收器在销毁一个对象之前调用的。在对象被销毁时,finalize方法会被自动调用,用于清理资源或者执行一些特定的操作。finalize方法只会被调用一次,并且是在对象被销毁之前调用的。

因此,final、finally和finalize这三个关键字的含义和用法不同,需要根据具体的语境进行理解和使用。

3.try-catch-finally中哪个部分可以省略?

在 Java 中,try 和 catch 两个部分都是可选的,但是 try 和 catch 至少要有一个存在。finally 块也是可选的,可以省略。

try 块中包含可能会引发异常的代码。如果在 try 块中引发了异常,Java 将跳过 try 块的余下部分,转而查找与该异常匹配的 catch 块。如果找到了匹配的 catch 块,则其中的代码将被执行,然后程序将继续执行 catch 块之后的代码。

如果没有找到匹配的 catch 块,则该异常将被传递到更高层的 try 块(如果有的话),或者传递给调用代码。如果没有任何代码处理该异常,则程序将崩溃,并显示错误消息。

finally 块用于包含在任何情况下都必须执行的代码,例如清理资源或关闭文件。即使在 try 块中发生异常,finally 块中的代码也将被执行。

因此,虽然在某些情况下可以省略 catch 块或 finally 块,但是为了确保代码的健壮性和可维护性,最好编写包含 try、catch 和 finally 块的完整异常处理代码。

4.try-catch-finally中,如果catch中return了,fially还会执行吗?

如果在 catch 块中使用了 return 语句,那么 finally 块中的代码也将被执行。无论是否在 catch 块中使用了 return 语句,finally 块中的代码都将在 try 块中的代码执行完毕之后被执行。

finally 块中的代码是用于清理资源或者执行必须的收尾工作的,例如关闭数据库连接、释放文件句柄等。无论在 try 块中发生了什么,finally 块中的代码都应该被执行,以确保程序能够正常结束并且不会出现资源泄漏等问题。

需要注意的是,在 catch 块中使用 return 语句会导致在异常处理时提前退出方法,并且 finally 块中的代码会在返回之前被执行。因此,在使用 catch 块中的 return 语句时,应该确保 finally 块中的代码不会影响返回值的计算,以避免潜在的错误。

5. RunTimeException和其他Exception的区别

RuntimeException:是运行时异常: 如 : NullPointerException 、 ClassCastException ;当出现这些异常时,可以不用程序员进行处理,不影响程序的鲁棒性。
其他Exception:一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,或者是throw到上一层进行处理。

6. 常见的异常类有哪些?

  1. NullPointerException(空指针异常):当尝试在一个 null 引用上调用实例方法或字段时抛出。
  2. IndexOutOfBoundsException(数组下标越界异常):当尝试访问数组、列表或字符串中不存在的索引时抛出。
  3. IllegalArgumentException(非法参数异常):当传递给方法的参数不合法时抛出。
  4. IllegalStateException(非法状态异常):当对象处于不正确的状态下调用方法时抛出。
  5. ArithmeticException(算术异常):当发生除以零或取模为零的算术运算时抛出。
  6. ClassCastException(类型转换异常):当尝试将一个对象转换为不兼容的类型时抛出。
  7. NoSuchMethodException(方法不存在异常):当尝试访问一个不存在的方法时抛出。
  8. FileNotFoundException(文件不存在异常):当尝试打开不存在的文件时抛出。
  9. IOException(输入输出异常):当发生输入输出错误时抛出。
  10. SQLException(SQL 异常):当与数据库交互时出现错误时抛出。
  11. ConcurrentModificationException(并发修改异常):当尝试在迭代集合时修改集合时抛出。
  12. IllegalArgumentException(非法参数异常):当传递给方法的参数不合法时抛出。
  13. IllegalStateException(非法状态异常):当对象处于不正确的状态下调用方法时抛出。
  14. NoSuchElementException(无元素异常):当尝试从空的集合或迭代器中获取元素时抛出。
  15. NumberFormatException(数字格式异常):当尝试将字符串转换为数字时,如果字符串格式不正确,则抛出该异常。
  16. SecurityException(安全异常):当尝试违反安全性限制时抛出。
  17. UnsupportedOperationException(不支持的操作异常):当尝试执行不支持的操作时抛出,例如对不可修改的集合执行添加操作。
  18. ParseException(解析异常):当尝试解析字符串时,如果字符串的格式不正确,则抛出该异常。
  19. SocketException(套接字异常):当套接字操作失败时抛出。
  20. TimeoutException(超时异常):当尝试等待超时时抛出。

十一: 网络

1.http响应码301和302代表的是什么?有什么区别?

HTTP响应码301和302都是重定向状态码,它们告诉客户端浏览器需要到另一个URL地址去请求资源,但它们的含义稍有不同。

HTTP响应码301表示永久重定向。这意味着所请求的资源已经被永久地移动到了另一个URL地址,以后所有对该资源的请求都应该使用新的URL地址。搜索引擎也会更新其索引以反映此更改。301重定向应该在以下情况下使用:当网站更改其域名或URL结构时,或者当页面永久性移动到新的URL地址时。

HTTP响应码302表示暂时重定向。这意味着所请求的资源只是暂时被移动到了另一个URL地址,以后对该资源的请求应该继续使用原始URL地址。搜索引擎不会更新其索引以反映此更改。302重定向应该在以下情况下使用:当网站正在进行维护时,或者当页面只是暂时地移动到新的URL地址时。

总的来说,301重定向适用于永久更改,而302重定向适用于暂时更改。

2.forward和redirect的区别?

Forward和Redirect是在web开发中常用的两种跳转技术,它们的区别如下:

  1. Forward(转发)是在服务器内部进行的跳转,也称为服务器端跳转,它是一种服务器将请求直接转发给目标页面的技术,用户的浏览器并不知道这个过程,它只认为自己访问的是目标页面。而Redirect(重定向)则是在客户端进行的跳转,也称为客户端跳转,它是一种服务器返回一个HTTP响应,告诉浏览器需要跳转到另一个URL,浏览器再去请求新的URL。
  2. Forward跳转是单次请求,请求转发到目标页面后,目标页面无法获取原请求的参数信息;而Redirect跳转是两次请求,浏览器会发送两次请求,第一次请求会得到服务器返回的重定向响应,第二次请求会访问重定向后的URL。
  3. Forward跳转速度快,因为它是在服务器内部进行的跳转,不需要浏览器重新发送请求;而Redirect跳转速度慢,因为它需要两次请求,而且浏览器需要等待服务器返回重定向响应后才能再次发送请求。
  4. Forward跳转地址栏不会改变,而Redirect跳转会改变地址栏。
  5. Forward跳转只能在同一个Web应用程序中跳转,而Redirect跳转可以在不同的Web应用程序中跳转。

因此,如果要在服务器内部跳转,传递一些参数,而且不需要改变地址栏的话,可以使用Forward;如果需要在客户端进行跳转,而且需要改变地址栏,可以使用Redirect。

(从区别来看:

  1. forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来然后把这些内容再发给浏览器。浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址
  2. redirect是服务器根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址。所以地址栏显示的是新的URL。所以redirect等于客户端向服务器端发出两次request,同时也接受两次response。

从原理上看:

  1. Forward(直接转发方式)用的更多一些,一般说的请求转发指的就是直接转发方式。Web应用程序大多会有一个控制器。由控制器来控制请求应该转发给那个信息资源。然后由这些信息资源处理请求,处理完以后还可能转发给另外的信息资源来返回给用户,这个过程就是经典的MVC模式。
  2. Redirect(间接转发方式),有时也叫重定向,它一般用于避免用户的非正常访问。例如:用户在没有登录的情况下访问后台资源,Servlet可以将该HTTP请求重定向到登录页面,让用户登录以后再访问。

从工作流程上看:

  1. forword过程:客户浏览器发送http请求—>web服务器接受此请求—>调用内部的一个方法在容器内部完成请求处理和转发动作—>将目标资源 发送给客户。
  2. redirect过程:客户浏览器发送http请求—>web服务器接受后发送302状态码响应及对应新的location给客户浏览器—>客户浏览器发现 是302响应,则自动再发送一个新的http请求,请求url是新的location地址—>服务器根据此请求寻找资源并发送给客户。

从运用的地方上看:

  1. forword 一般用于用户登录的时候,根据角色转发到相应的模块;
  2. redirect一般用于用户注销登录时返回主页面或者跳转到其他网站。

**从效率上看:**forword效率高,而redirect效率低。 )

TCP/IP协议分为哪四层,具体作用是什么?

  1. 应用层:应用程序间沟通的层,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。
  2. 传输层:在此层中,它提供了节点间的数据传送,应用程序之间的通信服务,主要功能是数据格式化、数据确认和丢失重传等。如传输控制协议(TCP)、用户数据报协议(UDP)等,TCP和UDP给数据包加入传输数据并把它传输到下一层中,这一层负责传送数据,并且确定数据已被送达并接收。
  3. 互连网络层:负责提供基本的数据封包传送功能,让每一块数据包都能够到达目的主机(但不检查是否被正确接收),如网际协议(IP)。
  4. 网络接口层(主机-网络层):接收IP数据报并进行传输,从网络上接收物理帧,抽取IP数据报转交给下一层,对实际的网络媒体的管理,定义如何使用实际网络(如Ethernet、Serial Line等)来传送数据。

3.简述tcp和udp的区别?

TCP (Transmission Control Protocol) 和 UDP (User Datagram Protocol) 是两种常用的网络传输协议。

TCP 是一种面向连接的协议,它在传输数据前,需要先建立连接、确认收到数据和重传数据等步骤,确保数据的可靠性和有序性。TCP 适用于需要高可靠性、顺序性和流量控制的应用,比如网页浏览、电子邮件、文件传输等。

UDP 是一种无连接的协议,它直接将数据包发送给目标地址,不需要建立连接和确认收到数据,因此传输速度快,但是数据传输的可靠性较差。UDP 适用于需要高速传输、实时性和简单性的应用,比如视频和音频流、在线游戏等。

总的来说,TCP 和 UDP 适用于不同的应用场景。TCP 适合对可靠性和有序性要求较高的应用,UDP 适合对实时性和传输速度要求较高的应用。

4.tcp为什么要三次握手,两次不行吗?为什么?

TCP 采用三次握手建立连接的原因是确保双方的通信能力、网络连通性和数据传输的可靠性。

在 TCP 的三次握手过程中,首先客户端向服务器发送 SYN 报文段,表示客户端请求建立连接,并选择一个随机的初始序列号。服务器接收到 SYN 报文段后,向客户端发送 SYN+ACK 报文段,表示服务器收到了请求,并确认双方的通信能力和网络连通性,同时服务器也选择了一个随机的初始序列号。最后,客户端再向服务器发送 ACK 报文段,表示客户端收到了服务器的确认,连接建立成功。

采用三次握手的好处在于,可以避免以下情况的发生:

  1. 防止已经失效的连接请求报文段突然又传送到了服务端,因而产生错误。
  2. 防止连接请求报文段迟到而被错误地当成新的连接请求报文段,从而产生错误。
  3. 防止服务端因延迟而发出的确认报文段在客户端已经失效的连接上出现,从而产生错误。

如果采用两次握手建立连接,那么在第二次握手时,服务器将无法确定客户端是否收到了第一次握手时发送的 SYN 报文段。如果客户端没有收到 SYN 报文段,那么客户端会重新发送 SYN 报文段,这样就可能导致服务端收到了两个 SYN 报文段,产生连接的错误。因此,为了确保可靠性,TCP 采用三次握手建立连接。

5.说一下tcp粘包是怎么产生的?哪些情况下会发生粘包现象

TCP 粘包是指在数据传输过程中,多个数据包粘在一起发送或接收的现象。产生 TCP 粘包的原因:

  1. 发送端的应用程序发送数据太快:如果发送端的应用程序发送数据的速度过快,会导致多个数据包在短时间内一起发送到接收端,从而产生 TCP 粘包的现象。
  2. 网络传输过程中的延迟:如果在网络传输过程中,某个数据包的到达时间延迟,那么后续的数据包就会在此时一起到达,从而产生 TCP 粘包的现象。
  3. 接收端的应用程序接收数据不及时:如果接收端的应用程序不能及时接收到数据,那么多个数据包就会堆积在接收端的缓冲区中,从而产生 TCP 粘包的现象。

发生粘包的情况:

  1. 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据很小,会合到一起,产生粘包)
  2. 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了-段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

TCP 粘包的产生对于接收端的应用程序来说是不可避免的,因为在网络传输过程中,数据包的到达时间和数量是不可预测的。因此,为了避免 TCP 粘包产生的影响,接收端的应用程序通常需要进行数据包的拆分和重组,以保证数据的完整性和正确性。而发送端的应用程序可以通过限制数据发送的速度或者设置数据包的长度等方式,来减少 TCP 粘包的产生。

6.OSI的七层模型都有哪些?

OSI 的七层模型包括以下七层:

  1. 物理层(Physical Layer):负责数据的物理传输,包括电器、光学、物理特性等,实现数据的比特流传输。
  2. 数据链路层(Data Link Layer):负责在物理层上传输数据的时候,提供介质访问和链路管理,以及错误检测和纠正等功能。
  3. 网络层(Network Layer):负责为不同的主机提供逻辑上的连接,以及实现数据的分组、路由和转发等功能。
  4. 传输层(Transport Layer):负责在网络层上传输数据的时候,提供端到端的可靠数据传输、错误检测和纠正、流量控制等功能。
  5. 会话层(Session Layer):负责建立、管理和终止会话,以及实现数据同步、流量控制、数据复制等功能。
  6. 表示层(Presentation Layer):负责对数据进行加密、压缩、解压、解密等处理,以及实现数据的格式转换和数据的表示和转义等功能。
  7. 应用层(Application Layer):负责为用户提供各种应用服务,包括电子邮件、文件传输、远程登录、网络管理等功能。

7.get和post的请求有那些区别?

GET 和 POST 是 HTTP 协议中常用的两种请求方法,它们之间的区别如下:

  1. 参数位置:GET 请求的参数会附加在 URL 后面,而 POST 请求的参数会放在请求体中。
  2. 参数大小:GET 请求的参数大小有限制,一般不能超过 2048 个字符,而 POST 请求没有参数大小的限制。
  3. 参数安全性:GET 请求的参数是以明文形式传递的,容易被截获,因此不适合传递敏感信息,而 POST 请求的参数是以密文形式传递的,相对来说更加安全。
  4. 参数类型:GET 请求的参数只能是 ASCII 码,而 POST 请求的参数可以是任意类型,包括二进制数据。
  5. 缓存:GET 请求会被浏览器缓存,而 POST 请求不会被浏览器缓存。
  6. 幂等性:GET 请求是幂等的,即多次请求同一个 URL,得到的结果是一样的,不会产生副作用,而 POST 请求是非幂等的,即多次请求同一个 URL,得到的结果可能不同,会产生副作用。
  7. 使用场景:GET 请求适用于查询操作,POST 请求适用于提交数据和修改数据等操作。

需要注意的是,GET 和 POST 请求的区别只是它们传递数据的方式不同,并不是说 GET 请求不能修改数据,也不是说 POST 请求不能查询数据,根据需要选择适合的请求方法。

8.如何实现跨域?

跨域:当浏览器执行脚本时会检查是否同源,只有同源的脚本才会执行,如果不同源即为跨域。

这里的同源指访问的协议、域名、端口都相同。
同源策略是由 Netscape 提出的著名安全策略,是浏览器最核心、基本的安全功能,它限制了一个源中加载脚本与来自其他源中资源的交互方式。
Ajax 发起的跨域 HTTP 请求,结果被浏览器拦截,同时 Ajax 请求不能携带与本网站不同源的 Cookie。

你可能感兴趣的:(面试题,java,面试,java,后端,mysql)