面试必问【java基础】——类的生命周期,jvm,多线程,锁,HashMap,aqs,cas,并发包

java基础
https://blog.csdn.net/qq_22067469/article/details/85808492?utm_medium=distribute.pc_relevant.none-task-blog-title-9&spm=1001.2101.3001.4242

面试必问的线程池
面试必问的mysql
面试必问的cas
面试必问的hashMap

java跨平台特性

Java->jvm解释执行
但是要实现网络通信的Java代码,还需要内嵌tomcat,将tomcat以jar包的形式嵌入,更好的实现跨平台,省得安装不同版本的tomcat
注意:一般仅开发环境用内嵌tomcat,生产环境一般不这么用

tomcat:提供对jsp和Servlet的支持的轻量级web容器,是一个servlet容器

构造器的特性

特点:和方法同名,没有返回值和返回类型void等
作用:类实例化对象会调用
注意:
1、如果重载了自己的有参构造器,系统不指定默认无参构造器,如果想用无参构造器实例化对象,会出错。
2、构造器一般是public的,这样其他类中才能调用构造器
3、构造器只能被重载,属于一个类中定义多个同名方法,参数不同

为什么子类构造函数会先调用父类构造函数

为子类初始化部分属性

没有声明构造器可以实例化对象吗

会有默认无参构造器

重写

1、重写是子类继承父类中的方法对父类的方法做一个自己的实现。 方法名,参数,返回值都会不同
2、构造器不能被继承,属于父类的一部分,所以不能被重写

类的生命周期 加载,验证,准备,解析,初始化,使用,删除

类的加载是
1、根据全类名获取.class文件
2、将class文件转换为方法区中的存储结构
3、在方法区生成类的CLAZZ对象 java.lang.class,CLASS对象提供对类的属性的获取方法

类加载器种类

1、启动类加载器,加载的是rt.jar中的基础类,jdk包中的类 基本数据类型string
Class.forName(“java.lang.String”).getClassLoader()
2、扩展类加载器,加载\lib\ext,extention类
3、应用程序类加载器,加载ClassPath中的类库
4、自定义类加载器,继承java.lang.ClassLoader这个类,用户可以自定义类的加载方式

什么是双亲委派原则

1、类加载器之间,底层类加载器会将加载的任务委托给父类加载器。
2、父类加载器如果确实不能加载,就会让子类加载器加载

双亲委派好处

1、避免重复加载
2、安全性,防止修改jdk中api的类,因为启动类加载器已经加载string,是不会加载自定义的string

线程上下文加载器,打破双亲委派原则

比如DriverManager加载驱动,需要调用jdbc.mysql.driver。
此时driver不是由下层类加载器委托启动类加载器加载的,启动类加载器无法加载,可以委托给线程上下文加载器

可以不可以自己写个String类

双亲委派原则。启动类加载器已经加载string,是不会加载自定义的string

类加载器与类的”相同“判断

即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。
要判断两个类是否“相同”,前提是这两个类必须被同一个类加载器加载,否则这个两个类不“相同”。

类初始化时机

