Java面试自用简洁版

Posted qq_40707462

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java面试自用简洁版相关的知识,希望对你有一定的参考价值。

目录

一、Java基础

1、编译型与解释型

编译型 :编译型语言 会通过编译器 将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。

解释型 :解释型语言 会通过解释器 一句一句的将代码解释为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、javascriptphp 等等。

Java:既是编译性语言(需要由编译器编译为.class字节码文件),又是解释性语言(需要由JVM读一行执行一行,由解释器解释为操作系统能执行的命令)

Java的编译器是javac.exe,解释器是java.exe

2、基本数据类型

引⽤数据类型:数组、类、接口

基本数据类型存放在 Java 虚拟机中的局部变量表中,而包装类型属于对象类型,存在于中。

所有整型包装类对象之间值的比较,全部使用 equals 方法比较。

3、==与equals

对象相等:比的是内存中存放的内容是否相等。
引用相等:比较的是他们指向的内存地址是否相等。

==: 基本数据类型(八种基本数据类型)比较的是,引用数据类型(数组、类、接口)比较的是内存地址

equals() : 只能判断引⽤数据类型,若没有覆盖equals,则等价于 ==;若覆盖了,大多数情况比如string,integer,则是比较内容

string:虚拟机在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象

String x = "string";
String y = "string";
String z = new String("string");
System.out.println(x==y); // true,引用相同
System.out.println(x==z); // false,==:string比较引用,开辟了新的堆内存空间,所以false
System.out.println(x.equals(y)); // true,equals:string:比较值,相同
System.out.println(x.equals(z)); // true,equals:string比较值,相同

hashcodeequals:
获取哈希码(散列码),返回int,确定对象在哈希表中的索引位置,任何类都有hashcode()函数。

如果hashcode相同,再调用equals()检查两个对象是否真的相同,否则hashset不会让其加入成功,这样减少了equals使用次数,提高就执行速度。

两个对象相同,则有相同的hashcode;反之hashcode相同,对象不一定相等。所以要重写equals时必须重写hashcode()函数,如果没有重写 hashCode(),则该 class的两个对象⽆论如何都不会相等。

4、浅拷贝、深拷贝、引用拷贝

5、StringBuilderStringBuffer

都继承自 AbstractStringBuilder 类,没有用 final 关键字修饰,所以这两种对象都是可变的。String 类中使用 final 关键字修饰,不可变。

StringBuffer 是线程安全的(加了同步锁)。 StringBuilder 是非线程安全的。

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String对象。
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

  1. 操作少量的数据: 适用 String
  2. 单线程大量数据: 适用 StringBuilder
  3. 多线程大量数据: 适用 StringBuffer

6、接口与抽象类

  1. 抽象类要被子类继承, 接口要被类实现,一个类可以实现多个接口,但只能实现一个抽象类。
  2. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。
  3. 接口方法默认修饰符是 public,只能有static、 final 变量;抽象方法可以有 public、 protected 和 default 这些修饰符(为了被重写所以不能使用 private )
  4. 抽象类里可以没有抽象方法;如果一个类里有抽象方法, 那么这个类只能是抽象类。抽象方法只能申明,不能实现。abstract void abc(),不能写成abstract void abc()。

7、重载和重写

重写:
1.重写发生在父类与子类之间,是运行时的多态性
2.方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同
3.访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)
4.不可被重写:final,static,private

重载:
1.重载Overload发生在一个类中,是编译时的多态性
2.重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序),返回值类型可以相同也可以不相同。

8、static和final

static修饰的方法:
1、父类中的静态方法可以被继承、但不能被子类重写。
2、如果在子类中写一个和父类中一样的静态方法,那么该静态方法由该子类特有,两者不构成重写关系。
3、static修饰的属性的初始化在编译期(类加载的时候),初始化后能改变。跟该类的具体对象无关

final修饰:
1、修饰类表示不允许被继承。
2、修饰方法表示不允许被子类重写,但是可以被子类继承,不能修饰构造方法。一个类中的private方法会隐式地被指定为final方法。
3、修饰变量表示不允许被修改
a)方法内部的局部变量,使用前被赋值即可(只能赋值一次),没有必要非得初始化。
b)类中的成员变量(如果没有在定义时候初始化,那么只能在构造代码块中或者构造方法中赋值)
c)基本数据类型的变量(初始化赋值之后不能更改)
d)引用数据类型的变量(初始化之后不能再指向另外一个对象,但对象的内容是可以变的)

