《瘋狂Java講義》讀書筆記(十):多執行緒,網路程式設計,類載入機制與反射
第十六章:多執行緒
1、一般而言,程序包含如下3個特徵:獨立性,動態性,併發性。併發性和並行性是兩個概念,並行指同一時刻,有多條指令在多個處理器上同時執行;併發指同一時刻只能有一條指令執行,但多個程序指令被快速輪換執行。一個程式執行後至少有一個程序,一個程序裡可以包含多個執行緒,但至少要包含一個執行緒。
多執行緒的優勢:①程序之間不能共享記憶體,但執行緒之間共享記憶體非常容易;②系統建立程序時需要為該程序重新分配系統資源,但建立執行緒則代價小得多,因此使用多執行緒來實現多工併發比多程序的效率高;③Java語言內建了多執行緒功能支援。
執行緒的建立和啟動:
方法1:繼承Thread類建立執行緒類,步驟:
publicclassFirstThread1extendsThread{
privateinti;
@Override
publicvoid run(){
for(;i<100;i++){//當執行緒繼承Thread類,直接使用this即可獲得當前執行緒
System.out.println(this.getName()+" "+i);}}
publicstaticvoid main(
for (int i = 0; i < 100; i++) {
//呼叫Thread的currentThread()方法獲取當前執行緒
System.out.println(Thread.currentThread().getName()+" "+i);
if(i==30){newFirstThread1().start();
//建立並啟動第一個執行緒
newFirstThread1().start();//建立並啟動第二個執行緒
}}}
執行效果:主執行緒main執行到30(或之後)時,會啟動兩個執行緒,然後三個執行緒一起執行。順序未知。主執行緒是必須會有的。Thread.currentThread
程式可以通過setName(String name)方法來為執行緒設定名字。
方法2:實現Runnable介面建立執行緒類:步驟:①定義Runnable介面的實現類,並重寫該介面的run()方法;②建立Runnable實現類的例項,並以此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件。③呼叫執行緒物件的start方法啟動執行緒。
publicclassSecondThread2implements Runnable{
privateinti;
@Override
publicvoid run(){
for(;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);}}
publicstaticvoid main(String[] args) {
for(int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
if (i==10) {
SecondThread2 thread2=newSecondThread2();//通過new Thread(target,name)方法建立新執行緒
newThread(thread2,"這是執行緒1的名字").start();
newThread(thread2,"執行緒2的名字").start();}}}
方法3:使用Callable和Future建立執行緒:Callable提供了call方法可以作為執行緒執行體,可以有返回值,可以丟擲異常。Java 5提供了Future介面來代表Callable接口裡的call方法的返回值,併為Future介面提供了FutureTask實現類:①V get():返回Callable任務裡call方法的返回值,呼叫該方法導致執行緒阻塞,必須等到子執行緒結束後才會得到返回值。②V get(long timeout,TimeUnit unit):返回Callable任務裡call返回值,該方法讓程式最多阻塞timeout和unit指定的時間,如果超時,則丟擲異常。③boolean isCancelled():如果Callable任務正常完成前被取消,返true;④boolean isDone()如果Callable任務已經完成,返回true。
建立並啟動有返回值的執行緒的步驟如下:
①建立Callable介面的實現類,並實現call方法;②使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件的call方法的返回值;③使用FutureTask物件作為Thread物件的target建立並啟動新執行緒;④呼叫FutureTask物件的get方法來獲得子執行緒執行結束後的返回值。
publicvoid doThing()throws Exception{
FutureTask<Integer> fTask=newFutureTask<>(new Callable<Integer>() {
@Override
publicInteger call() throws Exception {
System.out.println("子執行緒在執行:"+Thread.currentThread().getName());
int sum=2+3;return sum;}});
Thread thread=newThread(fTask,"子執行緒名字-扁蛋");
thread.start();
System.out.println("主執行緒在執行任務");
System.out.println("task執行結果:"+fTask.get());}
建立執行緒的三種方式對比:通過繼承Thread類或實現Runnable、Callable介面都可以實現多執行緒,不過實現Runnable介面與實現Callable介面的方式基本相同,只是Callable接口裡定義的方法有返回值,可以宣告丟擲異常而已,因此可以歸為一類。繼承Thread類的優勢是編寫簡單,無須使用Thread.currentThread()方法,直接使用this即可獲得當前執行緒,劣勢是不能再繼承其它父類。因此一般推薦採用實現Runnable介面、Callable介面的方式來建立多執行緒。
執行緒的生命週期:
新建、就緒、執行、阻塞和死亡5種狀態。程式使用new建立執行緒後,處於新建狀態,呼叫start()方法後進入就緒狀態,何時執行取決於JVM裡執行緒排程。啟動執行緒使用start()方法而不是run方法,run方法是普通的方法,在run方法沒有執行完之前其它執行緒無法並行執行。因此不要呼叫執行緒的run方法。
提示:如果希望呼叫子執行緒的start方法後子執行緒立即開始執行,程式可以使用Thread.sleep(1)來讓當前執行的執行緒(主執行緒)睡眠1毫秒。
被阻塞的執行緒在合適的時候重新進入就緒狀態,而不是執行狀態。執行緒會以如下3個方式結束:①run()或者call()方法執行完成,執行緒正常結束;②執行緒丟擲一個未捕獲的Exception或Error;③直接呼叫執行緒的stop()方法——該方法容易導致死鎖,通常不推薦使用。
執行緒的suspend()方法將執行緒掛起,但容易造成死鎖,應該少用。
判斷某個執行緒是否已經死亡:呼叫該執行緒的isAlive()方法,處於就緒、執行、阻塞三種狀態的返回true,新建、死亡的返回false。
如何控制執行緒?
join執行緒:Thread提供了一個讓執行緒等待另一個執行緒完成的方法——join()方法。當某個執行緒執行過程中呼叫其它執行緒的join()方法時,呼叫執行緒將被阻塞,直到被join()方法加入的join執行緒執行完畢。thread.interrupt();//中斷執行緒
join()方法有如下3種過載形式:①join()等待join的執行緒執行完畢;②join(long millis):等待被join的執行緒的時間最長為millis毫秒,還沒結束的話,將不再等待;③join(long millis,int nanos):等待最長millis毫秒加nanos毫秒。(不推薦第三種)
後臺執行緒:執行在後臺,主要為其它執行緒提供服務,用法:在新建執行緒後,直接設定為後臺執行緒,再呼叫start方法,否則異常。
執行緒睡眠:sleep(long millis),當前執行緒睡眠這麼多毫秒時間裡,該執行緒不會獲得執行的機會。
執行緒讓步:yield():它可以讓當前正在執行的執行緒暫停,但它不會阻塞該執行緒,只是將該執行緒轉為就緒狀態,完全可能的情況是:當某個執行緒呼叫了yield方法後,執行緒排程器又將其排程出來重新執行。實際上,當某個執行緒呼叫yield方法暫停之後,只有優先順序與當前執行緒相同或者比它高的處於就緒狀態的執行緒才會被執行。
對比sleep和yield兩個方法:sleep比yield方法有更好的移植性,通常不建議使用yield方法來控制併發執行緒的執行。
改變執行緒優先順序:Thread類提供了setPriority(int newPriority)、getPriority()方法來設定和返回指定執行緒的優先順序。
其中int newPriority引數可以是一個整數,範圍是1-10之間,也可以使用Thread類的三個靜態常量:
①MAX_PRIORITY:其值是10;②MIN_PRIORITY:其值是1;③NORM_PRIORITY值是5。數值越大,優先順序越高。(執行緒,類似前程,數值越高即錢越高,優先級別越大。)通常推薦使用靜態常量,有更好的移植性。
執行緒同步:
①同步程式碼塊:synchronized關鍵字來修飾某個方法,該方法稱為同步方法。
publicsynchronizedvoid draw(double drawAmount)
☞不要對執行緒安全類的所有方法都進行同步,只對那些會改變競爭資源的方法進行同步。
②同步鎖:Java 5開始就提供了功能更加強大的執行緒同步機制——通過顯示定義同步鎖物件來實現同步,使用Lock。Lock提供了比synchronized方法和程式碼塊更加廣泛的鎖定操作。
privatefinalReentrantLocklock=newReentrantLock();
publicvoidmethod(){lock.lock();//加鎖
try {} catch (Exception e){}
finally{lock.unlock();//解鎖
}
執行緒池:系統啟動一個新的執行緒的成本是比較高的,因此使用執行緒池可以很好的提高效能。與資料庫連線池類似,執行緒池在系統啟動過程即建立大量的空閒執行緒,當run或者call方法執行結束後,該執行緒並不會死亡,而是再次返回執行緒池中稱為空閒狀態,等待執行下一個Runnable物件的run或者call方法。
使用執行緒池來執行執行緒任務的步驟如下:
①呼叫Executors類的靜態工廠方法來建立一個ExecutorService 物件,該物件代表一個執行緒池;②建立Runnable實現類或者Callable實現類的例項,作為執行緒執行任務;③呼叫ExecutorService物件的submit()方法來提交Runnable例項;④當不想提交任何任務時,呼叫shutdown()方法關閉執行緒池。
ExecutorService pool=Executors.newFixedThreadPool(5);
Runnable target=new Runnable() {
@Override
publicvoid run(){
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+" i的值是:"+i);
}}};
pool.submit(target);
pool.submit(target);
pool.shutdown();
執行緒相關的類ThreadLocal類,是執行緒安全的工具類。通過使用ThreadLocal類可以簡化多執行緒程式設計時的併發訪問,可以簡潔的隔離多執行緒程式的競爭資源。ThreadLocal類的用法非常簡單,只有三個public方法:①T get():返回此執行緒區域性變數中當前執行緒副本中的值;②void remove():刪除此執行緒區域性變數中當前執行緒的值;③void set(T value):設定此執行緒區域性變數中當前執行緒副本中的值。
在編寫多執行緒程式碼時,可以把不安全的整個變數封裝進ThreadLocal,或者把該物件與執行緒相關的狀態使用ThreadLocal儲存。通常建議:如果多個執行緒之間需要共享資源,以達到執行緒之間的通訊功能,就使用同步機制;如果僅僅需要隔離多個執行緒之間的共享衝突,則可以使用ThreadLocal。
用法:
static ThreadLocal<HashMap> map0 = new ThreadLocal<HashMap>(){
@Override
protected HashMap initialValue() {
System.out.println(Thread.currentThread().getName()+"initialValue");
return new HashMap(); }};
包裝執行緒不安全的集合:比如ArrayList、LinkedList、HashSet、TreeSet、HashMap等都是執行緒不安全的,如果有多個執行緒訪問這些元素,可以使用Collections提供的類方法把這些集合包裝成執行緒安全的集合。這個跟之前的集合那章節的方法一樣!
同步控制:
Collections類提供了多個synchronizedXXX()方法,該方法將指定集合包裝成執行緒安全的集合,可以解決多執行緒併發訪問集合元素時的安全性問題。Java中常用到的集合框架中的HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList、HashMap和TreeMap都是執行緒不安全的,如果有多個執行緒訪問它們,而且有超過一個執行緒試圖修改它們,則存線上程安全問題。例子:
Collection c1=Collections.synchronizedCollection(newArrayList<>());
Collection c2=Collections.synchronizedCollection(newHashSet<>());
List list2=Collections.synchronizedList(newArrayList<>());
Set set=Collections.synchronizedSet(newHashSet<>());
Map map=Collections.synchronizedMap(newHashMap());
注:不難發現,前面類名都是跟後面synchronizedXXX的XXX是相同的。
第十七章 網路程式設計
IP地址分為5類ABCDE,
A類:10.0.0.0~10.255.255.255;
B類:172.16.0.0~172.31.255.255;
C類:192.168.0.0~192.168.255.255
公認埠:0~1023;
註冊埠:1024~49151,應用程式通常使用這些埠;
動態和/或私有埠:49152~65535,應用程式一般不會主動使用這些埠。
使用InetAddress:InetAddress沒有提供構造器,而是提供兩個靜態方法來獲取InetAddress例項:①getByName(String host):根據主機獲取對應的InetAddress物件;②getByAddress(byte[] addr):根據原始IP地址獲取對應的InetAddress物件。InetAddress還提供了3個方法來獲取InetAddress例項對應的IP地址和主機名:①String getCanoicalHostName():獲取此IP地址的全限定域名;②String getHostAddress():返回此InetAddress例項對應的IP地址和字串;③String getHostName():獲取此IP地址的主機名。InetAddress還提供了一個getLocalHost()方法來獲取本機IP地址和InetAddress例項。InetAddress還提供了isReachable()方法來測試是否可以到達該地址。
InetAddress ip=InetAddress.getByName("www.crazyit.org");
System.out.println("是否可以到達某網站:"+ip.isReachable(2000));
System.out.println(ip.getHostAddress());
InetAddress local=InetAddress.getByAddress(newbyte[] {127,0,0,1});
System.out.println("本機是否可達:"+local.isReachable(2000));
System.out.println(local.getCanonicalHostName());
輸出:是否可以到達某網站:true222.73.85.205本機是否可達:true127.0.0.1
使用ServerSocket建立TCP服務端:ServerSocket物件用於監聽來自客戶端的Socket請求,如果沒有連線,將一直處於等待狀態:①Socket accept():如果接收到一個客戶端Socket請求,將返回一個與客戶端Socket對應的Socket。ServerSocket提供瞭如下幾個構造器:①Socket accept(int port):用指定埠port來建立一個ServerSocket,有效值0-65535。當ServerSocket使用完畢後,應使用close方法來關閉該ServerSocket。
IP地址是:127.0.0.1一般代表本機的IP地址。Socket提供了2個方法來獲取輸入流和輸出流:InputStream getInputStream和OutputStream getOutputStream。Socket物件提供了一個setSoTimeout(int millis)的超時連線。
Socket通訊例子
客戶端:Socket conToServer=new Socket("localhost",5500);
//資料輸入流:
DataInputStream inFromSer=new DataInputStream(conToServer.getInputStream());
//資料輸出流:
DataOutputStream outToSer=new DataOutputStream(conToServer.getOutputStream());
服務端:ServerSocket serSock=new ServerSocket(5500);
//偵聽來自客戶端的連線請求
Socket conFromCli=serSock.accept();
//接收資料:
DataInputStream inFromCli=new DataInputStream(conFromCli.getInputStream());
使用DatagramSocket傳送、接收資料:DatagramSocket構造器有3個:①DatagramSocket()無引數構造器表示繫結到本機預設IP地址、本機的所有可以用的隨機埠;②DatagramSocket(int port)③DatagramSocket(int port,InetAddress add),都很好理解。通過如下兩個方法來接收和傳送資料:receive(DatagramSocket p); send(DatagramSocket p)。
使用代理伺服器:
代理伺服器的功能就是代理使用者去取得網路資訊,瀏覽器不是直接去向Web伺服器傳送請求,而是向代理伺服器傳送請求。代理伺服器有2個好處:①突破自身IP限制,對外隱藏自身IP地址,包括訪問國外受限站點,訪問國內特定單位、團體的內部資源;②提高訪問速度,代理伺服器提供快取功能可以避免使用者直接訪問遠端主機,從而提高客戶端的訪問速度。
第十八章 類載入機制與反射
JVM程序終止的原因有以下幾種:
①程式執行到最後正常結束;
②程式使用了System.exit()或Runtime.getRuntime().exit()結束程式;
③程式執行過程遇到未知的異常或錯誤;④程式所在的平臺強制結束了JVM程序。
通過反射檢視類資訊:
instanceof關鍵字能夠進行判斷是否是屬於某個類。
在Java程式中獲得Class物件通常有3種形式:
①使用Class類的forName(String clazzName)靜態方法,傳入的字串是某個類的全限定名包括包名。
②呼叫某個類的class屬性來獲取該類的Class物件,例如Person.class將返回Person類對應的Class物件;
③呼叫某個物件的getClass()方法,所有的物件都可以呼叫該方法,該方法返回該物件所屬類對應的Class物件。
對比:第二種方法有兩種優勢,程式碼更安全和程式效能更好,因為無須呼叫方法,所以效能更好。
1、獲取類的構造器:Connstructor<T>getConstructor...有4種方法,不一一介紹。
2、獲取Class對應類所包含的方法:Method []getMethod
3、訪問成員變數:Field[] getFields()
4、獲取所有註解:Annotation[] getAnnotations()
5、獲取所有內部類:Class<?>[] getDeclaredClasses()
6、訪問該Class物件對應類所實現的介面:Class<?>[] getInterfaces
7、訪問所繼承的父類:Class<? super T>getSuperclass()。
使用反射生成並操作物件:例子
publicclassTest8Class {
//定義一個物件池,前面是物件名,後面是實際物件
private Map<String, Object>objectPool=newHashMap<>();
//定義一個方法,只需要傳入一個字串的名字,程式就可以根據該類名生成Java物件
privateObject createObject(String name) throws Exception{
Class<?>clazz=Class.forName(name);
return clazz.newInstance();}
//初始化物件池
publicvoid initPool(String fileName){
try {
FileInputStream fis=newFileInputStream(fileName);
Properties props=newProperties();
props.load(fis);
for(String name:props.stringPropertyNames()){
//每次取出一對 key-value,就執行一次方法
objectPool.put(name, createObject(props.getProperty(name)));}} catch (Exception e) {e.printStackTrace();}}
//從物件池中取出指定的name對應的物件
publicObject getObject(String name){returnobjectPool.get(name);}
publicstaticvoid main(String[] args)throws Exception {
Test8Class t8=newTest8Class();
t8.initPool("WebRoot/obj.txt");
System.out.println(t8.getObject("name"));
System.out.println(t8.getObject("password"));}}
注:使用配置檔案來配置物件,然後由程式根據配置檔案來建立物件的方式非常有用,大名鼎鼎的Spring框架就是採用這種方式大大簡化了JavaEE的開發。Spring採用的是XML的配置方式。