Android学习指南 — Java知识点汇总,flutter页面跳转数据

本文深入探讨了Android开发中的内存管理,详细讲解了Java堆、方法区(包括运行时常量池)、本地方法栈的工作原理,以及对象存活判断和垃圾收集算法。内容涵盖内存分配、垃圾收集器如CMS和G1的特点,同时讨论了并发标记、复制、标记-整理等算法。此外,还提到了线程、单例模式、反射和集合框架如ArrayList和LinkedList的区别。
摘要由CSDN通过智能技术生成

本地方法栈

本地方法栈(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 异常。

方法指令


| 指令 | 说明 |

| — | — |

| invokeinterface | 用以调用接口方法 |

| invokevirtual | 指令用于调用对象的实例方法 |

| invokestatic | 用以调用类/静态方法 |

| invokespecial | 用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法 |

类加载器


| 类加载器 | 说明 |

| — | — |

| BootstrapClassLoader | Bootstrap 类加载器负责加载 rt.jar 中的 JDK 类文件,它是所有类加载器的父加载器。Bootstrap 类加载器没有任何父类加载器,如果你调用 String.class.getClassLoader(),会返回 null,任何基于此的代码会抛出 NUllPointerException 异常。Bootstrap 加载器被称为初始类加载器 |

| ExtClassLoader | 而 Extension 将加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从 jre/lib/ext 目录下或者 java.ext.dirs 系统属性定义的目录下加载类。Extension 加载器由 sun.misc.Launcher$ExtClassLoader 实现 |

| AppClassLoader | 第三种默认的加载器就是 System 类加载器(又叫作 Application 类加载器)了。它负责从 classpath 环境变量中加载某些应用相关的类,classpath 环境变量通常由 -classpath 或 -cp 命令行选项来定义,或者是 JAR 中的 Manifest 的 classpath 属性。Application 类加载器是 Extension 类加载器的子加载器 |

| 工作原理 | 说明 |

| — | — |

| 委托机制 | 加载任务委托交给父类加载器,如果不行就向下传递委托任务,由其子类加载器加载,保证 java 核心库的安全性 |

| 可见性机制 | 子类加载器可以看到父类加载器加载的类,而反之则不行 |

| 单一性机制 | 父加载器加载过的类不能被子加载器加载第二次 |

垃圾回收 gc


对象存活判断

  • 引用计数

每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

  • 可达性分析

从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

在Java语言中,GC Roots包括:

  • 虚拟机栈中引用的对象。
  • 方法区中类静态属性实体引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中 JNI 引用的对象。

垃圾收集算法

  • 标记 -清除算法

“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

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

  • 复制算法

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

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

  • 标记-整理算法

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

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  • 分代收集算法

GC 分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

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

垃圾收集器

  • 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 收集器收集老年代对象会有短暂停顿。

内存模型与回收策略

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 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记——整理算法。

Object

======

equals 方法


对两个对象的地址值进行的比较(即比较引用是否相同)

public boolean equals(Object obj) {

return (this == obj);

}

hashCode 方法


hashCode() 方法给对象返回一个 hash code 值。这个方法被用于 hash tables,例如 HashMap。

它的性质是:

  • 在一个Java应用的执行期间,如果一个对象提供给 equals 做比较的信息没有被修改的话,该对象多次调用 hashCode() 方法,该方法必须始终如一返回同一个 integer。

  • 如果两个对象根据 equals(Object) 方法是相等的,那么调用二者各自的 hashCode() 方法必须产生同一个 integer 结果。

  • 并不要求根据 equals(Object) 方法不相等的两个对象,调用二者各自的 hashCode() 方法必须产生不同的 integer 结果。然而,程序员应该意识到对于不同的对象产生不同的 integer 结果,有可能会提高 hash table 的性能。

在 JDK 中,Object 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法直接返回对象的 内存地址。在 String 类,重写了 hashCode 方法

public int hashCode() {

int h = hash;

if (h == 0 && value.length > 0) {

char val[] = value;

for (int i = 0; i < value.length; i++) {

h = 31 * h + val[i];

}

hash = h;

}

return h;

}

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 中的代码依然会继续执行

| 常见的Error | | |

| — | — | — |

| OutOfMemoryError | StackOverflowError | NoClassDeffoundError |

| 常见的Exception | | |

| — | — | — |

| 常见的非检查性异常 | | |

| ArithmeticException | ArrayIndexOutOfBoundsException | ClassCastException |

| IllegalArgumentException | IndexOutOfBoundsException | NullPointerException |

| NumberFormatException | SecurityException | UnsupportedOperationException |

| 常见的检查性异常 | | |

| IOException | CloneNotSupportedException | IllegalAccessException |

| NoSuchFieldException | NoSuchMethodException | FileNotFoundException |

内部类

===

  • 非静态内部类没法在外部类的静态方法中实例化。

  • 非静态内部类的方法可以直接访问外部类的所有数据,包括私有的数据。

  • 在静态内部类中调用外部类成员,成员也要求用 static 修饰。

  • 创建静态内部类的对象可以直接通过外部类调用静态内部类的构造器;创建非静态的内部类的对象必须先创建外部类的对象,通过外部类的对象调用内部类的构造器。

匿名内部类


  • 匿名内部类不能定义任何静态成员、方法

  • 匿名内部类中的方法不能是抽象的

  • 匿名内部类必须实现接口或抽象父类的所有抽象方法

  • 匿名内部类不能定义构造器

  • 匿名内部类访问的外部类成员变量或成员方法必须用 final 修饰

多态

==

  • 父类的引用可以指向子类的对象

  • 创建子类对象时,调用的方法为子类重写的方法或者继承的方法

  • 如果我们在子类中编写一个独有的方法,此时就不能通过父类的引用创建的子类对象来调用该方法

抽象和接口

=====

  • 抽象类不能有对象(不能用 new 关键字来创建抽象类的对象)

  • 抽象类中的抽象方法必须在子类中被重写

  • 接口中的所有属性默认为:public static final;

  • 接口中的所有方法默认为:public abstract;

集合框架

====

  • List接口存储一组不唯一,有序(插入顺序)的对象, Set接口存储一组唯一,无序的对象。

HashMap


结构图

  • JDK 1.7 HashMap 结构图

  • JDK 1.8 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 的性能。

HashMap 与 HashTable 对比

HashMap 是非 synchronized 的,性能更好,HashMap 可以接受为 null 的 key-value,而 Hashtable 是线程安全的,比 HashMap 要慢,不接受 null 的 key-value。

HashMap.java

public class HashMap<K,V> extends AbstractMap<K,V>

implements Map<K,V>, Cloneable, Serializable {

···

public V put(K key, V value) {

return putVal(hash(key), key, value, false, true);

}

···

public V get(Object key) {

Node<K,V> e;

return (e = getNode(hash(key), key)) == null ? null : e.value;

}

···

}

HashTable.java

public class Hashtable<K,V>

extends Dictionary<K,V>

implements Map<K,V>, Cloneable, java.io.Serializable {

···

public synchronized V put(K key, V value) {

// Make sure the value is not null

if (value == null) {

throw new NullPointerException();

}

···

addEntry(hash, key, value, index);

return null;

}

···

public synchronized V get(Object key) {

HashtableEntry<?,?> tab[] = table;

int hash = key.hashCode();

int index = (hash & 0x7FFFFFFF) % tab.length;

for (HashtableEntry<?,?> e = tab[index] ; e != null ; e = e.next) {

if ((e.hash == hash) && e.key.equals(key)) {

return (V)e.value;

}

}

return null;

}

···

}

ConcurrentHashMap


Base 1.7

ConcurrentHashMap 最外层不是一个大的数组,而是一个 Segment 的数组。每个 Segment 包含一个与 HashMap 数据结构差不多的链表数组。

正在上传…重新上传取消​

在读写某个 Key 时,先取该 Key 的哈希值。并将哈希值的高 N 位对 Segment 个数取模从而得到该 Key 应该属于哪个Segment,接着如同操作 HashMap 一样操作这个 Segment。

Segment 继承自 ReentrantLock,可以很方便的对每一个 Segmen 上锁。

对于读操作,获取 Key 所在的 Segment 时,需要保证可见性。具体实现上可以使用volatile关键字,也可使用锁。但使用锁开销太大,而使用volatile时每次写操作都会让所有CPU内缓存无效,也有一定开销。ConcurrentHashMap 使用如下方法保证可见性,取得最新的Segment:

Segment<K,V> s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)

