多線程(Java)

注:本文為本人學習過程中的筆記

1.導入

1.進程和線程

我們希望我們的程序可以并發執行以提升效率,此時引入了多進程編程。可是創建進程等操作開銷太大,于是就將進程進一步拆分成線程,減少開銷。進程與進程之間所涉及到的資源是相互獨立的,不會相互干擾。至于線程之間具體是怎么調度的,我們很難知道,這主要是操作系統隨機調度。

進程是操作系統資源分配的基本單位

線程是操作系統調度執行的基本單位

進程是存在父子關系的,而線程不存在

2.多線程代碼的簡單寫法

1.創建線程

1.重寫Thread中的run方法

public class Test{public static void main(String[] args){Thread t1 = new MyThread;}
}
public class MyThread extends Thread{public void run() {....}
}

2.重寫Runnable接口中的run方法?

public class Test implements Runnable{public static void main(String[] args){Runnable myRunnable = new MyRunnable();Thread t1 = new Thread(myRunnable);}
}
public class MyRunnable{public void run(){...}
}

3.使用lamda表達式?

public class Test{public static void main(String[] args){Thread t1 = new Thread(() -> {public void run(){...} });}
}

2.一些方法

new Thread()

a.Thread()? ? ? ? 直接創建線程對象

b.Thread(Runnable target)? ? ? ? 使用Runnable對象創建線程對象

c.Thread(String name)? ? ? ? 創建線程對象并命名

d.Thread(Runnable target, String name)? ? ? ? 使用Runnable對象創建線程對象并命名

run()

run方法是線程的入口方法,進入線程自動就會調用,不需要我們調用

start()

這是啟動線程的方法

sleep()?

這個方法要通過Thread類來調用,效果是使當前線程休眠一定時間。在括號里可以設置休眠的時間,單位是毫秒。

Thread.sleep(1000);

使用這個方法時會拋出InterruptedException異常?

因為線程的調度是不可控的,所以這個方法只能保證實際休眠時間大于等于參數設置的時間。代碼調用sleep,相當于讓當前線程讓出cpu資源,后續時間到的時候就需要操作系統內核把這個線程重新調度到cpu上才能繼續執行。

sleep(0)是一種特殊寫法,意味著讓當前線程立即放棄cpu資源等待操作系統重新調度

getId()

Java中會給每個運行的線程分配id,獲取id

getName()

獲取名字

getState

獲取狀態

getPriority

獲取優先級

isDaemon()

判斷是否是后臺線程

Java中存在后臺線程和前臺線程,后臺線程隨著進程的開啟而開啟,關閉而關閉,不影響進程的狀態,我們創建的線程和main線程是前臺線程,可以通過setDaemon方法修改

setDaemon()

在括號里填寫true或者false來設置線程是否是后臺線程

isAive()

判斷線程是否存活

Java代碼中創建的Thread對象和系統中的線程是一一對應關系。但是,Thread對象的生命周期和系統中的生命周期是不同的,可能存在Thread對象還存貨,但是系統中的線程已經銷毀的情況

interrupt()

關閉線程

調用這個方法時,會修改isInterruptted方法內部的標志位將其設為true。如果我們在使用了sleep方法并且喚醒了該方法,那sleep方法就會把isInterruptted的標志位設置為false,這時sleep會拋出Interruptted異常,我們可以修改try-catch語句中的代碼,達到我們想要的效果而不是直接關閉線程

isInterruptted()

判斷線程是否關閉

Thread.currentThread()

這是一個靜態方法,在哪個線程中調用就能獲得哪個線程的應用

join()

join能夠要求多個線程結束的先后順序。比如在main線程中調用t.join就會使main線程等待t線程先結束。只要t線程不結束,主線程的join就會一直的等待下去。我們可以在括號里設置最大等待時間,當到達時間join就不會再等待,繼續執行下面的代碼

wait() / notify()

這兩個方法是用來協調線程之間的執行邏輯的順序。雖然我們不能干預調度器的調度順序,但是我們可以讓后執行的線程進行等待,等到先執行的線程執行完了,通知當前線程,繼續執行。

在Java標準庫中,每個產生阻塞的方法都會拋出InterrupttedException異常,會被interrupt方法喚醒,wait也是一樣

