最后
对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!
最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。
对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。
为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。
以下是今天给大家分享的一些独家干货:
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
- 某个引用对象为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 wr = new WeakReference(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)
最后
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。这也是我这些年来养成的习惯,一定要学会把好的东西,归纳整理,然后系统的消化吸收,这样才能极大的提高学习效率和成长进阶。碎片、零散化的东西,我觉得最没有价值的。就好比你给我一张扑克牌,我只会觉得它是一张废纸,但如果你给我一副扑克牌,它便有了它的价值。这和我们收集资料就要收集那些系统化的,是一个道理。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
。
java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。
linux进程通信有几种
Linux中的进程间通信有哪些?解释Binder通信为什么高效?Binder通信有什么限制?
Linux中的进程间通信有如下几种:
- 信号(signal)
最后
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
当然我也为你们整理好了百度、阿里、腾讯、字节跳动等等互联网超级大厂的历年面试真题集锦。这也是我这些年来养成的习惯,一定要学会把好的东西,归纳整理,然后系统的消化吸收,这样才能极大的提高学习效率和成长进阶。碎片、零散化的东西,我觉得最没有价值的。就好比你给我一张扑克牌,我只会觉得它是一张废纸,但如果你给我一副扑克牌,它便有了它的价值。这和我们收集资料就要收集那些系统化的,是一个道理。
[外链图片转存中…(img-Q8x21Yhn-1715619759545)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!