java 技术学习文档

1. HashMap原理

jdk8后采用数组+链表+红黑树的数据结构,利用元素的key的hash值对数组长度取模得到在数组上的位置。当出现hash值一样的情形,就在数组上的对应位置形成一条链表。据碰撞越来越多大于8的时候,就会把链表转换成红黑树。

2. HashMap中put()如何实现的

https://blog.csdn.net/qq_38182963/article/details/78942764

1.Key.hashCode和无符号右移16位做异或运算得到hash值,取模运算计算下标index

对key的hashCode 和右移16位做异或运算,之后hash(key) & (capacity - 1)做按位与运算得到下标。

2.下标的位置没有元素说明没有发生碰撞,直接添加元素到散列表中去

3.如果发生了碰撞(hash值相同),进行三种判断

4.1:若key地址相同或equals相同,则替换旧值

4.2:key不相等,如果是红黑树结构,就调用树的插入方法

4.3:key不相等,也不是红黑树,循环遍历直到链表中某个节点为空,用尾插法(1.8)/头插法(1.7)创建新结点插入到链表中,遍历到有节点哈希值相同则覆盖,如果,链表的长度大于等于8了,则将链表改为红黑树。

4.如果桶满了大于阀值,则resize进行扩容

3. HashMap中get()如何实现的

1.Key.hashCode的高16位做异或运算得到hash值,取模运算计算下标index

2.找到所在的链表的头结点,遍历链表,如果key值相等,返回对应的value值,否则返回null

4.为什么HashMap线程不安全

1.多线程put的时候可能导致元素丢失

2.put非null元素后get出来的却是null

3.多线程扩容,引起的死循环问题

1.put的时候会根据tab[index]是否为空执行直接插入还是走链表红黑树逻辑, 并发时,如果两个put 的key发生了碰撞,同时执行判断tab[index]是否为空,两个都是空,会同时插入,就会导致其中一个线程的 put 的数据被覆盖。

2.元素个数超出threshold扩容会创建一个新hash表,最后将旧hash表中元素rehash到新的hash表中, 将旧数组中的元素置null。线程1执行put时,线程2执行去访问原table,get为null。

3.在扩容的时候可能会让链表形成环路。原因是会重新计算元素在新的table里面桶的位置,而且还会将链表翻转过来。 多线程并发resize扩容,头插造成了逆序A-B 变成了C-B ,t1执行e为A,next为B挂起, t2执行完毕导致B指向A,继续执行t1,他继续先头插eA,再头插nextB, 由于t2程导致B后面有A,所以继续头插, A插到B前面,出现环状链表。 get一个在这个链表中不存在的key时,就会出现死循环了。

https://juejin.im/post/6844903796225605640#heading-5

https://coolshell.cn/articles/9606.html/comment-page-3#comments

https://www.iteye.com/blog/firezhfox-2241043

5.HashMap1.7和1.8有哪些区别

参考: https://blog.csdn.net/qq_36520235/article/details/82417949

由数组+链表的结构改为数组+链表+红黑树。

优化了高位运算的hash算法:h^(h>>>16)

扩容后,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变。

头插改为尾插

(1)由 数组+链表 的结构改为 数组+链表+红黑树 。

拉链过长会严重影响hashmap的性能, 在链表元素数量超过8时改为红黑树,少于6时改为链表,中间7不改是避免频繁转换降低性能。

(2) 优化了高位运算的hash算法

h^(h>>>16)将hashcode无符号右移16位,让高16位和低16位进行异或。

(3)扩容 扩容后数据存储位置的计算方式也不一样

1.7是直接用hash值和需要扩容的二进制数进行与操作,1.8(n-1)&hash,位运算省去了重新计算hash,只需要判断hash值新增的位是0还是1,0的话索引没变,1的话索引变为原索引加原来的数组长度 ,且链表顺序不变。

(4)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法。

因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序链表形成环路导致死循环问题。 但是在JDK1.8之后是因为加入了红黑树,使用尾插法,能够避免出现逆序且链表死循环的问题。

6.解决hash冲突的时候,为什么用红黑树

链表取元素是从头结点一直遍历到对应的结点,这个过程的复杂度是O(N) , 而红黑树基于二叉树的结构,查找元素的复杂度为O(logN) , 所以,当元素个数过多时,用红黑树存储可以提高搜索的效率。

7.红黑树的效率高,为什么一开始就用红黑树存储呢?

红黑树虽然查询效率比链表高,但是结点占用的空间大,treenodes的大小大约是常规节点的两倍 只有达到一定的数目才有树化的意义,这是基于时间和空间的平衡考虑。 如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

8.不用红黑树,用二叉查找树可以不

https://blog.csdn.net/T_yoo_csdn/article/details/87163439

但是二叉查找树在特殊情况下会变成一条线性结构

