彻底搞懂单例模式
作者:互联网
彻底搞懂单例模式
一、普通单例模式
饿汉式与懒汉式
1、饿汉式
public class SingleTonDemo {
private final static SingleTonDemo singletonDemo=new SingleTonDemo();
private SingleTonDemo(){
}
public static SingleTonDemo getInstance(){
return singletonDemo;
}
}
2、懒汉式
public class SingleTonDemo {
private static SingleTonDemo singleTonDemo;
private SingleTonDemo(){
}
public static SingleTonDemo getInstance(){
if (singleTonDemo==null){
singleTonDemo=new SingleTonDemo();
}
return singleTonDemo;
}
}
以上就是基本的单例模式 饿汉式和懒汉式,可以发现,饿汉式在定义属性的时候就创建对象了,相当于在类一创建的时候就创建对象,因此效率较低。而懒汉式是在调用**getInstance()**方法的时候才创建对象,能够更好的节省计算资源。
但是饿汉式相对于懒汉式,饿汉式是线程安全的。
(1)饿汉模式线程安全
public class SingleTonDemo {
private final static SingleTonDemo singletonDemo=new SingleTonDemo();
private SingleTonDemo(){
System.out.println(Thread.currentThread().getName()+"-->"+"create object finish");
}
public static SingleTonDemo getInstance(){
return singletonDemo;
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
getInstance();
},"thread"+i).start();
}
}
}
构造方法中加一个输出,输出线程名称,发现只有main线程创建了该对象。
因此该对象只被创建了一次,因此在创建对象时,饿汉模式是安全的,他不会多创建对象。而懒汉模式,如果创建对象时两个线程同时进入构造器,则可能会创建两个对象。
(2)懒汉式线程不安全
public class SingleTonDemo {
private static SingleTonDemo singleTonDemo;
private SingleTonDemo(){
System.out.println(Thread.currentThread().getName()+"create object finish");
}
public static SingleTonDemo getInstance(){
if (singleTonDemo==null){
singleTonDemo=new SingleTonDemo();
}
return singleTonDemo;
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
SingleTonDemo instance = getInstance();
}).start();
}
}
}
几次运行的结果都不一样,有时候创建了一个对象,有时候创建了多个对象,这就说明懒汉式是线程不安全的。
二、线程安全的懒汉式模式
1、双重检查锁的懒汉式
public class SingleTonDemo {
//为了方式重新编排导致的小概率错误,使用volatile关键字
private volatile static SingleTonDemo singleTonDemo;
private SingleTonDemo(){
System.out.println(Thread.currentThread().getName()+"create object finish");
}
public static SingleTonDemo getInstance(){
//双重检查锁
if (singleTonDemo==null){
synchronized (SingleTonDemo.class){
if (singleTonDemo==null){
singleTonDemo=new SingleTonDemo();
}
}
}
return singleTonDemo;
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
SingleTonDemo instance = getInstance();
}).start();
}
}
}
测了好几次,都是只有一个对象被创建。
双重检查锁顾名思义就是两个判断,加锁之前先看看对象有没有被其他线程创建,没有的话加上同步代码块,再判断判断对象有没有被其他线程创建,没有的话才创建对象。
2、静态内部类
public class SingleTonDemo {
private SingleTonDemo(){
System.out.println(Thread.currentThread().getName());
}
private static class Singleton{
private volatile static SingleTonDemo single = new SingleTonDemo();
}
public static SingleTonDemo getInstance(){
return Singleton.single;
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(()->{
getInstance();
}).start();
}
}
}
静态内部类的getInstance方法没有被同步,只是把类的加载给延迟了,这样既不是类一创建就创建对象,也不用加锁,内部类是一个静态的,主类被创建的时候,他就加载了,但是内部的构造方法并没有被加载,需要被调用的时候才加载,因此当线程访问内部类时,就可以创建主类对象了,而且内部类是一个类,他把主类对象作为他的属性,就被创建了一次,内部类内部相当于是一个饿汉模式,这样可以很大的节省资源。
以上两种方法虽然是线程安全的,但是太容易被反射修改对象内容,存在安全问题。
public class SingleTonDemo {
//为了方式重新编排导致的小概率错误,使用volatile关键字
private volatile static SingleTonDemo singleTonDemo;
private SingleTonDemo(){
System.out.println(Thread.currentThread().getName()+"create object finish");
}
public static SingleTonDemo getInstance(){
//双重检查锁
if (singleTonDemo==null){
synchronized (SingleTonDemo.class){
if (singleTonDemo==null){
singleTonDemo=new SingleTonDemo();
}
}
}
return singleTonDemo;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
SingleTonDemo instance1 = SingleTonDemo.getInstance();
Constructor<SingleTonDemo> declaredConstructor = SingleTonDemo.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
SingleTonDemo instance2=declaredConstructor.newInstance();
System.out.println("singleTonDemo1 = " + instance1);
System.out.println("singleTonDemo2 = " + instance2);
}
}
上图可以看出,通过反射,对象已经被更改了。
public class SingleTonDemo {
//为了方式重新编排导致的小概率错误,使用volatile关键字
private volatile static SingleTonDemo singleTonDemo;
private SingleTonDemo(){
if (singleTonDemo!=null){
throw new RuntimeException("不要通过反射篡改对象");
}else {
System.out.println(Thread.currentThread().getName()+"create object finish");
}
}
public static SingleTonDemo getInstance(){
//双重检查锁
if (singleTonDemo==null){
synchronized (SingleTonDemo.class){
if (singleTonDemo==null){
singleTonDemo=new SingleTonDemo();
}
}
}
return singleTonDemo;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
SingleTonDemo instance1 = SingleTonDemo.getInstance();
Constructor<SingleTonDemo> declaredConstructor = SingleTonDemo.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
SingleTonDemo instance2=declaredConstructor.newInstance();
System.out.println("singleTonDemo1 = " + instance1);
System.out.println("singleTonDemo2 = " + instance2);
}
}
可以将双重检测升级成三重检测,每当构造对象的时候判断一下,对象是否为空,如不为空,说明已经被篡改了,然后抛出异常。
但是上面的方法是基于正常方式创建过一次对象了才有用,如果侵入者两次都是用反射创建对象,改方法就没用了。
反射获取构造器,创建对象是直接通过构造器创建对象,如果在反射之前已经通过正常方式创建了对象,则getInstance方法会把对象中的SingleTonDemo singleTonDemo赋值,此时SingleTonDemo singleTonDemo就不再时null了,当采用反射通过构造器创建对象时,因为SingleTonDemo singleTonDemo已经有值了 因此会满足判断singleTonDemo!=null从而抛出异常,但是如果一开始就不用getInstance创建对象,那么singleTonDemo属性也不会有值,所有后面用反射通过构造器创建对象是可以创建出来的。。。。。。道高一尺魔高一丈,因此双重锁和静态内部类是不能避免反射的。
当然如果实在想用双重锁和静态内部类,可以定义一个属性,通过加密,保护这个属性的属性名,不被知道,然后再同步代码块中再加一个判断。这里就不加演示了。。。。因为总有方法可以破解你的属性名,还是不够安全。
public class SingleTonDemo {
//为了方式重新编排导致的小概率错误,使用volatile关键字
private volatile static SingleTonDemo singleTonDemo;
private SingleTonDemo(){
if (singleTonDemo!=null){
throw new RuntimeException("不要通过反射篡改对象");
}else {
System.out.println(Thread.currentThread().getName()+"create object finish");
}
}
public static SingleTonDemo getInstance(){
//双重检查锁
if (singleTonDemo==null){
synchronized (SingleTonDemo.class){
if (singleTonDemo==null){
singleTonDemo=new SingleTonDemo();
}
}
}
return singleTonDemo;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<SingleTonDemo> declaredConstructor = SingleTonDemo.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
SingleTonDemo instance1=declaredConstructor.newInstance();
SingleTonDemo instance2=declaredConstructor.newInstance();
System.out.println("singleTonDemo1 = " + instance1);
System.out.println("singleTonDemo2 = " + instance2);
}
}
为了解决反射修改属性的问题,可以采用枚举类方式的懒汉模式。
3、枚举类
点进newInstance()方法,发现
public enum EnumSingle{
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
测试:
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance1=EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2=declaredConstructor.newInstance();
}
}
报错,没有空参构造器??? ,正常应该报错反射不能用于枚举类才对。。。。
javap -p 类名.class 可以将字节码转换成代码
1.用jd-gui
终于用jd-gui发现确实没有空参构造器。
2.用jad
a.用jad把jad.exe复制到字节码所在目录下
b.然后在路径处输入cmd,进入命令行
c.在命令行输入 jad -sjava 类名.class
d.回到字节码所在的目录发现了生成的java文件
e.打开查看代码
因此改造代码
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance1=EnumSingle.INSTANCE;
//换成有参构造器
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(
String.class,int.class
);
declaredConstructor.setAccessible(true);
EnumSingle instance2=declaredConstructor.newInstance();
}
}
=EnumSingle.INSTANCE;
//换成有参构造器
Constructor declaredConstructor = EnumSingle.class.getDeclaredConstructor(
String.class,int.class
);
declaredConstructor.setAccessible(true);
EnumSingle instance2=declaredConstructor.newInstance();
}
}
[外链图片转存中...(img-jeURGa1H-1608621603977)]
报这个错就正常了,说明反射确实不能操作枚举类。
标签:SingleTonDemo,getInstance,彻底,class,static,单例,搞懂,singleTonDemo,public 来源: https://blog.csdn.net/m0_46473771/article/details/111556630