多線程
程序、進程、線程的區別
- 程序:本地文件
- 進程:正在運行的程序,進程至少有一個線程
- 線程:一個進程中的多個“同時”進行的任務,兩個以上線程稱爲多線程
多線程的“同時”運行:多線程並非是同時運行的。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--);
}
}
此案例將可能出現的問題:
-
兩個線程獲取相同的bean
-
無法結束程序
第一個問題的原因在於線程獲取的時間片長短是不確定的,t1線程在完成輸出即將進行bean–操作時,時間片結束,cpu切換到t2執行任務,如此一來有可能導致bean的輸出混亂,所以這樣的代碼不算真正的線程同步。
第二個問題的原因與第一個問題相同,有可能過短的時間片使程序直接越過0這個閾值,導致無法正常結束程序(雖然改成“<=0”就能解決,但是那樣就看不到線程鎖的效果了)。
實現線程同步的方法
加上線程鎖,實現排隊執行任務(同一時間只允許一個線程工作),這樣一來線程不安全變成了線程安全。
關鍵字:synchronized
- 方法上加鎖
class Table{
int bean = 20;
public synchronized void getBean() {
if(bean == 0) {
Thread.yield();
throw new RuntimeException("豆子沒了!");
}
System.out.println(Thread.currentThread().getName()+":"+bean--);
}
}
如果將此看成是一個試衣間,那麼加鎖就相當於拴上試衣間的門鎖,同一時間只准有一個線程進入此方法,其它線程只能等待進入方法的線程結束後才能進入(這也是線程安全效率低,線程不安全效率高的原因)。
- 鎖住代碼塊
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();
}
}