Linux——線程互斥

文章目錄

  • 一、有關概念
    • 原子性
    • 錯誤認知澄清
    • 加鎖
  • 二、鎖的相關函數
    • 全局鎖
    • 局部鎖
    • 初始化
    • 銷毀
    • 加鎖
    • 解鎖
  • 三、鎖相關
    • 如何看待鎖
    • 一個線程在執行臨界區的代碼時,可以被切換嗎?
    • 鎖是本身也是臨界資源,它如何做到保護自己?(鎖的實現)
      • 軟件層面的互斥鎖的實現
      • 硬件層面的互斥鎖的實現
    • 鎖是不允許拷貝構造或者賦值拷貝的
    • 鎖的饑餓問題

一、有關概念

  • 共享資源:多執行流運行時都能使用的資源
  • 臨界資源:多線程執行流被保護的共享的資源就叫做臨界資源
  • 臨界區:每個線程內部,訪問臨界資源的代碼,就叫做臨界區
  • 互斥:任何時刻,互斥保證有且只有一個執行流進入臨界區,訪問臨界資源,通常對臨界資源起保護作用
  • 原子性:不會被任何調度機制打斷的操作,該操作只有兩態,要么完成,要么未完成
  • 保護的方式常見:互斥與同步
  • 多個執行流,訪問臨界資源的時候,具有一定的順序性,叫做同步
  • 在進程中涉及到互斥資源的程序段叫臨界區。你寫的代碼=訪問臨界資源的代碼(臨界區)+不訪問臨界資源的代碼(非臨界區)
  • 所謂的對共享資源進行保護,本質是對訪問共享資源的代碼進行保護

原子性

原子性是指一個操作在執行過程中不會被其他線程或者中斷所干擾

即這個操作要么完全執行,要么完全不執行,不會出現只執行了一部分的情況。

注意:
在計算機系統中,原子性指令的設計目標就是確保其執行過程不可分割,即使在多核并行環境下,同一原子指令也不可能被兩個CPU核心真正“同時”執行

原因:

  • 總線鎖定

    原理:當CPU核心執行原子指令時,會通過總線信號鎖定內存區域,阻止其他核心訪問對應變量物理內存地址,防止變量被修改被讀取
    代價:鎖定總線會導致其他核心的訪存操作被阻塞,影響整體性能

  • 緩存鎖定

    原理:利用緩存一致性協議,在緩存行級別鎖定內存區域,無需全局總線鎖定。
    優勢:更高效,僅阻塞對特定緩存行的訪問

  • 硬件指令原子性
    某些指令(如x86的LOCK前綴指令)直接在硬件層面保證原子性,例如:

 LOCK ADD [mem], 1  ; 原子遞增內存值

錯誤認知澄清

誤區:原子操作等同于“互斥”?
錯誤觀點:原子操作讓其他線程完全無法訪問變量

現實:原子操作僅保證特定操作的原子性,其他線程仍可自由訪問變量(例如,通過非原子方式讀取,或執行其他原子操作)

std::atomic<int> x(0);
int y = 0;線程A(原子寫)
x.store(42, std::memory_order_relaxed);線程B(非原子讀!)
int local_x = x.load(std::memory_order_relaxed);  正確:原子讀
int local_y = y;                                  錯誤:非原子讀,可能讀到未同步的值

原子操作和互斥鎖雖然都能實現線程安全,但它們的核心機制和適用場景不同:

  • 原子操作:針對單個變量的特定操作,通過硬件指令實現高效無鎖同步
  • 互斥鎖:保護代碼塊內的任意操作(無論涉及多少變量),通過阻塞實現強一致性

所以:
原子性和互斥鎖都能保證對共享資源的進行某一操作時,多執行流必須串行執行,但是互斥鎖保護的范圍比原子性更大

多執行流時,共享資源如果不加保護會怎么樣?

多執行流時,共享資源不互斥(沒有原子性)可能會怎樣?
很可能產生數據不一致問題


下面是4個線程同時進行搶票的操作,票數就是全局變量ticket

