JAVA多線程進階

目錄

線程的同步方式(也叫同步鎖)

  • 方式一: synchronized 代碼塊
synchronized(資源對象){// 實現鎖的對象資源
	// 需要統一執行的原子資源
	// 這段內容是不可分割的
}
  1. synchronized後面的資源對象,最好是線程需要競爭的唯一的
  2. 內部代碼塊中執行的內容是統一整體當結束代碼塊時候鎖被釋放掉
public class TestSynchronized{
	public static void main(Stirng[] args){
		ThreadOne one = new ThreadOne();
		Thread th = new Thread(one);
		Thread th1 = new Thread(one); 
		
		th.start();
		th.start();
	}
}
// 資源
class ThreadOne implements Runnable{
	public void run(){
	// 同步代碼塊上鎖
	synchronized(this){
		for(int i=1;i < 100;i++){
	System.out.print(Thread.currentThread().getName()+":"+i);
		}
	}
	}
}
  • 方式二: 同步方法
 synchronized 返回值類型 方法名稱(形參列表0){// 當前對象(this)加鎖
}
  1. 只有擁有對象互斥鎖標記的線程,才能進入該對象的同步方法中

  2. 線程退出同步方法時,會釋放相應的互斥鎖標記

  3. 已知的java jdk內庫中線程安全類有:StringBuffer、Vector、Hashtable、這幾個類中的公開方法,均爲synchronized修飾的方法

public class TestSynchronized{
	public static void main(Stirng[] args){
		ThreadOne one = new ThreadOne();
		Thread th = new Thread(one);
		Thread th1 = new Thread(one); 
		
		th.start();
		th.start();
	}
}
// 資源
class ThreadOne implements Runnable{
	public  void run(){
	
		start();
	}
	// 方法的同步修飾
	public synchronized void start(){
		for(int i=1;i < 100;i++){
			System.out.print(Thread.currentThread().getName()+":"+i);
			}
	}
}

同步規則

  • 只有調用同步代碼塊的方法,或者同步方法時,才需要對象的鎖標記
  • 如調用不包含同步代碼塊的方法,或普通方法時,則不需要鎖標記,可直接調用

加鎖的場景
- 寫(增、刪、改)操作時候加鎖
- 讀操作時候,不加鎖

死鎖

  • 當第一個線程擁有對象A的鎖標記,並且等待B對象的鎖標記,同時第二個線程擁有B對象鎖標記,並等待A對象鎖標記時,產生死鎖問題。
  • 一個線程可以同時擁有多個對象的鎖標記,當線程阻塞時,不會釋放已經擁有餓鎖標記,由此可能造成死鎖問題。

常見的死鎖具體問題

  • 生產者與消費者問題
    若干個生產者在生產產品,這些產品將提供給若干個消費者去消費,爲了使生產者和消費者能併發執行,在兩者之間設置一個能存儲多個產品的緩衝區,生產者將生產的產品放入緩衝區,消費者從緩衝區中取走產品進行消費,顯然生產者和消費者之間必須保持同步,即不允許消費者到一個空的緩衝區取走產品,也不允許生產者向一個滿的緩衝區中放入產品

線程通信

線程通信時解決思索地有效方法

  • 等待
    • public final void wait()
    • public final void wait(long timeout)
    • 必須在對obj加鎖的同步代碼塊中。在一個線程中,調用obj.wait()時,此線程會釋放其擁有的所有鎖標記。同時此線程阻塞在o的等待隊列中。釋放鎖,進入等待隊列。
  • 通知
    • public finall void notify()
    • public final void notifyAll()
    • 必須在對obj加鎖的同步代碼塊中。從obj的Waiting中釋放一個或全部線程。對自身沒有任何影響

以下是個簡單的案例
也就是李敢敢(丈夫)與趙巖巖(妻子) 銀行卡子母卡同時取錢的案例

public class TestWaitNotify {
	public static void main(String[] args) {
		//臨界資源,被共享的對象
		//臨界資源對象只有一把鎖
		Account acc =new Account("6002","1234",2000);
		//兩個線程對象 共享同一銀行卡資源對象。
		//給定任務後,第二個參數是對線程自定義命名
		Thread husband = new Thread(new Husband(acc),"丈夫");
		Thread wife = new Thread(new Wife(acc),"妻子");
		// 啓動線程
		husband.start();
		wife.start();
	}
}
class A extends Thread{
	
}
// 模擬現實世界的子母卡
class Husband implements Runnable{
	Account acc;
	public Husband(Account acc) {
		this.acc = acc;
	}
	public void run() {
		this.acc.withdrawal("6002","1234",1200);//整體式原子操作
	}
}
class Wife implements Runnable{
	Account acc;
	public Wife(Account acc) {
		this.acc = acc;
	}
	public void run() {
		this.acc.withdrawal("6002","1234",1200);//整體式原子操作
	}
}
class Account{
	String cardNO;// 卡號
	String password;// 密碼
	double balance; // 餘額
	public Account(String cardNO, String password, double balance) {
		super();
		this.cardNO = cardNO;
		this.password = password;
		this.balance = balance;
	}
	// 取款操作(整體是個原子操作,從插卡開始驗證,到取款成功的一系列步驟,不可以缺少或者打亂)
	public synchronized void withdrawal(String no,String pwd,double money) {
		// 等待! --->阻塞狀態
		System.out.println(Thread.currentThread().getName()+"正在讀卡...");
		if(no.equals(this.cardNO) && pwd.equals(this.password)) {
			System.out.println(Thread.currentThread().getName()+"驗證成功...");
			if(money <=  this.balance) {
				try {
					Thread.sleep(1000);//模擬現實世界ATM機器查錢
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				this.balance = this.balance - money;
				System.out.println(Thread.currentThread().getName()+"取款成功!當前餘額爲:"+this.balance);
			}else {
				System.out.println(Thread.currentThread().getName()+"當前卡內餘額不足!");
			}
		}else {
			System.out.println(Thread.currentThread().getName()+"卡號或密碼錯誤!");
		}
	}
}

高級多線程

線程池的原理
  • 現有問題:
    • 線程是寶貴的內存資源、單個線程約佔1MB空間,過多分配易造成內存溢出
    • 頻繁的創建及銷燬線程會增加虛擬機的回收頻率、資源開銷、造成程序性能下降

所以就引入線程池來解決這個問題

