一篇文章快速了解多線程

1. 基本概念

1.1 進程與線程

  • 程序(program) 是為完成特定任務、用某種語言編寫的一組指令的集合。即指一段靜態的代碼,靜態對象。
  • 進程(process)是程序的一次執行過程,或是正在運行的一個程序。 是一個動態的過程:有它自身的產生、存在和消亡的過程。——生命周期 如:運行中的QQ,運行中的MP3播放器 程序是靜態的,進程是動態的 進程作為資源分配的單位,系統在運行時會為每個進程分配不同的記憶體區域
  • 線程(thread),進程可進一步細化為線程,是一個程序內部的一條執行路徑。 若一個進程同一時間並行執行多個線程,就是支持多線程的 線程作為調度和執行的單位,每個線程擁有獨立的運行棧和程序計數器(pc),線程切換的開銷小
    一個進程中的多個線程共享相同的記憶體單元/記憶體地址空間,它們從同一堆中分配對象,可以訪問相同的變量和對象。這就使得線程間通信更簡便、高效。但多個線程操作共享的系統資源可能就會帶來安全的隱患

一篇文章快速了解多線程一篇文章快速了解多線程

1.2 並行與並發

  • 並行:多個CPU同時執行多個任務,指同⼀時刻內發⽣兩個或多個事件。在不同實體上的多個事件。 比如:多個人同時做不同的事。
  • 並發:一個CPU(採用時間片)同時執行多個任務,同⼀時間間隔內發⽣兩個或多個事件,同⼀實體上的多個事件。比如:秒殺、多個人做同一件事。
  • 並⾏是針對進程的,並發是針對線程的

2. 實現多線程

2.1 繼承Thread

  1. 定義子類繼承Thread類
  2. 子類中重寫Thread類中的run方法
  3. 創建Thread子類對象,即創建了線程對象
  4. 調用線程對象start方法:啟動線程,調用run方法
//1. 創建一個繼承於Thread類的子類
class MyClass extends Thread {
//2. 重寫Thread類中的run方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":  " + i);
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3.創建Thread類的子類的對象
MyClass t1 = new MyClass();
//4.通過此方法調用start方法
t1.start();
MyClass t2 = new MyClass();
t2.start();
for (int i = 0; i < 100; i++) {
System.out.println("main");
}
}
}
複製代碼

注意事項:

如果自己手動調用run()方法,那麼就只是普通方法,沒有啟動多線程模式,想要啟動多線程,必須調用start方法一個線程對象只能調用一次start()方法啟動,如果重複調用了,則將拋出以上的異常「IllegalThreadStateException」。

2.2 實現Runnable接口

  1. 定義子類,實現Runnable接口
  2. 子類中重寫Runnable接口中的run方法
  3. 通過Thread類含參構造器創建線程對象
  4. 將Runnable接口的子類對象作為實際參數傳遞給Thread類的構造器中。
  5. 調用Thread類的start方法:開啟線程,調用Runnable子類接口的run方法。
//1.創建一個實現了Runnable接口的類
class MyThread1 implements Runnable {
//2.實現類去實現Runnable中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadTest2 {
public static void main(String[] args) {
//3.創建實現類的對象
MyThread1 myThread1 = new MyThread1();
//4. 將此對象作為參數傳遞到Thread類的構造器中,創建Thread類的對象
Thread t1 = new Thread(myThread1);
t1.setName("線程一");
//5.通過Thread類的對象調用start()
t1.start();
}
}
複製代碼

2.3 實現Callable接⼝

與使用Runnable相比, Callable功能更強大些

  • 相比run()方法,可以有返回值
  • 方法可以拋出異常
  • 支持泛型的返回值、

Future接口

  • 可以對具體Runnable、Callable任務的執行結果進行取消、查詢是否完成、獲取結果等。
  • FutrueTask是Futrue接口的唯一的實現類
  • FutureTask 同時實現了Runnable, Future接口。它既可以作為Runnable被線程執行,又可以作為Future得到Callable的返回值
  1. 創建一個實現Callable的實現類
  2. 實現call方法,將此線程需要執行的操作聲明在call()中
  3. 創建Callable接口實現類的對象
  4. 將此Callable接口實現類的對象作為傳遞到FutureTask構造器中,創建FutureTask的對象
  5. 將FutureTask的對象作為參數傳遞到Thread類的構造器中,創建Thread對象,並調用start()
  6. 獲取Callable中call方法的返回值(可選)