如果构建根节点以后插入的数据是有序的,那么构造出来的二叉搜索树就不是平衡树,而是一个链表,它的时间复杂度就是 O(n),遍历查找会非常慢。

红黑树,每次更新数据以后再进行平衡,以此来保证其查找效率。

9.为什么阀值是8才转为红黑树

容器中节点分布在hash桶中的频率遵循泊松分布

各个长度的命中概率依次递减,源码注释中给我们展示了1-8长度的具体命中概率。

当长度为8的时候,概率概率仅为0.00000006,这么小的概率,大于上千万个数据时HashMap的红黑树转换几乎不会发生。

10.为什么退化为链表的阈值是6

主要是一个过渡,避免链表和红黑树之间频繁的转换。 如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊, 就会频繁的发生树转链表、链表转树,效率会很低。

11.hash冲突你还知道哪些解决办法?

(1)开放定址法 (2)链地址法 (3)再哈希法 (4)公共溢出区域法

12.HashMap在什么条件下扩容

如果bucket满了(超过load factor*current capacity),就要resize。

为什么负载因子是0.75 小于0.5,空着一半就扩容了, 如果是0.5 , 那么每次达到容量的一半就进行扩容,默认容量是16, 达到8就扩容成32,达到16就扩容成64, 最终使用空间和未使用空间的差值会逐渐增加,空间利用率低下。

当负载因子是1.0的时候, 出现大量的Hash的冲突时,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。

是0.75的时候

空间利用率比较高,而且避免了相当多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。

13.HashMap中hash函数怎么实现的?还有哪些hash函数的实现方式?

对key的hashCode 和右移16位做异或运算,之后hash(key) & (capacity - 1)做按位与运算得到下标。

Hash函数是指把一个大范围映射到一个小范围。把大范围映射到一个小范围的目的往往是为了节省空间,使得数据容易保存。

如果不同的输入得到了同一个哈希值,就发生了"哈希碰撞"(collision)。

比较出名的有MurmurHash、MD4、MD5等等。

14.为什么不直接将hashcode作为哈希值去做取模,而是要先高16位异或低16位?

均匀散列表的下标,降低hash冲突的几率。 不融合高低位,hashcode返回的值都是高位的变动的话,造成散列的值都是同一个。 融合后,高位的数据会影响到 index 的变换,依然可以保持散列的随机性。 打个比方,当我们的length为16的时候,哈希码(字符串“abcabcabcabcabc”的key对应的哈希码)对(16-1)与操作,对于多个key生成的hashCode,只要哈希码的后4位为0,不论不论高位怎么变化,最终的结果均为0。 扰动函数优化后:减少了碰撞的几率。

15.为什么扩容是2的次幂?

%运算不如位移运算快

在 B 是 2 的幂情况下:A % B = A & (B - 1)

和这个(n - 1) & hash的计算方法有着千丝万缕的关系 按位与&的计算方法是,只有当对应位置的数据都为1时,运算结果也为1,当HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞。

例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞。

而长度为5的时候,3&(5-1)=0 2&(5-1)=0,都在0上,出现碰撞了

16.链表的查找的时间复杂度是多少?

HashMap 如果完全不存在冲突则 通过 key 获取 value 的时间复杂度就是 O(1), 如果出现哈希碰撞,HashMap 里面每一个数组(桶)里面存的其实是一个链表,这时候再通过 key 获取 value 的时候时间复杂度就变成了 O(n), HashMap 当一个 key 碰撞次数超过8 的时候就会把链表转换成红黑树,使得查询的时间复杂度变成了O(logN)。 通过高16位异或低16位运算降低hash冲突几率。

17.红黑树
1.什么是虚拟机

Java 虚拟机是一个字节码翻译器,它将字节码文件翻译成各个系统对应的机器码,确保字节码文件能在各个系统正确运行。 Java 虚拟机规范去读取 Class 文件,并按照规定去解析、执行字节码指令。

2.Jvm的内存模型

[图片上传失败...(image-50e2e-1631067660635)]

https://www.cnblogs.com/chanshuyi/p/jvm_serial_06_jvm_memory_model.html

https://www.cnblogs.com/yychuyu/p/13275970.html

https://juejin.cn/post/6844903636829487112#heading-22

线程都共享的部分:Java 堆、方法区、常量池

线程的私有数据:PC寄存器、Java 虚拟机栈、本地方法栈

Java 堆

Java 堆指的是从 JVM 划分出来的一块区域,这块区域专门用于 Java 实例对象的内存分配,几乎所有实例对象都在会这里进行内存的分配

Java 堆根据对象存活时间的不同,Java 堆还被分为年轻代、老年代两个区域,年轻代还被进一步划分为 Eden 区、From Survivor 0、To Survivor 1 区

[图片上传中...(image-b05856-1631067660635-5)]

