JAVA基础+JAVA集合+JVM+JUC,2021最详解

JAVA基础+JAVA集合+JVM+JUC,2021最详解

【java基础】

【问】构造器(Constructor)就等于构造函数:

(1)只能重载,不能重写。

(2)this(...)表示此类的构造函数,super(...)表示父类的构造函数。他们都是为了解决构造方法中的代码冗余。

【问】abstract关键字冲突:final、static、private:

(1)final和private修饰的方法不能被重写抽象方法就是为了重写,因此冲突;

(2)static是为了外部直接访问,但是abstract修饰成员没有方法体,所以访问没有意义,冲突。

【问】普通类和抽象类的区别?


抽象类

普通类

实例化

不能

方法

抽象方法(abstract)+实现方法(static)

实现方法(无抽象方法)





【问】接口和抽象类的区别?


接口

抽象类

设计思想层面

行为抽象设计(像什么)

模板抽象设计(是什么)

成员变量

变量默认为static+ final ,且只能通过接口访问

不一定

成员方法

只有抽象方法,无修饰符,默认为public abstract。

实现方法和抽象方法都有。

(抽象方法abstract时,与final、static、private冲突;

实现方法下,权限只要不是private就行)

继承与实现角度

可以多实现、多继承

单继承







【问】强制类型转换?

多态情形下的,父类型转子类型。

class Person { public void eat() { System.out.println("The people were eating"); }} class Boy extends Person { public void eat() { System.out.println("The boy were eating"); }} public class Test { public static void main(String[] args) { Person person = new Boy(); Boy boy = (Boy) person; boy.eat(); }}

【问】==、equals、hashcode?

(1)== :判断地址是否相同。

(2)equals:重写前,与“==”一样;重写后,判断内容(属性值)是否相同。

(3)hashCode:重写前,根据内存地址得到;重写后,根据内容(所有属性)得到。

【问】hashcode和equals的关系?为什么重写equals一定要重写hashcode?

涉及到这两个的未重写,都是在做自定义类、集合放置自定义对象的时候,一般java自带的类都是重写过的。

(1)

判断两个对象内容是否相等的过程:

...

中间多了一个hashcode的环节是为了减少equals的使用,hashcode比equals更快!

(2)

从两个点来回答这个问题:

(1)重写hashcode也是为了让hashcode派上用场,减少equals的使用。

(2)equals和hashcode不重写前都是根据内存地址得出的,重写后才能进行内容比较。如果不重写,hashcode就是根据内存地址得出的,当HashMap碰到两个地址不同,内容相同的对象,HashMap就会存入两个内容一样的键。

【问】什么是java中的值传递?java中方法对原始对象的影响总结?

(1)


值传递

引用传递(地址)

定义

(java)将实际参数的地址复制一份,传递到函数中

将实际参数的地址直接传递到函数中

能否修改原始对象的内容?

不能






(2)

java方法不能修改一个基本数据类型的参数(boolean、byte、short、int、long;char;float、double;)。 java方法可以改变一个对象或数组的状态。(数组就是一个对象) java方法不能更改原对象的引用。

【问】java中的异常了解哪些?Throwable类的方法?try-catch-finally?throw和throws?

(1)以下都是类,均代表异常的类型:

...

(2)

Throwable类的方法

含义

getMessage()

返回异常发生时的简要信息

toString()

详细信息

getLocalMessage()

本地化信息

printStackTrace()

对象封装的异常信息


(3)try-catch-finally

...

++

finally不执行的四种情况:

(1)退出JVM的语句 System.exit(int)

(2)finally代码块一进去就有异常

(3)线程死亡

(4)CPU关闭

(4)

throw:

语句抛出异常,出现于函数内部,用来抛出一个具体异常实例;throw被执行后面的语句不起作用,直接转入异常处理阶段。

throws:

函数方法抛出异常,一般写在方法的头部,用来抛出一些异常,本身不进行解决,抛给方法的调用者进行解决(try...catch...)。

【问】什么是序列化?Java 序列化中如果有些变量不想进行序列化,怎么办?

(1)

序列化:

将Java对象转换成字节流的过程。当 Java 对象需要在 网络上传输 或者 持久化存储 到文件中时,就需要对 Java 对象进行序列化处理。

反序列化:

将字节流转换成 Java 对象的过程。

(2)

对于不想进行序列化的变量,使用 transient 关键字修饰。(transient 只能修饰变量,不能修饰类和方法。)


transient 关键字的作用是:序列化时,阻止实例中那些⽤此关键字修饰的的变量序列化;当对象被反序列化时, 被 transient 修饰的变量值不会被恢复。


【问】什么是IO流?java IO流的种类?有了字节流,为什么还要字符流?

(1)

java数据想传输到别的地方(比如网络传输、持久化存储时),必须以IO流的形式(非java的数据形式有字节和字符。);传入java的流叫输入流,传出java的流叫输出流。“流”=数据传输的管道。

...

(2)

按照不同角色对流进行划分:

按照流的流向分:输入流,输出流 按照操作单元分:字节流,字符流(字节B/byte<字符) 按照流的角色:节点流、处理流。(目标设备中的类>>节点流>>处理流)

Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的:

InputStream/Reader:所有输入流的基类(字节/字符) OutputStream/Writer:所有输出流的基类(字节/字符)

按照操作方式分类结构图:

...

按操作对象分类结构图:

...

(3)

有了字节流,为什么还要字符流?(字符>字节)

虽然字节是传输的基本单位,但想要生成字节,需要通过JVM由字节转换,这个过程非常耗时,并且容易产生乱码;

所以,java的IO流直接设计出字符流,方便直接对字符进行流操作。


字节流

字符流

适用场景

音频、图片等媒体文件

涉及到字符的文件


【问】NIO和传统IO的区别(网络通信)?


传统BIO

NIO

特点

同步阻塞

同步非阻塞(java4)

性能及原因

性能较低;1个线程处理1个请求,线程在IO期间不能做别的事情(比如客户端没有数据发送给服务端,就一直等待)

性能高;1个线程可以处理多个请求,线程在IO期间可以做别的事情(客户端没有数据发送过来,线程可以去做别的事情,性能高)

实现

基于Stream;

套接字:Socket、ServerSocket

基于Buffer+Channel,另外还有Selector选择器;套接字:SocketChannel、ServerSocketChannel









NIO模型的三大特点:

...

(1)channel:双向的,既可以读,也可以写。

(2)buffer:缓冲,是一块内存。

(3)selector:selector监测管道是否有数据变化(事件),在别的未获取锁的且有数据变化的线程中,提前把数据写入buffer或把数据从buffer中取出。

【问】static,this,super关键字?

static:

