Java的異常機制與多線程

【Java總結】-【提高篇(第六週)】 後續每週總結一次

目錄

異常

  • 概念:程序在運行過程中出現的特殊情況
異常的分類
  • Throwable:可拋出的一切錯誤或異常的父類,位於java.lang包下
    • Error:JVM、硬件、執行邏輯錯誤、不能手動處理
    • Exception:程序在運行和配置過程中產生的問題,可處理
      • RuntimeException:運行時異常,可處理,可不處理
      • CheckedException: 受查異常,必須手動處理
異常的產生
  • 自動拋出異常:當程序在運行時遇到不符合規範的代碼或結果時,會產生異常。
  • 手動拋出異常:throw new 異常類型(“實際參數”)
  • 產生異常結果:相當於程序因異常而終止
異常的傳遞
  • 按照方法的調用鏈條反向傳遞,如果始終沒有處理異常,最終會由JVM進行默認處理(打印堆棧跟蹤信息)
  • 受查異常
    • throws異常類型
    • 聲明異常
    • 修飾在方法參數列表後端
  • 由於運行時異常,是可處理可不處理的,所以無需聲明異常
異常的處理
  • 常見的異常處理結構
1.try-catch
try{

}catch(Exception e){

}
2.try-catch-catch...
try{

}catch(){

}catch(){

}...catch(Exception e){

}
3.try-catch-finally
try{

}catch(Exception e){

}finally{

}
4.try-finally
try{

}finally{

}
5.try()-catch
try(){

}catch(Exception e){

}
等等
異常方法的覆蓋
  • 方法的參數、返回值類型、方法名必須和要重寫的父類保持一致
  • 子類覆蓋方法的權限必須大於等於父類方法的權限
  • 父類方法沒有聲明異常時則子類方法覆蓋時不能聲明
  • 父類方法聲明異常時,則子類方法可以聲明異常也可以不聲明
  • 父類方法聲明異常是,則子類方法可以聲明的異常必須是父類方法異常的子異常或其本身
  • 在編譯期間,調用父類中聲明的方法是由異常的,需要處理
自定義異常
  • 需要繼承Exception或Exception的子類,常用RunntimeException
  • 必須要提供構造方法
    • 無參構造方法
    • String message 方法信息
反編譯
  • javap-verbosr 文件名稱.class > 自定義文件名稱.bytecode

異常的綜合展示

public class TestException {
	public static void main(String[] args) {
		// 運行時異常
		isGreaterZero(0);
		// 受查時異常,可以拋出,可以捕獲,這裏捕獲下
		try {
			isGreaterFive(4);
		}catch(FiveGreaterThan e) {
			System.out.println(e.getMessage());
		}
		
	}
	// 運行
	//判斷是否大於0
	public static void isGreaterZero(int a) {
		if(a>0) {
			System.out.println(a);
		}else {
			System.out.println("參數有誤");
			throw new ZeroGreaterThan("參數有誤");
		}
	}
	//受查
	//判斷是否輸入大於5
	public static void isGreaterFive(int a) throws FiveGreaterThan{
		if(a > 5) {
			System.out.println(a);
		}else {
			System.out.println("參數有誤");
			throw new FiveGreaterThan("參數有誤");
		}
	}
}
//自定義一個運行時異常類
class ZeroGreaterThan extends RuntimeException{
	
	public ZeroGreaterThan(String message){
		super(message);
	}
	
}
class FiveGreaterThan extends Exception{
	
	public FiveGreaterThan(String message){
		super(message);
	}
	
}
參數有誤
Exception in thread "main" com.qfedu.review.exception.ZeroGreaterThan: 參數有誤
	at com.qfedu.review.exception.TestException.isGreaterZero(TestException.java:22)
	at com.qfedu.review.exception.TestException.main(TestException.java:6)

線程

進程的介紹
  • 運行時的程序,叫做進程
  • 單核CPU在任一時間點上,只能運行一個進程
    • 單核 宏觀並行,微觀串行
  • window10系統查看本機幾核CPU
    • 1.win+r 輸入 wmic
    • 2.cpu get NumberOfCores
線程的介紹
  • 輕量級進程
  • 程序中的一個順序控制流程,也是cpu的基本調度單位
  • JVM虛擬機是一個進程,默認包含主線程(Main函數),可通過代碼創建多個線程與main併發執行
  • 進程由多個線程組成,彼此間完成不同工作、交替執行,稱爲多線程
線程的組成
  • 任何一個線程都具有基本的組成部分
    • CPU時間片:操作系統(os)會爲每個線程分配執行時間
    • 運行數據:
      • 堆空間: 存儲線程需要使用的對象,多個線程可以共享堆中的對象
      • 棧空間: 存儲線程需要使用的局部變量,每個線程都有獨立的棧
線程的創建
  • 方式一:繼承Thread類
    • 創建一個類繼承Thread類,此時此類就是一個線程
    • 重寫run方法
    • 子類對象調用start()
  • 方式二:實現Runnable接口
    • 創建一個類,實現Runnable接口,實現後的類是一個資源任務,而不是一個線程
    • 重寫run方法
    • new一個接口類對象
    • 創建線程對象
    • 調用start()方法
    • 實現Runnable接口,實現的後的類屬於是資源而不是一個線程,可以多個線程調用
  • 方式三:實現Callable接口
    • 實現Callable接口
    • 重寫call方法
    • 創建線程池
    • 放入線程池
    • 調用線程池的submit()
public class TestThread {
	public static void main(String[] args) {
		//方式一的調用
		Thread1 t1 = new Thread1();
		t1.start();
		//方式二的調用
		Runnable run = new Thread2();
		Thread t2 = new Thread(run);
		t2.start();
		//方式三的調用
		//1.創建線程池
		ExecutorService eService = Executors.newFixedThreadPool(5);
		eService.submit(new Thread3());
	}
}
// 方式一 繼承Thread
class Thread1 extends Thread{
	
	@Override
	public void run() {
		System.out.println("我是一個線程以繼承Thread方式實現");
	}
}
// 方式二 實現Runnable
class Thread2 implements Runnable{
	
	public void run() {
		System.out.println("我是一個資源實現Runnable接口方式實現");
	}
}
// 方式三 實現Callable<E> 注意此方法有返回值
class Thread3 implements Callable<String>{

	@Override
	public String call() throws Exception {
		// TODO Auto-generated method stub
		System.out.println("我是一個資源實現Callable接口方式實現");
		return "1";
	}
	
	
}
線程常見的方法
休眠:
public static void sleep(long millis)
當前線程主動休眠millis

放棄:
public static void yield()
當前線程主動放棄時間片,回到就緒狀態,競爭下一次時間片

結合:
public final void join()
允許其他線程加入到當前線程中
  • join是無限期等待:等待條件爲調用join方法的線程執行完畢後!再進入就緒狀態,競爭時間片
三種方法的區別
  • 實現Runnable,Callable的時候是資源任務,資源任務是可以多個線程對象共享
  • 任務是可以多個線程對象共享
  • 更靈活 Callable使用時有返回值,更有利於並行計算
線程運行中的狀態
  • 初始狀態:線程對象被創建即爲初始化狀態,只在堆中開闢內存,與常規對象無異。
  • Ready(就緒狀態):new狀態調用start()方法後達到Ready就緒狀態等待OS選中,並分配時間片
  • Running(運行狀態):就緒狀態或的時間片後進入運行狀態,如果時間片到期就回到就緒狀態
  • 有限期等待狀態:運行狀態調用sleep()方法達到限期等待狀態,時間到期後達到Ready就緒狀態
  • 無限期等待狀態:運行狀態調用join()方法達到無限期等待時間到期後達滿足條件就到就緒狀態競爭時間片
  • 阻塞狀態:synchronized同步鎖或者其他鎖使得線程達到就緒狀態
  • 終止狀態:運行狀態運行結束後達終止狀態

JDK中將運行狀態和就緒狀態統稱Runnable狀態

線程安全問題
線程不安全情況
  • 當多線程併發訪問臨界資源時,如果破壞了原子操作,就可能會造成數據不一致
  • 原子操作:不可以分割的多步操作,被稱爲統一整體,從順序和步驟不可打亂或者缺省
  • 臨界資源:共享資源(同一對象),一次僅允許一個線程使用,才保證其正確性
解決方法 加鎖
  • 同步代碼塊

格式:

synchronized(資源對象){
	需要同步的代碼原子
	不可分割的操作
}
  • 同步方法
synchronized 返回值類型 方法名稱(形參列表){
//當前對象(this)加鎖
}
  • 同步規則
    • 只有在調用包含代碼同步塊的方法,或者同步方法時,才需要對象的鎖標記
    • 如調用不包含同步代碼塊的方法,或普通方法時,則不需要鎖標記,可直接調用
已知的JDK中的線程安全類
  • StringBuffer
  • Vector
  • Hashtable
  • 上面三個類中的公開方法,均爲synchronized修飾的同步方法
加鎖的場景
  • 寫(增、刪、改)操作加鎖
  • 讀操作、不加鎖
死鎖問題
  • 當第一個線程擁有A對象鎖標記時,同時請求B對象鎖標記,同時第二個線程擁有B對象鎖標記,請求A對象鎖標記時,產生死鎖。
  • 一個線程可以同時擁有多個對象標記,當前成阻塞時,不會釋放已經擁有的鎖標記,由此可能造成死鎖
線程通信
  • 等待
public final void wait()
public final void wait(long timeout)
必須在對obj加鎖的同步代碼塊中。在一個線程中,調用obj.wait()時,
此線程會釋放其擁有的所有鎖標記。同時此線程阻塞在o的等待隊列中。釋放鎖,進入等待隊列。
  • 通知
public final void notify()
public final void notifyAll()
必須在對obj加鎖的同步代碼塊中。從obj的waiting中釋放一個或者全部線程。對自身沒有任何影響

使用展示:

public class TestThreadMethod {
	public static void main(String[] args) throws InterruptedException {
		
		Thread t1 = new Thread(new TR(),"線程1");
		t1.start();
		Thread t2 = new Thread(new TR(),"線程2");
		t2.start();
		//for (int i = 0; i < 50; i++) {
			//使得當前線程停止 n秒進入到有限期等待狀態
			//Thread.sleep(1000);
			//將t1線程加入主線程,當t1線程執行完成後,主線程繼續執行 ,調用後進入無限期等待
			//if(i==25){
			//	t1.join();	
			//}
			//if(i==10) {
				// 當前線程放棄本次時間片回到就緒狀態進行競爭下一次時間片
				//Thread.yield();
			//}
		
		//}
	}
}

class TR implements Runnable{
	@Override
	 public void run() {
		// TODO Auto-generated method stub
		synchronized(this) {
			for (int i = 0; i < 50; i++) {
				
				System.out.println(Thread.currentThread().getName()+"線程的第"+i+"次");
			}
			
		}
		
	}
	
}
class TR1 implements Runnable{
	@Override
	synchronized public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 50; i++) {
			
			System.out.println(Thread.currentThread().getName()+"線程的第"+i+"次");
		}
	}
	
} 
public class TestThreadMethod2 {
	public static void main(String[] args) throws InterruptedException {
		Resource resource = new Resource();
		new TR4(resource);
		new TR5(resource);
		
	}
}
// 
class Resource{
	int n;
	boolean valueSet = false;
	synchronized int get() {
        if (!valueSet)
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println("InterruptedException caught");
            }
        System.out.println("Get: " + n);
        valueSet = false;
        notify();
        return n;
    }

    synchronized void put(int n) {
        if (valueSet)
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println("InterruptedException caught");
            }
        this.n = n;
        valueSet = true;
        System.out.println("Put: " + n);
        notify();
    }
}
class TR4 implements Runnable{
	 private Resource r;

	 TR4(Resource r) {
	        this.r = r;
	        new Thread(this, "TR4").start();
	    }

	    public void run() {
	        while (true) {
	           r.get();
	        }
	    }
	
}
class TR5 implements Runnable{
	 private Resource r;

	 TR5(Resource r) {
	        this.r = r;
	        new Thread(this, "TR5").start();
	    }

	    public void run() {
	    	int i =0;
	        while (true) {
	           r.put(i++);
	        }
	    }
	
} 

高級多線程

現有問題
  • 線程是寶貴的內存資源、單個線程約佔1MB的空間,過多分配容易造成內存溢出
  • 頻繁的創建及銷燬小城會增加虛擬機的回收頻率、資源開銷、造成程序性能下降
線程池概念
  • 1.線程容器,可設定線程分配的數量上限
  • 2.將預先創建的線程對象存入池中,並重用線程池中的線程對象
  • 3.避免頻繁的創建和銷燬
線程池原理
- 將任務提交給線程池,由線程池統一分配線程、運行任務,並在當前任務結束後複用線程。
java.util.concurrent包介紹
Executor:線程池的頂級接口
  • public interface ExecutorService extends Executor
  • ExecutorService:線程池接口(常用的更豐富也是個接口),可通過submit() 提交任務代碼,提交一個Runnable任務用於執行,並返回一個標識該任務的Future(結果)
Executors工廠類:通過此類可以獲得一個線程池
  • 通過static ExecutorService new FixedThreadPool(int nThreads)獲取固定數量的線程池。參數:線程池中線程個數
  • 通過newCachedThreadPool()獲得動態數量的線程池,如果不夠則創建新的,沒有上限
Future接口
  • 結果類型返回這個未來的 get方法
  • Future表示異步計算的結果
  • V get() 等待,如果需要計算完成,然後檢索其結果。
  • 異步接收ExecutorService.submit()所返回的狀態結果,當中包含了call()的返回值
