JAVA基礎複習(七)

    本着重新學習(看到什麼複習什麼)的原則,這一篇講的是JAVA的多線程。看了諸位大神的解釋後詳細的查了一些東西,記錄下來,也感謝各位在網絡上的分享!!!

    mark一下:https://www.cnblogs.com/television/p/9462214.html(非常清晰,每個問題都很精髓,得弄懂)

    多線程這一塊一直是我比較薄弱的,也是強行拿出來寫一些東西,爲了能夠鞏固和紮實所學,也能更多的去用實踐驗證理論。在日常的JAVA使用中,一般不會考慮到線程的問題,我總會在跟時間和資源相關的調優時想到多線程。今天也從時間和資源的方向思考幾個場景並以此入手,前提就是有一個四人團隊,團隊接了很多的項目

    (1).在團隊中所有的事情都是一個人在做,那麼會出現什麼情況?怎麼處理?

    會出現一人忙碌,三人等待。解決辦法就是四個人一起做。

    (2).如果項目非常複雜,需要消耗很長的時間,那麼會出現什麼情況?怎麼處理?

    會出現單位時間無法完成項目,客戶需要無休止的等待。解決辦法就是四個人一起想辦法處理。

    可以看到,第一個例子就是從資源的角度考慮,我們的程序一直在單線程的執行,對於大部分的簡單應用來說,單線程就能實現。但是對於那些複雜的邏輯來說,單線程去跑程序肯定是不科學的。因爲我們還擁有着很多其他線程可供使用。第二個例子就是從時間的角度考慮,單線程會導致程序的使用者會爲了程序中的某些操作(如大量的I/O操作,文件操作等)而長時間的等待,這也是不合理的。所以最好的解決辦法就是另開線程。這是爲了能夠更快的完成計算任務,更好的完成程序服務,同時也能更有效的利用CPU資源。知道了多線程目的其實也就等於是知道了使用多線程的優勢。那麼接下來先看一下線程和進程的概念。

    1.什麼是進程?

    在計算機中,我們可以同時開啓多個任務,每一個任務之間都是交替執行的,從而達到了一種同時進行的狀態。而每一個任務,我們就可以稱之爲一個進程(但不是每一個任務都只有一個進程),進程是應用程序運行的載體。

    2.什麼是線程?

    在某些進程中還需要同時執行多個子任務,如我們在使用WORD時,我們通過打字鍵入內容,我們還可以看到WORD在對我們鍵入的內容進行拼寫檢查,那麼這些就是子任務,也就是線程。進程和線程之間的關係比較密切,一個進程可以包含一個或多個線程,線程是操作系統調度的最小任務單位,並且如何調度線程是完全由操作系統決定的,也就是說程序自己不能決定執行時間或執行時長。線程是由進程創建出來的,但是線程沒有獨立的地址空間(內存空間),進程擁有。

    實現多任務的方法有多進程模式(每個進程只有一個線程),多線程模式(一個進程有多個線程),多進程+多線程模式。第三種方式是複雜度最高的,暫且不論,單純考慮多線程和多進程的話,進程間相互獨立所以使用多進程的穩定性較於使用多線程會高,在進程之間由於數據是分開的,所以共享比較複雜,需要使用IPC進行進程間通信,並且多進程只需通過增加該程序在CPU中的進程數就能提高性能,但是進程在創建的開銷和通信速度上不佔優勢。相反,線程由於創建在同一個進程之中,所以通信速度快,但是由於數據是在一起的,所以需要考慮數據共享的問題,並且一個線程崩潰將有可能導致整個進程崩潰。所以多進程和多線程在不同的系統中,面對不同的應用程序的需求,也是仁者見仁智者見智的。當然,能互通有無,彼此互補當然是更好的。但是一般情況下,在面對大量計算或者WEB服務時,會優先使用多線程,而集羣等顧名思義需要分佈式的時候優先使用多進程。在JAVA中,一個JAVA程序實際上就是一個JVM進程,JVM會使用一個主線程來執行main方法,但在main方法中又可以開啓多個線程。

    3.如何創建線程?

    創建線程的方式包括繼承Tread類,重寫run方法;實現Runnable接口;實現Callable接口。針對前兩種方式,主線程創建了Tread對象,但是真正執行Tread對象中重寫的run方法的是在主線程使用start方法後JAVA虛擬機開啓的新的子線程。而且只有在Thread對象中,即使用了start方法,纔是開啓一個新線程,直接使用run方法相當於是直接調用一個類內定義方法,並不創建新線程。這三種方式的區別最簡單的就在於繼承Thread類後便無法再繼承其他類了,但是編寫簡單,獲取線程號也只需要使用this.getName(),而後兩者的劣勢在於訪問線程必須使用Thread.currentThread()方法,並且較爲複雜。但是優勢就是避免了單繼承,多個線程間可以共享一個代理對象(target對象),對多線程訪問數據的場景非常合適。而後兩者之間的區別就在於Callable接口有返回值Future,而Runnable接口沒有返回值。如下所示,我們還可以通過線程池來管理已經創建的線程對象。