9、异常

Throwable 类有两个重要的子类 Exception(异常)和 Error(错误)。

  • Exception :程序本身可以处理的异常,可以通过 try-catch 来进行捕获。Exception 又可以分为 受检查异常(必须处理否则无法通过编译 ) 和 不受检查异常(可以不处理)。
  • Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

try-catch-finally:
无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行,且会覆盖之前的返回值。

除非: 在异常语句之前System.exit(int);线程死亡;关闭CPU

10、I/O

I/O

  • 序列化: 将对象转换成可取用格式(例如二进制字节流,存成文件,存于缓冲,或经由网络中发送)的过程
  • 反序列化:将在序列化过程中所生成的二进制字节流转换成对象的过程

对于不想进行序列化的变量,使用 transient 关键字修饰。 当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。

  • transient 只能修饰变量,不能修饰类和方法。
  • transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0。
  • static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。

I/O 模型:

我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。会经历两个步骤:

  • 内核等待 I/O 设备准备好数据
  • 内核将数据从内核空间拷贝到用户空间。

Java 中 3 种常见 IO 模型:BIO,NIO,AIO

BIO (Blocking I/O): 同步阻塞

应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。

NIO (Non-blocking/New I/O):同步非阻塞

NIO 基于通道(Channel)和缓存区(Buffer),对于高负载、高并发的(网络)应用,应使用 NIO 。可以直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,显著提升性能。

NIO提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。

阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

NIO 通过轮询操作,避免了一直阻塞。应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

IO 多路复用模型中:线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。

在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多。

AIO (Asynchronous I/O):异步

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。

当用户线程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个 asynchronous read 之后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它 read 操作完成了。

用户线程只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了

同步IO操作会引起进程阻塞直到IO操作完成。
异步IO操作不引起进程阻塞。

11、反射

指在运行状态中,动态获取信息以及动态调用对象方法的功能

  • 对于任意一个类都能够知道这个类所有的属性和方法;
  • 对于任意一个对象,都能够调用它的任意一个方法;

对象在运行是都会出现两种类型:编译时类型和运行时类型。编译时类型由声明对象时使用的类型来决定,运行时的类型由实际赋值给对象的类型决定 。比如编译时根本无法预知该对象和类属于哪些类,程序只能靠运行时信息来发现该对象和类的信息,那就要用到反射了。

反射最大的用途就是框架,比如:Spring中的Di/IoC、Hibernate中的find(Class clazz)、Jdbc中的classForName()、SpringBoot的Service注解等,很多开发框架都用到了反射机制

优点:
1)能够运行时动态获取类的实例,提高灵活性;
2)与动态编译结合
缺点:
1)使用反射性能较低,需要解析字节码,将内存中的对象进行解析。

获取class对象的三种方式:

// 这一new 产生一个Student对象,一个Class对象。*
Student student = new Student(); 
// 调用某个类的 class 属性来获取该类对应的 Class 对象
Class studentClass2 = Student.class; 
// 使用 Class 类中的 forName() 静态方法 ( 最安全 / 性能最好 )
Class studentClass3 = Class.forName("com.reflect.Student") 

12、泛型

泛型的原理就是“类型的参数化”,即把类型看作参数。也就是说把所有要操作的数据类型看作参数,就像方法的形式参数是运行时传递的值的占位符一样。

好处:
①类型安全。泛型的主要目标是实现java的类型安全。 泛型可以使编译器知道一个对象的限定类型是什么,这样编译器就可以在一个高的程度上验证这个类型
②消除了强制类型转换。使得代码可读性好,减少了很多出错的机会
③安全简单。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

原理:

泛型的实现是靠类型擦除技术。类型擦除是在编译期完成的,在编译期,编译器会将泛型的类型参数都擦除成它的限定类型,如果没有,则擦除为object类型,之后在获取的时候再强制类型转换为对应的类型。 在运行期间并没有泛型的任何信息,因此也没有优化。

二、集合

Collection: List、SetQueue
Map: hashmap、hashtable、hashtree

1、List,Set,Queue,Map

List(有序):

存储的元素是有序的、可重复的。(可以有多个元素引⽤相同的对象)

  • Arraylist: Object[] 数组
  • Vector:Object[] 数组(线程安全)
  • LinkedList: 双向链表

