關於多執行緒、執行緒池、執行緒安全問題
阿新 • • 發佈:2021-01-21
多執行緒
1、基礎概念
1.1 多執行緒技術
- 從軟體或者硬體上實現同時執行多個任務
- 具有多執行緒能攔的計算機因有硬體支援而能夠在同一時間執行多個執行緒
- 多執行緒程式設計常常也將其稱之為併發程式設計
1.2 併發和並行
- 並行
- 在同一時刻,有多個指令在多個CPU上同時進行
- 併發
- 在同一時刻,有多個指令在單個CPU上交替進行
1.3 程序和執行緒
- 程序:是正在執行的軟體,且一個程序最少包括一個執行緒
- 獨立性:程序是一個可以獨立執行的基本單位,也是作業系統排程的最小單元,同時也是系統分配資源和排程的獨立單位
- 動態性:程序的實質是程式的一次執行過程,程序是動態產生,動態消亡的
- 併發性:任何程序都可以同其他程序一起併發執行
- 執行緒:是程序中的單個順序控制流,是一條執行路徑
- 單執行緒:一個程序如果只有一條執行路徑,則稱為單執行緒程式
- 多執行緒:一個程式如果有多條執行路徑,則稱為多執行緒程式
- 二者關係
- 執行緒是程序中的單個順序控制流,是依賴於程序的,而一個程序最少包括一個執行緒,程序可以存在很多工,每一個任務就是一個執行緒,這些執行路徑之間沒有任何的關聯關係
2、執行緒的實現
2.1 繼承Thread類
- Thread類
- Thread類就是JDK給我們定義的一個執行緒類
- 方法介紹
方法名 | 說明 |
---|---|
void run() | 線上程開啟後,此方法將被呼叫執行 |
void start() | 使此執行緒開始執行,Java虛擬機器會呼叫run方法() |
- 實現步驟
- 定義一個類繼承自Thread類,這裡我們定義這個類為MyThread
- 重寫Thread類中的run方法(run方法中重寫的就是要被執行緒所執行的程式碼)
- 建立MyThread類的物件
- 啟動執行緒(呼叫start方法)
- 注意:多執行緒的執行,具有隨機性
- 程式碼實現
public class MyThread extends Thread {
@Override
public void run() {
for(int i=0; i<100; i++) {
System.out.println(i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
// my1.run();
// my2.run();
//void start() 導致此執行緒開始執行; Java虛擬機器呼叫此執行緒的run方法
my1.start();
my2.start();
}
}
- 兩個問題
- 為什麼重寫run方法
- 因為run方法封裝的就是要被執行緒執行的程式碼
- run方法和start方法的區別
- run():封裝執行緒執行的程式碼,直接呼叫,相當於普通方法的呼叫
- start():啟動執行緒;然後由JVM呼叫此執行緒的run()方法
- start方法只能呼叫一次,如果呼叫多次程式將報錯
- 為什麼重寫run方法
2.2 實現Runnable介面
- Thread構造方法
方法名 | 說明 |
---|---|
Thread(Runnable target) | 分配一個新的Thread物件 |
Thread(Runnable target, String name) | 分配一個新的Thread物件 |
- 實現步驟
- 定義一個類實現Runnable介面,這裡我們定義為MyRunnable
- 在MyRunnable類中重寫run()方法
- 建立MyRunnable類的物件
- 建立Thread類的物件,把MyRunnable類物件作為構造方法的引數
- 啟動執行緒
- 程式碼實現
public class MyRunnable implements Runnable {
@Override
public void run() {
for(int i=0; i<100; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
public class MyRunnableDemo {
public static void main(String[] args) {
//建立MyRunnable類的物件
MyRunnable my = new MyRunnable();
//建立Thread類的物件,把MyRunnable物件作為構造方法的引數
//Thread(Runnable target)
// Thread t1 = new Thread(my);
// Thread t2 = new Thread(my);
//Thread(Runnable target, String name)
Thread t1 = new Thread(my,"坦克");
Thread t2 = new Thread(my,"飛機");
//啟動執行緒
t1.start();
t2.start();
}
}
2.3 實現Callable類
- 方法介紹
方法名 | 說明 |
---|---|
V call() | 計算結果,如果無法計算結果,則丟擲一個異常 |
FutureTask(Callable callable) | 建立一個 FutureTask,一旦執行就執行給定的 Callable |
V get() | 如有必要,等待計算完成,然後獲取其結果 |
- 實現步驟
- 定義一個類實現Callable介面,這裡我們將這個類定義為MyCallable
- 在MyCallable類中重寫call()方法
- 建立MyCallable類的物件
- 建立Future的實現類FutureTask物件,把MyCallable物件作為構造方法的引數
- 建立Thread類的物件,把FutureTask物件作為構造方法的引數
- 啟動執行緒
- 呼叫get方法,就可以獲取執行緒結束之後的結果
- 程式碼實現
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("跟女孩表白" + i);
}
//返回值就表示執行緒執行完畢之後的結果
return "答應";
}
}
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//執行緒開啟之後需要執行裡面的call方法
MyCallable mc = new MyCallable();
//Thread t1 = new Thread(mc);
//可以獲取執行緒執行完畢之後的結果.也可以作為引數傳遞給Thread物件
FutureTask<String> ft = new FutureTask<>(mc);
//建立執行緒物件
Thread t1 = new Thread(ft);
String s = ft.get();
//開啟執行緒
t1.start();
//String s = ft.get();
System.out.println(s);
}
}
2.4 三種實現方式的區別
- 站在返回值的角度
- 繼承Thread類和實現Runnable介面的方式沒有返回值,獲取不到執行緒執行的結果
- 實現Callable介面的方式有返回值,所以可以獲取執行緒執行的結果
- 站在繼承方式的角度
- 繼承Thread類的方式,程式設計比較簡單,可以直接使用Thread類的方法,但同時它的擴充套件性相對較弱
- 實現Runnable或Callable介面,因為在Java中一個類可以實現多個介面,因此擴充套件性較高,但同時程式設計相對複雜,不能直接使用Thread類中的方法
3、Thread類的API
3.1 和執行緒名稱相關
- 方法介紹
方法名 | 說明 |
---|---|
void setName(String name) | 設定執行緒名稱 |
public final String getName( ) | 獲取執行緒名稱 |
Thread currentThread() | 返回對當前正在執行的執行緒物件的引用 |
- 程式碼實現
public class MyThread extends Thread {
public MyThread() {}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+":"+i);
}
}
}
public class MyThreadDemo {
public static void main(String[] args) {
MyThread my1 = new MyThread();
MyThread my2 = new MyThread();
//void setName(String name):將此執行緒的名稱更改為等於引數 name
my1.setName("高鐵");
my2.setName("飛機");
//Thread(String name)
MyThread my1 = new MyThread("高鐵");
MyThread my2 = new MyThread("飛機");
my1.start();
my2.start();
//static Thread currentThread() 返回對當前正在執行的執行緒物件的引用
System.out.println(Thread.currentThread().getName());
}
}
3.2 執行緒休眠
- 相關方法
方法名 | 說明 |
---|---|
static native void sleep (long millis) | 使當前正在執行的執行緒停留(暫停執行)指定的毫秒數 |
- 程式碼實現
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
/*System.out.println("睡覺前");
Thread.sleep(3000);
System.out.println("睡醒了");*/
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.start();
t2.start();
}
}
3.3 執行緒優先順序
3.3.1 執行緒排程
- 兩種排程方式
- 分時排程模型
- 所有執行緒輪流使用CPU的使用權,平均分配每個執行緒佔用CPU的時間片
- 搶佔式排程模型
- 優先順序高的執行緒優先使用CPU,如果優先順序相同,那麼隨機選擇一個,優先順序高的執行緒獲取的CPU時間片相對多一些
- 分時排程模型
- JAVA使用的是搶佔式排程模型
- 隨機性
- 假如計算機只有一個CPU ,那麼某一時刻只能執行一條指令,執行緒只有得到CPU時間片,也就是使用權,才可以執行指令。所以說多執行緒程式的執行是隨機性的,因為誰搶到CPU的使用權是不一定的
3.3.2 相關方法
方法名 | 說明 |
---|---|
final int getPriority() | 返回此執行緒的優先順序 |
final void setPriority(int newPriority) | 更改此執行緒的優先順序執行緒預設優先順序是5;執行緒優先順序的範圍是:1-10 |
- 程式碼實現
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
return "執行緒執行完畢了";
}
}
public class Demo {
public static void main(String[] args) {
//優先順序: 1 - 10 預設值:5
MyCallable mc = new MyCallable();
FutureTask<String> ft = new FutureTask<>(mc);
Thread t1 = new Thread(ft);
t1.setName("飛機");
t1.setPriority(10);
//System.out.println(t1.getPriority());//5
t1.start();
MyCallable mc2 = new MyCallable();
FutureTask<String> ft2 = new FutureTask<>(mc2);
Thread t2 = new Thread(ft2);
t2.setName("坦克");
t2.setPriority(1);
//System.out.println(t2.getPriority());//5
t2.start();
}
}
3.4 守護執行緒
- Java語言中的執行緒可以分為普通執行緒和守護執行緒
- 守護執行緒的作用
- 為普通執行緒服務,如果普通執行緒結束,守護執行緒也會結束
- JVM會檢查執行緒的型別,如果當前的JVM程序中所有的執行緒都是守護執行緒,Jvm停止執行。
- 相關方法
方法名 | 說明 |
---|---|
void setDaemon(boolean on) | 將此執行緒標記為守護執行緒 |
- 程式碼實現
public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "---" + i);
}
}
}
public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("備胎");
//把第二個執行緒設定為守護執行緒
//當普通執行緒執行完之後,那麼守護執行緒也沒有繼續執行下去的必要了.
t2.setDaemon(true);
t1.start();
t2.start();
}
}
4、執行緒安全問題
4.1 資料安全問題
- 安全問題出現的條件
- 是多執行緒環境
- 有共享資料
- 有多條語句操作共享資料
- 如何解決多執行緒安全問題
- 基本思想
- 讓程式沒有安全問題的環境
- 如何實現
- 把多條語句操作共享資料的程式碼鎖起來,讓任意時刻只能有一個執行緒執行即可
- 基本思想
- 鎖在Java中存在兩種
- 內部鎖
- 就是通過synchronized進行實現
- 顯式鎖
- 是Jdk1.5之後提供的Lock物件進行實現
- 內部鎖
- 內部鎖的實現
- 同步程式碼塊
- 同步方法
- 靜態同步方法
- 顯式鎖
- 建立Lock類的物件
4.2 同步程式碼塊
- 格式
- synchronized(任意物件) { 多條語句操作共享資料的程式碼 }
- synchronized(任意物件)
- 就相當於給程式碼加鎖,任意物件就能看成是一把鎖
- 同步的好處和弊端
- 好處
- 解決了多執行緒的資料安全問題
- 弊端
- 當執行緒很多時,因為每個執行緒都會判斷同步上的鎖,這會很耗費資源,降低程式執行效率
- 好處
- 程式碼實現
public class SellTicket implements Runnable {
private int tickets = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) { // 對可能有安全問題的程式碼加鎖,多個執行緒必須使用同一把鎖
//t1進來後,就會把這段程式碼給鎖起來
if (tickets > 0) {
try {
Thread.sleep(100);
//t1休息100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//視窗1正在出售第100張票
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "張票");
tickets--; //tickets = 99;
}
}
//t1出來了,這段程式碼的鎖就被釋放了
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket st = new SellTicket();
Thread t1 = new Thread(st, "視窗1");
Thread t2 = new Thread(st, "視窗2");
Thread t3 = new Thread(st, "視窗3");
t1.start();
t2.start();
t3.start();
}
}
- 注意
- 多個執行緒一定要使用同一把鎖
4.3 同步方法
- 同步方法
- 就是把synchronized關鍵字加到方法上
- 格式
- 修飾符 synchronized 返回值型別 方法名(方法引數) { 方法體;}
- 同步方法的鎖物件
- 就是this
- 同步靜態方法
- 修飾符 static synchronized 返回值型別 方法名(方法引數) { 方法體;}
- 同步靜態方法的鎖物件
- 就是類名.class
- 程式碼實現
public class Demo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.setName("視窗一");
t2.setName("視窗二");
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable {
private static int ticketCount = 100;
@Override
public void run() {
while(true){
if("視窗一".equals(Thread.currentThread().getName())){
//同步方法
boolean result = synchronizedMthod();
if(result){
break;
}
}
if("視窗二".equals(Thread.currentThread().getName())){
//同步程式碼塊
synchronized (MyRunnable.class){
if(ticketCount == 0){
break;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() + "在賣票,還剩下" + ticketCount + "張票");
}
}
}
}
}
private static synchronized boolean synchronizedMthod() {
if(ticketCount == 0){
return true;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName()