Android面向面试复习-Java篇

306 阅读35分钟

面向过程与面向对象的优缺点

面向过程:性能高,单片机,嵌入式等需要性能。但是不容易复用维护和扩展

面向对象:容易维护复用和扩展,但是性能低


静态内部类和非静态内部类的区别

  1. 静态内部类可以有静态成员变量和方法也可以有非静态的成员变量和方法(根据Oracle官方内部类添加static后升级为顶级类,与普通类无异,不再是严格意义上的内部类),而非静态内部类只能由非静态的成员变量和方法
  2. 静态内部类只能访问外部类的静态成员变量和方法,而非静态内部类可以访问外部类所有的成员变量和方法
  3. 静态内部类实例可以直接创建,不需要和外部类实例绑定,而创建非静态内部类需要先创建外部类实例
  4. 非静态内部类默认持有外部类引用,可能导致内存泄漏,而静态内部类没有外部类引用

面向对象三大原则并举例

封装:

封装性是面向对象编程的核心思想,指的就是将描述某种实体的数据和基于这些数的操作集合到一起,形成一个封装体,封装的思想保证了类内部数据结构的完整性,使用户无法轻易直接操作类的内部数据,这样降低了对内部数据的影响,提高了程序的安全性和可维护性。

延申:public private proteced default的区别

(由小到大) private:本类可见 default:本包可见(即默认的形式)(本包中的子类非子类均可访问,不同包中的类及子类均不能访问) protected:本包和所有子类都可见(本包中的子类非子类均可访问,不同包中的子类可以访问,不是子类不能访问) public:所有类可见


继承:

继承是Java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或类从父 类继承方法,使得子类具有父类相同的行为。

延申:继承的限制和初始化顺序

不能被继承的父类成员:

1.private成员

2.子类与父类不在同包,使用默认访问权限的成员

3.构造方法

初始化顺序:父类属性——父类构造方法——子类属性——子类构造方法


多态:

将父类对象应用于子类对象的特征就是面向对象编程中的多态性的体现,多态指的就是在应用程序中出现的“ 重名 ” 现象。多态性允许以统一的风格编写程序,以处理种类繁多的已存在的类及其相关类。这样既降低了维护难度,又节省了时间

延申:多态的底层实现

静多态:静多态主要是依据于方法的重载,编译器只需要查看方法签名就能决定在编译时为特定方法调用调用哪个方法。对应早期绑定。 动多态:动多态的底层实现是依靠动态绑定,即在运行时才把方法调用与方法实现联系起来。对应晚期绑定。 多态的实现是因为虚拟机在编译时确定了一个虚方法表,每次调用传过来对象时只需要调整虚方法表的指针来确定调用哪个方法。子类直接使用子类方法表指针,有非子类的方法则找到Object类里。父类直接使用父类方法表指针,有非父类的方法则也找到Object类里。


面向对象设计六大原则

单一职责原则:一个类只负责一个职责

开闭原则:一个软件实体应该对扩展开放,对修改关闭

里氏替换原则:所有引用父类的地方用其子类对象均可替换,编译器察觉不出子父类区别

依赖倒置原则:思考设计问题时应先抽象大局,后思考细节

接口隔离原则:将大接口打散成小的接口

迪米特原则:一个对象应该对其他对象有最少地了解,低耦合,高内聚


八大基本数据类型所占字节

byte、short、int、long:对应1、2、4、8字节

float、double:对应4、8字节

char:对应2字节

boolean:比较特殊,不同处理器上不一样

另外,32位和64位处理机上基本数据类型所占字节数除了long似乎没有变化,因此不可由基本数据类型所占字节数反推出位数


int和integer的区别:

integer是int的包装类,Integer执行++操作时会先拆箱为int++再装箱回Integer

Integer的缓存区域为(-128,127),超过区域要新创建对象,在区域内可以直接复用


sync + static关键字修饰的方法对比sync关键字修饰的方法

sync关键字锁住的是对象锁,sync + static关键字锁住的是类级别锁


static关键字的作用

  1. 修饰的变量为类实例对象所共享
  2. 修饰的方法可以被类名直接调用
  3. 修饰的类只能是内部类,不持有外部类引用

