java单例方法_Java单例模式
單例模式,是特別常見的一種設(shè)計(jì)模式,因此我們有必要對(duì)它的概念和幾種常見的寫法非常了解,而且這也是面試中常問的知識(shí)點(diǎn)。
所謂單例模式,就是所有的請(qǐng)求都用一個(gè)對(duì)象來處理,如我們常用的Spring默認(rèn)就是單例的,而多例模式是每一次請(qǐng)求都創(chuàng)建一個(gè)新的對(duì)象來處理,如structs2中的action。
使用單例模式,可以確保一個(gè)類只有一個(gè)實(shí)例,并且易于外部訪問,還可以節(jié)省系統(tǒng)資源。如果在系統(tǒng)中,希望某個(gè)類的對(duì)象只存在一個(gè),就可以使用單例模式。
那怎么確保一個(gè)類只有一個(gè)實(shí)例呢?
我們知道,通常我們會(huì)通過new關(guān)鍵字來創(chuàng)建一個(gè)新的對(duì)象。這個(gè)時(shí)候類的構(gòu)造函數(shù)是public公有的,你可以隨意創(chuàng)建多個(gè)類的實(shí)例。所以,首先我們需要把構(gòu)造函數(shù)改為private私有的,這樣就不能隨意new對(duì)象了,也就控制了多個(gè)實(shí)例的隨意創(chuàng)建。
然后,定義一個(gè)私有的靜態(tài)屬性,來代表類的實(shí)例,它只能類內(nèi)部訪問,不允許外部直接訪問。
最后,通過一個(gè)靜態(tài)的公有方法,把這個(gè)私有靜態(tài)屬性返回出去,這就為系統(tǒng)創(chuàng)建了一個(gè)全局唯一的訪問點(diǎn)。
以上,就是單例模式的三個(gè)要素。總結(jié)為:私有構(gòu)造方法
指向自己實(shí)例的私有靜態(tài)變量
對(duì)外的靜態(tài)公共訪問方法
單例模式分為餓漢式和懶漢式。它們的主要區(qū)別就是,實(shí)例化對(duì)象的時(shí)機(jī)不同。餓漢式,是在類加載時(shí)就會(huì)實(shí)例化一個(gè)對(duì)象。懶漢式,則是在真正使用的時(shí)候才會(huì)實(shí)例化對(duì)象。
餓漢式單例代碼實(shí)現(xiàn):
public class Singleton {
// 餓漢式單例,直接創(chuàng)建一個(gè)私有的靜態(tài)實(shí)例
private static Singleton singleton = new Singleton();
//私有構(gòu)造方法
private Singleton(){
}
//提供一個(gè)對(duì)外的靜態(tài)公有方法
public static Singleton getInstance(){
return singleton;
}
}
懶漢式單例代碼實(shí)現(xiàn)
public class Singleton {
// 懶漢式單例,類加載時(shí)先不創(chuàng)建實(shí)例
private static Singleton singleton = null;
//私有構(gòu)造方法
private Singleton(){
}
//真正使用時(shí)才創(chuàng)建類的實(shí)例
public static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
稍有經(jīng)驗(yàn)的程序員就發(fā)現(xiàn)了,以上懶漢式單例的實(shí)現(xiàn)方式,在單線程下是沒有問題的。但是,如果在多線程中使用,就會(huì)發(fā)現(xiàn)它們返回的實(shí)例有可能不是同一個(gè)。我們可以通過代碼來驗(yàn)證一下。創(chuàng)建十個(gè)線程,分別啟動(dòng),線程內(nèi)去獲得類的實(shí)例,把實(shí)例的 hashcode 打印出來,只要相同則認(rèn)為是同一個(gè)實(shí)例;若不同,則說明創(chuàng)建了多個(gè)實(shí)例。
public class TestSingleton {
public static void main(String[] args) {
for (int i = 0; i < 10 ; i++) {
new MyThread().start();
}
}
}
class MyThread extends Thread {
@Override
public void run() {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.hashCode());
}
}
/**
運(yùn)行多次,就會(huì)發(fā)現(xiàn),hashcode會(huì)出現(xiàn)不同值
668770925
668770925
649030577
668770925
668770925
668770925
668770925
668770925
668770925
668770925
*/
所以,以上懶漢式的實(shí)現(xiàn)方式是線程不安全的。那餓漢式呢?你可以手動(dòng)測(cè)試一下,會(huì)發(fā)現(xiàn)不管運(yùn)行多少次,返回的hashcode都是相同的。因此,認(rèn)為餓漢式單例是線程安全的。
那為什么餓漢式就是線程安全的呢?這是因?yàn)?#xff0c;餓漢式單例在類加載時(shí),就創(chuàng)建了類的實(shí)例,也就是說在線程去訪問單例對(duì)象之前就已經(jīng)創(chuàng)建好實(shí)例了。而一個(gè)類在整個(gè)生命周期中只會(huì)被加載一次。因此,也就可以保證實(shí)例只有一個(gè)。所以說,餓漢式單例天生就是線程安全的。(可以了解一下類加載機(jī)制)
既然懶漢式單例不是線程安全的,那么我們就需要去改造一下,讓它在多線程環(huán)境下也能正常工作。以下介紹幾種常見的寫法:
1) 使用synchronized方法
實(shí)現(xiàn)非常簡(jiǎn)單,只需要在方法上加一個(gè)synchronized關(guān)鍵字即可
public class Singleton {
private static Singleton singleton = null;
private Singleton(){
}
//使用synchronized修飾方法,即可保證線程安全
public static synchronized Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
這種方式,雖然可以保證線程安全,但是同步方法的作用域太大,鎖的粒度比較粗,因此,執(zhí)行效率就比較低。
2) synchronized 同步塊
既然,同步整個(gè)方法的作用域大,那我縮小范圍,在方法里邊,只同步創(chuàng)建實(shí)例的那一小部分代碼塊不就可以了嗎(因?yàn)榉椒ㄝ^簡(jiǎn)單,所以鎖代碼塊和鎖方法沒什么明顯區(qū)別)。
public class Singleton {
private static Singleton singleton = null;
private Singleton(){
}
public static Singleton getInstance(){
//synchronized只修飾方法內(nèi)部的部分代碼塊
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
return singleton;
}
}
這種方法,本質(zhì)上和第一種沒什么區(qū)別,因此,效率提升不大,可以忽略不計(jì)。
3) 雙重檢測(cè)(double check)
可以看到,以上的第二種方法只要調(diào)用getInstance方法,就會(huì)走到同步代碼塊里。因此,會(huì)對(duì)效率產(chǎn)生影響。其實(shí),我們完全可以先判斷實(shí)例是否已經(jīng)存在。若已經(jīng)存在,則說明已經(jīng)創(chuàng)建好實(shí)例了,也就不需要走同步代碼塊了;若不存在即為空,才進(jìn)入同步代碼塊,這樣可以提高執(zhí)行效率。因此,就有以下雙重檢測(cè)了:
public class Singleton {
//注意,此變量需要用volatile修飾以防止指令重排序
private static volatile Singleton singleton = null;
private Singleton(){
}
public static Singleton getInstance(){
//進(jìn)入方法內(nèi),先判斷實(shí)例是否為空,以確定是否需要進(jìn)入同步代碼塊
if(singleton == null){
synchronized (Singleton.class){
//進(jìn)入同步代碼塊時(shí)也需要判斷實(shí)例是否為空
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
需要注意的一點(diǎn)是,此方式中,靜態(tài)實(shí)例變量需要用volatile修飾。因?yàn)?#xff0c;new Singleton() 是一個(gè)非原子性操作,其流程為:
a.給 singleton 實(shí)例分配內(nèi)存空間
b.調(diào)用Singleton類的構(gòu)造函數(shù)創(chuàng)建實(shí)例
c.將 singleton 實(shí)例指向分配的內(nèi)存空間,這時(shí)認(rèn)為singleton實(shí)例不為空
正常順序?yàn)?a->b->c,但是,jvm為了優(yōu)化編譯程序,有時(shí)候會(huì)進(jìn)行指令重排序。就會(huì)出現(xiàn)執(zhí)行順序?yàn)?a->c->b。這在多線程中就會(huì)表現(xiàn)為,線程1執(zhí)行了new對(duì)象操作,然后發(fā)生了指令重排序,會(huì)導(dǎo)致singleton實(shí)例已經(jīng)指向了分配的內(nèi)存空間(c),但是實(shí)際上,實(shí)例還沒創(chuàng)建完成呢(b)。
這個(gè)時(shí)候,線程2就會(huì)認(rèn)為實(shí)例不為空,判斷 if(singleton == null)為false,于是不走同步代碼塊,直接返回singleton實(shí)例(此時(shí)拿到的是未實(shí)例化的對(duì)象),因此,就會(huì)導(dǎo)致線程2的對(duì)象不可用而使用時(shí)報(bào)錯(cuò)。
4)使用靜態(tài)內(nèi)部類
思考一下,由于類加載是按需加載,并且只加載一次,所以能保證線程安全,這也是為什么說餓漢式單例是天生線程安全的。同樣的道理,我們是不是也可以通過定義一個(gè)靜態(tài)內(nèi)部類來保證類屬性只被加載一次呢。
public class Singleton {
private Singleton(){
}
//靜態(tài)內(nèi)部類
private static class Holder {
private static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
//調(diào)用內(nèi)部類的屬性,獲取單例對(duì)象
return Holder.singleton;
}
}
而且,JVM在加載外部類的時(shí)候,不會(huì)加載靜態(tài)內(nèi)部類,只有在內(nèi)部類的方法或?qū)傩?此處即指singleton實(shí)例)被調(diào)用時(shí)才會(huì)加載,因此不會(huì)造成空間的浪費(fèi)。
5)使用枚舉類
因?yàn)槊杜e類是線程安全的,并且只會(huì)加載一次,所以利用這個(gè)特性,可以通過枚舉類來實(shí)現(xiàn)單例。
public class Singleton {
private Singleton(){
}
//定義一個(gè)枚舉類
private enum SingletonEnum {
//創(chuàng)建一個(gè)枚舉實(shí)例
INSTANCE;
private Singleton singleton;
//在枚舉類的構(gòu)造方法內(nèi)實(shí)例化單例類
SingletonEnum(){
singleton = new Singleton();
}
private Singleton getInstance(){
return singleton;
}
}
public static Singleton getInstance(){
//獲取singleton實(shí)例
return SingletonEnum.INSTANCE.getInstance();
}
}
總結(jié)
以上是生活随笔為你收集整理的java单例方法_Java单例模式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vue 前端商城框架_前端工程师要掌握几
- 下一篇: python22个字符串长度_pytho