1. Java基础相关笔记
- Java的char是两个字节,如何存utf-8字符?
面试官视角:这道题想考察什么? 1. 是否熟悉Java char和字符串(初级) 2. 是否了解字符的映射和存储细节(中级) 3. 是否能触类旁通,横向对比其它语言(高级) 题目结论: 初级:Java char不存utf-8的字节,而是utf-16,占两个字节;utf-8是1-6个字节存储;Unicode字符集(码点,人类认知层面的字符映射成整数,字符集不是编码) 中级:Java char中存储的是utf-16编码。Unicode通用字符集占两个字节,'中',占一个char两个字节,byte[] bytes = '中'.getBytes("utf-16")--> 结果为:fe ff 4e 2d(fe ff为字节序) 高级: 1. 令人迷惑的字符串长度:字符串长度 != 字符数(比如:表情emoji,一个字符数,占两个长度) 2. 触类旁通: 1). Java9中对拉丁(Latin)字符优化:例如若字符串中是ascii码字符,只需要7个byte一个字节表示存储,而使用utf-16存储明显是有压力的。Java9若发现整个字符串中只有ascii码字符,则会使用byte来存,不使用char存储,这样就会节省一半字符,此时字符串长度 也!= 字符数。 2). Java(String emoji = "emoji表情",长度为2) == Python(>=3.3,emoji = u"emoji表情",长度为1)
- Java String可以有多长?
面试官视角:这道题想考察什么? 1. 字符串有多长是指字符数还是字节数(初级) 2. 字符串有几种存在形式(中级) 3. 字符串的不同形式受到何种限制(高级) 题目结论: 初级:分为两种形式写在代码中的字面量和文件中的字符串,即存在于栈内存中和堆内存中 中级: 栈内存中:字面量中,受限于Java代码编译完成的字节码文件CONSTANT_Utf8_info结构中的u2 length。u2代表两个字节,即长度最大为16位二进制表示,因此最大长度为65535。CONSTANT_Utf8_info运行时,会被加载到Java虚拟机方法区的常量池中,若常量池很小,字符串太大,会出现异常,常量池一般不会连65535都无法存储。 高级: 1. Java编译器的bug:实战过程中发现输入65535个Latin字符,编译无法通过,而65534则可以。Java不会和c语言中一样,在字符串中加一个'\0'。Java编译器(Javac)中判断字符串长度时使用的是 <65535 号,而非 <=65535,因此65535无法编译通过。kotlin中是没有问题的 2. String中若为中文字符,比如String longString = "烫烫烫...烫烫烫"; 烫占3个字节,理论上最长为 65535/3 个。实战输入65535/3个烫,发现是可以编译通过的,原因:Java编译器对于中文字符这样,需要Utf8编码这种,没办法在编译时直接知道到底需要占用多少字节,只能先通过utf-8编码,然后再来查看占多少字节。此写法判断长度时使用 >65535 ,因此此处是正确的。 3. 总结:Latin字符,受Javac限制,最多65534个;非Latin字符最终对应字节个数差距较大,最多字节数为65535个;若运行时方法区设置较小(比如:嵌入式设备),也会受方法区大小限制。 4. 堆内存中:new String(bytes); 受String内部value[] 数组,此数组受虚拟机指令 newarray [int],因此数组理论上最大个数为 Integer.MAX_VALUE。有一些虚拟机可能保留一下头信息,因此实际上最大个数小于 Integer.MAX_VALUE。即堆中String理论上最长为Integer.MAX_VALUE,实际受虚拟机限制小于Integer.MAX_VALUE,并且若堆内存较小也受堆内存大小限制。
- Java匿名内部类有哪些限制?
面试官视角:这道题想考察什么? 1. 考察匿名内部类的概念和用法(初级) 2. 考察语言规范以及语言的横向对比等(中级) 3. 作为考察内存泄漏的切入点(高级) 题目结论: 初级:没有人类认知意义上的名字;匿名内部类的实际名字:包名+外部类+$N,N是匿名内部类的顺序(即在外部类中所有匿名内部类从上到下排第几) 中级: 匿名内部类的继承结构:匿名内部类必然有其父类或者父接口,在初始化匿名内部类new InnerClass(){ ... },InnerClass类实际上就是此匿名内部类的父类 匿名内部类只能继承一个父类或者实现一个接口,不能同时即继承父类由实现接口;kotlin中是可以的 高级: 匿名内部类的构造方法:编译器生成,匿名内部类可能会有外部类的引用,可能会导致内存泄漏。 匿名内部类实际参数列表: 1. 外部类实例(定义在非静态域内) 2. 父类的外部对象(父类非静态,即父类是定义在一个类中,外部对象是指父类的外部类实例) 3. 父类的构造方法参数(父类若由构造方法且参数列表不为空) 4. 外部捕获的变量(方法体内有引用外部的final变量) 匿名内部类参数列表所知: 1. 父类定义在是非静态作用域内(即父类是定义在一个类中),会引用父类的外部类实例 2. 如果定义在非静态作用域内(非静态方法内),会引用外部类实例(非静态方法内所在类的实例) 3. 只能捕获外部作用域的final变量 4. Java8:创建时只有单一方法的接口可以用Lambda转换(SAM类型),只能是接口且接口中只有一个方法
- Java 方法分派
概念引入1:Java的虚方法(需要注意虚方法和抽象方法并不是同一个概念) 虚方法出现在Java的多态特性中,父类与子类之间的多态性,对父类的函数进行重新定义。 Java虚方法你可以理解为java里所有被overriding的方法都是virtual的,所有重写(覆写)的方法都是override的。 在JVM字节码执行引擎中,方法调用会使用invokevirtual字节码指令来调用所有的虚方法。 概念引入2:方法调用 java是一种半编译半解释型语言,也就是class文件会被解释成机器码,而方法调用也会被解释成具体的方法调用指令,大致可以分为以下五类指令: invokestatic:调用静态方法; invokespecial:调用实例构造方法,私有方法和父类方法; invokevirtual:调用虚方法(普通实例方法调用是在运行时根据对象类型进行分派的,相当于C++中所说的虚方法); invokeinterface:调用接口方法,在运行时再确定一个实现此接口的对象; invokedynamic:在运行时动态解析出调用点限定符所引用的方法之后,调用该方法; 注意:invokedynamic 指令是jdk1.7才加入的,但是在jdk1.7中并没有开始使用。在jdk1.8中才开始大量使用,主要就是我们大量用的 lambda 表达式。 方法绑定: 静态绑定:如果在编译时期解析,那么指令指向的方法就是静态绑定,也就是private,final,static和构造方法,也就是上面的invokestatic和invokespecial指令,这些在编译器已经确定具体指向的方法。 动态绑定:而接口和虚方法调用无法找到真正需要调用的方法,因为它可能是定义在子类中的方法,所以这种在运行时期才能明确类型的方法我们成为动态绑定。 C++中,如果不将函数xxx声明为virtual,那么无论子类是否自己定义了和父类同名xxx的方法,父类Base中调用的xxx方法永远都是父类中的(静态绑定)。 当为xxx函数添加virtual声明,使其成为一个虚函数时,此时Base类会产生和维护一个虚函数表。同理,派生的子类Sub也会有一个虚函数表,对虚函数的调用都是动态绑定的,与JAVA原理类似,都是使用虚函数在虚函数表中的索引偏移量来取得函数的实际地址。 概念引入3:虚分派(虚方法的分派) 概念2中5种方法调用指令最复杂的要属 invokevirtual 指令,它涉及到了多态的特性,使用 virtual dispatch 做方法调用。 virtual dispatch 机制会首先从 receiver(被调用方法的真实对象)的类的实现中查找对应的方法,如果没找到,则去父类查找,直到找到函数并实现调用,而不是依赖于引用的类型。 面试官视角:这道题想考察什么? 1. 多态、虚方法表的认识(初级) 2. 对编译和运行时的理解和认识(中级) 3. 对Java语言规范和运行机制的深入认识(高级) 4. 横向对比各类语言的能力(高级) 1. Groovy,Gradle DSL 5.0以前唯一正式语言 2. C++,Native程序开发必备 题目剖析:怎样理解Java 的方法分派? 1. 就是确定调用谁的、哪个方法 2. 针对方法重载的情况进行分析 3. 针对方法覆写的情况进行分析 题目示例: SuperClass super = new SubClass(); printHello(super); public static void printHello(SuperClass super) { System.out.println("Hello " + super.getName()); } public static void printHello(SubClass sub) { System.out.println("Hello " + sub.getName()); } 题目结论: 初级:方法的输出?输出的子类重写的方法,取决于运行时的实际类型 中级:方法的调用?Java调用编译时期声明的类型,即:printHello(SuperClass super) 高级: Java方法分派: 静态分派(方法重载分派):编译器确定;依据调用者的声明类型和方法参数类型 动态分派(方法重写分派):运行时确定;依据调用者的实际类型分派 Groovy语言:方法输出同Java;方法调用根据实际类型调用,即:printHello(SubClass sub) C++语言: C++题目示例: // 方法输出 SuperClass super = SubClass(); // 分配于栈内存中 cout << super.getName() <<endl; SuperClass *superClass = new SubClass(); // 分配于堆内存中 cout << superClass->getName() <<endl; delete(superClass); // 方法调用 void printHello(SuperClass super) { cout << super.getName() <<endl; } void printHello(SubClass sub) { cout << sub.getName() <<endl; } void printHello(SuperClass* superClass) { cout << superClass.getName() <<endl; } void printHello(SubClass* subClass) { cout << subClass.getName() <<endl; } printHello(super); // 1 printHello(*superClass); // 2 printHello(&super); // 3 printHello(superClass); // 4 1. 方法的输出: 若父类函数没有添加virtual声明,即没有声明为虚方法,则C++中均使用父类方法 若父类函数添加virtual了声明,则又分两种情况: 1. 栈内存中(直接声明):由于多余部分被舍弃,因此输出的还是父类方法 2. 堆内存中(new出来的):堆内存中,同Java,因此同Java,输出的是子类的方法 注:C++中对象new出来和直接声明是有区别的,new出来的对象是直接使用堆空间,而直接声明(局部)一个对象是放在栈中。 new出来的对象的生命周期是具有全局性,譬如在一个函数块里new一个对象,可以将该对象的指针返回回去,该对象依旧存在,因此需要delete销毁。new对象指针用途广泛,比如作为函数返回值、函数参数等。 直接声明是根据声明的类型分配内存的,即SuperClass super = SubClass(); 是根据SuperClass分配内存的。而SubClass占用内存>SuperClass占用内存,因此若直接声明这种,会将SubClass中多余的部分去掉,即相当于分配的就是SuperClass对象。 2. 方法的调用(getName已是虚方法): 1. 栈内存中(直接声明): 1. printHello 1 :调用的printHello(SuperClass super),由于已经被剪裁(舍弃) 过啦,输出的是父类方法 2. printHello 3 :由于传入&super,即super的地址(即指针),而super类型为SuperClass,因此指针类型也是SuperClass,最终调用printHello(SuperClass* superClass)。由于已经被剪裁(舍弃) 过啦,输出的是父类方法。 2. 堆内存中(new出来的): 1. printHello 2 :由于*superClass 已经将指针指向的值取出来了,而且指针类型为SuperClass,所以此值也被剪裁过啦,最终调用的printHello(SuperClass super)。 2. printHello 4 :传入一个SuperClass指针,因此调用的是printHello(SuperClass* superClass)。而superClass指针指向的真实对象是SubClass,而getName前提已经是虚方法,因此最终输出的是子类SubClass的getName方法。
- Java 泛型的实现机制是怎样的?
概念引入1:方法签名(在Java的世界中, 方法名称+参数列表,就是方法签名) 1. 在Java中,函数签名包括函数名,参数的数量、类型和顺序。关于方法签名定义的注意点:方法签名中的定义中不包括方法的返回值、方法的访问权限。 2. 谈到方法签名,就要谈谈重写,Override,重写方法,就是方法签名完全一致的情况,子类的方法签名必须与父类的一模一样。 3. Overload方法重载(overloading) 是在一个类里面,方法名字相同,而参数不同。 4. 获取方法签名: javap -s 包名.类名 概念引入2:类型擦除 泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。 原始类型(类型擦除后保留的原始类型):就是擦除了泛型信息,最后在字节码中的类型变量的真正类型。在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T> 则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String> 则类型参数就被替换成类型上限String。 类型擦除示例: List<String> ll = new ArrayList<>(); List<Integer> kk = new ArrayList<>(); System.out.println(ll.getClass());//输出:class java.util.ArrayList System.out.println(kk.getClass());//输出:class java.util.ArrayList System.out.println(ll.getClass() == kk.getClass());//输出:true 类型擦除引起的问题及解决方法: 1. 先检查,再编译以及编译的对象和引用传递问题 一问:既然说类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList<String> 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢? 一答:Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,最后进行编译。 二问:这个类型检查是针对谁的呢?我们先看看参数化类型和原始类型的兼容(以 ArrayList举例子): 以前的写法:ArrayList list = new ArrayList(); 现在的写法:ArrayList<String> list = new ArrayList<String>(); 如果是与以前的代码兼容,各种引用传值之间,必然会出现如下的情况: ArrayList<String> list1 = new ArrayList(); //第一种情况 ArrayList list2 = new ArrayList<String>(); //第二种情况 这样是没有错误的,不过会有个编译时警告。不过在第一种情况,可以实现与完全使用泛型参数一样的效果,第二种则没有效果。 因为类型检查就是编译时完成的,new ArrayList()只是在内存中开辟了一个存储空间,可以存储任何类型对象,而真正设计类型检查的是它的引用,因为我们是使用它引用list1来调用它的方法,比如说调用add方法,所以list1引用能完成泛型类型的检查。而引用list2没有使用泛型,所以不行。 二答:类型检查针对的是引用,因此ArrayList<T>类型检测是根据引用来决定的。即ArrayList内存存储类型,是通过引用的<T>来决定 三问:泛型的引用传递问题(以 ArrayList举例子): ArrayList<String> list1 = new ArrayList<Object>(); //第一种,编译错误 相当于: ArrayList<Object> list1 = new ArrayList<Object>(); list1.add(new Object()); list1.add(new Object()); ArrayList<String> list2 = list1; //编译错误 ArrayList<Object> list2 = new ArrayList<String>(); //第二种,编译错误 相当于: ArrayList<String> list1 = new ArrayList<String>(); list1.add(new String()); list1.add(new String()); ArrayList<Object> list2 = list1; //编译错误 三答: 第一种:我们先假设它编译没错。那么当我们使用list2引用用get()方法取值的时候,返回的都是String类型的对象(上面提到了,类型检测是根据引用来决定的),可是它里面实际上已经被我们存放了Object类型的对象,这样就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(这也是泛型出现的原因,就是为了解决类型转换的问题,我们不能违背它的初衷)。 第二种:这样的情况比第一种情况好的多,最起码,在我们用list2取值的时候不会出现ClassCastException,因为是从String转换为Object。可是,这样做有什么意义呢,泛型出现的原因,就是为了解决类型转换的问题。我们使用了泛型,到头来,还是要自己强转,违背了泛型设计的初衷。所以java不允许这么干。再说,你如果又用list2往里面add()新的对象,那么到时候取得时候,我怎么知道我取出来的到底是String类型的,还是Object类型的呢? 2. 自动类型转换 问:因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢? 答:ArrayList.get()方法中 return (E) elementData[index]; 可以看到,在return之前,会根据泛型变量进行强转。假设泛型类型变量为Date,虽然泛型信息会被擦除掉,但是会将(E) elementData[index],编译为(Date)elementData[index]。所以我们不用自己进行强转。当存取一个泛型域时也会自动插入强制类型转换。 3. 泛型类型变量不能是基本数据类型:不能用类型参数替换基本类型。就比如,没有ArrayList<double>,只有ArrayList<Double>。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。 4. 运行时类型查询:因为类型擦除之后,ArrayList<String>只剩下原始类型,泛型信息String不存在了。那么,运行时进行类型查询( if( arrayList instanceof ArrayList<String>) )的时候是错误的。 5. 泛型在静态方法和静态类中的问题 1. 泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数。因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。 // 示例: public class Test2<T> { public static T one; //编译错误 public static T show(T one){ //编译错误 return null; } } 2. 注意区分下面的一种情况:因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T。 public class Test2<T> { public static <T >T show(T one){ //这是正确的 return null; } } 6. 类型擦除与多态的冲突和解决方法 解决方法:桥方法 具体参考:https://www.cnblogs.com/wuqinglong/p/9456193.html 中的3.3节 类型擦除优点: 1. 运行时内存负担小 2. 兼容性好 类型擦除带来的局限性:类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。 泛型中值得注意的地方: 1. 泛型类或者泛型方法中,不接受 8 种基本数据类型。 2. 对泛型方法的困惑:public <T> T test(T t){ return null; } 连续的两个 T,其实 <T> 是为了说明类型参数,是声明,而后面的不带尖括号的 T 是方法的返回值类型。 3. Java 不能创建具体类型的泛型数组:例如: List<Integer>[] li1 = new ArrayList<Integer>[]; 和 List<Boolean> li2 = new ArrayList<Boolean>[]; 无法在编译器中编译通过的。原因还是类型擦除带来的影响。 List<Integer> 和 List<Boolean> 在 Jvm 中等同于List<Object> ,所有的类型信息都被擦除,程序也无法分辨一个数组中的元素类型具体是 List<Integer>类型还是 List<Boolean> 类型。 解决方式:使用通配符 List<?>[] li3 = new ArrayList<?>[10]; li3[1] = new ArrayList<String>(); List<?> v = li3[1]; 借助于无限定通配符却可以,?代表未知类型,所以它涉及的操作都基本上与类型无关,因此 Jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素因为通配符原因,它只能读,不能写。比如,上面的 v 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作。 通配符 ?:除了用 <T> 表示泛型外,还有 <?> 这种形式。? 被称为通配符。通配符的出现是为了指定泛型中的类型范围。 示例:class Base{} class Sub extends Base{} 说明:Base 是 Sub 的父类,它们之间是继承关系,所以 Sub 的实例可以给一个 Base 引用赋值 List<Sub> lsub = new ArrayList<>(); List<Base> lbase = lsub; 说明:编译器不会让它通过的。Sub 是 Base 的子类,不代表 List<Sub> 和 List<Base> 有继承关系。 但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。 通配符有 3 种形式: 1. <?> 被称作无限定的通配符。 2. <? extends T> 被称作有上限的通配符。 3. <? super T> 被称作有下限的通配符。 通配符的未知性: public void testWildCards(Collection<?> collection){} testWildCards方法内的参数是被无限定通配符修饰的 Collection 对象,它隐略地表达了一个意图或者可以说是限定,那就是 testWidlCards() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法。即当 <?> 存在时,Collection 对象丧失了 add() 方法的功能,编译器不通过。 面试官视角:这道题想考察什么? 1. 对Java泛型使用是否仅停留在集合框架的使用(初级) 2. 对泛型的实现机制的认知和理解(中级) 3. 是否有足够的项目开发实战和"踩坑"经验(中级) 4. 对泛型(或模板)编程是否有深入的对比研究(高级) 5. 对常见的框架原理是否有过深入剖析(高级) 题目剖析: 1. 题目区分度非常大 2. 回答需要提及一下几点才能显得有亮点: 1. 类型擦除从编译角度的细节 2. 类型擦除对运行时的影响 3. 类型擦除对反射的影响 4. 对比类型不擦除的语言 5. 为什么Java选择类型擦除 3. 可从类型擦除的优劣着手分析回答 题目结论: 初级:泛型的类型擦除,可以缓解运行时内存压力(方法区只要保留一份List即可)以及其拥有更好的兼容性(1.5以前List就是List,ArrayList就是ArrayList,两者无关联) 中级: 泛型的缺陷1:基本类型无法作为泛型参数,因此基本类型为参就多了装箱和拆箱的过程。Google为规避基本类型的装箱和拆箱,自己为Android定制的SparseArray 泛型的缺陷2:泛型类型无法用作方法重载(因为类型擦除后,类型一致) public void printList(List<Integer> list){} public void printList(List<String> list){} 泛型的缺陷3:泛型类型无法当做真实类型使用(包括:运行时类型查询) if( arrayList instanceof ArrayList<String>){} // 非法类型判断,将<String>去掉则可以 知识迁移:Gson.fromJson(String json,Class<T> class) 为什么需要传入Class?需要根据参数传入的Class类型,最终强转后返回。即自动类型转换,需要告诉框架转换的类型 泛型的缺陷4:静态方法无法引用类泛型参数 泛型的缺陷5:类型强转的运行时开销,例如:ArrayList.get 高级: 附加的泛型类型签名信息(即泛型的真实类型,特定场景可通过反射获取): class SuperClass<T> {} class SubClass extends SuperClass<String> { public List<Map<String, Integer>> getValue() { return null; } } ParameterizedType parameterizedType = (ParameterizedType) SubClass.class.getGenericSuperclass(); // 获取方法返回值真实的泛型实参类型 ParameterizedType parameterizedTypeMethod = (ParameterizedType) SubClass.class.getMethod("getValue").getGenericReturnType(); Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); for (Type actualTypeArgument : actualTypeArguments) { System.out.println(actualTypeArgument); } // 输出:class java.lang.String 获取签名信息总结: 1. 如果是继承基类而来的泛型,就用 getGenericSuperclass() , 转型为 ParameterizedType 来获得实际类型 2. 如果是实现接口而来的泛型,就用 getGenericInterfaces() , 针对其中的元素转型为 ParameterizedType 来获得实际类型 3. 我们所说的 Java 泛型在字节码中会被擦除,并不总是擦除为 Object 类型,而是擦除到上限类型 4. 能否获得想要的类型可以在 IDE 中,或用 javap -v <your_class> 来查看泛型签名来找到线索 注意:混淆时要保留签名信息(Proguard文件中添加 -keepattributes Signature) 迁移:使用泛型签名的两个示例 1. Gson: Type collectionType = new TypeToken<Collection<Integer>>(){}.getType(); Collection<Integer> ints = gson.fromJson(json, collectionType); 2. Retrofit: @GET("users/{login}") Call<User> getUserCallBack(@Path("login")String login); 迁移:kotlin 反射的实现原理 使用Metadata注解,若使用kotlin的反射,并且需要混淆时,则注意:Proguard文件中添加(-keep class kotlin.Metadata{*;})
- Android的onActivityResult使用起来非常麻烦,为什么不设计成回调?
面试官视角:这道题想考察什么? 1. 是否熟悉onActivityResult的用法(初级) 2. 是否思考过用回调代替onActivityResult(中级) 3. 是否实践过用回调代替onActivityResult(中级) 4. 是否意识到回调的问题(高级) 5. 是否能给出匿名内部类对外部引用的解决方案(高级) 题目剖析: 1. onActivityResult为什么麻烦?(初级) 2. 为什么不使用回调?(中级) 题目结论: 初级:1). 代码处理逻辑分离,容易出现遗漏和不一致问题 2). 写法不够直观,且结果数据没有类型安全保障 3). 结果种类较多时,onActivityResult就会逐渐臃肿难以维护 第一步:ActivityA ————> ActivityB startActivtityResult(intent, requestCode); 代码处理逻辑分离,容易出现遗漏和不一致问题 第二步:ActivityA <———— ActivityB setResult(resultCode, intent); 写法不够直观,且结果数据(Bundle)没有类型安全保障 第三步:ActivityA @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {} 结果种类较多时,onActivityResult就会逐渐臃肿难以维护 中级: 假设尝试使用回调startActivtityResult(intent, new onResultCallBack() { ... });,第一步和第三步能够解决,ActivityB finish后回调ActivityA。能解决1和3步也不错,那为什么不使用回调呢? 总有例外状况,若ActivityB长时间处于前台,而ActivityA由于内存或者策略的原因被销毁啦。此时再由ActivityB回到ActivityA时,ActivityA已被销毁,而系统会再重新创建一个ActivityA' ,而ActivityA和ActivityA' 已经是完全不同的实例啦。 而回调中匿名内部类会引用外部实例的引用,所以若在onResultCallBack中更新或者处理逻辑,实际上此时已经不是ActivityA' 中的操作啦,而是被销毁的ActivityA中的操作,因此无法直接使用回调的方式。 不使用回调的原因: 1. onActivityResult确实麻烦 2. onResultCallBack确实可以简化代码的编写 3. 但Activity的销毁和恢复机制"不允许匿名内部类"出现 高级:基于注解处理器和Fragment回调实现--ActivityStarter Fragment中也有onActivityResult,并且Activity被回调回来时,Fragment中的onActivityResult也会被调用。因此现在也有些人在尝试使用Fragment中的onActivityResult替代Activity中的onActivityResult。 实现原理:添加空Fragment只ActivityA中,然后在ActivityB finish返回后使用Fragment的onActivityResult替代ActivityA中的onActivityResult。RxPermissions就是如此,而onActivityResult替代框架则为ActivityStarter或者RxActivityResult。 外部引用变换解决方案原理:在外部类引用变换时,通过反射替换匿名内部类以及回调逻辑中的的外部引用。 匿名内部类也是一个类,也可以使用反射去访问,只要通过反射拿到这个类型对应的引用,然后替换成新的引用。 如何拿到新的引用?Fragment的onActivityResult会被调用,而被调用时此Fragment已经是新的Fragment,因为Activity已经是新的啦,Fragment必然是新恢复创建的。因此可以通过新的Fragment拿到新的ActivityA,去替换新的引用。 例如:回调中处理 mTextView.setText(str); mTextView是成员变量,则可以通过上述方法完成。若改成textView.setText(str);,此时textView则是外部自由变量,无法通过外部类的引用就可以引用到的,而这种变量是直接通过构造方法直接捕获了外部的局部变量,直接拿过来一个副本。 因此此时需要找新的ActivityA中对应的textView,如何找?记录textView.getId(),通过id来找,当然可能会出现代码写的不是很好,而出现id重复,id重复会引发各种问题,比如在保存和恢复时以及此处查找出问题等,因此需要尽量避免。 现在假设id一定不会重复,我们需要在ActivityA销毁前保存textView的id,然后在新的ActivityA回调时,恢复成新的textView。 除了View之外,还有Fragment,Activity被销毁时,Fragment也会被销毁,恢复时随之恢复,所以类似的这些都需要被更新。Fragment也有类似的id,但是都不太可靠,比如contentid,但是若Fragment添加过多,还是无法确认是哪个Fragment,而tag也是可以重复的。 Fragment有一个字段叫mWho,此字段不是公开的,但却可标定Fragment的唯一身份,因此可以通过反射捕获F让个没头脑的该字段来去更新Fragment。
2. 线程安全(高并发)笔记
-
如何停止一个线程?
面试官视角:这道题想考察什么? 1. 是否对线程的用法有所了解(初级) 2. 是否对线程的stop方法有所了解(初级) 3. 是否对线程的stop过程中存在的问题有所认识(中级) 4. 是否熟悉interrupt中断的用法(中级) 5. 是否能解释清楚使用booblen标志位的好处(高级) 6. 是否知道interrupt底层的细节(高级) 7. 通过该题目能够转移话题到线程安全,并阐述无误(高级) 题目剖析及结论: 如何停止一个线程?stop/stop(throw),早已被废弃 初级:为什么不能简单的停止一个线程? 1. 多个线程相继访问同一块内存(CPU/文件),若某一个线程A暂停了,那么线程A仍持有内存锁,而其它线程就会被阻塞,若其它线程再持有线程A所必须的锁,那么就会造成死锁,因此线程的暂停/继续是不允许的。 2. 多个线程相继访问同一块内存(CPU/文件),若某一个线程A停止啦,那么线程A立即释放内存锁,立即释放内存锁会造成写数据写一半不能写了而且还无法释放资源,然后锁就会被另外的某一线程B所持有。等线程B过一会持有CPU时间片后,发现内存状态是异常的,是一个莫名其妙的值,这是因为线程A还没来的急清理,此时线程B也会发生异常。 总结:停止一个线程是非常危险的,因此现在无论是何种语言,基本都把线程停止方法给废弃啦。 中级:如何设计可以随时被中断而取消的任务线程? 线程往往和任务是强绑定的,任务执行完成,线程自然也就结束啦,线程虽然不能直接停止,但是任务是可以停止的。线程的运作模式应该是一个:协作的任务执行模式。 线程自己运行任务完成和线程直接stop对于我们来说都是停止,是没有区别的。但是对于程序来说是有区别的:直接stop,就没时间清理线程内部自己创建的资源,只能留给别的线程;线程自己运行完成,肯定有时间清理线程内部自己创建的资源。 设计可以随时被中断的任务线程应具备:1. 通知目标线程自行结束,而不是强制结束。 2. 目标线程应当具备处理中断的能力。 中断方式:1. Interrupt 2. boolean标志位 1. Interrupt 的原生支持 class InterruptableThread extends Thread { @Override public void run() { try{ // 不支持中断for循环 sleep(5000); for(int i = 0;i < 100000;i++){ System.out.println(i); } }catch(InterruptedException e) // 支持中断for循序 System.out.println("interrupted 清理资源"); for(int i = 0;i < 100000;i++){ if(interrupted()){break;} System.out.println(i); } } } } // 中断使用 Thread thread = new InterruptableThread(); thread.start(); // 开始 thread.interrupt(); // 中断 注1:有一些情况是不支持Interrupt的,比如:线程中死循环或者循环很多次,但是没有支持Interrupt中断方式,若支持则每次循环需要判断interrupted()是否中断。 注2:interrupted()和isInterrupted() interrupted():是静态方法,获取当前线程中断状态,并清空 当前运行的线程;中断状态调用后清空,重复调用后续返回false isInterrupted():是非静态方法,获取该线程的中断状态,不清空 调用的线程对象对应的线程;可重复调用,中断清空前一直返回true 高级: 2. boolean标志位 class InterruptableThread extends Thread { /* * isStopped存在线程间可见问题 * <p> * volatile让变量每次在使用的时候,都从主存中取。而不是从各个线程的“工作内存”。 * volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”,也就是说不保证线程执行的有序性。 * 也就是说,volatile变量对于每次使用,线程都能得到当前volatile变量的最新值。但是volatile变量并不保证并发的正确性。 */ volatile boolean isStopped = false; @Override public void run() { super.run(); for (int i = 0; i < 1000000; i++) { if (isStopped) { break; } System.out.println(i); } } } // 标志位使用 InterruptableThread thread = new InterruptableThread(); thread.start(); // ... thread.isStopped = true; 3. Interrupt和boolean标志位对比 interrupt boolean标志位 1. 系统方法 是 否 2. 使用JNI 是 否 3. 加锁 是 否 4. 触发方式 抛异常 布尔值判断,也可抛异常 5. 需要支持系统方法时用中断(功能性) 6. 其它情况用boolean标志位(性能较好)
-
如何写出线程安全的程序?
面试官视角:这道题想考察什么? 1. 是否对线程安全有初步了解(初级) 2. 是否对线程安全的产生原因有思考(中级) 3. 是否知道final、volatile关键字的作用(中级) 4. 是否清楚1.5之前Java DCL为什么又缺陷(中级) 5. 是否清楚的知道如何编写线程安全的程序(高级) 6. 是否对ThreadLocal的使用注意事项有认识(高级) 题目剖析: 1. 什么是线程安全?可变资源(内存)线程间共享 2. 如何实现线程安全? 1. 不共享资源(不共享就不会不安全) 1. 不涉及任何外部副本变量 2. ThreadLocal 使用建议:声明成全局静态final成员;避免存储大量对象;用完后及时移除对象 2. 共享不可变资源(不可变,就不存在线程安全问题):final变量 3. 共享可变资源 1. 保证可见性 1. 使用final关键字 2. 使用volatile关键字 3. 加锁,锁释放时会强制将缓存刷新到主内存。加锁是对跟你征用同一把锁的线程保证可见性 2. 保证操作原子性 -原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。比如 a = 1; -如何保证方法的原子性? 加锁,保证操作的互斥性,比如:synchronized同步代码块、lock锁机制 cas原子类工具:非公开,需要使用反射。CAS操作(原子操作,底层使用处理器的CAS指令),内部通过乐观锁(旧值和新值比较或版本号比较)实现 使用原子数值类型(如:AtomicInteger) 使用原子属性更新器:AtomicReferenceFieldUpdater 3. 禁止重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。 1. final可以禁止重排序,因为final修饰的成员,一定会在构造方法中初始化。某些虚拟机的实现或者某些CPU架构,若指令会重排序,则可能出现把非final的成员初始化方法构造方法之外,即构造方法都调用完成啦,非final成员还没有赋值。 2. volatile关键字修饰共享变量可以禁止重排序,以及更多保证线程间内存可见性。 DCL(双重检查锁定)单例时,注意要使用volatile。因为若不使用volatile,而代码发生重排序,则可能出现第一个线程初始化单例对象在锁里将单例引用赋值啦,但是其对象构造方法还没执行完,此时第二个线程获取到的单例是未初始化完全的,就会出问题。 题目结论: 初级:线程安全就是可变资源(内存)线程间共享。 中级:不共享资源和禁止重排序 高级:保证可见性和保证操作原子性
volatile是怎么保障内存可见性以及防止指令重排序的?blog.csdn.net/lsunwing/ar…
-
ConcurrentHashMap(简写:CHM)如何支持并发访问(线程安全)?
概念引入1: 1. ConcurrentHashMap的线程安全指的是,它的每个方法单独调用(即原子操作)都是线程安全的,但是代码总体的互斥性并不受控制。以代码为例: concurrentHashMap.put(KEY, concurrentHashMap.get(KEY) + 1); 实际上并不是原子操作,它包含了三步:1. concurrentHashMap.get() 2. +1操作 3. concurrentHashMap.put() 2. synchronized不管是用来修饰方法,还是修饰代码块,其本质都是锁定某一个对象。 修饰非静态方法时,锁上的是调用这个方法的对象,即this;修饰静态方法时,锁上的是当前类的class对象;修饰代码块时,锁上的是括号里的那个对象。 synchronized关键字判断当前对象是否是锁定的对象,本质上是通过 == 运算符来判断的。换句话说,可以采用任何一个常量,或者每个线程都共享的变量(比如:静态变量)。只要该变量与synchronized锁定的目标变量相同(==),就可以使synchronized生效。 概念引入2,在 Java 中,HashMap 是非线程安全的,如果想在多线程下安全的操作 map,主要有以下解决方法: 1. 使用Hashtable线程安全类(基本等同于直接synchronized(map){}); Hashtable 是一个线程安全的类,Hashtable 几乎所有的添加、删除、查询方法都加了synchronized同步锁! 相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差,所以 Hashtable 不推荐使用! 2. 使用Collections.synchronizedMap方法,对方法进行加同步锁; 使用方式:static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>()); 如果传入的是 HashMap 对象,其实也是对 HashMap 做的方法做了一层包装,里面使用对象锁来保证多线程场景下,操作安全,本质也是对 HashMap 进行全表锁!使用Collections.synchronizedMap方法,在竞争激烈的多线程环境下性能依然也非常差,所以不推荐使用! 3. 使用并发包中的ConcurrentHashMap类; ConcurrentHashMap 类,JDK 1.7之前所采用的正是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment。 JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用 CAS + synchronized 来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于 8 时,链表结构转为红黑二叉树)结构。ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍! 概念引入3:ConcurrentHashMap类 定位Segment 注:" 1. m << n,结果为m * 2^n;2. m & (2^n - 1),只要低n位一样则结果一样,都是2^n - 1。" ConcurrentHashMap 1.7之前使用分段锁Segment来保护不同段的数据,那么在插入和获取元素的时候,必须先通过哈希算法定位到Segment。 ConcurrentHashMap会首先使用Wang/Jenkins hash的变种算法对元素的hashCode(key)进行一次再哈希。之所以进行再哈希,其目的是为了减少哈希冲突,使元素能够均匀的分布在不同的Segment上,从而提高容器的存取效率。 假如哈希的质量差到极点,那么所有的元素都在一个Segment中,不仅存取元素缓慢,分段锁也会失去意义。若不通过再哈希而直接执行哈希计算。hash & 15,只要低位一样,无论高位是什么数,其哈希值总是一样为15。 hash >>> segmentShift) & segmentMask//定位Segment所使用的hash算法,高位代表Segment的下标 int index = hash & (tab.length - 1);// 定位HashEntry所使用的hash算法,tab.length为2^n-1,最小为16,符合注2中的说法,与低位的值,因此低位代表HashEntry的下标 默认情况下segmentShift为28,segmentMask为15,再哈希后的数(上两行中的hash)最大是32位二进制数据,向右无符号移动28位,意思是让高4位参与到hash运算中,即高位代表Segment的下标,而低位代表HashEntry的下标。 面试官视角:这道题想考察什么? 1. 是否熟练掌握线程安全的概念(高级) 2. 是否深入理解CHM的各项并发优化的原理(高级) 3. 是否掌握锁优化的方法(高级) 题目剖析: 1. 并发方法即考察线程安全问题 2. 回答 CHM 原理即可 3. 若对CHM的原理不了解 1. 分析下HashMap为什么不是线程安全的 2. 编写并发程序时你会怎么做,举例说明最佳 题目结论: CHM的并发优化历程: 0. 前提(概念3):ConcurrentHashMap类中hash(key)获取的值中,高位代表Segment的下标,而低位代表HashEntry的下标 1. JDK 1.5:分段锁,必要时加锁 hash(key)算法质量差,30000以下的Segment的下标基本都是15,分段锁失去意义。 2. JDK 1.6:分段锁,优化二次Hash算法 hash(key)算法优化后,使元素能够均匀的分布在不同的Segment上。 3. JDK 1.7:段懒加载,volatile & cas JDK 1.7之前Segment直接初始化,默认16个。JDK 1.7开始,需要哪个初始化哪个,因此1.7中实例化segment时为确保segment的可见性,大量使用了对数组的volatile(getObjectVolatile) 4. JDK 1.8:摒弃段,基于 HashMap 原理的并发实现 摒弃分段加锁,ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发。 CHM如何计数: 1. JDK 5~7基于段元素个数求和,二次不同就加锁 2. JDK 8引入CounterCell,本质上也是分段计数 CHM是弱一致性的 1. 添加元素后不一定马上能读到 2. 清空之后可能仍会有元素 3. 遍历之前的段元素的变化会读到(比如:现在遍历到14位,而15位发生变化,则15位的变化能读到) 4. 遍历之后的段元素读不到(比如:现在遍历到15位,而14位发生变化,则14位的变化不能读到) 5. 遍历时元素发生变化不抛异常 HashTable的问题: 大锁:对HashTable对象加锁 长锁:直接对方法加锁 读写锁共用:只有一把锁,从头锁到尾 CHM的解法: 小锁:分段锁(5~7),8为桶节点锁 短锁:先尝试获取,失败再加锁 分离读写锁:读失败再加锁(5~7),volatile读 CAS写(7~8) 如何进行锁优化? 1. 长锁不如短锁:尽可能只锁必要的部分 2. 大锁不如小锁:尽可能对加锁的对象进行拆分 3. 公锁不如私锁:尽可能将锁的逻辑放到私有代码中 4. 嵌套锁不如扁平锁:尽可能在代码设计时避免嵌套锁 5. 分离读写锁:尽可能将读锁和写锁分离 6. 粗化高频锁:尽可能合并处理频繁过短的锁(加锁需要开销,减少开销) 7. 消除无用锁:尽可能不加锁,或用volatile代替锁
HashMap 在多线程环境下操作可能会导致程序死循环:www.cnblogs.com/dxflqm/p/12…
ConcurrentHashMap 线程安全最牛逼的一篇文章:mp.weixin.qq.com/s/B1XboYOpG…
-
AtomicReference和AtomicReferenceFieldUpdater有何异同?
概念引入1:乐观锁与悲观锁 独占锁是一种悲观锁,synchronized就是一种独占锁,它假设最坏的情况,并且只有在确保其它线程不会造成干扰的情况下执行,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。 概念引入2:Java中的原子操作( atomic operations) 原子操作指的是在一步之内就完成而且不能被中断。原子操作在多线程环境中是线程安全的,无需考虑同步的问题。比如:int i=0; i++; 这种写法是线程不安全的 示例说明1:1. i = i++; 2. i = ++i; 1. i=i++;等同于: temp=i; (等号右边的i) i=i+1; (等号右边的i) i=temp; (等号左边的i) 2. i=++i;则等同于: i=i+1; temp=i; i=temp; 注:Java使用了中间缓存变量机制所以导致i=i++和i=++i不一致,而c语言中两者是一致的。 示例说明2:为什么long型赋值(long foo = 65465498L;)不是原子操作呢? 实际上Java会分两步写入这个long变量,先写32位,再写后32位。这样就线程不安全了。 若改成右边的就线程安全了:private volatile long foo; // 因为volatile内部已经做了synchronized。 概念引入3:CAS无锁算法--乐观锁 实现无锁(lock-free)的非阻塞算法有多种实现方法,其中 CAS(比较与交换,Compare and swap)是一种有名的无锁算法。CAS, CPU指令,在大多数处理器架构,包括IA32、Space中采用的都是CAS指令。 CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项 乐观锁 技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。 示例说明:AtomicLong通过CAS算法提供了非阻塞的原子性操作 AtomicLong 的incrementAndGet的代码使用CAS算法,但是CAS失败后还是通过无限循环的自旋锁不端的尝试: public final long incrementAndGet() { for (;;) { long current = get(); long next = current + 1; if (compareAndSet(current, next)) // compareAndSet(CAS算法),current为旧的预期值,next为新的需要修改的值。若当前值就是旧的预期值,AtomicLong就认为其它线程没有操作该值,而当前线程就可以修改成新的next值。 return next; } } 注:LongAdder--LongAdder类是JDK1.8新增的一个原子性操作类。 AtomicLong CAS失败后无限循环的自旋锁不断的尝试,在高并发下CAS性能会较低。 高并发下N多线程同时去操作一个变量会造成大量线程CAS失败,然后处于自旋状态,导致严重浪费CPU资源,降低了并发性。既然AtomicLong性能问题是由于过多线程同时去竞争同一个变量的更新而降低的,那么如果把一个变量分解为多个变量,让同样多的线程去竞争多个资源。 LongAdder则是内部维护一个Cells数组,每个Cell里面有一个初始值为0的long型变量,在同等并发量的情况下,争夺单个变量的线程会减少,这是变相的减少了争夺共享资源的并发量,另外多个线程在争夺同一个原子变量时候,如果失败并不是自旋CAS重试,而是尝试获取其他原子变量的锁,最后当获取当前值时候是把所有变量的值累加后再加上base的值返回的。 LongAdder应用场景:状态采集、统计计数等场景。在高并发的场景下,LongAdder有着明显更高的吞吐量,但是有着更高的空间复杂度。LongAdder有两大方法,add和sum。其更适合使用在多线程统计计数的场景下,在这个限定的场景下比AtomicLong要高效一些。 概念引入4:Atomic类 原理:Atomic类是通过自旋CAS操作volatile变量实现的。 为什么使用Atomic类? 在JDK1.6之前,synchroized是重量级锁,即操作被锁的变量前就对对象加锁,不管此对象会不会产生资源竞争。这属于悲观锁的一种实现方式。 而CAS会比较内存中对象和当前对象的值是否相同,相同的话才会更新内存中的值,不同的话便会返回失败。这是乐观锁的一中实现方式。这种方式就避免了直接使用内核状态的重量级锁。 但是在JDK1.6以后,synchronized进行了优化,引入了偏向锁,轻量级锁,其中也采用了CAS这种思想,效率有了很大的提升。 Atomic类的缺点: ABA问题:比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。 尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。因此前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。这允许一对变化的元素进行原子操作。 自旋问题:Atomic类会多次尝试CAS操作直至成功或失败,这个过程叫做自旋。通过自旋的过程我们可以看出自旋操作不会将线程挂起,从而避免了内核线程切换,但是自旋的过程也可以看做CPU死循环,会一直占用CPU资源。 这种情形在单CPU的机器上是不能容忍的,因此自旋一般都会有个次数限制,即超过这个次数后线程就会放弃时间片,等待下次机会。因此自旋操作在资源竞争不激烈的情况下确实能提高效率,但是在资源竞争特别激烈的场景中,CAS操作会的失败率就会大大提高,这时使用中重量级锁的效率可能会更高。当前,也可以使用LongAdder类来替换,它则采用了分段锁的思想来解决并发竞争的问题。 Jdk中相关原子操作类: 更新基本类型类:AtomicBoolean、AtomicInteger、AtomicLong 更新数组类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray 更新引用类型类:AtomicReference、AtomicMarkableReference、AtomicStampedReference 原子更新字段类:AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 面试官视角:这道题想考察什么? 1. 是否熟悉掌握原子操作的概念(中级) 2. 是否熟悉AR和ARFU这两个类的用法和原理(中级) 3. 是否对Java对象的内存占用有认识(高级) 4. 是否有较强的敏感度和深入探究的精神(高级) 题目结论: 使用方法: AtomicReference<String> atomicReference = new AtomicReference("HelloAtomic"); // 使用atomicReference atomicReference.compareAndSet("Hello", "World"); // Hello代表旧预期值,World代表新值 class SimpleValueHolder{ // 将被更新的字段必须是volatile修饰 volatile String value = "HelloAtomic"; // 参数一:包含该对象(valueUpdater)的类 参数二:将被更新的对象的类 参数三:将被更新的字段的名称 public static AtomicReferenceFieldUpdater<SimpleValueHolder, String> valueUpdater = AtomicReferenceFieldUpdater.newUpdater(SimpleValueHolder.class,String.class,"value"); } // 使用valueUpdater SimpleValueHolder holder = new SimpleValueHolder(); SimpleValueHolder.valueUpdater.compareAndSet(holder, "Hello", "Atomic"); // 参数一类似于反射 1. AR和ARFU的功能一致,原理相同,都是基于Unsafa的CAS操作 2. AR通常作为对象的成员使用(由于多占用,避免创建过多,内存问题),占16B(指针压缩)、24B(指针不压缩) AR内部本质上也有一个类似于private volatile T value;,因此AR比ARFU多了创建出一个对象value,32位则多出16字节(对象Header:12B,Field:4B),64位若启动指针压缩(-XX:-UseCompressedOops)则也是多16位否则多出24位(Field:8B)。 3. ARFU通常作为类的静态成员使用,对(参数二)实例成员进行修改(只需创建一个,然后对参数二类型的成员进行修改) 4. AR使用更友好,ARFU更适合类实例比较多的场景(即:线程安全中若存在较多实例发生改变时,假如:JDK中BufferedInputStream,使用ARFU来更新其内部的buf[]数组,因为BufferedInputStream在虚拟机运行过程中需要创建很多实例,因此使用ARFU可以比使用AR减少,每个AR内部的value实例,减少内存压力)。
Java中的Unsafe:www.jianshu.com/p/db8dce092…
LongAdder原理分析:blog.csdn.net/jiangtianji…
-
如何在Android中写出优雅的异步代码?
面试官视角:这道题想考察什么? 1. 是否熟练编写异步和同步代码(中级) 2. 是否熟悉回调地狱(中级) 3. 是否能够熟练使用RxJava(中级) 4. 是否对Kotlin协程有了解(高级) 5. 是否具备编写良好代码的意识和能力(高级) 题目剖析: 1. 什么是异步? 2. 如何定义优雅? 题目结论: 什么是异步? 同步:阻塞,通俗的讲,就是代码必定按照编写的顺序执行。 异步:通俗的讲,就是代码并不是按照编写的顺序执行。例如:线程(实现异步的手段之一)、Handler(post、postDelay、sendMessage...)、AsyncTask、回调(onClick) 异步的目的:提高CPU利用率(异步过程中程序若是CPU密集型的,异步或者高并发往往会降低CPU利用率,因为切换线程时会有一些开销;若是IO密集型的,则可以提供CPU利用率)、提升GUI程序的响应速度(GUI程序切换到IO线程时,往往是为了提高CPU利用率)、异步不一定快(若运行的是算法,多线程会让CPU利用率增高,若线程很多,切线程本身也会让CPU吃不消) 回调地狱:太多层的嵌套,整体代码形成一个倒的三角形,非常难以阅读。 RxJava(将异步逻辑扁平化):一个在 Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库。 RxJava基本用法:查看代码示例,具体可参考:com.android.baselibrary.RxJavaActivity RxJava异常处理:onErrorReturn RxJava取消处理:RxLifecycle或者AutoDispose RxJava代码示例: @SuppressLint("CheckResult") private void makeRxJava() { // 问:有一些异常无法映射成一个结果,在RxJava使用过程中无法捕获? // 答:则可以做一个全局的异常捕获,并且日志上报,但是此异常若为很验证的异常,则抛出 RxJavaPlugins.setErrorHandler(e -> { // OnErrorNotImplementedException此异常里真实的异常为e.getCause(),若不提取,则会包含大量无用信息 report(e instanceof OnErrorNotImplementedException ? e.getCause() : e); // 致命的错误抛出 Exceptions.throwIfFatal(e); }); String[] strings = new String[]{"111", "222", "333", "444", "555"}; Observable.fromArray(strings) .map(Integer::getInteger) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) //.compose(bindUntilEvent(ActivityEvent.DESTROY)) // RxLifecycle:防止内存泄漏,自动取消处理。需要继承自 RxActivity 或 RxFragment 等 .onErrorReturn(t -> throwableToResponse()) // AutoDispose:防止内存泄漏,自动取消处理。使用AutoDispose时需要当前Activity实现LifecycleOwner接口,而AppCompatActivity是默认实现了该接口 //.as(RxLifecycleUtils.bindLifecycle(this)) // as1 规范性,封装AutoDispose工具类 //.as(AutoDispose.autoDisposable(ViewScopeProvider.from(button))) // as2 监听View状态自动取消订阅,即根据button按钮与Window分离时onDetachedFromWindow取消处理 .as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))) // as3 根据声明周期取消处理 .subscribe(this::onNextResponse); } private void report(Throwable throwable) { // 日志上报处理 } private Integer throwableToResponse() { // 返回异常数字,自定义 return -1000; } private void onNextResponse(Integer integer) { } Kotlin协程(将异步逻辑同步化):参考下面Kotlin协程链接
- 并发和并行,异步与多线程区别:blog.csdn.net/woliuyunyic…
- Kotlin 的协程用力瞥一眼:juejin.cn/post/684490…
- Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了:juejin.cn/post/684490…
- 到底什么是「非阻塞式」挂起?协程真的更轻量级吗?juejin.cn/post/684490…
3. JNI 编程的细节笔记
- CPU架构适配需要注意哪些问题?
面试官视角:这道题想考察什么? 1. 是否有过Native开发经验(中级) 2. 是否关注过CPU架构适配(中级) 3. 是否有过含Native代码的SDK开发的经历(中级) 4. 是否针对CPU架构适配做过包体积优化(高级) 题目剖析: 1. Native开发才会关注CPU架构 2. 不同CPU架构之间的兼容性如何 3. so库太多如何优化Apk体积 4. SDK开发者应当提供哪些so库 题目结论: CPU架构的指令兼容性 1. mips64/mips(已废弃) 2. x86_64/x86 3. arm64-v8a/armeabi-v7a/armeabi(兼容性最好,可兼容2和3中的ALL) 兼容模式运行的一些问题: 1. 兼容模式运行的Native库无法获得最优性能 - x86_64/x86的电脑上运行arm的虚拟机会很慢 2. 兼容模式容易出现一些难以排查的内存问题 3. 系统优先加载对应架构目录下的so库(要么提供一套,要么不提供) 减小包体积优化:为App提供不同架构的Natvie库 1. 性能不敏感且无运行时异常,则可以只通过armeabi一套so库。 2. 结合目标用户群体提供合适的架构:目前现在大部分手机都是基于armeabi-v7a架构,因此可以只提供一套armeabi-v7a的so库。 3. 线上监控问题,针对性提供Native库:根据线上反馈,来决定该为哪个so库提供别的版本so库。不同版本的so库可以都放到一个目录下,然后通过判断当前设备属于哪个架构,加载不同的so库(代表作:微信)。 4. 动态加载Native库:非启动加载的库可云端下发 5. 优化so体积: 1. 默认隐藏所有符号,只公开必要的(减小符号表大小):-fvisibility=hidden 2. 禁用C++ Exception 和 RTTI:-fno-exceptions -fno-rtti 3. 不要使用iostream,应优先使用Android Log 4. 使用gc-sections去除无用代码 LOCAL_CFLAGS += -ffunction-sections -fdata-sections LOCAL_LDFLAGS += -Wl,--gc-sections 6. 构建时分包:不少应用商店已经支持按CPU架构分发安装包 splits{ abi{ enable true reset() include "armeabi-v7a","arm64-v8a","x86_64","x86" universalApk true } } SDK开发者需要注意什么? 1. 尽量不在Native层开发,降低问题跟踪维护成本 2. 尽量优化Native库的体积,降低开发者的使用成本 3. 必须提供完整的CPU架构依赖
- Java Native方法与Native函数是怎么绑定的?
面试官视角:这道题想考察什么? 1. 是否有过Native开发经验(中级) 2. 是否面对知识善于完善背后的原因(高级) 题目剖析: 1. 静态绑定:通过命名规则映射 2. 动态绑定:通过JNI函数注册 代码示例: package com.jni.study.native; public class NativeStudy{ public static native void callNativeStatic(); } // javah生成native方法的.h头文件 extern "C" JNIEXPORT void JNICALL Java_com_jni_study_native_NativeStudy_callNativeStatic(JNIEnv*,jclass) 题目结论: 一星:静态绑定:包名的.变成_ + 类名_ + 方法名(JNIEnv*,jclass/jobject) 二星:.h头文件说明 extern "C":告诉编译器,编译该Native函数时一定要按照C的规则保留这个名字,不能混淆这个函数的名字(比如:C++混编) JNIEXPORT:为编译器设置可见属性,强制在符号表中显示,优化so库时可以隐藏不需要公开的符号INVISIBLE,而此处不可隐藏需要设置为DEFAULT JNICALL:部分平台上需要(比如:mips、windows),告诉编译器函数调用的惯例是什么,比如:参数入栈以及返回清理等等 三星:动态绑定 1. 获取Class FindClass(className) 2. 注册方法 jboolean isJNIErr= env->RegisterNatives(class, methods, methodsLength) < 0; 3. 动态绑定可以在任何时刻触发 4. 动态绑定之前根据静态规则查找Native函数 5. 动态绑定可以在绑定后的任意时刻取消 6. 动态绑定和静态绑定对比: 动态绑定 静态绑定 Native函数名 无要求 按照固定规则编写且采用C的名称修饰规则 Native函数可见性 无要求 可见 动态更换 可以 不可以 调用性能 无需查找 有额外查找开销 开发体验 几乎无副作用 重构代码时较为繁琐 Android Studio支持 不能自动关联跳转 自动关联 JNI函数可跳转
- JNI如何实现数据传递?
面试官视角:这道题想考察什么? 1. 是否有过Native开发经验(中级) 2. 是否对JNI数据传递中的细节有认识(高级) 3. 是否能够合理的设计JNI的界限(高级) 题目剖析: 1. 传递什么数据? 2. 如何实现内存回收? 3. 性能如何? 4. 结合实例来分析更有效 题目结论: 一星:Bitmap Native层也有一个类对应 // 示例:Bitmap的compress方法,将Bitmap压缩到一个流中。 private final long mNativePtr; // 成员变量mNativePtr指针,对应Native层对应的Bitmap.h/cpp类 public boolean compress(CompressFormat format, int quality, OutputStream stream) { ... boolean result = nativeCompress(mNativePtr, format.nativeInt, quality, stream, new byte[WORKING_COMPRESS_STORAGE]); ... } - Native层nativeCompress方法通过传入的mNativePtr指针找到Native层对应的Bitmap.h/cpp类,然后进行压缩。 二星:字符串操作 0. 字符串操作都有一个参数 jboolean* isCopy,若返回为true则表示是从Java虚拟机内存中复制到Native内存中的。 1. GetStringUTFChars/ReleaseStringUTFChars * get返回值:const char* * 拷贝出Modified-UTF-8的字节流(字节码的格式也是Modified-UTF-8) * 若字符串里有\0字符,则\0编码成0xC080,不会影响C字符串结尾(C字符串结尾需要添加\0结束) 2. GetStringChars/ReleaseStringChars * get返回值:const jchar* * JNI函数自动处理字节序转换(Java字节序是大端,C的字节序是小端) 3. GetStringUTFRegion/GetStringRegion * 先在C层创建足够容量的空间 * 将字符串的某一部分复制到开辟好的空间 * 针对性复制,减少读取时效率更优 4. GetStringCritical/ReleaseStringCritical * 调用对中间会停止调用JVM GC * 调用对之间不可有其它JNI操作 * 调用对可嵌套 * 为何停止调用gc?防止该操作isCopy返回false,表示没有复制,而是该字符串指针指向Java虚拟机内存中, 若此时发生gc,则Java虚拟机中内存会进行整理,则该字符串指针可能变成野指针,很危险。 取决于虚拟机实现,多数总是倾向于拷贝即isCopy为true。而GetStringCritical得到原地址的可能性更高。 三星:对象数组传递 1. 访问Java对象,使用Java反射 2. 对象数组较大时,LocalRef使用有数量限制,比如:比较大的512个。使用完尽量释放env->DeleteLocalRef(obj);,若函数中使用的数量较少,也可以不释放,当方法调用完成会自动释放。 3. DirectBuffer:物理内存,Java代码写完,Native可直接使用,无需复制 Java示例: ByteBuffer buffer = ByteBuffer.allocateDifect(100); buffer.putInt(...); buffer.flip(); NativeCInf.useDifectBuffer(buffer, buffer.limit()); C示例: int * buffPtr = (int*)env->GetDirectBufferAddress(buffer); for(int i = 0; i < length / sizeof(int); i++) { LOGI("useArray:%d", buffPtr[i]); // 注意字节序 }
- 如何全局捕获Native异常?
面试官视角:这道题想考察什么? 1. 是否熟悉Linux的信号(中级) 2. 是否熟悉Native层任意位置获取jclass的方法(高级) 3. 是否熟悉底层线程与Java虚拟机的关系(高级) 4. 通过实现细节的考察,确认候选人的项目经验(高级) 题目剖析: 1. 如何捕获Native异常 2. 如何清理Navtive层和Java层的资源 3. 如何为问题的排查提供支持 题目结论:代码示例如下 JavaVM* javaVM; extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) { // Java虚拟机指针 javaVM = vm; } class JNIEnvHelper { public: JNIEnv *env; JNIEnvHelper() { needDetach = false; // 通过javaVM获取JNIEnv if(javaVM->GetEnv((void**)&env, JNI_VERSION) != JNI_OK) { // 若获取不到,则将Java虚拟机绑定到当前线程,重新获取 // 如果是Native线程,需要绑定到JVM才可以获取到JNIEnv if(javaVM->AttachCurrentThread(&env, NULL) == JNI_OK) { needDetach = true; } } } // 析构函数 ~JNIEnvHelper() { // 如果是Native线程,只有解绑时才会清理期间创建的JVM对象 if(needDetach) javaVM->DetachCurrentThread(); } private: bool needDetach; } // 程序开始时设置类加载器 static jobject classLoader; jint setUpClassLoader(JNIEnv *env) { jclass applicationClass = env->FindClass("com/jni/study/android/App"); jclass classClass = env->GetObjectClass(applicationClass); jmethodID getClassLoaderMethod = env->GetMethodID(classClass, "getClassLoader", "()Ljava/lang/ClassLoader"); // Native函数对Java虚拟机上对象的引用有什么注意事项? // jni函数获取到的引用都是本地引用(即出了引用的这个函数作用域就会被释放,若只是保存返回值则是无效的),因此需要保存则需要NewGlobalRef。 classLoader = env->NewGlobalRef(env->CallObjectMethod(applicationClass, getClassLoaderMethod)); return classLoader == NULL ? JNI_ERR : JNI_OK; } // 捕获Native异常 static struct sigaction old_signalhandlers[NSIG]; void setUpGlobalSignalHandler() { // "ThrowJNI----异常捕获"; struct sigaction handler; memset(&handler, 0, sizeof(struct sigaction)); handler.sa_sigaction = android_signal_handler; handler.sa_flags = SA_RESETHAND; #define CATCHSIG(X) sigaction(X, &handler, &old_signalhandlers[X]) CATCHSIG(SIGTRAP); CATCHSIG(SIGKILL); CATCHSIG(SIGKILL); CATCHSIG(SIGILL); CATCHSIG(SIGABRT); CATCHSIG(SIGBUS); CATCHSIG(SIGFPE); CATCHSIG(SIGSEGV); CATCHSIG(SIGSTKFLT); CATCHSIG(SIGPIPE); #undef CATCHSIG } // 传递异常到Java层 static void android_signal_handler(int signum, siginfo_t *info, void *reserved) { if(javaVM) { JNIEnvHelper jniEnvHelper; // package com.jni.study.native; jclass errorHandlerClass = findClass(jniEnvHelper.env, "com/jni/study/native/HandlerNativeError"); if(errorHandlerClass == NULL) { LOGE("Cannot get error handler class"); } else { jmethodID errorHandlerMethod = jniEnvHelper.env->GetStaticMethodID(errorHandlerClass, "nativeErrorCallback", "(I)V"); if(errorHandlerMethod == NULL) { LOGE("Cannot get error handler method"); } else { LOGE("Call java back to notify a native crash"); jniEnvHelper.env->CallStaticVoidMethod(errorHandlerClass, errorHandlerMethod, signum); } } } else { LOGE("Jni unloaded."); } old_signalhandlers[signum].sa_handler(signum); } jclass findClass(JNIEnv *env, const char* name) { if(env == NULL) return NULL; jclass classLoaderClass = env->GetObjectClass(classLoader); jmethodID loadClassMethod = env->GetMethodID(classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); jclass cls = static_cast<jclass>(env->CallObjectMethod(classLoader, classLoaderClass, env->NewStringUTF(name))); return cls; } // Java代码:package com.jni.study.native; public class HandlerNativeError { public static void nativeErrorCallback(int signal) { Log.e("NativeError", "[" + Thread.currentThread.getName + "] Signal:" + signal); } } // 捕获Native异常堆栈 /** * 1. 设置备用栈,防止SIGSEGV因栈溢出而出现堆栈被破坏 * 2. 创建独立线程专门用于堆栈收集并回调至Java层 * 3. 收集堆栈信息: * 1. [4.1.1——5.0) 使用内置libcorkscrew.so * 2. [5.0——至今) 使用自己编译的libunwind * 4. 通过线程关联Native异常对应的Java堆栈 /
- 只有C、C++可以编写JNI的Native库吗?
面试官视角:这道题想考察什么? 1. 是否对JNI函数绑定的原理有深入认识(高级) 2. 是否对底层开发有丰富的经验(高级) 题目剖析: 1. Native程序与Java关联的本质是什么? 2. 举例如何用其它语言编写符合JNI命名规则的符号 题目结论: 一星(按照14题):JNI对Native函数的要求 1. 静态绑定: 1. 符号表可见 2. 命名符合Java Native方法的 包名_类名_方法名 3. 符合名按照C语言的规则修饰 2. 动态绑定: 1. 函数本身无要求 2. JNI可识别入口函数如 JNI_OnLoad 进行注册即可 二星:可选的Native语言(理论上) * Golang * Rust * Kotlin Native * Scala Native * ... 三星:以Kotlin Native为例讲述如何开发