Android面试宝典2020-持续更新

Android面试宝典2020-持续更新

一、Java基础

1、java基本数据类型和引用类型

  1. 基本数据类型:
    整型:byte,short,int,long
    浮点型:float,double
    字符型:char
    布尔型:boolean
    其中,占一个字节的是byte,short和char占两个字节,int,float占四个字节,double和long占8个字节,boolean只有true和false。
  2. 引用数据类型:
    类、 接口、 数组、 枚举、 注解

例如,String类型就是引用类型,还有Double,Byte,Long,Float,Char,Boolean,Short(注意这里和基本类型相比首字母是大写),简单来说,所有的非基本数据类型都是引用数据类型。

  1. 基本数据类型和引用数据类型区别
    3.1 存储位置
    基本变量类型:在方法中定义的非全局基本数据类型变量的具体内容是存储在栈中的
    引用变量类型:引用数据类型变量,其具体内容都是存放在堆中的,而栈中存放的是其具体内容所在内存的地址
public class Main{
   public static void main(String[] args){
       //基本数据类型
       int i=1;
       double d=1.2;
       
       //引用数据类型
       String str="helloworld";
   }
}

在这里插入图片描述
3.2 传递方式
基本数据类型:在方法中定义的非全局基本数据类型变量,调用方法时作为参数是按数值传递的
引用数据类型:调用方法时作为参数是按引用传递的

2、object

equals和==的区别

==:基本类型比较值,引用类型比较地址。
equals:对两个对象的地址值进行比较(即比较引用是否相同)

public boolean equals(Object obj) {
    return (this == obj);
}

equals和hashcode的关系?

equals和hashcode之间的关系:
默认情况下,equals相等,hashcode必相等,hashcode相等,equals不是必相等。hashcode基于内存地址计算得出,可能会相等,虽然几率微乎其微。

3、static关键字

在类中,用static声明的成员变量为静态成员变量,也成为类变量。static修饰的成员方法为静态方法。类变量的生命周期和类相同,在整个应用程序执行期间都有效。

这里要强调一下:

static修饰的成员变量和方法,从属于

普通变量和方法从属于对象

静态方法不能调用非静态成员,编译会报错。

3.1.static关键字的用途

一句话描述就是:方便在没有创建对象的情况下进行调用(方法/变量)。

显然,被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。

3.2.static方法

由于静态方法不依赖于任何对象就可以直接访问,因此对于静态方法来说,是没有this的,因为不依附于任何对象,既然都没有对象,就谈不上this了,并且由于此特性,在静态方法中不能访问类的非静态成员变量和非静态方法,因为非静态成员变量和非静态方法都必须依赖于具体的对象才能被调用

虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法和静态成员变量。

特别说明:static方法是属于类的,非实例对象,在JVM加载类时,就已经存在内存中,不会被虚拟机GC回收掉,这样内存负荷会很大,但是非static方法会在运行完毕后被虚拟机GC掉,减轻内存压力。

3.3.static变量

静态变量被所有对象共享,在内存中只有一个副本,在类初次加载的时候才会初始化

非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响

3.4.静态变量和成员变量的区别

(1)所属不同:静态变量属于类,为类变量;成员变量属于对象,称为对象变量
(2)内存中位置不同:静态变量位于方法区中的静态区,成员变量存储于堆内存
(3)成员变量随着实例对象创建而存在,随着实例对象被回收而消失。静态变量随着类的加载而存在,随着类的消失而消失。
(4)调用不同:静态变量可以通过类名调用,也可以通过对象名调用,成员变量只能通过对象名调用

所以,成员变量可以称为对象的特有数据,静态变量称为对象的共享数据。

3.5. static代码块

public class CodeBlock{
     static{
        System.out.println("静态代码块");  
	}      
}

执行时机:静态代码块在类被加载的时候就运行了,而且只运行一次,并且优先于各种代码块以及构造函数。如果一个类中有多个静态代码块,就会按照书写的顺序执行。

静态代码块的作用:一般情况下,如果有些代码需要在项目启动的时候执行,这时就需要静态代码快,比如一个项目启动需要加载很多配置文件等资源,就可以都放在静态代码块中。

执行顺序:.静态代码块>构造代码块>构造函数

4、final关键字

可以声明成员变量、方法、类以及本地变量
final 成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误
final 变量是只读的
final 申明的方法不可以被子类的方法重写
final 类通常功能是完整的,不能被继承
final 变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销
final 关键字提高了性能,JVM 和 Java 应用都会缓存 final 变量,会对方法、变量及类进行优化
方法的内部类访问方法中的局部变量,必须用 final 修饰才能访问

5、String、StringBuffer、StringBuilder

String 为什么要设计成不可变的?

String是不可变的(修改String时,不会在原有的内存地址修改,而是重新指向一个新对象),String用final修饰,不可继承,String本质上是个final的char[]数组,所以char[]数组的内存地址不会被修改,而且String 也没有对外暴露修改char[]数组的方法。不可变性可以保证线程安全以及字符串串常量池的实现。

三者在执行速度方面的比较:StringBuilder > StringBuffer > String

String每次变化一个值就会开辟一个新的内存空间

StringBuilder:线程非安全的

StringBuffer:线程安全的

对于三者使用的总结:

1.如果要操作少量的数据用 String。

2.单线程操作字符串缓冲区下操作大量数据用 StringBuilder。

3.多线程操作字符串缓冲区下操作大量数据用 StringBuffer。

String 是 Java 语言非常基础和重要的类,提供了构造和管理字符串的各种基本逻辑。它是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。

StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。

StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

6、异常处理

Java异常结构中定义有Throwable类。 Exception和Error为其子类。

Exception是程序本身可以处理的异常,这种异常分两大类:运行时异常非运行时异常,程序中应当尽可能去处理这些异常。

运行时异常:都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等, 这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的, 程序应该从逻辑角度尽可能避免这类异常的发生。

非运行时异常:是需要显示用try-catch捕捉处理的异常,如IOException等

Error是程序无法处理的错误,比如OutOfMemoryError、StackOverflowError。这些异常发生时, Java虚拟机(JVM)一般会选择线程终止。

Java语言异常处理

  1. 对代码块用try…catch进行异常捕获处理;
	finally块没有处理异常的能力。处理异常的只能是catch块。
	
	不管有没有异常,finally 中的代码都会执行
	
	当 try、catch 中有 return 时,finally 中的代码依然会继续执行
  1. 在方法体外用throws进行抛出声明
	public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN
	{ 
	     //foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。
	}

3.在代码块用throw手动抛出一个异常对象

public void save(User user)
	{
	      if(user  == null) 
	          throw new IllegalArgumentException("User对象为空");
	      //......
	}

异常处理的两个基本原则:

  1. 尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常。
  2. 不要生吞异常。

7、谈谈对java多态的理解?(重要)

多态是指父类的某个方法被子类重写时,可以产生自己的功能行为,同一个操作作用于不同对象,可以有不同的解释,产生不同的执行结果。

多态的三个必要条件:

1.继承父类。

2.重写父类的方法。

3.父类的引用指向子类对象。

什么是多态

面向对象的三大特性:封装、继承、多态。从一定角度来看,封装和继承几乎都是为多态而准备的。这是我们最后一个概念,也是最重要的知识点。

多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)

实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

多态的作用:消除类型之间的耦合关系。

现实中,关于多态的例子不胜枚举。比方说按下 F1 键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在 Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。

多态的好处:

1.可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。

2.可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。

3.接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。

4.灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。

5.简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

Java中多态的实现方式:接口实现,继承父类进行方法重写,同一个类中进行方法重载。

8、抽象和接口(重要)

抽象类的意义?

为其子类提供一个公共的类型,封装子类中的重复内容,定义抽象方法,子类虽然有不同的实现,但是定义是一致的。

接口的意义?

规范、扩展、回调。

共同点

  1. 是上层的抽象层。
  2. 都不能被实例化。
  3. 都能包含抽象的方法,这些抽象的方法用于描述类具备的功能,但是不提供具体的实现。