1、创建类的实例,也就是new一个对象
2、访问某个类或接口的静态变量,或者对该静态变量赋值
3、调用类的静态方法
4、反射(Class.forName(“com.lyj.load”))
5、初始化一个类的子类(会首先初始化子类的父类
6、JVM启动时标明的启动类main,即文件名和类名相同的那个类

java如何创建一个对象

1、new对象
2、对象的clone方法
3、反射机制
4、序列化和反序列化会创建一个对象

类的生命周期

加载、验证、准备、解析、初始化、使用、卸载。
• 1. 加载:查找并加载类的二进制数据至方法区,CLAZZ对象
• 2. 连接
– 2.1 验证:确保被加载的类的正确性
– 2.2 准备:为类的 静态变量分配内存,并将其初始 化为默认值0 如果是final的则确定值
– 2.3 解析:把类中的符号引用转换为直接引用
• 3. 初始化:为类的静态变量赋予 正确的初始值
准备
为静态变量分配内存并初始化为0
非静态变量或者成员变量在初始化才分配内存
public static int factor = 3;//会分配内存并初始化为0
public String website = “www.cnblogs.com/chanshuyi”;//不会分配内存

final关键字在的静态变量在准备时的作用
public static int sector = 3;//没有final,会被初始化为0
public static final int number = 3;//有final,会被初始化为3

初始化执行顺序
将类的.class 字节码文件加载到方法区中,然后解析.class字节码
静态先于非静态
父类先于子类

父类静态数据->子类静态数据->父类字段->子类字段->父类构造函数->子类构造函数

静态变量方法和非静态方法初始化的区别

类加载顺序应为 加载静态方法(加载不是执行)-初始化静态变量(赋值0)-执行静态代码块
静态域中的代码只会被执行一次。

实例化时 先加载非静态方法(加载不是执行)-实例化非静态变量-执行构造代码块-执行构造函数
然后执行构造代码和构造方法,每次使用new关键字实例化对象时,会执行多次
在虚拟机栈中,父类和子类构造方法轮流入栈

抽象类是什么?

比如动物,对猫狗的总称
1、abstract修饰,不能和private,final,static,native一块修饰类
2、不能实例化,必须由子类继承,子类来实例化
3、子类如果不是抽象类,就必须重写所有抽象方法!!!
4、只要有抽象方法,就必然是抽象类
5、抽象类可以没有抽象方法,可以有普通方法
6、子类如果是抽象类,想定义自己的抽象方法,必须不能和父类的抽象方法重名!!

栈和队列,区别,用处?

栈先进后出,可以用链表或者数组实现,应用是表达式求值,和括号匹配

括号匹配:遇到左括号入栈,遇到右括号,判断与栈顶括号是否匹配,如果匹配就出栈。最后栈如果为空,就是成对的括号

队列,先进先出,数组和链表实现,应用消息队列,aqs队列

java的异常吗,有什么区别,常见的异常有哪些呢?

空指针异常,变量引用的对象为空,还不存在,此时用变量去使用对象的方法
算数异常,堆栈溢出,数组越界

所有异常的父类是什么?

throwable

equals 和==

==的作用:
  基本类型:比较值是否相等
  引用类型:比较内存地址值是否相等

equals是object类下的方法
  引用类型:默认情况下,比较内存地址值是否相等。

不过可以按照需求逻辑,重写对象的equals方法 比如按照对象name相等则相等

hashcode()和equals()的作用、区别、联系

equals 和hashcode都是用于判断对象是相等的
实际上,hashcode是根据对象的内存地址经哈希算法得来的。
hashcode类似于布隆过滤器,如果hashcode不相同,对象一定不等,但是相同不一定相等,所以先用hashcode判断是否相等,如果确实不同,就不需要用equals了,只有hashCode相等才需要再用equals比较

为什么重写equals一定要重写hashcode

实际上,hashcode是根据对象的内存地址经哈希算法得来的。,equal比较的是对象内存地址
如果equals相等,理论上hashcode需要相等的。重写equals方法,改变比较方式,但是内存地址不同的时候hashcode会不同,此时产生矛盾
hashCode 就是要求如果相等的两对象( equals返回true),那么其hashCode要相等。

针对String作为一个基本类型来使用:

1。如果String作为一个基本类型来使用,那么我们视此String对象是String缓冲池所拥有的。
2。如果String作为一个基本类型来使用,并且此时String缓冲池内不存在与其指定值相同的String对象,那么此时虚拟机将为此创建新的String对象,并存放在String缓冲池内。
3。如果String作为一个基本类型来使用,并且此时String缓冲池内存在与其指定值相同的String对象,那么此时虚拟机将不为此创建新的String对象,而直接返回已存在的String对象的引用。

针对String作为一个对象来使用:

1。如果String作为一个对象来使用,那么虚拟机将为此创建一个新的String对象,即为此对象分配一块新的内存堆,并且它并不是String缓冲池所拥有的,即它是独立的。

string不能修改final

String类被final关键字修饰,意味着String类不能被继承,并且它的成员方法都默认为final方法;字符串一旦创建就不能再修改。

string 大量循环+连接符操作效率问题

string+连接符操作底层用了stringbuilder的append操作,然后tostring
大量循环操作+源码

String s = "abc";
for (int i=0; i<10000; i++) {
    
    
    s += "abc";
}

/**
 * 反编译后
 */
String s = "abc";
for(int i = 0; i < 1000; i++) {
    
    
     s = (new StringBuilder()).append(s).append("abc").toString();    
}

会在堆内存形成大量stringbuilder对象,所以在这种情况下建议在循环体外创建一个StringBuilder对象调用append()方法手动拼接

/**
 * 循环中使用StringBuilder代替“+”连接符
 */
StringBuilder sb = new StringBuilder("abc");
for (int i = 0; i < 1000; i++) {
    
    
    sb.append("abc");
}
sb.toString();

为什么string用常量池?

string使用特别多,需要分配内存空间,JVM为了提高性能和减少内存的开销,jvm优化,使用字符串常量池。

如何使用常量池?

每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,常量池中一定不存在两个相同的字符串。

jdk7后常量池变化

jdk7之前将常量池放在方法区
jdk7之后将常量池放在堆,便于扩展内存,扩展参数命令**-XX:StringTableSize=66666**

string不可变的原理?

string类源码

public final class String implements Serializable, Comparable<String>, CharSequence {
    
    
    private final char[] value;
    private int hash;

类是final的不可继承,其属性,string的具体数据结构数组是final的不能改变内存地址,而不是里面的具体内容数据。

所以,其实string中的字符串是可以改变的
修改引用:

final int[] value={
    
    1,2,3}
int[] another={
    
    4,5,6};
value = another;//编译器报错,final不可变

修改具体内容

final int[] value={
    
    1,2,3};
value[2]=100;//这时候数组里已经是{1,2,100}

string为什么不可变,为什么其他final的引用数据类型可变??

String是不可变,是因为 private final char value[]这一句里没有去动Array里的元素,没有暴露内部成员字段。 内部数据无法修改

final的hashmap是否可变?

引用不可变,但是具体的数值内容是可变的

string不可变的优点?

安全且高效!!!!
安全:因为不可变对象不能被改变,所以他们可以自由地在多个线程之间共享。不需要任何同步处理。
高效:string的hash值也会被缓存,而hashmap,hashtable的key都是用string类型,获取key的hash值可以直接取缓存中的值

string不能重写的原因?

string的方法是final的,不能重写改变。

string jdk6和jdk7的区别?

stringjdk6将常量池放在方法区(永久代),但是由于永久代容易内存溢出
stringjdk7放在堆中

在使用 HashMap 的时候,用 String 做 key 有什么好处?

HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。

String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的

可变性
StringBuilder与StringBuffer 这两种对象都是可变的。

线程安全性
String是不可变的,线程安全的
StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。

final 修饰类,属性,方法 有什么用?

修饰类、属性和方法;
被final修饰的类不可以被继承
被final修饰的方法不可以被重写
被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,即内存地址,而不是引用指向的内容,引用指向的内容是可以改变的

重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分

重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。

接口是否可继承接口? 抽象类是否可实现(implements)接口?

接口可以继承接口,接口可以多继承

构造器Constructor是否可被重写

只能重载,因为构造器是private的,不能被继承

值传递和引用传递有什么区别

值传递:传递的是值的拷贝。
引用传递:传的是也变量所对应的内存空间的地址。

什么是java序列化,如何实现java序列化?,为什么要序列化serialVersionUID 是什么

原因:Java对象需要持久化到数据库或者用网络传输时做序列化
实现:实现Serializable 接口就可以序列化
private static final long serialVersionUID = 1L;
serialVersionUID 会写在序列化文件中,用来判断序列化文件是否失效
不写的话,jvm会生成一个。

java反射是什么

将类抽象为CLASS对象 类的属性,方法,构造器都抽象为对象,可以用class类做一个调用
所以class类调用构造器对象的instance方法可以直接生成对象

反射构造类的步骤

1.获取类对象 Class class = Class.forName(“pojo.Hero”);
2.获取构造器对象 Constructor con = clazz.getConstructor(形参.class);
3 获取对象 Hero hero =con.newInstance(实参);

静态变量和普通变量区别? 方法区,static final

静态变量是static修饰的变量,和静态方法一样,在类加载的时候就初始化在方法区,只有一份

java中的编译器和解释器,程序执行过程

java源代码->编译器->字节码.class文件->jvm解释器->二进制机器码 解释执行

什么是jvm

Java虚拟机模拟计算机,是虚拟计算机,有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。

1、 jvm内存模型

程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。
本地方法栈:和虚拟栈相似,只不过它服务于Native方法,线程私有。
Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,线程共享。
方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,各线程共享
在这里插入图片描述

内存模型中的Java虚拟栈->栈帧(局部变量表,操作数栈,动态链接,方法出口)

虚拟机栈的结构图:
每个线程一个栈
每个栈里包含:多个栈帧
每个栈帧里包含:局部变量表、操作数栈、动态链接、方法出口

在这里插入图片描述

局部变量表和操作数栈是什么?

存放局部变量,和对对象的引用,和返回地址
在这里插入图片描述

什么是局部变量表中对象的引用?

通过引用在栈中的app变量引用堆中的App对象
在这里插入图片描述
jvm虚拟机内存分析透彻博文---------

java虚拟机的生命周期

启动main方法时生命周期开始,只要任意非守护线程没有结束,虚拟机都不结束

启动了几个main函数就会启动几个Java虚拟机

为什么是本地方法

这类方法存在的意义当然是填补java代码不方便实现的缺陷而提出

为什么要垃圾回收

防止程序员人为失误忘记手动释放不需要用的对象。造成内存溢出

垃圾回收如何判断对象是否需要被回收

1、引用计数法,在对象内部通过计数器记录被引用的次数。但是无法解决循环引用的问题
2、可达性分析法 通过gc root 对象寻找所有被引用的对象,没有在引用链上的对象可以删除

可达性分析哪些对象可以作为gc root?

1、虚拟机栈栈帧中,局部变量表中的变量引用的对象
2、方法区的静态变量引用的对象
3、本地方法栈中本地方法引用的对象

关于引用的定义

Java中引用的定义很传统: 如果引用类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用

1、强引用:Object obj = new Object() gc不会删除
2、软引用:SoftReference 内存溢出之前进行回收
3、弱引用:weakReference 二次回收会回收
4、虚引用:虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null

类型 回收时间 应用场景
强引用 一直存活,除非GC Roots不可达 所有程序的场景,基本对象,自定义对象等
软引用 内存不足时会被回收 一般用在对内存非常敏感的资源上,用作缓存的场景比较多,例如:网页缓存、图片缓存
弱引用 只能存活到下一次GC前 生命周期很短的对象,例如ThreadLocal中的Key。
虚引用 随时会被回收, 创建了可能很快就会被回收 可能被JVM团队内部用来跟踪JVM的垃圾回收活动

finalize()方法

jvm垃圾回收之前会调用方法清理对象

对象生存还是死亡?对象回收可达性分析和finalize()方法 二次死亡

第一次死亡:对象不在可达性分析后的引用链上
第二次死亡:对象的类没有实现finalize()方法 或者已经执行过finalize()方法

jvm 如果对象不在引用链,会查看是否finalize(),如果已经finalize()则直接回收
如果没有finalize(),会放入f队列顺序执行finalize()方法
如果finalize()能够将对象重新加入引用链,则对象复活

垃圾回收算法

1、标记-清除
通过可达性分析,标记不在引用链上的对象一次性删除
缺点:产生大量内存碎片,对象存放后还是容易gc
在这里插入图片描述

2、复制-清除
原理:为了解决标记清楚的问题,将内存 分为两块,每次使用其中一块,垃圾回收时将不需要删除的对象复制到另一块,删除其余半块
缺点:可用内存小一半
在这里插入图片描述

现在的商用虚拟机(包括HotSpot)都是采用这种收集算法来回收新生代

98%的对象是很快死的,不需要半块内存
将新生代内存按8:1:1分为伊甸区和两个survivor区 两个Survivor区域一个称为From区,另一个称为To区域
回收时将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间
所以每次新生代可用内存空间为整个新生代容量的90%,而剩下的10%用来存放回收后存活的对象

HotSpot实现的复制算法流程

1、伊甸区满了触发minor gc ,把存活对象复制到from。
再次minor gc 把伊甸区和from区都清理,存活的对象放在to区
2、伊甸区再次minor gc 会清理伊甸区和to区。把存活对象放在from区
3、对象在from和to交换了15次,就存入老年代 JVM参数MaxTenuringThreshold决定,这个参数默认是15

为什么要有两个survivor区?

1、一次minor gc后会将存活对象放到to区,第二次再放到from区,15次交换还没有被回收的对象才放到老年代。减少对象移入老年代,减少full gc
2、保证新生代一直有可用的连续空间

标记-整理算法

对于老年代对象,对象存活高,复制删除开销大,用标记整理方法,将对象整理到内存的一端。

分代收集算法

分代收集就是上面说的,把内存分为新生代和老年代
新生代对象存活率低,用复制删除法
老年代对象存活率高,用标记整理法

Minor GC和Full GC么,这两种GC有什么不一样吗?

Minor GC时新生代的gc
Full GC 清理整个内存堆 – 包括年轻代和年老代。
Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次Minor GC,比Minor GC慢10倍以上。

什么时候触发minor gc?

老年代的最大连续空间大于新生代的空间
新生代的空间满了

什么时候触发full gc?

老年代的空间不足

调用system.gc() 为什么又可能不触发gc?

因为system.gc只是告诉jvm可以开始gc,但是实际由判断,符合条件才会执行
不要用这个函数,避免增加full gc的频率

工作中如何避免gc?

1、对象初始化之前置为null
为null的对象一般会被判断为垃圾,更利于直接删除
2、不要用system.gc
这个函数会增加full gc 的频率
3、不要用静态变量
静态变量存在方法区,其引用会一直占用内存
4、不要用string累加字符串,用stringbuffer累加字符串
string累加字符串,会产生大垃圾对象,占满内存
5、配置上增大-Xmx的值

jmm 内存模型

Java内存模型存在意义基于Java并发
jvm有主内存,共享数据存放在主内存中
每个线程有工作内存,只能将主内存数据拷贝到工作内存中进行操作。
线程无法直接操作主内存中的数据

jvm调优对gc的监控,gc什么情况属于异常?

1、full gc 时间越来越长,甚至5秒
2、full gc 次数越来越频繁,甚至1分钟一次

gc的参数设置的几种例子

1、设置堆大小的参数-xmx -xms 一般设置为相同,防止来回变化
2、设置新生代大小-XX:newsize -XX:MaxnewSize 一般设置为相同,防止来回变化
3、-XX:NewRatio 1:2设置年轻代与老年代在堆结构的占比
4、可以为年老代选择并行收集算法: -XX:+UseParallelOldGC
5、对象放入老年代最大年龄-XX:MaxTenuringThreshold=15

年轻代和年老代设置多大才算合理

项目中新对象多的新生代设置大一些,持久化大对象多的,设置老年代大一些
为了减少full gc 希望能够尽量持久化常用对象在老年代中,所以可以把老年代设置大一些

1)更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC

2)更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率

