JAVA基础
JVM内存模型
方法区
- 方法区主要是放一下类似类定义、常量、编译后的代码、静态变量等
- 在JDK1.7中,HotSpot VM的实现就是将其放在永久代中,这样的好处就是可以直接使用堆中的GC算法来进行管理,但坏处就是经常会出现内存溢出
- 在JDK1.8中,HotSpot VM取消了永久代,用元空间取而代之,元空间直接使用本地内存
堆
- 几乎所有对象、数组等都是在此分配内存的,在JVM内存中占的比例也是极大的,也是GC垃圾回收的主要阵地
- 平时所说的新生代、老年代、永久代就在堆空间中
虚拟机栈
- 当JVM在执行方法时,会在此区域中创建一个栈帧来存放方法的各种信息,比如返回值,局部变量表和各种对象引用等,方法开始执行前就先创建栈帧入栈,执行完后就出栈
本地方法栈
- 和虚拟机栈类似,不过区别是专门提供给Native方法用的
程序计数器
- 占用很小的一片区域,我们知道JVM执行代码是一行一行执行字节码,所以需要一个计数器来记录当前执行的行数
垃圾回收算法
标记清除
- 对内存中的对象依次的进行判断,如果对象需要回收那么就打一个标记,如果对象仍然需要使用,那么就保留下来
- 所有的对象都会被筛选判断一次,完毕后将内存中已经标记的对象依次进行清除
- 标记和清除需要两遍循环内存中的对象,标记本身也是一个比较麻烦的工作,因此这种算法的效率不是特别的高
- 对于分配的内存来说,往往是连续的比较好,因为这样有利于分配大数据的对象,这种算法会导致大量不连续的内存空间,在存放大对象时没有足够连续的空间而触发内存回收,造成空间不足而导致频繁GC和性能下降
复制算法
- 从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存,将原来的那一块儿内存全部回收掉
- JVM将堆空间年轻代的Eden和Survivor区同时作为复制算法的使用区域,Survivor又分为From区和To区
- 每次GC的时候都会将Eden和Survivor的From区中的有效对象进行标记,一同复制到Survivor的To区。然后彻底清除原来的Eden区和From区的内存对象。与此同时To区就是下一次回收的From区。
- 因为需要一块空白的区域作为内存对象要粘贴的区域,所以会造成内存空间的浪费,因此在有效对象占据总回收内存是非常小的时候,这种算法的性价比才会达到最高
标记整理
- 从根集合节点进行扫描,标记出所有的存活对象,被标记到的可用对象整体向一侧移动,然后直接清除掉可用对象边界以外的内存
- 老年代中的幸存对象较多,而且对象内存占用较大,一旦出现内存回收,需要被回收的对象并不多,碎片也就相对的比较少,因此这种方法常常被应用到老年代中
分代收集
- 这种算法就是将内存以代的形式划分,然后针对情况分别使用性价比最高的算法进行处理
- 越新的对象越可能被回收,越老的对象反而会存活的越久。因此针对这两种场景,新生代和老年代也分别采用复制算法和标记整理算法
- 当系统创建一个对象的时候,总是在Eden区操作,当这个区满了,那么就会触发一次YoungGC,也就是年轻代的垃圾回收,将还能使用的对象复制到 server From 区
- 当Eden区再次被用完,就再触发一次YoungGC,将Eden区与From区还在被使用的对象复制到To区
- 再下一次YoungGC的时候,则是将Eden区与To区中的还在被使用的对象复制到From区
- 经过若干次YoungGC后,有些对象在From与To之间来回游荡,达到From和To区的阈值15,将被复制到老年代中
- 并不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中所有相同年龄的对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代
- 当老年代的内存空间不够用,就会触发 Full GC
- 要合理设置年轻代与老年代的大小,尽量减少Full GC的操作,提高系统性能
GC
GC区域
- jvm 中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭,栈帧随着方法的进入和退出做入栈和出栈操作,实现了自动的内存清理,因此,我们的内存垃圾回收主要集中于 java 堆和方法区中
GC的对象
- 需要进行回收的对象就是已经没有存活的对象,判断一个对象是否存活常用的有两种办法:引用计数和可达分析
引用计数
- 每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题
可达性分析
- 这是Java虚拟机采用的判定对象是否存活的算法
- 通过一系列的称为“GC Roots"的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的
- 可作为GC Roots的对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象。本地方法栈引用的对象
finalize的执行过程
- 首先,大致描述一下finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”
- 对象可由两种状态,涉及到两类状态空间,一是终结状态空间 F; 二是可达状态空间 R
- unfinalized 新建对象会先进入此状态,GC并未准备执行其finalize方法,因为该对象是可达的
- finalizable 表示GC可对该对象执行finalize方法,GC已检测到该对象不可达。正如前面所述,GC通过F-Queue队列和一专用线程完成finalize的执行
- finalized 表示GC已经对该对象执行过finalize方法
- reachable 表示GC Roots引用可达
- finalizer-reachable 表示不是reachable,但可通过某个finalizable对象可达
- unreachable 对象不可通过上面两种途径可达
状态变迁图
变迁说明 - 新建对象首先处于[reachable, unfinalized]状态(A)
- 随着程序的运行,一些引用关系会消失,导致状态变迁,从reachable状态变迁到f-reachable(B, C, D)或unreachable(E, F)状态
- 若JVM检测到处于unfinalized状态的对象变成f-reachable或unreachable,JVM会将其标记为finalizable状态(G,H)。若对象原处于[unreachable, unfinalized]状态,则同时将其标记为f-reachable(H)
- 在某个时刻,JVM取出某个finalizable对象,将其标记为finalized并在某个线程中执行其finalize方法。由于是在活动线程中引用了该对象,该对象将变迁到(reachable, finalized)状态(K或J)。该动作将影响某些其他对象从f-reachable状态重新回到reachable状态(L, M, N)
- 处于finalizable状态的对象不能同时是unreahable的,由第4点可知,将对象finalizable对象标记为finalized时会由某个线程执行该对象的finalize方法,致使其变成reachable
- 程序员手动调用finalize方法并不会影响到上述内部标记的变化,因此JVM只会至多调用finalize一次,即使该对象“复活”也是如此。程序员手动调用多少次不影响JVM的行为
- 若JVM检测到finalized状态的对象变成unreachable,回收其内存(I)
- 若对象并未覆盖finalize方法,JVM会进行优化,直接回收对象(O)
- 注意 System.runFinalizersOnExit()等方法可以使对象即使处于reachable状态,JVM仍对其执行finalize方法
synchronized
使用方式
- 修饰实例方法 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法 给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
- 修饰代码块 指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁
- 注意 synchronized 关键字加到 static 静态方法和代码块上都是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a), 因为JVM中字符串常量池具有缓存功能!
底层实现
- 实现synchronized的基础:Java对象头+Monitor
对象头的机构如下:
虚拟机位数 | 头对象结构 | 说明
- | - | -
32/64bit | Mark Word | 默认存储对象的hashcode,分代年龄,锁类型,锁标志位等信息
32/64bit | Class Metadata Address | 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的数据
Mark Word 说明图:

Monitor锁的竞争与释放如下

- Java6之前,synchronized属于重量级锁,依赖于Mutex Lock实现。线程之间的切换需要从用户态转换到核心态,开销很大
- Java 6之后,synchronized性能得到很大提升,主要引入
自旋锁与自适应自旋锁
在很多情况下,共享数据的锁定状态持续时间较短,切换线程不值得。就通过让线程执行忙循环等待锁的释放,而不让出CPU。缺点是若锁被其他线程长时间占用,会带来许多性能上的开销.若用户使用preBlockspin来修改等待时间、次数,反而不好设定。自适应自旋锁就出现了,它自旋的次数不固定,由前一次在同一个锁上的自旋时间及锁的持有者的状态来决定
锁消除
JIT编译时,对运行的上下文进行扫描,去除不可能存在竞争的锁,节省毫无意义的请求锁的时间
锁粗化
通过扩大加锁的范围,避免反复加锁和解锁
偏向锁
如果一个线程获得了锁,那么锁就进入了偏向锁模式,此时Mark Word的结构也变为偏向结构,当该线程再次请求锁的时候,无需做任何操作,即获得锁的过程只需检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作,减少了同一线程获取锁的代价。不适用于锁竞争比较激烈的多线程场合
轻量级锁
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁竞争的时候,偏向锁就会升级为轻量级锁。若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
与ReentrantLock区别
- synchronized是关键字,ReentrantLock是类
- ReentrantLock可以获取锁的时间进行设置,避免死锁
- ReentrantLock可以获取各种锁的信息
- ReentrantLock可以灵活实现多路通知
- 机制:sync操作Mark Word,lock调用Unsafe类的park()方法
- ReentrantLock,能够实现比synchronized更细粒度的控制,比如控制公平性
- ReentrantLock在调用lock()之后,必须调用unlock()释放锁。也是可重入的
- ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁
与volatile区别
synchronized 表示只有一个线程可以获取作用对象的锁,执行代码,阻塞其他线程
volatile 表示变量在 CPU 的寄存器中是不确定的,必须从主存中读取。保证多线程环境下变量的可见性;禁止指令重排序
- volatile 是变量修饰符;synchronized 可以修饰类、方法、变量
- volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性
- volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块
类加载过程
- 类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中类加载过程包括加载、验证、准备、解析和初始化五个阶段
各阶段详解
加载
- 简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例,这个Class对象在日后就会作为方法区中该类的各种数据的访问入口
链接
- 链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由 验证、准备 和 解析 三个阶段
- 验证
- 验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等
格式验证:验证是否符合class文件规范
语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明
操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证
- 准备
- 为类中的所有静态变量分配内存空间,并为其设置一个初始值
- 被final修饰的静态变量,会直接赋予原值;类字段的字段属性表中存在ConstantValue属性,则在准备阶段,其值就是ConstantValue的值
- 解析
- 将常量池中的符号引用转为直接引用,(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行
- 可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)
初始化
- 将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作
- 所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是
方法,即类/接口初始化方法。该方法的作用就是初始化一个中的变量,使用用户指定的值覆盖之前在准备阶段里设定的初始值。任何invoke之类的字节码都无法调用 方法,因为该方法只能在类加载的过程中由JVM调用 - 如果父类还没有被初始化,那么优先对父类初始化,但在
方法内部不会显示调用父类的 方法,由JVM负责保证一个类的 方法执行之前,它的父类 方法已经被执行 - JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程
双亲委派原则
- 当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委托给它的父类加载器去执行,每一层的类都采用相同的方式,直至委托给最顶层的启动类加载器为止
- 如果父类加载器无法加载委托给它的类,便将类的加载任务退回给下一级类加载器去执行加载
- 能够有效确保一个类的全局唯一性,当程序中出现多个限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类
- Java类随着它的类加载器一起具备了一种带有优先级的层次关系
- 实现双亲委托的代码都集中在java.lang.ClassLoader的loadClass()方法中
- 先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass方法进行加载。
并发编程
创建线程的四种方式
继承Thread类
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "run()方法正在执行--------->MyThread");
}
}
实现Runnable接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "run()方法正在执行--------->MyRunnable");
}
}
实现Callable接口
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "run()方法正在执行--------->Callable");
return 1;
}
}
使用Executors创建线程池
private static void executorTest(){
ExecutorService executorService = Executors.newCachedThreadPool();
MyRunnable myRunnable = new MyRunnable();
for (int i = 0; i < 10; i++) {
executorService.execute(myRunnable);
}
}
测试
MyThread myThread = new MyThread();
myThread.start();
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
MyCallable myCallable = new MyCallable();
FutureTask<Integer> task = new FutureTask<>(myCallable);
Thread callableThread = new Thread(task);
callableThread.start();
executorTest();
Runnable和Callable区别
相同点
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
不同点
- Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
- Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞
线程状态和基本操作
- 新建(new) 新创建了一个线程对象
- 可运行(runnable) 线程对象创建后,当调用线程对象的 start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权
- 运行(running) 可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中、
- 阻塞(block) 处于运行状态中的线程由于某种原因,暂时放弃对 CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被 CPU 调用以进入到运行状态
- 等待阻塞 运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列(waitting queue)中,使本线程进入到等待阻塞状态
- 同步阻塞 线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),,则JVM会把该线程放入锁池(lock pool)中,线程会进入同步阻塞状态
- 其他阻塞 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态
- 死亡(dead) 线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生
线程相关方法
- wait() 使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁
- sleep() 使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常
- notify() 唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关
- notityAll() 唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态
- yield() 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)
sleep和wait区别
- sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法
- sleep() 不释放锁;wait() 释放锁
- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行
- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒
sleep和yield区别
- sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会
- 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态
- sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常
- sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行
线程池
创建方式
- Executors(不推荐)
- newSingleThreadExecutor 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行
- newFixedThreadPool 创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool方法来创建线程池,这样能获得更好的性能
- newCachedThreadPool 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小
- newScheduledThreadPool 创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求
- ThreadPoolExecutor(推荐)
- 核心参数
- corePoolSize 核心线程数,表示最小可以同时运行的线程数量
- maximumPoolSize 线程池中允许存在的工作线程的最大数量
- workQueue 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中
- 其他常见参数
- keepAliveTime 线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁
- unit keepAliveTime 参数的时间单位
- threadFactory 为线程池提供创建新线程的线程工厂、
- handler 线程池任务队列超过 maxinumPoolSize 之后的拒绝策略
- 常用的拒绝策略
- AbortPolicy 抛出RejectedExecutionException来拒绝新任务的处理
- CallerRunsPolicy 调用执行自己的线程运行任务。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果应用程序可以承受此延迟并且不能丢弃任何一个任务请求的话,可以选择这个策略
- DiscardPolicy 不处理新任务,直接丢弃掉
- DiscardOldestPolicy 此策略将丢弃最早的未处理的任务请求
- 核心参数
实现原理图
5种状态
- RUNNING 这是最正常的状态,接受新的任务,处理等待队列中的任务
- SHUTDOWN 不接受新的任务提交,但是会继续处理等待队列中的任务。
- STOP 不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
- TIDYING 所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
- TERMINATED terminated()方法结束后,线程池的状态就会变成这个
单例
饿汉式 (非线程安全)
public class SimpleHungrySingleton {
// 私有的构造方法 初始化的时候直接常见实例对象
private static SimpleHungrySingleton instance = new SimpleHungrySingleton();
private SimpleHungrySingleton(){}
private static SimpleHungrySingleton getInstance() {
return instance;
}
}
懒汉式 (非线程安全)
public class SimpleLazySingleton {
// 私有的构造方法 获取的时候才取创建实例对象
private static SimpleLazySingleton instance;
private SimpleLazySingleton(){}
private static SimpleLazySingleton getInstance(){
if (instance == null) {
instance = new SimpleLazySingleton();
}
return instance;
}
}
懒汉式 (非线程安全)
public class OneCheckLazySingleton {
private static OneCheckLazySingleton instance;
private OneCheckLazySingleton(){}
// 在if为true执行完毕没有创建实例时cpu进行切换,另一个线程判断也为true并且创建了实例,完毕后cpu切换回来会创建另外的实例
private static synchronized OneCheckLazySingleton getInstance() {
if (instance == null) {
instance = new OneCheckLazySingleton();
}
return instance;
}
}
懒汉式 (线程安全)
public class DoubleCheckLazySingleton {
// volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行
private volatile static DoubleCheckLazySingleton instance;
private DoubleCheckLazySingleton() {}
// 双重检验保证线程安全,只创建一次
public static DoubleCheckLazySingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckLazySingleton.class) {
if (instance == null) {
instance = new DoubleCheckLazySingleton();
}
}
}
return instance;
}
}
静态内部类
public class StaticInnerClassSingleton {
// 在静态内部类中创建实例
private static class SingletonHolder {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
private StaticInnerClassSingleton () {}
public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
CAS实现的单例
class Singleton {
private static final AtomicInteger count = new AtomicInteger();
private Singleton() {}
private static Singleton singleton;
public static Singleton getInstance() {
for (;;) {
if (0 != count.get()) {
return singleton;
}
if (count.compareAndSet(0, 1)) {
singleton = new Singleton();
return singleton;
}
}
}
}
枚举(推荐)
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance() {
return INSTANCE;
}
}

浙公网安备 33010602011771号