区别

  1. 在抽象类中可以写非抽象的方法,从而避免在子类中重复书写他们,这样可以提高代码的复用性,这是抽象类的优势,接口中只能有抽象的方法。
  2. 多继承:一个类只能继承一个直接父类,这个父类可以是具体的类也可是抽象类,但是一个类可以实现多个接口。
  3. 抽象类可以有默认的方法实现,接口根本不存在方法的实现。
  4. 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明方法的实现。子类使用关键字implements来实现接口。它需要提供接口中所有声明方法的实现。
  5. 构造器:抽象类可以有构造器,接口不能有构造器。
  6. 和普通Java类的区别:除了你不能实例化抽象类之外,抽象类和普通Java类没有任何区别,接口是完全不同的类型。
  7. 访问修饰符:抽象方法可以有public、protected和default修饰符,接口方法默认是public abstract**** 。你不可以使用其它修饰符。接口中的所有属性默认为:public static final ****.
  8. main方法:抽象方法可以有main方法并且我们可以运行它,接口没有main方法,因此我们不能运行它。
  9. 速度:抽象类比接口速度要快,接口是稍微有点慢的,因为它需要时间去寻找在类中实现的方法。
  10. 添加新方法:如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。如果你往接口中添加方法,那么你必须改变实现该接口的类。

8、内部类,匿名内部类

内部类
非静态内部类没法在外部类的静态方法中实例化。
非静态内部类的方法可以直接访问外部类的所有数据,包括私有的数据。
在静态内部类中调用外部类成员,成员也要求用 static 修饰。
创建静态内部类的对象可以直接通过外部类调用静态内部类的构造器;创建非静态的内部类的对象必须先创建外部类的对象,通过外部类的对象调用内部类的构造器。
匿名内部类
匿名内部类不能定义任何静态成员、方法
匿名内部类中的方法不能是抽象的
匿名内部类必须实现接口或抽象父类的所有抽象方法
匿名内部类不能定义构造器
匿名内部类访问的外部类成员变量或成员方法必须用 final 修饰

9、集合框架

所有的集合都在 java.util 包下,java的集合几乎是从Collection 和 map这两个接口中派生出来的,而这两个接口又有一些子类(包括子接口和实现类)

9.1.集合和数组的区别:

在这里插入图片描述

9.2. 常用集合的分类:

标题

9.3. Collection集合的方法:

在这里插入图片描述

9.4. list和set的区别:

在这里插入图片描述
List:
在这里插入图片描述

(1)ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
(2)LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
(3)Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素

Set
HashSet
HashSet 是一个没有重复元素的集合。它是由HashMap实现的,不保证元素的顺序(这里所说的没有顺序是指:元素插入的顺序与输出的顺序不一致),而且HashSet允许使用null。但是只允许有一个null元素!

LinkedHashSet
LinkedHashSet继承自HashSet,其底层是基于LinkedHashMap来实现的,有序,非同步。(LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。)

TreeSet
TreeSet是一个有序集合,其底层是基于TreeMap实现的,非线程安全。TreeSet可以确保集合元素处于排序状态。

9.5. Map

Map用于保存具有映射关系的数据,Map里保存着两组数据:key和value,它们都可以使任何引用类型的数据,但key不能重复。所以通过指定的key就可以取出对应的value。

HashMap和HashTable的比较:
在这里插入图片描述
TreeMap
在这里插入图片描述
HashMap的特点是什么?HashMap的原理?(重点)
HashMap的特点:
1.基于Map接口,存放键值对。
2.允许key/value为空。
3.非多线程安全。
4.不保证有序,也不保证使用的过程中顺序不会改变。

简单来讲,核心是数组+链表/红黑树

HashMap的原理就是存键值对的时候:

通过键的Hash值确定数组的位置。
找到以后,如果该位置无节点,直接存放。
该位置有节点即位置发生冲突,遍历该节点以及后续的节点,比较key值,相等则覆盖。
没有就新增节点,默认使用链表,相连节点数超过8的时候,在jdk 1.8中会变成红黑树。
如果Hashmap中的数组使用情况超过一定比例,就会扩容,默认扩容两倍。

当然这是存入的过程,其他过程可以自行查阅。这里需要注意的是:

key的hash值计算过程是高16位不变,低16位和高16位取抑或,让更多位参与进来,可以有效的减少碰撞的发生。
初始数组容量为16,默认不超过的比例为0.75。

10、IO

IO流的分类:

按照“流”的数据流向,可以将其化分为:输入流输出流

按照“流”中处理数据的单位,可以将其区分为:字节流字符流。在java中,字节是占1个Byte,即8位;而字符是占2个Byte,即16位。而且,需要注意的是,java的字节是有符号类型,而字符是无符号类型!

字节流的抽象基类:
InputStream,OutputStream

字符流的抽象基类:
Reader,Writer

由这四个类派生出来的子类名称都是以其父类名作为子类名的后缀,如InputStream的子类FileInputStream,Reader的子类FileReader。

字节流和字符流的区别

字节流操作的基本单元是字节;字符流是Unicode字符
字节流不使用缓冲区,字符流使用缓冲区
字节流通常用于处理二进制数据,实际上它可以处理任意类型的数据,但它不支持直接写入或读取Unicode码元;字符流通常处理文本数据,它支持写入及读取Unicode码元。

IO和NIO(new IO)区别
IO面向流,NIO面向缓冲区
IO是阻塞的,NIO是非阻塞的
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道

11、反射(非常重要)

反射是框架设计的灵魂

11.1.思考:Java中创建对象大概有几种方式?

  1. 使用new关键字:这是我们最常见的也是最简单的创建对象的方式
  2. 使用Clone的方法:无论何时我们调用一个对象的clone方法,JVM就会创建一个新的对象,将前面的对象的内容全部拷贝进去
  3. 使用反序列化:当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象
  4. 反射

11.2.什么是反射?反射能干什么?反射的优缺点?

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

如果给定一个类名,就可以通过反射机制来获取类的所有信息,也可以动态的创建对象和编译。一般来说反射是用来做框架的,或者说可以做一些抽象度比较高的底层代码,常用的需求场景有:动态代理、工厂模式优化、Java JDBC数据库操作等。有一句很经典的话:反射是框架设计的灵魂

优点
灵活性高。因为反射属于动态编译,即只有到运行时才动态创建 、获取对象实例。

编译方式说明: 
1. 静态编译:在编译时确定类型、绑定对象。如常见的使用new关键字创建对象 
2. 动态编译:运行时确定类型、绑定对象。动态编译体现了Java的灵活性、多态特性、降低类之间的藕合性

缺点
执行效率低。 因为反射的操作主要通过JVM执行,所以时间成本 高于直接执行相同操作。

因为接口的通用性,Java的invoke方法是传object和object[]数组的。基本类型参数需要装箱和拆箱,产生大量额外的对象和内存开销,频繁促发GC。
编译器难以对动态调用的代码提前做优化,比如方法内联。
反射需要按名检索类和方法,有一定的时间开销。

11.3.反射的使用

