[MySQL] PHP IP登入限制的實現
單例設計模式
餓漢式單例
餓漢式單例是在類載入的時候就立即初始化,並且建立單例物件。絕對執行緒安全,線上
程還沒出現以前就是例項化了,不可能存在訪問安全問題。
優點:沒有加任何的鎖、執行效率比較高,在使用者體驗上來說,比懶漢式更好,絕對執行緒安全,線上程還沒出現以前就是例項化了,不可能存在訪問安全問題
缺點:類載入的時候就初始化,不管用與不用都佔著空間,浪費了記憶體,有可能佔著茅
坑不拉屎。
普通餓漢式單例
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中,為每個執行緒都提供一個物件,實際上是以空間換時間來實現執行緒間隔離的。