0x1、定义
0x2、单例写法的演进
① 饿汉式(没有懒加载,线程安全,常用)
public class Singleton () { private static Singleton instance = new Singleton(); private Singleton(){ } public static Singleton getInstance() { return instance; } }
- 优点:类装载(ClassLoader)时就完成实例化,避免线程同步问题,没加锁,执行效率高;
- 缺点:没有懒加载,即使没用到这个实例还是会加载;
② 懒汉式(懒加载,线程不安全,不推荐使用)
就是在饿汉式的基础上加了一个判空,调用getInstance()方法才初始化实例:
public class Singleton { private static Singleton instance = null; private Singleton() { } private static Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } }
虽然实现了懒加载,却存在线程安全问题,比如两个线程,都刚好走到判空,实例为空初始化,结果可能导致实例化了两个Singleton对象,破坏了单例,一种升级版的解决方式是加锁。
③ 升级版懒汉式(线程安全,但效率低,不推荐使用)
public class Singleton { private Singleton instance = null; private Singleton() { } public static synchronized Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } }
给getInstance()函数加锁,保证了线程安全,但也导致了函数的并发度很低,相当于串行操作,频繁调用此函数,会频繁地加锁、释放锁、效率太低。
而且,其实只需要在new的时候考虑线程同步就行了,所以改进后的DCL单例来了~
④ 懒汉式双重校验锁(DCL,线程安全,推荐使用)
public class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance == null) { synchronized(Singleton.class) { if(instance == null) { instance = new Singleton(); } } } return instance; } }
代码看似完美,但还存在最后一个问题:指令重排序
JVM在保证最终结果正确的情况下,可以不按照编码的顺序执行语句,尽可能地提高程序的性能。
创建一个对象,在JVM中会经过这三步:
- 1、为instance分配内存空间;
- 2、初始化instance对象;
- 3、将instance指向分配好的内存空间;
在这三步中,第2、3步有可能发生指令重排现象,导致对象的创建顺序变成了:1-3-2,多个线程在获取对象时,有可能获取到为初始化的instance对象对象,引起NPE异常。示例流程图如下所示:
而使用volatile关键字修饰变量,可以防止指令重排序(原理是内存屏障),使得指令执行顺序与程序指明顺序一致。
修改后的代码如下:
public class Singleton { private static volatile Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance == null) { synchronized(Singleton.class) { if(instance == null) { instance = new Singleton(); } } } return instance; } }
上面这个防止指令重排序又称**有序性
,接着说说可见性
**,即 每一时刻线程读取该变量的值都是内存中最新的值。
也可以这样理解:
volatile修饰的变量,在线程对其进行写入操作时,不会把值缓存到工作内存中,而是直接将修改后的值重新刷回主内存。而当处理器监控(嗅探)到其他线程中该变量在主内存中的内存地址发生变化时,会让这些线程重新到主内存中拷贝这个变量的最新值到工作内存中,而不是继续使用工作内存中的旧缓存。
未加volatile的简单代码示例如下:
public class JavaTest { public static void main(String[] args) { Test test = new Test(); test.start(); while (true){ if (test.isFlag()) { System.out.println("flag为true"); break; } } } } class Test extends Thread { private boolean flag = false; public boolean isFlag() { return flag; } @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag = " + flag); } }
运行后,程序只输出了一个flag = true,然后就死循环卡住了,不会输出:flag为true!原因是:
我们在子线程中修改了flag的值,但是主线程并不知道这个更改,使用的依旧是之前的旧值,所以会一直死循环。
而只要我们为flag添加volatile修饰,程序就能正常结束了:
除此之外为if(test.ifFlag())加上synchronized
锁也可以解决可见性问题~
线程在进入synchronized代码块前后,会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存称为副本,执行代码,将修改后的副本的值刷回主内存中,最后线程释放锁。
最后一点,volatile无法保证原子性(一次操作,要么完全成功,要么完全失败),比如下面的代码示例:
public class VolatileTest { public static volatile int count = 0; public static void increase() { count++; } public static void main(String[] args) { Thread[] threads = new Thread[20]; for(int i = 0; i < threads.length; i++) { threads[i] = new Thread(() -> { for(int j = 0; j < 1000; j++) { increase(); } }); threads[i].start(); } // 等待所有累加线程结束,此处>2的原因是idea执行用户代码时会创建一个监控线程Monitor // 可以调用 Thread.currentThread().getThreadGroup().list() 查看一番~ while (Thread.activeCount() > 2) { Thread.yield(); } System.out.println(count); } }
创建了20个线程,每个线程对变量count进行1000此自增,并发结果正常应该是20000,但实际运行过程中结果很多时候都不够20000,原因是count++这个自增操作不是院子操作。解决方法也很简单,要么加锁,要么使用原子类,如:AtomicInteger。
总结下就是:
volatile是JVM提供的一种最轻量级的同步机制,可看做轻量版的synchronized,但不保证原子性,如果是对共享变量进行多个线程的赋值而没有其他操作,那么可以用volatile来代替synchronized。