在使用Java反射机制时,主要步骤包括:

  1. 获取 目标类型的Class对象
  2. 通过 Class 对象分别获取Constructor类对象、Method类对象 、 Field 类对象
  3. 通过 Constructor类对象、Method类对象 、Field类对象分别获取类的构造函数、方法、属性的具体信息,并进行后续操作
    //步骤1:
    //获取目标类型的class对象方式1:static method Class.forName 前提:已明确类的全路径名(最常用)
    Class cls = Class.forName("com.text.Person");
    //获取目标类型的class对象方式2:Object.getClass()  适合有对象示例的情况下
    Person person= new Person(); 
    Class<?> cls = person.getClass(); 
    //获取目标类型的class对象方式3:T.class语法   说明:仅适合在编译前就已经明确要操作的 Class
    Class<?> classType = Person.class; 
    
    //步骤2:
	// 以下方法都属于Class类的方法。
	<-- 1. 获取类的构造函数(传入构造函数的参数类型)->>
	// a. 获取指定的构造函数 (公共 / 继承)
	Constructor<T> getConstructor(Class<?>... parameterTypes)
	// b. 获取所有的构造函数(公共 / 继承) 
	Constructor<?>[] getConstructors(); 
	// c. 获取指定的构造函数 ( 不包括继承)
	Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 
	// d. 获取所有的构造函数( 不包括继承)
	Constructor<?>[] getDeclaredConstructors(); 
	// 最终都是获得一个Constructor类对象
	 
	// 特别注意:
	// 1. 不带 "Declared"的方法支持取出包括继承、公有(Public)、不包括有(Private)的构造函数
	// 2. 带 "Declared"的方法是支持取出包括公共(Public)、保护(Protected)、默认(包)访问和私有(Private)的构造方法,但不包括继承的构造函数
	// 下面同理
	 
	<--  2. 获取类的属性(传入属性名) -->
	// a. 获取指定的属性(公共 / 继承)
	Field getField(String name) ;
	// b. 获取所有的属性(公共 / 继承)
	Field[] getFields() ;
	// c. 获取指定的所有属性 (不包括继承)
	Field getDeclaredField(String name)// d. 获取所有的所有属性 (不包括继承)
	Field[] getDeclaredFields()// 最终都是获得一个Field类对象
	 
	<-- 3. 获取类的方法(传入方法名 & 参数类型)-->
	// a. 获取指定的方法(公共 / 继承)
	Method getMethod(String name, Class<?>... parameterTypes)// b. 获取所有的方法(公共 / 继承)
	Method[] getMethods()// c. 获取指定的方法 ( 不包括继承)
	Method getDeclaredMethod(String name, Class<?>... parameterTypes)// d. 获取所有的方法( 不包括继承)
	Method[] getDeclaredMethods()// 最终都是获得一个Method类对象
	 
	<-- 4. Class类的其他常用方法 -->
	getSuperclass(); 
	// 返回父类
	String getName(); 
	// 作用:返回完整的类名(含包名,如java.lang.String ) 
	Object newInstance(); 
	// 作用:快速地创建一个类的实例
	// 具体过程:调用默认构造器(若该类无默认构造器,则抛出异常 
	// 注:若需要为构造器提供参数需使用java.lang.reflect.Constructor中的newInstance()

	步骤3:通过 Constructor类对象、Method类对象、Field类对象分别获取类的构造函数、方法、属性的具体信息进行操作
	// 以下方法都分别属于`Constructor`类、Method类、Field类的方法。
	
	<-- 1. 通过Constructor 类对象获取类构造函数信息 -->
	String getName()// 获取构造器名
	Class getDeclaringClass()// 获取一个用于描述类中定义的构造器的Class对象
	int getModifiers()// 返回整型数值,用不同的位开关描述访问修饰符的使用状况
	Class[] getExceptionTypes()// 获取描述方法抛出的异常类型的Class对象数组
	Class[] getParameterTypes()// 获取一个用于描述参数类型的Class对象数组
	 
	<-- 2. 通过Field类对象获取类属性信息 -->
	String getName()// 返回属性的名称
	Class getDeclaringClass()// 获取属性类型的Class类型对象
	Class getType()// 获取属性类型的Class类型对象
	int getModifiers()// 返回整型数值,用不同的位开关描述访问修饰符的使用状况
	Object get(Object obj)// 返回指定对象上 此属性的值
	void set(Object obj, Object value) // 设置 指定对象上此属性的值为value
	 
	<-- 3. 通过Method 类对象获取类方法信息 -->
	String getName()// 获取方法名
	Class getDeclaringClass()// 获取方法的Class对象 
	int getModifiers()// 返回整型数值,用不同的位开关描述访问修饰符的使用状况
	Class[] getExceptionTypes()// 获取用于描述方法抛出的异常类型的Class对象数组
	Class[] getParameterTypes()// 获取一个用于描述参数类型的Class对象数组
	
	使用方法:method.invoke(Object obj,Object... args)
	
	<--额外:java.lang.reflect.Modifier类 -->
	// 作用:获取访问修饰符
	 
	static String toString(int modifiers)   
	// 获取对应modifiers位设置的修饰符的字符串表示
	 
	static boolean isXXX(int modifiers) 
	// 检测方法名中对应的修饰符在modifiers中的值

12、引用类型

  1. 强引用(FinalReference):在内存不足时不会被回收。平常用的最多的对象,如新创建的对象。
  2. 软引用(SoftReference):在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
  3. 弱引用(WeakReferenc):无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
  4. 虚引用(PhantomReference):虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收.

13、java泛型

1. 为什么要引入泛型?

在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者实际参数类型可以预知的情况下进行的。对于强制类型换错误的情况,编译器可能不提示错误,在运行的时候出现异常,这是一个安全隐患。

2. 说一下对泛型的理解

泛型的本质是参数化类型,在不创建新的类型的情况下,通过泛型指定不同的类型来控制形参具体限制的类型。也就是说在泛型的使用中,操作的数据类型被指定为一个参数,这种参数可以被用在类、接口和方法中,分别被称为泛型类、泛型接口和泛型方法。 Java语言引入泛型的好处是安全简单.
泛型是Java中的一种语法糖,能够在代码编写的时候起到类型检测的作用,但是虚拟机是不支持这些语法的。

泛型的优点:

类型安全,避免类型的强转。

提高了代码的可读性,不必要等到运行的时候才去强制转换。

3. 什么是类型擦除?

不管泛型的类型传入哪一种类型实参,对于Java来说,都会被当成同一类处理,在内存中也只占用一块空间。通俗一点来说,就是泛型只作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的信息擦除,也就是说,成功编译过后的class文件是不包含任何泛型信息的。

二、java进阶

2.1 java多线程(非常重要)

2.1.1. 线程

线程和进程的区别?

线程是CPU调度的最小单位,一个进程中可以包含多个线程,在Android中,一个进程通常是一个App,App中会有一个主线程,主线程可以用来操作界面元素,如果有耗时的操作,必须开启子线程执行,不然会出现ANR,除此以外,进程间的数据是独立的,线程间的数据可以共享。

java多线程实现方式主要有:

  1. 继承Thread
    优点 : 方便传参,可以在子类添加成员变量,通过方法设置参数或构造函数传参。
    缺点
    1.因为Java不支持多继承,所以继承了Thread类以后,就无法继承其他类。
    2.每次都要新建一个类,不支持通过线程池操作,创建和销毁线程对资源的开销比较大。
    3.从代码结构上讲,为了启动一个线程任务,都要创建一个类,耦合性太高。
    4.无法获取线程任务的返回结果。

    	Thread syncTask = new Thread() {
    	    @Override
    	    public void run() {
    	        // 执行耗时操作
    	    }
    	};
    	syncTask.start();//启动线程
    
  2. 实现Runnable
    优点 : 此方式可以继承其他类。也可以使用线程池管理,节约资源。创建线程代码的耦合性较低。推荐使用此种方式创建线程。
    缺点: 不方便传参,只能使用主线程中用final修饰的变量。其次是无法获取线程任务的返回结果。

    //写法1:集成Runnable接口定义任务类
    public class ThreadTask implements Runnable {
    
    	@Override
    	public void run() {
    			while(true) {
    			System.out.println(Thread.currentThread().getName()+" is running...");
    			try {
    	            Thread.sleep(1000);
    	        } catch (InterruptedException e) {
    	            e.printStackTrace();
    	        }
    		}
    	}
    }
    //在其他地方使用
    new Thread(new ThreadTask ()).start(); 
    
    //写法2:匿名内部类写法
    new Thread(new Runnable() {
    	@Override
        public void run() {
            //做操作    
                
        }
    }).start();
    
  3. 实现Callable
    此种方式创建线程底层源码也是使用实现Runnable接口的方式实现的,所以不是一种新的创建线程的方式,只是在实现Runnable接口方式创建线程的基础上,同时实现了Future接口,实现有返回值的创建线程。

    Runnable 与 Callable的区别:

    1. Runnable是在JDK1.0的时候提出的多线程的实现接口,而Callable是在JDK1.5之后提出的; 
    2. Runnable 接口之中只提供了一个run()方法,并且没有返回值;
    3. Callable接口提供有call(),可以有返回值;
    

    扩展:

    Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;
    当不调用此方法时,主线程不会阻塞
    
    
    public class CallableImpl implements Callable<String> {
     
        public CallableImpl(String acceptStr) {
            this.acceptStr = acceptStr;
        }
     
        private String acceptStr;
     
        @Override
        public String call() throws Exception {
            // 任务阻塞 1 秒
            Thread.sleep(1000);
            return this.acceptStr + " append some chars and return it!";
        }
     
     
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            Callable<String> callable = new CallableImpl("my callable test!");
            FutureTask<String> task = new FutureTask<>(callable);
            long beginTime = System.currentTimeMillis();
            // 创建线程
            new Thread(task).start();
            // 调用get()阻塞主线程,反之,线程不会阻塞
            String result = task.get();
            long endTime = System.currentTimeMillis();
            System.out.println("hello : " + result);
            System.out.println("cast : " + (endTime - beginTime) / 1000 + " second!");
        }
    }
     
    //执行结果
     
    hello : my callable test! append some chars and return it!
    cast : 1 second!
    

    总结

    根据Oracle提供的JAVA官方文档的说明,Java创建线程的方法只有两种方式,即继承Thread类和实现Runnable接口。其他所有创建线程的方式,底层都是使用这两种方式中的一种实现的,比如通过线程池、通过匿名类、通过lambda表达式、通过Callable接口等等,全是通过这两种方式中的一种实现的。所以我们在掌握线程创建的时候,必须要掌握的只有这两种,通过文章中优缺点的分析,这两种方法中,最为推荐的就是实现Runnable接口的方式去创建线程。

