若是你们去面Android客户端岗位,那么必问Java基础和Kotlin基础,因此,我打算花3,4篇文章的样子来给你们总结下Android面试中会问到的一些Java基础知识。java
面向过程:面向过程性能比面向对象高。由于对象调用须要实例化,开销比较大,较消耗资源,因此当性能是最重要的考量因素的时候,好比单片机、嵌入式开发、Linux/Unix 等,通常采用面向过程开发。可是,面向过程没有面向对象易维护、易复用、易扩展。
面向对象:面向对象易维护、易复用、易扩展。由于面向对象有封装、继承、多态性的特性,因此可设计出低耦合的系统,使得系统更加灵活、更加易于维护。面试
那为何,面向过程性能比面向对象高呢?
面向过程也须要分配内存,计算内存偏移量,Java 性能差的主要缘由并非由于它是面向对象语言,而是由于 Java 是半编译语言,最终的执行代码并非能够直接被 CPU 执行的二进制机器码。而面向过程语言大多都是直接编译成机器码在电脑上执行,而且其它一些面向过程的脚本语言性能也并不必定比 Java 好。算法
当 .class 字节码文件经过 JVM 转为机器能够执行的二进制机器码时,JVM 类加载器首先加载字节码文件,而后经过解释器逐行进行解释执行,这种方式的执行速度相对比较慢。并且有些方法和代码块是反复被调用的(也就是所谓的热点代码),因此后面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成一次编译后,会将字节码对应的机器码保存下来,下次能够直接调用。这也解释了咱们为何常常会说 Java 是编译与解释共存的语言。数据库
Java虚拟机所管理的内存包含程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区5个部分,模型图以下图所示。编程
因为Java虚拟机的多线程是经过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个肯定的时刻,一个处理器只会执行一条线程中的指令。为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储,这类内存区域为【线程私有】的内存。数组
程序计数器具备以下的特色:缓存
Java虚拟机栈也是线程私有的,它的生命周期与线程的生命周期同步,虚拟机栈描述的是Java方法执行的线程内存模型。每一个方法被执行的时候,Java虚拟机都会同步建立一个内存块,用于存储在该方法运行过程当中的信息,每一个方法被调用的过程都对应着一个栈帧在虚拟机中从入栈到出栈的过程。安全
Java虚拟机栈有以下的特色:服务器
本地方法栈与虚拟机所发挥的做用很类似,区别在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的本地方法服务。数据结构
Java堆是虚拟机所管理的内存中最大的一块,Java堆是被全部线程共享的一块内存区域,在虚拟机启动时建立。
此内存区域的惟一目的就是存放对象实例,java中“几乎”全部的对象实例都在这里分配内存。这里使用“几乎”是由于java语言的发展,及时编译的技术发展,逃逸分析技术的日渐强大,栈上分配、标量替换等优化手段,使java对象实例都分配在堆上变得不那么绝对。
Java堆是垃圾收集器管理的主要区域,所以不少时候也被称作“GC堆”。从内存回收的角度来看,因为如今收集器基本都采用分代收集算法(G1以后开始变得不同,引入了region,可是依旧采用了分代思想),Java堆中还能够细分为:新生代和老年代。再细致一点的有Eden空间、From Survivor空间、ToSurvivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,简写TLAB)。
OOM异常
Java堆的大小既能够固定也能够扩展,可是主流的虚拟机,堆的大小都是支持扩展的。若是须要线程请求分配内存,但堆已满且内存已没法再扩展时,就抛出 OutOfMemoryError 异常。好比:
/** * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError */ public class HeapOOMTest { public static final int _1MB = 1024 * 1024; public static void main(String[] args) { List<Integer[]> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { Integer[] ints = new Integer[2 * _1MB]; list.add(ints); } } }
方法区和Java堆同样,是各个线程共享的内存区域,他用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
在 HotSpot JVM 中,永久代(永久代实现方法区)中用于存放类和方法的元数据以及常量池,好比Class和Method。每当一个类初次被加载的时候,它的元数据都会放到永久代中。永久代是有大小限制的,所以若是加载的类太多,颇有可能致使永久代内存溢出,为此咱们不得不对虚拟机作调优。
后来HotSpot放弃永久代(PermGen),jdk1.7版本中,HotSpot已经把本来放在永久代的字符串常量池、静态变量等移出,到了jdk1.8,彻底废弃了永久代,方法区移至元空间(Metaspace)。好比类元信息、字段、静态属性、方法、常量等都移动到元空间区。元空间的本质和永久代相似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。所以,默认状况下,元空间的大小仅受本地内存限制。
经常使用的JVM调参以下表:
参数 | 做用描述 |
---|---|
-XX:MetaspaceSize | 分配给Metaspace(以字节计)的初始大小。若是不设置的话,默认是20.79M,这个初始大小是触发首次 Metaspace Full GC 的阈值,例如 -XX:MetaspaceSize=256M |
-XX:MaxMetaspaceSize | 分配给Metaspace 的最大值,超过此值就会触发Full GC,此值默认没有限制,但应取决于系统内存的大小。JVM会动态地改变此值。可是线上环境建议设置,例如-XX:MaxMetaspaceSize=256M |
-XX:MinMetaspaceFreeRatio | 最小空闲比,当 Metaspace 发生 GC 后,会计算 Metaspace 的空闲比,若是空闲比(空闲空间/当前 Metaspace 大小)小于此值,就会触发 Metaspace 扩容。默认值是 40 ,也就是 40%,例如 -XX:MinMetaspaceFreeRatio=40 |
-XX:MaxMetaspaceFreeRatio | 最大空闲比,当 Metaspace 发生 GC 后,会计算 Metaspace 的空闲比,若是空闲比(空闲空间/当前 Metaspace 大小)大于此值,就会触发 Metaspace 释放空间。默认值是 70 ,也就是 70%,例如 -XX:MaxMetaspaceFreeRatio=70 |
运行时常量池
运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译期间生成的各类字面量与符号引用,这部份内容将在类加载后存放到方法区的运行时常量池中。
方法区中存放:类信息、常量、静态变量、即时编译器编译后的代码。常量就存放在运行时常量池中。
当类被 Java 虚拟机加载后, .class文件中的常量就存放在方法区的运行时常量池中。并且在运行期间,能够向常量池中添加新的常量。如String类的intern()方法就能在运行期间向常量池中添加字符串常量。
直接内存并非虚拟机运行时数据区的组成部分,在 NIO 中引入了一种基于通道和缓冲的 IO 方式。它能够经过调用本地方法直接分配Java虚拟机以外的内存,而后经过一个存储在堆中的DirectByteBuffer对象直接操做该内存,而无须先将外部内存中的数据复制到堆中再进行操做,从而提升了数据操做的效率。
因为直接内存并不是Java虚拟机的组成部分,所以直接内存的大小不受 Java 虚拟机控制,但既然是内存,若是内存不足时仍是会抛出OutOfMemoryError异常。
下面是直接内存与堆内存的一些异同点:
服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但常常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而致使动态扩展时出现OutOfMemoryError异常。
Java的类加载器能够分为BootstrapClassLoader、ExtClassLoader和AppClassLoader,它们的做用以下。
类加载会涉及一些加载机制。
Java的内存管理主要涉及三个部分:堆 ( Java代码可及的 Java堆 和 JVM自身使用的方法区)、栈 ( 服务Java方法的虚拟机栈 和 服务Native方法的本地方法栈 ) 和 保证程序在多线程环境下可以连续执行的程序计数器。
Java堆是进行垃圾回收的主要区域,故其也被称为GC堆;而方法区的垃圾回收主要针对的是新生代和中生代。总的来讲,堆 (包括Java堆 和 方法区)是 垃圾回收的主要对象,特别是Java堆。
引用计数
每一个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时能够回收。此方法虽然简单,但没法解决对象相互循环引用的问题。
可达性分析
从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证实此对象是不可用的。在Java中,GC Roots包括:
本地方法栈中 JNI 引用的对象。
标记清除法
如它的名字同样,算法分为“标记”和“清除”两个阶段:首先标记出全部须要回收的对象,在标记完成后统一回收掉全部被标记的对象。之因此说它是最基础的收集算法,是由于后续的收集算法都是基于这种思路并对其缺点进行改进而获得的。
标记复杂算法有两个主要的缺点:一个是效率问题,标记和清除过程的效率都不高;另一个是空间问题,标记清除以后会产生大量不连续的内存碎片,空间碎片太多可能会致使,当程序在之后的运行过程当中须要分配较大对象时没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。
复制算法
复制的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。
它的优势是每次只须要对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效。而缺点也是显而易见的,内存缩小为原来的一半,持续复制长生存期的对象则致使效率下降。
标记整理法
复制收集算法在对象存活率较高时就要执行较多的复制操做,效率将会变低。更关键的是,若是不想浪费50%的空间,就须要有额外的空间进行分配担保,以应对被使用的内存中全部对象都100%存活的极端状况,因此在老年代通常不能直接选用这种算法。
根据老年代的特色,有人提出了另一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法同样,但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存。
分代收集算法
分代收集算法,就是把Java堆分为新生代和老年代,这样就能够根据各个年代的特色采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。而老年代中由于对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
方法重写
在Java程序中,类的继承关系能够产生一个子类,子类继承父类,它具有了父类全部的特征,继承了父类全部的方法和变量。子类能够定义新的特征,当子类须要修改父类的一些方法进行扩展,增大功能,程序设计者经常把这样的一种操做方法称为重写,也叫称为覆写或覆盖。
方法重写有以下一些特色:
方法重载
方法重载是让类以统一的方式处理不一样类型数据的一种手段。调用方法时经过传递给它们的不一样个数和类型的参数来决定具体使用哪一个方法,这就是多态性。所谓方法重载是指在一个类中,多个方法的方法名相同,可是参数列表不一样。参数列表不一样指的是参数个数、参数类型或者参数的顺序不一样。
按值传递:值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中若是对参数进行修改,将不会影响到实际参数。简单来讲就是直接复制了一份数据过去,由于是直接复制,因此这种方式在传递时若是数据量很是大的话,运行效率天然就变低了,因此Java在传递数据量很小的数据是值传递,好比Java中的各类基本类型:int、float、double、boolean等类型。
引用传递:引用传递其实就弥补了上面说的不足,若是每次传参数的时候都复制一份的话,若是这个参数占用的内存空间太大的话,运行效率会很底下,因此引用传递就是直接把内存地址传过去,也就是说引用传递时,操做的其实都是源数据,这样的话修改有时候会冲突,记得用逻辑弥补下就行了,具体的数据类型就比较多了,好比Object,二维数组,List,Map等除了基本类型的参数都是引用传递。
下面是使用hashCode()与equals()的相关规定:
为何必需要重写 hashcode 方法?其实就是为了保证同一个对象,保证在 equals 相同的状况下 hashcode 值一定相同,若是重写了 equals 而未重写 hashcode 方法,可能就会出现两个没有关系的对象 equals 相同的(由于 equals 都是根据对象的特征进行重写的),但 hashcode 确实不相同的。
相同点:
异同点:
HashMap底层采用了数组+链表的数据结构,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
若是定位到的数组位置不含链表,那么执行查找、添加等操做很快,仅需一次寻址便可;若是定位到的数组包含链表,对于添加操做,其时间复杂度为O(n),首先遍历链表,存在即覆盖,不然新增;对于查找操做来说,仍需遍历链表,而后经过key对象的equals方法逐一比对查找。因此,性能考虑,HashMap中的链表出现越少,性能才会越好。
HashMap有4个构造器,其余构造器若是用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值initialCapacity默认为16,loadFactory默认为0.75。
public HashMap(int initialCapacity, float loadFactor) { //此处对传入的初始容量进行校验,最大不能超过MAXIMUM_CAPACITY = 1<<30(230) if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; init();//init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现 }
加载因子存在的缘由,仍是由于减缓哈希冲突,若是初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。因此加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。
Put过程
Get过程
JDK 1.8的HashMap底层采用的是链表+红黑树,增长一个阈值进行判断是否将链表转红黑树,HashEntry 修改成 Node,目的是解决hash冲突形成的链表愈来愈长、查询慢的问题。
Get过程
Get过程
JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操做,那什么是CAS。
CAS是compare and swap的缩写,中文称为【比较交换】。CAS是一种基于锁的操做,并且是乐观锁。在Java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个以前得到锁的线程释放锁以后,下一个线程才能够访问。而乐观锁采起了一种宽泛的态度,经过某种方式不加锁来处理资源,性能较悲观锁有很大的提升。
CAS 操做包含三个操做数 —— 内存位置(V)、预期原值(A)和新值(B)。若是内存地址里面的值和A的值是同样的,那么就将内存里面的值更新成B。CAS是经过无限循环来获取数据的,若是在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程须要自旋,到下次循环才有可能机会执行。
Java 按照锁的实现分为乐观锁和悲观锁,乐观锁和悲观锁并非一种真实存在的锁,而是一种设计思想。
悲观锁
悲观锁是一种悲观思想,它总认为最坏的状况可能会出现,它认为数据极可能会被其余人所修改,因此悲观锁在持有数据的时候总会把资源 或者 数据 锁住,这样其余线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。悲观锁的实现每每依靠数据库自己的锁功能实现。
Java 中的 Synchronized 和 ReentrantLock 等独占锁(排他锁)也是一种悲观锁思想的实现,由于 Synchronzied 和 ReetrantLock 无论是否持有资源,它都会尝试去加锁,生怕本身心爱的宝贝被别人拿走。
乐观锁
乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,因此读取不会上锁,可是乐观锁在进行写入操做的时候会判断当前数据是否被修改过(具体如何判断咱们下面再说)。乐观锁的实现方案通常来讲有两种: 版本号机制 和 CAS实现 。乐观锁多适用于多度的应用类型,这样能够提升吞吐量。
在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
线程是进程中可独立执行的最小单位,也是 CPU 资源(时间片)分配的基本单位,同一个进程中的线程能够共享进程中的资源,如内存空间和文件句柄。线程有一些基本的属性,如id、name、以及priority。
id:线程 id 用于标识不一样的线程,编号可能被后续建立的线程使用,编号是只读属性,不能修改。
name:线程的名称,默认值是 Thread-(id)
daemon:分为守护线程和用户线程,咱们能够经过 setDaemon(true) 把线程设置为守护线程。守护线程一般用于执行不重要的任务,好比监控其余线程的运行状况,GC 线程就是一个守护线程。setDaemon() 要在线程启动前设置,不然 JVM 会抛出非法线程状态异常,可被继承。
priority:线程调度器会根据这个值来决定优先运行哪一个线程(不保证),优先级的取值范围为 1~10,默认值是 5,可被继承。Thread 中定义了下面三个优先级常量:
一个线程被建立后,会经历从建立到消亡的状态,下图是线程状态的变动过程。
下表是展现了线程的生命周期状态变化:
状态 | 说明 |
---|---|
New | 新建立了一个线程对象,但尚未调用start()方法。 |
Runnable | Ready 状态 线程对象建立后,其余线程(好比 main 线程)调用了该对象的 start() 方法。该状态的线程位于可运行线程池中,等待被线程调度选中 获取 cpu 的使用权。Running 绪状态的线程在得到 CPU 时间片后变为运行中状态(running)。 |
Blocked | 线程由于某种缘由放弃了cpu 使用权(等待锁),暂时中止运行。 |
Waiting | 线程进入等待状态由于如下几个方法: Object#wait()、 Thread#join()、 LockSupport#park() |
Terminated | 该线程已经执行完毕。 |
线程同步和并发一般会问到Synchronized、volatile、Lock的做用。其中,Lock是一个类,而其他两个则是Java关键字。
Synchronized
Synchronized是Java的关键字,也是Java的内置特性,在JVM层面实现了对临界资源的同步互斥访问,经过对对象的头文件来操做,从而达到加锁和释放锁的目的。使用Synchronized修饰的代码或方法,一般有以下特性:
正是由于上面的特性,因此Synchronized的缺点也是显而易见的:即若是一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其余线程便只能一直等待,所以效率很低。
volatile
保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。而且volatile是禁止进行指令重排序。
所谓指令重排序,指的是处理器为了提升程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行前后顺序同代码中的顺序一致,可是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
volatile为了保证原子性,必须具有如下条件:
按照做用的不一样,Java的锁能够分为以下:
悲观锁认为本身在使用数据的时候必定有别的线程来修改数据,所以在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java 中,synchronized 关键字和 Lock 的实现类都是悲观锁。悲观锁适合写操做多的场景,先加锁能够保证写操做时数据正确。
而乐观锁认为本身在使用数据时不会有别的线程修改数据,因此不会添加锁,只是在更新数据的时候去判断以前有没有别的线程更新了这个数据。若是这个数据没有被更新,当前线程将本身修改的数据成功写入。若是数据已经被其余线程更新,则根据不一样的实现方式执行不一样的操做(例如报错或者自动重试)。乐观锁在 Java 中是经过使用无锁编程来实现,最常采用的是 CAS 算法,Java 原子类中的递增操做就经过 CAS 自旋实现。乐观锁适合读操做多的场景,不加锁的特色可以使其读操做的性能大幅提高。
这里说到了CAS算法,那么什么是CAS算法呢?
一个线程失败或挂起并不会致使其余线程也失败或挂起,那么这种算法就被称为非阻塞算法。而CAS就是一种非阻塞算法实现,也是一种乐观锁技术,它能在不使用锁的状况下实现多线程安全,所以是一种无锁算法。
CAS算法的定义:CAS的主要做用是不使用加锁就能够实现线程安全,CAS 算法又称为比较交换算法,是一种实现并发算法时经常使用到的技术,Java并发包中的不少类都使用了CAS技术。CAS具体包括三个参数:当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改成B并返回true,不然什么都不作,并返回false。
原子更新的基本操做包括:
以AtomicInteger为例,代码以下:
public class AtomicInteger extends Number implements java.io.Serializable { //返回当前的值 public final int get() { return value; } //原子更新为新值并返回旧值 public final int getAndSet(int newValue) { return unsafe.getAndSetInt(this, valueOffset, newValue); } //最终会设置成新值 public final void lazySet(int newValue) { unsafe.putOrderedInt(this, valueOffset, newValue); } //若是输入的值等于预期值,则以原子方式更新为新值 public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } //原子自增 public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } //原子方式将当前值与输入值相加并返回结果 public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } }
再如,下面是使用多线程对一个int值进行自增操做的代码,以下所示。
public class AtomicIntegerDemo { private static AtomicInteger atomicInteger = new AtomicInteger(0); public static void main(String[] args){ for (int i = 0; i < 5; i++){ new Thread(new Runnable() { public void run() { //调用AtomicInteger的getAndIncement返回的是增长以前的值 System.out.println(atomicInteger.getAndIncrement()); } }).start(); } System.out.println(atomicInteger.get()); } }
阻塞或唤醒一个 Java 线程须要操做系统切换 CPU 状态来完成,这种状态转换须要耗费处理器时间。在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。若是物理机器有多个处理器,可以让两个或以上的线程同时并行执行,咱们就可让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程【稍等一下】,咱们需让当前线程进行自旋,若是在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就能够没必要阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁。
当前线程拥有其余线程须要的资源,当前线程等待其余线程已拥有的资源,都不放弃本身拥有的资源。
所谓反射,指的是在运行状态中,对于任意一个类,都可以获取这个类的全部属性和方法;对于任意一个对象,都可以调用它的任意一个方法和属性,而这种动态获取的信息以及动态调用对象的方法的功能就被称为Java语言的反射机制。
使用反射前须要事先获取到的字节码,在Java中,获取字节码的方式有三种:
Java 语言中的类、方法、变量、参数和包等均可以被标注。和 Javadoc 不一样,Java 标注能够经过反射获取标注内容。根据做用时机的不一样,Java的注解能够分为三种:
为了保证只有一个对象存在,可使用单例模式,网上有,单例模式的七种写法。咱们介绍一下常见的几种:
懒汉式
懒汉式使用的是static关键字,所以是线程不安全的。
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
若是要线程安全,那么须要使用synchronized关键字。
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
不过,使用synchronized锁住以后,运行效率明显下降。
静态内部类
静态内部类利用了classloder的机制来保证初始化instance时只有一个线程。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
双重校验锁
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
双重检查锁定是synchronized的升级的写法,那为何要使用volatile关键字呢,是为了禁止初始化实例时的重排序。咱们知道,初始化一个实例在java字节码中会有4个步骤:
然后两步是有可能会重排序,而使用volatile能够禁止指令重排序。