jvm垃圾收集串行和并行和并发方式

1、串行
一个线程
系统会暂停
-XX:+UseSerialGC
2、并发
多个线程
系统会暂停
-XX:+UseParNewGC(新生代使用并行收集器,老年代使用串行回收收集器)
关键字(ParNew)
-XX:+UseParallelGC(新生代使用并行回收收集器,老年代使用串行收集器)
-XX:+UseParallelOldGC(新生代,老年代都使用并行回收收集器)
关键字(PSYoungGen)
3、并行
多个线程
系统不会暂停
-XX:+UseConcMarkSweepGC(设置年老代为并发收集,年轻代并行,适合于响应要求高的系统)

jvm垃圾收集器cms和G1

cms收集器
基于标记-删除,并发, 低停顿。会基于安全点(程序停顿无影响处)进行垃圾收集

G1收集器
基于标记整理
分段收集,并发,内存碎片小
可以预测系统停顿

指令重排和happens-before原则

可以对指令重排提高效率
但重排必须遵循happens-before原则,如果b的结果依赖a的结果,不是说a-b不能变成b-a,而是如果变成b-a会改变执行结果,指令就不能重排

happens-before原则定义

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

happens-before原则规则:

程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
必须要小lock,才能进行unlock操作
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
变量一定要先读,然后才能写
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
一个对象一定要先初始化,才能finalize()