当有对象需要分配时,一个对象永远优先被分配在年轻代的 Eden 区,等到 Eden 区域内存不够时,Java 虚拟机会启动垃圾回收。此时 Eden 区中没有被引用的对象的内存就会被回收,而一些存活时间较长的对象则会进入到老年代

什么 Java 堆要进行这样一个区域划分

虚拟机中的对象必然有存活时间长的对象,也有存活时间短的对象,这是一个普遍存在的正态分布规律。如果因为存活时间短的对象有很多,那么势必导致较为频繁的垃圾回收。而垃圾回收时不得不对所有内存都进行扫描,但其实有一部分对象,它们存活时间很长,对他们进行扫描完全是浪费时间。因此为了提高垃圾回收效率

Java虚拟机栈

Java 虚拟机栈,线程私有,生命周期和线程一致

每一个运行时的线程,都有一个独立的栈。栈中记录了方法调用的历史,每有一次方法调用,栈中便会多一个栈桢

每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

撕开栈帧,一不小心,局部变量表、操作数栈、动态链接、方法出口 哗啦啦地散落一地。

栈桢中通常包含四个信息:

局部变量:方法参数和方法中定义的局部变量,对象引用

操作数栈:存放的就是方法当中的各种操作数的临时空间

动态连接:Class文件的常量池中存在有大量的符号引用,而将部分符号引用在运行期间转化为直接引用,这种转化即为动态链接

返回地址:当前方法的返回地址,一个方法在执行完毕之后,就应该返回到方法外面之后继续执行main()后面的代码(应该返回到下一条指令执行位置)。

本地方法栈

与java虚拟机栈类似,不过存放的是native方法执行时的局部变量等数据存放位置。因为native方法一般不是由java语言编写的,常见的就是.dll文件当中的方法(由C/C++编写),比如Thread类中start()方法在运行时就会调用到一个start0()方法,查看源码时就会看到private native void start0();这个方法就是一个本地方法。本地方法的作用就相当于是一个“接口”,用来连接java和其他语言的接口。

方法区

[图片上传失败...(image-f4ad6b-1631067660635)]

方法区中,存储了每个

1.类的信息

类的名称

类的访问描述符(public、private、default、abstract、final、static)

2.字段信息(该类声明的所有字段)

字段修饰符(public、protect、private、default)

字段的类型

字段名称

3.方法信息

方法修饰符

方法返回类型

方法名

4.类变量(静态变量)

就是静态字段( public static String static_str="static_str";)

虚拟机在使用某个类之前,必须在方法区为这些类变量分配空间。 5.指向类加载器的引用

6.指向Class实例的引用

7.运行时常量池(Runtime Constant Pool)

永久代和方法区的关系

《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 同时大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。因此,我们得到了结论,永久代是HotSpot的概念,方法区是Java虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现。其他的虚拟机实现并没有永久带这一说法。Java7及以前版本的Hotspot中方法区位于永久代中,HotSpot 使用永久代实现方法区,HotSpot 使用 GC分代来实现方法区内存回收。

元空间

Java8, HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在。那么取代永久代的就是元空间。它可永久代有什么不同的?

存储位置不同,永久代物理是是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;

存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。

Java8为什么要将永久代替换成Metaspace?

字符串存在永久代中,容易出现性能问题和内存溢出。

类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困 难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

常量池

分为class常量池和运行时常量池,运行时的常量池是属于方法区的一部分,而Class常量池是Class文件中的。

Class常量池

[图片上传失败...(image-328ea5-1631067660635)]

class 文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池 ,用于存放编译器生成的各种字面量 和符号引用 。

String str = "str"; int i = 1; "str"和1都是字面量,有别于变量。

符号引用:可以是任意类型的字面量。只要能无歧义的定位到目标。在编译期间由于暂时不知道类的直接引用,因此先使用符号引用代替。最终还是会转换为直接引用访问目标。

运行时常量池

行时常量池相对于 Class 文件常量池来说具备动态性,Class 文件常量只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用

字符串常量池

运行时常量池中的字符串字面量若是成员的,则在类的加载初始化阶段就使用到了字符串常量池;若是本地的,则在使用到的时候(执行此代码时)才会使用到字符串常量池

在 jdk1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的实例;

在 jdk1.7(含)之后是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中;

jdk1.8 已移除永久代,字符串常量池是在本地内存当中,存储的也只是引用。

程序计数器

每个线程启动的时候,都会创建一个PC(Program Counter,程序计数器)寄存器,是保存线程当前正在执行的方法。如果这个方法不是 native 方法,那么 PC 寄存器就保存 Java 虚拟机正在执行的字节码指令地址。如果是 native 方法,那么 PC 寄存器保存的值是 undefined

3.类加载机制

https://www.cnblogs.com/chanshuyi/p/jvm_serial_07_jvm_class_loader_mechanism.html

https://zhuanlan.zhihu.com/p/33509426

http://www.ityouknow.com/jvm/2017/08/19/class-loading-principle.html