wait和join的區別

join也是等,當join是等另一個線程徹底執行完才繼續執行

wait是等另一個線程執行到notify才繼續走,不需要等另一個線程執行完

應用場景

當多個線程競爭一把鎖的時候,獲取鎖的線程如果釋放了,其他哪個線程能拿到這把鎖是不確定,我們不能控制操作系統怎么調度,當我們可以使用wait和notify語句來控制這個順序

使用方法

使用wait時,線程會釋放鎖,所以我們要使用wait時線程必須獲取鎖,否則會報錯

wait的等待最關鍵的一點就是先釋放鎖,給其他線程獲取鎖的機會,并且阻塞等待。如果其他線程做完了必要的工作,調用notify喚醒這個wait線程,wait就會解除阻塞,重新獲取到鎖,然后繼續執行

synchronized (locker) {locker.wait();
}synchronized(locker) {locker.notify();
}

這其中的鎖對象必須時同一個鎖對象才能產生效果,如果多個線程在同一個鎖對象上wait,進行notify的時候是隨機喚醒其中一個線程,一次notify喚醒一個wait。wait也可以在括號填寫超時時間,不死等。

wait和sleep的區別

wait有等待時間,可以用notify喚醒。sleep也有等待時間,可以使用interrupt提前喚醒

wait必須要搭配鎖使用,先加鎖,才能用wait,sleep不需要

如果都是在synchronized內部使用,wait會釋放鎖,而sleep不會釋放鎖

notifyAll()

使用這個方法可以一次喚醒所有相關的wait

3.小工具

1.jconsole

這個小工具在jdk的bin目錄下,使用這個工具可以連接java程序,從而觀察線程的信息

3.線程狀態

NEW

安排了工作,還未開始行動

也就是new了Thread對象,還沒start

TERMINATED

工作完成了

內核中的線程已經結束了但是Thread對象還在

RUNNABLE

可工作的,又可以分成正在工作中和即將開始工作

a.線程正在cpu上執行

b.線程隨時可以去cpu上執行

TIMED_WAITTING

表示排隊等著其他事情,有超時時間

當我們調用join方法,線程就會進入這個狀態。

WAITING

表示排隊等待其他事情,沒有超時時間,死等

BLOCKED

表示排隊等待其他事情

這個比較特殊,是由于鎖導致的阻塞

總圖

4.線程安全

一段代碼,如果再多線程并發執行的情況下,出現bug,就稱為線程不安全

1.線程安全問題產生的原因

Java中的一行代碼對應著cpu上的多條指令,并不是原子的,所以當cpu隨機調度的時候就可能出現一些問題,也就會產生線程安全問題,以下是產生線程安全問題的原因

a.(根本原因)操作系統對于線程的調度是隨機的,搶占式執行

b.多個線程同時修改一個變量

c.修改操作不是原子的

d.內存可見性,jvm會優化代碼

e.指令重排序,jvm會優化代碼

2.如何解決線程安全問題

a.針對問題a我們是無法解決的,因為搶占式執行是操作系統的底層設定

b.針對問題b,這個問題和代碼結構相關,我們可以調整代碼結構,規避一些線程不安全的代碼。但是這樣的方案是不夠通用的,有些情況下,需求上就是需要多線程同時修改一個變量

c.針對問題c,我們可以使用加鎖操作,這也是Java中解決線程安全問題最主要的方案,通過加鎖操作我們可以將不是原子的操作打包成原子的操作。

d.使用volatile關鍵字

jvm對于我們的代碼會自動的進行一些優化,比如說如果我們寫出這樣的一個語句

public class Test{int count = 0;public static void main(String[] args) throws InterrupttedException{Thread t1 = new Thread(() -> {while(count == 0) {}});t1.start();Thread.sleep(30000);count = 1;}
}

當我們啟動這個代碼時,線程t1中的循環會被一直執行下去,這是為什么呢?明明我們在主線程中都修改了count的值

因為這個時候jvm優化了我們的代碼,在t1線程中,while語句不斷地從內存中讀取count的值,這個操作的開銷是比較大的,此時jvm會將count的值保留在cpu寄存器中,直接讀取寄存器的count值,這就導致了我們修改了count的值而不會被讀取到