这里的一定和顺序,是因为不按照这个顺序,会发生错误,因为不按照顺序根本是不合逻辑的啊。

什么是单例模式

一个单例的类只能自己创建一个实例

为什么有单例模式

线程池,缓存,日志等对象占用空间较大的对象,希望是单例的

单例模式的实现代码

public class SingleInstence{
    
    
 	private volatile static SingleInstence instence = null;

	private SingleInstance(){
    
    }
	
	private static SingleInstence getInstance(){
    
    
		 if(instence == null){
    
    
			     synchronized(SingleInstence.class){
    
    
			     if (instence == null){
    
    
					instence  = new SingleInstance();
					}
			     }
			  }
		}
}

进程和线程的区别?

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

一个java程序至少有几个线程?

一个java应用程序至少有两个线程,一个是主线程(main),一个是执行垃圾回收的线程

java主类,主线程,run和start

java主类是包含main方法的类,主线程为main线程,还会有垃圾回收线程

守护线程和非守护线程

例子
1、敲键盘写稿子是非守护线程,错误检查器是守护线程,当不写稿子的时候,就没必要有守护线程了
2.就像 城堡门前有个卫兵 (守护线程),里面有诸侯(非守护线程),他们是可以同时干着各自的活儿,但是 城堡里面的人都搬走了, 那么卫兵也就没有存在的意义了。

