目录
1、Java基础之class,Object,Class的区别
5、final关键字修饰的变量与没有final修饰符修饰变量加载的区别
1、Java基础之class,Object,Class的区别
~Object是一个特殊的类,所有的类都继承该类,包括Class也继承Object,也就说Class(注意Class大写)是Object的子类。且可以通过eclipse的关系树中看出
~Class 只是一个名字比较特殊的类,是关键字class修饰的类,一般应用于反射,只是名称比较特殊而已,可以通过Class类型来获取其他类型的元数据(metadata),
比如字段,属性,构造器,方法等等,可以获取并调用。注意,Class不能直接通过new实例化,Object不是Class的实例
public class shapes{}
Class obj= Class.forName("shapes");
~class class是一个关键字,是用来修饰类
2、JVM如何加载一个类的
Java语言的一个非常重要的特点就是与平台的 无关性。而使用Java虚拟机是实现这一特点的关键。使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改的运行。
1、类加载器
类加载器的作用就是加载类文件到内存,
2、执行引擎
执行引擎也叫做解释器,负责解释命令,提交操作系统执行。
3、本地接口
本地接口的作用是为了融合不同的编程语言为Java所用。它的初衷是为了融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须要有一个聪明的、睿智的调用C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时
加载加载native libraries。目前该方法只有在与硬件有关的应用中才会使用,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用WebService等。
即主要是调用C或C++实现的本地方法及返回结果。
4、运行数据区
是在JVM运行的时候操作所分配的内存区。我们所写的程序都被加载到这里,之后才开始运行,运行时内存区主要可以划分为5个区域,
1.方法区(Method Area):用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。虽然JVM规范把方法区描述为堆的一个逻辑部分, 但它却有个别名non-heap(非堆),所以大家不要搞混淆了。方法区还包含一个运行时常量池。
2.java堆(Heap):存储java实例或者对象的地方。这块是GC的主要区域(后面解释)。从存储的内容我们可以很容易知道,方法区和堆是被所有java线程共享的。
3.java栈(Stack):java栈总是和线程关联在一起,每当创建一个线程时,JVM就会为这个线程创建一个对应的java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是现成私有的。
4.程序计数器(PC Register):用于保存当前线程执行的内存地址。由于JVM程序是多线程执行的(线程轮流切换),所以为了保证线程切换回来后,还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。
5.本地方法栈(Native Method Stack):和java栈的作用差不多,只不过是为JVM使用到的native方法服务的。
Java中的所有类,必须被装载到JVM中才能运行,这个装载工作是由JVM中的类装载器完成的,类装载器所做的工作实质是把类文件从硬盘读取到内存中,作用就是在运行时加载类。
Java类加载器基于三个机制:委托、可见性和单一性。
(1)委托机制是指加载一个类的请求交给父类加载器,如果这个父类加载器不能够找到或加载这个类,那么再加载它。
(2)可见性的原理是子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类。
(3)单一性原理是指一个类仅被加载一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类。
类装载有两种方式
(1)隐式装载:
程序在运行过程中当碰到通过new等方式生成类或者子类对象、使用类或者子类的静态域时,隐式调用类加载器加载对应的的类到JVM中。
(2)显式装载:
通过调用Class.forName()或者ClassLoader.loadClass(className)等方法,显式加载需要的类。
类加载的动态性体现
一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载再运行,他总是把保证程序运行的基础类一次性加载到JVM中,其他类等到JVM用到的时候再加载,这样是为了节省内存的开销,因为Java最早就是为嵌入式系统而设计的,内存宝贵,而用到时再加载这也是Java动态性的一种体现。
参考:【JVM】JVM加载class文件的原理机制_renjingjingya0429的博客-优快云博客_jvm加载class文件的原理机制
JVM结构原理_ago_lei的博客-优快云博客_jvm结构
3、对GC的理解
GC如其名,就是垃圾收集,以应用程序的root为基础,遍历应用程序在Heap上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是已经死亡的、哪些仍需要被使用。已经不再被应用程序的root或者别的对象所引用的对象就是已经死亡的对象,即所谓的垃圾,需要被回收(回收的是该对象占用的内存空间)。这就是GC工作的原理。
JVM的堆是Java对象的活动空间,程序中的类的对象从中分配空间,其存储着正在运行着的应用程序用到的所有对象。这些对象的建立方式就是那些new一类的操作,当对象无用后,是GC来负责这个无用的对象。
(1) 新域:存储所有新成生的对象
(2) 旧域:新域中的对象
垃圾回收的原因
从计算机组成的角度来讲,所有的程序都是要驻留在内存中运行的。而内存是一个限制因素(大小)。除此之外,托管堆也有大小限制。因为地址空间和存储的限制因素,托管堆要通过垃圾回收机制,来维持它的正常运作,保证对象的分配,尽可能不造成“内存溢出”。
垃圾回收的基本原理
(算法思路都是一致的:把所有对象组成一个集合,或可以理解为树状结构,从树根开始找,只要可以找到的都是活动对象,如果找不到,这个对象就被回收了)
垃圾回收分为两个阶段:
标记 --> 压缩标记的过程,其实就是判断对象是否可达的过程。当所有的根都检查完毕后,堆中将包含可达(已标记)与不可达(未标记)对象。标记完成后,进入压缩阶段。在这个阶段中,垃圾回收器线性的遍历堆,以寻找不可达对象的连续内存块。并把可达对象移动到这里以节约内存空间。
常用算法:
Mark-Sweep标记清理算法
阶段1: 先假设heap中所有对象都可以回收,然后找出不能回收的对象,给这些对象打上标记,最后heap中没有打标记的对象都是可以被回收的;
阶段2: 压缩阶段,对象回收之后heap内存空间变得不连续,在heap中移动这些对象,使他们重新从heap基地址开始连续排列(节省内存资源)。
Heap内存经过回收、压缩之后,可以继续采用前面的heap内存分配方法, 即仅用一个指针记录heap分配的起始地址就可以。主要处理步骤:将线程挂起→确定roots→创建reachable objects graph→对象回收→heap压缩→指针修复。
复制算法
新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
标记整理算法
与标记清理算法过程一样,只是不直接清理可回收对象,而是将所有存活对象移动到一端,之后清理边界之外的对象内存
分代收集算法
现代商用虚拟机基本都采用分代收集算法来进行垃圾回收。这种算法没什么特别的,无非是上面内容的结合罢了,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保
的(老年代),采用标记-清理算法或者标记-整理算法。
什么时候发生GC;
1、当应用程序分配新的对象,GC的代的预算大小已经达到阈值,比如GC的第0代已满
2、代码主动显式调用System.GC.Collect()
3、其他特殊情况,比如,windows报告内存不足、CLR卸载AppDomain、CLR关闭,甚至某些极端情况下系统参数设置改变也可能导致GC回收
参考:GC垃圾回收机制详解 - 简书
新参考: https://juejin.cn/post/6891589544161116168#heading-14
4、类何时加载,何时触发初始化
为一个类型创建一个新的对象实例时(比如new、反射、序列化)
调用一个类型的静态方法时(即在字节码中执行invokestatic指令)
调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式
调用JavaAPI中的反射方法时(比如调用java.lang.Class中的方法,或者java.lang.reflect包中其他类的方法)
初始化一个类的派生类时(Java虚拟机规范明确要求初始化一个类时,它的超类必须提前完成初始化操作,接口例外)
JVM启动包含main方法的启动类时。
5、final关键字修饰的变量与没有final修饰符修饰变量加载的区别
public static final String = "aa";
final修饰的常量已经赋值的,会在编译阶段存入到调用这个常量的方法所在类的常量池中,本质上调用类并没有直接引用到定义常量的类,因此并不会触发定义常量类的初始化
public static final String = UUID.randomUUID().toString();
当一个常量的值并非运行时可以确认的,那么就不会将其放入调用者类所在的常量池中,这时在程序运行时会主动使用该常量类所在的类,就会导致该常量类所在类的初始化
参考:三、final关键字的修饰变量的加载 - 简书
6、局部变量final
public class Hello {
public static void main(String[] args) {
final String str="haha";
new Thread() {
@Override
public void run() {
System.out.println(str);
}
}.start();
//system.out.print(str);
}
}
原因:匿名内部类访问局部变量的限制
在 Java 中,匿名内部类(包括线程、Runnable等)可以访问外部的局部变量,但这些局部变量必须是 final
或有效的 final
(在 Java 8 之后引入的概念)。这是由于内部类的实现机制以及潜在的并发问题。
内部类的实现机制
当你在匿名内部类中引用局部变量时,Java 编译器实际上会将这个局部变量的副本传递给内部类。为了确保这个副本与外部局部变量的一致性,Java 要求该变量是 final
或有效的 final
。这是为了保证在内部类实例化后,变量不会被修改,确保线程安全和一致性。
并发问题
如果局部变量在内部类实例化后可以被修改,可能会导致不一致的状态或线程安全问题。例如,如果你启动了多个线程并修改该变量,每个线程可能会看到不同的值,这会导致不可预测的行为。
Java 8 之后的改进
在 Java 8 之后,引入了**有效的 final
**的概念,这意味着即使局部变量没有显式声明为 final
,只要它在定义后没有被修改,它也可以被匿名内部类引用。例如:
public class Hello {
public static void main(String[] args) {
String str = "haha"; // 不需要显式声明为 final
new Thread() {
@Override
public void run() {
System.out.println(str);
}
}.start();
// 如果此处尝试修改 str 的值,会导致编译错误
}
}
在这个例子中,只要 str
变量在其定义后没有被修改,编译器会将其视为有效的 final
,并允许匿名内部类引用它。
总结
局部变量在匿名内部类中被引用时必须是 final
或有效的 final
,这是为了确保变量的一致性和线程安全。Java 8 之后,变量只要在定义后没有被修改,就不需要显式声明为 final
。这种机制确保了匿名内部类在并发编程中使用局部变量时的安全性和一致性。
7、多态
面向对象的三大特性:封装、继承、多态。从一定角度来看,封装和继承几乎都是为多态而准备的。这是我们最后一个概念,也是最重要的知识点。
多态即多种形态。比如父类调用子类重写的方法。 至于重载,在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同甚
至是参数顺序不同)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但不能通过返回类型是否相同来判断重载。重载是不是
属于多态,目前有两种,一种是不是,一种是,我个人更倾向于是。
8、hashcode()与equals()的区别
equals它的作用也是判断两个对象是否相等,如果没有重写,比较两个对象的地址是否相同,
hashCode()返回该对象的哈希码值,该值通常是一个由该对象的内部地址转换而来的整数。
通常 如果 两个对象 equal 相同那么,即地址相同,他们的hashcode() 一定相同。
但是 如果两个 对象 hashcode()相同的时候,那么两个equals 不一定相同。
如 在使用基于散列值(hash)的集合类的前提下,需要注意两种情况:
1.重写了 equals
方法后,也必须重写 hashCode
方法,主要原因是为了维护两个方法之间的一致性,确保对象在集合中的正确行为。
2.如何散列集合里比较两个对象是否相等的话,先比较两个对象hashCode是否相同。如果不相等的话完毕,就是不同,如果相同的话,再通过equals 比较两个对象是否相等。
9、泛型相关 特性:
泛型,即“参数化类型”。顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数。然后在使用/调用时传入具
体的类型。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛
型类、泛型接口、泛型方法。
List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
Log.d("泛型测试","item = " + item);
}
毫无疑问,程序的运行结果会以崩溃结束:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
通配符:
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
入参不能是Generic,因为泛型Generic不能被看作为`Generic的子类,那么可以使用通配符。类型通配符一般是使用?可以解决当具体类型不确定的时 候,这个通配符就是 ? 。当操作类型时,不需要使用类
型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。
例子:
public void showKeyValue1(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
那么入参就可以是:Generic和Generic
泛型方法:
/**
* 泛型方法的基本介绍
* @param tClass 传入的泛型实参
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
示例:https://jingyan.baidu.com/article/fdffd1f875d675f3e88ca158.html
泛型的上下边界:
类型实参只准传入某种类型的子类为上边界:
类型实参只准传入某种类型种类型的父类为下边界
注:T为具体的 比如水果泛型上边界为:<? extends Fruit>, 下边界为: <? super Fruit>
泛型相关参考:java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一 - little fat - 博客园
10、HashMap的理解
Hashmap实际上是一个数组和链表的结合体(在数据结构中,一般称之为“链表散列“)
HashMap的主干是一个Entry数组。Entry【Node】是HashMap的基本组成单元,每一个Entry【Node】包含一个key-value键值对。【1.8版本Entry改成Node】结构大概如图:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算 公式 hash=hash(key.hashcode())
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
HashMap 是如何插入数据的:
1、初始化
HashMap的构造方法在执行时会初始化一个数组table,大小为0。HashMap的PUT方法在执行时首先会判断table的大小是否为0,如果为0则会进行真初始化,也叫做延迟初始化。当进行真初始化时,数组的默认大小为16,当然也
可以调用HashMap的有参构造方法由你来指定一个数组的初始化容量,你想初始化一个大小为n的数组,但是HashMap会初始化一个大小大于等于n的二次方数的一个数组。比如你传入的是6,初始化的数组大小为 2*2*2=8。比如你传入的
是20,则初始化大小为2*2*2*2*2 =32。
2、放入元素
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
//h-key的hashCode,length-map的数组长度
return h & (length-1);
}
put时会调用key做哈希运算,得到哈希值,而是使用了key的hashCode的二进制与数组长度的二进制进行逻辑与运算得出数组下标。(这就是为什么在真初始化HashMap的时候,对于数组的长度一定要是二次方数)
3、哈希冲突
哈希函数的特点是对于相同的参数那么返回的HashCode肯定是相同的,对于不相同的参数,函数会尽可能的去返回不相同的HashCode,所以换一个角度理解,对于哈希函数,给不相同的参数可能会返回相同的HashCode,这个就叫哈希冲突或哈希碰撞。
当PUT两个不同的key时可能会得到相同的HashCode从而得到相同的数组下标,其实在HashMap中就算key所对应的HashCode不一样,那么也有可能在经过逻辑与操作之后得到相同的数组下标,而解决冲突的方式就是在同一个数组下标中引入链表结构来解决。这也就是HashMap的数据结构为什么是数组加动态链表的数据结构的原因
4、因为jdk1.7版本的HashMap在插入数据的时候使用的是头插法,所以在数据扩容的时候会产生死循环问题,在1.8版本之后已经优化,插入时使用了尾插法来解决这个问题
5、何时扩容
阈值:(默认大小为16,负载因子0.75,阈值12) 16*0.75 =12
底层数据结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构(当链表长度大于8,转为红黑树)。
参考:
HashMap深入理解详细分析原理以及常见面试问题_jjc120074203的博客-优快云博客
11、hasmap 与 hashtable 的区别
共同点:都是双列集合,底层都是哈希算法
1、对Null key 和Null value的支持不同
Hashtable既不支持Null key也不支持Null value。
HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
2、线程安全性不同
Hashtable是线程安全的,效率低,它的每个方法中都加入了Synchronize方法。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步
HashMap不是线程安全的,效率高,在多线程并发的环境下,可能会产生死锁等问题。
3. 继承的父类不同
HashTable 继承自 Dictionary 类,而 HashMap 是 Java1.2 引进的 Map interface 的一个实现。
4、哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
//hashtable代码是这样的:
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
//而HashMap重新计算hash值,而且用与代替求模:
int hash = hash(k);
int i = indexFor(hash, table.length);
5、Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。
参考:https://blog.youkuaiyun.com/weixin_43892898/article/details/88979688
https://blog.youkuaiyun.com/varyall/article/details/80992123
12、线程的5种状态
1. 新建(NEW):新创建了一个线程对象。
2. 可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
3. 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
4. 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运
行(runnable)状态。
5. 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
13、线程的创建方式
1.继承Thread类,重写run方法
public class ThreadDemo extends Thread{
public ThreadDemo(){
//编写子类的构造方法,可缺省
}
public void run(){
//编写自己的线程代码
System.out.println(Thread.currentThread().getName());
}
}
2.实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
public class ThreadDemo {
public static void main(String[] args){?
System.out.println(Thread.currentThread().getName());
Thread t1 = new Thread(new MyRunnable());
t1.start();?
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName()+"-->我是通过实现接口的线程实现方式!");
}
}
3.通过Callable和FutureTask创建线程,这种线程是有返回值的
public class TestCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i=0;i<1000;i++){
System.out.println("TestCallable被调用了"+i+"次");
sum +=i;
}
return sum;
}
public static void main(String[] args) {
TestCallable tc = new TestCallable();
FutureTask<Integer> future = new FutureTask<Integer>(tc);
new Thread(future).start();
for(int i=0;i<1000;i++){
System.out.println("主线程被执行了"+i+"次");
}
try {
//result用于接收线程new Thread(future).start()执行结果
Integer result = future.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
参考:Thread、Runnable、Callable三种创建线程的简单示例及区别简介_DongVagrant的博客-优快云博客
14、线程池的理解
1、什么是线程池
线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新创建一个线程。线程池中线程的数量通常完全取决于可用内存数量和应用程序的需求;每个线程都有被分配一个任务,一旦任务完成了,线程回到池子中并等待下一次分配任务。
2、为什么使用线程池
降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
3、ThreadPoolExecutor类
构造器中各个参数的含义:
corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可
以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,
直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
threadFactory:线程工厂,主要用来创建线程;
handler:表示当拒绝处理任务时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
3、常用四种线程池介绍
ExeCutors.newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(先进先出, 后进先出, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
ExeCutors.newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
ExeCutors.newFixedThreadPool()
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中
没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
ExeCutors.newScheduledThreadPool()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
4、队列的类型:
线程池的三种队列区别:SynchronousQueue、LinkedBlockingQueue 和ArrayBlockingQueue
1.SynchronousQueue(CachedThreadPool) 类似交警只是指挥车辆,并不管理车辆
SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。
超出直接corePoolSize个任务,直接创建新的线程来执行任务,直到(corePoolSize+新建线程)> maximumPoolSize。不是核心线程就是新建线程。
2.LinkedBlockingQueue(single,fixed)类似小仓库,暂时存储任务,待系统有空的时候再取出执行
BlockingQueue是双缓冲队列。BlockingQueue内部使用两条队列,允许两个线程同时向队列一个存储,一个取出操作。在保证并发安全的同时,提高了队列的存取效率。
LinkedBlockingQueue是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSizes就相当于无效了),每个线程完全独立于其他线程。生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并
行操作队列中的数据。
3.ArrayBlockingQueue
ArrayBlockingQueue是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的
maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错
4、PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
:缓存队列-核心线程数-最大线程数之间的关系。
当需要加入进程时:
(1)当前线程数小于核心线程数,当前线程直接运行。
(2)当前线程数大于核心线程数,当前线程会加入到阻塞队列中,
(3)此时阻塞队列未满,直接加入,等待机会运行。
(4) 此时阻塞队列已满,但此时线程数小于最大线程数,则直接创建线程运行。
(5)此时线程数大于等于最大线程数,则实行线程池自定义的拒绝策略。
————————————————
参考:Java并发编程——线程池的使用_西木风落-优快云博客
java常用的几种线程池比较 - 割肉机 - 博客园
多线程-线程池(队列-最大线程数-核心线程数)_多线程 核心线程 和 队列-优快云博客
15、Synchronized方法锁、对象锁、类锁区别
synchronized,这个东西咱们通常称之为”同步锁“,他在修饰代码块的时候须要传入一个引用对象做为“锁”的对象。jvm
在修饰方法的时候,默认是当前对象做为锁的对象
在修饰类时,默认是当前类的Class对象做为所的对象
故存在着方法锁、对象锁、类锁 这样的概念函数
参考:Synchronized方法锁、对象锁、类锁区别 (精) - 尚码园
16、sleep,wait,yield,join的区别
位于Object类:
wait()方法的作用是将当前运行的线程挂起(即让其进入阻塞状态,并释放锁),直到notify或notifyAll方法来唤醒线程.waite()和notify()必须在synchronized函数或synchronized block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运行时会发生
IllegalMonitorStateException的异常。
wait(long timeout),该方法与wait()方法类似,唯一的区别就是在指定时间内,如果没有notify或notifAll方法的唤醒,也会自动唤醒。
位于Thread类:
sleep()使当前线程进入阻塞状态,在指定时间内不会执行,不会释放锁。
yield()方法的作用是暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态。
调度器可能会忽略该方法。
使用的时候要仔细分析和测试,确保能达到预期的效果。
很少有场景要用到该方法,主要使用的地方是调试和测试。
join()方法的作用是父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程。JDK中提供三个版本的join方法,其实现与wait方法类似,join()方法实际上执行的join(0),而join(long millis, int nanos)也与wait(long millis, int nanos)的实现方式一致,暂时对纳秒的支持也是
不完整的。
参考:Java 并发编程:线程间的协作(wait/notify/sleep/yield/join) - liuxiaopeng - 博客园
17、内存泄露与溢出
1.内存泄露 memory leak
指程序在申请内存后,被某个对象一直持有,无法释放已申请的内存空间
一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
2.内存溢出 out of memory
指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;
内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
18、常见的内存泄露与处理
1、Handler持有的引用最好使用弱引用,在Activity被释放的时候要记得清空Message,取消Handler对象的Runnable;
2、非静态内部类、非静态匿名内部类会自动持有外部类的引用,为避免内存泄露,可以考虑把内部类声明为静态的;
3、对于生命周期比Activity长的对象,要避免直接引用Activity的context,可以考虑使用ApplicationContext;(如单例模式)
4、广播接收器、EventBus等的使用过程中,注册/反注册应该成对使用;
5、不再使用的资源对象Cursor、File、Bitmap等要记住正确关闭;
6、集合里面的东西、有加入就应该对应有相应的删除。
可以使用使用 LeakCanary 检测 Android 的内存泄漏
参考:android 常见内存泄漏原因及解决办法 - 安大叔 - 博客园
19、静态代理和动态代理的区别和联系
一、代理概念
为某个对象提供一个代理,以控制对这个对象的访问。 代理类和委托类有共同的父类或父接口,这样在任何使用委托类对象的地方都可以用代理对象替代。代理类负责请求的预处理、过滤、将请求分派给委托类处理、以及委托类执行完请求后的后续处理。
二、静态代理
由程序员创建或工具生成代理类的源码,再编译代理类。所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。
静态代理类优缺点
优点:业务类只需要关注业务逻辑本身,保证了业务类的重用性。这是代理的共有优点。?
缺点:
1)代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理,静态代理在程序规模稍大时就无法胜任了。
2)如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。?
三、动态代理
动态代理类的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定。
参考:动态代理与静态代理区别_ikownyou的博客-优快云博客_动态代理和静态代理的区别
20、加密算法
分类:
对称加密:对称式加密就是加密和解密使用同一个密钥
常用的加密: DES、3DES、Blowfish、IDEA、RC4、RC5、RC6 和 AES
非对称加密:非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。 非对称加密算法实现机密信息交换的基本过程是:甲方生成一对
密钥并将公钥公开,需要向甲方发送信息的其他角色(乙方)使用该密钥(甲方的公钥)对机密信息进行加密后再发送给甲方;甲方再用自己私钥对加密后的信息进行解密。甲方想要回复乙方时正好相反,使用乙方的公钥对数据进行加密,同理,乙方使用自己的私钥来进行解密。(用于防止密文被破解、被第三方得到明文)
另一方面,甲方可以使用自己的私钥对机密信息进行签名后再发送给乙方;乙方再用甲方的公钥对甲方发送回来的数据进行验签。(用于防止明文被篡改,确保消息的完整性和正确的发送方。)
常用的加密: RSA、ECC(移动设备用)、Diffie-Hellman、El Gamal、DSA(数字签名用)
hash加密:Hash算法特别的地方在于它是一种单向算法,用户可以通过Hash算法对目标信息生成一段特定长度的唯一的Hash值,却不能通过这个Hash值重新获得目标信息。因此Hash算法常用在不可还原的密码存储、信息完整性校验等。
常用的加密:MD2、MD4、MD5、HAVAL、SHA、SHA-1、HMAC、HMAC-MD5、HMAC-SHA1
21、HTTP与HTTPS握手的那些事
HTTP三次握手
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
HTTPS握手过程
1. 客户端发起HTTPS请求
2. 服务端的配置
采用HTTPS协议的服务器必须要有一套数字证书,可以是自己制作或者CA证书。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用CA证书则不会弹出提示页面。这套证书其实就是一对公钥和私钥。公钥给别人加密使用,私钥给自己解密使用。
3. 传送证书
这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等。
4. 客户端解析证书
这部分工作是有客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值,然后用证书对该随机值进行加密。
5. 传送加密信息
这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。
6. 服务段解密信息
服务端用私钥解密后,得到了客户端传过来的随机值(私钥),然后把内容通过该值进行对称加密。所谓对称加密就是,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。
7. 传输加密后的信息
这部分信息是服务段用私钥加密后的信息,可以在客户端被还原。
8. 客户端解密信息
客户端用之前生成的私钥解密服务段传过来的信息,于是获取了解密后的内容。
PS: 整个握手过程第三方即使监听到了数据,也束手无策。
HTTPS和HTTP的区别
1. https协议需要到ca申请证书或自制证书。
2. http的信息是明文传输,https则是具有安全性的ssl加密。
3. http是直接与TCP进行数据传输,而https是经过一层SSL(OSI表示层),用的端口也不一样,前者是80(需要国内备案),后者是443。
4. http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
参考:HTTP与HTTPS握手的那些事 - 海角在眼前 - 博客园
22、java的访问级别
本类 | 本包 | 子类 | 其他包 | |
private | Y | X | X | X |
无修饰 | Y | Y | X | X |
protected | Y | Y | Y | X |
public | Y | Y | Y | Y |