package com.day_7.excercise_1;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class TryThread{
	
	public static class ExtendsThread extends Thread{
		@Override
		public void run() {
			System.out.println("Extends Thread Type : new Thread");
		}
	}
	public static class ImplementRunnable implements Runnable{
		@Override
		public void run() {
			System.out.println("Implement Runnable Type : new Thread");
		}
	}
    public static class ImplementCallable implements Callable<String> {
        @Override
        public String call() {
        	return "Implement Callable Type : new Thread";
        }
    }
	public static void main(String[] args) throws Exception {
		// 1.繼承Thread類
		ExtendsThread extendsThread = new ExtendsThread();
		extendsThread.start();
		System.out.println(extendsThread.getName());
		// 2.實現Runnable接口
		Thread implementRunnable = new Thread(new ImplementRunnable());
//		ImplementRunnable implementRunnable = new ImplementRunnable();
		implementRunnable.start();
		System.out.println(Thread.currentThread());
		// 3.實現Callable接口
        FutureTask<String> futureTask = new FutureTask<String>(new ImplementCallable());
        new Thread(futureTask).start();
		System.out.println(Thread.currentThread());
		
        // 創建線程池
        ExecutorService pool = Executors.newFixedThreadPool(3);
        Future<String> future = pool.submit(new ImplementCallable());
        pool.submit(new ImplementRunnable());
        pool.submit(new ExtendsThread());
        System.out.println(future.get());
		
        // 中斷線程
        extendsThread.interrupt();
        implementRunnable.interrupt();
        futureTask.cancel(true);
        pool.shutdown();
		
	}
}

    線程的生命週期

    (1).初始狀態:當程序使用new關鍵字創建一個線程後,該線程還不能使用,只是被JVM分配了內存進行初始化。

    (2).就緒狀態:當線程對象使用了start方法後該線程就處於就緒狀態。就緒狀態在等待CPU進行調度,也就是等待CPU分配資源,誰先獲得CPU資源,誰先開始執行。

    (3).運行狀態:進入運行狀態代表着就緒狀態的該線程獲得了CPU資源,並開始執行run方法內的方法體,run方法內定義了線程的具體操作和功能實現。若再給定時間內沒有完成run方法,將會重新回到就緒狀態。

    (4).阻塞狀態:處於運行狀態的線程可能因爲sleep方法或wait方法等進入阻塞狀態。在阻塞狀態的線程將暫時停止運行,並歸還CPU資源。並只有在解決阻塞的具體原因時重新進入就緒狀態,等待CPU重新分配資源。

    (5).終止狀態:線程正常執行run方法結束,出現異常終止或使用stop方法強制結束(不推薦),會進入終止狀態。

    JAVA線程的狀態包括:New(創建),Runnable(運行中),Blocked(阻塞),Waiting(等待),Timed Waiting(計時等待),Terminated(終止)。

    與此同時,在JAVA中存在CPU資源分配的監控器,即線程調度器,用於監控處於就緒狀態 的所有線程。並可以通過判斷線程的優先級來決定哪些線程有優先執行的權利。JAVA中優先級可以用(1~10)表示,低優先級(1~4),默認優先級(5),高優先級(6~10)。可以使用Thread.setPriority(int n)來進行優先級的設定,優先級高的線程被調用的優先級最高,但是不能通過設置優先級的高低來保證功能的執行順序。 

    4.什麼是線程安全與線程不安全?

    在使用多線程的情況下,由於不同的線程要訪問同一份數據,所以會出現對於數據訪問的安全問題,即能否在使用多線程時也能保持數據的順序調用申請值的一致。線程安全也就是數據結果一致,並且某一個線程對該數據進行修改後,另一個線程得到的是修改值而不是原值。線程不安全則相反。線程安全需要確保數據的一致性,所以需要對數據進行控制,增加了開銷,但也保證了線程間的數據讀寫順序。線程安全的類包括不變類(String,Integer,LocalDate,因爲其一旦創建,實例內部的成員變量不能改變,所以多線程不能寫只能讀,不需要同步),沒有成員變量的類(Math,因爲在類中只提供了靜態方法,自身沒有成員)和正確使用synchronized類(StringBuffer)。非線程安全的類不能在多線程中共享實例並修改(ArrayList),但是可以在多線程中只讀方式共享。那如何保證線程安全呢?