守护线程是为了其他线程的运行提供服务,比如说GC线程
main主线程和其他线程是非守护线程

实现线程的四种方法

1、继承Thread类,重写run方法
2、实现java.lang.Runnable接口,实现run()方法
3、通过线程池创建
4、通过Callable和Future创建线程

实现线程中的4种方法哪种更好?

实现runnable接口更好 因为java支持实现多个接口,但是只能继承一个类

run和start的区别

start可以使线程就绪,实现多线程
run只是执行线程中的方法,使围绕单一线程的

线程的五种状态

1、创建,new线程对象,会生成新线程,分配空间
2、对象调用start方法,线程进入就绪状态
3、运行,cpu调度就绪的线程,执行其重写的run方法
4、阻塞,sleep,wait等调用
5、死亡,run执行完成,或者stop就死亡了,用start无法重新开启线程

线程的wait和sleep方法的区别

sleep(),interrupt(),是Thread类的方法,是让线程睡眠一段时间,作用于线程,不用释放同步代码块的锁。
wait()和notify(),wait作用于对象,wait必须在同步代码块中调用,需要释放对象的锁。线程进入等待就绪的状态,当别的线程执行完,获取完锁,释放,用notify或者notifyall方法,线程才被唤醒,进入就绪状态

一个线程结束的标志是:run()方法结束。

为什么要有线程池

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

如何创建线程池?

1、 new ThreadPoolExecutor
2、threadPool.execute
3、threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
4、threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

如何终止线程池?

shutdown,温柔终止,终止前会把任务执行完
shutdownnow,暴力终止,不会执行提交了没执行完的任务

线程池的5个状态

running 接受新任务并接受排队的任务
SHUTDOWN:不接受新任务,但处理排队的任务。
STOP:不接受新任务,不处理排队的任务,并中断正在进行的任务。
TIDYING:所有任务都已终止,workerCount 为零,线程转换到 TIDYING 状态将运行 terminated() 钩子方法。
TERMINATED:terminated() 已完成。

线程池的工作流程

有新任务,没有到达核心线程数,就创建工作线程
如果到了核心线程数,还没到最大线程数,加入阻塞队列
如果大于最大线程数,执行拒绝策略

常用的阻塞队列

基于数组的有界队列,先进先出
基于链表的有界或无界队列,先进先出
具有优先级的无界队列,按照优先级对队列排序

两种常见的阻塞队列情况

有界队列,队列可能会满
无界队列,可能会内存溢出

线程池有哪些拒绝策略?

中止策略。默认的拒绝策略,抛出异常
抛弃策略。什么都不做,直接抛弃被拒绝的任务。
抛弃最老策略。抛弃阻塞队列中最老的任务
调用者运行策略。不会抛出异常或者什么都不做,而是回归调用者

线程池各个参数的作用

threadFactory(线程工厂):用于创建工作线程的工厂。
corePoolSize(核心线程数):当线程池运行的线程少于 corePoolSize 时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态。
workQueue(队列):用于保留任务并移交给工作线程的阻塞队列。
maximumPoolSize(最大线程数):线程池允许开启的最大线程数。
handler(拒绝策略):往线程池添加任务时,将在下面两种情况触发拒绝策略:1)线程池运行状态不是 RUNNING;2)线程池已经达到最大线程数,并且阻塞队列已满时。
keepAliveTime(保持存活时间):如果线程池当前线程数超过 corePoolSize,则多余的线程空闲时间超过 keepAliveTime 时会被终止。

4个功能型线程池

嫌上面使用线程池的方法太麻烦?其实Executors已经为我们封装好了4种常见的功能线程池,如下:

定长线程池(FixedThreadPool),控制线程的最大并发数
定时线程池(ScheduledThreadPool ),执行定时的任务
可缓存线程池(CachedThreadPool),执行数量多,耗时少的任务
单线程化线程池(SingleThreadExecutor),不适合并发,保证任务在一个线程执行

不建议使用executor的四个线程,而是建议直接使用ThreadPoolExecutor的方式, 避免oom

4个功能线程池的缺点

**FixedThreadPool和SingleThreadExecutor:**主要问题是堆积的请求处理队列均采用LinkedBlockingQueue,可能会耗费非常大的内存,甚至OOM。
**CachedThreadPool和ScheduledThreadPool:**主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

什么是ThreadLocal

