Android复习资料——Java知识点汇总(宝藏干货)

jvm
jvm工作流程

运行时数据区(Runtime Data Area)


方法指令

类加载器


static
static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。
静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。
能通过this访问静态成员变量吗? 所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。
static是不允许用来修饰局部变量
final
可以声明成员变量、方法、类以及本地变量
final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误
final变量是只读的
final申明的方法不可以被子类的方法重写
final类通常功能是完整的,不能被继承
final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销
final关键字提高了性能,JVM和Java应用都会缓存final变量,会对方法、变量及类进行优化
方法的内部类访问方法中的局部变量,但必须用final修饰才能访问
String、StringBuffer、StringBuilder
String是final类,不能被继承。对于已经存在的Stirng对象,修改它的值,就是重新创建一个对象
StringBuffer是一个类似于String的字符串缓冲区,使用append()方法修改Stringbuffer的值,使用toString()方法转换为字符串,是线程安全的
StringBuilder用来替代于StringBuffer,StringBuilder是非线程安全的,速度更快
异常处理
Exception、Error是Throwable类的子类
Error类对象由Java虚拟机生成并抛出,不可捕捉
不管有没有异常,finally中的代码都会执行
当try、catch中有return时,finally中的代码依然会继续执行

内部类
内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。
内部类的方法可以直接访问外部类的所有数据,包括私有的数据。
多态
父类的引用可以指向子类的对象
创建子类对象时,调用的方法为子类重写的方法或者继承的方法
如果我们在子类中编写一个独有的方法,此时就不能通过父类的引用创建的子类对象来调用该方法
抽象和接口
抽象类不能有对象(不能用new此关键字来创建抽象类的对象)
抽象类中的抽象方法必须在子类中被重写
接口中的所有属性默认为:public static final ****;
接口中的所有方法默认为:public abstract ****;
集合

List接口存储一组不唯一,有序(插入顺序)的对象, Set接口存储一组唯一,无序的对象。
HashMap是非synchronized的,性能更好,HashMap可以接受为null的key和value,而Hashtable是线程安全的,比HashMap要慢,不接受null
反射
单例
饿汉式
双重检查模式
静态内部类模式
静态内部类的原理是:
当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。getInstance()方法并没有多次去new对象,取的都是同一个INSTANCE对象。
虚拟机会保证一个类的<clinit>()
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()
方法完毕
缺点在于无法传递参数,如Context等
线程

valatile
当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步,因此在读取volatile类型的变量时总会返回最新写入的值。

当一个变量定义为 volatile 之后,将具备以下特性:
保证此变量对所有的线程的可见性,不能保证它具有原子性(可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的)
禁止指令重排序优化
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行
AtomicInteger 中主要实现了整型的原子操作,防止并发情况下出现异常结果,其内部主要依靠JDK 中的unsafe 类操作内存中的数据来实现的。volatile 修饰符保证了value在内存中其他线程可以看到其值得改变。CAS操作保证了AtomicInteger 可以安全的修改value 的值。
HashMap

HashMap的工作原理
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存Entry对象。当两个对象的hashcode相同时,它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry会存储在链表中,当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。
如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
为什么String, Interger这样的wrapper类适合作为键?
因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
synchronized
当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。
Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。
根据获取的锁分类
获取对象锁
synchronized(this|object) {}
修饰非静态方法
获取类锁
synchronized(类.class) {}
修饰静态方法
原理
同步代码块:
monitorenter和monitorexit指令实现的
同步方法
方法修饰符上的ACC_SYNCHRONIZED实现
Lock

悲观锁、乐观锁
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现。乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
自旋锁、适应性自旋锁
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。
自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。
死锁
当前线程拥有其他线程需要的资源,当前线程等待其他线程已拥有的资源,都不放弃自己拥有的资源。
本次的分享就到这啦,喜欢的话可以点个赞👍或关注,
这里的话我也将我之前在学习过程中用到的一些学习资料整理成了文档,以及我自身之前的一个面试文案和知识点补充,有需要的朋友可以评论区留言,学习资料免费下载,这里也祝愿大家最终都能够学有所成,早日找到满意的工作!
适合初学者学习的2021最新Java学习视频,书籍,面试题,PDF文档,都是经典干货!