Arraylist线程不安全解决:

  1. 使用VectorArrayList所有方法加synchronized,太重)。
  2. 使用java.concurrent.CopyOnWriteArrayList(推荐)。是JUC的类,通过写时复制来实现读写分离。比如其add()方法,就是先复制一个新数组,长度为原数组长度+1,然后将新数组最后一个元素设为添加的元素。

Arraylist扩容:
当添加元素时,如果元素个数+1> 当前数组长度 【size + 1 > elementData.length】时,进行扩容, int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍,将旧数组内容通过Array.copyOf全部复制到新数组(如果5,就扩成5+2=7)

ArraylistLinkedList

  1. 都不同步,不保证线程安全
  2. Arraylist 底层是 object 动态数组,支持快速随机访问,适合频繁查找 get(int index)
  3. LinkedList底层是双向链表,插入、添加、删除快,更占内存
  4. vector是同步的,线程安全,但效率太低,尽量用 Arraylist

Set(无重复):

不允许重复的集合。不会有多个元素引用相同的对象。无序,hashcode决定

  • HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素 。只不过Value被写死了,是一个private static final Object对象。用于不需要保证元素插入和取出顺序的场景
  • LinkedHashSet: LinkedHashSet 是 HashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。 底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO
  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树) ,用于支持对元素自定义排序规则的场景

Queue(实现排队功能的叫号机):

按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的

  • Queue :是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则
  • Deque :是双端队列,在队列的两端均可以插入或删除元素
  • PriorityQueue: Object[] 数组来实现二叉堆,优先级最高的元素先出队,默认是小顶堆,通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。非线程安全的,不支持存储null
  • ArrayQueue: Object[] 数组 + 双指针
  • 阻塞队列:当阻塞队列为空时,获取(take)操作是阻塞的;当阻塞队列为满时,添加(put)操作是阻塞的。不用手动控制什么时候该被阻塞,什么时候该被唤醒

阻塞队列的应用:
①生产者消费者模式,不用加锁
②线程池

Map(键值对):

使用键值对存储。key 是无序的、不可重复的,value 是无序的、可重复的,两个Key可以引用相同的对象,但Key不能重复,Key可以是任何对象

  • HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间

红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。)

  • LinkedHashMap: LinkedHashMap 继承自 HashMap,它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
  • Hashtable数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap红黑树(自平衡的排序二叉树)

2、HashMap , Hashtable, HashSet ,TreeMap , ConcurrentHashMap

HashMap 和 Hashtable

  1. HashMap 是非线程安全的,无并发控制,可能会链表成环; HashTable 是线程安全的(内部的方法基本都经过synchronized 修饰);
  2. HashMap 中, null 可以作为键,这样的键只有一个,在table[0]位置,可以有一个或多个键所对应的值为 null。但是在 HashTable 中会异常。
  3. ①创建时如果不指定容量初始值, Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。 HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
    ②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方

2的幂次方:Hash 值的范围值非常大,先做对数组的长度取模运算,得到的余数才是对应的数组下标。当length 是 2 的 幂次方,hash%length等价于hash&(length-1)&相比%效率较高)

  1. HashMap 在解决哈希冲突时,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 Hashtable 没有这样的机制。

hashmap扩容

容量(初始16),加载因子(0.75),阈值(容量x加载因子)
如果加载因子比较大,扩容发生的频率比较低,浪费的空间比较小,发生hash冲突的几率比较大,底层的红黑树变得异常复杂。对于查询效率极其不利;

  • JDK1.7中扩容的时候需要将所有的数据重新计算HashCode( &),然后赋给新的HashMap<K,V>,十分耗费性能;JDK1.8中不需要重新计算HashCode,经过rehash之后元素的位置,要么是在原位置,要么是在原位置再移动2次幂的位置。
  • JDK1.7 链表采用头插法,多线程打乱插入新链表的顺序会造成环形链数据丢失的情况。JDK1.8 采用尾插法
  • 1.7中是只要大于等于阈值就直接扩容2倍;而1.8中,当数组容量未达到64时,以2倍进行扩容,超过64之后,若桶中元素个数超过7,就将链表转换为红黑树(同时红黑树中的元素个数小于6就会还原为链表),当红黑树中元素超过32的时候才会再次扩容。
  • 1.7 先判断是否需要扩容,再插入,1.8 先进行插入,插入完成再判断是否需要扩容