此時就可以使用volatile關鍵字修飾count這個變量讓jvm不優化我們的代碼

注意:在Java的官方文檔中這樣寫道,每個線程,有一個自己的“工作內存”,同時這些線程共享一個“主內存”。當一個線程循環進行上述讀取變量操作的時候,就會把主內存中的數據,拷貝到......

這里提到的“工作內存”就是我們所說的cpu寄存器,cpu上還有緩存,因為Java是跨平臺的語言,設計者不希望程序員有學習硬件知識的成本,所以將其抽象為“工作內存的概念”。

e.使用volatile關鍵字

volatile關鍵字不僅可以解決內存可見性問題,還可以不讓jvm進行指令重排序。

3.鎖

加鎖/解鎖本身是系統提供的api,很多編程語言都對這樣的api進行了封裝,大多數的封裝風格都是采取lock和unlock兩個函數,Java中采用的是synchronized關鍵字

synchronized

synchronized(鎖對象) {//進入代碼塊相當于加鎖...
}//離開代碼塊相當于解鎖

括號里需要我們填寫加鎖的對象,這個鎖的類型不重要,重要的是是否有幾個線程嘗試針對同一個鎖對象進行加鎖。只有兩個線程針對同一個鎖對象加鎖,才能產生互斥效果。即一個線程獲取鎖之后,另一個線程為了也能獲取鎖只能阻塞等待第一個線程中的鎖釋放出來?

synchronized的變種寫法

synchronized可以對方法進行加鎖

當要加鎖的方法是被static修飾時,synchronized修飾static方法就相當于針對類對象進行加鎖

死鎖

產生死鎖的原因
1.互斥

當兩個線程爭奪同一個鎖時會產生互斥,這是鎖的基本特性

2.不可剝奪

當一個線程獲得鎖之后,這個鎖是不能被搶走的,只能等待它釋放出來,這也是鎖的基本特性

3.請求和保持

當一個線程已經獲取一把鎖之后,繼續請求其他的鎖

4.循環等待

當一個線程已經獲取一把鎖之后,繼續請求其他鎖,且有另一個線程也在獲得鎖的情況下請求相同的鎖,形成循環

解決死鎖的辦法

針對問題1和問題2是無法解決的,這是鎖的基本特性

針對問題3

我們可以避免鎖嵌套

針對問題4

可以約定加鎖的順序,使爭奪鎖的過程形不成循環

2.Java中線程安全的東西

String

系統api沒有提供修改String的方法,導致String天然就是線程安全的

5.多線程代碼案例

1.單例模式

單例模式是指在某個類,某個程序中只允許有唯一一個實例,不允許有多個實例,不允許new多次。

1.應用場景

比如我們有一個100G的數據庫要加載到內存中方便讀取,由于這個數據庫數據非常多,創建等操作需要的開銷都非常大,此時我們希望只創建一次即可,否則將會產生非常多的額外的開銷也可能導致服務器的內存不夠用。

2.兩種寫法

懶漢模式和餓漢模式是存在缺陷的,可以通過反射來創建實例,但是反射本身屬于非常規的手段,一般編寫代碼的時候不使用

1.餓漢模式

餓是指盡早創建實例

1.寫法
class SingleTon{private static SingleTon instance;//靜態成員的初始化是在類加載的時候就觸發的,往往程序一啟動類就會加載public static SingleTon getInstance(){return instance;}//后續統一使用getInstance這個方法來獲取實例private SingleTon{}//由于構造方法是私有的,所以在類外new instance都會編譯失敗
}
2.線程安全問題

由于餓漢模式中的instance在程序啟動的時候就創建好了,所以我們后續的操作都只涉及到讀操作,不會產生線程安全問題。?

2.懶漢模式

懶是指盡可能晚地創建實例,延遲創建

