1. 程式人生 > 實用技巧 >[MySQL] PHP IP登入限制的實現

[MySQL] PHP IP登入限制的實現

技術標籤:設計模式設計模式java面試

單例設計模式

餓漢式單例

餓漢式單例是在類載入的時候就立即初始化,並且建立單例物件。絕對執行緒安全,線上
程還沒出現以前就是例項化了,不可能存在訪問安全問題。

優點:沒有加任何的鎖、執行效率比較高,在使用者體驗上來說,比懶漢式更好,絕對執行緒安全,線上程還沒出現以前就是例項化了,不可能存在訪問安全問題
缺點:類載入的時候就初始化,不管用與不用都佔著空間,浪費了記憶體,有可能佔著茅
坑不拉屎。

普通餓漢式單例

public class HungrySingleton {
    private static final HungrySingleton hungrySingleton =
new HungrySingleton(); private HungrySingleton(){} public static HungrySingleton getInstance(){ return hungrySingleton; } }

靜態餓漢式單例

public class HungryStaticSingleton {
    private static final HungryStaticSingleton hungrySingleton;
    static {
        hungrySingleton = new
HungryStaticSingleton(); } private HungryStaticSingleton(){} public static HungryStaticSingleton getInstance(){ return hungrySingleton; } }

餓漢式適用於物件較少的情況。

懶漢式單例

懶漢式單例的特點是:被外部類呼叫的時候內部類才會載入

普通懶漢式

單鎖

/懶漢式單例
//在外部需要使用的時候才進行例項化
public class LazySimpleSingleton {
    private LazySimpleSingleton
(){} //靜態塊,公共記憶體區域 private static LazySimpleSingleton lazy = null; public synchronized static LazySimpleSingleton getInstance(){ if(lazy == null){ lazy = new LazySimpleSingleton(); } return lazy; } }

雙重鎖

public class LazyDoubleCheckSingleton {
	private volatile static LazyDoubleCheckSingleton lazy = null;
	private LazyDoubleCheckSingleton(){}
	public static LazyDoubleCheckSingleton getInstance(){
		if(lazy == null){
			synchronized (LazyDoubleCheckSingleton.class){
				if(lazy == null){
				lazy = new LazyDoubleCheckSingleton();
                //1.分配記憶體給這個物件
                //2.初始化物件
                //3.設定 lazy 指向剛分配的記憶體地址
				}
			}
		}
		return lazy;
	}
}

雙重檢查模式,進行了兩次判斷,第一次是為了避免不要的示例,第二次是為了進行同步,避免執行緒安全問題。由於

lazy = new LazyDoubleCheckSingleton();物件的建立在jvm可能會進行排序,在多執行緒訪問下存在風險,使用volatile修飾的LazyDoubleCheckSingleton例項變數有效,解決改問題

總結

懶漢式單例存線上程安全的問題,在多執行緒情況下有一點機率建立不同的物件,

如何來使得懶漢式單例線上程環境下安全呢?

給getInstance()加上synchronized關鍵字,使這個方法變成執行緒同步方法。

但是用synchronized加鎖,線上程數量比較多情況下,如果CPU 分配壓力上升,會導致大批
量執行緒出現阻塞,從而導致程式執行效能大幅下降。

有沒有更好的解決方式?

—使用靜態內部類的方式

靜態內部類的方式建立單例

//這種形式兼顧餓漢式的記憶體浪費,也兼顧synchronized效能問題
//完美地遮蔽了這兩個缺點
public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton(){}

    //static 是為了使單例的空間共享,保證這個方法不會被重寫,過載
    //jvm優先載入靜態內部類,只加載一次,利用jvm底層執行邏輯,完美的避免了執行緒安全的問題
    public static final LazyInnerClassSingleton getInstance(){
        //在返回結果以前,一定會先載入內部類
        return LazyHolder.LAZY;
    }