volatile

  1. 是一个类型修饰符,作为指令关键字,确保本条指令不会因为编译器优化而省略
  2. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值对其他线程来说时立即可见的
  3. 禁止进行指令重排序
  4. 只能保证单词读/写的原子性

写理解:当写一个volatile变量时,JMM会把该线程对应的本地中的共享变量值刷新到主内存,如下图线程A调用init方法,线程B调用use方法 image.png 读理解:当读一个volatile变量时,JMM会把线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量

image.png 简单来说:A和B线程之间进行一次通信,线程A在写volatile变量时,实际上就像是给B发送了一个消息告诉线程B的值是旧的,线程B读取这个volatile变量时就是接收了线程A刚刚发送的消息,自己的存储信息是旧的,只能去主内存去取


transient

  1. 将不需要序列化的属性前添加transient,序列化对象时,这个属性就不会被序列化
  2. Serialization将对象状态存储到硬盘,需要时取出,而transient使其修饰的字段生命周期仅存于调用者内存而与磁盘无关,
  3. 对静态变量无效,静态变量存储在方法区,获取时通过方法区获取,与Serialization序列化存储于磁盘无关,所以对静态变量无效

对象的创建流程

  1. 虚拟机遇到一个new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用
  2. 检查这个符号引用代表的类是否已经被加载,解析和初始化过,如果没有那必须先执行相应的类的加载过程
  3. 在类加载检查通过后,为新生对象分配内存,对象所需的内存大小在类加载完成后便可确定

对象的内存布局

  1. 对象头 1.1 MarkWord:hash值,GC分代年龄,锁标志,持有的锁的指针,偏向线程ID

1.2 类型指针:指向他的类元数据的指针,如果是数组,对象头中还有一块用于记录数组的长度

  1. 实例数据

2.1 对象数据的内存区域,真正的有效信息

  1. 对齐填充

3.1 对齐填充不是必然存在的,HotSpot虚拟机中自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍,而对象头部分正好是8字节的整数倍。因此当对象实例数据部分没有对齐时,就需要通过对其补充来补全


synchronized

  1. 三个有关性质 1.1 原子性:一个操作或者多个操作,要么全部执行且执行过程不会被任何因素打断,要么就都不执行

1.2 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值

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

  1. synchronized加锁保证方法操作的原子性,那么锁加给谁? 2.1 隐式锁

加在普通方法上时,默认是锁当前对象,即this

加在static方法上时,默认锁当前类的class对象

2.2 显示锁

明确写出synchronized(对象)括号中的对象

2.3 互斥性为了保证同时只能有一个线程持有锁,其他线程与这个持有锁的线程互斥,处于等待状态,多个线程持有的锁要相同才可以互斥。

Monitor机制及特点

  1. 互斥
  2. 提供signal机制
  3. 依赖底层OS的Mutex Lock实现,成本高
  4. java中只要是对象都有各自的monitor

Monitor在JVM中的实现

  1. 是线程私有的数据结构
  2. 由ObjectMonitor实现

一次简单的获取锁流程

  1. 首先线程A进入EntryList

  2. 尝试去获取synchronized锁对应的monitor对象,此时Owner和Recursion不再为空,Owner变为线程对象,Recursion加1

  3. 获得锁后返回,并且进入The Owner,执行同步方法块中的内容

  4. 反复调用同步方法块即反复调用同一把锁会使Recursion根据次数自增,不会重复获取锁

image.png

一次简单的线程间争抢锁流程

  1. 线程B进入EntryList

  2. 同样尝试去获取锁的monitor对象,但是获取不到

  3. 回到EntryList进行等待

image.png

假如B争抢到了锁后的流程

  1. A执行完后进入WaitSet,monitor对象被释放

  2. 线程B被唤起去竞争锁,Owner和Recursion被置空后变为线程B对象,Recursion加1

  3. 回到EntryList后进入The Owner,同样执行自身的同步代码块

image.png

MarkWord存放的信息

  1. 对象的Hashcode
  2. 分代年龄
  3. GC标记
  4. 锁的标记

MarkWord中存放锁信息及状态

  1. 无锁状态:对象的Hashcode,分代年龄,是否偏向锁,锁标志位
  2. 偏向锁状态:线程ID,Epoch,分代年龄,是否偏向锁,锁标志位
  3. 轻量级锁:指向栈中锁记录的指针,锁标志位
  4. 重量级锁:指向重量级的锁指针,锁标志位

image.png

偏向锁

设计偏向锁之前每次进出同步块线程都要获取锁和释放锁,同一个线程如此操作会很浪费资源,所以当线程第一次进入时会在MarkWord里记录当前线程ID,如果记录成功,则设置当前锁为偏向锁。第二次当前线程进入时先与MarkWord里记录ID进行比较,如果相同则直接进入,无需获取锁。节省资源,提高性能。

轻量级锁

同上情况,如果第二次进入线程非第一次进入线程,即两次线程ID不同,则认为发生锁竞争,锁升级膨胀为轻量级锁(亦说明上述偏向锁不会主动释放锁,有竞争才释放锁),将MarkWord中内容拷贝进线程私有栈帧中的LockRecor,使用CAS操作将竞争对象的MarkWord中的值更新成指向LockRecord的指针,也将LockRecord中的owner指向原始MarkWord。

  1. 如果更新成功则代表该线程拥有轻量级锁。
  2. 如果更新失败则先检查MarkWord是否指向当前线程栈帧 2.1 如果是则说明当前线程持有该锁

2.2 如果不是则说明存在竞争,使用重量级锁

  1. CAS操作:Compare and Swap,比较并替换

重量级锁

即最开始的synchronized实现,当线程竞争很多时,轻量级锁再次膨胀为重量级锁,线程不会自旋,但是会消耗OS层面互斥量的资源。

对比:

image.png

锁的重入

假如线程A持有锁1时进入同步块1,在同步块1中调用同步块2,然而同步块2也需要锁1,但是由于线程每次进出都要获取和释放锁,在同步块1中调用需要相同锁的同步块2,此时没有释放锁,但是可以调用同步块2,称为锁的重入,一些锁支持重入一些不支持。

锁自旋

让一个线程去执行一个无意义的循环,虽然消耗CPU的时间,目的是为了等待自旋结束后立刻重新去竞争锁,维持锁的活跃状态而不阻塞

锁的适应性自旋

JDK1.6之后由JVM自己控制自旋次数,比如线程超过10次自旋则将线程挂起,下次该线程的自旋次数会减少,如果获取到了说明当前可以较为宽松的获取锁,下一次自旋的循环次数会增加

锁粗化

将多个连续的加锁解锁后合称为一个,减少不必要的开销

锁消除

删除不必要的加锁操作,根据代码逃逸技术,如果判断一段代码中堆上的数据不会逃逸处当前线程,则可认为这段代码是线程安全的,不必要加锁


公平锁和非公平锁

公平锁:多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到顺序 非公平锁:多个线程同时尝试获取同一把锁时,获取锁时线程可以不按顺序插队


乐观锁和悲观锁

悲观锁

适用于写多读少(多写)。当要对数据库中一条数据进行修改时,为了避免同时被他人修改,最好的办法就是直接对该数据加锁防止并发。这种借助数据库锁机制,在修改数据之前先锁定再修改的方式称为悲观并发控制。悲观锁主要分为以下两类:

共享锁:shared locks,简称读锁,意为多个事务对同一数据可以共享一把锁,都能访问到数据,但只能读不能修改

排他锁,exclusive locks,简称写锁,意为不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据行读取和修改。

说明:悲观并发控制是“先取锁再访问“,保证数据安全,效率方面会让数据库产生额外开销,还增加了产生死锁的机会,降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那一行数据。 使用方式:

  1. 数据库行锁,表锁,读锁,写锁等
  2. synchronized关键字 举例:

image.png

乐观锁

乐观锁:适用读多写少(多读),乐观锁相对悲观锁而言,假设数据一般情况下不会造成冲突,在数据进行提交更新时才会对数据的冲突与否进行检测。如果发现冲突,则返回给用户错误的信息,让用户决定如何去做。这样提高了程序吞吐量。 使用方式:

  1. CAS实现:java.util.concurrent.atomic下原子变量使用了乐观锁
  2. 版本号控制 说明:乐观并发控制相信事务之间的数据竞争概率较小,因此尽可能直接做下去,提交时才锁定,所以不会产生任何锁和死锁

举例:

image.png


泛型

Java的泛型是伪泛型,编译期间List和List被看作是同一个List,类型参数在编译时会被去掉,称为类型擦除。体现在字节码文件中jvm通过Signature保存原始泛型信息。此外,通过反射可以添加非泛型类型的参数。

泛型中区分 <? extends T> 和 <? super T>。亦称为协变和逆变

extends相当于 <=,即只能匹配T及其子类数据,PE原则频繁读取get() super相当于 >=,即只能匹配T及其父类数据,CS原则频繁插入add()

泛型是一种语法糖,含义为增加编译效率,但编译时效果与不加语法糖相同


注解

  1. 编写文档:通过元数据生成文档
  2. 代码分析:通过元数据对代码进行分析
  3. 编译检查:通过元数据让编译器实现基本的编译检查
  4. 自定义注解:

image.png


IO流

image.png

执行到read时,底层发生操作如下:

  1. 内核给磁盘控制器发命令要读取磁盘某块信息
  2. 在DMA控制下,磁盘数据读入内核缓冲区
  3. 内核把数据从内核缓冲区复制到用户缓冲区

异常

image.png 异常分为两种:

  1. 运行时异常:编译阶段无法检查,如除以0等,大多是代码错误
  2. 编译时异常:编译时可以检查,解决方式有 2.1 throws

2.2 try/catch捕获

try、catch、finally、return执行顺序

  1. try中return如果可以正常执行,且finally中没有return,则执行return,但是在执行return之后,返回调用处之前会执行finally中的语句,如果返回的是引用类型,finally中对该引用内容的修改会生效,如果返回的时基本数据类型,则finally中的对返回的数据变化将不生效
  2. try中如果return可以正常执行,且finally中有return,则finally中return覆盖try中return
  3. finally无论如何都会执行,除非try根本没有执行到,或者在try时终止虚拟机

延申:Error与Exception的区别

Error:一般是编译时错误,导致与虚拟机相关的内存溢出,系统崩溃等,建议终止虚拟机。 Exception:可捕获,程序可处理,不建议让虚拟机终止。

类比:Error就像水池,Exception就像水池里的水。 Error好比水池里的水溢出,或者水池破坏崩溃,与外部环境相关。 Exception好比水池里的水被污染,变浑浊,影响水质。


反射

反射时,需要调用访问权限受限的属性或方法时,需要设置setAccessible(Boolean flag)为true,才能不检查访问权限正常访问,默认为false


接口和抽象类

JDK7中接口只能定义全局常量和抽象方法 JDK8中接口可以有抽象方法,静态方法,默认方法。 JDK9中接口中方法访问修饰符甚至可以声明为private。

接口和抽象类不能实例化

抽象类中可以有抽象方法,也可以有别的有方法体的方法,也有构造器(只要是类就有构造器)


List

Vector: 作为List接口的古老实现类,线程安全,但是效率低,底层使用Object[] element存储数据。JDK7和8中通过构造器创建对象时,默认数组长度为10,扩容时默认扩容为原来的2倍。

LinkedList:对于频繁的插入删除效率比ArrayList高,线程不安全,底层是双向链表。内部声明了first和last的头尾指针,封装数据会创建Node对象。

ArrayList:List接口的主要实现类,线程不安全,但是效率高,底层使用Object[] element存储。 JDK7中创建长度为10的Object[]数组,如果添加导致底层数组长度不够则扩容,默认扩容为原来的1.5倍,同时需要将原有数组复制到新数组中。类似单例饿汉式。 JDK8中初始化数组时没有创建,第一次add时才创建,类似单例懒汉式。


数组和链表的区别:

  1. 数组里存储的数据应该是连续的,链表存储的数据不一定内存上连续,但是逻辑上是的
  2. 数组的插入删除效率比较低,链表增加和删除很容易
  3. 数组的大小是锁死的,不够还要扩容,链表不用定义大小,数据扩展方便
  4. 数组有随机读取查找效率高,链表不具有随机读取性查找数据效率低

Map

  1. Map结构中的key是无序的,不可重复的,使用Set存储,而且重写equals和hashcode方法,只重写equals方法在判断两个对象是否相等的时候可能会产生问题。
  2. Map结构中的value是无序的,可重复的,使用Collection存储,重写equals方法。
  3. Map结构中的key——value键值对构成一个Entry对象,也是无序不可重复的,用Set存储。

HashMap

JDK7

  1. 作为Map的主要实现类,线程不安全,效率高,存储null的key和value,底层是数组+链表+红黑树。

  2. 实例化后创建底层长度为16的一维数组Entry[] table,调用put方法时,首先调用key所在类的hashCode()计算哈希值,得到Entry在数组中存放的位置,从而产生以下几种情况区分: 1.如果此位置上为空,此时的k1 - v1添加成功。 2.如果此位置上的数据不为空(多个以链表形式存储),比较k1和已经存在的一个或多个数据的哈希值:

    2.1如果k1的哈希值与已经存在的数据哈希值都不相同,此时k1 - v1键值对添加成功。

    2.2如果k1的哈希值与已经存在的某一个数据的哈希值相同,调用k1所在类的equals方法,比较:

    2.2.1如果equals返回false,则添加成功。

    2.2.2如果equals返回true,使用v1替换v2值。

不断添加数据会扩容为原来的2倍,并将原有数组复制过来。如果设定初始值,转换理论值为最近的2的n次幂.

JDK8

实例化时底层没有创建数组,首次调用put时才创建底层Node[]数组,添加红黑树作为数据结构存储信息。

LinkedHashMap

保证在遍历Map元素时,可以按照添加的顺序实现遍历,在原有基础上添加了一对指针,指向前一个和后一个,对于频繁的遍历操作效率高于HashMap。

TreeMap

保证按照添加的键值对进行排序,实现排序遍历,底层使用红黑树。

HashTable

古老实现类,线程安全,效率低,不能存储null的key和value。


Set

Set存储无序的,不可重复的数据

HashSet

Set接口的主要实现类,线程不安全,可以存储null,底层为链表+数组

LinkedSet

作为HashSet子类,遍历其内部数据可以按照添加顺序遍历

TreeSet

其中数据必须属于同一个类,可以按照添加对象的指定属性进行排序


并发集合

ConcurrentHashMap:

JDK1.7之前

  1. 线程安全。
  2. 一个指定个数的Segment数组,每一个元素Segment相当于一个HashTable。
  3. 扩容只需要扩充Segment而非整个HashTable。
  4. 不存储null。
  5. initialCapacity,代表HashEntry[]数组的大小,初始化默认为16
  6. loadFactor加载因子默认0.75
  7. concurrentLevel,并发级别即默认分段锁个数为16 7.1 并发设置过小会带来锁竞争

7.2 并发设置过大会是原本位于同一个Segment内的访问会扩散到不同的Segment中,cpuCache命中率下降,引起程序性能下降

put元素:

  1. 根据key获取hash值
  2. 根据hash算出Segment
  3. 根据hash值与Segment中的HashEntry的容量-1按位与获取HashEntry的index
  4. 若不存在相同的hash,直接返回新节点插入链头,将原来的头节点设为新结点next
  5. 若存在相同hash值,根据onlyIfAbsent决定是否替换旧值 ConcurrentHashMap基于concurrencyLevel划分多个Segment存储key-value,这样put元素时只锁住当前Segment,可以避免put的时候锁住整个map,减少并发时的阻塞现象。

get元素:

  1. 根据key获取hash值
  2. 根据hash算出Segment
  3. 根据hash值与Segment中的HashEntry的容量-1按位与获取HashEntry的index
  4. 遍历链表,找出hash和key与给定参数相等的HashEntry element 4.1 没找到,返回null