讀取方法通常也需要同步

對共享變量進行寫入時,必須保證是原子操作(即不能被中斷的一個或一系列操作),JVM規範了幾種原子操作(基本類型(long和double除外)的賦值),引用類型的賦值。

    (1).使用局部變量,局部變量不需要同步。

    (2).Synchronized關鍵字:同步的本質就是給指定的對象加鎖,加鎖對象必須是同一個實例

    

  加鎖對象數量 多線程訪問同一個對象同步代碼塊
對象鎖(普通方法) 調用方法的這一個對象 不受影響,每一個對象擁有自己的鎖
類鎖(類) 該類的所有對象 受影響,所有對象共享同一個鎖
靜態方法鎖(同類鎖) 該類的所有對象 受影響,所有對象共享同一個鎖
同步代碼塊 調用代碼塊的這一個對象 不受影響,每一個對象擁有自己的鎖

    當synchronized關鍵字修飾一個時,稱之爲類鎖或全局鎖。其修飾關鍵字是static sychronized,含義是無論創建多少個該類的對象,都共享同一個鎖,所以每一個對象都會按照要求順序進行操作。

package com.day_7.excercise_1;

public class TrySynchronized5 extends Thread{
    public static int salary = 0;
    public void run(){
    	// 1.
        addSalary();
        // 2.
//        minusSalary();
    }
    public static synchronized void addSalary() {
    	System.out.println("================");
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName()+"線程調用該方法開始時salary值爲:"+salary);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        salary += 1;
        System.out.println(Thread.currentThread().getName()+"線程調用該方法結束後salary值爲:"+salary);
    }
    public synchronized void minusSalary() {
    	System.out.println("================");
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName()+"線程調用該方法開始時salary值爲:"+salary);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        salary -= 1;
        System.out.println(Thread.currentThread().getName()+"線程調用該方法結束後salary值爲:"+salary);        
    }
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
        	TrySynchronized5 trySynchronized5 = new TrySynchronized5();
            new Thread(trySynchronized5).start();
        }
    }
}

    如上,我在類中定義了一個使用static synchronized修飾的加法操作,一個僅用synchronized修飾的減法操作(即場景1和場景2)。而後在主函數中我循環創建對象,並開啓線程。在方法中我需要該方法在等待500ms後輸出一句語句,並且有對應的開頭和結束。那麼在使用減法操作時,結果如左圖,並且沒有任何等待時間,這說明線程之間爭搶了資源,導致了數據的混亂。而在使用加法操作時,結果如右圖,是順序出現的,並且有對應的等待時間,說明線程之間沒有進行爭搶,即每一個線程執行時其他線程都是在等待資源鎖釋放的。這就是類鎖,使得該類的所有對象使用同一個鎖進行資源分配和等待。

    當synchronized關鍵字修飾實例方法(不包括靜態方法)時,稱之爲實例鎖或對象鎖。其作用是給當前實例加鎖,可以將整個方法變成同步代碼塊。當一個線程使用同步代碼前需要首先獲取當前實例的鎖。與類鎖不同的是,對象鎖每個對象都擁有自己的鎖,多線程訪問同一個對象的同步代碼塊時不受影響。

package com.day_7.excercise_1;