//1.創建一個實現Callable的實現類
class NumberThread implements Callable<Integer> {
//2.實現call方法,將此線程需要執行的操作聲明在call()中
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) {
//3.創建Callable接口實現類的對象
NumberThread numberThread = new NumberThread();
//4.將此Callable接口實現類的對象作為傳遞到FutureTask構造器中,創建FutureTask的對象
FutureTask<Integer> futureTask = new FutureTask<>(numberThread);
//5.將FutureTask的對象作為參數傳遞到Thread類的構造器中,創建Thread對象,並調用start()
new Thread(futureTask).start();
Integer sum = null;
try {
//6.獲取Callable中call方法的返回值
//get()返回值即為FutureTask構造器參數Callable實現類重寫的call()的返回值。
sum = futureTask.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
System.out.println("總和為: "+sum);
}
}
複製代碼

2.4 線程池

經常創建和銷毀、使用量特別大的資源,比如並發情況下的線程,對性能影響很大。可以提前創建好多個線程,放入線程池中,使用時直接獲取,使用完放回池中。可以避免頻繁創建銷毀、實現重複利用。類似生活中的公共運輸工具。

好處:

  • 提高響應速度(減少了創建新線程的時間)
  • 降低資源消耗(重複利用線程池中線程,不需要每次都創建)
  • 便於線程管理 corePoolSize:核心池的大小 maximumPoolSize:最大線程數 keepAliveTime:線程沒有任務時最多保持多長時間後會終止

ExecutorService:真正的線程池接口。 常見子類ThreadPoolExecutor

  • void execute(Runnable command) :執行任務/命令,沒有返回值,一般用來執行Runnable
  • < T > Future< T > submit(Callable< T > task):執行任務,有返回值,一般又來執行Callable
  • void shutdown() :關閉連接池

Executors:工具類、線程池的工廠類,用於創建並返回不同類型的線程池

  • Executors.newCachedThreadPool():創建一個可根據需要創建新線程的線程池
  • Executors.newFixedThreadPool(n); 創建一個可重用固定線程數的線程池
  • Executors.newSingleThreadExecutor() :創建一個只有一個線程的線程池
  • Executors.newScheduledThreadPool(n):創建一個線程池,它可安排在給定延遲後運行命令或者定期地執行
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定線程數量的線程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//設置線程池的屬性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();
//2.執行指定的線程的操作。需要提供實現Runnable接口或Callable接口實現類的對象
service.execute(new NumberThread());//適合適用於Runnable
service.execute(new NumberThread1());//適合適用於Runnable
//        service.submit(Callable callable);//適合使用於Callable
//3.關閉連接池
service.shutdown();
}
}
複製代碼

3. Thread類

3.1 構造方法

  • Thread():創建新的Thread對象
  • Thread(String threadname):創建線程並指定線程實例名
  • Thread(Runnable target):指定創建線程的目標對象,它實現了Runnable接口中的run方法
  • Thread(Runnable target, String name):創建新的Thread對象

3.2 常用方法

  • void start(): 啟動線程,並執行對象的run()方法
  • run(): 線程在被調度時執行的操作
  • String getName(): 返回線程的名稱
  • void setName(String name):設置該線程名稱
  • static Thread currentThread(): 返回當前線程。在Thread子類中就 是this,通常用於主線程和Runnable實現類
  • static void yield():線程讓步 暫停當前正在執行的線程,把執行機會讓給優先級相同或更高的線程 若隊列中沒有同優先級的線程,忽略此方法
  • join() :當某個程序執行流中調用其他線程的 join() 方法時,調用線程將被阻塞,直到 join() 方法加入的 join 線程執行完為止 低優先級的線程也可以獲得執行
  • static void sleep(long millis):(指定時間:毫秒) 令當前活動線程在指定時間段內放棄對CPU控制,使其他線程有機會被執行,時間到後重排隊。 拋出InterruptedException異常
  • stop(): 強制線程生命期結束,不推薦使用,過時了
  • boolean isAlive():返回boolean,判斷線程是否還活著

3.3 線程優先級