    //內部類預設不載入,使用到了內部類才載入
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

這種形式兼顧餓漢式的記憶體浪費,也兼顧synchronized效能問題。內部類一定是要在方
法呼叫之前初始化,巧妙地避免了執行緒安全問題

反射破壞單例

但是,上述的構造方法只用private之外,沒有做任何處理,如果我們使用反射來呼叫其構造方法,然後,再呼叫getInstance()方法,應該就會兩個不同的例項。我們看一下測試程式碼

cpublic class LazyInnerClassSingletonTest {
    public static void main(String[] args) {
        try{
            //想搞進行破壞
            Class<?> clazz = LazyInnerClassSingleton.class;
            //通過反射拿到私有的構造方法
            Constructor c = clazz.getDeclaredConstructor(null);
            //強制訪問,跳過安全檢查,必須得從了
            c.setAccessible(true);
            //暴力初始化
            Object o1 = c.newInstance();
            //呼叫了兩次構造方法,相當於new了兩次,犯了原則性問題
            Object o2 = c.newInstance();

            System.out.println(o1 == o2);//執行結果為false
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

執行結果是false

很明顯,建立了的兩個不同的例項。現在我們在構造方法中進行一些限制,一旦重複建立,就直接丟擲異常,來看一下優化後的程式碼

public class LazyInnerClassSingleton {
    //預設使用LazyInnerClassGeneral的時候,會先初始化內部類
    //如果沒使用的話,內部類是不載入的
    private LazyInnerClassSingleton(){
        if(LazyHolder.LAZY != null){//防止反射暴力破壞單例
            throw new RuntimeException("不允許建立多個例項");
        }
    }
    //static 是為了使單例的空間共享,保證這個方法不會被重寫,過載
    //jvm優先載入靜態內部類,只加載一次,利用jvm底層執行邏輯,完美的避免了執行緒安全的問題
    public static final LazyInnerClassSingleton getInstance(){
        //在返回結果以前,一定會先載入內部類
        return LazyHolder.LAZY;
    }
    //內部類預設不載入,使用到了內部類才載入
    private static class LazyHolder{
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

再次執行的結果:

...
Caused by: java.lang.RuntimeException: 不允許建立多個例項
	at com.gupaoedu.vip.pattern.singleton.lazy.LazyInnerClassSingleton.<init>(LazyInnerClassSingleton.java:17)
	... 5 more

至此,得到的最好用的單例建立方式!!

序列破壞單例

有時候,創建出來的單例物件需要序列化寫到磁碟中,下次使用的時候,再從磁碟讀取到物件,反序列化為實體物件,反序列化後的物件會重新分配記憶體,即重新建立,違背了單例模式的初衷,來看一段程式碼

//序列化:把記憶體中的狀態通過轉換成位元組碼的形式,通過一個IO流,寫入到其他地方
//反序列化:將已經持久化的位元組碼內容,通過IO流的讀取,進而將讀取的內容轉換為Java物件,轉換的過程中需要重新建立物件new
public class SeriableSingleton implements Serializable {    
	public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}
    
    public static SeriableSingleton getInstance(){
        return INSTANCE;
    } 
}

編寫測試程式碼:

public class SeriableSingletonTest {
    public static void main(String[] args) {

        SeriableSingleton s1 = null;
        SeriableSingleton s2 = SeriableSingleton.getInstance();

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream("SeriableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SeriableSingleton)ois.readObject();
            ois.close();

            System.out.println(s1 == s2);//執行結果是false

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

執行結果是false

可以看出,反序列化後的物件和手動建立的物件是不一致的,例項化了兩次,違背了單例的設計初衷。

我們如何保證序列化的情況下也能夠實現單例?其
實很簡單,只需要增加readResolve()方法即可。來看優化程式碼:

public class SeriableSingleton implements Serializable {


    public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
    private SeriableSingleton(){}

    public static SeriableSingleton getInstance(){
        return INSTANCE;
    }

    private  Object readResolve(){
        return  INSTANCE;
    }

}

為什麼增加了readResolve()方法就能解決這個問題?

因為ObjectInputStream類的readObject()方法,呼叫了該類中的readOrdinaryObject()方法,通過反射找到一個無參的readResolve()方法,並且儲存下來,通過readResolve()方法返回例項,解決了單例被破壞的問題。但是實際上例項化了兩次,只不過新建立的物件沒有被返回而已。

註冊式單例

註冊式單例又稱為登記式單例,就是將每一個例項都登記到某一個地方,使用唯一的標識獲取例項。註冊式單例有兩種寫法:一種為容器快取,一種為列舉登記。

列舉登記

先來看列舉式單例的寫法,來看程式碼,建立EnumSingleton類:

public enum EnumSingleton {
    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

測試:

public static void main(String[] args) {
        try {
            EnumSingleton instance1 = null;

            EnumSingleton instance2 = EnumSingleton.getInstance();
            instance2.setData(new Object());

            FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");            			 ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance2);
            oos.flush();
            oos.close();
            
            FileInputStream fis = new FileInputStream("EnumSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            instance1 = (EnumSingleton) ois.readObject();
            ois.close();

            System.out.println(instance1.getData());
            System.out.println(instance2.getData());
            System.out.println(instance1.getData() == instance2.getData());

       }catch (Exception e){
            e.printStackTrace();
        }
    }

執行結果為true

為什麼?

反編譯EnumSingleton.java檔案得到

static 
{ 
	INSTANCE = new EnumSingleton("INSTANCE", 0); 
	$VALUES = (new EnumSingleton[] { INSTANCE }); 
}

列舉式單例在靜態程式碼塊中就給INSTANCE進行了賦值,是餓漢式單例的實現。

我們再來測試一些反射能否破壞單例

public static void main(String[] args) {
        try {
            Class clazz = EnumSingleton.class;
            Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
            c.setAccessible(true);
            EnumSingleton enumSingleton = (EnumSingleton)c.newInstance("123",666);

        }catch (Exception e){
            e.printStackTrace();
        }
    }

執行報錯:

java.lang.IllegalArgumentException: Cannot reflectively create enum objects

不能用反射來建立列舉型別

來看一下jdk的Constructor 的newInstance()方法原始碼:

  @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

可以看到 , 在newInstance()方法中做了強制性的判斷,如果修飾符是Modifier.ENUM列舉型別,直接丟擲異常。

容器快取

public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
    public static Object getInstance(String className){
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            } else {
                return ioc.get(className);
            }
        }
    }
}

來看看Spring中的容器式單例的實現程式碼:

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory { 
    /** Cache of unfinished FactoryBean instances: FactoryBean name --> BeanWrapper */ 		private final Map<String, BeanWrapper> factoryBeanInstanceCache = new ConcurrentHashMap<>(16);
    ...
} 

容器式寫法適用於建立例項非常多的情況,便於管理。但是,是非執行緒安全的。

ThreadLocal單例

執行緒單例的實現ThreadLocal:

ThreadLocal 不能保證其建立的物件是全域性唯一,但是能保證在單個執行緒中是唯一的,天生的執行緒安全。

我們來看程式碼:

public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
            new ThreadLocal<ThreadLocalSingleton>(){
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };

    private ThreadLocalSingleton(){}

    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}

測試程式碼:

class ExectorThread implements Runnable{
    @Override
    public void run() {
        ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance();
    }
}
public class ThreadLocalSingletonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
        
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());
    }
}

main中無論呼叫多少次,獲取到的例項都是同一個,都在兩個子執行緒中分別獲取到了不同的例項。

ThreadLocal是如果實現這樣的效果的呢?

我們知道上面的單例模式為了達到執行緒安全的目的,給方法上鎖,以時間換空間。ThreadLocal將所有的物件全部放在ThreadLocalMap中,為每個執行緒都提供一個物件,實際上是以空間換時間來實現執行緒間隔離的。