複習18:線程

多線程

程序、進程、線程的區別

  • 程序:本地文件
  • 進程:正在運行的程序,進程至少有一個線程
  • 線程:一個進程中的多個“同時”進行的任務,兩個以上線程稱爲多線程

多線程的“同時”運行:多線程並非是同時運行的。CPU負責執行線程,而一個CPU在一段時間內只能運行一個線程,之所以會形成“同時”運行的假象,原因在於CPU切換線程的速率極其快(毫秒單位),假設現在有A、B、C三個線程:

  • A線程運行10ms
  • B線程運行10ms
  • C線程運行10ms

三個線程之間來回切換,在外界看來是同時運行,這樣的行爲叫做併發,真正的同時運行叫做並行

  • 併發:在一段時間內來回切換任務,造成同時運行的假象
  • 並行:所有任務同時運行

實現多線程的方式

實現多線程有三種方式。

繼承Thread類
package day20191203;

public class Demo01 {
	public static void main(String[] args) {
		Thread t = new MyThread01();
		/**
		 * 注意:開啓線程需要調用的是start(),而不是重寫後的run()
		 * start():自動調用run()
		 */
		t.start();
	}
}
class MyThread01 extends Thread{

	@Override
	public void run() {
		/**
		 * run():線程執行的任務
		 */
		for(int i=0;i<100;i++) {
			System.out.println(this.getName()+":"+i);
		}
	}
}
實現Runnable接口
package day20191203;

public class Demo02 {
	public static void main(String[] args) {
		/**
		 * 聲明線程對象時需要傳入一個實現了Runnable接口的類對象作爲參數
		 */
		Thread t = new Thread(new MyThread02());
		t.start();
	}
}
class MyThread02 implements Runnable{

	@Override
	public void run() {

		for(int i=0;i<100;i++) {
			System.out.println(Thread.currentThread().getName()+":"+i);
		}
	}
	
}

匿名內部類實現線程
package day20191203;

public class Demo03 {
	public static void main(String[] args) {
		/**
		 * 優勢:使用比較自由
		 * 劣勢:只能使用一次
		 */
		Thread t1 = new Thread() {
			public void run() {
				for(int i=0;i<100;i++) {
					System.out.println(this.getName()+":"+i);
				}
			}
		};
		t1.start();
		
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
				for(int i=0;i<100;i++) {
					System.out.println(Thread.currentThread().getName()+":"+i);
				}
			}
		});
		t2.start();
	}
}

主方法也是一個線程,主方法結束不意味着程序結束,所有前置線程都結束才標誌着程序結束

多線程的特點

  • 執行順序隨機:寫在前面的線程不一定先執行,寫在後面的線程不一定後執行,CPU執行線程的順序完全隨機。

  • 執行時間隨機:CPU分配給線程的時間片長短是完全隨機的,沒有任何規律可以尋找。這種情況導致的結果就是程序每一次運行的輸出結果都不完全相同。

  • 執行過程不連續:由於CPU分配的時間片是隨機的,所以無法保證線程能在一個時間片完成任務,一個任務極有可能被分割成多次完成。

時間片:由CPU分配給線程的最大運行時間,時間片結束則暫停當前線程的運行,將運行權交給另外的線程,被暫停的線程回到就緒狀態等待下一次被分配。

線程的生命週期

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-JGSrpIDD-1576547042891)(D:\BaiDu\BaiduNetdiskDownload\MyNotes\pic\Thread.jpg)]

線程的API

  • Thread.currentThread():靜態方法,用於獲得當前線程對象。
package day20191208;

public class Demo02 {
	public static void main(String[] args) {
//		System.out.println(Thread.currentThread().getName());
//		doIt();
		Thread t = new Thread() {
			public void run() {
				System.out.println(Thread.currentThread().getName());
				doIt();
			}
		};
		t.start();
	}
	public static void doIt() {
		System.out.println(Thread.currentThread().getName());
	}
}

使用該方法,我們可以得到結論:執行該方法的線程,就是調用該方法的線程,不會發生變化。

  • Thread.yield():當前線程讓出cpu的時間片交給其它線程執行,類似於模擬了一次cpu切換。
  • Thread.sleep(long millons):令當前線程進入休眠狀態[millons]毫秒,時間一到,線程自動甦醒。
package day20191208;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Demo03 {
	/**
	 * 簡單時鐘的製作
	 * @param args
	 */
	public static void main(String[] args) {
		Thread t = new Thread() {
			public void run() {
				while(true) {
					Date date = new Date();
					SimpleDateFormat sf = new SimpleDateFormat("HH:mm:ss");
					String str = sf.format(date);
					System.out.println(str);
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			}
		};
		t.start();
	}
}

  • getName():獲得當前線程對象的名稱。
  • getID():獲得當前線程對象的ID。
  • getPriority():獲得當前線程的優先級。
  • setPriority():設置當前線程的優先級(1~10),1(MIN_PRIORITY)最小,10(MAX_PRIORITY)最大,每個線程的優先級默認值是5。優先級越高,被分配到時間片的可能性越大;反之,被分配到時間片的可能性越小。
  • isDaemon():判斷線程是否是一個守護線程。
  • isInterrupted():判斷線程的休眠是否被打斷
  • isAlive():判斷線程是否是一個活躍線程
  • join():阻塞當前線程,等待方法調用者結束後再執行當前線程
package day20191208;

public class Demo04 {
	public static void main(String[] args) {
		Thread t1 = new Thread() {
			public void run() {
				for(int i=0;i<100;i++) {
					System.out.println("正在下載:"+i+"%");
				}
				System.out.println("下載完成");
			}
		};
		Thread t2 = new Thread() {
			public void run() {
				try {
					/**
					 * 阻塞當前線程t2,當方法調用者t1結束後纔會執行t2
					 */
					t1.join();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println("顯示圖片!");
			}
		};
		t1.start();
		t2.start();
	}
}

守護線程

守護線程又叫後置線程,與前置線程同時執行,當所有前置線程結束時守護線程隨之結束(必定結束)

package day20191208;

public class Demo01 {
	public static void main(String[] args) {
		Thread t1 = new Thread() {
			public void run() {
				for(int i=0;i<10;i++) {
					System.out.println(this.getName()+":"+i);
				}
			}
		};
		Thread t2 = new Thread() {
			public void run() {
				for(int i=0;true;i++) {
					System.out.println(this.getName()+":"+i);
				}
			}
		};
		t1.start();
		
		/**
		 * 開啓守護線程
		 */
		t2.setDaemon(true);
		t2.start();
	}
}

JDK中已存在的守護線程:GC

線程的同步

兩個線程共享數據,互相之間競爭資源(可能引發線程不安全的問題)

package day20191208;

public class Demo05 {
	public static void main(String[] args) {
		Table t = new Table();
		Thread t1 = new Thread() {
			public void run() {
				while(true) {
					t.getBean();
				}
			}
		};
		
		Thread t2 = new Thread() {
			public void run() {
				while(true) {
					t.getBean();
				}
			}
		};
		
		t1.start();
		t2.start();
	}
}
class Table{
	int bean = 20;
	public void getBean() {
		if(bean == 0) {
			Thread.yield();
			throw new RuntimeException("豆子沒了!");
		}
		System.out.println(Thread.currentThread().getName()+":"+bean--);
	}
}

此案例將可能出現的問題:

  1. 兩個線程獲取相同的bean

  2. 無法結束程序

第一個問題的原因在於線程獲取的時間片長短是不確定的,t1線程在完成輸出即將進行bean–操作時,時間片結束,cpu切換到t2執行任務,如此一來有可能導致bean的輸出混亂,所以這樣的代碼不算真正的線程同步。

第二個問題的原因與第一個問題相同,有可能過短的時間片使程序直接越過0這個閾值,導致無法正常結束程序(雖然改成“<=0”就能解決,但是那樣就看不到線程鎖的效果了)。

實現線程同步的方法

加上線程鎖,實現排隊執行任務(同一時間只允許一個線程工作),這樣一來線程不安全變成了線程安全。

關鍵字:synchronized

  1. 方法上加鎖
class Table{
	int bean = 20;
	public synchronized void getBean() {
		if(bean == 0) {
			Thread.yield();
			throw new RuntimeException("豆子沒了!");
		}
		System.out.println(Thread.currentThread().getName()+":"+bean--);
	}
}

如果將此看成是一個試衣間,那麼加鎖就相當於拴上試衣間的門鎖,同一時間只准有一個線程進入此方法,其它線程只能等待進入方法的線程結束後才能進入(這也是線程安全效率低,線程不安全效率高的原因)。

  1. 鎖住代碼塊
class Table{
	int bean = 20;
	public void getBean() {
		/**
		 * 鎖住代碼塊時需要傳入被鎖的對象(被鎖的是方法就傳入所屬的類對象)
		 */
		synchronized(this){
			if(bean == 0) {
				Thread.yield();
				throw new RuntimeException("豆子沒了!");
			}
			System.out.println(Thread.currentThread().getName()+":"+bean--);
		}
	}
}

加鎖的原則:鎖的範圍越小越好

死鎖

資源沒有被正常釋放,使後續線程無法進入方法,導致程序不能正常結束。

package day20191208;

public class Demo06 {
	public static void main(String[] args) {
		Method m = new Method();
		Thread t1 = new Thread() {
			public void run() {
				m.a();
			}
		};
		
		Thread t2 = new Thread() {
			public void run() {
				m.b();
			}
		};
		t1.start();
		t2.start();
	}
}
class Method{
	Object o = new Object();
	Object k = new Object();
	public void a() {
		synchronized(o) {
			System.out.println(Thread.currentThread().getName()+":a");
			b();
		}
	}
	public void b() {
		synchronized(k) {
			System.out.println(Thread.currentThread().getName()+":b");
			a();
		}
	}
}

從上面得案例中,我們可以提取以下信息:

  • a()鎖了o對象,執行完a才能釋放o,但是要執行完a()必須先執行b()

  • b()鎖了k對象,執行完b才能釋放k,但是要執行完b()必須先執行a()

  • t1線程調用了a(),t2線程調用了b()

運行程序,我們可以發現:o對象被t1線程所佔,等待k對象被t2線程釋放;k對象被t1線程所佔,等待o對象被t1線程釋放。兩個線程互不相讓,都在等待對方釋放資源,由此發生了死鎖。

線程同步的核心問題

如何解決資源搶佔的問題

線程的wait()與notify()

  • wait():線程進入阻塞狀態
  • wait(long time):線程進入阻塞狀態[time]毫秒
  • notify():喚醒進入阻塞狀態的線程,與wait()配合使用
  • notifyAll():喚醒所有進入阻塞狀態的線程

注意:使用wait()、wait(long time)、notify()、notifyAll()時,需要加鎖

package day20191208;

public class Demo07 {
	public static void main(String[] args) {
		/**
		 * 1.三種功能:加載圖片、顯示圖片、下載圖片
		 * 2.顯示圖片必須在加載完成之後才能執行
		 * 3.顯示圖片可以和下載圖片同時執行
		 */
		Object o = new Object();
		Thread t1 = new Thread() {
			public void run() {
				System.out.println("正在加載");
				for(int i=0;i<100;i++) {
					System.out.println("加載進度:"+i+"%");
					
				}
				synchronized(o) {
					o.notify();
				}
				System.out.println("正在下載");
				for(int i=0;i<100;i++) {
					System.out.println("下載進度:"+i+"%");
				}
				System.out.println("下載完成!");
			}
		};
		Thread t2 = new Thread() {
			public void run() {
				synchronized(o) {
					try {
						o.wait();
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				System.out.println("顯示圖片!");
			}
		};
		
		t1.start();
		t2.start();
	}
}

wait()進入的阻塞狀態不同於join()和sleep()。wait()帶鎖進入阻塞狀態後會將鎖釋放,被喚醒後會被重新上鎖。

簡單比喻:櫃檯前排隊辦事時發現材料沒帶夠,於是在一旁等待家人將材料送過來,等待的時間內後面排隊的人輪流辦事,材料送到後插隊繼續之前的工作。


線程池

作用
  • 控制線程數量:規定了內部線程的數量
  • 重用線程:線程執行完任務後不會進入死亡狀態,而是獲取正在隊列中的任務後,重新進入執行狀態
創建線程池

關鍵詞:Executors

  • Executors.newCachedThreadPool():創建一個可根據需要創建新線程的線程池
  • Executors.newFixedThreadPool(int nThreads):創建一個可重用固定線程集合的線程池,以共享的無界隊列方式來運行這些線程
  • Executors.newScheduledThreadPool(int corePoolSize):創建一個線程池,它會在給定延遲後運行命令或者定期執行
  • Executors.newSingleThreadExecutors() :創建一個使用單個worker線程的Executors,以無界隊列方式來運行該線程(單例模式)

無界隊列:沒有規範的,誰先搶到就給誰使用

package day20191208;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo08 {
	public static void main(String[] args) {
		ExecutorService es = Executors.newFixedThreadPool(2);
		//創建一個線程池,內部線程數量爲2
		for(int i=0;i<10;i++) {
			Runnable run = new Runnable() {
				public void run() {
					System.out.println(Thread.currentThread().getName()+":"+"i");
				}
			};
		//只能傳入Runnable對象,Thread對象本身就是一個線程
			es.execute(run);
		}
		es.shutdown();
	}
}


package day20191208;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo08 {
	public static void main(String[] args) {
		ExecutorService es = Executors.newFixedThreadPool(2);
		//創建一個線程池,內部線程數量爲2
		for(int i=0;i<10;i++) {
			Runnable run = new Runnable() {
				public void run() {
					System.out.println(Thread.currentThread().getName()+":"+"i");
				}
			};
		//只能傳入Runnable對象,Thread對象本身就是一個線程
			es.execute(run);
		}
		es.shutdown();
	}
}


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