單例模式 Singleton
很多時(shí)候,我們對(duì)于某個(gè)類,是不需要有很多實(shí)例存在的。打個(gè)比方:1個(gè)教室里面有很多學(xué)生要喝水,但是只有1臺(tái)飲水機(jī),沒有必要給每個(gè)學(xué)生都安排1臺(tái)飲水機(jī)。
這個(gè)時(shí)候,我們就需要使用單例模式
一.餓漢式(最常用)
public class Singleton {
//1.final修飾的常量一般字母大寫
//2.自己內(nèi)部new出1個(gè)對(duì)象后 給構(gòu)造方法加private讓其他人無法new
private static final Singleton INSTANCE = new Singleton();
private Singleton(){};
//3.private修飾的 需要有1個(gè)返回方法
public static Singleton getInstance(){return INSTANCE;};
public static void main(String[] args) {
Singleton m1 = Singleton.getInstance();
Singleton m2 = Singleton.getInstance();
//驗(yàn)證 是不是 只會(huì)new出1個(gè)對(duì)象 如果地址相等 那么一定是同1個(gè)
System.out.println(m1 == m2);
}
}
餓漢式的優(yōu)點(diǎn):
1.類加載到內(nèi)存后,就實(shí)例化一個(gè)單例,而且JVM線程安全(因?yàn)镴VM保證每個(gè)class類 只會(huì)load到內(nèi)存一次 static變量是在類load后馬上進(jìn)行初始化的 所以它也只會(huì)初始化一次 多線程訪問也沒有關(guān)系)
2.簡單方便
private static final Singleton INSTANCE;
//可以把 static修飾符 變成static代碼塊
static {
INSTANCE = new Singleton();
}
...其他不變
二.懶漢式
懶漢式 :臨到用時(shí)方恨少 用到時(shí)才創(chuàng)建
public class Singleton2 {
//注意我們這個(gè)對(duì)象不能 定義為final 因?yàn)槎x了final 就是常量要進(jìn)行static初始化(要么用static修飾 要么用static靜態(tài)代碼塊)
private static Singleton2 INSTANCE;
private Singleton2(){
}
public static Singleton2 getInstance() {
//懶漢式 :臨到用時(shí)方恨少 用到時(shí)才創(chuàng)建
if(INSTANCE == null ){
INSTANCE = new Singleton2();
}
return INSTANCE;
}
public static void main(String[] args) {
//測試一下 是否獲取的是同一對(duì)象
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Singleton2.getInstance().hashCode());
}).start();
}
}
}
缺點(diǎn):多線程同時(shí)訪問時(shí) 線程不安全
public static Singleton2 getInstance() {
if(INSTANCE == null ){
INSTANCE = new Singleton2();
}
return INSTANCE;
}
假如第1個(gè)線程 調(diào)用了getInstance() 但是還沒有new Singleton2()
此時(shí)另外一個(gè)線程來了 它也會(huì)判斷if 有沒有INSTANCE 它此時(shí)是null的 那么第二個(gè)線程就會(huì)new一個(gè)Singletion2() 而第一個(gè)線程也繼續(xù)執(zhí)行 那么這個(gè)INSTANCE在兩個(gè)線程中 已經(jīng)不是同一個(gè)實(shí)例了
我們可以做個(gè)實(shí)驗(yàn) 在INSTACE = new Singleton2()之前 讓線程sleep一會(huì)兒
if(INSTANCE == null ){
try {
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE = new Singleton2();
}
return INSTANCE;

三.加鎖版
為了解決懶漢式線程不安全的問題,我們可以在調(diào)用getInstace()前,給方法加上鎖
public class Singleton3 {
private static Singleton3 INSTANCE;
private Singleton3(){
}
public static synchronized Singleton3 getInstance(){
if(INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Singleton3();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Singleton3.getInstance().hashCode());
}).start();
}
}
}
缺點(diǎn):
1.內(nèi)存中的對(duì)象 大得多比原來的
2.用的時(shí)候要看 有沒有加鎖,有沒有申請(qǐng)到這把鎖 效率降低
四.1個(gè)錯(cuò)誤的單例模式
想減少鎖住的范圍,提高效率,但是卻讓線程不安全
public class Singleton4 {
private static Singleton4 INSTANCE;
private Singleton4(){
}
public static Singleton4 getInstance() {
if(INSTANCE == null ){
synchronized (Singleton4.class) {
INSTANCE = new Singleton4();
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Singleton4.getInstance().hashCode());
}).start();
}
}
}
這種方法不能做到只new出來1個(gè)實(shí)例:因?yàn)楫?dāng)?shù)?個(gè)線程 執(zhí)行到 synchronized之前時(shí),第2個(gè)線程 往下執(zhí)行完了,申請(qǐng)到了鎖,并且new出來對(duì)象Singleton4;然后第2個(gè)線程釋放鎖,第1個(gè)進(jìn)程進(jìn)入synchronized繼續(xù)執(zhí)行,它又new出來了1個(gè)Singleton4對(duì)象
本質(zhì)原因:就是因?yàn)?它對(duì)象的判空if(INSTANCE == null) 沒有和創(chuàng)建對(duì)象INSTANCE = new Singleton4(); 同時(shí)操作 只要有1個(gè)時(shí)間間隙 就會(huì)出現(xiàn) 不同的對(duì)象 就會(huì)線程不安全 必須在INSTANCE==null的前提條件 去new Singleton4才可以
五.雙重判斷/檢查法
我們?cè)趎ew Singleton前 我們?cè)倥锌?次 就行了
public class Singleton5 {
private static Singleton5 INSTANCE;
private Singleton5(){
}
public static Singleton5 getInstance(){
if(INSTANCE == null){
synchronized (Singleton5.class){
//雙重檢查
if(INSTANCE == null ){
INSTANCE = new Singleton5();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Singleton4.getInstance().hashCode());
}).start();
}
}
}
為什么要判斷兩次?第一次可以不可以去掉?
if(INSTANCE == null){
synchronized (Singleton5.class){
其實(shí)有必要 第一次的判斷 因?yàn)檫@樣會(huì)省很多事 不然很多情況都要被上鎖 有很多情況 一旦INSTANCE被初始化后 進(jìn)來看到不為空后 它就不會(huì)繼續(xù)執(zhí)行下去了 提高效率
六.靜態(tài)內(nèi)部類方法
public class Singleton6 {
//給構(gòu)造方法加上private是單例模式的標(biāo)配了
private Singleton6(){
}
//加載外部類時(shí)不會(huì)加載內(nèi)部類,這樣可以實(shí)現(xiàn)懶加載
private static class Singleton6Holder {
//只有它的內(nèi)部類在內(nèi)部 才可以訪問 外部類訪問不了
private final static Singleton6 INSTANCE = new Singleton6();
}
public static Singleton6 getInstance() {
return Singleton6Holder.INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Singleton6.getInstance().hashCode());
}).start();
}
}
}
加載外部類時(shí)不會(huì)加載內(nèi)部類,這樣可以實(shí)現(xiàn)懶加載,只有當(dāng)我們調(diào)用 getInstance()的時(shí)候 才會(huì)被加載,它是線程安全的,因?yàn)镴VM只加載class一次
這比上面幾種都好
七.枚舉類(Java創(chuàng)始人的寫法)
public enum Singleton7 {
INSTANCE;
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
System.out.println(Singleton7.INSTANCE.hashCode());
}).start();
}
}
}
這樣不僅可以解決線程不同步 還可以反序列化(因?yàn)閖ava 反射可以通過class文件 把整個(gè)class load到內(nèi)存 new出1個(gè)實(shí)例 前面的寫法 他都可以找到你的class文件 來new 1個(gè)INSTANCE出來 通過 反序列化的方式 而枚舉單例 不會(huì)被反序列化 它只是一個(gè)抽象類,因?yàn)槊杜e類 沒有構(gòu)造方法 就算你拿到他的class文件 也沒有辦法 構(gòu)造他的對(duì)象,返回的只是1個(gè)值 返回的是單例創(chuàng)建的1個(gè)對(duì)象)
缺點(diǎn):本來你是一個(gè)resource manger的類 但是你定義為枚舉 有點(diǎn)別扭