https://juejin.im/post/6876968255597051917#heading-12

Java 虚拟机把源码编译为字节码之后,虚拟机便可以将字节码读取进内存,从而进行解析、运行等整个过程,这个过程叫:Java 虚拟机的类加载机制。

JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。

在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定。

另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

[图片上传中...(image-7ca411-1631067660635-2)]

加载

简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。

  • 通过一个类的全限定名来获取其定义的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

验证

主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。

  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内
  • 元数据验证:对字节码描述的信息进行语义分析,类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
  • 字节码验证:保证程序语义的合理性,比如要保证类型转换的合理性。
  • 符号引用验证:校验符号引用中的访问性(private,public等)是否可被当前类访问?

准备

主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值

1.Java语言支持的变量类型有:

类变量:独立于方法之外的变量,用 static 修饰。

实例变量:独立于方法之外的变量,不过没有 static 修饰。

局部变量:类的方法中的变量。

在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。

例如下面的代码在准备阶段,只会为 factor 属性分配内存,而不会为 website 属性分配内存。

public static int factor = 3;

public String website = "www.cnblogs.com/chanshuyi";

2.初始化的类型。在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。

例如下面的代码在准备阶段之后,sector 的值将是 0,而不是 3。

public static int sector = 3;

解析

将常量池内的符号引用替换为直接引用的过程。

在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。

初始化

这个阶段主要是对类变量初始化,是执行类构造器的过程。

换句话说,只对static修饰的变量或语句进行初始化。

类初始化时机: 有当对类的主动使用的时候会先进行类的初始化,类的主动使用包括以下4种:

1.创建类的实例,调用类的静态方法,访问某个类或接口的静态变量如果类没有进行过初始化,则需要先触发其初始化

2.使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

3.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

4.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化

如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

java对象实例化时的顺序为:父类优于子类,静态优于非静态,只有在第一次创建对象的时候才会初始化静态块。

1,父类的静态成员变量和静态代码块加载

2,子类的静态成员变量和静态代码块加载

3,父类成员变量和方法块加载

4,父类的构造函数加载

5,子类成员变量和方法块加载

6,子类的构造函数加载

4.类加载器

在 JVM 中有三个非常重要的编译器,它们分别是:前端编译器、JIT 编译器、AOT 编译器。

前端编译器,最常见的就是我们的 javac 编译器,其将 Java 源代码编译为 Java 字节码文件。JIT 即时编译器,其将 Java 字节码编译为本地机器代码。AOT 编译器则能将源代码直接编译为本地机器码。

ClassLoader 代表类加载器,是 java 的核心组件,可以说所有的 class 文件都是由类加载器从外部读入系统,然后交由 jvm 进行后续的连接、初始化等操作。

jvm 会创建三种类加载器,分别为启动类加载器、扩展类加载器和应用类加载器

启动类加载器

主要负责加载系统的核心类,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下

扩展类加载器

主要用于加载 lib\ext 中的 java 类,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类

应用类加载器

Application ClassLoader 主要加载用户类,即加载用户类路径(ClassPath)上指定的类库,一般都是我们自己写的代码

类加载有三种方式:

1、命令行启动应用时候由JVM初始化加载

2、通过Class.forName()方法动态加载

3、通过ClassLoader.loadClass()方法动态加载

Class.forName()和ClassLoader.loadClass()区别

Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

Class.forName()方法实际上也是调用的CLassLoader来实现的。

双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

[图片上传失败...(image-98a345-1631067660635)]

双亲委派模式优势

避免重复加载 + 避免核心类篡改

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

5.垃圾回收机制

如何判断一个对象是死亡的

如果一个对象不可能再被引用,那么这个对象就是垃圾,应该被回收

https://www.zhihu.com/question/21539353

引用计数法

在一个对象被引用时加一,被去除引用时减一,对于计数器为0的对象意味着是垃圾对象,可以被GC回收。

优点:

引用计数收集器执行简单,判定效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。

缺点:

难以检测出对象之间的循环引用。 引用计数器增加了程序执行的开销。

可达性算法

从 GC Root 出发,所有可达的对象都是存活的对象,而所有不可达的对象都是垃圾。, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达。

可以作为GC Roots的对象

虚拟机栈的局部变量引用的对象;

本地方法栈的JNI所引用的对象;

方法区的静态变量和常量所引用的对象;

https://blog.csdn.net/u010798968/article/details/72835255

[图片上传失败...(image-20fac9-1631067660635)]

对象实例1、2、4、6都具有GC Roots可达性,也就是存活对象,不能被GC回收的对象。 而对于对象实例3、5直接虽然连通,但并没有任何一个GC Roots与之相连,这便是GC Roots不可达的对象,这就是GC需要回收的垃圾对象。

垃圾回收算法

标记清除算法

对根集合进行扫描,对存活的对象进行标记。标记完成后,再对整个空间内未被标记的对象扫描,进行回收。

