寫(xiě)軟件的時(shí)候經(jīng)常需要用到打印日志功能,可以幫助你調(diào)試和定位問(wèn)題,項(xiàng)目上線后還可以幫助你分析數(shù)據(jù)。但是Java原生帶有的System.out.println()方法卻很少在真正的項(xiàng)目開(kāi)發(fā)中使用,甚至像findbugs等代碼檢查工具還會(huì)認(rèn)為使用System.out.println()是一個(gè)bug。
為什么作為Java新手神器的System.out.println(),到了真正項(xiàng)目開(kāi)發(fā)當(dāng)中會(huì)被唾棄呢?其實(shí)只要細(xì)細(xì)分析,你就會(huì)發(fā)現(xiàn)它的很多弊端。比如不可控制,所有的日志都會(huì)在項(xiàng)目上線后照常打印,從而降低運(yùn)行效率;又或者不能將日志記錄到本地文件,一旦打印被清除,日志將再也找不回來(lái);再或者打印的內(nèi)容沒(méi)有Tag區(qū)分,你將很難辨別這一行日志是在哪個(gè)類里打印的。
你的leader也不是傻瓜,用System.out.println()的各項(xiàng)弊端他也清清楚楚,因此他今天給你的任務(wù)就是制作一個(gè)日志工具類,來(lái)提供更好的日志功能。不過(guò)你的leader人還不錯(cuò),并沒(méi)讓你一開(kāi)始就實(shí)現(xiàn)一個(gè)具備各項(xiàng)功能的牛逼日志工具類,只需要一個(gè)能夠控制打印級(jí)別的日志工具就好。
這個(gè)需求對(duì)你來(lái)說(shuō)并不難,你立刻就開(kāi)始動(dòng)手編寫(xiě)了,并很快完成了第一個(gè)版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public class LogUtil { public final int DEBUG = 0 ; public final int INFO = 1 ; public final int ERROR = 2 ; public final int NOTHING = 3 ; public int level = DEBUG; public void debug(String msg) { if (DEBUG >= level) { System.out.println(msg); } } public void info(String msg) { if (INFO >= level) { System.out.println(msg); } } public void error(String msg) { if (ERROR >= level) { System.out.println(msg); } } } |
通過(guò)這個(gè)類來(lái)打印日志,只需要控制level的級(jí)別,就可以自由地控制打印的內(nèi)容。比如現(xiàn)在項(xiàng)目處于開(kāi)發(fā)階段,就將level設(shè)置為DEBUG,這樣所有的日志信息都會(huì)被打印。而項(xiàng)目如果上線了,可以把level設(shè)置為INFO,這樣就只能看到INFO及以上級(jí)別的日志打印。如果你只想看到錯(cuò)誤日志,就可以把level設(shè)置為ERROR。而如果你開(kāi)發(fā)的項(xiàng)目是客戶端版本,不想讓任何日志打印出來(lái),可以將level設(shè)置為NOTHING。打印的時(shí)候只需要調(diào)用:
1
|
new LogUtil().debug( "Hello World!" ); |
你迫不及待地將這個(gè)工具介紹給你的leader,你的leader聽(tīng)完你的介紹后說(shuō):“好樣的,今后大伙都用你寫(xiě)的這個(gè)工具來(lái)打印日志了!”
可是沒(méi)過(guò)多久,你的leader找到你來(lái)反饋問(wèn)題了。他說(shuō)雖然這個(gè)工具好用,可是打印這種事情是不區(qū)分對(duì)象的,這里每次需要打印日志的時(shí)候都需要new出一個(gè)新的LogUtil,太占用內(nèi)存了,希望你可以將這個(gè)工具改成用單例模式實(shí)現(xiàn)。
你認(rèn)為你的leader說(shuō)的很有道理,而且你也正想趁這個(gè)機(jī)會(huì)練習(xí)使用一下設(shè)計(jì)模式,于是你寫(xiě)出了如下的代碼(ps:這里代碼是我自己實(shí)現(xiàn)的,而且我開(kāi)始確實(shí)沒(méi)注意線程同步問(wèn)題):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
public class LogUtil { private static LogUtil logUtilInstance; public final int DEBUG = 0 ; public final int INFO = 1 ; public final int ERROR = 2 ; public final int NOTHING = 3 ; public int level = DEBUG; private LogUtil() { } public static LogUtil getInstance() { if (logUtilInstance == null ) { logUtilInstance = new LogUtil(); } return logUtilInstance; } public void debug(String msg) { if (DEBUG >= level) { System.out.println(msg); } } public void info(String msg) { if (INFO >= level) { System.out.println(msg); } } public void error(String msg) { if (ERROR >= level) { System.out.println(msg); } } public static void main(String[] args) { LogUtil.getInstance().debug( "Hello World!" ); } } |
首先將LogUtil的構(gòu)造函數(shù)私有化,這樣就無(wú)法使用new關(guān)鍵字來(lái)創(chuàng)建LogUtil的實(shí)例了。然后使用一個(gè)sLogUtil私有靜態(tài)變量來(lái)保存實(shí)例,并提供一個(gè)公有的getInstance方法用于獲取LogUtil的實(shí)例,在這個(gè)方法里面判斷如果sLogUtil為空,就new出一個(gè)新的LogUtil實(shí)例,否則就直接返回sLogUtil。這樣就可以保證內(nèi)存當(dāng)中只會(huì)存在一個(gè)LogUtil的實(shí)例了。單例模式完工!這時(shí)打印日志的代碼需要改成如下方式:
1
|
LogUtil.getInstance().debug( "Hello World" ); |
你將這個(gè)版本展示給你的leader瞧,他看后笑了笑,說(shuō):“雖然這看似是實(shí)現(xiàn)了單例模式,可是還存在著bug的哦。
你滿腹狐疑,單例模式不都是這樣實(shí)現(xiàn)的嗎?還會(huì)有什么bug呢?
你的leader提示你,使用單例模式就是為了讓這個(gè)類在內(nèi)存中只能有一個(gè)實(shí)例的,可是你有考慮到在多線程中打印日志的情況嗎?如下面代碼所示:
1
2
3
4
5
6
7
|
public static LogUtil getInstance() { if (logUtilInstance == null ) { logUtilInstance = new LogUtil(); } return logUtilInstance; } |
如果現(xiàn)在有兩個(gè)線程同時(shí)在執(zhí)行g(shù)etInstance方法,第一個(gè)線程剛執(zhí)行完第2行,還沒(méi)執(zhí)行第3行,這個(gè)時(shí)候第二個(gè)線程執(zhí)行到了第2行,它會(huì)發(fā)現(xiàn)sLogUtil還是null,于是進(jìn)入到了if判斷里面。這樣你的單例模式就失敗了,因?yàn)閯?chuàng)建了兩個(gè)不同的實(shí)例。
你恍然大悟,不過(guò)你的思維非常快,立刻就想到了解決辦法,只需要給方法加上同步鎖就可以了,代碼如下:
1
2
3
4
5
6
7
|
public synchronized static LogUtil getInstance() { if (logUtilInstance == null ) { logUtilInstance = new LogUtil(); } return logUtilInstance; } |
這樣,同一時(shí)刻只允許有一個(gè)線程在執(zhí)行g(shù)etInstance里面的代碼,這樣就有效地解決了上面會(huì)創(chuàng)建兩個(gè)實(shí)例的情況。
你的leader看了你的新代碼后說(shuō):“恩,不錯(cuò)。這確實(shí)解決了有可能創(chuàng)建兩個(gè)實(shí)例的情況,但是這段代碼還是有問(wèn)題的。”
你緊張了起來(lái),怎么還會(huì)有問(wèn)題啊?
你的leader笑笑:“不用緊張,這次不是bug,只是性能上可以優(yōu)化一些。你看一下,如果是在getInstance方法上加了一個(gè)synchronized,那么我每次去執(zhí)行g(shù)etInstace方法的時(shí)候都會(huì)受到同步鎖的影響,這樣運(yùn)行的效率會(huì)降低,其實(shí)只需要在第一次創(chuàng)建LogUtil實(shí)例的時(shí)候加上同步鎖就好了。我來(lái)教你一下怎么把它優(yōu)化的更好。”
首先將synchronized關(guān)鍵字從方法聲明中去除,把它加入到方法體當(dāng)中:
1
2
3
4
5
6
7
8
9
10
11
12
|
public static LogUtil getInstance() { if (logUtilInstance == null ) { synchronized (LogUtil. class ) { if (logUtilInstance == null ) { // 這里是必須的,因?yàn)橛锌赡軆蓚€(gè)進(jìn)程同時(shí)執(zhí)行到synchronized之前 logUtilInstance = new LogUtil(); } } } return logUtilInstance; } |
代碼改成這樣之后,只有在sLogUtil還沒(méi)被初始化的時(shí)候才會(huì)進(jìn)入到第3行,然后加上同步鎖。等sLogUtil一但初始化完成了,就再也走不到第3行了,這樣執(zhí)行g(shù)etInstance方法也不會(huì)再受到同步鎖的影響,效率上會(huì)有一定的提升。
你情不自禁贊嘆到,這方法真巧妙啊,能想得出來(lái)實(shí)在是太聰明了。
你的leader馬上謙虛起來(lái):“這種方法叫做雙重鎖定(Double-Check Locking),可不是我想出來(lái)的,更多的資料你可以在網(wǎng)上查一查。”
其實(shí)在java里實(shí)現(xiàn)單例我更習(xí)慣用餓漢模式
懶漢式的特點(diǎn)是延遲加載,實(shí)例直到用到的時(shí)候才會(huì)加載
餓漢式的特點(diǎn)是一開(kāi)始就加載了,所以每次用到的時(shí)候直接返回即可(我更推薦這一種,因?yàn)椴恍枰紤]太多線程安全的問(wèn)題,當(dāng)然懶漢式是可以通過(guò)上文所說(shuō)的雙重鎖定解決同步問(wèn)題的)
用餓漢式實(shí)現(xiàn)日志記錄的代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
public class LogUtil { private static final LogUtil logUtilInstance = new LogUtil(); public final int DEBUG = 0 ; public final int INFO = 1 ; public final int ERROR = 2 ; public final int NOTHING = 3 ; public int level = DEBUG; private LogUtil() { } public static LogUtil getInstance() { return logUtilInstance; } public void debug(String msg) { if (DEBUG >= level) { System.out.println(msg); } } public void info(String msg) { if (INFO >= level) { System.out.println(msg); } } public void error(String msg) { if (ERROR >= level) { System.out.println(msg); } } public static void main(String[] args) { logUtil.getInstance().debug( "Hello World!" ); } } |