jdk1.7数据丢失与链表成环:

  • 数据丢失:假设两个线程A、B都在进行put操作,当线程A判断是否出现hash碰撞后,由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全
  • 链表成环:链表采用头插法,导致插入的顺序和原来链表的顺序相反的。两个线程同时插入 a 和 b ,指可能出现 a.next=b 同时b.next=a的情况。jdk1.8采用尾插法解决这个问题

hashmap的get操作:

  • 当调用get方法时会调用hash函数,这个hash函数会将key的hashCode值返回,返回的hashcode与entry数组长度-1进行逻辑与运算得到一个index值,用这个index值来确定数据存储在entry数组当中的位置。
  • 通过循环来遍历索引位置对应的链表
  • 如果hash函数得到的hash值与entry对象key的hash值相等,并且entry对象当中的key值与get方法传进来的key值equals相同则返回entry对象的value值,否则返回null。

HashMap 和 HashSet 区别

HashSet 底层就是基于 HashMap 实现的,仅存储对象,使用成员对象计算hashcode,(hashmap存储键值对,使用key计算hashcode)

HashMap 和 TreeMap 区别

HashMap来说, TreeMap 主要多了对集合中的元素根据键排序(SortedMap 接口)的能力,以及对集合内元素的搜索(NavigableMap 接口)的能力。

ConcurrentHashMap

ConcurrentHashMap 解决多线程并发环境下 HashMap死循环问题,线程安全的,比Hashtable效率高。底层为数组+链表/红黑树。

  • JDK1.7用数组+链表,并发控制segment分段锁(Segment 继承于 ReentrantLock)。value 以及链表都是 volatile 修饰的,保证了获取时的可见性,但不能保证并发原子性,所以还需要加锁
  • JDK1.8用 Node 数组+链表+红黑树的数据结构,并发控制使用synchronized+CAS 代替 Segment。synchronized 只锁定当前链表或红黑二叉树的首节点。先 CAS(Unsafe.compareAndSwapObject方法)尝试插入,如果有其它线程在该位置提前插入了节点,就自旋再次尝试,失败了再加锁。

(Hashtable使用synchronized,没有分段,效率低)

3、解决哈希冲突

产生原因:
由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,因此总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。(两个不同的数据计算后的结果一样)

解决方法:

①开放地址方法

(1)线性探测:按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上往后加一个单位,直至不发生哈希冲突。 
(2)再平方探测:按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等等。直至不发生哈希冲突。   
(3)伪随机探测:按顺序决定值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来值的基础上加上随机数,直至不发生哈希冲突。

②链式地址法(HashMap的哈希冲突解决方法)
  创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
  