  • 線程池:
    • 線程容器,可設定線程分配的數量上限
    • 將預先創建的線程對象存入池中,並重用線程池中的線程對象
    • 避免頻繁的創建和銷燬

將任務提交給線程池,有線程池分配線程、運行任務,並在當前任務結束後服用線程

  • java.util.concurrent 所有的線程池類的祖先

併發編程中很常用的實用工具類

  • Executor: 線程池的頂級接口
  • ExecutorService:線程池接口(常用的更豐富也是個接口),可通過submit()(提交任務代碼)提交一個Runnable任務用於執行,並返回一個標識該任務的Future(結果)
  • Executors工廠類:通過此類可以獲得一個線程池
  • 通過static ExecutorService new FixedThreadPool(int nThreads) 獲取固定數量的線程池。參數:線程池中的線程的數量
  • 通過newCachedThreadPool() 獲得動態數量的線程池,如果不夠則創建新的,沒有上限
public class TestThreadPool {
	public static void main(String[] args) {
	// 利用線程池的工廠方法來生產對象 參數3代表創建三個線程
		ExecutorService es = Executors.newFixedThreadPool(3) ;//手動限定線程池裏的線程數量。
		MyTask a = new MyTask();
		//2.將任務提交到線程池,由線程池調度、執行
		es.submit(a);
		es.submit(a);
		es.submit(a);
		es.submit(a);
		es.submit(a);
		es.submit(a);
	}
}

// 線程任務
class MyTask implements Runnable{
	public void run() {
		for(int i = 1;i<=50;i++) {
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
}
public class TestThreadPool {
	public static void main(String[] args) {
	// 利用線程池的工廠方法來生產對象 參數3代表創建三個線程
		ExecutorService es = Executors.newCachedThreadPool();//自動擴充線程池中線程數量
		MyTask a = new MyTask();
		//2.將任務提交到線程池,由線程池調度、執行
		es.submit(a);
		es.submit(a);
		es.submit(a);
		es.submit(a);
		es.submit(a);
		es.submit(a);
	}
}

// 線程任務
class MyTask implements Runnable{
	public void run() {
		for(int i = 1;i<=50;i++) {
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
}

newCachedThreadPool() 與 newFixedThreadPool(3) 的對比

  • newCachedThreadPool()是動態創建線程,不用對程序進行線程創建數量的預估,而且用戶體檢感較好,但是最大的詬病是線程足夠多時會內存溢出
  • newFixedThreadPool()給與評估過後的線程數量,可能會在用戶過多時候慢些,也可通過維護服務器時修改線程數量等方法增加擴充。
接口Callable
  • 類似於Runnable,兩者都是爲了哪些實例可能被另一個線程執行的類設計的,但是Runnable沒有返回結果
public interface Callable<V>{public  V 	call() throws Exception;

}
  • JDK5加入的,與Runnable接口類似,實現之後代表一個線程任務
  • Callable具有泛型返回值、可以聲明異常
public class TestCallable {
	public static void main(String[] args) {
		// 創建一個線程池
		ExecutorService es = Executors.newFixedThreadPool(3);
		
		MyTask1 task = new MyTask1();
		// 將任務放入線程池
		es.submit(task);
	}
}

class MyTask1 implements Callable<Integer>{
	
	public Integer call() throws Exception{
		
		for (int i = 0; i < 100; i++) {
			if(i == 30) {
				Thread.sleep((int)(Math.random()*1000));
			}
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
		return null;
	}
}

Future接口
  • 異步接收ExecutorService.submit()所返回的狀態結果,當中包含了call()返回值
  • 方法:V get()以阻塞形式等待Future中的異步處理結果(call()的返回值)

示例:兩個線程,併發計算1 ~ 50、51 ~ 100的和,在進行彙總統計

public class TestFuture {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		ExecutorService es = Executors.newFixedThreadPool(3);
		
		MyCall mc = new MyCall();
		MyCall2 mc2 = new MyCall2();
		//通過submit執行提交的任務,Future接受返回的結果
		Future<Integer> result = es.submit(mc);
		Future<Integer> result1 = es.submit(mc2);
		//通過Future的get方法,獲得線程執行完畢後的結果.
		Integer value =  result.get();
		Integer value2 = result1.get();
		System.out.println(value + value2);
	}
}

//計算1~50的和
class MyCall implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		Integer sum = 0;
		for(int i = 1;i<=50;i++) {
			sum = sum + i;
		}
		return sum;
	}
	
}
//計算51~100的和
class MyCall2 implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		Integer sum = 0;
		for(int i =51;i<=100;i++) {
			sum = sum + i;
		}
		return sum;
	}
}
擴充
  • 同步
    • 形容一次方法調用,同步一旦開始,調用者必須等待該方法返回,才能繼續。
    • 單條執行路徑
  • 異步
    • 形容一次方法調用,異步一旦開始,像是一次消息傳遞,調用者告知之後立刻返回。二者競爭時間片,併發執行
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章