懒汉模式(线程不安全)
描述 :只有在需要实例化对象时才会去实例化,但是由于多线程环境下会出现多个线程同时获取到 singleton_1==null 的情况。此时多个线程会出现重复实例化,并持有不同实例对象。所以一般只在单线程环境下使用。
public class LazySingleton {
private static LazySingleton singleton = null;
private LazySingleton() {
System.out.println(this);
}
public static LazySingleton getInstance(){
if (singleton == null){
singleton = new LazySingleton();
}
return singleton;
}
public static void main(String[] args) {
//验证饿汉模式的线程安全性
for (int i = 0; i < 100; i++) {
new Thread(LazySingleton::getInstance).start();
}
}
}
懒汉模式(线程安全)
描述:为了解决懒汉模式下多个线程同时实例化的情况,可以在获取实例的方法加上 synchronized 关键字。但是会出现一个线程持有锁后,其它线程无法访问到该方法。其它线程也只有在前一个线程释放锁后才能去竞争这个锁。竞争到锁的线程可以访问该方法。也就造成了多线程只能串行去访问该方法。
严重影响了执行效率。
public class SyncLazySingleton {
private static SyncLazySingleton singleton = null;
private SyncLazySingleton() {
System.out.println(this);
}
public static synchronized SyncLazySingleton getInstance(){
if (singleton == null){
singleton = new SyncLazySingleton();
}
return singleton;
}
public static void main(String[] args) {
//验证饿汉模式的线程安全性
for (int i = 0; i < 100; i++) {
new Thread(SyncLazySingleton::getInstance).start();
}
}
}
饿汉模式
描述:通过 final 和 static 修饰类对象变量,确保 INSTANCE 在类被装载时就初始化,并无法被修改。不需要加锁就能保证多线程环境下只会有一个实例。但是类加载时就初始化,浪费内存。
public class HungerSingleton {
private static final HungerSingleton INSTANCE = new HungerSingleton();
private HungerSingleton() {
System.out.println(this);
}
public static HungerSingleton getInstance(){
return INSTANCE;
}
public static void main(String[] args) {
//验证饿汉模式的线程安全性
for (int i = 0; i < 100; i++) {
new Thread(HungerSingleton::getInstance).start();
}
}
}
Java标准库有一些类就是单例,例如Runtime这个类:
Runtime runtime = Runtime.getRuntime();
双检锁
描述:双检锁是对加锁后的懒汉模式改进。加入双重检查机制,可以在第一检查 singleton_4 == null 时才考虑后面操作加锁,避免了每个线程进入该方法都要持有锁的情况出现。在线程持有锁后为什么要再次检查对象 singleton_4 == null 呢?主要还是为了避免第一次检查时多个线程都走到了第一次检查 singleton_4 == null 的情况,但是有一个线程完成了实例化后,其他线程可以再次判断。
而 volatile 关键字对类对象变量的修饰是为了阻止指令重排序的情况出现。因为实例化对象的过程其实有步骤:
1.分配内存
2.初始化对象
3.将对象指向刚才分配的内存空间
其中第二步和第三步可能会出现指令重排序的情况,这就导致了对象变量指向了一个未初始化的内存空间。这种对象变量如果被其他线程获取后使用会出现异常。
public class DoubleCheckLockSingleton {
private static volatile DoubleCheckLockSingleton singleton = null;
private DoubleCheckLockSingleton() {
System.out.println(this);
}
public static DoubleCheckLockSingleton getInstance(){
if (singleton==null){
synchronized(DoubleCheckLockSingleton.class){
if (singleton == null){
singleton = new DoubleCheckLockSingleton();
}
}
}
return singleton;
}
public static void main(String[] args) {
//验证饿汉模式的线程安全性
for (int i = 0; i < 100; i++) {
new Thread(DoubleCheckLockSingleton::getInstance).start();
}
}
}
内部静态类
描述:和饿汉模式一样都是利用ClassLoader类加载机制来保证初始化时只有一个线程。不一样的是,这里的内部静态类只有在被调用时才会被装载。相比饿汉模式会节约内存。
public class StaticInnerClassSingleton {
private static StaticInnerClassSingleton singleton = null;
private StaticInnerClassSingleton() {
System.out.println(this);
}
private static class StaticInnerClassSingletonHolder{
private static final StaticInnerClassSingleton SINGLETON = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return StaticInnerClassSingletonHolder.SINGLETON;
}
public static void main(String[] args) {
//验证饿汉模式的线程安全性
for (int i = 0; i < 100; i++) {
new Thread(StaticInnerClassSingleton::getInstance).start();
}
}
}
以上5中单例模式无法避免通过反射来破坏单例;可以通过枚举来防止反射破坏单例;
枚举
描述:这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
public enum EnumSingleton {
SINGLETON;
EnumSingleton() {
System.out.println(this);
}
public static void main(String[] args) {
//验证饿汉模式的线程安全性
for (int i = 0; i < 100; i++) {
new Thread(()->{
EnumSingleton singleton = EnumSingleton.SINGLETON;
System.out.println(singleton);
}).start();
}
}
}
约定大于配置: Spring中的单例对象不是按照以上这些单例模式实现语法来实现的,因为这样会很麻烦,使用者也不希望自己写的每个类写成单例都这么麻烦。所以Spring一般是通过 BeanFactory 实例化后,放入 ConcurrentHashMap 进行管理,使用者一般不会去 new 一个新的实例;
关于单例优缺点的思考
优点:
- 一个对象在内存中只有一个实例,减少了内存的开销。
- 可以避免资源的多重占用(如:写文件操作)。
缺点:
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
- 要求生产唯一序列号。
- WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。