某公司老板在招程序員時承諾幫助解決單身問題,給程序員分配一個女朋友,于是單身的小強毫不猶豫就去應(yīng)聘了,并被順利錄用。那么我們怎么用代碼來模擬一下呢?首先定義一個女朋友的類,擁有兩個屬性,姓...
本文經(jīng)授權(quán)轉(zhuǎn)自公眾號 程序員修煉(ID:lixing2457)
作者:靜幽水
如若轉(zhuǎn)載請聯(lián)系原公眾號
01
問題背景
某公司老板在招程序員時承諾幫助解決單身問題,給程序員分配一個女朋友,于是單身的小強毫不猶豫就去應(yīng)聘了,并被順利錄用。那么我們怎么用代碼來模擬一下呢?首先定義一個女朋友的類,擁有兩個屬性,姓名和年齡:
public class GirlFriend {
private String name;
private Integer age;
public GirlFriend(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "GirlFriend{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
接著程序員小強就可以new出來一個女朋友的實例了,只需要傳進去姓名和年齡就可以了,如下:
public class Programmer {
public static void main(String[] args){
GirlFriend girlFriend = new GirlFriend("小美",20);
System.out.println(girlFriend.toString());
}
}
打印出的結(jié)果是GirlFriend{name='小美', age=20}
02
有何問題
突然有一天,程序員小強已經(jīng)不滿足只有一個女朋友了,于是他私自new出了多個女朋友對象出來,如下:
public class Programmer {
public static void main(String[] args){
GirlFriend girlFriend = new GirlFriend("小美",20);
GirlFriend girlFriend2 = new GirlFriend("小紅",18);
GirlFriend girlFriend3 = new GirlFriend("小麗",19);
System.out.println(girlFriend.toString());
System.out.println(girlFriend2.toString());
System.out.println(girlFriend3.toString());
}
}
打印結(jié)果如下:
GirlFriend{name='小美', age=20}
GirlFriend{name='小紅', age=18}
GirlFriend{name='小麗', age=19}
但是不久就被老板發(fā)現(xiàn)了,因為內(nèi)存中存在多個女朋友實例對象,嚴重浪費了公司的資源,老板決定只能給小強分配一個女朋友,老板絞盡腦汁,終于想出了應(yīng)對方法。
03
解決方法
老板發(fā)現(xiàn),問題的根源就是不能把創(chuàng)造女朋友的權(quán)限交給小強,應(yīng)該給他一個創(chuàng)造好的對象,并且姓名和年齡也不能由小強來決定,不然他肯定只要18歲的。而是要把創(chuàng)建實例的權(quán)限收回,讓類自身負責自己類實例的創(chuàng)建。
來看看代碼如何實現(xiàn):
public class GirlFriend {
private String name;
private Integer age;
//定義一個變量來存儲創(chuàng)建好的類實例
private static GirlFriend girlFriend = null;
//私有化構(gòu)造方法,防止外部調(diào)用
private GirlFriend(String name, Integer age) {
this.name = name;
this.age = age;
}
//定義一個方法為程序員類提供女朋友實例
public static GirlFriend getGirlFriend(){
//判斷存儲實例是否為空
if(girlFriend==null){
//如果沒值,就new出一個實例,并賦值給存儲實例的變量
girlFriend = new GirlFriend("小美",28);
}
return girlFriend;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "GirlFriend{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
主要的核心思想有三點:
1.定義一個變量來存儲創(chuàng)建好的類實例;
2.私有化構(gòu)造函數(shù),防止外部new該對象;
3.對外提供一個能獲取到該對象的方法。
程序員小強該如何獲取呢:
public class Programmer {
public static void main(String[] args){
GirlFriend girlFriend = GirlFriend.getGirlFriend();
System.out.println(girlFriend.toString());
}
}
直接使用GirlFriend類調(diào)用獲取對象的getGirlFriend方法獲取到實例對象,打印結(jié)果如下:
GirlFriend{name='小美', age=28}
04
模式講解
上面這種解決方案就是單例模式(Singleton),單例模式定義:保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。
通用類圖如下:
Singleton:負責創(chuàng)建單例類自己的唯一實例,并提供一個getInstance的方法讓外部來訪問這個類的唯一實例。
單例模式功能:保證類在運行期間只允許被創(chuàng)建一個實例。有懶漢式和餓漢式兩種實現(xiàn)方式。
懶漢式:上面的代碼就是懶漢式的實現(xiàn)方式,顧名思義,懶漢式指只有當該實例被使用到的時候才會創(chuàng)建,通過三個步驟就可以實現(xiàn)懶漢式:
1.私有化構(gòu)造方法:防止外部使用。
2.提供獲取實例的方法:全局唯一的類實例訪問點。
3.把獲取實例的方法改為靜態(tài):因為只有靜態(tài)的方法才能直接通過類名來調(diào)用,否則就要通過實例調(diào)用,這就陷入了死循環(huán)。
完整代碼:
public class Singleton{
//定義變量存放創(chuàng)建好的實例,因為要在靜態(tài)方法中使用,所以變量也必須是靜態(tài)的
private static Singleton uniqueInstance = null;
//私有化構(gòu)造方法,可以在內(nèi)部控制創(chuàng)建實例的數(shù)目
private Singleton(){
}
//定義一個方法為客戶端提供類實例,synchronized同步保證線程安全
public static synchronized Singleton getInstance(){
//判斷是否已經(jīng)有實例
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
//有就直接用
return uniqueInstance;
}
}
這里使用到了synchronized用來保證線程安全,如果不加會帶來什么問題呢?比如兩個線程A和B,就有可能導(dǎo)致并發(fā)的問題,如圖所示:
這種情況就會創(chuàng)建出兩個實例出來,單例模式也就失效了。加上synchronized雖然能保證線程安全,但是卻降低了訪問速度,影響了性能,可以考慮使用雙重檢查加鎖來解決這個問題,雙重檢查加鎖意思是并不是每次進入getInstance方法都需要同步,而是先不同步,進入方法之后先檢查實例是否存在,如果不存在才進入同步塊。這是第一重檢查。進入同步塊之后再檢查實例是否存在,如果不存在,就在同步的情況下創(chuàng)建一個實例,這是第二重檢查。
代碼實現(xiàn):
public class Singleton{
//對保存實例的變量添加volatile修飾
private volatile static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
//第一次檢查
if(instance == null){
//同步塊,線程安全的創(chuàng)建實例
synchronized (Singleton.class){
//第二次檢查
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
這種方式即可以安全的創(chuàng)建線程,又不會對性能造成太大的影響。
餓漢式:所謂餓漢式也就是在類加載的時候直接new出一個對象來,不管以后用不用得到,是一種以空間換取時間的策略。代碼也非常簡單:
public class Singleton{
//定義一個變量來存儲創(chuàng)建好的實例,直接在這里創(chuàng)建實例,只能創(chuàng)建一次
//static變量在類加載時進行初始化,并且只被初始化一次。
private static Singleton uniqueInstance = new Singleton();
//私有化構(gòu)造方法,可以在內(nèi)部控制創(chuàng)建實例的數(shù)目,防止在外部創(chuàng)建
private Siingleton(){
}
//定義一個方法為客戶端提供類實例,方法上加static將該方法變?yōu)殪o態(tài)
//目的是不需要對象實例就可以在外部直接通過類來調(diào)用
public static Singleton getInstance(){
//直接使用已經(jīng)創(chuàng)建好的實例
return uniqueInstance;
}
}
單例模式作用范圍:目前Java里面實現(xiàn)的單例是一個虛擬機的范圍,虛擬機在通過自己的`ClassLoader`裝載餓漢式實現(xiàn)的單例類時就會創(chuàng)建一個類實例。如果一個虛擬機中有多個類加載器或者一個機器中有多個虛擬機,那么單例就不再起作用了。
單例模式優(yōu)缺點:
1.節(jié)約內(nèi)存資源;
2.時間和空間:懶漢式是以時間換空間,餓漢式是以空間換時間;
3.線程安全:不加同步synchronized的懶漢式是線程不安全的,而餓漢式是線程安全的,因為虛擬機只會裝載一次,并且在裝載的時候是不會發(fā)生并發(fā)的。加上synchronized和雙重檢查加鎖也能保證懶漢式的線程安全。
05
新的問題
由于小強工作很賣命,公司業(yè)績發(fā)展的不錯,老板決定再招一名程序員,應(yīng)聘者小華也是一個單身漢,老板也承諾會給他分配女朋友,單是問題來了,之前的GirlFriend只能new出一個對象,總不能讓小強和小華共用一個對象吧。于是要想辦法實現(xiàn)一個可以提供兩個實例的GirlFriend類。
其實思路很簡單,只需要通過Map來緩存實例即可,代碼如下:
import java.util.HashMap;
import java.util.Map;
public class GirlFriend {
private String name;
private Integer age;
private static int maxNumsOfGirlFriends = 2;//最大數(shù)量
private static int number = 1;//當前編號
//定義一個變量來存儲創(chuàng)建好的類實例
private static Map
//私有化構(gòu)造方法,防止外部調(diào)用
private GirlFriend(String name, Integer age) {
this.name = name;
this.age = age;
}
//定義一個方法為程序員類提供女朋友實例
public static GirlFriend getGirlFriend(){
GirlFriend girlFriend = girlFriendMap.get(number+"");
if(girlFriend==null){
//new一個新實例,并放到map中,用number當做key,實例是value
girlFriend = new GirlFriend("小美",28);
girlFriendMap.put(number+"",girlFriend);
}
number++;
if(number>maxNumsOfGirlFriends){
number = 1;
}
return girlFriend;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
程序員類進行獲取女朋友實例,如下:
public class Programmer {
public static void main(String[] args){
GirlFriend girlFriend1 = GirlFriend.getGirlFriend();
System.out.println(girlFriend1);
GirlFriend girlFriend2 = GirlFriend.getGirlFriend();
System.out.println(girlFriend2);
GirlFriend girlFriend3 = GirlFriend.getGirlFriend();
System.out.println(girlFriend3);
GirlFriend girlFriend4 = GirlFriend.getGirlFriend();
System.out.println(girlFriend4);
}
}
上面代碼獲取了四次,看看打印的結(jié)果如何:
GirlFriend@6e0be858
GirlFriend@61bbe9ba
GirlFriend@6e0be858
GirlFriend@61bbe9ba
可以看出,第一次和第三次是一樣的,第二次和第四次是一樣的,一共就只有兩個對象,解決了這個問題。但是如何判斷哪個女朋友實例是小強的哪個是小華的呢?一種簡單的方法是通過給獲取實例的函數(shù)getGirlFriend傳參,比如小強獲取的時候傳如number = 1,小華的number = 2。
06
相關(guān)擴展
在Java中還用一種更好的單例實現(xiàn)方式,既能夠?qū)崿F(xiàn)延遲加載,又能夠?qū)崿F(xiàn)線程安全。這種解決方案被稱為Lazy initialization holder class模式,這個模式綜合使用了Java的類級內(nèi)部類和多線程缺省同步鎖的知識,很巧妙地同時實現(xiàn)了延遲加載和線程安全。
類級內(nèi)部類:
1.類級內(nèi)部類指的是有static修飾的成員式內(nèi)部類。如果沒有static修飾的成員式內(nèi)部類被稱為對象級內(nèi)部類。
2.類級內(nèi)部類相當于其外部類的static成分,它的對象與外部類對象間不存在依賴關(guān)系,因此可以直接創(chuàng)建。
3.類級內(nèi)部類中可以定義靜態(tài)方法。在靜態(tài)方法中能夠引用外部類中的靜態(tài)成員方法或者成員變量。
4.類級內(nèi)部類相當于其外部類的成員,只有在第一次被使用的時候才會被裝載。
缺省同步鎖:在某些情況下,JVM已經(jīng)隱含地執(zhí)行了同步,不需要自己進行同步控制了,這些情況包括:
1.由靜態(tài)初始化器初始化數(shù)據(jù)時。
2.訪問final字段時
3.在創(chuàng)建線程之前創(chuàng)建對象時
4.線程可以看見它將要處理的對象時。
思路:使用靜態(tài)初始化器的方式,由jvm保證線程安全。但是這樣就像餓漢式的實現(xiàn)方式了,浪費一定的空間。采用類級內(nèi)部類,在這個類級內(nèi)部類里面創(chuàng)建對象實例,只要不使用這個類級內(nèi)部類,就不會創(chuàng)建實例對象。
代碼:
public class Singleton{
//類級內(nèi)部類,該內(nèi)部類的實例與外部類的實例沒有綁定關(guān)系,
// 而且只有被調(diào)用到時才會裝載,從而實現(xiàn)延遲加載
private static class SingletonHolder{
//靜態(tài)初始化器,由jvm保證線程安全
private static Singleton instance = new Singleton();
}
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
當getInstance方法第一次被調(diào)用的時候,它第一次讀取 SingletonHolder.instance導(dǎo)致SingletonHolder類得到初始化,從而創(chuàng)建Singleton實例。
枚舉實現(xiàn)單例:
1.Java的枚舉類型實質(zhì)上是功能齊全的類,因此可以有自己的屬性和方法。
2.Java枚舉類型的基本思想是通過共有的靜態(tài)fianl域為每個枚舉常量導(dǎo)出實例的類。
3.從某種角度將,枚舉是單例的泛型化,本質(zhì)上是單元素的枚舉。
代碼:
public enum Singleton{
uniqueInstance;
//單例自己的操作函數(shù)
public void singletonOperation(){
//功能處理
}
}
使用枚舉來實現(xiàn)單例控制更加簡潔,而且無償?shù)靥峁┝诵蛄谢臋C制,并由JVM從根本上提供保障,絕對防止多次實例化。
在Spring中,每個Bean默認就是單例的,這樣的優(yōu)點是Spring容器可以管理這些Bean的生命周期,決定什么時候創(chuàng)建出來,什么時候銷毀,銷毀的時候如何處理等等。
使用單例模式需要注意的就是JVM的垃圾回收機制,如果我們的一個單例對象在內(nèi)存中長久不使用,JVM就認為這個對象是一個垃圾,在CPU資源空閑的情況下該對象會被清理掉,下次再調(diào)用時就需要重新產(chǎn)生一個對象。
來源:本文內(nèi)容搜集或轉(zhuǎn)自各大網(wǎng)絡(luò)平臺,并已注明來源、出處,如果轉(zhuǎn)載侵犯您的版權(quán)或非授權(quán)發(fā)布,請聯(lián)系小編,我們會及時審核處理。
聲明:江蘇教育黃頁對文中觀點保持中立,對所包含內(nèi)容的準確性、可靠性或者完整性不提供任何明示或暗示的保證,不對文章觀點負責,僅作分享之用,文章版權(quán)及插圖屬于原作者。
Copyright?2013-2024 JSedu114 All Rights Reserved. 江蘇教育信息綜合發(fā)布查詢平臺保留所有權(quán)利
蘇公網(wǎng)安備32010402000125
蘇ICP備14051488號-3技術(shù)支持:南京博盛藍睿網(wǎng)絡(luò)科技有限公司
南京思必達教育科技有限公司版權(quán)所有 百度統(tǒng)計