单例模式
单例模式,即单个实例,只有一个实例
1、饿汉式
饿汉模式,可以想象一个很饿的人,需要立马吃东西,饿汉模式便是这样,在类加载时就创建对象,由于在类加载时就创建单例,因此不存在线程安全问题。反射可破坏。
public class HungryDemon {
private static final HungryDemon hungry = new HungryDemon();
private HungryDemon() {
}
public static HungryDemon getInstance(){
return hungry;
}
}
// 测试
class HungryTest{
public static void main(String[] args) {
HungryDemon instance1 = HungryDemon.getInstance();
HungryDemon instance2 = HungryDemon.getInstance();
System.out.println(instance1.equals(instance1)); // true,说明实例一样
}
}
但饿汉式也存在一定的问题,即如果在该类里面存在大量开辟空间的语句,如很多数组或集合,但又不马上使用他们,这时这样的单例模式会消耗大量的内存,影响性能。
2、懒汉式
顾名思义,懒汉式,就是懒,即在类加载时并不会立马创建单例对象,而是只生成一个单例的引用,即可以延时加载。
单线程懒汉式:
public class LazyDemon {
private static LazyDemon lazy;
private LazyDemon() {}
public static LazyDemon getInstance(){
if(lazy == null){
lazy = new LazyDemon();
}
return lazy;
}
}
// 测试
class LazyTest{
public static void main(String[] args) {
LazyDemon instance1 = LazyDemon.getInstance();
LazyDemon instance2 = LazyDemon.getInstance();
System.out.println(instance1.equals(instance2)); // true
}
}s
该懒汉式,在单线程下是安全的,但是在多线程之下是不安全的。
比如:
public class LazyDemon {
private static LazyDemon lazy;
private LazyDemon() {
System.out.println(Thread.currentThread().getName()+" OK");
}
public static LazyDemon getInstance(){
if(lazy == null){
lazy = new LazyDemon();
}
return lazy;
}
}
// 测试
class LazyTest{
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyDemon.getInstance();
}).start();
}
}
}
/* 输出结果:
Thread-0 OK
Thread-2 OK
Thread-1 OK
// 可见,这次执行至少创建了3个实例,就不是单实例了
**解决:**可通过加Lock锁解决,也可以通过synchronized去解决,但是效率低。
// 在 LazyDemon 方法上加上了 synchronized 关键字
public static synchronized LazyDemon getInstance(){
if(lazy == null){
lazy = new LazyDemon();
}
return lazy;
}
/* 输出结果:
Thread-0 OK 永远只有一个单实例*/
// 在 LazyDemon 方法上加上了 Lock 锁
private static Lock lock = new ReentrantLock();
public static LazyDemon getInstance(){
lock.lock();
try{
if(lazy == null){
lazy = new LazyDemon();
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
return lazy;
}
}
/* 输出结果:
Thread-0 OK 永远只有一个单实例*/
多线程安全懒汉式
public class LazyDemon {
private static LazyDemon lazy;
private LazyDemon() {
System.out.println(Thread.currentThread().getName()+" OK");
}
public static synchronized LazyDemon getInstance(){
if(lazy == null){
lazy = new LazyDemon();
}
return lazy;
}
}
class LazyTest{
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyDemon.getInstance();
}).start();
}
}
}
3、DCL懒汉式
DCL懒汉(双重监测懒汉式),同样是在类加载时只提供一个引用,不会直接创建单例对象,不需要对整个方法进行同步,缩小了锁的范围,只有第一次会进入创建对象的方法,提高了效率。
public class DCLLazyDemon {
private static volatile DCLLazyDemon dclLazy; // 1
private DCLLazyDemon(){
System.out.println(Thread.currentThread().getName()+ " OK");
};
public static DCLLazyDemon getInstance(){
if (dclLazy == null){
synchronized (DCLLazyDemon.class){
if(dclLazy==null){
dclLazy = new DCLLazyDemon();
}
}
}
return dclLazy;
}
}
// 测试
class DCLLazyTest{
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
DCLLazyDemon.getInstance();
}).start();
}
}
}
1号代码为什么要加volatile关键字?
在极端情况时,会出现问题。可能会出现重排问题。在new一个实例时,不是原子性操作,创建实例分为
1.分配内存空间
2.执行构造方法
3.将空间地址复制给变量
以上3步执行完成之后,才创建完整的一个实例。
可能当底层重排的时候,执行顺序为132。以这个顺序执行到3,但还没来得及执行2还没来得及将对象初始化,这时又来了一个线程在执行这个方法,此刻dclLazy就已经不是null了,但是dclLazy引用的是空间是空的,该空间是没有任何东西的,这时这个线程返回的对象就是不存在的。因此就会出现问题。为了解决该问题,需要在1号代码上加volatile关键字。private static volatile DCLLazyDemon dclLazy;
通过反射破坏单实例
第1种
public class DCLLazyDemon {
private static volatile DCLLazyDemon dclLazy;
private DCLLazyDemon(){
System.out.println(Thread.currentThread().getName()+ " OK");
};
public static DCLLazyDemon getInstance(){
if (dclLazy == null){
synchronized (DCLLazyDemon.class){
if(dclLazy==null){
dclLazy = new DCLLazyDemon();
}
}
}
return dclLazy;
}
}
class DCLLazyTest{
public static void main(String[] args) throws Exception {
Constructor<DCLLazyDemon> constructor = DCLLazyDemon.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
DCLLazyDemon instance1 = constructor.newInstance();
DCLLazyDemon instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
/*输出:
main OK
main OK
singletonmode.DCLLazyDemon@74a14482
singletonmode.DCLLazyDemon@1540e19d
可见被创建了2个实例,这里是反射通过构造器去创建对象的,因此可以给构造器一个信号量*/
解决:
public class DCLLazyDemon {
private static volatile DCLLazyDemon dclLazy;
private static boolean flag = false; // 1
private DCLLazyDemon(){
synchronized (DCLLazyDemon.class){
if(flag == false){ // 2
flag = true; // 如果为false,设置为true并初始化对象
}else {
throw new RuntimeException("不要使用反射来破坏");
}
}
};
public static DCLLazyDemon getInstance(){
if (dclLazy == null){
synchronized (DCLLazyDemon.class){
if(dclLazy==null){
dclLazy = new DCLLazyDemon();
}
}
}
return dclLazy;
}
}
class DCLLazyTest{
public static void main(String[] args) throws Exception {
Constructor<DCLLazyDemon> constructor = DCLLazyDemon.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
DCLLazyDemon instance1 = constructor.newInstance();
DCLLazyDemon instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
/*输出:
Caused by: java.lang.RuntimeException: 不要使用反射来破坏
通过添加代码1,修改代码2。当通过反射来创建多个实例,会得到异常抛出。*/
第2种
通过上一种的升级,但还是可能会出现安全问题。尽管信号变量通过加密成了一个很难破解的变量,但是依旧会有被破解的可能。如果被破解知道了信号量,那么安全问题如下:
public class DCLLazyDemon {
private static volatile DCLLazyDemon dclLazy;
private static boolean flag = false;
private DCLLazyDemon(){
synchronized (DCLLazyDemon.class){
if(flag == false){
flag = true;
}else {
throw new RuntimeException("不要使用反射来破坏");
}
}
};
public static DCLLazyDemon getInstance(){
if (dclLazy == null){
synchronized (DCLLazyDemon.class){
if(dclLazy==null){
dclLazy = new DCLLazyDemon();
}
}
}
return dclLazy;
}
}
class DCLLazyTest{
public static void main(String[] args) throws Exception {
Constructor<DCLLazyDemon> constructor = DCLLazyDemon.class.getDeclaredConstructor(null);
// 如果获取到了信号量的变量,就可通过反射获取该变量
Field flag = DCLLazyDemon.class.getDeclaredField("flag");
// 并设置该变量不进行安全监测
flag.setAccessible(true);
constructor.setAccessible(true);
DCLLazyDemon instance1 = constructor.newInstance();
// 在第一个实例创建完成之后,将信号量的值更改为true,又可以创建一个实例
flag.set(instance1,false);
DCLLazyDemon instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
/*输出:
singletonmode.DCLLazyDemon@677327b6
singletonmode.DCLLazyDemon@14ae5a5
创建了2个实例*/
如何解决该问题呢?
通过反射的分析newInstance()源码
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
得出,枚举不能被反射创建。因此使用单例枚举式
4、静态内部类
使用静态内部类在类加载器加载的时候static就已经被创建,解决了线程安全问题,并实现了延时加载。可被反射破坏。
public class StaticClass {
private static StaticClass staticClass;
private StaticClass(){
System.out.println(Thread.currentThread().getName() + " OK");
};
private static class StaticClassIn{
private static final StaticClass instance = new StaticClass();
}
public static StaticClass getInstance(){
return StaticClassIn.instance;
}
}
// 测试
class StaticClassTest{
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
StaticClass.getInstance();
}).start();
}
}
}
5、枚举
public enum EnumDemon {
INSTANCE;
public static EnumDemon getInstance(){
return INSTANCE;
}
}
class EnumTest{
public static void main(String[] args) throws Exception{
EnumDemon instance1 = EnumDemon.getInstance();
EnumDemon instance2 = EnumDemon.getInstance();
System.out.println(instance1.equals(instance2)); // true
}
}
尝试反射获取枚举实例
通过反射的分析newInstance()源码
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
得出,枚举不能被反射创建。
第一种尝试
【查看枚举的构造器】
通过查看IDEA生成的class文件:
通过javap -p命令对class文件进行反编译:
得知,枚举其实是一个final类继承了枚举类。
这里得知枚举的构造器为空构造器。
public enum EnumDemon {
INSTANCE;
public static EnumDemon getInstance(){
return INSTANCE;
}
}
class EnumTest{
public static void main(String[] args) throws Exception{
Constructor<EnumDemon> constructor = EnumDemon.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
EnumDemon enumDemon = constructor.newInstance();
System.out.println(enumDemon);
}
}
输出:
//Exception in thread "main" java.lang.NoSuchMethodException:
// singletonmode.EnumDemon.<init>()
说明这构造器是假的。尝试失败!
第二种尝试
通过jad工具反编译class字节码文件,会生成一个java文件:
通过jad反编译生成的java文件,可以看到构造器是有参数的。这才是枚举的真正构造器。
public enum EnumDemon {
INSTANCE;
public static EnumDemon getInstance(){
return INSTANCE;
}
}
class EnumTest{
public static void main(String[] args) throws Exception{
// 通过查看到的真正构造器的参数,进行反射获取实例
Constructor<EnumDemon> constructor = EnumDemon.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumDemon enumDemon = constructor.newInstance();
System.out.println(enumDemon);
}
}
输出:
Exception in thread "main" java.lang.IllegalArgumentException:
// Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at singletonmode.EnumTest.main(EnumDemon.java:30)
看到了源码的那句话,Cannot reflectively create enum objects
证实了枚举单例不可通过反射手段获取实例。尝试失败!
总结
- 饿汉式:线程安全(反射可破坏),调用效率高,不能延时加载
- 懒汉式:线程安全(反射可破坏),调用效率不高,可以延时加载
- DCL懒汉式:线程安全(反射可破坏)。由于JVM底层模型原因,偶尔出现问题,不建议使用。
- 静态内部类式:线程安全(反射可破坏),调用效率高,可以延时加载
- 枚举单例:线程安全,调用效率高,不能延时加载