Java的調度方法

  • 同優先級線程組成先進先出隊列(先到先服務),使用時間片策略
  • 對高優先級,使用優先調度的搶占式策略

線程的優先級等級

  • MAX_PRIORITY:10
  • MIN _PRIORITY:1
  • NORM_PRIORITY:5
  • 涉及的方法 getPriority() :返回線程優先值 setPriority(int newPriority) :改變線程的優先級
  • 說明 線程創建時繼承父線程的優先級 低優先級只是獲得調度的機率低,並非一定是在高優先級線程之後才被調用

4. 線程的生命周期

JDK中用Thread.State類定義了線程的幾種狀態要想實現多線程,必須在主線程中創建新的線程對象。Java語言使用Thread類及其子類的對象來表示線程,在它的一個完整的生命周期中通常要經歷如下的五種狀態:

  • 新建: 當一個Thread類或其子類的對象被聲明並創建時,新生的線程對象處於新建狀態
  • 就緒:處於新建狀態的線程被start()後,將進入線程隊列等待CPU時間片,此時它已具備了運行的條件,只是沒分配到CPU資源
  • 運行:當就緒的線程被調度並獲得CPU資源時,便進入運行狀態, run()方法定義了線程的操作和功能
  • 阻塞:在某種特殊情況下,被人為掛起或執行輸入輸出操作時,讓出 CPU 並臨時終止自己的執行,進入阻塞狀態
  • 死亡:線程完成了它的全部工作或線程被提前強制性地中止或出現異常導致結束

5. 線程同步

當多條語句在操作同一個線程共享數據時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行。導致共享數據的錯誤。所以呢,對多條操作共享數據的語句,只能讓一個線程都執行完,在執行過程中,其他線程不可以參與執行。

5.1 synchronized鎖

對於並發工作,你需要某種方式來防止兩個任務訪問相同的資源(其實就是共享資源競爭)。 防止這種衝突的方法就是當資源被一個任務使用時,在其上加鎖。第一個訪問某項資源的任務必須鎖定這項資源,使其他任務在其被解鎖之前,就無法訪問它了,而在其被解鎖之時,另一個任務就可以鎖定並使用它了。

注意:

  • 任意對象都可以作為同步鎖。所有對象都自動含有單一的鎖(監視器)。
  • 同步方法的鎖:靜態方法(類名.class)、非靜態方法(this)
  • 同步代碼塊:自己指定,很多時候也是指定為this或類名.class
  • 必須確保使用同一個資源的多個線程共用一把鎖,這個非常重要,否則就無法保證共享資源的安全
  • 一個線程類中的所有靜態方法共用同一把鎖(類名.class),所有非靜態方法共用同一把鎖(this),同步代碼塊(指定需謹慎)

5.1.1 同步代碼塊