2.1.2. 线程的状态有哪些?

Java中定义线程的状态有6种,可以查看Thread类的State枚举:

public static enum State
  {
    NEW,  RUNNABLE,  BLOCKED,  WAITING,  TIMED_WAITING,  TERMINATED;
    
    private State() {}
  }
初始(NEW):新创建了一个线程对象,还没调用start方法;

运行(RUNNABLE):java线程中将就绪(ready)和运行中(running)统称为运行(RUNNABLE)。线程创建后调用了该对象的start方法,此时处于就绪状态,当获得CPU时间片后变为运行中状态;

阻塞(BLOCKED):表现线程阻塞于锁;

等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断);

超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定时间后自行返回;

终止(TERMINATED):表示该线程已经执行完毕。

状态详细说明

  1. 初始状态(NEW)
    实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。

  2. 就绪状态(RUNNABLE之READY)
    就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
    调用线程的start()方法,此线程进入就绪状态。
    当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
    当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
    锁池里的线程拿到对象锁后,进入就绪状态。

    运行中状态(RUNNABLE之RUNNING)
    线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。

  3. 阻塞状态(BLOCKED)
    阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。

  4. 等待(WAITING)
    处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。

  5. 超时等待(TIMED_WAITING)
    处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

  6. 终止状态(TERMINATED)
    当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
    在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

2.1.3. 线程的状态转换及控制

在这里插入图片描述

主要由这几个方法来控制:sleep、join、yield、wait、notify以及notifyAll。

wait() / notify() / notifyAll()

wait(),notify(),notifyAll() 是定义在Object类的实例方法,用于控制线程状态,三个方法都必须在synchronized 同步关键字所限定的作用域中调用(只能在同步控制方法或者同步控制块中使用),否则会报错 java.lang.IllegalMonitorStateException。

join() / sleep() / yield()

join()

如果线程A调用了线程B的join方法,线程A将被阻塞,等待线程B执行完毕后线程A才会被执行。这里需要注意一点的是,join方法必须在线程B的start方法调用之后调用才有意义。join方法的主要作用就是实现线程间的同步,它可以使线程之间的并行执行变为串行执行。

sleep()

当线程A调用了 sleep方法,则线程A将被阻塞,直到指定睡眠的时间到达后,线程A才会重新被唤起,进入就绪状态。


public class Test {
 
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
            try {
                Thread.sleep(1000);        // 阻塞当前线程1s
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

yield()  当线程A调用了yield方法,它可以暂时放弃处理器,但是线程A不会被阻塞,而是进入就绪状态。执行了yield方法的线程什么时候会继续运行由线程调度器来决定。


public class YieldThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
            // 主动放弃
            Thread.yield();
        }
    }
}

sleep方法和wait方法的区别是什么?

wait方法既释放cpu,又释放锁。 sleep方法只释放cpu,但是不释放锁。

sleep 方法是Thread类的一个静态方法,其作用是使运行中的线程暂时停止指定的毫秒数,从而该线程进入阻塞状态并让出处理器,将执行的机会让给其他线程。但是这个过程中监控状态始终保持,当sleep的时间到了之后线程会自动恢复。

wait 方法是Object类的方法,它是用来实现线程同步的。当调用某个对象的wait方法后,当前线程会被阻塞并释放同步锁,直到其他线程调用了该对象的 notify 方法或者 notifyAll 方法来唤醒该线程。所以 wait 方法和 notify(或notifyAll)应当成对出现以保证线程间的协调运行。

2.1.4. Java如何正确停止线程

注意

Java中线程的stop()、suspend()、resume()三个方法都已经被弃用,所以不再使用stop()方法停止线程。

如何停止线程

我们只能调用线程的interrupt()方法通知系统停止线程,并不能强制停止线程。线程能否停止,何时停止,取决于系统。

2.1.5 线程池(非常重要)

线程池的地位十分重要,基本上涉及到跨线程的框架都使用到了线程池,比如说OkHttp、RxJava、LiveData以及协程等。

与新建一个线程相比,线程池的特点?

节省开销: 线程池中的线程可以重复利用。
速度快:任务来了就能开始,省去创建线程的时间。
线程可控:线程数量可空和任务可控。
功能强大:可以定时和重复执行任务。

ExecutorService简介

通常来说我们说到线程池第一时间想到的就是它:ExecutorService,它是一个接口,其实如果要从真正意义上来说,它可以叫做线程池的服务,因为它提供了众多接口api来控制线程池中的线程,而真正意义上的线程池就是:ThreadPoolExecutor,它实现了ExecutorService接口,并封装了一系列的api使得它具有线程池的特性,其中包括工作队列、核心线程数、最大线程数等。

