1.Java基础
1.1 Java最基础
1.java语言的特点是什么?
- java相比于C/C++等语言,去除了一些复杂的语法简单易学;
- 面向对象的编程语言(封装,继承,多态);
- 平台无关性( Java 虚拟机实现平台无关性);
- 编译与解释并存;
- 支持多线程
- 可靠性、安全性,java在编译的过程中就能够检测出一些错误,而有些语言需要在执行时才能检测出来。
- 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便);
2.JVM vs JDK vs JRE解释
JVM是 Java 虚拟机 ,它的基本作用是运行java字节码,即.class文件,JVM针对不同的操作系统的有特定实现 ,它的最终目的是实现跨平台性,即同样的java字节码在不同的操作系统java虚拟机上有相同的执行结果。
JDK是功能齐全的 Java SDK ,是整个java的核心,它其实包含了JRE,还包含了编译器javac和其他的一些工具 , 它能够创建和运行java程序
JRE是运行时环境,运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,其他的一些基础构件。但是,它不能用于创建新程序。
3.Java代码运行的整个流程是什么?
首先 Java 开发环境中进行程序代码的输入,最终会生成形成后缀名为 .java 的 Java 源文件。 然后通过 Java 编译器对源文件进行错误排査的过程,编译后将生成后缀名为 .class 的字节码文件 。最后通过 Java 解释器将字节码文件翻译成机器代码,执行并显示结果。 (java解释器只是JVM的一部分)
4.java与c++的区别?
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存。C++需要
- C++与java都是面向对象
5.字符型常量和字符串常量的区别?
- 形式 : 字符常量是单引号引起的一个字符,字符串常量是引起的 0 个或若干个字符。
- 含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。
- 占内存大小 : 字符常量只占 2 个字节; 字符串常量占若干个字节。
6.静态方法为什么不能调用非静态成员?
- 静态方法是属于类的,在类加载的时候就会分配内存;非静态成员属于实例对象,只有在对象实例化之后 才会分配内存,相当于静态方法早非静态成员一步,因此在内存中根本找不到非静态成员,因此在语法上这种行为也是不合法的,必须调用静态成员。
7.静态方法的与实例方法有何不同?
1.调用方式: 在外部调用静态方法时,可以使用 类名.方法名
的方式, 也可以使用实例化对象,用对象名加方法名的方式调用(但是不推荐),而实例方法只能用后面这种方法
2.访问本类成员时:静态方法只允许访问静态变量,不能访问 一般实例成员,实例方法没有这个限制。
3.分配内存:静态方法在在类加载的时候就会分配内存,实例方法是在实例化后才分配的内存
8.重载和重写的区别?
重载 : 发生在同一个类中(或者父类和子类之间),方法名必须相同,参数不同:类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 即:同样的一个方法能够根据输入数据的形式不同,做出不同的处理 。
重写 :发生在子类和父类之间, 对父类的允许访问的方法进行重新编写 的过程,重写时有一个规则, 两同两小一大 : 方法名相同、参数相同 ; 子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等 ; 访问权限应比父类方法的访问权限更大或相等。
9.基本数据类型和对应的包装类型的区别?
1.在成员变量中,基本类型有默认值,包装类型没有,为null;
2.包装类型可用于泛型,而基本类型不可以。
3.基本数据类型的局部变量存放在 Java 虚拟机栈中,基本数据类型的成员变量(未被 static
修饰 )存放在 Java 虚拟机的堆中。包装类型属于对象类型,我们知道几乎所有对象实例都存在于堆中。
4.相比于对象类型, 基本数据类型占用的空间非常小。
10.包装类型的缓存机制了解么?
当包装类加载时 ,会初始化 一个包装类类型数组 -128 ~127到缓存中,当用自动装箱的方法创建一个包装类对象时,它会先看是否在上述数组范围类,如果在就直接返回缓存中对象,而不去新建一个对象。
Byte,
Short,
Integer,
Long` 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据
创建一个包装类对象有两种方法:
(1)构造器方法(就是new出来);都会返回一个新对象。
(2)自动装箱(就是编译器自动调用包装类的valueOf方法);(才会有一个缓存机制) Integer i1 = 40; 等价于 Integer i1= Integer.valueOf(40)
11.自动装箱与拆箱了解吗?原理是什么?
- 装箱:将基本类型用它们对应的引用类型包装起来; Integer i = 10 ,等价于 Integer i = Integer.valueOf(10)
- 拆箱:将包装类型转换为基本数据类型; int n = i
等价于
int n = i.intValue()`;
12.精度丢失问题?为什么会存在这种问题?
因为浮点数在计算机中的保存方式是二进制的,而且计算机在表示一个数字时,宽度是有限的 ,然后有些浮点类型的小数用二进制表示可能是无线循环的,因此会被截断,就会导致精度发生损失,从而计算出错。
解决办法:用 BigDecimal 。
13.面向过程与面向对象的区别?
面向过程就是分析出解决问题所需要的步骤,拆分为一个一个的方法,一步一步实现。 面向对象会把构成问题的实物抽象成一个一个的对象, 然后用对象执行方法的方式解决问题。 面向对象开发的程序一般更易维护、易复用、易扩展 。
对象引用与对象实例: 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。
14.对象的相等和引用相等的区别
- 对象的相等一般比较的是内存中存放的内容是否相等。
- 引用相等一般比较的是他们指向的内存地址是否相等。
15.系统会设置默认的构造方法、但是一旦手动设置,便不会有默认的构造方法了。构造方法可以重载不能重写
16.面向对象的三大特征:封装、继承以及多态是什么?
封装: 将对象的一些属性隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。 比如get、set
继承:不同类型的对象,相互之间经常有一定数量的共同点。,但有一些差异,我们可以让这些对象继承一个父类,父类的方法大家都拥有,并可以重写一些方法,同时可以新增一些方法属性进行扩展。这样可以提高代码的复用。
多态: 同一个行为具有多个不同表现形式或形态的能力,比如说同一个接口,可以有不同的类去实现它,或者父类可以有多个子类去继承,通常:表现为父类的引用指向子类的实例。
17.接口和抽象类有什么共同点和区别?
共同点:
- 都不能被实例化。
- 都可以包含抽象方法。(方法体中没有内容)
区别:
- 一个类只能继承一个抽象类,但是可以实现多个接口。
- 接口只能定义方法名,不能有方法的实现 ,即方法体, 而抽象类既可以定义没有方法体的方法,也可以定义有方法体的方法。
- 接口强调特定功能的实现,而抽象类强调所属关系。
- 接口成员变量默认为 public static final,必须赋初值,不能被修改;其所有的成员方法都是 public、abstract 的抽象方法。抽象类中成员变量默认 default,可在子类中被重新定义,也可被重新赋值
18.深拷贝与浅拷贝是什么?以及引用拷贝。
浅拷贝 和深拷贝都会在堆上创建一个新的对象 ;
浅拷贝 :它会拷贝原对象内部所有属性,但是如果内部属性是引用类型,会直接拷贝该引用。即地址,即两对象是不一样的但其内部的某个属性是指向同一个对象的。
深拷贝 : 深拷贝会完全复制整个对象,包括这个对象所包含的引用类型 ,即内部的引用类型会重新新建一个对象,进行拷贝。
19.== 和 equals() 的区别
- 对于基本数据类型来说,== 比较的是值。
- 对于引用数据类型来说,== 比较的是对象的内存地址。
- equals()不能用于判断基本数据类型,只能判断引用类型,在object类中其实现的方法还是==,因此如果其子类没有重写equals方法,比较两个对象时,是比较引用,即地址,如果重写了equal方法,一般是比较两个对量内部属性是否相等
-
hashCode
hashCode()
的作用是获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。 该方法通常用来将对象的内存地址转换为整数之后返回 ,hashCode()
和equals()
都是用于比较两个对象是否相等 。 这是因为在一些容器(比如HashMap
、HashSet
)中,有了hashCode()
之后,判断元素是否在对应容器中的效率会更高 , 两个对象的hashCode
值相等并不代表两个对象就相等 (哈希碰撞)- 如果两个对象的
hashCode
值相等,那这两个对象不一定相等(哈希碰撞)。 - 如果两个对象的
hashCode
值相等并且equals()
方法也返回true
,我们才认为这两个对象相等。 - 如果两个对象的
hashCode
值不相等,我们就可以直接认为这两个对象不相等。
- 如果两个对象的
-
String、StringBuffer、StringBuilder 的区别?
String
是不可变的 , 可以理解为常量,它是线程安全的。StringBuilder
与StringBuffer
都是继承的同一个类, 提供了一些操作字符串的方法,StringBuffer
对方法加了同步锁 ,所以是线程安全的,而StringBuilder
未加锁,所以是非线程安全的。- 操作少量的数据: 适用
String
,其实用String的+来操作字符串底层仍然是StringBuilder
,但是如果频繁用这种方式操作字符串时,就会频繁地创建对象和释放对象,是对资源的浪费。 - 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
- 操作少量的数据: 适用
-
final
final
关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象 -
字符串常量池是什么?
首先字符串的创建一般有两种方式: 字面量和new 一个对象,当采用字面量的方式时,它会在字符串常量池看是否有先要创建的这个值,如果有就直接返回常量池中的这个对象的引用,不存在就在常量池中新建一个对象。当采用new 一个对象这种方式,他也会看字符串常量池中是否有这个字符串,因为构造方法中里面的也相当于是一个字符串字面值有就直接引用,没有则创建。然后在堆中创建一个对象。
但是不管哪种方式,字符串对象都是常量,不可改变的(即使改变也是生成新的字符串对象,原来的字符串还是没变的)。
当创建 String
类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有 就把它赋给当前引用。如果没有就在常量池中重新创建一个 String
对象。
- Exception 和 Error 有什么区别?
这两个类其实都是继承同一个父类; Exception
:程序本身可以处理的异常,可以通过 catch
来进行捕获。 Error
属于程序无法处理的错误 这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
- 受检查异常和不受检查异常?
受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch
或者throws
关键字处理的话,就没办法通过编译 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
- 什么是泛型?有什么作用?
是 JDK 5 中引入的一个新特性。泛型的本质是参数化类型,在新建某个对象时,填入泛型参数类型,然后该对象内部会以该类型来处理。可以用在类,接口、和方法中,分别被称为泛型类、泛型接口、泛型方法 ;使用泛型参数,可以增强代码的可读性以及稳定性。 通过泛型参数可以指定传入的对象类型 ,如果不是指定类型就会报错,增加了安全性,使用泛型可以在泛型参数里填入不同的对象类型,因此也提高了代码复用性,不用单独去重新写一个类。
27.项目中哪些地方用到了泛型?
1.查询数据库返回多个实体类时,用了ArrayList的泛型
2.Excel处理类
ExcelUtil用于动态指定
Excel` 导出的数据类型
3.集合处理
28.什么是反射、以及有什么作用
通过反射机制可以操作字节码文件 ; 赋予了我们在运行时分析类以及执行类中方法的能力。通过反射可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
优点: 可以让代码更加灵活、为各种框架提供开箱即用的功能提供了便利 ,比如 Spring/Spring Boot、MyBatis 等框架。
缺点: 让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。 反射的性能也要稍差点
29.什么是注解
注解也是一种新特性, 可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,起到说明和配置的作用
。它和注释不同的是,它是有代码实现的,会存放一些信息。然后后面可以通过反射机制将这些信息提取以供使用。
30.java序列化。(1)什么是序列化与反序列化?(2)哪些场景用到序列化和反序列化?(3)序列化协议对应于 TCP/IP 4 层模型的哪一层?(应用层)(4)常见序列化协议
有时,我们需要将java对象持久化,比如存储在硬盘或者内存中、或者通过网络通信,都需要使用序列化。其实序列化就是将对象转换为二进制的字节流过程,相反,反序列化就是将 序列化过程中所生成的二进制字节流转换成对象的过程
场景:1.网络传输, 比如远程方法调用 RPC 的时候 ,。。。。
2.将对象存储到文件中的时候需要进行序列化,将对象从文件中读取出来需要进行反序列化。
3. 将对象存储到缓存数据库(如 Redis)时需要用到序列化,将对象从缓存数据库中读取出来需要反序列化
序列化协议一般有基于二进制和基于文本,基于二进制的比如Protobuf、hessian基于文本的比如json、XML
31.java代理模式
代理模式是一种比较好理解的设计模式。简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
1.2 集合
1.说说 List, Set, Queue, Map 四者的区别?
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。Set
(注重独一无二的性质): 存储的元素是无序的、不可重复的。Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
2.List是一个子接口,有实现类
ArrayList
:Object[]
数组Vector
:Object[]
数组LinkedList
: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
3.Set
HashSet
(无序,唯一即不可重复): 基于HashMap
实现的,底层采用HashMap
来保存元素LinkedHashSet
:LinkedHashSet
是HashSet
的子类,并且其内部是通过LinkedHashMap
来实现的。有点类似于我们之前说的LinkedHashMap
其内部是基于HashMap
实现一样,不过还是有一点点区别的TreeSet
(有序,唯一): 红黑树(自平衡的排序二叉树)
4.queue
PriorityQueue
:Object[]
数组来实现二叉堆ArrayQueue
:Object[]
数组 + 双指针
5.map
HashMap
: JDK1.8 之前HashMap
由数组+链表组成的,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间LinkedHashMap
:LinkedHashMap
继承自HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》open in new windowHashtable
: 数组+链表组成的,数组是Hashtable
的主体,链表则是主要为了解决哈希冲突而存在的TreeMap
: 红黑树(自平衡的排序二叉树)
6.如何选用集合?
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 Map
接口下的集合,需要排序时选择 TreeMap
,不需要排序时就选择 HashMap
,需要保证线程安全就选用 ConcurrentHashMap
。 当我们只需要存放元素值时,就选择实现Collection
接口的集合,需要保证元素唯一时选择实现 Set
接口的集合比如 TreeSet
或 HashSet
,不需要就选择实现 List
接口的比如 ArrayList
或 LinkedList
,然后再根据实现这些接口的集合的特点来选用。
7.有数组为什么还要用集合
数组满足不了各种各样的数据类型,比如说键值对这种类型;而且数组一旦声明之后,长度就不可变了,而集合是可变的,提高了数据存储的灵活性;数组存储的数据是有序的、可重复的,特点单一,满足不了特定的场景。 Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据。
-----------------------------------------------------------一些集合类的区别--------------------------------------------------------------
8.ArrayList 和 Vector 的区别?
ArrayList
是List
的主要实现类,底层使用Object[ ]
存储,适用于频繁的查找工作,线程不安全 ;Vector
是List
的古老实现类,底层使用Object[ ]
存储,线程安全的
9.ArrayList 与 LinkedList 区别?
- 是否保证线程安全:
ArrayList
和LinkedList
两者都是线程不安全的 - 底层数据结构:
ArrayList
底层使用的是Object
数组;LinkedList
底层使用的是 双向链表 数据结构( - 插入和删除是否受元素位置的影响:
ArrayList
采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。但是如果要在中间插入和删除元素的话(add(int index, E element)
)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。LinkedList
采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影响如果是要在指定位置i
插入和删除元素的话因为需要先移动到指定位置再插入。但不许要移位操作
- 是否支持快速随机访问:
ArrayList
可以通过序号快速访问到指定元素,而LinkedList
不支持这种方式,只能逐个遍历。 - 内存空间占用:
ArrayList
的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
10.HashSet、LinkedHashSet 和 TreeSet 三者的异同
HashSet
、LinkedHashSet
和TreeSet
都是Set
接口的实现类,都能保证元素唯一,并且都不是线程安全的。HashSet
、LinkedHashSet
和TreeSet
的主要区别在于底层数据结构不同。HashSet
的底层数据结构是哈希表(基于HashMap
实现)。LinkedHashSet
的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。
11.HashMap 和 Hashtable 的区别**?**
1.都实现了map接口
Hashtable
是线程安全的, 内部的方法基本都经过synchronized
修饰 ;HashMap
是非线程安全的- 效率上,
HashMap
要比Hashtable
效率高一点 ,Hashtable
基本被淘汰 - 对null的支持,hashMap支持空键和空值,为确保键的唯一性,只能有一个空键,但可以有多个空值。而HashTable不支持空键和空值
- 初始容量和扩容机制不一样,
Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。 - 底层的数据结构:
HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
12.HashMap 和 HashSet 区别 HashSet
底层就是基于 HashMap
实现的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cw6fXGO4-1658470405220)(C:\Users\pc\AppData\Roaming\Typora\typora-user-images\1658217158731.png)]
13.在多线程并发情况下尽量不使用 HashMap ,而使用 ConcurrentHashMap ,这是线程安全的
14.一些方法:List list = Arrays.asList(s); 数组转集合 转换后不能用add remove等方法,因此可以这样写List list = new ArrayList<>(Arrays.asList(“a”, “b”, “c”)) s=list.toArray(new String[0]);集合转数组。new String[0]相当于一个模板
1.3 IO
- 什么是IO?
输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。I/O 描述了计算机系统与外部设备之间通信的过程。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流**。 (以内存为中心) 。**
常见磁盘io 网络io
InputStream
/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream
/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
2.字节输入流InputStream(它是一个抽象类) ,用于从源头(通常是文件)读取数据(字节信息)到内存中 , 是所有字节输入流的父类 ,比如用于读文件的 FileInputStream 是其子类
3.OutputStream
用于将数据(字节信息)写入到目的地(通常是文件),java.io.OutputStream
抽象类是所有字节输出流的父类。 FileOutputStream
是最常用的字节输出流对象
4.为什么 I/O 流操作要分为字节流操作和字符流操作呢** ?
文件流输入输出一些字符时,可能由于操作不当导致乱码的问题,于是 I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
5.字节缓冲流
IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。字节缓冲流这里采用了装饰器模式来增强 InputStream
和OutputStream
子类对象的功能。
6.IO设计模式
装饰器模式, 装饰器(Decorator)模式 可以在不改变原有对象的情况下拓展其功能。 BufferedInputStream ,装饰类不直接继承需要增强的类,而是继承该类的抽象类或者是接口,于是不需要给那个接口或者抽象类的每一个子类添加增强类。
7.应用程序怎么调用io的?
为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。
当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接进行IO操作;从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。**
当应用程序发起 I/O 调用后,会经历两个步骤:内核等待 I/O 设备准备好数据;内核将数据从内核空间拷贝到用户空间。
8.阻塞和挂起是什么?
阻塞(block)因某资源没有满足的原因不能继续运行,从而交出当前 CPU 的使用权而暂停的一种状态。而当之前缺少的该资源被满足之后,该进程将被解除阻塞而逐步恢复之前的运行状态。
挂起(suspend)是指当前进程发生了内存等共享资源的紧急不足,或者由于用户的意愿,现阶段不需要运行等原因,处于一种不接受操作系统调度的状态。阻塞与之的区别在于,当之前缺少的该资源被满足之后,处于阻塞状态的进程将自动被解除现在的暂停运行的状态,这个自动的过程是由操作系统设法完成的。但对于处于挂起状态的进程来说,当前暂停运行的状态不会自动被解除,除非用户主动对其进行激活。
9.什么是同步和异步?什么是阻塞与非阻塞
- 同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
- 异步: 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
- 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
- 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
9**.BIO(同步阻塞),什么是BIO**?
BIO是传统的io,是同步阻塞的IO模型,在向内核发出IO相关请求时,如果数据没准备好,就会一直准备在嘛,比如说用java写socket通信的程序,有个accept如果没有客户端连进来就会一直阻塞在那,就算有,在读的时候,就是将内核空间拷贝到用户空间这段时间也会阻塞。因此解决办法就是一个连接对应一个线程, 客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销 。这种模式可以用于较小的架构,并且对服务器性能有严格要求。
10.同步非阻塞I/O: 同步非阻塞 IO 模型中,不会等到返回结果采取执行下一步,他可以一只调用io请求、但是在等待数据从内核空间拷贝到用户空间的这段时间里,这么一小段时间里其实线程依然是阻塞的,直到在内核把数据拷贝到用户空间。
11.什么是NIO?
**NIO是一种面向缓冲的,基于通道的I/O操作方法。 它是基于多路复用的I/O模型,在 IO 多路复用模型中,线程首先发起 select 调用(或者epoll),询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起I/O操作的调用, 减少无效的系统调用,减少了对 CPU 资源的消耗。(read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的 ),还有一个重要的特点是,多路复用有一个选择器,或者叫多路复用器,以socket通信举个简单的例子,有socket客户端连进来,先把他们放进一个列表里面。然后多路复用器,它会以单个线程的方式不断的轮询select/epoll系统调用所负责的成百上千的通道,如果该数据准备好了,就返回。而不是一来就直接去进行读写操作。**https://www.cnblogs.com/crazymakercircle/p/10225159.html
( NIO是一种多路复用的I/O模型,在Java 1.4 中引入了NIO框架 ,javaAPI里面其实是支持阻塞和非阻塞模式的,但一般都用它的非阻塞模式,提高性能。它的基本原理:)
它最大的特点就是非阻塞、同时引入了三个核心的组件缓冲、通道以及选择器。
NIO流是非阻塞的, NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情 。 Java BIO的各种流是阻塞的。这意味着,当一个线程调用 read()
或 write()
时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了
它有三个核心的组件 缓冲、通道、以及选择器
12.AIO
AIO是异步IO模型, 异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
1.4 并发编程、多线程
1.什么是线程?什么是进程?有什么区别?
进程: 是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 可以理解为一个在内存中运行的应用程序, 每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
线程: 是操作系统能够进行运算调度的最小单位。可以看成是进程中的一个执行任务, 一个进程至少有一个线程,一个进程 在其执行的过程中可以产生多个线程 ,多个线程可共享数据。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
线程与进程的区别: 线程具有许多传统进程所具有的特征 ,他可以看成是进程的一个个的任务,通常一个进程有多个线程组成。。区别: ①进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位 ②进程与进程之间有独立的代码和数据空间,进程切换开销大,进程相当于是一个容器,包含了多个线程,线程与线程之间共享一些资源,比如 堆和方法区 ,也自己的独立的资源比如 程序计数器、虚拟机栈 和 本地方法栈 。
2.程序计数器为什么是私有的?
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置
3.虚拟机栈和本地方法栈为什么是私有的?
-
虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
-
本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
-
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
4.线程的生命周期和状态
5.什么是上下文切换?
在cpu中,出现有些情况,会从占有cpu的状态退出,,比如调用了sleep()和wait()等,主动退出,或者,时间片用完, 因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死 。调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。,线程退出cpu, 要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。 (上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。 )
6.什么是线程死锁?如何避免死锁?
一个线程A正在占用一个资源,但是他想获取另一个资源,而另一个线程B正在占用着这个资源,同时他也想获取A正在占用的资源,但是它们只有获取了对方的资源过后才会释放锁,因此它们就会在那里一直等待,就导致死锁的现象。如何避免: 避免死锁就是在资源分配时,比如说调用另一资源前,要先释放已经有的资源,不要抱着不放。
借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
7.说说 sleep() 方法和 wait() 方法区别和共同点?
- 两者最主要的区别在于:
sleep()
方法没有释放锁,而wait()
方法释放了锁 。 - 两者都可以暂停线程的执行。
wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行。wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()
或者notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)
超时后线程会自动苏醒。
8.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
其实多线程是基于静态代理模式实现的, 调用 start()
方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start()
会执行线程的相应准备工作,然后自动执行 run()
方法的内容,这是真正的多线程工作。
9.synchronized关键字了解吗?
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized
关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
10.加锁是给对象加锁,不是给代码,有这个锁才能执行代码块,执行完毕后就会释放锁
修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁 (相当于默认给实例对象加锁)
修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,
修饰代码块 :指定加锁对象,对给定对象/类加锁。synchronized(this|object)
表示进入同步代码库前要获得给定对象的锁。synchronized(类.class)
表示进入同步代码前要获得 当前 class 的锁
synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是是给 Class 类上锁。synchronized
关键字加到实例方法上是给对象实例上锁。
11.什么是线程安全?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期是一样的,这就是线程安全。就是不提供数据访问保护,有可能出现多个线程先后更改数据造成数据不一致或者数据污染。
在多个线程同时访问同一个对像时会发生数据错误,不完整等情况,那就是线程不安全,不会发生以上错误时是线程安全的。一个线程访问资源的时候为其加锁,别的线程只有等到该线程释放资源后才能使用,这样做为了防止数据的非正常改变和使用
- 双重检验锁方式实现单例模式 为什么两次校验
提高效率, 第一个这个是为了代码提高代码执行效率,由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。 第二个是初始化时校验,防止重复创建、
13.volatile关键字 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。
为什么会造成数据的不一致: 当前的 Java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。 而volatile关键字, 就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
14.并发编程的三个重要特性
指令重排的问题!多线程下会存在问题
- 原子性 : 一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。
synchronized
可以保证代码片段的原子性。 - 可见性 :当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
volatile
关键字可以保证共享变量的可见性。 - 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。
volatile
关键字可以禁止指令进行重排序优化。
15.synchronized 关键字和 volatile 关键字的区别
volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好 。但是volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块 。volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性。
16.线程池是什么?为什么要用线程池或者是线程池的好处是什么?
线程池其实就是一种多线程处理形式 ,如果用传统的thread、runable去创建线程,就会平凡地创建和销毁线程,是对资源的浪费,于是就会引入线程池,它的主要思想就是: 进程开始时创建一定数量的线程,并加到池中以等待工作。 当有任务时, 它会唤醒池内的一个线程,然后将需要的服务传递给他,完成任务后,线程又回到池中等待工作。同时线程池可以设定线程池基本大小,就是就算没有任务,也会有这么多个线程在那等待任务。也可以设置最大线程数,就是任务过多时,它会在基本大小的基础上创建线程,但不能超过设定的最大线程数,同时对于这种刚创建的这种类型的线程设定存活时间,等待时间过长,且无任务执行就会销毁该线程
并且可以设定线程数量,处理过程会将每个任务放在一个队列中,先进先出。
1.5JVM
1.5.1 内存区
1.介绍下 Java 内存区域(运行时数据区)
一般会把java内存区域分为两个大的部分,即线程共享的区域和线程不共享的区域,即每一个线程独有的区域。
线程独有的区域包括程序计数器、虚拟机栈以及本地方法栈。 程序计数器其实就是一个指针,它指向了我们程序中下一句需要执行的指令 ,同时在线程切换时,线程切换回来,能够通过程序计数器恢复到正确的执行位置 。其次是java虚拟机栈:主要满足一些方法的调用, 每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。 满足先进后出的栈的数据结构。(栈帧包括 局部变量表、操作数栈、动态链接、方法返回地址。 局部变量表 主要存放了编译期可知的各种数据类型 ; 操作数栈 主要用于存放方法执行过程中产生的中间计算结果; 动态链接 主要服务一个方法需要调用其他方法的场景 );最后一个是本地方法栈: 和虚拟机栈所发挥的作用非常相似 ,只是本地方法栈服务的是native方法。
线程共有的区域主要包括:堆和方法区,堆在虚拟机启动时创建 ,堆的目的主要是 存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。 堆也是垃圾回收的主要区域,由垃圾分代收集的算法,可以将堆分为新生代和老年代(和永久代,新生代可具体细分为Eden区、survivor1和survivor2区)方法区则是 主要的作用是存放类的元数据信息,比如常量和静态变量、 方法信息 ···等。当它存储的信息过大时,会在无法满足内存分配时报错 。
补充: 方法区和永久代以及元空间是什么关系呢? 方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间 。
整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。
字符串常量池是放在堆中里的,最开始是放在永久区里面的
2.对象的创建过程
2.Java文件是如何被运行的?
首先将我们编写的java代码进行编译生成.class文件,然后在通过jvm在执行,基本过程先把这个class文件装进类加载器。接着就是类加载过程—最后执行和卸载
3.执行代码的具体流程
编译好代码成class 文件,系统会启动一个JVM进程,将类信息加载到运行时数据区的方法区内,进行一个类加载的过程。然后jvm会找到主程序的入口,执行main方法,在main方法中可能会实例化一些对象,然后会将对应的类又加载到方法区,并在堆中为该实例分配内存,再调用构造函数初始化实例。执行方法时, JVM 根据 student 的引用找到 student 对象,然后根据 student 对象持有的引用定位到方法区中 student 类的类型信息的方法表,获得 sayName() 的字节码地址 。
需要知道对象实例初始化时会去方法区中找类信息,完成后再到栈那里去运行方法。找方法就在方法表中找
1.5.3 类加载过程
1.类的生命周期
加载-连接-初始化-使用-卸载,连接又包括验证-准备-解析
2.类加载的过程
加载-验证-准备-解析-初始化
加载:需要做三件事:通过全类名获取定义此类的二进制字节流;将 字节流所代表的静态存储结构转换为方法区的运行时数据结构 ;在内存中生成一个代表该类的 Class 对象, 作为方法区这些数据的访问入口
验证:其目的是确保被加载类的正确性 , 保证这些信息被当作代码运行后不会危害虚拟机自身的安全。 验证主要是验证文件格式class文件规范,版本号是否在规定范围类、常量池类型是否支持等等,然后还有验证元数据,对字节码描述的信息进行语义分析,确保符合java语言规范。还有验证字节码。最复杂的一个阶段。通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。比如保证任意时刻操作数栈和指令代码序列都能配合工作。最后是符号引用验证,确保解析动作正确执行。
准备: 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配 。但是只分配类变量而不分配实例变量, 实例变量会在对象实例化时随着对象一块分配在 Java 堆中 。
解析: 虚拟机将类中的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行 ; 符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的, 肯定是要直接引用的。
初始化: 初始化阶段是执行初始化方法 (<clinit> )
方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码 ; 遇到new,getstatic,putstatic,invokestatic这4条指令;等会初始化
3.卸载
卸载类即该类的 Class 对象被 GC。 卸载需要三个要求
- 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
- 该类没有在其他任何地方被引用
- 该类的类加载器的实例已被 GC
4.所有的类都由类加载器加载,加载的作用就是将 .class
文件加载到内存。
5.JVM 中内置了三个重要的 ClassLoader (类加载器)
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现,负责加载
%JAVA_HOME%/lib
目录下的 jar 包和类或者被-Xbootclasspath
参数指定的路径中的所有类。 - ExtensionClassLoader(扩展类加载器) :主要负责加载
%JRE_HOME%/lib/ext
目录下的 jar 包和类,或被java.ext.dirs
系统变量所指定的路径下的 jar 包。 - AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
6.双亲委派模型
每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 loadClass()
处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader
中。当父类加载器无法处理时,才由自己来处理。当父类加载器为 null 时,会使用启动类加载器 BootstrapClassLoader
作为父类加载器。
( 其实这个双亲翻译的容易让别人误解,我们一般理解的双亲都是父母,这里的双亲更多地表达的是“父母这一辈”的人而已,并不是说真的有一个 Mother ClassLoader 和一个 Father ClassLoader 。另外,类加载器之间的“父子”关系也不是通过继承来体现的,是由“优先级”来决定。)
7.双亲委派模型的好处