#include <iostream>
#include <unistd.h>
#include <pthread.h>int ticket = 100;void* Route(void* args)
{char* buf = (char*)args;while(true){if(ticket > 0){sleep(1);std::cout << buf << "sell ticket: " << ticket << std::endl;ticket--;}else{break;}}return nullptr;
}int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1, nullptr, Route, (void*)"thread 1");pthread_create(&t2, nullptr, Route, (void*)"thread 2");pthread_create(&t3, nullptr, Route, (void*)"thread 3");pthread_create(&t4, nullptr, Route, (void*)"thread 4");pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);pthread_join(t4, nullptr);return 0;
}

在這里插入圖片描述

為什么最后搶票會搶出負數?
if(ticket>0)不是原子的
因為它會變成3條匯編指令,一條匯編指令雖然是原子的,但是3條匯編指令和在一起的操作就不是原子的了
所以在CPU在執行這3條匯編指令期間,都有可能進行線程切換。

比如:
ticket=1了,線程a把1讀取到寄存器之后,線程a就切換了,還沒去–ticket
線程b也來讀取了,也把ticket=1讀到寄存器里了
這個時候,線程a和線程b就都會判斷,ticket>0,就都進去搶了

而且
ticket–也不是原子的

線程/進程什么時候會發生切換?

  • 線程時間片到了
  • 來了一個(多個)優先級更高的進程/線程,此時CPU上的線程時間片沒有耗盡也可能會被切換
  • CPU上的線程執行阻塞了(比如執行了sleep暫停代碼,scanf等待鍵盤等)線程進入等待隊列,代碼不執行了
    CPU就不會讓這個線程占著茅坑不拉屎,就會直接切換到其他線程

因為ticketnum–編譯之后,會變成3條匯編指令

  1. 讀取ticket到CPU的寄存器
  2. CPU執行–計算
  3. 把計算之后的ticket結果寫回內存

所以ticket–不是原子的

所以上面的代碼,在ticket=1時:
線程1執行if判斷時,可以通過,然后執行sleep時,就會阻塞,就切換到線程2了

線程2執行if判斷時,ticket還是1,所以線程2也能通過,然后執行sleep,阻塞,就切換到線程3

線程3…

所以最后if的{}里面同時進入了4個線程
4個線程依次從阻塞狀態恢復,依次對ticket進行–
ticket就減到了-2

還是上面的4個線程搶票問題

因為ticket–編譯之后,會變成3條匯編指令

  1. 讀取ticket到CPU的寄存器
  2. CPU執行–計算
  3. 把計算之后的ticket結果寫回內存

所以ticket–不是原子的
假設線程1要執行ticket–了,此時ticket的值為10000

CPU執行第一個匯編指令,把10000寫進CPU寄存器
CPU執行第二個匯編指令,把10000減到了9999
CPU剛準備執行第3個匯編指令時,線程1的時間片到了
那么CPU就會把CPU中線程1相關的寄存器中的數據保存,即保存上下文數據(PC指針和9999等)

然后線程2被切換上來了,正好線程2也要執行ticket–

而線程2運氣比較好,它一直循環執行了9999次ticket–
于是線程2從10000開始減[ 因為線程1的9999沒有寫回內存,而線程的上下文是線程私有的 ]把ticket減到了1

線程2準備再次執行ticket–時,也和線程1一樣,剛執行到第二條匯編代碼,把ticket減到0,時間片就到了

線程2就被切換成了線程1
線程1恢復上下文之后,根據PC指針中的下一條匯編代碼繼續執行
就把自己計算的結果:9999寫回了內存中的ticket中
然后從循環從9999開始減…

所以線程2就白干了

加鎖

如何給共享資源增加互斥性質?

多執行流時保護共享資源的本質其實是:
保護臨界區的代碼,因為共享資源是通過臨界區的代碼訪問的

那么給共享資源增加互斥性質,本質就是給臨界區代碼添加互斥性質
讓任意時刻最多同時有一個執行流執行該臨界區的代碼
如何給臨界區添加互斥性質?

加鎖
Linux上提供的這把鎖叫互斥量。

加了鎖之后:
每個線程(執行流)執行這個互斥性質的臨界區的代碼之前,都必須先申請鎖,只有申請鎖成功的那個線程才能執行臨界區的代碼

二、鎖的相關函數

pthread_mutex_t類型的結構體

分為

全局鎖

  • 全局鎖可以使用pthread_mutex_init或者PTHREAD_MUTEX_INITIALIZER初始化
  • 全局鎖銷不銷毀無所謂,因為生命周期本來就和進程一樣長
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

局部鎖

  • 只能使用pthread_mutex_init初始化
  • 并且需要使用pthread_mutex_destroy銷毀局部鎖
  • 鎖是局部的,所以要讓所有線程都看到的話,就需要把鎖的地址/引用傳給所有線程

初始化

pthread_mutex_init
作用:初始化對應的鎖

#include <pthread.h>int pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t * attr);
  • pthread_mutex_t* mutex:要初始化的鎖的地址
  • const pthread_mutexattr_t* attr:用戶指定的鎖的屬性,一般不管,設置為nullptr
  • 返回值
    0 成功,互斥鎖(mutex)初始化完成。
    非 0 失敗,返回的錯誤代碼

銷毀

pthread_mutex_destroy

作用:銷毀對應的鎖

 int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • pthread_mutex_t*mutex:要銷毀的鎖的地址
  • 返回值
    0 成功
    非 0 失敗

加鎖

pthread_mutex_lock

作用:對一個臨界區上鎖(申請一個訪問對應臨界區的"入場券")

  • 申請成功:就獲得對應的入場券
  • 申請失敗:就說明其他線程已經把入場券搶完了,此時線程的PCB就進入對應的等待隊列阻塞
int pthread_mutex_lock(pthread_mutex_t *mutex);
  • pthread_mutex_t*mutex:鎖對象的地址
  • 返回值
    0 成功
    非 0 失敗

pthread_mutex_trylock

作用:對一個臨界區上鎖(申請一個訪問對應臨界區的"入場券"):

  • 申請成功:就獲得對應的入場券
  • 申請失敗:就說明其他線程已經把入場券搶完了,此時線程不阻塞,直接返回一個錯誤碼
 int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • pthread_mutex_t*mutex:鎖對象的地址
  • 返回值
    0 成功
    非 0 失敗

解鎖

pthread_mutex_unlock

作用:
解除對應的鎖(把一個訪問對應臨界區的"入場券"還回去,讓其他線程可以去搶"入場券")

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • pthread_mutex_t*mutex:鎖對象的地址
  • 返回值
    0 成功
    非 0 失敗

三、鎖相關

如何看待鎖

鎖的本質就是一個二元信號量
而二元信號量本質是一個值只可能為1或0的計數器
這個計數器作為鎖時:記的是訪問對應臨界區的"入場券"數量

  • 沒有線程申請訪問對應臨界區時,count為1
  • 有一個線程成功申請到了使用對應臨界區的資格時,count就變成0
  • 解鎖的話,count就從0變成1

所以鎖本質是一個預定機制

一個線程在執行臨界區的代碼時,可以被切換嗎?

可以被切換

而且這個線程切換了之后,其他線程依然不能進入臨界區
因為這個線程還沒有調用解鎖的接口,所以這個線程把鎖“拿走了”

所以一個線程執行臨界區代碼這個操作,對于其他線程來說就是具有原子性的!

因為對于其他線程而言:
這個臨界區的代碼要嗎沒有被這個線程執行,要嗎就是這個線程執行完了
執行過程中不可能被任何其他線程干擾

鎖是本身也是臨界資源,它如何做到保護自己?(鎖的實現)

每個線程(執行流)執行某個互斥性質的臨界區的代碼之前,都必須先申請鎖,只有申請成功的那個線程才能執行臨界區的代碼

鎖需要被所有線程共享訪問,因此它本身是一種共享資源。
由于鎖的實現必須保證自身操作的原子性(如通過硬件指令避免競爭),所以鎖也是一種臨界資源——它需要被自身的機制保護。

軟件層面的互斥鎖的實現

軟件層面鎖是如何自保的?

  • 加鎖和解鎖的操作是原子的 鎖的本質是一個二元信號量,即一個只有0和1的計數器
    我們知道++和–操作都不是原子的,所以鎖不能通過++或者–來修改自己的值

為了實現互斥鎖,體系結構[X86,X64等]提供了兩個新的匯編指令,swap和xchange
它們的作用都是:交換一個寄存器和一個物理內存中的變量的值

因為swap和xchange都只是一條匯編指令,所以他們兩個操作都是原子的

函數pthread_mutex_lock和unlock實現的偽代碼如下圖:

在這里插入圖片描述


調用pthread_mutex_init或者使用宏初始化鎖之后,物理內存中鎖mutex里面的值為1

  • ①movb $0,%al:就是把0放進一個寄存器中

  • ②xchge %al,mutex:就是交換寄存器和mutex中的值

    • 1.如果寄存器交換得到的值>0,這個線程就申請鎖成功,獲得進入臨界區的資格
    • 2.如果寄存器交換得到的值<0,這個線程就會被阻塞,等到獲取到鎖的線程解鎖之后,才會繼續運行
  • ④最后執行goot lock,即回到pthread_mutex_lock函數的開頭重新執行一遍,看能不能搶到鎖

線程進入pthread_mutex_lock函數之后依然可以進行切換,并且不會影響鎖的獲取
為什么?
假設有兩個線程
線程1先調用pthread_mutex_lock,當線程1執行完第②條匯編指令[xchge %al,mutex],把寄存器中的0與mutex中的1進行了交換

然后就被切換走了
切換之前,CPU會保護線程1的上下文數據,所以線程1就把mutex中的1放進上下文里帶走了

線程2切換上來之后,也執行了lock方法想要獲取鎖
線程2執行匯編指令①:把0放進寄存器中,把線程1留下的1覆蓋
執行匯編指令②,交換寄存器與mutex的值
但是此時線程2只能從mutex里面拿到被線程1換進去的0
拿不到1了,所以線程2獲取鎖失敗,被阻塞

所以

  • 線程1如果在執行匯編指令②之前被切換,本來就不影響鎖的競爭
  • 線程1如果在執行完匯編指令②并且成功獲取了鎖,之后被切換,即使線程1被切換了它也會把1(鎖)帶走

所以其實整個pthread_mutex_lock中,匯編指令②xchge %al,mutex就是申請鎖
pthread_mutex_unlock中movb $1,mutex就是解鎖

所以
線程們競爭的資源是什么?
是mutex這個變量空間嗎?不是!

因為所有線程都可以與變量空間中的值進行交換
線程們競爭的是1,是mutex初始時(或者解鎖操作執行后)mutex里面那唯一的一個1

mutex里面的值可能>1或者<0嗎?
不可能!!!
因為鎖只能使用pthread_mutex_init或者宏初始化,不支持其他任何初始化方法
解鎖時也只會把1放進mutex中

硬件層面的互斥鎖的實現


在某個線程要執行臨界區代碼之前,先關閉操作系統對與時鐘中斷和外部中斷的響應
這個線程執行完臨界區代碼之后,再打開

即:這個線程執行臨界區代碼時,操作系統不會進行切換
這樣就可以防止并發切換導致的線程安全問題

不過:一般用的是軟件實現鎖

鎖是不允許拷貝構造或者賦值拷貝的

因為如果要使用鎖對一個臨界資源進行保護的話
那么就應該保證所有想訪問這個臨界資源線程看到的都是同一把鎖
不然就不能起到保護的作用了

所以為了防止用戶無意識地進行鎖的拷貝構造/賦值導致出現線程安全問題
就直接禁止鎖進行拷貝構造和賦值拷貝了

鎖的饑餓問題

如果一個共享資源只加了鎖,就有可能出現鎖的饑餓問題

例:
一個死循環–計數器的代碼
while(1)
{
//加鎖
p–
//解鎖
}

一個線程a搶到鎖之后,其他線程想要鎖的進程就只能阻塞等待線程a解鎖
線程a使用完臨界區之后,解鎖之后,又進入下一次循環,又去搶鎖了
因為其他想搶鎖的線程還阻塞著,喚醒需要時間
但是線程a本來就醒著,所以線程a就比別的線程快,馬上又把鎖搶到了
其他想要鎖的線程只能再次進入阻塞狀態
就有可能一直是線程a拿著鎖,訪問臨界區

怎么解決這個問題?
就要用到同步

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

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

相關文章

扣子(Coze)宣布開源兩大核心項目——Coze Studio(扣子開發平臺)和Coze Loop(扣子羅盤),附安裝步驟