ThreadLocal有ThreadLocalMap, 把线程需要的共享变量创建副本,每个线程拥有副本。
可以用get,set方法可以设置获取变量的值。
在线程结束之前,一定要用remove删除ThreadLocalMap中的这个节点

什么是几种引用?

强引用:new,对象不被gc
软引用:内存不足才gc;
弱引用:只要gc就回收
虚引用:为寻找对象提供路径

为什么key的弱引用会造成内存泄漏

ThreadLocalMap中的entry中的key使用了threadLocal的弱引用。
当threadLocal被垃圾回收,key作为弱引用也会被回收,此时key是null,但是value还在
由于ThreadLocalMap到entry到value的引用链存在,value就永不会被回收,造成内存泄漏

使用ThreadLocal时会发生内存泄漏的前提条件(弱引用)

1、ThreadLocal引用变量被设置为null,没有remove
2、线程池中线程一直运行
3、此时触发了gc

为了防止内存泄漏,必须使用ThreadLocal时遵守以下两个小原则

1、ThreadLocal申明为private static final。
Private与final 尽可能不让他人修改变更引用,
Static 表示为类属性,只有在程序结束才会被回收。
2、如果是线程池,ThreadLocal使用后务必调用remove方法。

ctl,32bit的原子整数

ctl=线程运行状态+线程有效数量
int 类型有32位,其中 ctl 的低29为用于表示 workerCount,高3位用于表示 runState

ctl 为什么这么设计?有什么好处吗?

ctl 这么设计的主要好处是将对 runState 和 workerCount 的操作封装成了一个原子操作。
保证对线程运行状态和该状态下线程数的修改是一个原子性的

线程池的大小配置多少合适?线程池的参数配置要注意什么

对于计算密集型,设置 线程数 = CPU数 + 1,通常能实现最优的利用率。
对于I/O密集型,网上常见的说法是设置 线程数 = CPU数 * 2
还是按照cpu数和线程执行时间判断

集合体系

list,set,map,其中list和set是继承collection的,map是单独的是双列集合
在这里插入图片描述

1、单列集合List和set的区别

list集合元素有序并且集合元素可以重复
set集合元素无序并且集合元素不允许重复

2、单列集合Arraylist和Linkedlist的区别

arraylist 基于数组有索引下表,查询快,根据下标查询,插入删除慢,需要依次移动后面元素
linkedlist 基于双链表,没有索引 查询慢,插入删除快

java.util.Collections.SynchronizedList是线程安全的吗

基于 synchronized (mutex) {list.add(index, element);}实现了线程安全,是操作系统底层的线程安全

Collections.synchronizedMap是怎么实现线程安全

底层的synchronized(muntex)

HashMap的数据结构?

1.8之前数组+链表
1.8之后*数组+链表改成了数组+链表或红黑树
;**

hashmap默认值和构造函数

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 1 默认初始化的容器大小16;
static final int MAXIMUM_CAPACITY = 1 << 30; //最大的数据容量2的30次方。也就是说最多存放2的30次方个数据;
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认的加载因子 0.75f
散列表的加载因子=填入表中的元素个数/散列表的长度,加载因子越大,说明空闲位置越小,冲突越多,散列表的性能会下降。

hashMap的put方法过程 重要!!!!!!!

1、判断数组是否为空,为空进行初始化;
2、不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;
3、查看 table[index] 是否存在数据,没有数据就构造一个Node节点存放在 table[index] 中;
存在数据,说明发生了hash冲突(存在二个节点key的hash值一样), 继续判断key是否相等,相等,用新的value替换原数据(onlyIfAbsent为false);
4如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;(如果当前节点是树型节点证明当前已经是红黑树了)
5、如果不是树型节点,创建普通Node加入链表中;判断链表长度是否大于 8并且数组长度大于64, 大于的话链表转换为红黑树;
6、插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。

hashMap删除数据

如果在链表, p.next = p.next.next
e.key = null
e.value = null

hashMap查询的效率

因为直接根据key计算的hash下标获取,所以O(1)

HashMap怎么设定初始容量大小

new HashMap() 不传值,默认大小是16,负载因子是0.75, 如果自己传入初始大小k,初始化大小为 大于k的 2的整数次方,例如如果传10,大小为16。

为什么采用hashcode的高16位和低16位异或能降低hash碰撞?hash函数能不能直接用key的hashcode?

因为key的hashcode返回的是int类型,int值范围为**-2147483648~2147483647**,前后加起来大概40亿的映射空间 如果hashmap的初始空间是16,需要用数组长度取模运算,得到余数作为存数据的下标

源码中模运算就是把散列值和数组长度-1做一个"与"操作,位运算比取余%运算要快。
return h & (length-1);

为什么HashMap的数组长度要取2的整数幂

(数组长度-1)正好相当于一个“低位掩码”。

HashMap的扰动函数(hash函数)是怎么设计的?

用key的hashCode 高低十六位按位做异或

为什么这么设计?

位运算高效,能够更散列

为什么要扰动多次?

利用位运算增加随机性,但是后期hashmap没有4次,只有一次

散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。!!!
所以 扰动函数”的价值 如下
在这里插入图片描述
右移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

Java1.8相比1.7做了调整,1.7做了四次移位和四次异或,但明显Java 8觉得扰动做一次就够了,做4次的话,多了可能边际效用也不大,所谓为了效率考虑就改成一次了。

到1.8对hash函数做了优化,1.8还有别的优化吗?