优点:

实现简单,不需要进行对象进行移动。

缺点:

标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。

标记压缩算法

标记压缩算法可以说是标记清除算法的优化版 在标记阶段,从 GC Root 引用集合触发去标记所有对象。在压缩阶段,其则是将所有存活的对象压缩在内存的一边,之后清理边界外的所有空间。

优点:

解决了标记-清理算法存在的内存碎片问题。

缺点:

仍需要进行局部对象移动,一定程度上降低了效率。

复制算法

复制算法的核心思想是将原有的内存空间分为两块,每次只使用一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中。之后清除正在使用的内存块中的所有对象,之后交换两个内存块的角色,完成垃圾回收。

优点:

按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。

缺点:

可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

分代收集算法

JDK8堆内存一般是划分为年轻代和老年代,不同年代 根据自身特性采用不同的垃圾收集算法。

对于老年代,因为对象存活率高,没有额外的内存空间对它进行担保。因而适合采用标记-清理算法和标记-整理算法进行回收。试想一下,如果没有采用分代算法,而在老年代中使用复制算法。在极端情况下,老年代对象的存活率可以达到100%,那么我们就需要复制这么多个对象到另外一个内存区域,这个工作量是非常庞大的。

对于新生代,每次GC时都有大量的对象死亡,只有少量对象存活。比较适合采用复制算法。这样只需要复制少量对象,便可完成垃圾回收,并且还不会有内存碎片。

,在实际的 JVM 新生代划分中,却不是采用等分为两块内存的形式。而是分为:Eden 区域、from 区域、to 区域 这三个区域。那么为什么 JVM 最终要采用这种形式,而不用 50% 等分为两个内存块的方式?

要解答这个问题,我们就需要先深入了解新生代对象的特点。根据IBM公司的研究表明,在新生代中的对象 98% 是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间。所以在HotSpot虚拟机中,JVM 将内存划分为一块较大的Eden空间和两块较小的Survivor空间,其大小占比是8:1:1。当回收时,将Eden和Survivor中还存活的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Eden空间。

通过这种方式,内存的空间利用率达到了90%,只有10%的空间是浪费掉了。而如果通过均分为两块内存,则其内存利用率只有 50%,两者利用率相差了将近一倍。

java编译后是什么文件

https://www.cnblogs.com/chanshuyi/p/jvm_serial_04_from_source_code_to_machine_code.html

https://blog.csdn.net/qq_36791569/article/details/80269482

https://blog.csdn.net/q978090365/article/details/109465148

https://cloud.tencent.com/developer/article/1630650 javac 先将 Java 编译成class字节码文件
编译完要执行 通过解释器解释执行和Jit编译器转为本地字节码执行 前者启动快运行慢 后者启动慢运行快 因为JIT会将所有字节码都转化为机器码并保存下来 而解释器边解释边运行

Java9新特性AOT直接将class转为二进制可编译文件 和JIT区别是 运行前编译好,但缺点是全编译 不用的也编译了 不能动态加载 但避免了JIT运行时的内存消耗

1.Java中创建线程的方式

1.继承Thread类,重写run方法

2.实现Runnable接口,传递给Thread(runnable)构造函数

3.通过FutureTask 传递给Thread()构造函数

CallableTest callableTest = new CallableTest();

FutureTask futureTask = new FutureTask<>(callableTest);

new Thread(futureTask).start();

创建FutureTask对象,创建Callable子类对象,复写call(相当于run)方法

创建Thread类对象,将FutureTask对象传递给Thread对象

4.通过ExecutorService 线程池进行创建多线程

Callable和Runnable的区别

(1) Callable重写的是call()方法,Runnable重写的方法是run()方法

(2) call()方法执行后可以有返回值,run()方法没有返回值.运行Callable任务可以拿到一个Future对象,表示异步计算的结果 。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果

(3) call()方法可以抛出异常,run()方法不可以

实现Runnable/Callable接口相比继承Thread类的优势

由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活。

如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量。

Callable如何使用

Callable一般是配合线程池工具ExecutorService来使用的,Callable一般是配合线程池工具ExecutorService来使用的 通过这个Future的get方法得到结果。

Future和FutureTask

https://zhuanlan.zhihu.com/p/38514871

Future接口只有几个比较简单的方法:

public abstract interface Future { public abstract boolean cancel(boolean paramBoolean); public abstract boolean isCancelled(); public abstract boolean isDone(); public abstract V get() throws InterruptedException, ExecutionException; public abstract V get(long paramLong, TimeUnit paramTimeUnit) throws InterruptedException, ExecutionException, TimeoutException; }

也就是说Future提供了三种功能:

1)判断任务是否完成;

2)能够中断任务;

3)能够获取任务执行结果。

因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。

Future只是一个接口,而它里面的cancel,get,isDone等方法要自己实现起来都是非常复杂的。所以JDK提供了一个FutureTask类来供我们使用。