2025年7月26日,字節跳動旗下AI開發平臺“扣子(Coze)”宣布開源兩大核心項目——Coze Studio(扣子開發平臺)和Coze Loop(扣子羅盤),采用Apache 2.0協議,支持免費商用及本地化部署。 開源內容 Coze Studio:提供可視化AI智能體開發工具,支持零代碼/低代碼拖拽式工作流編…

InfluxDB Flux 查詢協議實戰應用(二)

四、實戰案例解析4.1 服務器性能監控數據查詢在服務器性能監控場景中&#xff0c;InfluxDB 和 Flux 查詢協議能夠發揮重要作用&#xff0c;幫助運維人員實時了解服務器的運行狀態&#xff0c;及時發現性能問題。假設我們的服務器性能監控數據存儲在名為server-monitoring的存儲…

二層隧道協議(PPP、PPTP、L2TP)

PPP —— 點對點鏈路上的“鏈路層會話層”協議&#xff0c;解決撥號認證、IP 分配和多協議封裝。PPTP —— 在 IP 網絡里開一條“PPP-over-GRE”隧道&#xff0c;把 PPP 封裝進公共網絡&#xff0c;速度快但已不安全。L2TP —— 在 IP/UDP 里再開一條“PPP-over-UDP”隧道&…

openmv特征點檢測

AGAST 角點檢測器和 FAST 角點檢測器&#xff1a; 兩者都是計算機視覺中快速檢測圖像角點的算法&#xff0c;核心目的是高效找到圖像中 "有辨識度的點"&#xff0c;但細節略有不同&#xff1a; &#xff08;1&#xff09;FAST 角點檢測器 ? 特點&#xff1a;速度極快…

基于深度學習的CT圖像3D重建技術研究

基于深度學習的CT圖像3D重建技術研究 摘要 本文詳細探討了使用深度學習技術進行CT(計算機斷層掃描)圖像3D重建的全過程。我們從CT成像基本原理出發,系統介紹了數據預處理、深度學習模型構建、訓練優化以及三維可視化等關鍵技術環節。研究采用了先進的深度學習架構如3D U-Net…

JVM相關面試八股

什么是雙親委派模型&#xff1f; 如果一個類加載器在接到加載類的請求時&#xff0c;它首先不會自己嘗試去加載這個類&#xff0c;而是把這個請求任務委托給父類加載器去完成&#xff0c;依次遞歸&#xff0c;如果父類加載器可以完成類加載任務&#xff0c;就返回成功&#xff…

Javaweb————HTTP消息體拆分講解

??????一.HTTP請求消息結構 &#xff08;1&#xff09;請求行 &#x1f499; 請求方法 &#x1f499;URL地址 &#x1f499;協議名 &#xff08;2&#xff09;請求頭 報文頭包含若千個屬性格式為“屬性名:屬性值”, 服務端據此獲取客戶端的基本信息 &#xff08;3&…

GitHub的免費賬戶的存儲空間有多少?

GitHub的免費賬戶在存儲空間方面的具體限制如下: 一、普通倉庫(非LFS)存儲限制 公共倉庫 總存儲:無明確總容量限制,但建議單個倉庫不超過1GB以確保性能。若倉庫過大(如超過5GB),可能會收到GitHub的優化提示郵件。 文件大小:單個文件最大100MB,超過100MB的文件會被直…

Java學習|黑馬筆記|Day23】網絡編程、反射、動態代理

【DAY23】 文章目錄【DAY23】一.網絡編程1&#xff09;三要素1.1&#xff09;IPInetAddress類的使用1.2&#xff09;端口號1.3&#xff09;協議2.1&#xff09;UDP協議發送數據2.2&#xff09;UDP協議接收數據2.3&#xff09;UDP的三種通信方式3.1&#xff09;TCP協議的發送和接…

【Linux】從普通進程到守護進程:系統服務的誕生之路

當你在深夜關閉SSH終端&#xff0c;為何Web服務器仍在默默響應請求&#xff1f;這背后是守護進程的魔法在守護著系統服務的不滅之火。一、守護進程的六大核心特征守護進程&#xff08;Daemon&#xff09;是Linux系統的無名英雄&#xff0c;它們舍棄了普通進程的"世俗享受&…

k8s常用基礎命令總結