1.8还有三点主要的优化:
1、数组+链表改成了数组+链表或红黑树
2、链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;
扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
3、在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容

为什么要做这几点优化?

1、止发生hash冲突,链表长度过长,将时间复杂度由O(n)降为O(logn);
2、因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;

A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环,如下图所示:

你前面提到链表转红黑树是链表长度达到阈值,这个阈值是多少?

阈值是8,红黑树转链表阈值为6

为什么是8,不是16,32甚至是7 ?又为什么红黑树转链表的阈值是6,不是8了呢?

1、因为经过计算,在hash函数设计合理的情况下,发生hash碰撞8次的几率为百万分之6,概率说话。。因为8够用了,
2、至于为什么转回来是6,因为如果hash碰撞次数在8附近徘徊,会一直发生链表和红黑树的互相转化,为了预防这种情况的发生。

HashMap内部节点是有序的吗?

是无序的,根据hash值随机插入

HashMap和HashTable 的异同?

key不同:HashTable 中 key和 value都不允许为 null,而HashMap中key和value都允许为 null
**实现方式不同:**Hashtable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。
初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。
扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。
迭代器不同:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。

为啥 Hashtable 是不允许 KEY 和 VALUE 为 null, 而 HashMap 则可以呢?

因为HashTable插入空值会抛异常

那有没有有序的Map?

LinkedHashMap 和 TreeMap

TreeMap 和LinkedHashMap的数据结构

都是有序的LinkedHashMap使用的是双向链表,在插入的时候保证有序
treeMap使用的是红黑树,可以重写compartor来实现排序

LinkedHashMap可实现LRU

手写LRU算法

那HashMap是线程安全的吗?

不是

线程不安全的问题有哪些类解决

HashTable、Collections.synchronizedMap、ConcurrentHashMap

1、HashTable是直接在操作方法上加synchronized关键字**,锁粒度大,比较低效率
2、Collections.synchronizedMap,也是synchronized,利用moniter的mutex enter和mutex exit
3、ConcurrentHashMap使用分段锁,降低了锁粒度,让并发度大大提高。
1.7:采用了分段锁的机制,当一个线程占用锁时,不会影响到其他的Segment对象
1.8:抛弃了原来的分段锁,采用了 CAS 和 synchronized 来保证并发的安全

那你知道ConcurrentHashMap的分段锁的实现原理吗?

ConcurrentHashMap成员变量使用volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用CAS操作和synchronized结合实现赋值操作,多线程操作只会锁住当前操作索引的节点
如下图,线程A锁住A节点所在链表,线程B锁住B节点所在链表,操作互不干涉。

为什么要用ConcurrentHashMap?

hashmap线程不安全,在插入高并发时,链表可能形成环
hashtable线程安全,但是用synconized锁粒度太粗,锁是os级别的,效率不高

ConcurrentHashMap 线程安全怎么做的?

1.7:采用了分段锁的机制,当一个线程占用锁时,不会影响到其他的Segment对象
1.8:抛弃了原来的分段锁,采用了 CAS 和 synchronized 来保证并发的安全

CAS是啥?ABA是啥?场景有哪些,怎么解决?

CAS: 是英文单词CompareAndSwap的缩写
是乐观锁的一种实现方式,是一种轻量级锁
线程在读取数据时不进行加锁,在准备写回数据时,比较原值是否修改,

CAS的缺点:

1、循环时间长开销cpu很大。
2、只能保证一个变量的原子操作。
3、ABA问题。

什么是ABA问题?

a被另一个线程修改为原来的值 当线程对值的修改过程中,另一个线程也对这个值进行了修改,并把它改为了原来的值 此时,这个值已经不是和原的值了

ABA问题怎么解决?

版本号+时间戳

synchronized 原理

任何一个对象都一个Monitor与之关联
Synchronized在JVM里的实现都是基于进入和退出Monitor对象
通过成对的MonitorEnter和MonitorExit指令

synconized关键字底层原理,为什么叫重量级锁

synconized底层基于监视器锁monitor。将代码反编译后可以看到底层是基于操作系统的mutex锁。
操作系统完成线程切换需要在用户态和内核态之间切换,消耗比较大

Synchronized实现同步的几种方式

锁住方法:锁是当前实例对象
锁住静态方法:锁是当前类的Class对象,因为静态方法不属于某一个实例,而是属于全部的
同步方法块:锁是Synchronized括号里配置的对象,也是对象锁

synchronized锁升级策略

1.先使用 偏向锁 优先同一线程然后再次获取锁
2.如果失败,就升级为 CAS 轻量级锁,失败就会短暂自旋,防止线程被系统挂起。
3.最后如果以上都失败就升级为重量级锁。

synconized锁升级的变化,偏向锁

默认一个对象做 syncronized 的时候,是加偏向锁,对象头的markwork 8个字节 会记录当前线程 ID

轻量级锁(自旋锁)

当有线程id和当前对象头中保存的id不同的线程争抢时,锁升级为自旋锁。线程自旋等待

自旋锁的问题

线程自选需要消耗cpu资源,不适合执行时间长的线程

重量级锁

线程竞争加剧,jdk1.6后可以配置jvm线程数的参数
会升级为重量级锁,线程加入等待队列,线程挂起,等待操作系统重新唤起

synconized是否可重入

synconized是重量级锁,是可重入的,原子的