获取 Segment 中的 HashEntry 时也使用了类似方法:

HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile

(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE)

对于写操作,并不要求同时获取所有 Segment

《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享

的锁,因为那样相当于锁住了整个Map。它会先获取该 Key-Value 对所在的 Segment 的锁,获取成功后就可以像操作一个普通的 HashMap 一样操作该 Segment,并保证该 Segment 的安全性。同时由于其它 Segment 的锁并未被获取,因此理论上可支持 concurrencyLevel(等于Segment的个数)个线程安全的并发读写。

获取锁时,并不直接使用 lock 来获取,因为该方法获取锁失败时会挂起。事实上,它使用了自旋锁,如果 tryLock 获取锁失败,说明锁被其它线程占用,此时通过循环再次以 tryLock 的方式申请锁。如果在循环过程中该 Key 所对应的链表头被修改,则重置 retry 次数。如果 retry 次数超过一定值,则使用 lock 方法申请锁。

这里使用自旋锁是因为自旋锁的效率比较高,但是它消耗 CPU 资源比较多,因此在自旋次数超过阈值时切换为互斥锁。

Base 1.8

1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题:查询遍历链表效率太低。因此 1.8 做了一些数据结构上的调整。

其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

ConcurrentHashMap.java

final V putVal(K key, V value, boolean onlyIfAbsent) {

if (key == null || value == null) throw new NullPointerException();

int hash = spread(key.hashCode());

int binCount = 0;

for (Node<K,V>[] tab = table;😉 {

Node<K,V> f; int n, i, fh;

if (tab == null || (n = tab.length) == 0)

tab = initTable();

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {

if (casTabAt(tab, i, null,

new Node<K,V>(hash, key, value, null)))

break; // no lock when adding to empty bin

}

else if ((fh = f.hash) == MOVED)

tab = helpTransfer(tab, f);

else {

V oldVal = null;

synchronized (f) {

if (tabAt(tab, i) == f) {

if (fh >= 0) {

binCount = 1;

···

}

else if (f instanceof TreeBin) {

···

}

else if (f instanceof ReservationNode)

throw new IllegalStateException(“Recursive update”);

}

}

···

}

addCount(1L, binCount);

return null;

}

ArrayList


ArrayList 本质上是一个动态数组,第一次添加元素时,数组大小将变化为 DEFAULT_CAPACITY 10,不断添加元素后,会进行扩容。删除元素时,会按照位置关系把数组元素整体(复制)移动一遍。 ArrayList.java

public class ArrayList extends AbstractList

implements List, RandomAccess, Cloneable, java.io.Serializable

···

// 增加元素

public boolean add(E e) {

ensureCapacityInternal(size + 1); // Increments modCount!!

elementData[size++] = e;

return true;

}

···

// 删除元素

public E remove(int index) {

if (index >= size)

throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

modCount++;

E oldValue = (E) elementData[index];

int numMoved = size - index - 1;

if (numMoved > 0)

System.arraycopy(elementData, index+1, elementData, index,

numMoved);

elementData[–size] = null; // clear to let GC do its work

return oldValue;

}

···

// 查找元素

public E get(int index) {

if (index >= size)

throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

return (E) elementData[index];

}

···

}

LinkedList


LinkedList 本质上是一个双向链表的存储结构。

LinkedList.java

public class LinkedList

extends AbstractSequentialList

implements List, Deque, Cloneable, java.io.Serializable

{

····

private static class Node {

E item;

Node next;

Node prev;

Node(Node prev, E element, Node next) {

this.item = element;

this.next = next;

this.prev = prev;

}

}

···

// 增加元素

void linkLast(E e) {

final Node l = last;

final Node newNode = new Node<>(l, e, null);

last = newNode;

if (l == null)

first = newNode;

else

l.next = newNode;

size++;

modCount++;

}

···

// 删除元素

E unlink(Node x) {

final E element = x.item;

final Node next = x.next;

final Node prev = x.prev;

if (prev == null) {

first = next;

} else {

prev.next = next;

x.prev = null;

}

if (next == null) {

last = prev;

} else {

next.prev = prev;

x.next = null;

}

x.item = null;

size–;

modCount++;

return element;

}

···

// 查找元素

Node node(int index) {

// assert isElementIndex(index);

if (index < (size >> 1)) {

Node x = first;

for (int i = 0; i < index; i++)

x = x.next;

return x;

} else {

Node x = last;

for (int i = size - 1; i > index; i–)

x = x.prev;

return x;

}

}

···

}

对于元素查询来说,ArrayList 优于 LinkedList,因为 LinkedList 要移动指针。对于新增和删除操作,LinedList 比较占优势,因为 ArrayList 要移动数据。

CopyOnWriteArrayList


CopyOnWriteArrayList 是线程安全容器(相对于 ArrayList),增加删除等写操作通过加锁的形式保证数据一致性,通过复制新集合的方式解决遍历迭代的问题。

CopyOnWriteArrayList.java

public class CopyOnWriteArrayList

implements List, RandomAccess, Cloneable, java.io.Serializable {

final transient Object lock = new Object();

···

// 增加元素

public boolean add(E e) {

synchronized (lock) {

Object[] elements = getArray();

int len = elements.length;

Object[] newElements = Arrays.copyOf(elements, len + 1);

newElements[len] = e;

setArray(newElements);

return true;

}

}

···

// 删除元素

public E remove(int index) {

synchronized (lock) {

Object[] elements = getArray();

int len = elements.length;

E oldValue = get(elements, index);

int numMoved = len - index - 1;

if (numMoved == 0)

setArray(Arrays.copyOf(elements, len - 1));

else {

Object[] newElements = new Object[len - 1];

System.arraycopy(elements, 0, newElements, 0, index);

System.arraycopy(elements, index + 1, newElements, index,

numMoved);

setArray(newElements);

}

return oldValue;

}

}

···

// 查找元素

private E get(Object[] a, int index) {

return (E) a[index];

}

}

反射

==

try {

Class cls = Class.forName(“com.jasonwu.Test”);

//获取构造方法

Constructor[] publicConstructors = cls.getConstructors();

//获取全部构造方法

Constructor[] declaredConstructors = cls.getDeclaredConstructors();

//获取公开方法

Method[] methods = cls.getMethods();

//获取全部方法

Method[] declaredMethods = cls.getDeclaredMethods();

//获取公开属性

Field[] publicFields = cls.getFields();

//获取全部属性

Field[] declaredFields = cls.getDeclaredFields();

Object clsObject = cls.newInstance();

Method method = cls.getDeclaredMethod(“getModule1Functionality”);

Object object = method.invoke(null);

} catch (ClassNotFoundException e) {

e.printStackTrace();

} catch (IllegalAccessException e) {

e.printStackTrace();

} catch (InstantiationException e) {

e.printStackTrace();

} catch (NoSuchMethodException e) {

e.printStackTrace();

} catch (InvocationTargetException e) {

e.printStackTrace();

}

单例

==

饿汉式


public class CustomManager {

private Context mContext;

private static final Object mLock = new Object();

private static CustomManager mInstance;

public static CustomManager getInstance(Context context) {

synchronized (mLock) {

if (mInstance == null) {

mInstance = new CustomManager(context);

}

return mInstance;

}

}

private CustomManager(Context context) {

this.mContext = context.getApplicationContext();

}

}

双重检查模式


public class CustomManager {

private Context mContext;

private volatile static CustomManager mInstance;

public static CustomManager getInstance(Context context) {

// 避免非必要加锁

if (mInstance == null) {

synchronized (CustomManger.class) {

if (mInstance == null) {

mInstacne = new CustomManager(context);

}

}

}

return mInstacne;

}

private CustomManager(Context context) {

this.mContext = context.getApplicationContext();

}

}

静态内部类模式


public class CustomManager{

private CustomManager(){}

private static class CustomManagerHolder {

private static final CustomManager INSTANCE = new CustomManager();

}

public static CustomManager getInstance() {

return CustomManagerHolder.INSTANCE;

}

}

静态内部类的原理是: 当 SingleTon 第一次被加载时,并不需要去加载 SingleTonHoler,只有当 getInstance() 方法第一次被调用时,才会去初始化 INSTANCE,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。getInstance 方法并没有多次去 new 对象,取的都是同一个 INSTANCE 对象。

虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 方法,其他线程都需要阻塞等待,直到活动线程执行 <clinit>() 方法完毕

缺点在于无法传递参数,如Context等

线程

==

线程是进程中可独立执行的最小单位,也是 CPU 资源(时间片)分配的基本单位。同一个进程中的线程可以共享进程中的资源,如内存空间和文件句柄。

属性

| 属性 | 说明 |

| — | — |

| id | 线程 id 用于标识不同的线程。编号可能被后续创建的线程使用。编号是只读属性,不能修改 |

| name | 名字的默认值是 Thread-(id) |

| daemon | 分为守护线程和用户线程,我们可以通过 setDaemon(true) 把线程设置为守护线程。守护线程通常用于执行不重要的任务,比如监控其他线程的运行情况,GC 线程就是一个守护线程。setDaemon() 要在线程启动前设置,否则 JVM 会抛出非法线程状态异常,可被继承。 |

| priority | 线程调度器会根据这个值来决定优先运行哪个线程(不保证),优先级的取值范围为 1~10,默认值是 5,可被继承。Thread 中定义了下面三个优先级常量: - 最低优先级:MIN_PRIORITY = 1 - 默认优先级:NORM_PRIORITY = 5 - 最高优先级:MAX_PRIORITY = 10 |

状态

正在上传…重新上传取消​

| 状态 | 说明 |

| — | — |

| New | 新创建了一个线程对象,但还没有调用start()方法。 |

| Runnable | Ready 状态 线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start() 方法。该状态的线程位于可运行线程池中,等待被线程调度选中 获取 cpu 的使用权。Running 绪状态的线程在获得 CPU 时间片后变为运行中状态(running)。 |

| Blocked | 线程因为某种原因放弃了cpu 使用权(等待锁),暂时停止运行 |

| Waiting | 线程进入等待状态因为以下几个方法: - Object#wait() - Thread#join() - LockSupport#park() |

| Timed Waiting | 有等待时间的等待状态。 |

| Terminated | 表示该线程已经执行完毕。 |

状态控制


  • wait() / notify() / notifyAll()

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

| 方法 | 说明 |

| — | — |

| wait() | 线程状态由 的使用权。Running 变为 Waiting, 并将当前线程放入等待队列中 |

| notify() | notify() 方法是将等待队列中一个等待线程从等待队列移动到同步队列中 |

| notifyAll() | 则是将所有等待队列中的线程移动到同步队列中 |

被移动的线程状态由 Running 变为 Blocked,notifyAll 方法调用后,等待线程依旧不会从 wait() 返回,需要调用 notify() 或者 notifyAll() 的线程释放掉锁后,等待线程才有机会从 wait() 返回。

  • join() / sleep() / yield()

在很多情况,主线程创建并启动子线程,如果子线程中需要进行大量的耗时计算,主线程往往早于子线程结束。这时,如果主线程想等待子线程执行结束之后再结束,比如子线程处理一个数据,主线程要取得这个数据,就要用 join() 方法。

sleep(long) 方法在睡眠时不释放对象锁,而 join() 方法在等待的过程中释放对象锁。

yield() 方法会临时暂停当前正在执行的线程,来让有同样优先级的正在等待的线程有机会执行。如果没有正在等待的线程,或者所有正在等待的线程的优先级都比较低,那么该线程会继续运行。执行了yield方法的线程什么时候会继续运行由线程调度器来决定。

volatile

========

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值