线程池(ThreadPoolExecutor)中的几个参数是什么意思?

  public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {//...}

参数解释如下(重要):

corePoolSize:核心线程数量,不会释放。

maximumPoolSize:允许使用的最大线程池数量,非核心线程数量,闲置时会释放。

keepAliveTime:闲置线程允许的最大闲置时间。它起作用必须在一个前提下,就是当线程池中的线程数量超过了corePoolSize时,它表示多余的空闲线程的存活时间,即:多余的空闲线程在超过keepAliveTime时间内没有任务的话则被销毁。而这个主要应用在缓存线程池中

unit:闲置时间的单位。

workQueue:阻塞队列,用来存储已经提交但未被执行的任务,不同的阻塞队列有不同的特性。

threadFactory:线程工厂,用来创建线程池中的线程,通常用默认的即可

handler:通常叫做拒绝策略,1、在线程池已经关闭的情况下 2、任务太多导致最大线程数和任务队列已经饱和,无法再接收新的任务 。在上面两种情况下,只要满足其中一种时,在使用execute()来提交新的任务时将会拒绝,而默认的拒绝策略是抛一个RejectedExecutionException异常

上面的参数理解起来都比较简单,不过workQueue这个任务队列却要再次说明一下,它是一个BlockingQueue<Runnable>对象,而泛型则限定它是用来存放Runnable对象的,刚刚上面讲了,不同的线程池它的任务队列实现肯定是不一样的,所以,保证不同线程池有着不同的功能的核心就是这个workQueue的实现了,细心的会发现在刚刚的用来创建线程池的工厂方法中,针对不同的线程池传入的workQueue也不一样,五种线程池分别用的是什么BlockingQueue:

1、newFixedThreadPool()—>LinkedBlockingQueue  无界的队列
2、newSingleThreadExecutor()—>LinkedBlockingQueue  无界的队列
3、newCachedThreadPool()—>SynchronousQueue  直接提交的队列
4、newScheduledThreadPool()—>DelayedWorkQueue  等待队列
5、newSingleThreadScheduledExecutor()—>DelayedWorkQueue  等待队列

实现了BlockingQueue接口的队列还有:ArrayBlockingQueue(有界的队列)、PriorityBlockingQueue(优先级队列)。这些队列的详细作用就不多介绍了。

线程池的种类有哪些:五种功能不一样的线程池

这样创建线程池的话,我们需要配置一堆东西,非常麻烦。所以,官方也不推荐使用这种方法来创建线程池,而是推荐使用Executors的工厂方法来创建线程池,Executors类是官方提供的一个工厂类,它里面封装好了众多功能不一样的线程池(但底层实现还是通过ThreadPoolExecutor),从而使得我们创建线程池非常的简便,主要提供了如下五种功能不一样的线程池:

newCachedThreadPool() :返回一个可以根据实际情况调整线程池中线程的数量的线程池。即该线程池中的线程数量不确定,是根据实际情况动态调整的。

newFixedThreadPool() :线程池只能存放指定数量的线程池,线程不会释放,可重复利用。

newSingleThreadExecutor() :单线程的线程池。即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按FIFO方式顺序执行任务队列中的任务。

newScheduledThreadPool() :可定时和重复执行的线程池。

newSingleThreadScheduledExecutor():同上。和上面的区别是该线程池大小为1,而上面的可以指定线程池的大小。

通过Executors的工厂方法来获取

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
ScheduledExecutorService singleThreadScheduledPool = Executors.newSingleThreadScheduledExecutor();

通过Executors的工厂方法来创建线程池极其简便,其实它的内部还是通过new ThreadPoolExecutor(…)的方式创建线程池的,我们看一下这些工厂方法的内部实现:

  public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

线程池ThreadPoolExecutor的使用

使用线程池,其中涉及到一个极其重要的方法,即:

execute(Runnable command) 

该方法意为执行给定的任务,该任务处理可能在新的线程、已入池的线程或者正调用的线程,这由ThreadPoolExecutor的实现决定。

五种线程池使用举例

  1. newFixedThreadPool 创建一个固定线程数量的线程池,示例为:
    创建了一个线程数为3的固定线程数量的线程池,同理该线程池支持的线程最大并发数也是3,而我模拟了10个任务让它处理,执行的情况则是首先执行前三个任务,后面7个则依次进入任务队列进行等待,执行完前三个任务后,再通过FIFO的方式从任务队列中取任务执行,直到最后任务都执行完毕。

    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 1; i <= 10; i++) {
            final int index = i;
            fixedThreadPool.execute(new Runnable() {
                 @Override
                 public void run() {
                     String threadName = Thread.currentThread().getName();
                     Log.v("zxy", "线程:"+threadName+",正在执行第" + index + "个任务");
                     try {
                            Thread.sleep(2000);
                     } catch (InterruptedException e) {
                            e.printStackTrace();
                     }
                 }
             });
         }
    

    在这里插入图片描述

  2. newSingleThreadExecutor
    创建一个只有一个线程的线程池,每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待线程处理完再依次处理任务队列中的任务,示例为:

     ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
            for (int i = 1; i <= 10; i++) {
                final int index = i;
                singleThreadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        String threadName = Thread.currentThread().getName();
                        Log.v("zxy", "线程:"+threadName+",正在执行第" + index + "个任务");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
    

    在这里插入图片描述
    其实我们通过newSingleThreadExecutor()和newFixedThreadPool()的方法发现,创建一个singleThreadExecutorPool实际上就是创建一个核心线程数和最大线程数都为1的fixedThreadPool。

  3. newCachedThreadPool
    创建一个可以根据实际情况调整线程池中线程的数量的线程池,为了体现该线程池可以自动根据实现情况进行线程的重用,而不是一味的创建新的线程去处理任务,我设置了每隔1s去提交一个新任务,这个新任务执行的时间也是动态变化的,示例为

     ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
            for (int i = 1; i <= 10; i++) {
                final int index = i;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                cachedThreadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        String threadName = Thread.currentThread().getName();
                        Log.v("zxy", "线程:" + threadName + ",正在执行第" + index + "个任务");
                        try {
                            long time = index * 500;
                            Thread.sleep(time);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
    

    在这里插入图片描述

  4. newScheduledThreadPool
    创建一个可以定时或者周期性执行任务的线程池,示例为:

        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
            //延迟2秒后执行该任务
            scheduledThreadPool.schedule(new Runnable() {
                @Override
                public void run() {
    
                }
            }, 2, TimeUnit.SECONDS);
            //延迟1秒后,每隔2秒执行一次该任务
            scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
    
                }
            }, 1, 2, TimeUnit.SECONDS);
    
  5. newSingleThreadScheduledExecutor
    创建一个可以定时或者周期性执行任务的线程池,该线程池的线程数为1,示例为

       ScheduledExecutorService singleThreadScheduledPool = Executors.newSingleThreadScheduledExecutor();
            //延迟1秒后,每隔2秒执行一次该任务
            singleThreadScheduledPool.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    Log.v("zxy", "线程:" + threadName + ",正在执行");
                }
            },1,2,TimeUnit.SECONDS);
    

    这个和上面的没什么太大区别,只不过是线程池内线程数量的不同,效果为:每隔2秒就会执行一次该任务
    在这里插入图片描述

自定义线程池ThreadPoolExecutor(自行了解)

线程池的停止
关于线程池的停止,ExecutorService为我们提供了两个方法:shutdown和shutdownNow,这两个方法各有不同,可以根据实际需求方便的运用,如下:

1、shutdown()  平滑的关闭线程池。(如果还有未执行完的任务,就等待它们执行完)。
2、shutdownNow() 简单粗暴的关闭线程池。(没有执行完的任务也直接关闭)。

线程池的工作流程
在这里插入图片描述

简单说:

任务来了,优先考虑核心线程。
核心线程满了,进入阻塞队列。
阻塞队列满了,考虑非核心线程。
非核心线程满了,再触发拒绝任务。

详细说明:

1 当一个任务通过submit或者execute方法提交到线程池的时候,如果当前池中线程数(包括闲置线程)小于coolPoolSize,则创建一个线程执行该任务。

2 如果当前线程池中线程数已经达到coolPoolSize,则将任务放入等待队列。

3 如果任务不能入队,说明等待队列已满,若当前池中线程数小于maximumPoolSize,则创建一个临时线程(非核心线程)执行该任务。

4 如果当前池中线程数已经等于maximumPoolSize,此时无法执行该任务,根据拒绝执行策略处理。

注意:当池中线程数大于coolPoolSize,超过keepAliveTime时间的闲置线程会被回收掉。回收的是非核心线程,核心线程一般是不会回收的。如果设置allowCoreThreadTimeOut(true),则核心线程在闲置keepAliveTime时间后也会被回收。

任务队列是一个阻塞队列,线程执行完任务后会去队列取任务来执行,如果队列为空,线程就会阻塞,直到取到任务。

2.1.6. java锁机制

在java中,解决同步问题,很多时候都会使用到synchronizedLock,这两者都是在多线程并发时候常使用的锁机制。在JDK1.6后,对synchronized进行了很多优化,如偏向锁、轻量级锁等,synchronized的性能已经与Reentrantlock大致相同,除非要使用Reentrantlock的一些高级功能(实现公平锁、中断锁等),一般推荐使用synchronized关键字来实现加锁机制

Synchronized 是Java 并发编程中很重要的关键字,另外一个很重要的是 volatile。Syncronized 一次只允许一个线程进入由他修饰的代码段,从而允许他们进行自我保护。进入由Synchronized 保护的代码区首先需要获取 Synchronized 这把锁,其他线程想要执行必须进行等待。Synchronized 锁住的代码区域执行完成后需要把锁归还,也就是释放锁,这样才能够让其他线程使用。

Lock 是 Java并发编程中很重要的一个接口,它要比 Synchronized 关键字更能直译"锁"的概念,Lock需要手动加锁和手动解锁,一般通过 lock.lock() 方法来进行加锁, 通过 lock.unlock() 方法进行解锁。与 Lock 关联密切的锁有 ReetrantLockReadWriteLock

ReetrantLock 实现了Lock接口,它是一个可重入锁,内部定义了公平锁与非公平锁。