4.2 找到element,获取element.value

4.2.1 element.value != null,直接返回

4.2.2 element.value == null,先加锁,并发put操作将value设置成功后返回

对于get操作,基本没有锁,只有当找到了element且element.value == null,有可能是当下的这个HashEntry刚刚被创建,value属性还没有设置成功,这时我们读到的是该HashEntry的value的默认值null,所以这里加锁,等到put结束后返回value

加锁情况:

  1. put
  2. get中找到了hash与key都和指定参数相同的HashEntry,但是value == null
  3. remove
  4. size(),三次尝试后还未成功,遍历所有Segment,分别加锁

总结:

  1. 分段锁机制

  2. 数组+链表+红黑树

  3. Segment继承ReetrantLock用来充当锁角色,每个Segment对象守护每个散列映射表的若干个桶

  4. HashEntry用来封装映射表的键值对

  5. 每个同是由若干个HashEntry对象连接起来的链表

image.png

JDK1.8

  1. 数据结构上取消了Segment分段锁结构,取而代之以Node数组+链表+红黑树结构
  2. 采用CAS+synchronized保证线程安全
  3. 对每个数组元素加锁,锁粒度降低
  4. 时间复杂度由O(n)变为O(logN)
  5. table默认为null,初始化发生在第一次插入操作,默认大小为16的数组,用来存储Node节点数据,扩容时大小为2的幂次方

sizeCtl

  1. 默认为0,用来控制table初始化和扩容操作
  2. -1表示table正在初始化
  3. -N表示有N – 1个线程正在进行扩容操作
  4. 其余情况 4.1 如果table未初始化,表示table需要初始化大小

4.2 如果table初始化完成,表示table的容量,默认是table大小的0.75倍

4.3 使用volatile来保持可见性

ForwardingNode

  1. 特殊节点,hash值为-1
  2. 仅当需要扩容时才发挥作用,作为占位符,表示当前访问的结点已经被迁移到ForwardingNode的nextTable中
  3. 初始化时并没有初始化容量,只是初始化了sizeCtl的值,初始化容量延后到第一次put,在initTable中,如果有其他线程正在初始化,则调用Thread.yield来让出时间片

put

  1. hash算法,下标计算和HashMap相同
  2. 获取Table元素时使用Unsafe.getObjectVolatile,因为我们的volatile只修饰了table,并不能保证table引用所指的内存区域是最新的,使用Unsafe.getObjectVolatile可以直接获取指定内存的数据,确保数据最新
  3. 如果元素为空,那么使用Unsafe.compareAndSwapObject插入Node结点,如果插入成功,则判断是否扩容,插入失败自旋重新尝试插入
  4. 如果非空,元素值为-1,说明正在resize,且当前结点已经被迁到nextTable的位置,然后放到扩容后的table的位置
  5. 其余情况即元素非空且hash值不为-1,则按照链表或红黑树插入,使用synchronized实现并发
  6. 可以看出虽然比原来高效,但是resize变复杂了

transfer

  1. 当插入一个新元素时可能发生扩容
  2. 加载因子0.75,扩容后数组长度为原来的两倍,容量为新数组长度的0.75
  3. 扩容时,将旧table中的元素拷贝进新table中,然后用CAS操作将已经处理过的桶元素替换成ForwardingNode,其中包含新table的指针,如果此时有线程访问或者修改桶中元素,那么可以将其指到新table,协助进行扩容

CopyOnWriteArrayList:

  1. 线程安全且在读操作时无锁的ArrayList。
  2. 采用的模式就是”CopyOnWrite”。
  3. 底层数据结构是Object[],初始容量位0,之后每增加一个元素,容量 + 1,数组复制一次。
  4. 遍历的是全局数组的一个副本,即使全局数组刚刚发生了变化,副本也不会变化,所以不会发生并发异常。但是可能在遍历中读到一些刚刚被删除的对象。
  5. 增删改上锁,读不上锁
  6. 读多写少且脏数据影响不大的并发情况下,选择CopyOnWriteArrayList

CopyOnWriteArraySet:

基于CopyOnWriteArrayList,不添加重复元素

ArrayBlockingQueue:

  1. 基于数组,先进先出,线程安全。可以实现指定时间的阻塞读写,并且容量可限制
  2. 由一个对象数组 + 1把锁ReentrantLock + 2个条件Condition

三种入队操作对比:

  1. offer(E e):如果队列没满,返回true;如果队列满了,返回false。——不阻塞
  2. put(E e):如果队列满了,一直阻塞,直到数组不满或者线程被中断。——阻塞
  3. offer(E e,long timeout,TimeUnit unit):在队尾插入一个元素,如果数组已满,则进入等待,直到出现以下三种情况。——阻塞 3.1 被唤醒

3.2 等待超时

3.3 当前线程被中断

三种出队操作对比:

  1. poll():如果没有元素,直接返回null;如果有元素,出队
  2. take():如果队列空了,一直阻塞,直到数组不为空或者线程被中断。——阻塞
  3. poll(long timeout,TimeUnit unit):如果数组不为空,出队;如果数组已空且已经超时,返回null;如果数组已空且时间没有超时,则进入等待,直到出现以下三种情况。 3.1 被唤醒

3.2 等待超时

3.3 当前线程被中断

注意:

  1. 数组必须是指定长度的,整个过程数组长度不变,队头随着出入队一直循环后移
  2. 锁的形式有公平和非公平两种
  3. 在只有入队高并发或出队高并发情况下,因为操作数组且不需要扩容,性能高

LinkedBlockingQueue:

基于链表实现,读写各用一把锁,在高并发读写操作都多的情况下,性能优于ArrayBlockingQueue

  1. 由1个链表 + 两把锁 + 两个条件
  2. 默认容量为整数最大值,可以看作没有容量限制
  3. 三种入队与三种出队与上面完全一致,只是因为容量无限,入队时无阻塞等待

哈希冲突如何解决

开放定址法:发生冲突找下一个空散列地址 再哈希法:用多个不同Hash函数,发生冲突用下一个 链地址法:每个节点有一个next指针,多个哈希表结点的next指针构成单链表,同一个索引上的多个结点可以用这个单链表连接起来 建立公共溢出区:发生冲突的元素一律放入溢出表


==、equals、hashcode

==:

基本数据类型,也称原始数据类型: byte,short,char,int,long,float,double,boolean 他们之间的比较,应用双等号(==),比较的是他们的值。

引用类型(类、接口、数组) : 当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。

对象是放在堆中的,栈中存放的是对象的引用(地址)。由此可见'=='是对栈中的值进行比较的。如果要比较堆中对象的内容是否相同,那么就要重写equals方法了。

equals:

默认情况(没有覆盖equals方法)下equals方法都是调用Object类的equals方法,而Object的equals方法主要用于判断对象的内存地址引用是不是同一个地址(是不是同一个对象)。

要是类中覆盖了equals方法,那么就要根据具体的代码来确定equals方法的作用了,覆盖后一般都是通过对象的内容是否相等来判断对象是否相等。

hashcode:

从方法的名称上就可以看出,其目的是生成一个hash码。hash码的主要用途就是在对对象进行散列的时候作为key输入,据此很容易推断出,我们需要每个对象的hash码尽可能不同,这样才能保证散列的存取性能。

equals与hashcode联系:

  1. 如果两个对象equals,Java运行时环境会认为他们的hashcode一定相等。
  2. 如果两个对象不equals,他们的hashcode有可能相等。
  3. 如果两个对象hashcode相等,他们不一定equals。
  4. 如果两个对象hashcode不相等,他们一定不equals。

为什么重写equals要伴随重写hashcode:

一个很常见的错误根源在于没有覆盖hashCode方法。在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。如果不这样做的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和Hashtable。

1.在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。

2.如果两个对象根据equals()方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。

3.如果两个对象根据equals()方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生相同的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。


单例设计模式

饿汉式:使用类时已经把对象加载完毕 懒汉式:调用get()时才加载对象 线程安全懒汉式:实例化时加上synchronized保证线程安全,但效率低 DCL双检查锁机制:两次检查instance==null,效率高,线程安全,如下