优点:
  (1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
  (2)由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
  (3)开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
  (4)在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
  缺点:
  指针占用较大空间时,会造成空间浪费,若空间用于增大散列表规模进而提高开放地址法的效率。

③建立公共溢出区

建立公共溢出区存储所有哈希冲突的数据。

④再哈希法

对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。

三、多线程

1、进程与线程

进程与线程:

  1. 线程是进程的一部分,一个进程拥有多个线程
  2. 进程是资源分配最小单位,线程是独立调度的最小单位

比如:启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

多进程与多线程:

  1. 多进程数据天然隔离,安全;多线程共享数据,方便。
  2. 进程间不会相互影响;线程一个挂掉可能导致整个进程挂掉
  3. 多进程开销大,创建销毁慢,切换复杂,CPU利用率低;多线程开销小,创建销毁快,切换简单,CPU利用率高;
  4. 多机分布的用进程,多核分布的用线程。

并行与并发:

  1. 并行–>单位时间内,多线程在多核CPU上就是并行
  2. 并发–>时间段内,多线程在单核CPU上就是并发

通信:

  • 进程:管道,信号,信号量,消息队列,共享内存,套接字socket
  • 线程:锁(互斥锁,共享锁);全局变量(用volatile关键字保证可见性);threadLocal;wait()+notify()

多线程的实现:

  • Java:①继承自 Thread 类②实现Runnable接口
  • python:①把一个函数传入并创建Thread实例,然后调用start()开始执行 ②定义一个类,继承自threading.Thread类,使用 init(self) 方法进行初始化,在 run(self)方法中写上该线程要执行的程序,然后调用 start() 方法执行

协程:

  • 协程是一种用户态的轻量级线程,协程的调度完全由用户控制(线程由操作系统调度)。
  • 协程位于同一个线程上,上下文切换开销非常小,可以不加锁。
  • 遇到耗时的 IO 操作时,通过协程调度,执行下一个任务,避免阻塞在 IO 上,让 CPU 空等

2、线程状态


sleep()wait()暂停线程的执行:

  • sleep() 没有释放锁,而 wait() 释放了锁 。
  • wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。
  • wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。

start()run()

  • start():会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了,会自动执行 run()。
  • run():main 线程下的普通方法去执行,不是多线程

上下文切换
如果线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,CPU 为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用。

发生线程切换,保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。

发生原因:

  • 主动让出 CPU,比如调用了 sleep(), wait() 等。
  • 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死。
  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
  • 被终止或结束运行

3、进程线程的通信与调度

进程控制块PCB(process control block),程序段、相关数据段,三部分构成了进程

进程通信(IPC)与进程同步

  • 进程同步:控制多个进程按一定顺序执行;
  • 进程通信:进程间传输信息。

进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息

临界区:对临界资源进行访问的那段代码称为临界区。需要互斥访问临界资源

  • 同步:多个进程按一定顺序执行;
  • 互斥:多个进程在同一时刻只有一个进程能进入临界区。

进程通信:管道(匿名/有名)、消息队列、信号、信号量、共享内存、套接字

  1. 管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。只存在于内存中的文件
  2. 有名管道(Names Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。 有名管道严格遵循先进先出(FIFO)。
  3. 消息队列(Message Queuing) :消息队列是消息的链表,与管道不同的是,消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以独立于读写进程存在,读进程可以根据消息类型有选择地接收消息,消息不一定要以先进先出的次序读取,也可以按消息的类型读取,比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字节流,以及缓冲区大小受限等缺点。
  4. 信号(Signal) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;承载信息量少。
  5. 信号量(Semaphores) :信号量是一个计数器,用于多进程对共享数据的访问,如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。主要用于解决与同步相关的问题并避免竞争条件。
  6. 共享内存(Shared memory) :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
  7. 套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

共享内存:

  • 进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。
  • 进程之间在共享内存时,会保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
  • mmap()系统调用实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

进程调度

为了确定首先执行哪个进程以及最后执行哪个进程以实现最大 CPU 利用率

  1. 先到先服务(FCFS)调度算法 : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
  2. 短作业优先(SJF)的调度算法 : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
  3. 时间片轮转调度算法 : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin)调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
  4. 多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。UNIX 操作系统采取的便是这种调度算法。
  5. 优先级调度 :为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。

线程间的通信方式

  1. wait() 和 notify()
  2. 管道流 PipeStream,一个线程发送数据到输出到管道,另一个线程从输出管道中读取
  3. ThreadLocal 主要解决为每个线程绑定自己的值

线程同步

线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。有下面三种线程同步的方式:互斥量、信号量、事件

  1. 互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
  2. 信号量(Semphares) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量
  3. 事件(Event) :Wait/Notify,通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操

进程切换与线程切换

  1. 切换新的页表,然后使用新的虚拟地址空间。
  2. 切换内核栈,硬件上下文切换。(CPU的所有寄存器中的值、进程的状态以及堆栈上的内容)

线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。

线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

僵尸进程与孤儿进程

僵尸进程: 一个父进程利用fork创建子进程,如果子进程退出,而父进程没有利用wait 或者 waitpid 来获取子进程的状态信息,那么子进程的状态描述符依然保存在系统中。

孤儿进程:一个父进程退出, 而它的一个或几个子进程仍然还在运行,那么这些子进程就会变成孤儿进程,孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集的工作

5、死锁

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。(无法破坏)
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(破坏:一次性申请所有资源)
  3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。(破坏:申请不到别的,主动释放已有资源)
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(破坏:按一定顺序申请资源,反序释放)

死锁恢复:

  • 利用抢占恢复
  • 利用回滚恢复
  • 通过杀死进程恢复
  • 建立有向图,拓扑排序,释放占用资源最少的线程

死锁避免:银行家算法。银行家算法的主要思想是避免系统进入不安全状态。在每次进行资源分配时,它首先检查系统是否有足够的资源满足要求ÿ

以上是关于Java面试自用简洁版的主要内容,如果未能解决你的问题,请参考以下文章

Java面试题总结

Java自用JVM-垃圾回收机制

计网面试(自用)

几个java面试题的简洁回答

JVM 大厂面试都会问,都会这么问,你能顶住么?

最新BAT大厂java秋招面试题整理,快来查缺补漏