Java并发JUC(十)

90 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第16天,点击查看活动详情
本文主要介绍JUC中的单例模式,以及饿汉式单例、懒汉式单例的一些改进与问题,以及最后提到了枚举单例。

19. 单例模式JUC

单例:构造器私有,别人无法new

饿汉式单例

//饿汉式单例:来个对象就接收
public class Hungry {
  
      // 可能会浪费空间
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];
  
    //构造器私有,别人无法new
    private Hungry(){
    }
    //先new一个对象(保证唯一)
    private final static Hungry HUNGRY = new Hungry();
    public static Hungry getInstance(){
        return HUNGRY;
    }
}

针对饿汉式单例不好的地方就是,来一个对象就接受。

懒汉式单例

下面这个是普通的懒汉式单例,懒汉式单例不会来一个对象就接收,他是在用的时候再加载。懒汉式单例在单线程的情况下是ok的,是单例的。但是在多线程的情况下,不见得就是单例的。根据这段程序的运行结果也知道它不是单例了。

public class LazyMan {
    private LazyMan(){
        System.out.println(Thread.currentThread().getName() + "ok");
    }
    private LazyMan lazyMan;
    //懒汉式单例
    public static LazyMan getInstance() {
                if(lazyMan == null){
                    lazyMan = new LazyMan();
                }
        return lazyMan;
    }
    //单线程下确实单例ok
    //多线程并发(单例不是单例,出现问题,要加锁,上面已经加锁)
    public static void main(String[] args) {
        for (int i = 1; i <=10 ; i++) {
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }
}

运行结果

Thread-0ok
Thread-1ok

针对上述情况的解决方式就是DCL懒汉式,双重检测锁模式的懒汉式单例。什么意思呢?就是需要针对下面这个方法进行修改如下:

  • 加锁验证:运行结果确实是单例了。但是这种在极端情况下也会有问题,原因就是lazyMan = new LazyMan();不是个原子性操作。非原子性会执行三步:1。分配内存空间2。执行构造方法,初始化对象3。把这个对象指向这个空间(可能发生指令重排),解决方式就是加一个volatile。
    public static LazyMan getInstance() {
        if(lazyMan == null){
            synchronized (LazyMan.class){
                if(lazyMan == null){
                    lazyMan = new LazyMan(); 
                }
            }
        }
        return lazyMan;
    }
private volatile static LazyMan lazyMan;

当然到这里也不是安全的,java反射就可以破坏这个单例。比如把测试代码改成这样:

    public static void main(String[] args) throws Exception {
        LazyMan instance = LazyMan.getInstance();
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        //单例又被破坏了
        LazyMan instance1 = declaredConstructor.newInstance();
      
        System.out.println(instance);//com.Juc.single.LazyMan@232204a1
        System.out.println(instance1);//com.Juc.single.LazyMan@4aa298b7
    }

程序运行结果:

com.Juc.single.LazyMan@232204a1
com.Juc.single.LazyMan@4aa298b7

很明显反射破坏了单例。解决方法就是在私有构造那里上锁:

 private LazyMan(){
​
        //解决反射破坏单例
        synchronized (LazyMan.class){
            if(lazyMan!=null){
                throw new RuntimeException("不要试用反射破坏异常");
            }
        }
//        System.out.println(Thread.currentThread().getName() + "ok");
    }

虽然解决了,但是改成下面这样又破坏了单例:

Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
//单例又被破坏了
LazyMan instance1 = declaredConstructor.newInstance();
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance1);//com.Juc.single.LazyMan@232204a1
System.out.println(instance2);//com.Juc.single.LazyMan@4aa298b7

就要用到枚举了,枚举的这个一定是单例;

//enum 本身是一个class类,jdk1。5版本就有
public enum EnumSingle {
    INSTANCE;//像这样一个对象默认就是单例,不可能被拿走
​
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}
​
class Test{
    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        EnumSingle instance2 = EnumSingle.INSTANCE;
​
//        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance3 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
        System.out.println(instance3);
​
    }
}