----------------------k8s常用基礎命令--------------------------------- 獲取 Pod 信息 # 1.獲取k8s的命名空間 kubectl get namespaces ?1)獲取 Pod 列表及簡要信息: kubectl get pods 2)以 YAML 格式獲取 Pod 詳細信息: kubectl get pod -o yaml 3)?獲取特定命名空間中…

Java高級之基于Java Attach與Byte-Buddy實現SQL語句增強

目錄 一 Agent 模塊 1 HookAgent.java 2 FormatAdvice.java 3 配置文件 二 Attacher 模塊 1 AttachMain.java 三 測試模塊 1 DruidTest.java 四 驗證步驟 五 原理解析 筆者目標寫一款數據分析中間件&#xff0c;用來增強當前主流開源項目&#xff0c;前幾天寫了一票用…

2025第五屆生物發酵營養源高峰論壇

一、會議時間會議時間:2025年8月8日二、會議地點上海新國際博覽中心–W4館現場2號會議室三、組織單位主辦單位:中國生物發酵產業協會承辦單位:浙江工業大學樂斯福集團Procelys 樂斯福發酵營養元參會福利&#xff0c;助力高效交流為提升參會體驗&#xff0c;組委會特別推出多項福…

Kubernetes 配置管理

這里寫目錄標題什么是 ConfigMap創建 ConfigMap基于目錄創建 ConfigMap創建 conf 目錄&#xff0c;里面放置兩個文件基于目錄下的所有文件創建 ConfigMap查看當前創建的 ConfigMap基于文件創建 ConfigMap創建測試文件 game-cfg基于單個文件創建 ConfigMap查看當前創建的 Config…

ESP32+MicroPython:用Python玩轉物聯網開發

什么是ESP32&#xff1f; ESP32作為當下最熱門的物聯網開發板&#xff0c;常被比作"嵌入式世界的瑞士軍刀"。但很多初學者會混淆芯片、模組和開發板的概念&#xff0c;其實它們的關系很簡單&#xff1a; 芯片(Soc)&#xff1a;核心處理器&#xff0c;如ESP32-D0WD模…

opencv學習(圖像金字塔)

1.什么是圖像金字塔圖像金字塔是一種多尺度圖像表示方法&#xff0c;通過對原始圖像進行下采樣&#xff08;縮小&#xff09;和上采樣&#xff08;放大&#xff09;&#xff0c;生成一系列不同分辨率的圖像集合&#xff0c;形似 “金字塔”&#xff08;底部是高分辨率原始圖像&…

從 C# 到 Python:項目實戰第五天的飛躍

在前面三天的學習中&#xff0c;我們已經掌握了 Python 的基礎語法、數據結構以及一些核心庫的使用。今天&#xff0c;我們將通過三個實戰項目&#xff0c;深入對比 C# 和 Python 在命令行工具開發、Web 應用開發以及數據處理方面的差異&#xff0c;感受 Python 在實際項目中的…

rabbitmq 03

一、mq的作用和使用場景 MQ的基本作用 MQ&#xff08;Message Queue&#xff0c;消息隊列&#xff09;是一種應用程序對應用程序的通信方法&#xff0c;主要作用包括&#xff1a; 異步處理&#xff1a;解耦生產者和消費者&#xff0c;允許生產者發送消息后立即返回&#xff0…

Ubuntu 24.04 顯示中文+使用中文鍵盤

ubuntu 24.04 中文顯示中文鍵盤Ubuntu中文輸入重啟iBus服務Ubuntu中文輸入 安裝的Ubuntu24.04&#xff0c;一般默認是英文的&#xff0c;要使用中文的話&#xff0c;可以通過命令行設置&#xff0c;也可以使用‘設置’&#xff0c;在圖形化界面中操作。 下面是在‘設置’的圖形…

Docker實戰:Tomcat容器從部署到自定義網頁的完整操作

Docker實戰&#xff1a;Tomcat容器從部署到自定義網頁的完整操作 繼Nginx容器部署后&#xff0c;我們再來實操Tomcat容器的使用——從拉取鏡像、啟動容器&#xff0c;到端口映射、網頁掛載&#xff0c;全程通過實際命令演示&#xff0c;帶你掌握Tomcat在Docker中的核心用法。 一…