可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

2. 线程的几种状态

3. 谈谈线程死锁,如何有效的避免线程死锁?

https://www.cnblogs.com/xiaoxi/p/8311034.html

什么是死锁

死锁 :在两个或多个并发进程中,如果每个进程持有某种资源而又都等待别的进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁

例如,某计算机系统中只有一台打印机和一台输入 设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2 所占用,而P2在未释放打印机之前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态。

死锁产生的原因

  1. 系统资源的竞争

通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局。

  1. 进程推进顺序非法

进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞。

3.信号量使用不当也会造成死锁。

进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。

如何避免死锁

1加锁顺序

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

2加锁时限

在尝试获取锁的时候加一个超时时间,超过时限则放弃对该锁的请求,并释放自己占有的锁

3死锁检测

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

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

那么当检测出死锁时,这些线程该做些什么呢?

一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。

4. 如何实现多线程中的同步

synchronized对代码块或方法加锁

reentrantLock加锁结合Condition条件设置

volatile关键字

cas使用原子变量实现线程同步

参照UI线程更新UI的思路,使用handler把多线程的数据更新都集中在一个线程上,避免多线程出现脏读

5. synchronized和Lock的使用、区别,原理;

https://juejin.cn/post/6844903542440869896#heading-17

使用

synchronized

修饰实例方法

修饰静态方法

修饰代码块