同步與異步
- 形容一次方法調用,同步一旦開始,調用者必須等待該方法返回,才能繼續。 - 形容一次方法調用,異步一旦開始,像是一次消息傳遞,調用者告知之後立刻返回。二者競爭時間片,併發執行 ##### 鎖
  • 在java.util.concurrent.locks 包下
  • Lock接口
    • JDK5加入的,與synchronized比較,顯示定義,結構更靈活
    • 提供更多實用性方法,功能更強大、性能更優越
  • 方法
    • void lock()獲取鎖
    • void lockInterruptibly()
    • Condition newCondition()
      返回綁定到此Lock示例的新Condition實例
    • boolean tryLock()
      僅在調用時鎖爲空閒狀態才能獲取該鎖
    • boolean tryLock(long time,TimeUnit unit)
      如果鎖在給定的等待時間內控先,並且當前線程未被只能高端,則獲取鎖
    • void unlock() 釋放鎖
  • 實現類
    • ReentrantLock
      • Lock接口的實現類,與synchronized一樣具有互斥鎖功能
      • 在遞歸時候上鎖會重複上鎖,要注意在遞歸的時候依此釋放鎖
      • 也叫重入鎖
    • ReentrantReadWriteLock
      • 一種支持一寫多讀的同步鎖,讀寫分離,可分別分配讀鎖、寫鎖
      • 支持多次分配讀鎖,使多個讀操作可以併發執行
      • 互斥鎖,互斥規則:
        • 讀-寫:互斥,讀阻塞寫,寫阻塞讀
        • 寫-寫:互斥,阻塞
        • 讀-讀:不互斥,不阻塞
        • 在讀操作遠遠高於寫操作的環境中,可在保障線程安全的情況下,提高運行效率
Collections中的線程安全的工具方法
Collections工具類中提供了多個可以獲得線程安全集合的方法
public static <T> Collection<T> synchronizedCollection(Collection<T> c)
public static <T> List<T> synchronizedList(List<T> c)
public static <T> Set<T> synchronizedSet(Set<T> c)
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> c)
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> c)
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> c)
JDK1.2提供,接口統一、維護性高,但性能沒有提升,均以synchronized實現
線程安全的集合
  • copyOnWriteArrayList
    • 線程安全的ArrayList, 加強版讀寫分離
    • 寫有鎖,讀無鎖,讀寫之間不阻塞,優先與讀寫鎖
    • 寫入時,先copty一個容器副本、在添加新元素,最後替換引用
    • 使用方式與ArrayList無異
    • List list = new CopyOnWriteArrayList();
    • 該集合通過對數組複製創建 不會對同一個資源進行訪問
  • CopyOnWriteArraySet
    • 線程安全的Set, 底層使用CopyOnWriteList實現
    • 唯一不同在於,使用addIfAbsent()添加元素,會遍歷數組。
    • 如存在元素,則不添加(扔掉副本)
    • Set set = new CopyOnWriteArraySet<>();
  • ConcurrentHashMap
    • 初始容量默認16端(Segment),使用分段鎖設計
    • 不對整個Map加鎖,而是爲每個Segment加鎖
    • 當多個對象存入同時Segment時,才需要互斥
    • 最理想狀態爲16個線程同時進行
    • 與HashMap操作無異
    • Map<String, String> hashMap = new ConcurrentHashMap<String,String>();
    • JDK1.7的時候是hash相同的先判斷key,如果有key相同則覆蓋,如果沒有則到此鏈表最後一個
    • JDK1.8 使用(CAS)交換算法和同步鎖
      • CAS交換算法
        V:要更新的變量
        E:預期值
        當你的V == E時, V=N
        如果在修改過程中,V已經發生變化了,V!=E,則取消當前賦值操作!做下一次操作
    • 同步鎖鎖的是表頭對象 拿到鎖的對象要先做節點遍歷,查看有沒有相同的key,相同覆蓋,不同,則掛在最後一個
Queue隊列
  • 列表,尾部添加(指定下標)
    ​鏈表,頭部添加
    ​隊列,FIFO
  • 是Collection的子接口,表示隊列FIFO(先進先出)
  • 基本的Collection操作都有還有獨特的插入、提取、檢查每個方法都有兩種形式
    • 一種是拋異常
    • 一種是返回一個特殊值(null 或 false)
    • add(e) offer(e) 添加 異常/特殊值
    • remove() poll() 刪除 異常/特殊值
    • element() peek() 檢查 異常/特殊值
  • 子實現ConcurrentLinkedQueue類
    • 線程安全、可高效讀寫的隊列,高併發下性能最好的隊列
    • 無鎖、CAS比較交換算法,修改的方法包含三個核心參數(V,E,N)
    • V:要更新的、E:預期值、N:新值
    • 只有當V==E時,V=N;否則表示已被更新過,則取消當前操作
  • BlockingQueue接口
    • Queue的子接口,阻塞的隊列,增加了兩個線程狀態爲無限期等待的方法
    • void put(E e)//將指定元素插入此隊列中,如果沒有可用空間,則等待
    • E take() // 獲取並移除此隊列頭部元素,如果沒有可用元素,則等待
    • 可以解決消費者、生產者問題
    • 子實現類
      • ArrayBlockingQueue:數組結構實現,有界隊列(手動實現界限)
      • LinkedBlockingQueue 無界隊列最大有Integer.MAX_VALUE
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章