synchronized 和 Lock 的区别、使用场景

Synchronized是内置的java关键字,Lock是一个java类。
Synchronized无法判断是否获取到了锁,Lock可以判断是否获取到了锁。
Synchronized会自动释放锁,Lock必须手动释放锁。(不可中断问题
Synchronized不可中断,线程获取了之后不释放会引发死锁
Synchronized适合锁少量的代码同步问题。Lock适合锁大量的同步代码

两个线程同时访问两个对象的非静态同步方法能保证线程安全吗?

不能,每个对象都拥有一把锁。两个对象相当于有两把锁,导致锁对象不一致。(PS:如果是类锁,则所有对象共用一把锁)

若synchronized方法抛出异常,会导致死锁吗?

JVM会自动释放锁,不会导致死锁问题

若synchronized的锁对象能为空吗?会出现什么情况?

锁对象不能为空,否则抛出NPE(NullPointerException)

在开发过程中,你经常使用synchronized方法多还是synchronized代码块?and why?

synchronized同步的范围是越小越好。因为若该方法耗时很久,那其它线程必须等到该持锁线程执行完才能运行。(黄花菜都凉了都…)
而synchronized代码块部分只有这一部分是同步的,其它的照样可以异步执行,提高运行效率。

synchronized方法多还是synchronized代码块底层的区别?

synchronized方法jvm会有 access_flags中多了一个ACC_SYNCHRONIZED,告知VM这是个同步方法.
synchronized代码块则是用monitor监视器,在代码块执行的入口和出口字节指令增加mutex enter和exit实现的

有没有遇到过synchronized失效的问题?

synchronized 虽然用法简单,但是如果锁对象不一致,就会失效。排查问题的时候一定要着重从锁对象是否一致上去判断.

AQS的底层实现原理

AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。其实就是个双端双向链表。AQS是JUC中很多同步组件的构建基础
AbstractQueuedSynchronizer

ReentrantLock的调用过程

该类继承了AbstractQueuedSynchronizer:
Sync又有两个子类:
final static class NonfairSync extends Sync view plain
final static class FairSync extends Sync
为了支持公平锁和非公平锁而定义,默认情况下为非公平锁。

可重入锁ReentrantLock公平锁和非公平锁的设置

构造函数true false,默认为非公平锁

ReentrantLock和aqs

ReentrantLock是个典型的独占模式AQS,同步状态为0时表示空闲。当有线程获取到空闲的同步状态时,它会将同步状态加1

什么是aqs

aqs是一个基于先进先出队列和同步信号量的多线程同步框架

公平锁的实现

公平锁,继承fairSync方法,线程在等待队列中排序,只有当前线程是队列首线程,才能够获取锁。tryAuire,等待队列中的线程尝试获取锁

非公平锁的实现

继承noFairSync方法,线程在aqs队列中,基于cas操作获取锁,刚切换的线程,也能获取锁。

ReentrantLock与Synchrionized的区别

ReentrantLock支持等待可中断,可以中断等待中的线程
ReentrantLock可实现公平锁
ReentrantLock可实现选择性通知,即可以有多个Condition队列

介绍下悲观锁和乐观锁

悲观锁synconized 乐观锁cas+自旋锁 操作

怎样写一个程序,快速打满方法区;
让你设计一下可达性分析,思路大概是什么;可重复读有用到锁

乐观锁 cas atomicInteger

在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

volatile关键字作用

防止指令重排造成执行语句顺序问题,可见性,

JDK 中的并发包中有哪些类

countDownLanc、CyclicBarrier、Semaphore、Exchanger

1、CountDwonLatch(闭锁) 以及步骤

1、创建一个CountDwonLatch对象 ,初始化要等待线程的数量
2、被等待的线程执行完后调用countDwon()方法让计数器减1
3、在等待其他线程的主线程中,调用await()方法来等待其他调用了countDwon()的线程,直到计数器为0,再执行该线程接下来的逻辑

2、CyclicBarrier(同步屏障)

1、要同时阻塞多个线程,先初始化数量
2、等所有线程都达到同步屏障,所有线程才开始进入就绪状态

3、Semaphore(信号量)

Semaphore用于流量控制,用于控制访问特定资源的线程数量,通过协调各个线程,以保证合理的使用公共资源。
1、创建一个Semaphore对象,并传入一个int类型的参数,初始化通行证数量;
2、在要占用公共资源的子线程业务逻辑前,调用s.acquire( )方法获得通行证,在实现业务逻辑后,调用release()方法释放通行证;

4、Exchanger(交换者)

设置一个同步点,两个线程都达到同步点,就可以交换数据

1、创建一个Exchanger对象;
2、在要交换(同步)数据的同步点调用excr.exchange( )方法

并发工具类的使用场景

1、CountDwonLatch:一个线程等到其它几个线程执行完才能执行的场景;
2、CyclicBarrier:需要多线程的计算结果最后对这些结果进行合并的场景;
3、Semaphore:公共资源有限而并发线程较多的场景;
4、Exchanger:需要数据交换共享的场景,例如遗传算法中

ReadWriteLock

读读不互斥,读写互斥,写写互斥 适合于读多写少的场景
获取写锁的前提是读锁和写锁均未被占用
获取读锁的前提是没有其他线程占用写锁

猜你喜欢

转载自blog.csdn.net/u010010600/article/details/109099327
今日推荐