public class TrySynchronized6 extends Thread{
    int salary = 0;
    public void run(){
    	// 1.
        addSalary();
        // 2.
//        minusSalary();
    }
    public synchronized void addSalary() {
    	System.out.println("================");
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName()+"線程調用該方法開始時salary值爲:"+salary);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        salary += 1;
        System.out.println(Thread.currentThread().getName()+"線程調用該方法結束後salary值爲:"+salary);
    }    
    public void minusSalary() {
    	System.out.println("================");
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName()+"線程調用該方法開始時salary值爲:"+salary);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        salary -= 1;
        System.out.println(Thread.currentThread().getName()+"線程調用該方法結束後salary值爲:"+salary);
    }
    public static void main(String[] args) {
    	TrySynchronized6 trySynchronized6 = new TrySynchronized6();
        for (int i = 0; i < 10; i++) {
            new Thread(trySynchronized6).start();
        }
    	// 3.
//        TrySynchronized6 trySynchronized62 = new TrySynchronized6();
//        for (int i = 0; i < 10; i++) {
//            new Thread(trySynchronized62).start();
//        }
    }
}

    如上,還是相同的例子,但是這次在main函數中是創建一個對象,循環開啓線程。相同的結果,減法操作時,結果如左圖,加法操作如右圖,並且如果我將場景3的註釋打開,也會出現,相當於是同時創建了兩個對象,兩個對象都要進行相同的等待和操作,結果是兩個對象調用方法都會有條不紊的順序執行加法操作,減法操作則會完全混亂,我就不放圖片了。

    當synchronized關鍵字修飾靜態方法時,使用的應該是synchronized關鍵字和static修飾符,這就相當於是使用了一個類鎖,故作用範圍等與類鎖相同。

    當synchronized關鍵字修飾代碼塊時,解決的是過多的同步方法會影響JAVA程序運行效率,所以儘量在使用同步時更加精準範圍,故使用synchronized修飾代碼塊來縮小覆蓋代碼面積。

    (3).使用ThreadLocal類,ThreadLocal使得每一個線程擁有自己的一套變量(每一個Thread線程內部都有一個Map,Map中存儲的是本地對象作爲key,變量副本作爲value),也就是說ThreadLocal會爲每一個線程提供相互獨立的變量副本,所以每一個線程間在任意時刻都互不影響。但是ThreadLocal也有着自己的問題,如無法處理需要更新共享變量的場景,還有若在使用ThreadLocal後不使用remove方法,會導致內存泄露

package com.day_7.excercise_1;

public class TrySynchronized7 { 
    private static ThreadLocal<Integer> numList = new ThreadLocal<Integer>() {  
        public Integer initialValue() {  
            return 0;  
        }  
    };  
    public int getNext() {  
        numList.set(numList.get() + 1);  
        return numList.get();  
    }  
    public static class Client extends Thread {  
        private TrySynchronized7 trySynchronized7;  
  
        public Client(TrySynchronized7 trySynchronized7) {  
            this.trySynchronized7 = trySynchronized7;  
        }  
  
        public void run() {  
            for (int i = 0; i < 3; i++) {  
            	try {
					Thread.sleep(500);
					System.out.println("thread[" + Thread.currentThread().getName() + "] --> num["  
	                         + trySynchronized7.getNext() + "]");  
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
            }  
        }  
    }
  
    public static void main(String[] args) {  
    	TrySynchronized7 trySynchronized7 = new TrySynchronized7();  
    	Client client_1 = new Client(trySynchronized7);  
    	Client client_2 = new Client(trySynchronized7);  
    	Client client_3 = new Client(trySynchronized7);  
    	client_1.start();  
    	client_2.start();  
    	client_3.start();  
    	numList.remove();
    }  
  
}

    (4).使用Lock接口:在使用synchronized關鍵字時,由於無法直觀判斷該線程是否獲取了鎖的狀態,並且在多線程操作時,若某個線程在使用資源時阻塞,則其他線程只能一直等待,這就會形成死鎖的局勢,加之synchronized線程在執行完同步代碼塊或者在出現異常後會自動釋放鎖。故可以使用Lock,Lock中存在tryLock()方法,通過判斷返回值(true/false)就可以判斷該線程是否獲取到了鎖,並且可以或者說必須手動釋放鎖(一般在try/catch/finally的finally中釋放,加鎖lock(),釋放鎖unlock())。並且Lock鎖不會一直等待鎖資源,在Lock中有多種獲取鎖的方式。

