Android面试指南(三)————Java基础篇
Java基础篇
指令重排
as-if-serial
不管指令怎么重排序,在单线程下执行结果不能改变
happens-before
一个操作的执行结果需要对另一个操作可见,则两个操作之间必须存在happens-before关系,主要强调在多线程情况中
public class ControlDep{
int a = 0;
boolean flag = true;
public void init(){
a = 1; //1
flag = true; //2
}
public void use(){
if(flag){ //3
int i = a * a; //4
}
}
}
存在两个线程A,B,当A执行init发生了重排序,即先执行2,在执行1,当执行2时,B执行了use方法,但是B拿到的a还是0,所以i = 0,而正确的答案应该是i = 1
解决上面问题有两种方案:
- 内存屏障(volatile),禁止关于a的指令重排
- synchronized锁,锁住该对象或者该类
JVM内存模型
本地方法栈,程序计数器,虚拟机栈都是线程私有的,不存在线程安全 方法区和堆区,所有线程共享的,需要加锁保证线程安全
- 程序计数器:占用内存小,线程私有,生命周期与线程相同,大致为字节码行号指示器
- 虚拟机栈:java方法执行的内存模型,包含局部变量表,操作栈,动态链接,方法出口等信息,用于管理java方法的调用,使用连续的内存空间
- 本地方法栈:本地方法栈用于管理本地方法的调用
- 堆区:与jvm生命周期相同,存储所有的对象实例(包括数组)
- 方法区:存储已被加载的类信息,常量池,静态变量,即使编译器编译后的代码
静态变量创建在方法区,程序结束后回收,与堆无关
stack的大小默认为1M,如果是递归调用,大概只支持800多次
JVM内存模型的三大特性
原子性:多线程情况下,一旦一个线程开始执行,就不能被其他线程干扰
可见行:当一个线程修改了变量后及时更新到主存
有序性:处理器在执行运算的时候,会对程序代码进行乱序执行优化,也叫做重排序优化
垃圾回收机制
如何判断对象是个垃圾?
- 引用计数法 要操作对象必须使用引用,所以通过引用计数来判断对象是否需要被回收。因为无法解决循环引用的问题,所以JAVA中并没有采用这种方式(python中采用)
- 可达性分析法
为了解决循环引用的问题,使用可达性分析。通过一系列的"GC ROOT"对象作为起点进行搜索,如果在"GC ROOT"和对象之间没有可达路径,那么该对象为不可达对象,并标记一次,标记两次后就会被回收。
"GC ROOT":
- 虚拟机栈中引用的对象(栈帧中的本地变量表);
- 方法区中的常量引用的对象;
- 方法区中的类静态属性引用的对象;
- 本地方法栈中JNI(Native方法)的引用对象。
- 活跃线程对象
垃圾回收机制是针对堆区的回收
比较常见的将对象判定为可回收变量
- 某个引用对象为null
Object obj = new Object();
obj = null;
- 已经指向某个对象的引用指向新的对象
Object obj1 = new Object();
Object obj2 = new Object();
obj1 = obj2;
- 局部引用所指向的对象
void fun() {
.....
for(int i=0;i<10;i++) {
Object obj = new Object();
System.out.println(obj.getClass());
}
}
循环每执行完一次,生成的Object对象都会成为可回收的对象。
- 只有弱引用修饰的
WeakReference<String> wr = new WeakReference<String>(new String("world"));
垃圾回收算法
- 标记清除算法 将可回收对象标记后指定删除对象 缺点:产生大量内存碎片
- 复制算法 为了解决内存碎片的问题,提出复制算法。把内存按容量分成两份,当一份用完了,将还存活的对象复制在另一块对象中,把已使用的内存空间一次性清理掉 缺点:空间上的两倍消耗,可使用内存空间减半
- 标记整理算法 为了充分利用内存空间,在标记回收对象后,将存活对象向一端移动,然后清理掉端边界以外的内存
- 分代回收算法 将内存分为新生代,老年代和永久代。 新生代: 使用复制算法,回收大量对象,但不是按照1:1分配内存空间,将内存空间分为3份,较大的Eden和两块较小的Survivor空间,每次使用Eden和一块Survivor,当进行回收时,会将Eden和一块Survivor中存活的对象复制到另一个Survivor中。(比例为8:1:1) 老年代: 使用标记整理算法(和标记清除算法----垃圾收集器种说),回收少量对象 永久代: 存在于方法区,不属于堆区,用来存储class类,常量,方法描述等,对永久代的回收主要包含两种:废弃常量和无用的类
注意: 在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
新生代 = 1/3的堆空间大小,老年代 = 2/3的对空间大小
新创建的对象都是在Eden区,大对象因为在新生代复制会影响性能,则直接创建在老年代
在Survivor中复制一次,就年龄计数+1,当年龄大大于15岁时,会移动到老年区
jdk7和jdk8上的JVM内存结构的变化?
jdk7:
在物理存储上,堆区和方法区是连续的,但是在逻辑上是分离的,因为物理存储上是存在一起的,所以在Full GC时,会触发堆永久代的回收
jdk8:
- 取消永久代,将类的结构等信息放入Native内存区,常量池和静态变量/全局变量存储在堆区
- 方法区存在元空间中,Native内存区就是元空间区
Native Memory(本地内存),空间不足,不会触发gc
为什么使用元空间替代永久代?
避免永久代的OOM发生,因为需要加载的类的总数,方法总数难以确定,分配的空间也难以确定,为了避免OOM,使用元空间,理论上可以获得本地内存中所有可用的空间
字符常量池存在那?
1.6:存储在方法区 1.7:对象存储在堆区中,引用存在字符串常量池,都在堆中 1.8:存储在堆区中
运行时常量池在哪?
1.8的时候移动到元空间中,之前都在方法区中
垃圾收集器
java种使用的是HotSpot虚拟机,HotSpot一共7种垃圾收集器,大致分为3类:
新生代收集器:Serial,ParNew,Parllel Scavenge
老年代收集器:Serial Old,CMS,Parllel Old
回收整个堆的G1收集器
- Serial(复制):新生代单线程收集器,在标记和清理都是单线程,优点是效率高,缺点是停留时间长。
- ParNew(复制):新生代并行收集器,Serial的多线程版本,在多核cpu环境下比Serial表现更好(只有他能和CMS配合)
- Parllel Scavenge(复制):新生代并行收集器,追求高吞吐量,高效利用CPU。尽快完成程序的运算任务,适合后台应用等对交互场景要求不高的场景。 吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),缩短工作线程的等待时间
- Serial Old(标记-整理):老年代的单线程收集器,老年版的单线程
- Parllel Old(标记-整理):老年代的并行收集器,老年版的Parllel Scavenge
- CMS(Concurrent Mark Sweep)(标记-清除):老年代并行收集器,以获取最短回收停顿时间为目标,具有高并发,低停顿的特点。追求最短GC回收停顿时间,就是GC的时间更短
缺点:
- 对CPU资源异常敏感,应用程序变慢,吞吐率下降
- 无法处理浮动垃圾。因为在标记和清除的时候,工作线程是运行的,所以期间会产生新的垃圾,但是本次无法回收。
- 产生大量内存碎片,会提前触发Full GC
- G1(Garbage First)(标记-整理):java并行收集器,G1的回收范围包含新生代和老年代。他用来作为下一代的收集器,保存新生代和老年代的概念,但是内部将Java堆划分为多个大小相等Region独立区域
优点:
- 并行和并发。使用多个CPU缩短回收停顿时间,与用户线程并发执行
- 分带收集。独立去管理整个堆区间,能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次GC的旧对象,以获取更好的收集效果
- 使用标记-整理算法。无内存碎片产生。
- 可预测的停顿。可以使开发者制定一个时间长度,在该时间长度内,需要完成垃圾回收。
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge收集器+Parallel Old收集器的组合
gc的种类和方式
- Minor GC:新生代GC
- 当Eden(['id(ə)n])区放满的时候,触发Minor GC
- Major GC:老年代GC
- Full GC:全局GC(青年+老年)
- System.gc()方法有可能触发Full GC
- 老年代存储满了
- 永久代存储满了,触发Full GC,针对常量池的回收和类型的卸载
- Minor GC后放入老年代大小>老年代可用内存,即老年代放不下
- Minor GC后,放入一个1区中时,放不下,溢出来部分放入老年区,老年区放不下就会触发Full GC
GC会触发“stop-the-world”,即工作线程全部关闭,进行gc回收,当gc回收结束后,才会执行任务
HashMap
(1)美团面试题:Hashmap的结构,1.7和1.8有哪些区别,史上最深入的分析
简述
影响性能的两个参数:
-
初始容量:2的幂,默认是16
-
加载因子:什么时候扩容的标志,默认0.75,即16*0.75=12的时候开始hashmap扩容(容量为原来的2倍)
-
最大容量:2的30次方,如果大于,则使用2的30次方的大小
-
可以存储key == null,value == null,key == null则存储在table[0]位置
-
删除元素的本质是“删除单向链表的节点”
-
Entry是单向链表
计算key的hash值,并将hash值添加到对应的链表中,若key存在,则更新vlaue值
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //计算出来的hash值
final K key; //key
V value; //value
Node<K,V> next; //链表next引用
......
}
和修改
- 因为是非synchronized的,非线程安全,所以比较快
- HashMap可以接受null键和null值
数组下标index的计算过程
//数组长度-1 & hash值
(n - 1) & hash
同等于hash值对数组长度的求余
描述一下具体的put过程
- 对key求hash值,然后计算数组下标
- 如果数组下标没有碰撞,将Node放置在数组中
- 如果碰撞,将Node以链表的形式连接在后面
- 如果链表长度超过阈值(8),将链表转化为红黑树,链表长度低于6,则将红黑树转回链表
- 如果节点存在,则替换旧值
- 如果数组快满了(最大容量16*加载因子0.75),就需要resize(扩容两倍)
为什么选择6和8 ?
因为中间7的位置放置频繁的数据结构切换后,影响性能
get方法
- 计算key的hash,在计算index值
- 在数组中查找index值,在比对key值,取出value,复杂度最好是O(1),最坏为O(n)
为什么不直接使用红黑树?
空间和时间的选择,链短的时候空间上占用小,时间还好,转化为红黑树后,便于查找,但是耗费空间。
处理hash冲突的方法有以下几种:
- 开放地址法(线性探测再散列(碰撞后,位置后挪,数组长度+x)x可为正数,二次探测再散列(数组长度+x的平方)x可为正负数,平方后均为正数)
- 再哈希法(多种计算哈希的方法,相同则替换方法,直到算出不重复的哈希值)
- 链地址法(链表)
- 建立公共溢出区(建立一个溢出表,存放冲突的数据)
HashMap的性能慢原因?
- 数据类型自动装箱问题
- resize扩容重新计算index值和hashcode,重新赋值(1.7) 1.8后,扩容位置 = hash值 & 数组长度,如果为0,则不动,反之则反
线程不安全会导致什么
环状链表,resize(扩容)时头插法导致环形链表(1.7版本)
都存在数据丢失的问题数据丢失,1.8版本修复环形链表(尾插)
HashMap中默认容量为什么是2的幂?
因为如果不是2的幂,可能会造成更多的hash碰撞(index 下标碰撞) 假设n为17,n-1的二进制为10000,01001和01101算出的index值均为0 假设n为16,n-1的二进制为01111,01001和01101算出的index值不同
hashcode计算原理
对于int类型,hashcode为它本身,eg:int i = 1; hashcode = 1; 对于对象来说,hashcode是内部地址和对象值的一个映射
hash()算法原理
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
拿到key的hashCode(),在将该值与该值的高16位(h无符号右移16位)进行亦或运算(相同为0,不同为1)
HashTable的理解
put和get方法是用了synchronized修饰,锁住了整个map,同一时刻只有一个线程可以操作
不可以存储null值和null健
SparseArray理解
原理
装箱,int数据类型---->Integer对象,拆箱,Integer对象---->int数据类型
默认容量是10
- key是int值(避免装箱问题),使用二分查找寻找key,同样也是用二分插入,从小到大排列好的
- 两个数组,一组存放key(int []),一组存放value(object [])
mKeys[i] = key;
mValues[i] = value;
- 如果冲突,直接替换value的值
二分插入:
while (lo <= hi) {
//二分法一分而二,数组中间下标
final int mid = (lo + hi) >>> 1;
//二分法一分而二,数组中间下标处的值
final int midVal = array[mid];
if (midVal < value) {
/**
如果数组中间处的值比要找的值小,代表要找的值
在数组的中后部部分,所以当前下标取值为mid + 1
*/
lo = mid + 1;
} else if (midVal > value) {
/**
如果数组中间处的值比要找的值大,代表要找的值
在数组的前中部部分,所以当前下标取值为mid - 1
*/
hi = mid - 1;
} else {
//数组中间处的值与要找的值相等,直接返回数组中部的下标mid
return mid; // value found
}
}
第一个值放到最中间位置
第二个值如果大于中间的值放置在左边的中间位置
………….
put方法中,容量充足,计算key值所需存放的index,如果key相同,就直接替换value,如果不同,就insert数组,后续index元素后移,新key放置在index上
较HashMap的优点
- 节省内存
- 性能更好,避免装箱问题
- 数据量不达到千级,key为int值,可以用SparseArray替换HashMap
SparseArray与HashMap的比较,应用场景是?
- SparseArray采用的不是哈希算法,HashMap采用的是哈希算法
- SparseArray采用的是两个一维数组分别用于存储键和值,HashMap采用的是一维数组+单向链表/红黑树
- SparseArray key只能是int类型,而HashMap可以任何类型
- SparseArray key是有序存储(升序),而HashMap不是
- SparseArray 默认容量是10,而HashMap默认容量是16
- SparseArray 内存使用要优于HashMap,因为:
- SparseArray key是int类型,而HashMap是Object
- SparseArray value的存储被不像HashMap一样需要额外的需要一个实体类(Node)进行包装
- SparseArray查找元素总体而言比HashMap要逊色,因为SparseArray查找是需要经过二分法的过程,而HashMap不存在冲突的情况其技术处的hash对应的下标直接就可以取到值
针对上面与HashMap的比较,采用SparseArray还是HashMap,建议根据如下需求选取:
- 如果对内存要求比较高,而对查询效率没什么大的要求,可以是使用SparseArray
- 数量在百级别的SparseArray比HashMap有更好的优势
- 要求key是int类型的,因为HashMap会对int自定装箱变成Integer类型
- 要求key是有序的且是升序
ArrayMap的理解
内部也使用二分算法进行存储和查找,设计上更多考虑了内存中的优化
- int []存储hash值,array[index]存储key,array[index+1]存储value
数据量最好在千级以内
ArrayMap和SparseArray怎么进行选取?
- 如果key为int,那么选取SparseArray进行存储, 不存在封/拆箱问题
- 如果key不为int,则使用ArrayMap
TreeMap的理解
TreeMap是一个二叉树的结构,红黑树
不允许重复的key
TreeMap没有调优选项,因为其红黑树总保持在平衡状态
TreeMap和HashMap的区别?
- TreeMap由红黑树构成,HashMap由数组+链表/红黑树构成
- HashMap元素没有顺序,TreeMap元素会根据可以进行升序排序
- HashMap进行插入,查找,删除最好,TreeMap进行自然顺序便利或者自定义顺序便利比较好
ThreadLocal的理解
面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)
线程隔离,数据不交叉
- ThreadLocalMap,每个thread都存在一个变量ThreadLocalMap threadLocals
- threadLocalMap中存在Entry,同ThreadLocal之间为弱引用关系
- ThreadLocalMap中key为ThreadLocal的弱引用,value为Entry,内部为一个object对象
- table默认大小为16,存在初始容量(16)和阈值(16*2/3)
- 在ThreadLocal中使用get()和set()方法初始化threadLocals
- get、set、remove方法将key==null的数据清除
- table是环形数组
线性探测法避免哈希冲突,增量查找没有被占用的地方
通过hashcode计算索引位置,如果key值相同,则替换,不同就nextIndex,继续判断,直到插入数据
ThreadLocal就是管理每个线程中的ThreadLocalMap,所以线程隔离了。
ThreadLocalMap的理解
新建ThreadLcoal的时候,创建一个ThreadLocalMap对象,计算hash的时候使用0x61c88647这个值,他是黄金分割数,导致计算出来的hash值比较均匀,这样回大大减少hash冲突,内部在采用线性探测法解决冲突 set:
- 根据key计算出数组索引值
- 遍历该索引值的链表,如果为空,直接将value赋值,如果key相等,直接更新value,如果key不相等,使用线性探测法再次检测。
ThreadLocal使用弱引用的原因
key使用了弱引用,如果key使用强引用,那么当ThreadLocal的对象被回收了,但ThreadLocalMap还持有ThreadLocal的强引用,回导致ThreadLocal不会被回收,导致内存泄漏
ThreadLocal的内存泄漏
- 避免使用static修饰ThreadLocal:延长生命周期,可能造成内存泄漏
- ThreadLocal弱引用被gc回收后,则key为null,object对象没有被回收,只有当再次调用set,get,remove方法的时候才会清楚key为null的对象
ThreadLocalMap清理过期key的方式
- 探测式清理 本该放在4的位置上的值,放到了7的位置上,当5过时后,将7的数据挪到5的位置上
- 启发式清理 遍历数组,清理数据
ConcurrentHashMap和HashMap的区别
jdk 1.7 ReentrantLock+segments + hashEntry(不可变)