1.寫法
class SingleTonLazy{private static SingleTonLazy instance = null;public SingleTonLazy getInstance(){if(instance == null){//懶漢模式創建實例的時機是第一次使用的時候而不是程序啟動的時候instance = new SingleTonLazy();}return instance;}private SingleTonLazy{}
}
2.線程安全問題?

懶漢模式這里getInstance里面給instance賦值這個操作,等號是原子的,但是當它和if語句組合在一起的時候,就變得不是原子的了,此時我們就可以給if語句和內部的賦值語句加鎖,讓它變成原子的。

class SingleTonLazy{Object locker = new Object();private static SingleTonLazy instance = null;public SingleTonLazy getInstance(){synchronized(locker){if(instance == null){instance = new SingleTonLazy();}return instance;}}private SingleTonLazy{}
}

這里加鎖之后,我們使用getInstance就是線程安全的了。可是,這又引出了一個新的問題,當我們第一次調用過getInstance語句之后,instance就創建好了,我們之后再調用getInstance都只需要使用return就好,可是我們在這里加了鎖,如果有多個線程同時調用這個方法,就會產生阻塞,影響程序的效率。?這個時候,我們就可以再加一個if語句

class SingleTonLazy{Object locker = new Object();private static SingleTonLazy instance = null;public SingleTonLazy getInstance(){if(instance == null){//這個if是來判斷是否需要加鎖synchronized(locker){if(instance == null){//這個if是來判斷是否需要創建instanceinstance = new SingleTonLazy();}return instance;}}}private SingleTonLazy{}
}

進行了這么多處理之后,代碼依然存在一些問題,instance = new SingleTonLazy()這個操作可能涉及指令重排序問題,jvm可能會更改指令的順序,new這個操作涉及到申請內存空間,初始化對象,將內存地址賦值給引用變量。如果這個new操作先把內存地址賦值給引用變量,再進行變量初始化的話,這時另一個線程使用getInstance方法時,instance這個實例就已經存在了,可是還沒有初始化,這又構成了線程安全問題,此時我們就可以使用volatile關鍵字來修飾instance,阻止jvm進行指令重排序

class SingleTonLazy{Object locker = new Object();private static volatile SingleTonLazy instance = null;public SingleTonLazy getInstance(){if(instance == null){//這個if是來判斷是否需要加鎖synchronized(locker){if(instance == null){//這個if是來判斷是否需要創建instanceinstance = new SingleTonLazy();}return instance;}}}private SingleTonLazy{}
}

這樣我們的代碼就是線程安全的了。?

2.阻塞隊列

阻塞隊列其實就是一種更復雜的隊列,它是線程安全的

1.特性

a.隊列為空時,嘗試出隊列,出隊列操作就會阻塞,阻塞到其他線程添加元素為止

b.隊列為滿時,嘗試入隊列,入隊列操作也會阻塞,阻塞到其他線程取走元素為止

2.應用場景

1.生產者消費者模型

簡單來說就是生產者生產產品,消費者消費產品,他們在一個消費場所進行這些操作,而這個消費場所就是阻塞隊列

1.該模型的優點
1.解耦合

這里的解耦合不一定是兩個線程之間,也可以是兩個服務器之間。

如果是A直接訪問B,此時A和B的耦合就會更高。編寫A的代碼的時候,多多少少會有一些和B相關的邏輯,編寫B的代碼的時候,也會有一些A的相關邏輯。?

此時添加一個阻塞隊列,讓A和隊列交互,讓B和隊列交互,這樣A和B之間就解耦合了。A和B是業務服務器,所以經常會涉及到改動,而阻塞隊列并不會經常修改,所以A,B分別和阻塞隊列耦合是沒什么問題的。阻塞隊列非常重要,有時甚至會把隊列單獨部署成一個服務,稱為“消息隊列”。

2.削峰填谷

在實際的應用場景中,服務器接收到的請求并不是穩定的,有時候會很多,有時又很少,當沒有阻塞隊列,兩個服務器直接進行交互時

當A遇到一波流量激增,此時它會把每個請求都轉發給B,B也會承擔一樣的壓力,此時就很容易把B給搞掛了。?

一般來說A這種上游的服務器,尤其是入口的服務器,干的活更簡單,單個請求消耗的資源較少,而像B這種下游的服務器,通常承擔更重的任務量(復雜的計算/存儲工作),單個請求消耗的資源更多。

如果有阻塞隊列的話,壓力就可以由阻塞隊列來承擔,B可以不關心數據量的多少,按照自己的節奏慢慢處理隊列里的數據即可。由于大量的請求一般都是突發的,時間也不長,所以B可以趁著峰值過去了繼續消費數據,利用波谷的時間,來消費之前積壓的數據。

2.該模型的缺點

1.引入隊列之后,整體的結構會更復雜,此時就需要更多的機器進行部署,生產環境的結構也會更加復雜,管理起來更麻煩

2.效率會有影響

3.使用

Java標準庫中提供了阻塞隊列,我們可以直接使用

1.put()/take()

阻塞隊列繼承了Queue接口,所以我們可以使用offer()和pull()來存取數據,但是想要達到阻塞效果的話必須使用take()和put()方法。?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/76432.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/76432.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/76432.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

在 Dev-C++中編譯運行GUI 程序介紹(三)有趣示例一組

在 Dev-C中編譯運行GUI程序介紹(三)有趣示例一組 前期見 在 Dev-C中編譯運行GUI 程序介紹(一)基礎 https://blog.csdn.net/cnds123/article/details/147019078 在 Dev-C中編譯運行GUI 程序介紹(二)示例&a…

【高校主辦】2025年第四屆信息與通信工程國際會議(JCICE 2025)

重要信息 會議網址:www.jcice.org 會議時間:2025年7月25-27日 召開地點:哈爾濱 截稿時間:2025年6月15日 錄用通知:投稿后2周內 收錄檢索:EI,Scopus 會議簡介 JCICE 2022、JCICE 2023、JCICE 2…

【Linux】Linux 操作系統 - 03 ,初步指令結尾 + shell 理解

文章目錄 前言一、打包和壓縮二、有關體系結構 (考)面試題 三、重要的熱鍵四、shell 命令及運行原理初步理解五、本節命令總結總結 前言 本篇文章 , 筆者記錄的筆記內容包含 : 基礎指令 、重要熱鍵 、shell 初步理解 、權限用戶的部分問題 。 內容皆是重要知識點 , 需要認真理…

Python: sqlite3.OperationalError: no such table: ***解析

出現該錯誤說明數據庫中沒有成功創建 reviews 表。以下是完整的解決方案: 步驟 1:創建數據庫表 在插入數據前,必須先執行建表語句。請通過以下任一方式創建表: 方式一:使用 SQLite 命令行 bash 復制 # 進入 SQLite 命令行 sqlite3 reviews.db# 執行建表語句 CREATE T…

VSCode CLine 插件自定義配置使用 Claude 3.7 模型進行 AI 開發

一個互聯網技術玩家,一個愛聊技術的家伙。在工作和學習中不斷思考,把這些思考總結出來,并分享,和大家一起交流進步。 本文介紹如何在 Visual Studio Code (VSCode) 中安裝和自定義配置 CLine 插件,并使用 Claude 3.7 模…

【VSCode配置】運行springboot項目和vue項目

目錄 安裝VSCode安裝軟件安裝插件VSCode配置user的全局設置setting.jsonworkshop的項目自定義設置setting.jsonworkshop的項目啟動配置launch.json 安裝VSCode 官網下載 安裝軟件 git安裝1.1.12版本,1.2.X高版本無法安裝node14以下版本 nvm安裝(github…

linux shell編程之條件語句(二)

目錄 一. 條件測試操作 1. 文件測試 2. 整數值比較 3. 字符串比較 4. 邏輯測試 二. if 條件語句 1. if 語句的結構 (1) 單分支 if 語句 (2) 雙分支 if 語句 (3) 多分支 if 語句 2. if 語句應用示例 (1) 單分支 if 語句應用 (2) 雙分支 if 語句應用 (3) 多分支 …

榕壹云在線商城系統:基于THinkPHP+ Mysql+UniApp全端適配、高效部署的電商解決方案

項目背景:解決多端電商開發的痛點 隨著移動互聯網的普及和用戶購物習慣的碎片化,傳統電商系統面臨以下挑戰: 1. 多平臺適配成本高:需要同時開發App、小程序、H5等多端應用,重復開發導致資源浪費。 2. 技術依賴第三方…

神經動力學系統與計算及AI拓展

大腦,一個蘊藏在我們顱骨之內的宇宙,以活動脈動,如同由電信號和化學信號編織而成的交響樂,精巧地協調著思想、情感和行為。但是,這種復雜的神經元舞蹈是如何產生我們豐富多彩的精神生活的呢?這正是神經動力…

K8s常用基礎管理命令(一)

基礎管理命令 基礎命令kubectl get命令kubectl create命令kubectl apply命令kubectl delete命令kubectl describe命令kubectl explain命令kubectl run命令kubectl cp命令kubectl edit命令kubectl logs命令kubectl exec命令kubectl port-forward命令kubectl patch命令 集群管理命…

本地化部署DeepSeek-R1蒸餾大模型:基于飛槳PaddleNLP 3.0的實戰指南

目錄 一、飛槳框架3.0:大模型推理新范式的開啟1.1 自動并行機制革新:解放多卡推理1.2 推理-訓練統一設計:一套代碼全流程復用 二、本地部署DeepSeek-R1-Distill-Llama-8B的實戰流程2.1 機器環境說明2.2 模型與推理腳本準備2.3 啟動 Docker 容…

單片機方案開發 代寫程序/燒錄芯片 九齊/應廣等 電動玩具 小家電 語音開發

在電子產品設計中,單片機(MCU)無疑是最重要的組成部分之一。無論是消費電子、智能家居、工業控制,還是可穿戴設備,小家電等,單片機的應用無處不在。 單片機,簡而言之,就是將計算機…

【位運算】兩整數之和

文章目錄 371. 兩整數之和解題思路:位運算 371. 兩整數之和 371. 兩整數之和 ? 給你兩個整數 a 和 b ,不使用 運算符 和 - ,計算并返回兩整數之和。 示例 1: 輸入:a 1, b 2 輸出:3示例 2&#xff1…

使用Python從零實現一個端到端多模態 Transformer大模型

嘿,各位!今天咱們要來一場超級酷炫的多模態 Transformer 冒險之旅!想象一下,讓一個模型既能看懂圖片,又能理解文字,然后還能生成有趣的回答。聽起來是不是很像超級英雄的超能力?別急&#xff0c…

新聞推薦系統(springboot+vue+mysql)含萬字文檔+運行說明文檔

新聞推薦系統(springbootvuemysql)含萬字文檔運行說明文檔 該系統是一個新聞推薦系統,分為管理員和用戶兩個角色。管理員模塊包括個人中心、用戶管理、排行榜管理、新聞管理、我的收藏管理和系統管理等功能。管理員可以通過這些功能進行用戶信息管理、查看和編輯用…

游戲引擎學習第218天

構建并運行,注意一下在調試系統關閉前人物的移動速度 現在我準備開始構建項目。如果我沒記錯的話,我們之前關閉了調試系統,主要是為了避免大家在運行過程中遇到問題。現在調試系統沒有開啟,一切運行得很順利,看到那個…

基于混合編碼器和邊緣引導的拉普拉斯金字塔網絡用于遙感變化檢測

Laplacian Pyramid Network With HybridEncoder and Edge Guidance for RemoteSensing Change Detection 0、摘要 遙感變化檢測(CD)是觀測和分析動態土地覆蓋變化的一項關鍵任務。許多基于深度學習的CD方法表現出強大的性能,但它們的有效性…

Go語言從零構建SQL數據庫(6) - sql解析器(番外)- *號的處理

番外:處理SQL通配符查詢 在SQL中,SELECT * FROM table是最基礎的查詢之一,星號(*)是一個通配符,表示"選擇所有列"。雖然通配符查詢看起來簡單,但在解析器中需要特殊處理。下面詳細介…

淺析Centos7安裝Oracle12數據庫

Linux下的Oracle數據庫實在是太難安裝了,事賊多,我都懷疑能安裝成功是不是運氣的成分更高一些。這里操作系統是Centos7,Oracle版本是Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production。 Oracle下載鏈接: https…

02-redis-源碼下載

1、進入到官網 redis官網地址https://redis.io/ 2 進入到download頁面 官網頁面往最底下滑動,找到如下頁面 點擊【download】跳轉如下頁面,直接訪問:【https://redis.io/downloads/#stack】到如下頁面 ? 3 找到對應版本的源碼 https…