原創/朱季謙安全
靈魂拷問,這位獨秀同窗,你會這道題嗎?多線程
請說說,「System.out.println()」原理......性能
這應該是剛開始學習Java時用到最多一段代碼,迄今爲止,與它算是老朋友了。既然是老朋友,就應該多去深刻了解下其「心裏」深處的「真正想法」。學習
在深刻了解以前,先給本身提幾個問題:測試
System是什麼?out是什麼?println又是什麼?三個代碼組成爲什麼能實現打印信息的功能?this
接下來,咱們就帶着問題,去熟悉咱們這位相處已久的老夥計。spa
先從System開始一步一步探究。線程
在百度百科上,有對System作了這樣的說明:System類表明系統,其中系統級的不少屬性和控制方法都放置在該類的內部。3d
簡而意之,該類與系統有關,可獲取系統內部的衆多屬性以及方法,其部分源碼以下:日誌
1 public final class System { 2 private static native void registerNatives(); 3 static { 4 registerNatives(); 5 } 6 private System() { 7 } 8 public final static InputStream in = null; 9 public final static PrintStream out = null; 10 public final static PrintStream err = null; 11 private static volatile SecurityManager security = null; 12 public static void setIn(InputStream in) { 13 checkIO(); 14 setIn0(in); 15 } 16 public static void setOut(PrintStream out) { 17 checkIO(); 18 setOut0(out); 19 } 20 ...... 21 }
打開源碼,發現這是一個final定義的類,其次,該類的構造器是以private權限進行定義的。根據這兩狀況能夠說明,該類即不能被繼承也沒法實例化成對象,同時需注意一點,就是這個類裏定義的不少變量和方法都是static來定義的,即這些類成員都是屬於類而非對象。
所以,若需調用類中的這些帶static定義的屬性或者方法,無需建立對象就能直接經過「類名.成員名」來調用。
在System源碼中,須要留意的是in,out,or三者,它們分別表明標準輸入流,標準輸出流,標準錯誤輸出流。
到這一步,即可以逐漸看到System.out.println中的影子,沒錯,這行代碼裏的System.out,即爲引用System類裏靜態成員out,它是PrintStream類型的引用變量,稱爲"字節輸出流"。做爲static定義的out引用變量,它在類加載時就被初始化了,初始化後,會建立PrintStream對象對out賦值,以後便能調用PrintStream類中定義的方法。
具體怎麼建立PrintStream並賦值給靜態成員out,我放在本文後面講解。
接着,進入到PrintStream類當中——
1 public class PrintStream extends FilterOutputStream 2 implements Appendable, Closeable 3 { 4 ...... 5 public void println() { 6 newLine(); 7 } 8 9 public void println(boolean x) { 10 synchronized (this) { 11 print(x); 12 newLine(); 13 } 14 } 15 16 public void println(char x) { 17 synchronized (this) { 18 print(x); 19 newLine(); 20 } 21 } 22 23 public void println(int x) { 24 synchronized (this) { 25 print(x); 26 newLine(); 27 } 28 } 29 30 public void println(long x) { 31 synchronized (this) { 32 print(x); 33 newLine(); 34 } 35 } 36 37 public void println(float x) { 38 synchronized (this) { 39 print(x); 40 newLine(); 41 } 42 } 43 44 public void println(double x) { 45 synchronized (this) { 46 print(x); 47 newLine(); 48 } 49 } 50 51 public void println(char x[]) { 52 synchronized (this) { 53 print(x); 54 newLine(); 55 } 56 } 57 58 public void println(String x) { 59 synchronized (this) { 60 print(x); 61 newLine(); 62 } 63 } 64 65 ...... 66 }
發現這PrintStream裏邊存在諸多以println名字命名的重載方法。
這個,就是咱們本文中最後須要回答的問題,即println是什麼?
它實際上是PrintStream打印輸出流類裏的方法。
每一個有傳參的println方法裏,其最後調用的方法都是print()與newLine()。
值得注意一點,這些帶有傳參的println方法當中,裏面都是經過同步synchronized來修飾,這說明System.out.println實際上是線程安全的。同時還有一點需注意,在多線程狀況下,當大量方法執行同一個println打印時,其synchronized同步性能效率均可能出現嚴重性能問題。所以,在實際生產上,廣泛是用log.info()相似方式來打印日誌而不會用到System.out.println。
在以上代碼裏,其中 newLine()是表明打印換行的意思。
衆所周知,以System.out.println()來打印信息時,每條打印信息都會換行的,之因此會出現換行,其原理就是println()內部經過newLine()方法實現的。
若換成System.out.print()來打印,則不會出現換行狀況。
爲何print()不會出現換行呢?
分析一下print()裏代碼即可得知,是由於其方法裏並無調用newLine()方法來實現換行的——
1 public void print(boolean b) { 2 write(b ? "true" : "false"); 3 } 4 5 public void print(char c) { 6 write(String.valueOf(c)); 7 } 8 9 public void print(int i) { 10 write(String.valueOf(i)); 11 } 12 13 public void print(long l) { 14 write(String.valueOf(l)); 15 } 16 17 public void print(float f) { 18 write(String.valueOf(f)); 19 } 20 21 public void print(double d) { 22 write(String.valueOf(d)); 23 } 24 25 public void print(char s[]) { 26 write(s); 27 } 28 29 30 public void print(String s) { 31 if (s == null) { 32 s = "null"; 33 } 34 write(s); 35 }
這些重載方法裏面都調用相同的write()方法,值得注意的是,在調用write()時,部分方法的實現是都把參數轉換成了String字符串類型,以後進入到write()方法詳情裏——
1 private void write(String s) { 2 try { 3 synchronized (this) { 4 ensureOpen(); 5 textOut.write(s); 6 textOut.flushBuffer(); 7 charOut.flushBuffer(); 8 if (autoFlush && (s.indexOf('\n') >= 0)) 9 out.flush(); 10 } 11 } 12 catch (InterruptedIOException x) { 13 Thread.currentThread().interrupt(); 14 } 15 catch (IOException x) { 16 trouble = true; 17 } 18 }
其中,ensureOpen()的方法是判斷out流是否已經開啓,其詳細方法以下:
1 private void ensureOpen() throws IOException { 2 if (out == null) 3 throw new IOException("Stream closed"); 4 }
由方法可得知,在進行寫入打印信息時,需判斷PrintStream流是否已經開啓,若沒有開啓,則沒法將打印信息寫入計算機,故而拋出說明流是關閉狀態的異常提示:「Stream closed」
若流是開啓的,便可執行 textOut.write(s);
根據我的理解,這裏的textOut是BufferedWriter引用變量,即爲常說的IO流裏寫入流,最終會將信息寫入到控制檯上,即咱們日常說的控制檯打印。能夠理解成,控制檯就是一個文件,可是能被咱們實時看到裏面是什麼的文件,這樣當每次寫入東西時,就會實時呈如今文件裏,也就是能被咱們看到的控制檯打印信息。
那麼,問題來了,哪行代碼是表示寫入到控制檯文件的呢?System、out、println又是如何組成到一塊兒來起做用的?
讓咱們回到System類最開始的地方——
1 public final class System { 2 3 /* register the natives via the static initializer. 4 * 5 * VM will invoke the initializeSystemClass method to complete 6 * the initialization for this class separated from clinit. 7 * Note that to use properties set by the VM, see the constraints 8 * described in the initializeSystemClass method. 9 */ 10 private static native void registerNatives(); 11 static { 12 registerNatives(); 13 } 14 15 }
以上的靜態代碼會在類的初始化階段被初始化,其會調用一個native方法registerNatives()。根據該方法的英文註釋「VM will invoke the initializeSystemClass method to complete」,可知,VM將調用initializeSystemClass方法來完成該類初始化。
咱們找到該initializeSystemClass方法,下面只列出本文須要用到的核心代碼,稍微作了一下注釋:
1 private static void initializeSystemClass() { 2 //被vm執行系統屬性初始化 3 props = new Properties(); 4 initProperties(props); 5 sun.misc.VM.saveAndRemoveProperties(props); 6 7 //從系統屬性中獲取系統相關的換行符,賦值給變量lineSeparator 8 lineSeparator = props.getProperty("line.separator"); 9 sun.misc.Version.init(); 10 //分別建立in、out、err的實例對象,並經過set()方法初始化 11 FileInputStream fdIn = new FileInputStream(FileDescriptor.in); 12 FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out); 13 FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err); 14 setIn0(new BufferedInputStream(fdIn)); 15 setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding"))); 16 setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding"))); 17 18 ...... 19 }
主要關注這兩行代碼:
1 FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out); 2 setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
一.這裏逐行進行分析,首先FileDescriptor是一個「文件描述符」,能夠通俗地把它當成一個文件,它有如下三個屬性:
in:標準輸入(鍵盤)的描述符
out:標準輸出(屏幕)的描述符
err:標準錯誤輸出(屏幕)的描述符
FileDescriptor.out表明爲「標準輸出(屏幕)」,能夠通俗地理解成標準輸出到控制檯的文件,即表示控制檯。
new FileOutputStream(FileDescriptor.out)該行代碼即說明經過文件輸出流將信息輸出到屏幕即控制檯上。
若仍是不理解,可舉一個比較常見的例子——
1 public static void main(String[] args) throws IOException { 2 FileOutputStream out=new FileOutputStream("C:\\file.txt"); 3 out.write(66); 4 }
這是比較簡單的經過FileOutputStream輸出流寫入文件的寫法,這裏的路徑「C:\file.txt」就與FileDescriptor.out作法相似,都是描述一個可寫入數據的文件,只不過FileDescriptor.out比較特殊,它描述的是屏幕,即常說的控制檯。
二.接下來是newPrintStream(fdOut, props.getProperty("sun.stdout.encoding"))——
1 private static PrintStream newPrintStream(FileOutputStream fos, String enc) { 2 if (enc != null) { 3 try { 4 return new PrintStream(new BufferedOutputStream(fos, 128), true, enc); 5 } catch (UnsupportedEncodingException uee) {} 6 } 7 return new PrintStream(new BufferedOutputStream(fos, 128), true); 8 }
該方法是爲輸出流建立一個BufferedOutputStream緩衝輸出流,起到流緩衝的做用,最後經過new PrintStream()建立一個打印輸出流。
經過該流的打印接口,如print(), println(),可實現打印輸出的做用。
三.最後就是執行 setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
可知,該方法是一個native方法,感興趣的童鞋可繼續深刻研究,這裏大概就是將生成的PrintStream對象賦值給System裏的靜態對象引用變量:out。
到這裏,就回到了咱們最開始的地方:System.out.println,沒錯,這裏面的out,就是經過setOut0來進行PrintStream對象賦值的,咱們既然能拿到了PrintStream的對象引用out,天然就能夠訪問PrintStream類裏的任何public方法裏,包括println(),包括print(),等等。
可提取以上初始化out的源碼重作一個手動打印的測試,如:
執行,發現能夠控制檯上打印出"測試打印"四字。
最後,總結一下,System.out.println的原理是在類加載System時,會初始化System的initializeSystemClass()方法,該方法中將建立一個打印輸出流PrintStream對象,隨後經過setOut0(PrintStream out)方法,會將初始化建立的PrintStream 對象賦值給System靜態引用變量out。out被賦值對象地址後,就能夠調用PrintStream中的各類public修飾的方法裏,其中就包括println()、print()這類打印信息的方法,經過out.println(「xxxx」)便可將「xxxx」打印到控制檯上,也就是等價於System.out.println("xxxx")。
1 System.out.println("打印數據"); 2 等價於---> 3 PrintStream out=System.out; 4 out.println("打印數據");
以上,就是System.out.println的執行原理。
如有不足,還請指出改正。