- 线程安全,分段线程锁,hashtable是整段锁,所以性能有所提高
- 默认分配16个锁,比Hashtable效率高16倍
- hashEnty是final的,不能被修改,只要被修改,该节点之前的链就要重新创建,采用头插插入,所以顺序反转
- 获取size,因为是多线程访问,所以size会获取三遍,如果前后两个相等就返回,假设不相等,就将Segment加锁后计算。
jdk 1.8 : synchronized +node+volatile+红黑树
put:
- 根据key的hash值算出Node数组的相应位置
- 如果该Node不为空,且当前该节点不处于移动状态,则对节点加synchronized锁,进行遍历节点插入操作
- 如果是红黑树节点,向红黑树插入操作
- 如果大于8个,拓展为红黑树
get:
- 计算hash值,定位到该table索引位置,如果是首节点符合就返回
- 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,通知在新表中查找该节点,匹配就返回
- 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
1.7和1.8的区别:
-
1.7:ReentrantLock+segments + hashEntry(不可变)
1.8:synchronized +node+volatile+红黑树
-
1.8的锁的粒度更低,锁的是一个链表(table[i]),而1.7锁的是一个小的hashmap(segement)
-
ReentrantLock性能比synchronized差
扩容:
1.7下进行小HashMap(segement)扩容操作
1.8下使用synchrozied节点加锁,所以可以通过多个线程扩容处理。一个线程创建新的ConcurrentHashMap,并设置大小,多个线程将旧的内容添加到新的map中,如果添加过的内容就会设置标记,其他线程就不会处理
为什么只有hashmap可以存储null值和null键
因为hashmap是线程不安全的,而在其他中都是线程安全的,在多线程访问时,无法判断key为null是没有找到,还是key为null
锁
常见锁
锁的分类
-
公平锁/非公平锁
-
公平锁:多个线程按照申请锁的顺序获取锁。
-
非公平锁:多个线程申请锁并不是按照顺序获取锁,有可能先申请后获取锁。(Synchronized)
ReentrantLock默认是非公平锁,通过构造传参可设置为公平锁。非公平锁的优点在于吞吐量比公平锁大
-
-
可重入锁:又名递归锁,指在外层方法获取锁以后,在进入内层方法也会自动获取锁。
synchronized void setA() throws Exception(){
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception(){
Thread.sleep(1000);
}
如果不是可重入锁,那么setB方法不会被当前线程执行,容易造成死锁
synchronized是可重入锁
-
独享锁/共享锁
- 独享锁:一个锁一次只能被一个线程所持有(ReentrantLock,synchronized)
- 共享锁:一个锁被多个线程所持有。(ReadWriteLock)
-
互斥锁/读写锁 上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。 互斥锁在Java中的具体实现就是ReentrantLock 读写锁在Java中的具体实现就是ReadWriteLock
-
乐观锁/悲观锁
- 悲观锁:对同一数据的并发操作,一定会发生修改的。(利用各种锁实现)
- 乐观锁:对同一数据的并发操作,一定不会发生修改的。(无锁编程,CAS算法,自旋实现原子操作的更新)
-
分段锁 是一种锁的设计,并不是具体的锁,在1.7版本的ConcurrentHashMap中,使用分段锁设计,该分段锁又称为Segment,map中每一个链表由ReentrantLock修饰
-
偏向锁/轻量级锁/重量级锁 这三种锁是描述synchronized的三种状态。
-
偏向锁:一段同步代码一直被一个线程访问,那么会自动获取锁,降低获取锁的代价
-
轻量级锁:当锁是偏向锁的时候,被另一个线程访问,偏向锁会升级为轻量级锁,其他线程通过自旋的方式获取锁,不会阻塞,提高性能
-
重量级锁:在轻量级锁的基础上,自旋达到上限就会阻塞,升级为重量级锁,会让其他线程进入阻塞,影响性能。
锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后无法降为偏向锁,这种升级无法降级的策略目的就是为了提高获得锁和释放锁的效率。
-
-
自旋锁 获取锁的过程中,不会立即阻塞,会采用循环的方式获取锁,减少线程切换上下文的消耗,缺点是循环会消耗cpu
java中常用锁的类型
- synchronized:非公平,悲观,独享,互斥,可重入,重量级锁
- ReentrantLock:默认非公平(可公平),悲观,独享,互斥,可重入,重量级锁
CAS,全称为Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子地更新某个位置的值,实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM 只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。
synchronized和volatile
简述synchronized的原理
可见性:表示A修改的值对于B执行时可以看见A修改后的值
- 内部使用monitorenter指令,同时只有一个线程可以获取monitor
- 未获取monitor的线程会被阻塞,等待获取monitor
- 线程A获取主内存值后加锁,在本地内存更新值(临时区)后,推送到主内存,通过synchronized隐式通知线程B访问主存获取值,在B的把本地内存更新值后推送到主存,重复以上操作。
通过Monitor对象来实现方法和代码块的同步,存在monitorEnter和monitorExit指令,插入程序中,在一个线程访问时,通过Monitor进行线程阻塞
synchronized修饰静态方法、⾮静态方法区别
静态方法:该类的对象,new出来的多个实例对象是被一个锁锁住的,多线程访问需要等待
非静态方法:实例对象
volatile
修饰成员变量,保证可见性,下一个操作再上一个操作之上。++操作不保证和原子性,
将本地缓存同步到主存中,使其他本地缓存失效,本地缓存通过嗅探检查自己的缓存是否过期。(下一次访问,主存不会主动通知)
volatile无法保证原子性,可以使用乐观锁的重试机制进行优化
synchronized和volatile区别
-
Synchronized 引起线程阻塞,而volatile不会
-
区别在于,synchronized是隐式通知B去主存获取值,volatile是B主动通过嗅探的方法发现自己的内存过期后去主存做同步
-
synchronized:先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。
-
都存在可见性,但是volatile不具备原子性,所以不会造成线程阻塞
假设某一时刻i=10,线程A读取10到自己的工作内存,A对该值进行加一操作,但正准备将11赋给i时,由于此时i的值并未改变,B读取了主存的值仍为10到自己的工作内存,并执行了加一操作,正准备将11赋给i时,A将11赋给了i,由于volatile的影响,立即同步到主存,主存中的值为11,并使得B工作内存中的i失效,B执行第三步,虽然此时B工作内存中的i失效了,但是第三步是将11赋给i,对B来说,我只是赋值操作,并没有使用i这个动作,所以这一步并不会去刷新主存,B将11赋值给i,并立即同步到主存,主存中的值仍为11。虽然A/B都执行了加一操作,但主存却为11,这就是最终结果不是10000的原因。
-
synchronized修饰方法,类,变量,代码块,volatile只能修饰变量
synchronized修饰不同对象的区别
- 修饰类:作用的对象是这个类的所有对象
- 方法:作用对象是这个方法的对象
- 静态方法:作用对象是这个类的对象
- 代码块:作用对象是这个代码块的对象
悲观锁和乐观锁(CAS)
悲观锁:当前线程获得锁会阻塞其他线程(sychronized)
乐观锁:不会添加锁,会存在三个值内存实际值,内存的旧值,更新的新值,如果内存实际值和旧值相等,则没有线程修改该值,将更新的新值直接赋值给内存,如果不相等,就重新尝试赋值操作(volatile)
CAS的缺点:
- ABA问题,A->B->A,乐观锁认为没有变化,都是A,所以直接赋值
- 重新赋值的话,会导致时间过长。
ReentrantLock
CAS+AQS实现,乐观锁
AQS(单链表队列)维护一个等待队列,将获取不到锁的线程放入到队列中进行等待,当当前线程执行结束后,进行出队操作,使用一个volatile的int成员变量(state)来表示同步状态
通过ReentrantLock的Lock方法进行加锁
通过ReentrantLock的unLock方法进行解锁
线程
新建线程有几种方式?
- new Thread
- 新建Runnable对象
- 新建Callable或者Future对象
- 线程池使用
new Thread的弊端
执行一个异步任务你还只是如下new Thread吗? new Thread的弊端如下:
- 每次new Thread新建对象性能差。
- 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
- 缺乏更多功能,如定时执行、定期执行、线程中断。
相比new Thread,Java提供的四种线程池的好处在于:
- 重用存在的线程,减少对象创建、消亡的开销,性能佳。
- 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
- 提供定时执行、定期执行、单线程、并发数控制等功能。
线程池
线程的5种状态
- NEW:创建一个新线程
- RUNNABLE:可运行
- BLOCKED:阻塞
- WAITING:进入等待状态
- TIMED_WAITING:等待结束,重新获取锁
- TERMINATED:结束
- RUNNING:运行中
- READY:就绪
一般来说分为五大状态:
- 新建(New): 创建线程对象,进入新建状态。eg:Thread thread = new Thread();
- 就绪(Runnable): 调用thread.start()方法,随时可被cpu执行
- 运行(Runnable): CPU执行线程
- 阻塞(Blocked):
出于某些原因,cpu放弃线程执行,线程进入暂停状态
- 等待阻塞:调用wait方法,进行阻塞,线程等待某工作完成
- 同步阻塞:在获取Synchronized同步锁时,进行等待
- 其他阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
- 死亡(Dead): 堪称执行完毕或者因异常退出,线程死亡,回收
start和run的区别?sleep和wait的区别?join,yield,interrupt
- start是启动一个线程
- run只是Thread的实现方法,主要实现是Runnable的接口回调run方法
- sleep不会释放对象锁,只是暂停了线程的运行,当指定时间到了,就恢复运行状态
- wait方法放弃对象锁,只有调用了notify()方法,才会重新获取锁,进入运行状态
- join方法是规定线程的执行顺序,如果在B线程中调用了A的join方法,那么,直到A执行完毕,才会执行B,按照顺序串行执行。实际内部方法是调用了wait方法,让B处于等待状态,A执行完成后,启动B
注意:wait方法是调用u哦在线程放弃对象锁,所以在B线程调用A的join方法,只是让B等待了。
- yield方法,通知cpu该线程任务不紧急,可以被暂停让其他线程运行
- interrupt方法,中断通知线程,具体操作由线程执行,根据不同状态,执行不同逻辑
线程t1、t2、t3,如何保证他们顺序执行?
t3开始中调用t2.join(),t2开始中调用t1.join()。
t1执行完毕后,t2中t1.join()方法不阻塞,即t1执行完,执行t2中的方法,后续类似 使用CountDownLacth,进行计数
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
//引用t1线程,等待t1线程执行完
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
//引用t2线程,等待t2线程执行完
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}
});
t3.start();
t2.start();
t1.start();
}
什么是死锁
资源竞争互相等待
假设线程A,线程B,资源A,资源B
线程A访问资源A,持有资源A锁,线程B访问资源B,持有资源B锁,而后线程A要访问资源B,但是线程B持有资源B锁,线程A等待,线程B要访问资源A,但是线程A持有资源A锁。所以B等待。
结果就是A、B相互等待对方释放资源,造成死锁。
一个线程崩溃会影响其他线程吗?
不一定。 如果崩溃发生在堆区(线程共享区域),会导致其他线程崩溃。 如果崩溃发生在栈区(线程私有区域),不会导致其他线程的崩溃
java反射
- 反射类及反射方法的获取,都是通过从列表中搜寻查找匹配的方法,所以查找性能会随类的大小方法多少而变化;
- 每个类都会有一个与之对应的Class实例,从而每个类都可以获取method反射方法,并作用到其他实例身上;
- 反射也是考虑了线程安全的,放心使用;
- 反射使用软引用relectionData缓存class信息,避免每次重新从jvm获取带来的开销;
- 反射调用多次生成新代理Accessor, 而通过字节码生存的则考虑了卸载功能,所以会使用独立的类加载器;
- 当找到需要的方法,都会copy一份出来,而不是使用原来的实例,从而保证数据隔离;
- 调度反射方法,最终是由jvm执行invoke0()执行;
使用反射从jvm中的二进制码文件中读取数据
反射原理
.java-->.class-->java.lang.Class对象
编译过程:
- 将.java文件编译成机器可以识别的二进制文件.class
- .class文件中存储着类文件的各种信息。 比如版本号、类的名字、字段的描述和描述符、方法名称和描述、是不是public、类索引、字段表集合,方法集合等等数据
- JVM从二进制文件.class中取出并拿到内存解析
- 类加载器获取类的二进制信息,并在内存中生成java.lang.Class对象
- 最后开始类的生命周期并初始化(先静态后非静态和构造,先父类在子类)
而反射操作的就是内存中的java.lang.Class对象。
总结来说.class是一种有顺序的结构文件,而Class对象就是对这种文件的一种表示,所以我们能从Class对象中获取关于类的所有信息,这就是反射的原理。
为什么反射耗时?
- 校验时间长
- 基本类型的封箱和拆箱
- 方法内联
什么是内联函数?
方法调用过多会进行内敛优化,减少方法的嵌套层级,加快执行,缓解栈的空间存储
反射可以修改final类型的成员变量吗?
已知final修饰后不会被修改,所以获取这个变量的时候就直接帮你在编译阶段就给赋值了
编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。
所以上述的getName方法经过JVM编译内联优化后会变成:
public String getName() {
return "Bob";
}
//打印出来也是Bob
System.out.println(user.name)
//经过内联优化
System.out.println("Bob")
反射是可以修改final变量的,但是如果是基本数据类型或者String类型的时候,无法通过对象获取修改后的值,因为JVM对其进行了内联优化。
反射可以修改static值吗?
Field.get(null) 可以获取静态变量。
Field.set(null,object) 可以修改静态变量。
Java异常
简析
java中的异常分为2大类,Error和Exception。Error中有StackOverFlowError和OutOfMemoryError。Exception分为IOException和RuntimeException。
Java中检查型异常和非检查型异常有什么区别?
检查型异常 extends Exception(编译时异常):需要使用try catch进行捕获,否则会出错,继承自Exception
非检查型异常 extends RuntimeException(运行时异常):不需要捕获,在必要时才会报错,
try-catch-finally-return执行顺序?
- 不管是否有异常产生,finally块中代码都会执行
- 当try和catch中有return语句时,finally块仍然会执行
- finally是在return后面的表达式运算执行的,所以函数返回值在finally执行前确定的,无论finally中的代码怎么样,返回的值都不会改变,仍然是之前return语句中保存的值
- finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值
throw和throws的区别
throw用在方法内部,抛出异常
throws用在方法外部,在方法中抛出异常
栈溢出StackOverFlowError发生的几种情况?
递归,栈内存存满,函数调用栈太深
Java常见异常有哪些
java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。
java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.
java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。
java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
java.lang.ArithmeticException:算术条
件异常。譬如:整数除零等。
java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。
java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。
java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。
java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。
java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。
java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。
linux进程通信有几种
Linux中的进程间通信有哪些?解释Binder通信为什么高效?Binder通信有什么限制?
Linux中的进程间通信有如下几种:
- 信号(signal)
- 消息队列
- 共享内存(Shared Memory) 共享内存允许两个或多个进程进程共享同一块内存(这块内存会映射到各个进程自己独立的地址空间)从而使得这些进程可以相互通信。
- 管道/命名管道(Pipe) Pipe这个词很形象地描述了通信双方的行为,即进程A与进程B。一根管道同时具有读取端和写入端。比如进程A从write end写入,那么进程B就可以从read end读取数据。
- Socket 本地和服务端各自维护一个“文件”,在建立连接打开后,向自己的文件中写入数据,供对方读取
Binder通信是Android系统特有的IPC机制,Binder的优点有以下几个:
- 性能:Binder的效率高,只需要一次内存拷贝;而Linux中的管道、消息队列、套接字都需要2次;共享内存的方式不需要拷贝数据,但是有多进程同步的问题。
- 稳定性:Binder的架构是基于C/S结构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,自然稳定性更好。共享内存虽然无需拷贝,但是控制负责,难以使用。从稳定性的角度讲,Binder 机制是优于内存共享的。
- 安全性:传统的 IPC 接收方无法获得对方可靠的进程用户ID/进程ID(UID/PID),从而无法鉴别对方身份。Android 为每个安装好的 APP 分配了自己的 UID,故而进程的 UID 是鉴别进程身份的重要标志。Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限。从安全角度,Binder的安全性更高。
Binder通信的另外一个限制是最多16个线程。最多只能传输1M的数据,否则会有TransactionTooLarge的Exception。
CountDownLatch原理
存在4个线程,想在4个线程都执行完毕后执行另一个线程,
countDownLatch是采用计数器的原理,存在两个方法:
countDown:计数-1
await:线程挂起,当计数为0时,执行其后的逻辑
Java泛型
泛型简述
java中泛型即是“参数化类型”,即该泛型类型是一个参数传入
只在程序的源代码中存在,在编译后的字节码中已经替换为原生类型,这种方法称为伪泛型。
java中的泛型只在编译时期有效,正确检验泛型的结果后,会将泛型相关的信息擦出,并在对象进入和离开的方法边界上添加类型检查
和类型转化
的方法。
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();
Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
if(classStringArrayList==classIntegerArrayList){ //返回true
System.out.println("类型相同");
}
泛型有泛型类
、泛型方法
和泛型接口
泛型类:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.key = key;
}
public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
return key;
}
}
泛型接口:
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class FruitGenerator<T> implements Generator<T>{
* 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
/**
* 传入泛型实参时:
* 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
泛型方法:
/**
* 泛型方法的基本介绍
* @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){
T instance = tClass.newInstance();
return instance;
}
泛型对方法重载的影响?
方法不能进行重载,会报错,两种方法都有相同的擦除
,在编译期间进行泛型擦除的,会导致擦出后都一样
public class MyMethod {
public void listMethod(List<String> list1){}
public void listMethod(List<Integer> list2){}
}
类加载
java类的初始化流程
父类到子类,静态到(非静态,构造),变量----->代码块
父类静态变量----父类静态代码块----子类静态变量----子类静态代码块----父类非静态----父类构造----子类非静态----子类构造
jvm类加载机制的7个流程
加载-----验证------准备------解析------初始化-------使用------卸载 JVM将.java文件加载成二进制文件.class 加载:
- 获取二进制流class文件
- 将静态存储结构转换为方法区中运行时的数据结构,存储到方法区中
- 在堆中生成一个java对象,作为方法区的引用
获取.class文件并在堆中生成一个class对象,将加载的类结构信息存储在方法区
验证:JVM规范校验,代码逻辑校验
准备:为类变量分配内存并设置类变量的初始化,如果变量被final修饰,会直接放入对应的常量池中,并赋值
解析:常量池符号引用替换为内存的直接引用
(上述三种统称为连接)
初始化:执行代码逻辑,对静态变量,静态代码块和类对象进行初始化
使用:使用初始化好的class对象
卸载:销毁创建class对象,负责运行的jvm退出内存
全局变量和局部变量的区别
- 全局变量应用于整个类文件。局部变量只在方法执行期间存在,之后被回收。静态局部变量对本函数体始终可见
- 全局变量,全局静态变量,局部静态变量都在静态存储空间。局部变量在栈(虚拟机栈)中分配空间
- 全局变量初始化需要赋值,局部变量不需要赋值
- 一个类中不能声明同名全局变量,一个方法中不能声明同名局部变量。若全局变量和局部变量同名,则在方法中全局变量不生效。
大致流程
当JVM碰到new字节码的时候,会先判断类是否已经初始化,如果没有初始化(有可能类还没有加载,如果是隐式装载,此时应该还没有类加载,就会先进行装载、验证、准备、解析四个阶段),然后进行类初始化。 如果已经初始化过了,就直接开始类对象的实例化工作,这时候会调用类对象的方法。
类初始化的时机
- 初始化main方法的主类
- new 关键字触发,如果类还没有被初始化
- 访问静态方法和静态字段时,目标对象类没有被初始化,则进行初始化操作
- 子类初始化过程中,如果发现父类没有初始化,则先初始化父类
- 通过反射API调用时,如果类没有初始化,则进行初始化操作
- 第一次调用java.lang.invoke.MethodHandle 实例时,需要初始化 MethodHandle 指向方法所在的类。
类的实例化触发时机
- new 触发实例化,创建对象
- 反射,class.newnIstance()和constructor.newnIstance()方法触发创建对象
- Clone方法创建对象
- 使用序列化和反序列化的机制创建对象
类的初始化和类的实例化的区别
类的初始化:为静态成员赋值,执行静态代码块 类的实例化:执行非静态方法和构造方法
- 类的初始化只会执行一次,静态代码块只会执行一次
- 类的实例化会执行多次,每次实例化执行一次
在类都没有初始化完毕之前,能直接进行实例化相应的对象吗?
正常情况下是先类初始化,再类实例化 在非正常情况下,比如在静态变量中
public class Run {
public static void main(String[] args) {
new Person2();
}
}
public class Person2 {
public static int value1 = 100;
public static final int value2 = 200;
public static Person2 p = new Person2();
public int value4 = 400;
static{
value1 = 101;
System.out.println("1");
}
{
value1 = 102;
System.out.println("2");
}
public Person2(){
value1 = 103;
System.out.println("3");
}
}
执行public static Person2 p = new Person2();这样就会直接实例化,然后在执行类的初始化,所以会打印
23123
多线程进行类的初始化会出问题吗?
不会,类初始化方法是阻塞的,多线程访问,只会有一个线程执行,其他阻塞。
一个实例变量在对象初始化的过程中最多可以被赋值几次?
4次
- 对象被创建时候,分配内存会把实例变量赋予默认值,这是肯定会发生的。
- 实例变量本身初始化的时候,就给他赋值一次,也就是int value1=100。
- 初始化代码块的时候,也赋值一次。
- 构造函数中,在进行赋值一次。
public class Person3 {
public int value1 = 100;
{
value1 = 102;
System.out.println("2");
}
public Person3(){
value1 = 103;
System.out.println("3");
}
}
屏幕
高刷手机,60hz,120hz指的是什么?
屏幕刷新率,1s内屏幕刷新的次数。这个参数由手机硬件决定 一般大于60hz的就是高刷收集,特点在于刷新频率更高,就算存在丢帧、卡顿,也能保持稳定性。
屏幕的刷新过程
从左到右,从上到下,顺序显示像素点。当整个屏幕刷新完毕,即一个垂直刷新周期后,(1000/60)16ms后再次刷新 一般一个图形界面的绘制,需要CPU准备数据,然后GPU进行绘制,绘制完写入缓存区,然后屏幕按照刷新频率来从这个缓存区中取图形显示。
所以整个刷新过程是CPU,GPU,屏幕(Display)三方合作的工作关系。
帧率,VSYNC是什么
帧率:GPU一秒内渲染绘制的操作的帧数,单位是fps,所以一般帧数和屏幕刷新度保持一致是效果最好的情况,不会导致一方浪费
VSYNC:垂直同步,作用是让帧率和屏幕刷新率保持一致,防止卡顿和跳帧。由于CPU和GPU绘制图像的时间不稳定,所以可能会发生卡顿情况,也就是下一帧的数据还没准备好无法正常显示在屏幕上,设置垂直同步后,要求CPU和GPU在16ms之内将下一帧的数据处理好,那么屏幕刷新的时候就可以直接从缓存中获取下一帧的数据并显示出来
屏幕中单缓存,双缓存,三缓存
- 单缓存:CPU计算好数据传递给GPU,GPU图像绘制后放到缓存区,display从缓存中获取数据并刷新屏幕 缺点:当第二帧的数据还没生成完成时,会导致屏幕中有一部分第一帧的数据,导致一个屏幕同时显示了两帧的数据
- 双缓存:CPU计算好数据传递到GPU,GPU图像会之后放入缓存区BackBuffer,当到达VSYNC垂直同步时间,将数据同步到缓存区FrameBuffer中,display从缓存区FrameBuffer中获取数据并显示 缺点:如果在一个垂直同步的时间内CPU+GPU没有渲染完成(开始绘制的时间在下次垂直同步时间附近,导致只有一小份垂直同步时间在绘制),就会浪费一个VSYNC垂直同步时间,当VSYNC垂直同步时间来临时,GPU正在处理数据,那么不会开启下一帧的处理,当GPU处理结束后,无法触发下一帧的数据处理,就会导致卡顿的情况
- 三缓存数据:当在一个垂直同步时间内没有完成处理,就会出现第三个缓存区,在第二个垂直同步时间,缓存下一帧的数据,这样两个缓存交替处理,保证FrameBuffer会拿到最新的数据,保证了显示的流畅度
代码中修改了UI,屏幕是怎么进行刷新的?
当调用invalidate/requestLayout中进行重绘工作时,会向VSYNC垂直同步服务请求,等待下一次VSYNC垂直同步时间,执行界面绘制刷新操作,CPU->GPU->Display
如果界面保持静止不变,屏幕会刷新吗?图像会被重新绘制吗?
屏幕不会刷新,不会重新绘制,如果屏幕不变,程序就收不到垂直同步时间,自动过滤,不处理屏幕刷新操作,只有当界面改变时,才会请求VSYNC垂直同步服务,触发下一次VSYNC垂直同步刷新屏幕
jvm垃圾回收机制
首先介绍4个引用
强引用:在使用时不会被回收
软引用:系统内存不足时会被回收
弱引用:下一次gc会被回收
虚引用:任何时候都可能被回收
小知识点
抽象类和接口的区别
- 抽象类中可包含普通方法+实现,接口类中只存在抽象方法,没有具体实现
- 抽象类中的值可以是任何类型的,接口中的值必须是public static final修饰的
- 一个类只能继承一个抽象类,一个类可以实现多个接口类
- 抽象类存在构造函数,接口类没有
- 抽象类中包含初始化块,接口中没有
static和final的区别
static
是可以直接调用的(类名.方法/变量),
可修饰属性,方法,代码段,内部类
所有对象只有一个值
final
可修饰属性,方法,类,局部变量
final修饰变量不可被更改值,方法不能被重写,类不能被继承
修饰集和的话,其引用不变,集和可以自由变化
java是值传递还是引用传递
如果是基本类型就是值传递
引用类型就是引用传递
String表现为值传递,但是其实是作为形参后重新创建了对象,引用已经变化,所以是值传递
public void test() {
String str = "123";
changeValue(str);
System.out.println("str值为: " + str); // str未被改变,str = "123"
}
public changeValue(String str) {
str = "abc";
}
public void test() {
Student student = new Student("Bobo", 15);
changeValue1(student); // student值未改变,不为null! 输出结果 student值为 name:Bobo、age:15
// changeValue2(student); // student值被改变,输出结果 student值为 name:Lily、age:20
System.out.println("student值为 name: " + student.name + "、age:" + student.age);
}
public changeValue1(Student student) {
student = null;
}
public static void changeValue2(Student student) {
student.name = "Lily";
student.age = 20;
}
String、StringBuilder、StringBuffer的区别
String是不可变的,每次赋值都是重新创建对象,对内存和性能都有损耗
StringBuilder是非线程安全的,存储通过一个可变长度的字符数组(char[])。
append值时,如果所需长度大于分配长度,新建数组长度为(2倍+2),如果所需长度大于(2倍+2),则使用所需长度大小,否则,使用(2倍+2)长度,默认长度为16,有参构造=16+参数长度
StringBuffer是线程安全的
效率上由快到慢:StringBuilder > StringBuffer > String
String为什么是final(不可变)的?
final+private保证了其不可修改性
- 不可变性保证了线程安全
- 不可变后避免了深拷贝,将String值放在字符串常量池(堆内)中,供其他方引用,提高效率,节约内存
hashcode、equals和== 的区别?
hashcode:
- 基本类型就是改值
- 引用类型是对象在内存地址的映射
equals:
- 在object中equals方法等效于==
- 在其他方法中,重写了equals方法,会判断值是否相等
==:
- 基本类型比对的是值
- 引用对象比对的是内存地址的映射
对于String,Integer对象,他们重写了equals方法,所以其equals方法可以判断值是否相等,而==只能判断引用是否相等
进程,线程,协程的区别?阻塞和非阻塞的区别?
进程
进程包含线程 进程是CPU分配资源的最小单位
线程
线程包含协程 线程是独立运行和独立调度的基本单位(CPU上真正执行的是线程) 线程间共享进程内资源 线程的调度切换比进程快
协程
协程是存在线程之上,通过异步IO处理执行多个协程的操作 协程的调度切换比线程快
阻塞与非阻塞
阻塞就是线程被cpu挂起,不执行线程逻辑 非阻塞就是线程不被cpu挂起,执行线程逻辑
并发和并行
并发是你执行一下,我执行一下,轮着执行 并行是一起执行
协程和线程的比较
- 协程运行在线程之上
- 线程执行由内核控制(内核态执行),控制线程切换消耗资源(抢先式),协程由程序执行(也就是在用户态执行)
- 协程比线程更加轻量
- 多核处理器的情况下,多个线程是可以并行的,但是运行的协程的函数却只有一个,其他协程都会被suspend(阻塞)。即协程是并发的,但不是并行的。
- 执行密集型IO操作,性能提高
- 在协程之间的切换不需要涉及任何系统调用或任何阻塞调用
IO
-
多路复用IO
执行A事件,同时执行B事件,通过状态下发,获取A,B执行状态
-
信号驱动IO
-
异步IO
执行A事件,通过异步处理,当A事件处理完成不哦,通知主进程/线程
-
阻塞IO:
执行完A事件,在执行B事件
-
非阻塞IO
执行A事件,同时执行B事件,一直监听A,B的执行过程
转载自:https://juejin.cn/post/6972046491506442253