修饰成员属性和成员方法: 被 static 修饰的成员属于类。可以被类直接调用,也可以通过对象调用(建议通过类调用)。 静态代码块: (执行顺序:静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次. 静态内部类(static修饰类的话只能修饰内部类): 静态内部类与非静态内部类的区别: 1. 静态内部类的创建不需要依赖外部类对象的创建。2. 它不能使用任何外部类的非静态成员。 静态导包(用来导入类中的静态资源,1.5之后的新特性): import static ,可以直接使用类中静态成员变量和静态成员方法。

this:

this关键字用于引用类的当前实例。

class Manager { Employees[] employees; void manageEmployees() { int totalEmp = this.employees.length; System.out.println("Total employees: " + totalEmp); this.report(); } void report() { }} this.employees.length:访问当前实例的变量。 this.report():访问当前实例的方法。

super:

super关键字用于从子类访问父类的变量和方法。

【问】深拷贝和浅拷贝?

Student类三个属性:name、age、teacher。

浅拷贝一个对象时:创建一个新对象,并对对象内的非静态属性和方法进行复制。对象的属性分为基本数据类型和引用类型。对基本数据类型的属性,直接在栈区进行复制;对引用类型的属性,则只复制引用但不复制对象,共同引用同一个对象。 ... 深拷贝一个对象时:创建一个新对象,并对对象内的非静态属性和方法进行复制。对基本数据类型的属性,直接在栈区进行复制;对引用类型的属性,复制引用也复制对象,分别引用不同的对象。 ...


【JAVA集合】

【问】java集合总结?哪些是并发集合(做了线程安全处理的集合)?

(1)

...

(2)

【List】

Vector:读、写都做了同步。

CopyOnWriteArrayList:写做了同步,读没做同步。适用于读多写少。(在写的时候会复制一个副本,对副本写,写完用副本替换原值。)

【Set】

CopyOnWriteArraySet:基于CopyOnWriteArrayList,依然是CopyOnWrtite的特性,但不允许重复元素。

【Queue】

ConcurrentLinkedQueue:通过无锁的方式实现,通常性能优于阻塞队列BlockingQueue。

LinkedBlockingQueue:阻塞队列的一种实现类,典型应用场景是“生产者-消费者”模式中,如果生产快于消费,生产队列装满时会阻塞,等待消费。进行了读、写锁的分离。

【Deque】

LinkedBlockingDueue:没有进行读、写锁的分离,因此同一时间只能有一个线程对其操作,因此在高并发应用中,它的性能要远远低于LinkedBlockingQueue,更低于ConcurrentLinkedQueue。

【Map】

HashTable:全部进行同步。

ConcurrentHashMap:

(jdk7)分段锁,锁粒度为segment。具体做法是将数组分为n段segment,用n个锁分别锁住这n个segment,并发性能是HashTable的n倍。通过ReenTrantLock加锁。

(jdk8)摒弃了 Segment 的概念,数据结构类似于java8的hashmap。锁的粒度就是HashEntry(首节点),只要不hash冲突,就不会产生并发。通过CAS+synchronized加锁。

【问】RandomAccess接口?

RandomAccess 接口 只是一个标识,没什么作用,只是说明该集合具 有快速随机访问功能。

【问】ArrayList的扩容机制?

扩容过程:

1.无参构造时,存储数组初始化容量为0;有参构造时,存储数组,初始化容量为传入参数。

2.第一次add时,若容量为0,发生扩容,扩为默认值10。

3.之后再调用add,且发现元素个数大于容量时,再次发生扩容:通过Arrays.copyOf()的方式将容量扩容为原来的1.5倍。


【问】HashMap和HashTable的区别?


HashMap

HashTable

线程安全性

不安全

安全

效率

键有无null

允许有null,放在数组第0个位置

不允许有null

红黑树机制

jdk8以后有

没有

扩容

初始默认16,扩容乘以二;

大小始终保证为2的幂次方

初始默认11,扩容乘以2n+1







【问】HashMap的底层实现?

(1)jdk1.7

数组(默认16)、链表+头插法(扩容产生死循环)

用HashEntry内部类实现的对象表示键值对。属性:K,V,next。

put实现:根据key的hashcode+扰动函数等一系列处理得到数组的索引位置。若发生哈希碰撞(两个key值不同,但算出来的数组索引相同),则在该索引位置通过头插法形成链表。

(2)jdk1.8的变化

做了改进:红黑树+尾插法

为什么红黑树?查询速度比链表快。

为什么尾插法?头插法的缺陷:并发条件下扩容rehash产生死循环(链表头插法的扩容会颠倒链表的顺序。在并发的时候,线程a的rehash使链表颠倒了引用顺序,线程b使链表保持原有的引用顺序,造成两个节点互相引用,最终形成了死循环)。

【问】HashMap链表升级为红黑树?红黑树退化为链表?

避免频繁的树化和退化过程:

(1)数组长度到64+链表长度到8;

(2)红黑树节点数量小于6,退化为链表;

【问】HashMap的加载因子?为什么是0.75?

(1)

加载因子是确定数组扩容时机的。扩容条件:存入数据的数量>=数组长度*加载因子。(反映内存利用率)

(2)

空间(内存)与时间(冲突)的平衡,官方测出来0.75最优。

...

【问】HashMap的数组长度为什么是2的幂次方?

为了减少哈希碰撞。计算索引时要对数组长度取余,用2的幂次方可以使结果的散列性更好。

【问】LinkedHashSet 和 TreeSet 的区别?(均为有序Set)

LinkedHashSet:是 HashSet 的子类;能够按照添加的顺序遍历; TreeSet:底层使用红黑树;能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。(TreeMap也是红黑树)


【JVM】

【问】JVM的总体结构?

JVM四大组成:类装载器+运行时数据区+执行引擎+本地接口。

...

【问】运行时数据区(java内存区域)的内存图?五个部分的详细介绍?

(1)

java8以前:

...

java8以后:

...

(2)

a、堆

存放:对象实例。

更新速度慢,二级缓存。程序员分配释放或GC机制。

java7及以前:

...

java8:

永久代(方法区)被从JVM中移除,用元空间替代,位于直接内存中。

b、方法区(java8移动到元空间)

java7方法区存放:常量/常量池+静态变量+类信息+即时编译后代码。

java8:抛弃方法区,常量+静态变量 存入jvm堆,其余元数据移动到元空间存放。

方法区和永久代的关系?

永久代是HotSpot虚拟机对方法区的⼀种实现方式,把方法区用来当作永久代。

常用参数?

java7:调节方法区大小:

...

java8:调节元空间大小:

...

为什么要把永久代替换为元空间?

元空间在直接内存中,不受jvm内存大小限制,溢出概率更小。

c、程序计数器(PC寄存器)

作用:

(1)字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。

(2)在多线程的情况下,程序计数器⽤于记录当前线程执行的位置,从⽽当线程被切换回来的时

候能够知道该线程上次运⾏到哪儿了。

注意:

唯一一个不会出现OOM问题的内存区域。

d、JVM栈

存放:对象引用和基本数据类型。

更新速度快,一级缓存。变量生命周期结束自动释放。

线程私有也是线程安全,一个线程对应一个虚拟机栈区域,一个方法的调用对应一个栈帧。(栈的内部由一个个的栈帧组成)(return或报错则弹栈)

e、本地方法栈(native)

调用本地方法(native方法)的区域,连接其他语言。(C/C++)

f、直接内存(不属于JVM,但也被频繁使用)


【问】JVM堆、栈的区别?


栈内存

堆内存

存储对象

对象引用和基本数据类型

对象实例

更新速度

快,一级缓存

慢,二级缓存

内存释放

变量生命周期结束自动释放

程序员分配释放、GC机制

线程

线程私有

线程公有



【问】java程序new一个对象后发生了什么,比如:A a=new A(); ?

编译A类>>类加载流程>>对象创建流程>>对象访问定位

【问】类的加载过程?类的生命周期?

...

(1)加载(双亲委派机制)

JVM读取Class文件,创建Class对象的过程。

(2)连接

a、验证

确保Class文件符合当前虚拟机的要求。

b、准备

在方法区中为类变量分配内存空间并设置默认值。(加了final修饰的直接准备)

c、解析

将常量池中的符号引用替换为直接引用。

(3)类的初始化

通过执行类构造器的<client>方法为类进行初始化。为类的静态变量赋初始值和收集静态代码块。

【问】类加载机制(双亲委派机制)?作用?

(1)

三个等级的加载器:

(1)根加载器

(2)扩展类加载器

(3)应用程序类加载器(负责加载我们自己写的类)

第一步:

由下往上走的逻辑,加载过的话不用加载;如果都没有加载过,就一直走到最上边(也就是根加载器);

第二步:

假如都没有加载过,走到了根,就采取第二步加载逻辑,由上往下走的逻辑。每到一个加载器判定自己能否加载,自己能加载就自己加载,加载不了就给子加载器加载。一直到最下面的应用程序类加载器(有时还有自定义类加载器,需要继承java.lang.ClassLoader,重写findclass方法)。若还是加载不了,抛出ClassNotFound异常。

(2)

a、第一步的逻辑是为了节省时间,提升性能。

b、第二步的逻辑是为了当修改下层加载器,不影响上层加载器,保证安全性。

【问】反射:

定义:

JAVA反射机制是在运行状态中,通过class类对象动态的获取类的信息或动态调用对象的方法,这种机制称为java的反射机制。

特点:

反射可以实现动态创建对象和编译,体现出很大的灵活性,是Java被视为动态语言的关键。但是会影响一些性能。


【问】Java对象的创建过程?(5步)(运行时数据区里进行)

...

(1)类加载检查:

虚拟机遇到⼀条 new 指令时,检查是否完成了完整的类加载过程,如果没有,必须执行类加载过程。(加载、连接、初始化)

(2)内存分配:

为新生对象分配堆内存。

内存分配方式?

分配方式有 “指针碰撞”“空闲列表” 两种方式。选择哪种分配方式取决于java堆是否有内存碎片(取决于垃圾收集器)

...

解决内存分配并发问题?

(1)乐观锁CAS+失败重试: CAS 是乐观锁的⼀种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采⽤ CAS配上失败重试的方式保证更新操作的原子性。

(2)独立内存TLAB: 为每⼀个线程预先在 Eden 区分配⼀块独立内存TLAB,JVM 在给线程中的对象分配内存时,首先在TLAB 分配,当对象大于TLAB 中的剩余内存或 TLAB 的内存已⽤尽时,再采⽤上述的CAS进行内存分配.

(3)对象的初始化零值:

将分配到的内存空间都初始化为零值(不包括对象头),保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使⽤,程序能访问到这些字段的数据类型所对应的零值。

(4)设置对象头:

设置对象头信息:这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC 分代年龄等。

(5)执行init方法:

接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样⼀个真正可用的对象才算完全产生出来。(字段不再是默认值,而是程序员设定的值)


【问】对象的访问定位有哪两种方式?

我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种:

1. 句柄池:

Java堆中将会划分出⼀块内存来作为句柄池,reference 中存储的就是对象的句柄地址,⽽句柄中包含了对象实例数据与类型数据各⾃的具体地址信息;

...

2. 直接指针:

如果使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象的地址。

...


这两种对象访问方式各有优势:

(1)句柄池的好处:

对象移动时地址修改方便。引用中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,⽽不需要更改引用。

(2)直接指针的好处:

就是访问对象速度快。它节省了⼀次指针定位的时间开销。


【问】堆内存中分配对象的流向?

...

新创建小对象进入新生代的eden区,新创建的大对象直接进入老年代。新生代中年龄过大的对象(>15)将进入老年代。

对象年龄判定?

每熬过一次MinorGC,年龄加1.

MinorGC流程?(复制算法)

首先进行存活判定,把eden和ServivorFrom区存活的对象年龄+1,并复制到ServiorTo;清空eden和servivorFrom;ServivorTo和ServivorFrom对调,经历下一次MinorGC。

MajorGC算法(标记-清除,标记整理)


【问】垃圾回收GC?(判定-算法-回收器)

一、根据区域对GC进行分类:

...

二、如何判断对象将被回收(成为垃圾/死亡)?

判断哪些对象已经死亡,对已经死亡的对象进行垃圾回收GC。

(1)引用计数器:

给每个对象中添加⼀个引用计数器,每当有⼀个地方引用它,计数器就加1;当引用失效,计数器就减1;引用计数器的值为0的对象就是不能再被使用的,将被回收。

循环引用问题:两个对象循环引用,导致引用一直存在,不能被回收。

(2)可达性分析算法(解决循环引用问题):

首先定义一些GC根节点,通过这些GC根节点和引用关系往下搜索,如果GC根节点和某个对象之间没有可达路径,则该对象不可达。标记两次后仍然不可达,则将被回收。

...

三、如何判断一个常量将被回收(成为废弃常量)?

常量:程序运行中,固定不变的量。

存放于:运行时常量池。

废弃常量判定:没有被引用的常量,就是废弃常量;如果这时发生内存回收的话而且有必要的话,废弃常量就会被系统清理出常量池。

四、如何判断一个类可以被回收(成为无用的类)?

类的元数据存放位置:java7方法区,java8元数据区。

判定无用的类(同时满足以下三个条件):

加载该类的 ClassLoader 已经被回收。 该类对应的 Class 对象没有在任何地方被引⽤。 Java 堆中不存在该类的任何实例。

虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

五、有哪些垃圾回收算法?

(1)复制:

将内存分为大小相同的两块,每次使用其中的⼀块。当这⼀块的内存使用完后,就将还存活的对象复制到另⼀块去,然后再把使用的空间⼀次清理掉。这样就使每次的内存回收都是对内存区间的⼀半进行回收。

效率高。

...

(2)标记-清除:

该算法分为“标记”和“清除”阶段:标记出所有不需要回收的对象,然后统一没收所有没有被标记的对象。

它是最基础的收集算法,这种垃圾收集算法会带来两个明显的问题:

1. 效率问题

2. 空间问题(标记清除后会产生大量不连续的碎片)

...

(3)标记-整理:

首先也是将对象进行标记,然后让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存。

效率提升,内存碎片减少。

(4)分代收集:

将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

(1)新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。

(2)老年代中,对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择标记-清除标记-整理算法进行垃圾收集。

HotSpot为什么要分为新生代、老年代?

主要是为了提升 GC 效率。

六、常用的垃圾回收器?

收集算法是垃圾回收的方法论,那么垃圾收集器就是垃圾回收的具体实现。

...

(1)Parallel(客户端使用,Parallel+ParallelOld)

可以调节吞吐量(高效率地利用CPU)。所谓吞吐量就是 CPU 中⽤于运⾏⽤户代码的时间与 CPU 总消耗时间的⽐值

(2)CMS(ParNew+CMS)

CMS 等垃圾收集器的关注点更多的是减少用户线程的停顿时间(提高用户体验),进行可达性分析(垃圾判定)的时候不需要用户线程停下来。

CMSHotSpot 第⼀款真正意义上的并发收集器, 它第⼀次实现了让垃圾收集线程与⽤户线程(基本上)同时⼯作。

...

(1)初始标记(STW):

暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;

(2)并发标记:

同时开启 GC 和⽤户线程,⽤⼀个闭包结构去进行可达性分析。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。

因为⽤户线程可能会不断的更新引⽤域,所以 GC 线程⽆法保证可达性分析的实时性。所以这个算法⾥会跟踪记录这些发生引用更新的地⽅。

...

(3)重新标记(STW):

重新标记阶段就是为了修正并发标记期间因为⽤户进程运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓, 远远⽐并发标记阶段时间短.

(4)并发清除:

开启⽤户线程,同时 GC 线程开始对未标记的区域做清扫。

主要优点:

并发收集、低停顿。

三个明显的缺点:

CPU 资源敏感; 无法处理浮动垃圾; 有大量内存碎片产生。

(3)G1(单独收集器,新生代+老年代,java8以后)

G1 (Garbage-First) 是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极⾼概率满足 GC 停顿时间要求的同时,还具备⾼吞吐量性能特征.

...

它具备以下特点:

并行与并发:G1 能充分利⽤ CPU、多核环境下的硬件优势,使⽤多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。 分代收集:虽然 G1 可以不需要其他收集器配合就能独⽴管理整个 GC 堆,但是还是保留了分代的概念。 空间集成:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。 可预测的停顿:这是 G1 相对于 CMS 的另⼀个大优势,降低停顿时间是 G1 和 CMS 共同 的关注点,但 G1 除了追求低停顿外,还能创建可预测的停顿时间模型,能让使⽤者明确指定在⼀个长度为 M 毫秒的时间片段内。

G1 收集器的运作大致分为以下几个步骤:

初始标记 并发标记 最终标记 筛选回收

G1 收集器在后台维护了⼀个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。


【多线程(JUC)】

【问】进程、线程的标准回答?

进程:是系统运行程序的基本单位,是程序的实体。

线程:是操作系统能够进行运算、调度的最小单位;线程被包含在进程中,一个进程可以有多个线程。

【问】什么是线程的上下文切换?

当前线程任务在执行完 CPU 时间片切换到另⼀个线程任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。

线程任务的状态保存和再加载就是上下文切换。

【问】并发、并行?

并行(Parallel):多个CPU处理多个线程任务(一个CPU对应处理一个线程任务)

并发(Concurrent):单个CPU处理多个线程任务(线程之间抢夺CPU时间片)

【问】死锁四个条件?处理死锁?(死锁是由于加锁,导致线程之间的循环等待的一种现象)

... ...

【问】java实现线程安全的几种方法?

sychronized同步代码块:对代码块进行加锁,括号内的对象就是锁。 sychronized同步方法:对方法进行加锁,普通方法的锁对象是this,静态方法的锁对象是“class类对象/类名.class”。 Lock接口:一般使用实现类可重入锁ReentrantLock创建锁的实例,try{lock.lock()} 加锁的方法,finally{lock.unlock()} 释放锁的方法,两个方法之间就是锁定的内容。 volatile关键字:免锁机制,属于轻量级同步策略。在需要同步的变量前面加上volatile修饰,并使用原子类(原子性操作),即可实现线程同步。 用ThreadLoacl类对象创建共享变量的线程本地副本,实现线程隔离。

使用sychronized属于重量级的同步方式,volatile是轻量级的。

【问】线程的基本状态?

创建new()之后处于初始状态(NEW)调用 start() 方法后处于就绪状态(READY)获取cpu时间片后处于运行中状态(RUNNING)。 遇到同步方法/代码块,在没有获取到锁时,进入阻塞状态(BLOCKED),阻塞在synchronized代码块/方法外面执行 wait() 方法进入 等待状态(WAITING),进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。 超时等待状态(TIME_WAITING)相当于在等待状态的基础上增加了超时限制,比如 sleep(long millis) 方法或 wait(long millis) 方法。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。 run()方法执行完毕进入 终止状态(TERMINATED)。 ...

有的说法把同步阻塞、等待、超时等待都划分到阻塞状态里,然后细分:

(1)等待阻塞:wait()

(2)同步阻塞:同步为获取锁

(3)其他阻塞:sleep()


【问】wait和sleep的区别?


wait

sleep

是否释放锁

释放锁给其他线程

不释放锁

作用对象

定义在Object类中,

作用于对象本身

sleep方法定义java.lang.Thread中,作用于当前线程

唤醒条件

其他线程调用对象的notify()或者notifyAll()方法,或者也是超时

超时或者调用interrupt()方法体







【问】调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new ⼀个 Thread,线程进入了新建状态。调用 start() 方法,会启动⼀个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执⾏线程的相应准备⼯作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

但是,直接执行 run() 方法,会把 run() 方法当成⼀个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 直接执行 run() 方法的话没有使一个新线程就绪,所以不会以多线程的方式执行;调用 start() 方法方可启动线程并使线程进入就绪状态,可以以多线程的方式执行。


【问】线程池的作用?线程池参数?四种拒绝策略?线程池的实现?

(1)

提高响应速度、降低资源消耗(线程的创建与销毁需要资源和时间上的开销);便于管理。

(2)

corePoolSize 核心线程数量 maximumPoolSize 最大线程数量 keepAliveTime 空闲线程存活时间:空闲状态+核心线程数满了,那么keepAliveTime后线程销毁。 unit 空闲线程存活时间的单位:keepAliveTime的时间单位。 workQueue 阻塞队列:新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务 threadFactory 线程工厂:创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等 handler 拒绝策略:判定是否拒绝执行线程任务;比如阻塞队列满了+最大线程区也满了,就会采取拒绝策略。

(3)四种拒绝策略:

丢弃。 丢弃+抛出异常。 将阻塞队列的头节点丢弃,放入阻塞队列头节点。 交由主线程(即调用者)来执行该任务。

(4)

ExcutorService接口类型,Excutors实现类。

...


【问】Synchronized?

一、单例模式,“双重检验+锁方式”实现单例模式?

什么是单例模式?

1、单例类只能有一个实例。

2、单例类必须自己创建自己的唯一实例。

3、单例类必须给所有其他对象提供这一实例。(单例类+静态)

a、枚举实现单例(缺点:不可继承):

public enum EnumDemo { A; //这里的A表示EnumDemo枚举出来的实例,因为只有一个实例,故为单例 public void doSomeThing() { System.out.println("dosomething"); } //通过枚举实例调用方法 public static void main(String[] args) { EnumDemo.A.doSomeThing(); }}

b、饿汉式(缺点:浪费内存):

类加载+对象的创建(JVM)

public class Hungry { private byte[] data=new byte[1024*1024]; private Hungry(){} //类加载,加载静态成员;从而导致对象创建。 //对象没有使用也会造成内存浪费 private final static Hungry HUNGRY=new Hungry(); private static Hungry getInstance(){ return HUNGRY; }}

c、懒汉式(缺点:多线程并发下单例模式会无效):

在static方法里面创建对象,类加载不会造成对象创建。

//要用对象的时候再new对象public class LazyMan { private LazyMan(){} private static LazyMan lazyMan; public static LazyMan getInstance(){ if (lazyMan==null){ lazyMan=new LazyMan(); } return lazyMan; }}

d、懒汉式+双重检验锁(DCL懒汉式单例)

//要用对象的时候再new对象public class LazyMan { private LazyMan(){ System.out.println(Thread.currentThread().getName()+"ok"); } private static volatile LazyMan lazyMan; public static LazyMan getUniqueInstance(){ if (lazyMan==null){ synchronized (LazyMan.class){ if (lazyMan==null){ lazyMan=new LazyMan(); } } } return lazyMan; } public static void main(String[] args) { for(int i=0;i<10;i++){ new Thread(()->{ LazyMan.getUniqueInstance(); }).start(); } }}

单例属性采用 volatile 关键字修饰也是很有必要的:

因为lazyMan=new LazyMan();这段代码其实是分为三步执行(并非原子操作):

1. 为 lazyMan单例对象分配内存空间;

2. 执行构造方法,初始化lazyMan单例对象;

3. 单例对象的访问定位;

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。

(指令重排在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时线程T2 调⽤ getUniqueInstance () 后发现 uniqueInstance 不为空,因此返回uniqueInstance ,但此时 uniqueInstance 还未被初始化。)

所以使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

二、synchronized 关键字的底层原理?

相同点:两者的本质都是对“对象监视器 monitor”的获取。

(1)synchronized 修饰代码块:

使用的是 monitorenter 和 monitorexit 指令;其中 monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。

monitorenter主要是获取监视器锁,monitorexit主要是释放监视器锁。

(2)synchronized 修饰方法:

ACC_SYNCHRONIZED标识;该标识指明了该方法是⼀个同步方法。


【问】Volatile?

一、JMM(java内存模型)?Volatile?

在 JDK1.2 之前(过时):

Java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。

而在当前的 Java 内存模型下:

线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值,而另外⼀个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不⼀致,导致并发不安全。

...

要解决这个问题,就需要把变量声明为 volatile ,这就指示 JVM:这个变量是共享且不稳定的, 每次使用它都到主存中进行读取。

所以, volatile 关键字除了(1)防止 JVM 的指令重排 ,还有⼀个重要的作用就是(2)保证变量的可见性(每次使用此变量都到主存中进行读取)。

...

二、为什么要弄一个CPU高速缓存呢?

总结:CPU缓存的目的是解决 CPU 处理速度内存IO速度不匹配的问题。

现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache:

...


三、synchronized 关键字和 volatile 关键字的区别?

synchronized关键字和volatile关键字是两个互补的存在,而不是对立的存在!


synchronized

volatile

性能

低,重量级同步

高,轻量级同步

保证

可见性+原子性

保证可见性,不保证原子性

(需要原子类来进行原子性操作)

保证

保证访问资源的同步性

保证访问资源的可见性


四、原子类(Atomic)?

1、介绍

原子性操作(Atomic)是指⼀个操作是不可中断的。即使是在多个线程⼀起执行的时候,⼀个操作⼀旦开始,就不会被其他线程干扰。

原子类:就是具有原子性操作特征的类

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

...

2、JUC 包中的原子类是哪 4 类?

基本类型:

AtomicInteger :整形原子类

AtomicLong :长整型原子类

AtomicBoolean :布尔型原子类

数组类型:

AtomicIntegerArray :整形数组原子类

AtomicLongArray :长整形数组原子类

AtomicReferenceArray :引用类型数组原子类

引用类型:

AtomicReference :引用类型原子类

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

AtomicMarkableReference :原子更新带有标记位的引⽤类型

对象的属性修改类型:

AtomicIntegerFieldUpdater :原子更新整形字段的更新器

AtomicLongFieldUpdater :原子更新⻓整形字段的更新器

AtomicReferenceFieldUpdater :原子更新引用类型字段的更新器


【问】ThreadLocal类?

一、基本使用

在类中创建一个ThreadLocal类型的对象,和一个成员变量(共享变量)。

每次更改共享变量时,先set() ThreadLocal变量,线程会保存一份该工作线程的共享变量的本地副本,再更改共享变量。

获取共享变量的时候,用ThreadLocal的get()方法,线程会直接访问本地副本。

这样就实现了线程隔离(每个线程的资源互不干扰),达到线程安全的目的。(空间换时间的思想)

public class ThreadLocalDemo { ThreadLocal<String> tl=new ThreadLocal<>(); private String content; //改 public void setContent(String content) { tl.set(content); this.content=content; } //查 public String getContent() { String s=tl.get(); return s; } public static void main(String[] args) { ThreadLocalDemo demo=new ThreadLocalDemo(); for(int i=0;i<10;i++){ Thread thread=new Thread(new Runnable() { @Override public void run() { demo.setContent(Thread.currentThread().getName()+"的数据"); System.out.println(Thread.currentThread().getName()+"--->"+demo.getContent()); } }); thread.setName("线程"+i); thread.start(); } }}

二、原理

jdk8:用Thread维护ThreadLocalMap,有以下两个好处:

1.每个Map存储的Entry数量变少

2.当Thread销毁的时候,ThreadLocalMap也会随之销毁,减少内存的使用。

... ...

set()方法原理:

...

三、内存泄漏

ThreadLocalMap中使用的 key 为 ThreadLocal的弱引用,而value是强引用。所以,如果ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而value 不会被清理掉。

这样⼀来, ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。

因此,使用完 ThreadLocal 方法后最好手动调用 remove() 方法;下一次调用set、get、remove任意方法都会被清除。

四、强引用、软引用、弱引用、虚引用?

(1)强引用(最常见):

如果一个对象具有强引用,垃圾回收器绝不会回收它。造成内存泄露的原因。

实现:把一个对象赋给一个引用变量。

(2)软引用:

如果一个对象只具有软引用。当内存空间足够,垃圾回收器就不会回收它;当内存空间不足了,就会回收这些对象的内存。

实现:通过SoftReference类。

(3)弱引用:

在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

实现:通过WeakReference类。

(4)虚引用:

虚引用和引用队列联合使用,主要用于跟踪对象的垃圾回收状态。(虚引用并不会决定对象的⽣命周期)

实现:通过PhantomReference类。


【问】AQS?

一、简介

AQS 的全称为( AbstractQueuedSynchronizer ),这个类在java.util.concurrent.locks 包下⾯。

...

AQS 是⼀个用来构建锁和同步器的框架,使⽤ AQS 能简单且⾼效地构造出应⽤⼴泛的大量的同步器,比如我们提到的 ReentrantLock , Semaphore ,其他的诸如

ReentrantReadWriteLock , SynchronousQueue , FutureTask 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS非常轻松容易地构造出符合我们自己需求的同步器

二、原理

1.原理概览

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的⼯作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要⼀

套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加⼊到队列中。


CLH(Craig,Landin,and Hagersten)队列是⼀个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成

⼀个CLH 锁队列的⼀个结点(Node)来实现锁的分配。

...


AQS 使⽤⼀个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队⼯作。AQS 使⽤ CAS 对该同步状态进⾏原⼦操作实现对其值的修改。

...

状态信息通过 protected 类型的 getState , setState , compareAndSetState 进⾏操作:

...

2.AQS 对资源的共享方式

AQS 定义两种资源共享方式:

Exclusive(独占):只有⼀个线程能执行,如 ReentrantLock 。又可分为公平锁和非公平锁:

公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

Share(共享):多个线程可同时执行,如CountDownLatch 、 Semaphore 、 CountDownLatch 、 CyclicBarrier 、 ReadWriteLock 我们都会在后面讲到。

ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某资源进行读。

不同的自定义同步器争用共享资源的方式也不同。⾃定义同步器在实现时只需要实现共享资源state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

3.AQS 底层使用了模板方法模式

同步器的设计是基于模板方法模式的,如果需要⾃定义同步器⼀般的⽅式是这样(模板⽅法模式 很经典的⼀个应⽤):

1. 使⽤者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)

2. 将 AQS 组合在⾃定义同步组件的实现中,并调⽤其模板方法,而这些模板方法会调⽤使⽤者重写的⽅法。

这和我们以往通过实现接⼝的⽅式有很⼤区别,这是模板⽅法模式很经典的⼀个运⽤。

AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:

...

默认情况下,每个方法都抛出 UnsupportedOperationException 。 这些方法的实现必须是内部线程安全的,并且通常应该简短⽽不是阻塞。AQS 类中的其他⽅法都是 final ,所以⽆法被其他类使用,只有这几个方法可以被其他类使用。

以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调⽤tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为⽌,其它线程才有机会获取该锁。当然,释放锁之前,A 线程⾃⼰是可以重复获取此锁的(state 会累加),这就是可重⼊的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到状态的。

再以 CountDownLatch 以例,任务分为 N 个⼦线程去执⾏,state 也初始化为 N(注意 N 要与线程个数⼀致)。这 N 个⼦线程是并⾏执⾏的,每个⼦线程执⾏完后 countDown() ⼀次,state 会CAS(Compare and Swap)减 1。等到所有⼦线程都执⾏完后(即 state=0),会 unpark()主调⽤线 程,然后主调⽤线程就会从 await() 函数返回,继续后余动作。⼀般来说,⾃定义同步器要么是独占⽅法,要么是共享⽅式,他们也只需实现 tryAcquiretryRelease 、 tryAcquireShared-tryReleaseShared 中的⼀种即可。但 AQS 也⽀持⾃定义同步器同时实现独占和共享两种方式,如 ReentrantReadWriteLock 。

三、组件总结

Semaphore (信号量)-允许多个线程同时访问: synchronized 和 ReentrantLock 都是⼀次只允许⼀个线程访问某个资源, Semaphore (信号量)可以指定多个线程同时访问某个资源。

CountDownLatch (倒计时器): CountDownLatch 是⼀个同步工具类,⽤来协调多个线程之间的同步。这个⼯具通常⽤来控制线程等待,它可以让某⼀个线程等待直到倒计时结 束,再开始执行。

CyclicBarrier (循环栅栏): CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强⼤。主要应⽤场景和CountDownLatch 类似。CyclicBarrier的字⾯意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让⼀组线程到达⼀个屏障(也可以叫同步点)时被阻塞,直到最后⼀个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。 CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties) ,其参数表示屏障拦截的线程数量,每个线程调⽤ await() 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

四、用过 CountDownLatch么?什么场景下用的?

CountDownLatch 的作用就是允许 count 个线程阻塞在⼀个地方,直⾄所有线程的任务都执行完毕。之前在项目中,有⼀个使⽤多线程读取多个文件处理的场景,我⽤到了 CountDownLatch 。

具体场景是下⾯这样的:

我们要读取处理 6 个⽂件,这 6 个任务都是没有执⾏顺序依赖的任务,但是我们需要返回给⽤户 的时候将这⼏个⽂件的处理的结果进⾏统计整理。

为此我们定义了⼀个线程池和 count 为 6 的 CountDownLatch 对象 。使⽤线程池处理读取任务, 每⼀个线程处理完之后就将 count-1,调⽤ CountDownLatch 对象的 await() ⽅法,直到所有⽂件读取完之后,才会接着执⾏后⾯的逻辑。

public class CountDownLatchExample1 { // 处理⽂件的数量 private static final int threadCount = 6; public static void main(String[] args) throws InterruptedException { // 创建⼀个具有固定线程数量的线程池对象(推荐使⽤构造⽅法创建) ExecutorService threadPool = Executors.newFixedThreadPool(10); final CountDownLatch countDownLatch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { final int threadnum = i; threadPool.execute(() -> { try { //处理⽂件的业务操作 ...... } catch (InterruptedException e) { e.printStackTrace(); } finally { //表示⼀个⽂件已经被完成 countDownLatch.countDown(); } }); } countDownLatch.await(); threadPool.shutdown(); System.out.println("finish"); }}

有没有可以改进的地方呢?

可以使⽤ CompletableFuture 类来改进!Java8 的 CompletableFuture 提供了很多对多线程友好的⽅法,使⽤它可以很⽅便地为我们编写多线程程序,什么异步、串⾏、并⾏或者等待所有线程执行完任务什么的都非常方便。

CompletableFuture<Void> task1 = CompletableFuture.supplyAsync(()->{ //⾃定义业务操作 });......CompletableFuture<Void> task6 = CompletableFuture.supplyAsync(()->{ //⾃定义业务操作 });......CompletableFuture<Void>headerFuture=CompletableFuture.allOf(task1,.....,task6); try { headerFuture.join(); } catch (Exception ex) { ...... }System.out.println("all done. ");

上⾯的代码还可以接续优化,当任务过多的时候,把每⼀个task都列出来不太现实,可以考虑通过循环来添加任务。

//⽂件夹位置List<String> filePaths = Arrays.asList(...)// 异步处理所有⽂件List<CompletableFuture<String>> fileFutures = filePaths.stream() .map(filePath -> doSomeThing(filePath)) .collect(Collectors.toList());// 将他们合并起来CompletableFuture<Void> allFutures = CompletableFuture.allOf( fileFutures.toArray(new CompletableFuture[fileFutures.size()]));