文章目錄
1. 基本概念
1. 程序
爲完成特定任務,用某種語言編寫的一組指令的集合。即指一段靜態的代碼
2. 進程
正在運行的一個程序,是一個動態的過程
3. 線程
是一個程序內部的一條執行路徑
4. 並行
多個CPU同時執行多個任務
5. 併發
一個CPU同時執行多個任務
2. 線程的創建和使用
1. 方式一:繼承 Thread 類
JDK中這樣描述:
There are two ways to create a new thread of execution. One is to declare a class to be a subclass of
Thread
. This subclass should override therun
method of classThread
. An instance of the subclass can then be allocated and started.
整理一下可分爲四步:
- 創建一個繼承於 Thread 類的子類
- 重寫 Thread 類的 run() 方法
- 創建 Thread 類的子類對象
- 通過此對象調用 start() 方法
舉個栗子:
// 1. 創建一個繼承於 Thread 類的子類
class MyThread extends Thread {
// 2. 重寫 Thread 類的 run() 方法
@Override
public void run() {
System.out.println("我是一個多線程");
}
}
public class ThreadTest {
public static void main(String[] args) {
// 3. 創建 Thread 類的子類對象
MyThread myThread = new MyThread();
// 4. 通過此對象調用 start() 方法
// ①啓動當前線程
// ②調用當前線程的 run()方法
myThread.start();
// 或者使用匿名內部類的方式
new Thread() {
@Override
public void run() {
System.out.println("我也是一個多線程");
}
}.start();
}
}
需要注意的點:
- 當執行
myThread.run()
並沒有啓動線程,是直接調用了 myThread 類的 run() 方法 - 當需要再次啓動一個線程時,需要重新創建一個對象
2. 方式二:實現 Runnable 接口
JDK中是這樣描述的:
The other way to create a thread is to declare a class that implements the
Runnable
interface. That class then implements therun
method. An instance of the class can then be allocated, passed as an argument when creatingThread
, and started
整理一下可分爲五步:
- 創建實現了 Runnable 接口的類
- 實現類實現 Runnable 中的抽象方法:run()
- 創建實現類的對象
- 將此對象作爲參數傳遞到 Thread 類的構造器中,創建 Thread 類的對象
- 通過 Thread 類的對象調用 start() 方法
舉個栗子:
// 1. 創建實現了 Runnable 接口的類
class MyThread implements Runnable{
// 2. 實現類實現 Runnable 中的抽象方法:run()
@Override
public void run() {
System.out.println("我是一個多線程");
}
}
public class ThreadTest {
public static void main(String[] args) {
//3. 創建實現類的對象
MyThread myThread = new MyThread();
//4. 將此對象作爲參數傳遞到 Thread 類的構造器中,創建 Thread 類的對象
Thread thread = new Thread(myThread);
// 5, 通過 Thread 類的對象調用 start() 方法
thread.start();
}
}
3. 創建線程的兩種方式的比較
- 優先選擇:實現 Runnable 接口的方式,原因如下:
- 實現的方式沒有類的單繼承的侷限性
- 實現的方式更適合處理多個線程有共享數據的情況
- 聯繫:Thread 類也實現了 Runnable 接口
- 相同點:都需要將線程要執行的邏輯重寫在 Run() 方法中
4. Thread 類中的常用方法
-
start():
Causes this thread to begin execution; the Java Virtual Machine calls the
run
method of this thread.啓動當前線程;調用當前線程的 run() 方法
-
run():
If this thread was constructed using a separate
Runnable
run object, then thatRunnable
object’srun
method is called; otherwise, this method does nothing and returns.創建的線程要執行的操作聲明在此方法中
-
yield():
A hint to the scheduler that the current thread is willing to yield its current use of a processor.
讓出當前線程的使用權
-
join():
Waits for this thread to die.
在線程 a 中調用線程 b 的 join(),此時線程 a 進入阻塞狀態,直到 b 完全執行後,線程 a 才結束阻塞狀態(在哪個線程裏調用,哪個線程被阻塞)
-
sleep(long millis):
Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers.
使當前線程休眠指定 millis 毫秒
-
getName():獲取當前線程的名字
-
setName(String name):設置當前線程的名字
5. 線程的調度
- Java 中線程的調度
- 同優先級線程先到先服務,平均分配CPU執行時間
- 高優先級的線程搶佔CPU
-
線程的優先級
- MIN_PRIORITY:1
- NORM_PRIORITY:5 -----> 默認優先級
- MAX_PRIORITY:10
-
設置和獲取線程的優先級
- getPriority():獲取線程的優先級
- setPriority(int newPriority):設置線程的優先級
說明:高優先級的線程要搶佔低優先級線程的CPU執行權,並不是意味着高優先級線程執行完後才執行低優先級線程,只是高優先級獲取CPU執行權的概率更高
3. 線程的生命週期
- 新建:線程剛被創建,但是還未執行 start() 方法
- 就緒:處於新建狀態的線程被start() 後,將進入線程隊列等待 CPU 分配時間片
- 運行:當就緒狀態的線程被調度並獲得 CPU 資源時,進入運行狀態
- 阻塞:在特殊情況下,線程被迫讓出CPU並臨時中止自己的執行,進入阻塞狀態
- 死亡:線程完成全部工作或出現異常退出
4. 線程的同步
1. 問題的提出
-
舉個小栗子:
三個窗口同時賣票,總票數爲 10 張,使用實現 Runnable 接口的方式
class MyThread implements Runnable { private int ticket = 10; @Override public void run() { while (true) { if (ticket > 0) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "出售一張票,票號爲:" + ticket); ticket--; } else { break; } } } } public class ThreadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread thread1 = new Thread(myThread); Thread thread2 = new Thread(myThread); thread1.setName("窗口一"); thread2.setName("窗口二"); thread1.start(); thread2.start(); } }
其中一次輸出結果爲:
窗口二出售一張票,票號爲:10 窗口一出售一張票,票號爲:10 窗口一出售一張票,票號爲:8 窗口二出售一張票,票號爲:7 窗口二出售一張票,票號爲:6 窗口一出售一張票,票號爲:5 窗口二出售一張票,票號爲:4 窗口一出售一張票,票號爲:4 窗口一出售一張票,票號爲:2 窗口二出售一張票,票號爲:1 窗口一出售一張票,票號爲:0
-
輸出分析
- 賣票過程中,出現了重票問題,如多個窗口(線程)出售票號爲 10
- 賣票過程中,出現了錯票問題,如窗口(線程)一最後出售票號爲 0
-
問題分析
- 當線程 a 執行 run() 方法,遇到 sleep 進入堵塞狀態,線程 b 得到 CPU 的時間片進入 run() 方法,遇到 sleep 進入堵塞狀態,線程 a 率先從堵塞狀態回到就緒狀態,CPU 分配時間片給線程 a ,線程 a 進入運行狀態,當線程 a 執行 System.out.println 語句後還未執行 ticket-- 語句時,線程 b 也從堵塞狀態回到就緒狀態,CPU 分配時間片給線程 b,線程 b 進入運行狀態,也執行 System.out.println 語句,此時 ticket 的值併爲被更改,所以窗口一和二售出的票號都爲 10
- 當 ticket 爲 1 時,線程 a 進入 if 語句,執行 sleep 進入堵塞狀態,此時 ticket 的值依然爲 1,線程 b 得到CPU分配的時間片也進入 if 語句,所以窗口一售出票號爲 0
-
問題歸納
多線程操縱共享數據數據時,可能造成共享數據的不確定性,即出現了多線程的安全問題
2. 解決辦法
-
解決思路
當一個線程 a 操作共享數據時,其他線程不能參與進來。直到線程 a 操作完共享數據,其他線程才能開始操作共享數據。即使線程 a 進入阻塞狀態,只要未操作完共享數據,其他線程依然不能開始操作共享數據
在 Java 中,通過同步機制,解決線程的安全問題
-
解決方法
- 同步代碼塊
synchronized(同步監視器){ // 需要被同步的代碼 ... }
說明:
- 操作共享數據的代碼,即未需要被同步的代碼
- 共享數據:多個線程共同操作的變量
- 同步監視器:俗稱:鎖。任何一個類的對象,都可以充當鎖(包括類對象)。要求:多個線程必須共用一把鎖
- 在實現 Runnable 接口創建多線程的方式中,可以考慮使用 this 充當同步監視器;在繼承 Thread 類創建多線程的方式中,考慮使用當前類(類名.class)充當同步監視器
class MyThread implements Runnable { private int ticket = 10; @Override public void run() { while (true) { synchronized (MyThread.class) { if (ticket > 0) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "出售一張票,票號爲:" + ticket); ticket--; } else { break; } } } } } public class ThreadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread thread1 = new Thread(myThread); Thread thread2 = new Thread(myThread); thread1.setName("窗口一"); thread2.setName("窗口二"); thread1.start(); thread2.start(); } }
- 同步方法
如果操作共享數據的代碼完整聲明在一個方法中,可以考慮將此方法聲明爲同步的,爲了鎖都爲同一個對象,可以同時將方法聲明爲靜態的
說明:
- 同步方法仍然涉及同步監視器,只是不需要顯示聲明
- 非靜態方法的同步方法,同步監視器是:this;靜態方法的同步方法,同步監視器是:當前類本身
class MyThread implements Runnable { private int ticket = 10; @Override public void run() { while (true) { show(); } } // 相當於用 synchronized(this) 把整個方法體包裹起來 private synchronized void show() { if (ticket > 0) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "出售一張票,票號爲:" + ticket); ticket--; } } } public class ThreadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread thread1 = new Thread(myThread); Thread thread2 = new Thread(myThread); thread1.setName("窗口一"); thread2.setName("窗口二"); thread1.start(); thread2.start(); } }
-
lock 鎖
與 synchronized 的區別?
- synchronized 機制在執行完相應的同步代碼之後,自動的釋放同步監視器
- lock 需要手動啓動同步(lock()),結束同步也需要手動實現(unlock())
import java.util.concurrent.locks.ReentrantLock; class MyThread implements Runnable { private int ticket = 10; //1. 實例化ReentrantLock private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { try { // 2. 調用鎖定方法 lock() lock.lock(); if (ticket > 0) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "出售一張票,票號爲:" + ticket); ticket--; } else { break; } } finally { // 3. 調用解鎖方法 unlock() lock.unlock(); } } } } public class ThreadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread thread1 = new Thread(myThread); Thread thread2 = new Thread(myThread); thread1.setName("窗口一"); thread2.setName("窗口二"); thread1.start(); thread2.start(); } }
-
3. 優缺點分析
-
好處:
解決了線程安全問題
-
缺點:
操作同步代碼時,只有一個線程參與,效率低
5. 線程的通信
1. 常用方法:
-
wait():
Causes the current thread to wait until it is awakened, typically by being notified or interrupted.
當前線程進入阻塞狀態,並釋放同步監視器
-
notify():
Wakes up a single thread that is waiting on this object’s monitor.
喚醒被 wait 的線程,如果有多個線程,優先喚醒優先級高的線程
-
notifyAll():
Wakes up all threads that are waiting on this object’s monitor.
喚醒所有被 wait 的線程
說明:
- 三個方法必須使用在同步代碼塊或同步方法中
- 三個方法的調用者必須是同步代碼塊或同步方法中的同步監視器
- 三個方法都是定義在 java.lang.Object 類中
2. 實例
兩個線程輪流打印 1 - 100
class MyThread implements Runnable {
private int number = 0;
@Override
public void run() {
while (true) {
synchronized (this) {
if (number < 100) {
// 喚醒被 wait()阻塞的一個線程
notify();
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
// 使得調用wait()方法的線程進入阻塞狀態
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread);
Thread thread2 = new Thread(myThread);
thread1.setName("線程1");
thread2.setName("線程2");
thread1.start();
thread2.start();
}
}
3. sleep() 和 wait() 方法的異同
相同點:一旦執行方法,都可以使得當前線程進入阻塞狀態
不同點:
- 兩個方法聲明的位置不同:Thread 類中聲明 sleep(),Object 類中聲明 wait()
- 調用的要求不同:sleep() 可以在任何場景下調用,wait() 只能在同步代碼塊或同步方法中調用
- 關於釋放同步監視器:如果兩個方法都是在同步代碼塊或同步方法中調用,sleep() 不會釋放鎖,wait() 會釋放鎖
6. JDK1.5 新增創建多線程的方法
1. 實現 Callable 接口
JDK 中是這樣描述的:
The
Callable
interface is similar toRunnable
, in that both are designed for classes whose instances are potentially executed by another thread.
具體說來步驟爲:
- 創建一個實現 Callable 接口的實現類
- 實現類實現 call() 方法,將此線程需要執行的操作聲明在其中
- 創建實現 Callable 接口的實現類對象
- 將此 Callable 接口實現類對象作爲參數傳遞到 FutureTask 構造器中,創建 FutureTask 的對象
- 將 FutureTask 的對象作爲參數傳遞到 Thread 類的構造器中,創建 Thread 對象,並調用 start() 啓動線程
- FutureTask 對象調用 get() 方法獲取 Callable 實現類中 Call() 的返回值
舉個栗子:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//1. 創建一個實現 Callable 接口的實現類
class numThread implements Callable<Integer> {
//2. 實現類實現 Call() 方法,將此線程需要執行的操作聲明在其中
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
public class ThreadTest {
public static void main(String[] args) throws Exception {
//3. 創建實現 Callable 接口的實現類對象
numThread numThread = new numThread();
//4. 將此 Callable 接口實現類對象作爲參數傳遞到 FutureTask 構造器中,創建 FutureTask 的對象
FutureTask<Integer> futureTask = new FutureTask<>(numThread);
//5. 將 FutureTask 的對象作爲參數傳遞到 Thread 類的構造器中,創建 Thread 對象,並調用 start() 啓動線程
new Thread(futureTask).start();
//6. FutureTask 對象調用 get() 方法獲取 Callable 實現類中 Call() 的返回值
Integer o = futureTask.get();
System.out.println(o);
}
}
Callable 接口與 Runnable 接口的比較:
- call() 可以有返回值
- call() 可以拋出異常
- Callable 支持泛型
2. 線程池
import java.util.concurrent.*;
class MyThread implements Runnable {
@Override
public void run() {
System.out.println("我是Runnable接口");
}
}
class MyThread2 implements Callable {
@Override
public Object call() throws Exception {
System.out.println("我是Callable接口");
return null;
}
}
public class ThreadTest {
public static void main(String[] args) {
// 1. 提供指定線程數的線程池
ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
// 2. 執行指定線程的操作
// 適用於Runnable
executorService.execute(new MyThread());
// 適用於Callable
executorService.submit(new MyThread2());
// 3. 關閉連接池
executorService.shutdown();
}
}
使用線程池的好處:
- 提高響應速度(減少了創建新線程的時間)
- 降低資源消耗(重複利用線程池中的線程)
- 便於線程管理