当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this); 当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁; 当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;(https://www.cnblogs.com/aspirant/p/11470858.html)

类锁:锁是加持在类上的,用synchronized static 或者synchronized(class)方法使用的锁都是类锁,因为class和静态方法在系统中只会产生一份,所以在单系统环境中使用类锁是线程安全的https://zhuanlan.zhihu.com/p/31537595

对象锁:synchronized 修饰非静态的方法和synchronized(this)都是使用的对象锁,一个系统可以有多个对象实例,所以使用对象锁不是线程安全的,除非保证一个系统该类型的对象只会创建一个(通常使用单例模式)才能保证线程安全;

lock

Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock

Lock

lock()、tryLock()、tryLock(long time, TimeUnit unit) 和 lockInterruptibly()都是用来获取锁的

lock:用来获取锁

unlock:释放锁 如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。

tryLock:tryLock方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,

lockInterruptibly:通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。

ReadWriteLock 接口只有两个方法:

//返回用于读取操作的锁
Lock readLock()
//返回用于写入操作的锁
Lock writeLock()

ReetrantLock

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞 Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

原理

首先ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始值为0。

当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0 则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是 如果status != 0的话会导致其获取锁失败,当前线程阻塞。

ReadWriteLock

https://www.cnblogs.com/myseries/p/10784076.html(使用)

共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后, 获得共享锁的线程只能读数据,不能修改数据。

ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作

只要没有 writer,读取锁可以由多个 reader 线程同时保持,而写入锁是独占的

区别:

synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

Lock可以让等待锁的线程响应中断,而synchronized却不行

通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

原理

synchronized(https://juejin.cn/post/6844903670933356551#heading-6,https://juejin.cn/post/6844904181510176775#heading-6)

monitor描述为一种同步机制,它通常被描述为一个对象,当一个 monitor 被某个线程持有后,它便处于锁定状态

每个对象都存在着一个 monitor 与之关联

jvm基于进入和退出Monitor对象来实现方法同步和代码块同步。

对象头和Monitor对象

对象头

synchronized 用的锁是存在Java对象头里的

Hopspot 对象头主要包括两部分数据:Mark Word(标记字段) 和 Klass Pointer(类型指针)

Mark Word:

Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是 无锁状态 偏向锁状态 轻量级锁状态 重量级锁状态

当对象状态为偏向锁时,Mark Word存储的是偏向的线程ID; 当状态为轻量级锁时,Mark Word存储的是指向栈中锁记录的指针; 当状态为重量级锁时,Mark Word为指向堆中的monitor对象的指针

在HotSpot JVM实现中,锁有个专门的名字:对象监视器Object Monitor

同步代码块

从字节码中可知同步语句块的实现使用的是monitorenter和monitorexit指令

线程将试图获取对象锁对应的 monitor 的持有权, monitor的进入计数器为 0,那线程可以成功取得monitor,并将计数器值设置为1,取锁成功。

同步方法

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中

当方法调用时,调用指令将会 检查方法的 访问标志是否被设置,如果设置了如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词),然后再执行方法,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。

6.volatile,synchronized和volatile的区别?为何不用volatile替代synchronized?

  1. 线程通信模型(http://concurrent.redspider.group/article/02/6.html)

内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存

从抽象的角度来说,JMM定义了线程和主内存之间的抽象关系。

一般来说,JMM中的主内存属于共享数据区域,他是包含了堆和方法区;同样,JMM中的本地内存属于私有数据区域,包含了程序计数器、本地方法栈、虚拟机栈。

根据JMM的规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读取。

所有的共享变量都存在主内存中。

每个线程都保存了一份该线程使用到的共享变量的副本。

如果线程A与线程B之间要通信的话,必须经历下面2个步骤: 线程A将本地内存A中更新过的共享变量刷新到主内存中去。 线程B到主内存中去读取线程A之前已经更新过的共享变量。

  1. volatile修饰的变量具有可见性

volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

  1. volatile禁止指令重排

使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性 是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。

区别

https://blog.csdn.net/suifeng3051/article/details/52611233 https://juejin.cn/post/6844903598644543502#heading-1

volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的

volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性

volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞

7.锁的分类,锁的几种状态,CAS原理

https://juejin.cn/post/6844904181510176775#heading-8 https://tech.meituan.com/2018/11/15/java-lock.html

无锁/偏向锁/轻量级锁/重量级锁

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

四种状态的转换(http://concurrent.redspider.group/article/02/9.html)

1.每一个线程在准备获取共享资源时: 第一步,检查MarkWord里面是不是放的自己的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。

2.第二步,如果MarkWord不是自己的ThreadId,会尝试使用CAS来替换Mark Word里面的线程ID为新线程的ID,成功,仍然为偏向锁,失败,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。

3.CAS将锁的Mark Word替换为指向锁记录的指针,成功的获得资源,失败的则进入自旋 。

4.自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。

乐观锁/悲观锁

对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)

悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。 synchronized关键字和Lock的实现类都是悲观锁。

乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。 java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢

CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

CAS算法涉及到三个操作数:

进行比较的值 A。 要写入的新值 B。 需要读写的内存值 C。

AtomicInteger的自增函数incrementAndGet()的源码时

去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞 Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

自旋锁

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

8.为什么会有线程安全?如何保证线程安全

https://github.com/Moosphan/Android-Daily-Interview/issues/108

在多个线程访问共同资源时,在某一个线程对资源进行写操作中途(写入已经开始,还没结束),其他线程对这个写了一般资源进行了读操作,或者基于这个写了一半操作进行写操作,导致数据问题

原子性(java.util.concurrent.atomic 包)

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

有序性( Synchronized, Lock)

即程序执行的顺序按照代码的先后顺序执行

可见性(Volatile)

指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

9.sleep()与wait()区别,run和start的区别,notify和notifyall区别,锁池,等待池

https://blog.csdn.net/u012050154/article/details/50903326 https://github.com/Moosphan/Android-Daily-Interview/issues/117 sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),而并不会释放同步资源锁!!!

wait()方法则是指当前线程让自己暂时退让出同步资源锁,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态

sleep不需要唤醒,wait需要唤醒

1.run和start的区别

start() 可以启动一个新线程,run()不会

tart()中的run代码可以不执行完就继续执行下面的代码 直接调用run方法必须等待其代码全部执行完才能继续执行下面的代码。

2.sleep wait区别

sleep是Thread方法,不会释放锁,wait是obje方法,会释放锁

sleep不需要Synchronized ,wait需要Synchronized

sleep休眠自动继续执行,wait需要notify来唤醒,得到锁

3.notify和notifyall区别

notify是只唤醒一个等待的线程,noftifyALl是唤醒所有等待的线程

notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争

某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中

https://www.zhihu.com/question/37601861/answer/145545371

锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中

10.Java多线程通信

http://concurrent.redspider.group/article/01/2.html

11.Java多线程

https://blog.csdn.net/weixin_40271838/article/details/79998327

http://concurrent.redspider.group/article/03/12.html

Java中开辟出了一种管理线程的概念,这个概念叫做线程池,多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。

好处

创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程。

控制并发的数量。并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃。(主要原因)

可以对线程做统一管理。

参数

https://blog.csdn.net/weixin_40271838/article/details/79998327

线程池中的corePoolSize就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收 maximumPoolSize就是线程池中可以容纳的最大线程的数量 keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间 TimeUnit unit:keepAliveTime的单位 BlockingQueue workQueue:阻塞队列,任务可以储存在任务队列中等待被执行。

两个非必须的参

ThreadFactory threadFactory

创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数,如是否守护线程、线程的优先级

RejectedExecutionHandler handler

拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略

Java中的线程池共有几种

https://www.jianshu.com/p/5936a2242322

CachedThreadPool: 该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。SynchronousQueue

FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程LinkedBlockingQueue

SecudleThreadPool:创建一个定长线程池,支持定时及周期性任务执行。

SingleThreadPool:只有一条线程来执行任务,使用了LinkedBlockingQueue(容量很大),所以,不会创建非核心线程。所有任务按照先来先执行的顺序执行LinkedBlockingQueue

何为阻塞队列?

http://concurrent.redspider.group/article/03/13.html

生产者-消费者模式 生产者一直生产资源,消费者一直消费资源,资源存储在一个缓冲池中,生产者将生产的资源存进缓冲池中,消费者从缓冲池中拿到资源进行消费。

我们自己coding实现这个模式的时候,因为需要让多个线程操作共享变量(即资源),所以很容易引发线程安全问题,造成重复消费和死锁。另外,当缓冲池空了,我们需要阻塞消费者,唤醒生产者;当缓冲池满了,我们需要阻塞生产者,唤醒消费者,这些个等待-唤醒逻辑都需要自己实现。

阻塞队列(BlockingQueue),你只管往里面存、取就行,而不用担心多线程环境下存、取共享变量的线程安全问题。

阻塞队列的原理很简单,利用了Lock锁的多条件(Condition)阻塞控制

put和take操作都需要先获取锁,没有获取到锁的线程会被挡在第一道大门之外自旋拿锁,直到获取到锁。

就算拿到锁了之后,也不一定会顺利进行put/take操作,需要判断队列是否可用(是否满/空),如果不可用,则会被阻塞,并释放锁。

有哪几种工作队列

1、LinkedBlockingQueue

一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor使用了这个队列。

2、SynchronousQueue

一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

线程池原理

从数据结构的角度来看,线程池主要使用了阻塞队列(BlockingQueue)

1、如果正在运行的线程数 < coreSize,马上创建核心线程执行该task,不排队等待; 2、如果正在运行的线程数 >= coreSize,把该task放入阻塞队列; 3、如果队列已满 && 正在运行的线程数 < maximumPoolSize,创建新的非核心线程执行该task; 4、如果队列已满 && 正在运行的线程数 >= maximumPoolSize,线程池调用handler的reject方法拒绝本次提交。

理解记忆:1-2-3-4对应(核心线程->阻塞队列->非核心线程->handler拒绝提交)。

https://juejin.cn/post/6844903511809851400 1.注解的分类

Java的注解分为元注解和标准注解。

标准注解 SDO

元注解

@Documented

是一个标记注解,没有成员变量。 会被JavaDoc 工具提取成文档

@Target(METHOD,TYPE类,FIELD用于成员变量,PACKAGE用于包) 表示被描述的注解用于什么地方

@Retention

描述注解的生命周期

SOURCE:在源文件中有效(即源文件保留) CLASS:在 class 文件中有效(即 class 保留) RUNTIME:在运行时有效(即运行时保留)

这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。

1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃; 2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期; 3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;

@Documented – 注解是否将包含在JavaDoc中 @Retention – 什么时候使用该注解 @Target – 注解用于什么地方 @Inherited – 是否允许子类继承该注解

注解的底层实现原理

注解的原理:

注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。

而我们通过反射获取注解时,返回的是Java 运行时生成的动态代理对象$Proxy1。

通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke方法。

1.什么是反射

JAVA反射机制是在运行状态中,

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

对于任意一个对象,都能够调用它的任意方法和属性;

这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

2.反射机制的相关类

类名 用途

Class类 代表类的实体,在运行的Java应用程序中表示类和接口

Field类 代表类的成员变量(成员变量也称为类的属性)

Constructor类 代表类的构造方法

Method类 代表类的方法

3.反射中如何获取Class类的实例

(1)通过.class

Class class1 = Person.class;

(2)通过创建实例对象来获取类对象

Person person =new Person();

Class class2 = person.getClass();

(3) 通过包名,调用class的forName方法

Class class3 = Class.forName("day07.Person");

4.如何获取一个类的属性对象 & 构造器对象 & 方法对象 。

通过class对象创建一个实例对象

Cls.newInstance();

通过class对象获得一个属性对象

Field c=cls.getFields():

获得某个类的所有的公共(public)的字段,包括父类中的字段。

Field c=cls.getDeclaredFields():

获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的声明字段

获取构造器对象

Clazz.getConstructor();

通过class对象获得一个方法对象

Cls.getMethod(“方法名”,class……parameaType);(只能获取公共的)

Cls.getDeclareMethod(“方法名”);(获取任意修饰的方法,不能执行私有)

5.Class.getField和getDeclaredField的区别,getDeclaredMethod和getMethod的区别

getField

获取一个类的 ==public成员变量,包括基类== 。

getDeclaredField

获取一个类的 ==所有成员变量,不包括基类== 。

getDeclaredMethod*()

获取的是类自身声明的所有方法,包含public、protected和private方法。

getMethod*()

获取的是类的所有共有方法,这就包括自身的所有public方法,和从基类继承的、从接口实现的所有public方法 现的所有public方法。 。

6.反射机制的优缺点

优点:

1)能够运行时动态获取类的实例,提高灵活性;

缺点:

1)使用反射性能较低,需要解析字节码,将内存中的对象进行解析。

2)相对不安全,破坏了封装性(因为通过反射可以获得私有方法和属性)

推荐阅读更多精彩内容