synchronized (同步監視器{ // 需要被同步的代碼; }

說明: 1.操作共享數據的代碼,即為需要被同步的代碼。不能包含代碼多了,也不能包含代碼少了。 2.共享數據:多個線程共同操作的變量。比如:ticket就是共享數據。 3.同步監視器,俗稱:鎖。任何一個類的對象,都可以充當鎖。要求:多個線程必須要共用同一把鎖。 4.在實現Runnable接口創建多線程的方式中,我們可以考慮使用this充當同步監視器。
5.在繼承Thread類創建多線程的方式中,慎用this充當同步監視器,考慮使用當前類充當同步監視器。

實現Runnable接口創建多線程的方式

class Window1 implements Runnable{
private int ticket = 100;
//    Object obj = new Object();
@Override
public void run() {
//        Object obj = new Object();
while(true){
synchronized (this){//此時的this:唯一的Window1的對象   //方式二:synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
複製代碼

繼承Thread類創建多線程的方式

class Window2 extends Thread{
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while(true){
//正確的
//            synchronized (obj){
synchronized (Window2.class){//Class clazz = Window2.class,Window2.class只會加載一次
//錯誤的方式:this代表著t1,t2,t3三個對象
//              synchronized (this){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":賣票,票號為:" + ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 t1 = new Window2();
Window2 t2 = new Window2();
Window2 t3 = new Window2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
複製代碼

5.1.2同步方法

synchronized放在方法聲明中,表示整個方法為同步方法。 例如: public synchronized void show (String name){ …. }

1.同步方法仍然涉及到同步監視器,只是不需要我們顯式的聲明。 2.非靜態的同步方法,同步監視器是:this 3.靜態的同步方法,同步監視器是:當前類本身

實現Runnable接口創建多線程的方式

class Window3 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private synchronized void show(){//同步監視器:this
//synchronized (this){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
ticket--;
}
//}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w = new Window3();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
複製代碼

繼承Thread類創建多線程的方式

class Window4 extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private static synchronized void show(){//同步監視器:Window4.class
//private synchronized void show(){ //同步監視器:t1,t2,t3。此種解決方式是錯誤的
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 t1 = new Window4();
Window4 t2 = new Window4();
Window4 t3 = new Window4();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
複製代碼

5.2 Lock鎖

Lock顯示鎖是JDK1.5之後才有的,這給協調線程帶來了更多的控制功能。一個鎖就是一個Lock接口的實例。

  • void lock():得到一個鎖
  • void unlock():釋放鎖
  • Condition newCondition():創建任意個數的Condition對象,用於線程通信。
  • ReentrantLock(boolean fair):Lock的常用實現,創建一個帶有公平策略的鎖,一個公平鎖偏愛等待時間更長的進程。但會降低性能,一般都是false。
  • ReentrantLock():等價於ReentrantLock(false)
class Window implements Runnable{
private int ticket = 100;
//1.實例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
//2.調用鎖定方法lock()
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票,票號為:" + ticket);
ticket--;
}else{
break;
}
}finally {
//3.調用解鎖方法:unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
複製代碼

面試題:synchronized 與 Lock的異同? 相同:二者都可以解決線程安全問題
不同:synchronized機制在執行完相應的同步代碼以後,自動的釋放同步監視器
Lock需要手動的啟動同步(lock()),同時結束同步也需要手動的實現(unlock())

5.3 總結

5.3.1 釋放鎖的操作

  • 當前線程的同步方法、同步代碼塊執行結束。
  • 當前線程在同步代碼塊、同步方法中遇到break、return終止了該代碼塊、該方法的繼續執行。
  • 當前線程在同步代碼塊、同步方法中出現了未處理的Error或Exception,導致異常結束。
  • 當前線程在同步代碼塊、同步方法中執行了線程對象的wait()方法,當前線程暫停,並釋放鎖。

5.3.2 不會釋放鎖的操作

  • 線程執行同步代碼塊或同步方法時,程序調用Thread.sleep()、Thread.yield()方法暫停當前線程的執行
  • 線程執行同步代碼塊時,其他線程調用了該線程的suspend()方法將該線程掛起,該線程不會釋放鎖(同步監視器)。應儘量避免使用suspend()和resume()來控制線程

6. 線程通信

線程同步完全可以避免競爭條件的發生,但有時候,還需要線程之間的相互協調,可以使用條件實現線程之間的通信。

6.1 常用方法

  • wait():令當前線程掛起並放棄CPU、同步資源並等待,使別的線程可訪問並修改共享資源,而 當前線程排隊等候其他線程調用notify()或notifyAll()方法喚醒,喚醒後等待重新獲得對監視器的所有權後才能繼續執行
  • notify():喚醒正在排隊等待同步資源的線程中優先級最高者結束等待
  • notifyAll ():喚醒正在排隊等待資源的所有線程結束等待
class Number implements Runnable{
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj) {
obj.notify();
if(number <= 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得調用如下wait()方法的線程進入阻塞狀態
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("線程1");
t2.setName("線程2");
t1.start();
t2.start();
}
}
複製代碼

面試題:sleep() 和 wait()的異同? 1.相同點:一旦執行方法,都可以使得當前的線程進入阻塞狀態。 2.不同點: 1)兩個方法聲明的位置不同:Thread類中聲明sleep() , Object類中聲明wait() 2)調用的要求不同:sleep()可以在任何需要的場景下調用。 wait()必須使用在同步代碼塊或同步方法中
3)關於是否釋放同步監視器:如果兩個方法都使用在同步代碼塊或同步方法中,sleep()不會釋放鎖,wait()會釋放鎖。

7. 死鎖(以後補充)

不同的線程分別占用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖出現死鎖後,不會出現異常,不會出現提示,只是所有線程都處於阻塞狀態,無法繼續 我們使用同步時,要避免出現死鎖。

public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}

來源:kknews