    在Lock中存在的只有6個方法,即lock(用來獲取鎖,若鎖已經被其他線程獲取,則等待),lockInterruptibly(用來獲取鎖,若鎖被其他線程獲取,則當前線程被禁用線程調度,並處於interrupt中斷狀態),tryLock(用來嘗試獲取鎖,若獲取成功返回true,獲取失敗返回false,在無法獲取鎖資源時不會一直等待而是會立即返回),tryLock(long time,TimeUnit unit)(用來嘗試獲取鎖,若獲取成功或者在time時間內獲取成功,則返回true,在獲取失敗情況下等待time時間,若在time時間內還未獲取到鎖,則返回false),unlock(用來釋放鎖資源),newCondition(用來返回一個多線程間協調通信的工具類,用於監管線程)。常用的是實現了Lock接口的ReentrantLock類,ReentrantLock是一個可以重入鎖,這一點與synchronized關鍵字使用的鎖相同。還是之前的例子,改成使用ReentrantLock,結果與使用synchronized關鍵字是相同的,但是可見的是顯式的開鎖關鎖,更加清晰。

package com.day_7.excercise_1;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TryLock2 extends Thread{
    int salary = 0;
    private Lock lock = new ReentrantLock();
    public void run(){
        addSalary();
    }
    public void addSalary() {
    	lock.lock();
    	System.out.println("================");
        try {
            Thread.sleep(500);
            System.out.println(Thread.currentThread().getName()+"線程對象調用該方法開始時salary值爲:"+salary);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
			lock.unlock();
		}
        salary += 1;
        System.out.println(Thread.currentThread().getName()+"線程調用該方法結束後salary值爲:"+salary);
    }    
    public static void main(String[] args) {
    	TrySynchronized6 trySynchronized6 = new TrySynchronized6();
        for (int i = 0; i < 10; i++) {
            new Thread(trySynchronized6).start();
        }
    }
}

    (5).volatile關鍵字:在使用volatile關鍵字時,是保證了線程在每次使用變量時,都會讀取到變量修改後的最終值,即一個線程修改了變量的值後,其他線程都是立即使用新值的(基於CPU指令,內存屏障)。volatile只能保證單次讀/寫的原子性,而對於i++(讀取變量i的值——>變量i數值加1——>結果回寫)這種操作,不能保證其原子性。只有在變量狀態獨立時才使用volatile關鍵字。可以配合CAS(java.util.concurrent包建立在CAS之上,沒有CAS就沒有併發包)使用,但是CAS開銷很大,仍然只能保證一個共享變量原子性,並存在ABA問題。(沒看懂,mark一下。。。)

    5.什麼是守護線程?

    守護線程是爲其他線程服務的線程,所有非守護線程都執行完畢後,虛擬機退出,否則即使主線程已經執行結束,程序也不會結束。守護線程不能持有資源,但是我們一般把守護線程用作日誌或者監控線程。要注意守護進程的設定時間一定要在該線程開啓之前,也就是先thread.setDaemon(true)而後thread.start(),因爲已經開啓的線程無法設定爲守護線程。如下所示,在不使用場景1,即不設定守護線程時,主線程方法執行結束,KidThread線程依然在執行。而在設定了守護線程後,會在主線程結束後便結束。

package com.day_7.excercise_1;

public class TryDaemon {
	public static void mainMethod() {
		for (int i = 0; i < 10; i++) {
			try {
				Thread.sleep(500);
			} catch (Exception e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + ":" + i);
		}
	}
	public static class KidThread extends Thread {
		public void run() {
			while (true) {
				try {
					Thread.sleep(1000);

				} catch (Exception e) {
					// TODO: handle exception
				}
				System.out.println(Thread.currentThread().getName());
			}
		}
		public static void main(String[] args) {
			Thread thread = new KidThread();
			// 1.
//			thread.setDaemon(true);
			thread.start();
			mainMethod();
			System.out.println("主線程執行完畢...");

		}
	}
}

    說實話寫到這裏我已經有點蒙了,畢竟多線程的東西還太多太多,諸如各種鎖,volatile和CAS的底層問題等很多的內容都還沒有涉及到,近期去看個視頻課程理解一下,找時間再開寫這塊知識。同樣,感謝網上的各種資源,讓我有了各種瞎嘗試的可能性和明確錯誤原因的可能性。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章