image.png

使用volatile修饰静态实例是由于指令重排序,new Deemo()中包含三个指令,首先开辟空间,其次调用构造函数,最后返回对象引用。但是由于指令重排序,构造函数可能与返回对象引用的调用顺序交换,导致返回的引用所指向的对象并没有初始化。另一个线程尝试获取单例,然后if条件跳过,直接返回导致异常,加入volatile后,该变量的赋值指令前的指令必须完成后才可以进行赋值,避免了这一问题


代理设计模式

  1. 隐藏我们需要代理的类
  2. 通过代理添加其他功能,功能增强
  3. 具体有静态和动态实现两种方式 3.1 静态代理:创建代理类时接口和代理类已经被固定无法改变。符合开闭原则

3.2 动态代理:运行时动态生成,编译后没有实际的class文件。


使用clone()的浅拷贝与深拷贝

浅拷贝,复制了一份引用,指向原对象,对新引用的修改会影响原引用

image.png

深拷贝,全新的对象和引用,对新对象的修改自然不会影响到原对象

image.png


多线程的创建方式

  1. 继承Thread
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 通过线程池创建

对比

  1. Runnable和Callable接口 优势:线程实现接口还可以继承其他类,此方式下多个线程可以共享一个target对象,所以非常适合多个相同线程来处理同一份资源,从而可以将CPU代码和数据分开,形成清晰的模型 劣势:编程复杂,访问当前线程必须要使用Thread.currentThread方法
  2. 继承Thread: 优势:编程简单,this可以直接获取当前线程 劣势:线程类已经继承Thread,不能再继承其他类
  3. Callable和Runnable的区别 3.1 Callable重写的方法是call(),Runnable重写的方法是run() 3.2 Callable执行任务后可返回值,而Runnable任务不能返回 3.3 call方法可以抛出异常,run不可以 3.4 运行Callable任务可以拿到一个Future对象,表示异步计算结果,提供了检查计算是否完成的方法,以等待计算完成,并检索计算结果,通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取结果

线程的状态

  1. 新建状态:当线程对象创建后,即进入了新建的状态
  2. 就绪状态:当调用线程对象的start方法时,线程进入就绪状态,处于就绪状态的线程只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了start方法此线程会立即执行
  3. 运行状态:当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入运行状态,就绪状态是进入到运行状态的唯一入口,也就是说,线程想要进入运行状态执行,首先必须处于就绪状态中
  4. 阻塞状态:处于运行状态中的线程由于某种原因暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入就绪状态才有机会再次被CPU调用以进入到运行状态。根据阻塞产生原因不同,阻塞状态又分为三种 4.1等待阻塞:运行状态中的线程执行wait方法,使本线程进入到等待阻塞状态 4.2同步阻塞:线程在获取synchronized同步锁失败时,进入同步阻塞状态 4.3其他阻塞:通过调用线程的sleep或join发出IO请求,线程会进入到阻塞状态,当sleep状态超时,join等待线程终止或者超时,或者IO处理完毕,线程重新转入就绪状态
  5. 死亡状态:线程执行完了或者因异常退出了run方法,该线程结束生命周期

sleep()和wait()方法的区别

sleep是线程类的方法,wait是Object类的方法 sleep不释放对象锁,wait释放对象锁 sleep暂停线程,但是监控仍然保持,结束后自动恢复


happens-before

happens-before是Java中的有序性原则

  1. 程序次序规则:⼀个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  2. 锁定规则:⼀个unLock操作先行发生于后面对同⼀个锁的lock操作
  3. volatile变量规则:对⼀个变量的写操作先行发生于后面对这个变量的读操作
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个⼀个动作
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到 中断事件的发生
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  8. 对象终结规则:⼀个对象的初始化完成先行发生于他的finalize()方法的开始

a = a + b 和 a += b

在操作同类型数据时,这两个表达式的含义相同。在操作不同类型的数据时,a += b表达式会在内部进行一次精度转换,而a = a + b操作不同类型的数据时,则会编译期报错,无法通过


欢迎指正。