ReadWriteLock 一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。ReentrantReadWirteLock实现了ReadWirteLock接口,并未实现Lock接口。

Synchronized 的使用

修饰一个方法:即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候。

实例方法:锁住的是该类的实例对象
静态方法:锁住的是该类的类对象。
public synchronized void goHOme(){
}
public static synchronized void goHOme(){
}

修饰代码块:表示只能有一个线程进入某个代码段

public void numDecrease(Object num){
  synchronized (num){
    number++;
  }
}

修饰一个类:作用的对象是这个类的所有对象,只要是这个类型的class不管有几个对象都会起作用。

class Person {
    public void method() {
    //锁住的是该类的类对象,如果换成this或其他object,则锁住的是该类的实例对象
       synchronized(Person.class) {
          // todo
       }
    }
 }

获取对象锁

synchronized(this|object) {}
修饰非静态方法

获取类锁

synchronized(类.class) {}
修饰静态方法

Lock 的使用

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;  
    boolean tryLock();  
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  
    void unlock();  
    Condition newCondition();
}

在这里插入图片描述
使用示例:

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}
Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     }
}else {
    //如果不能获取锁,则直接做其他事情
}
public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {
     //.....
    }
    finally {
        lock.unlock();
    }
}

一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

synchronized和Lock的区别?

主要区别:

synchronized是Java中的关键字,是Java的内置实现;Lock是Java中的接口。
synchronized遇到异常会释放锁;Lock需要在发生异常的时候调用成员方法Lock#unlock()方法。
synchronized是不可以中断的,Lock可中断。
synchronized不能去尝试获得锁,没有获得锁就会被阻塞; Lock可以去尝试获得锁,如果未获得可以尝试处理其他逻辑。
synchronized多线程效率不如Lock,不过Java在1.6以后已经对synchronized进行大量的优化,所以性能上来讲,其实差不了多少。

死锁

所谓死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。

死锁触发的四大条件?

互斥锁
请求与保持
不可剥夺
循环的请求与等待

简单死锁代码示例:

public class DeadLock {
    public static String obj1 = "obj1";
    public static String obj2 = "obj2";
    public static void main(String[] args){
        Thread a = new Thread(new Lock1());
        Thread b = new Thread(new Lock2());
        a.start();
        b.start();
    }    
}
class Lock1 implements Runnable{
    @Override
    public void run(){
        try{
            System.out.println("Lock1 running");
            while(true){
                synchronized(DeadLock.obj1){
                    System.out.println("Lock1 lock obj1");
                    Thread.sleep(3000);//获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2
                    synchronized(DeadLock.obj2){
                        System.out.println("Lock1 lock obj2");
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
class Lock2 implements Runnable{
    @Override
    public void run(){
        try{
            System.out.println("Lock2 running");
            while(true){
                synchronized(DeadLock.obj2){
                    System.out.println("Lock2 lock obj2");
                    Thread.sleep(3000);
                    synchronized(DeadLock.obj1){
                        System.out.println("Lock2 lock obj1");
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
可以看到,Lock1获取obj1,Lock2获取obj2,但是它们都没有办法再获取另外一个obj,因为它们都在等待对方先释放锁,这时就是死锁。

2.1.7. Java中Volatile关键字(重要)

基本概念:Java 内存模型中的可见性原子性有序性

原子性:(原子是世界上的最小单位,具有不可分割性)原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。比如 a = 1;

非原子性:

也就是整个过程中会出现线程调度器中断操作的现象

类似"a ++"这样的操作不具有原子性,因为它可能要经过以下两个步骤:

(1)取出 a 的值

(2)计算 a+1

如果有两个线程t1,t2都在进行这样的操作。t1在第一步做完之后还没来得及加1操作就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1开始执行第二步(此时t1中a的值可能还是旧值,不是一定的,只有线程t2中a的值没有及时更新到t1中才会出现)。这个时候就出现了错误,t2的操作相当于被忽略了

类似于a += 1这样的操作都不具有原子性。还有一种特殊情况,就是long跟double类型某些情况也不具有原子性

只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

举例:请分析以下哪些操作是原子性操作:

x = 10;        //语句1
y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4

其实只有语句1是原子性操作,其他三个语句都不是原子性操作。

语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

同样的,x++和 x = x+1包括2个操作:读取x的值,进行加1操作,写入新的值。

如何保证原子性?

synchronized、Lock、cas原子类工具

由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。其次cas原子类工具。

共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这个几个线程的共享变量

可见性:一个线程对共享变量值的修改,能够及时的被其它线程看到。也就是一个线程对共享变量修改的结果,另一个线程马上就能看到修改的值。

如何保证可见性?

volatile、synchronized、Lock

要想实现变量的一定可见,可以使用volatile。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。(其实还有final,但是它初始化后,值不可更改,所以一般不用它实现可见性)。

指令重排:CPU在执行代码时,其实并不一定会严格按照我们编写的顺序去执行,而是可能会考虑一些效率方面的原因,对那些先后顺序无关紧要的代码进行重新排序,这个操作就被称为指令重排。指令重排在单线程情况下没有什么影响,但是在多线程就不一定了。

有序性:程序执行的顺序按照代码先后的顺序执行。

如何保证有序性?

volatile、synchronized、Lock

volatile:

volatile原理:Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
在这里插入图片描述
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的CPU cache 中。

而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。

当一个变量定义为 volatile 之后,将具备两种特性:

1.保证此变量对所有的线程的可见性。当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主内存是不确定的,当其他线程去读取时,此时主内存中可能还是原来的旧值,因此无法保证可见性。

2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。

volatile 性能:
  volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
  
volatile为什么不能保证原子性?
简单的说,修改volatile变量分为四步:

1)读取volatile变量到local
2)修改变量值
3)local值写回
4)插入内存屏障,即lock指令,让其他线程可见

这样就很容易看出来,前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。原子性需要锁来保证。(或者可以理解为线程安全需要锁来保证)。这也就是为什么,volatile只用来保证变量可见性和有序性,但不保证原子性。

2.2 jvm

2.2.1. java内存模型

  1. Jvm内存区域(运行时数据区)划分:

    程序计数器:当前线程的字节码执行位置的指示器,线程私有。

    Java虚拟机栈:描述的Java方法执行的内存模型,每个方法在执行的同时会创建一个栈帧,存储着局部变量、操作数栈、动态链接和方法出口等,线程私有。

    本地方法栈:本地方法执行的内存模型,线程私有。

    Java堆:所有对象实例分配的区域。

    方法区:所有已经被虚拟机加载的类的信息、常量、静态变量和即时编辑器编译后的代码数据
    在这里插入图片描述

详细说明:

程序计数器
程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。

因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。
如果线程正在执行的是一个 Native 方法,这个计数器值则为空(Undefined)。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java 虚拟机栈
Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等消息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。

其中 64 位长度的 long 和 double 类型的数据会占用两个局部变量空间(Slot),其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

在 Java 虚拟机规范中,对这个区域规定了两种异常状态:

如果线程请求的栈深度大于虚拟机所允许的的深度,将抛出 StackOverflowError 异常。
如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

本地方法栈
本地方法栈(Native Method Stack) 与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(例如:Sun HotSpot虚拟机)直接就把虚拟机栈和本地方法栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

Java 堆
对于大多数应用来说,Java 堆(Java Heap) 是 Java 虚拟机所管理的的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,从内存回收的角度来看,由于现在收集器基本采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。

从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。

方法区
方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量池(Runtime Constant Pool) 是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时就会抛出 OutOfMemoryError 异常。

扩展String s1 = "abc"和String s2 = new String(“abc”)的区别,生成对象的情况

指向方法区:"abc"是常量,所以它会在方法区中分配内存,如果方法区已经给"abc"分配过内存,则s1会直接指向这块内存区域。

指向Java堆:new String(“abc”)是重新生成了一个Java实例,它会在Java堆中分配一块内存。

2.2.1. GC机制(重要)

GC 是 garbage collection 的缩写, 垃圾回收的意思. 也可以是 Garbage Collector, 也就是垃圾回收器.
Java的内存分配与回收全部由JVM垃圾回收进程自动完成。

面试题:“你能不能谈谈,java GC”

1、哪些对象可以被回收。
2、何时回收这些对象。
3、采用什么样的方式回收。

问题1:哪些对象可以被回收?

对象存活判断(如何判断对象可回收/垃圾搜集)
判断一个对象可以回收通常采用的算法是引用计数算法可达性分析算法。由于互相引用导致的计数不好判断,Java采用的可达性算法。

  1. 引用计数算法
    每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,效率很高,但是主流的JVM并没有选用这种算法来判定可回收对象,因为它有一个致命的缺陷,那就是它无法解决对象之间相互循环引用的的问题对于循环引用的对象它无法进行回收。例:

    public class Object {
        public Object instance;
        public static void main(String[] args) {
            // 1
            Object objectA = new Object();
            Object objectB = new Object();
            
            // 2
            objectA.instance = objectB;
            objectB.instance = objectA;
            
            // 3
            objectA = null;
            objectB = null;
        }
    

    程序启动后,objectA和objectB两个对象被创建并在堆中分配内存,这两个对象都相互持有对方的引用,除此之外,这两个对象再无任何其他引用,实际上这两个对象已经不可能再被访问(引用被置空,无法访问),但是它们因为相互引用着对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知GC收集器回收它们。

    实际上,当第1步执行时,两个对象的引用计数器值都为1;当第2步执行时,两个对象的引用计数器都为2;当第3步执行时,二者都清为空值,引用计数器值都变为1。根据引用计数算法的思想,值不为0的对象被认为是存活的,不会被回收;而事实上这两个对象已经不可能再被访问了,应该被回收。

  2. 可达性分析算法(根搜索算法)
    在主流的JVM实现中,都是通过可达性分析算法来判定对象是否存活的。可达性分析算法的基本思想是:通过一系列被称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots对象没有任何引用链相连,就认为GC Roots到这个对象是不可达的,判定此对象为不可用对象,可以被回收。
    在这里插入图片描述
    在上图中,objectA、objectB、objectC是可达的,不会被回收;objectD、objectE虽然有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

在Java中,可作为GC Roots的对象包括下面几种:

   1、java虚拟机栈中引用的对象;

   2、方法区中类静态属性引用的对象;

   3、方法区中常量引用的对象;

   4、本地方法栈中Native(JNI)方法引用的对象。

第一和第四种都是指的方法的本地变量表,第二种表达的意思比较清晰,第三种主要指的是声明为final的常量值。

问题3:采用什么样的方式回收

GC常用算法

可达性分析算法只是知道了哪些对象可以回收,不过垃圾收集显然还需要解决后两个问题,什么时候回收以及如何回收,在根搜索算法的基础上,现代虚拟机的实现当中,垃圾搜集的算法主要有三种,分别是标记-清除算法、复制算法、标记-整理算法,这三种算法都扩充了根搜索算法,不过它们理解起来还是非常好理解的。

标记 -清除算法

就是当程序运行期间,若可以使用的内存被耗尽的时候,GC线程就会被触发并将程序暂停,随后将依旧存活的对象标记一遍,最终再将堆中所有没被标记的对象全部清除掉,接下来便让程序恢复运行。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高(递归与全堆对象遍历);另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,内存的布局自然会乱七八糟。空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法
“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

标记-整理算法
复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法。

分代搜集算法(重要)
GC 分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

“分代搜集”算法,把Java堆分为新生代老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

新生代GC(minor GC):指发生在新生代的垃圾回收动作,因为Java对象大多都具备朝生夕灭的特点,所以minor GC发生得非常频繁,一般回收速度也比较块。

老年代GC(Major GC/Full GC):指发生在老年代的GC,它的速度会比minor GC慢很多。

问题2:何时回收这些对象?

回收的时机
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC),它们所针对的区域如下。普通GC(minor GC):只针对新生代区域的GC。全局GC(major GC or Full GC):针对年老代的GC,偶尔伴随对新生代的GC以及对永久代的GC。由于年老代与永久代相对来说GC效果不好,而且二者的内存使用增长速度也慢,因此一般情况下,需要经过好几次普通GC,才会触发一次全局GC。

内存模型与回收策略
在这里插入图片描述

Java 堆(Java Heap)是JVM所管理的内存中最大的一块,堆又是垃圾收集器管理的主要区域,Java 堆主要分为2个区域-新生代与老年代,其中年轻代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 2个区。

Eden 区
大多数情况下,对象会在新生代 Eden 区中进行分配,当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。 通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。

Survivor 区
Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进入 Old 区)。Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。

Old 区
老年代占据着2/3的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记——整理算法。

java垃圾收集器:(共7种,着重了解CMS和G1)

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:

初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)

G1收集器

与CMS收集器相比G1收集器有以下特点:

1、空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。

2、可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了。

使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region 的集合。

G1的新生代收集跟 ParNew 类似,当新生代占用达到一定比例的时候,开始出发收集。和 CMS 类似,G1 收集器收集老年代对象会有短暂停顿。

2.2.1. 类加载过程

类加载的过程?

  1. 加载:将类的全限定名转化为二进制流,再将二进制流转化为方法区中的类型信息,从而生成一个Class对象。

  2. 验证:对类的验证,包括格式、字节码、属性等。

  3. 准备:为类变量分配内存并设置初始值。

  4. 解析:将常量池的符号引用转化为直接引用。

  5. 初始化:执行类中定义的Java程序代码,包括类变量的赋值动作和构造函数的赋值。

  6. 使用

  7. 卸载

只有加载、验证、准备、初始化和卸载的这个五个阶段的顺序是确定的。

类加载的机制,以及为什么要这样设计?

类加载的机制是双亲委派模型。大部分Java程序需要使用的类加载器包括:

启动类加载器:由C++语言实现,负责加载Java中的核心类。
扩展类加载器:负责加载Java扩展的核心类之外的类。
应用程序类加载器:负责加载用户类路径上指定的类库

双亲委派模型如下:
在这里插入图片描述
双亲委派模型要求出了顶层的启动类加载器之外,其他的类加载器都有自己的父加载器,通过组合实现。

双亲委派模型的工作流程:
当一个类加载的任务来临的时候,先交给父类加载器完成,父类加载器交给父父类加载器完成,知道传递给启动类加载器,如果完成不了的情况下,再依次往下传递类加载的任务。

这样设计的原因:
双亲委派模型能够保证Java程序的稳定运行,不同层次的类加载器具有不同优先级,所有的对象的父类Object,无论哪一个类加载器加载,最后都会交给启动类加载器,保证安全。

三、Android基础

3.1.Android系统架构

在这里插入图片描述

  1. 应用层
  2. 应用框架层(Framwork)
  3. 系统运行库层
  4. Linux内核层

3.2.四大组件

1. Activity

  1. 生命周期
    在这里插入图片描述
    Activity A 启动另一个Activity B,回调如下:
    Activity A 的onPause() → Activity B的onCreate() → onStart() → onResume() → Activity A的onStop();如果B是透明主题又或则是个DialogActivity,则不会回调A的onStop;

  2. activity四种启动模式

    standard:标准模式,每次都会在活动栈中生成一个新的Activity实例。通常我们使用的活动都是标准模式。

    singleTop:栈顶复用,如果Activity实例已经存在栈顶,那么就不会在活动栈中创建新的实例。比较常见的场景就是给通知跳转的Activity设置,因为你肯定不想前台Activity已经是该Activity的情况下,点击通知,又给你再创建一个同样的Activity。

    singleTask:栈内复用,如果Activity实例在当前栈中已经存在,就会将当前Activity实例上面的其他Activity实例都移除栈。常见于跳转到主界面。

    singleInstance:单实例模式,创建一个新的任务栈,这个活动实例独自处在这个活动栈中。

2. Service

  1. 启动Service的2种方式

    第一种方式:Context.startService():
    service会一直无限期运行下去,只有外部调用了stopService()或stopSelf()方法时,该Service才会停止运行并销毁。多次startService不会重复执行onCreate回调,但每次都会执行onStartCommand回调。

    两种方式创建:
    a.继承Service类
    请务必在Service中开启线程来执行耗时操作,因为Service运行在主线程中

    b.继承IntentService类
    IntentService继承于Service,若Service不需要同时处理多个请求,那么使用IntentService将是最好选择:只需要重写onHandleIntent()方法,该方法接收一个回传的Intent参数,可以在方法内进行耗时操作,因为它默认开启了一个子线程,操作执行完成后也无需手动调用stopSelf()方法,onHandleIntent()会自动调用该方法。

    第二种方式:Context.bindService()

  2. Service的销毁
    stopService/unbindService
    在Service的启动这一部分,我们已经简单介绍了销毁Service的方法。

    startService—>stopService(stopSelf)
    bindService—>unbindService

  3. 生命周期
    在这里插入图片描述

  4. 启动前台Service
    Service几乎都是在后台运行的,一直以来它都是默默地做着辛苦的工作。但是Service的系统优先级还是比较低的,当系统出现内存不足情况时,就有可能会回收掉正在后台运行的Service。如果你希望Service可以一直保持运行状态,而不会由于系统内存不足的原因导致被回收,就可以考虑使用前台Service。

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    
    Notification notification = new Notification(icon, text, System.currentTimeMillis());
    Intent notificationIntent = new Intent(this, ExampleActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
    notification.setLatestEventInfo(this, title, mmessage, pendingIntent);
    startForeground(ONGOING_NOTIFICATION_ID, notification);
    

3. BroadcastReceiver

target 26 之后,无法在 AndroidManifest 显示声明大部分广播,除了一部分必要的广播,如:

ACTION_BOOT_COMPLETED
ACTION_TIME_SET
ACTION_LOCALE_CHANGED

LocalBroadcastManager.getInstance(MainActivity.this).registerReceiver(receiver, filter);

静态注册和动态注册

4. ContentProvider

作用:进程间进行数据交互、共享,即跨进程通信
原理:ContentProvider的底层原理 = Android中的Binder机制
ContentProvider 管理对结构化数据集的访问。它们封装数据,并提供用于定义数据安全性的机制。 内容提供程序是连接一个进程中的数据与另一个进程中运行的代码的标准界面。

ContentProvider 无法被用户感知,对于一个 ContentProvider 组件来说,它的内部需要实现增删该查这四种操作,它的内部维持着一份数据集合,这个数据集合既可以是数据库实现,也可以是其他任何类型,如 List 和 Map,内部的 insert、delete、update、query 方法需要处理好线程同步,因为这几个方法是在 Binder 线程池中被调用的。

ContentProvider 通过 Binder 向其他组件乃至其他应用提供数据。当 ContentProvider 所在的进程启动时,ContentProvider 会同时启动并发布到 AMS 中,需要注意的是,这个时候 ContentProvider 的 onCreate 要先于 Application 的 onCreate 而执行。

ContentProvider 和 sql 在实现上有什么区别?

ContentProvider 屏蔽了数据存储的细节,内部实现透明化,用户只需关心 uri 即可(是否匹配)
ContentProvider 能实现不同 app 的数据共享,sql 只能是自己程序才能访问
Contentprovider 还能增删本地的文件,xml等信息

3.3.屏幕适配

3.3.1相关重要概念

屏幕尺寸
含义:手机对角线的物理尺寸
单位:英寸(inch),1英寸=2.54cm
Android手机常见的尺寸有5寸、5.5寸、6寸等等

屏幕分辨率
含义:屏幕像素点数总和
例子:1080x1920,即宽度方向上有1080个像素点,在高度方向上有1920个像素点
单位:px(pixel),1px=1像素点(UI设计师的设计图会以px作为统一的计量单位)

Android手机常见的分辨率:320x480、480x800、720x1280、1080x1920

屏幕像素密度
含义:每英寸的像素点数
单位:dpi(dots per ich)

假设设备内每英寸有160个像素,那么该设备的屏幕像素密度=160dpi

安卓手机对于每类手机屏幕大小都有一个相应的屏幕像素密度:
在这里插入图片描述
屏幕尺寸、屏幕分辨率、屏幕像素密度三者关系
在这里插入图片描述
密度无关像素
含义:density-independent pixel,叫dp或dip,与终端上的实际物理像素点无关
单位:dp,可以保证在不同屏幕像素密度的设备上显示相同的效果

Android开发时用dp而不是px单位设置图片大小,是Android特有的单位
场景:假如同样都是画一条长度是屏幕一半的线,如果使用px作为计量单位,那么在480x800分辨率手机上设置应为240px;在320x480的手机上应设置为160px,二者设置就不同了;如果使用dp为单位,在这两种分辨率下,160dp都显示为屏幕一半的长度。

dp与px的转换
因为ui设计师给你的设计图是以px为单位的,Android开发则是使用dp作为单位的,那么我们需要进行转换:
在这里插入图片描述
在Android中,规定以160dpi(即屏幕分辨率为320x480)为基准:1dp=1px

独立比例像素
含义:scale-independent pixel,叫sp或sip
单位:sp

Android开发时用此单位设置文字大小,可根据字体大小首选项进行缩放
推荐使用12sp、14sp、18sp、22sp作为字体设置的大小,不推荐使用奇数和小数,容易造成精度的丢失问题;小于12sp的字体会太小导致用户看不清

3.3.2.屏幕适配:目前最好的适配方案

  1. SmallestWidth适配(sw限定符适配)
    实现原理:Android会识别屏幕可用高度和宽度的最小尺寸的dp值(其实就是手机的宽度值),然后根据识别到的结果去资源文件中寻找对应限定符的文件夹下的资源文件。

    sw限定符适配宽高限定符适配类似,区别在于,前者有很好的容错机制,如果没有value-sw360dp文件夹,系统会向下寻找,比如离360dp最近的只有value-sw350dp,那么Android就会选择value-sw350dp文件夹下面的资源文件。这个特性就完美的解决了宽高限定符的容错问题。
      
    优点:1.非常稳定,极低概率出现意外
       2.不会有任何性能的损耗
       3.适配范围可自由控制,不会影响其他三方库
      
    缺点:就是多个dimens文件可能导致apk变大,几百k(影响很小)。

    附件:生成sw文件的工具

  2. 今日头条适配方案
    实现原理修改系统的density值(核心)

    今日头条适配是以设计图的宽或高进行适配的,适配最终是改变系统density实现的。
    在这里插入图片描述
    优点:使用成本低,侵入性低,修改一次项目所有地方都会适配,无性能损耗
    缺点:
    1.只需要修改一次 density,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反应了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的。
    2.这个方案依赖于设计图尺寸,但是项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图完全一样。

  3. AutoSize
    AndroidAutoSize 是基于今日头条适配方案,该开源库已经很大程度上解决了今日头条适配方案的两个缺点,可以对activity,fragment进行取消适配。也是目前我的项目中所使用的适配方案。

    使用也非常简单只需两步:
    (1)引入:

     implementation 'me.jessyan:autosize:1.1.2'
    

    (2)在 AndroidManifest 中填写全局设计图尺寸 (单位 dp),如果使用副单位,则可以直接填写像素尺寸,不需要再将像素转化为 dp,详情请查看 demo-subunits

    <manifest>
        <application>            
            <meta-data
                android:name="design_width_in_dp"
                android:value="360"/>
            <meta-data
                android:name="design_height_in_dp"
                android:value="640"/>           
         </application>           
    </manifest>
    

3.4.Android消息机制

子线程更新UI的方式

1、调用activity的runOnUIThread()

// 因为runOnUiThread是Activity中的方法,Context是它的父类,所以要转换成Activity对象才能使用
((Activity) context).runOnUiThread(new Runnable() {
    @Override
    public void run() {
        // 在这里执行你要想的操作 比如直接在这里更新ui或者调用回调在 在回调中更新ui
    }
});

2、Handle发送Runnable

new Handler(mContext.getMainLooper()).post(new Runnable() {
    @Override
        public void run() {
            // 在这里执行你要想的操作 比如直接在这里更新ui或者调用回调在 在回调中更新ui
        }
});

3、Handler发送Message

private Handler handler = new Handler() {
 	//处理消息
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
        case XXX:
       		 XXX XXX= msg.obj;
             break;
        }
    }
};

//发送消息
	Message msg = Message.obtain()