從源碼和設計模式深挖AQS(AbstractQueuedSynchronizer)

AQS 概念

AbstractQueuedSynchronizer(AQS) 是 Java 并發包 (java.util.concurrent.locks) 的核心基礎框架,它的實現關鍵是先進先出 (FIFO) 等待隊列和一個用volatile修飾的鎖狀態status。具體實現有 : ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLock 等常用的同步工具類。

AQS 的核心思想

AQS 的核心思想是 : 如果被請求的共享資源空閑,則將當前請求資源的線程設置為有效的工作線程,并且將共享資源設置為鎖定狀態。如果被請求的共享資源被占用,那么就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制 AQS 是用 CLH 隊列鎖 實現的,即將暫時獲取不到鎖的線程加入到隊列中。

CLH 隊列:Craig, Landin, and Hagersten (CLH) locks,是一種基于鏈表的可擴展、高性能、公平的自旋鎖。AQS 中的隊列是 CLH 變體的虛擬雙向隊列(FIFO)。

AQS 的核心架構

在這里插入圖片描述

  • AQS 使用一個 volatile int state 成員變量來表示同步狀態
  • 通過內置的 FIFO 隊列 來完成資源獲取線程的排隊工作。
  • Node中的thread變量用來存放進入AQS隊列里面的線程,Node節點內部:
    • prev記錄當前節點的前驅節點
    • next 記錄當前節點的后繼節點
  • SHARED用來標記該線程是獲取共享資源時被阻塞掛起后放入AQS隊列的
  • EXCLUSIVE用來標記線程是獲取獨占資源時被掛起后放入AQS隊列的
  • waitStatus 記錄當前線程等待狀態,可以為
    • CANCELLED (線程被取消了)
    • SIGNAL(線程需要被喚醒)
    • CONDITION(線程在CONDITION條件隊列里面等待)
    • PROPAGATE(釋放共享資源時需要要通知其他節點);

圖中源碼解釋 :

static final int WAITING   = 1;          // 等待狀態,必須為1
static final int CANCELLED = 0x80000000; // 取消狀態,必須為負數(最高位為1)
static final int COND      = 2;          // 在條件等待中/** CLH 節點 */
abstract static class Node {volatile Node prev;       // 前驅節點,最初通過 casTail 附加volatile Node next;       // 當可發出信號時 visibly nonnullThread waiter;            // 當入隊時 visibly nonnullvolatile int status;      // 由所有者寫入,其他線程通過原子位操作讀取// 用于 cleanQueue 的比較并交換前驅節點final boolean casPrev(Node c, Node v) {return U.weakCompareAndSetReference(this, PREV, c, v);}// 用于 cleanQueue 的比較并交換后繼節點final boolean casNext(Node c, Node v) {return U.weakCompareAndSetReference(this, NEXT, c, v);}// 用于信號傳遞的獲取并清除狀態位final int getAndUnsetStatus(int v) {return U.getAndBitwiseAndInt(this, STATUS, ~v);}// 用于離隊賦值的寬松設置前驅節點final void setPrevRelaxed(Node p) {U.putReference(this, PREV, p);}// 用于離隊賦值的寬松設置狀態final void setStatusRelaxed(int s) {U.putInt(this, STATUS, s);}// 用于減少不必要信號的狀態清除final void clearStatus() {U.putIntOpaque(this, STATUS, 0);}// 內存偏移量常量private static final long STATUS = U.objectFieldOffset(Node.class, "status");private static final long NEXT = U.objectFieldOffset(Node.class, "next");private static final long PREV = U.objectFieldOffset(Node.class, "prev");
}// 按類型標記的具體節點類
static final class ExclusiveNode extends Node { }  // 獨占模式節點
static final class SharedNode extends Node { }     // 共享模式節點// 條件節點,實現 ManagedBlocker 接口用于 ForkJoinPool
static final class ConditionNode extends Node implements ForkJoinPool.ManagedBlocker {ConditionNode nextWaiter; // 鏈接到下一個等待節點/*** 允許條件在 ForkJoinPools 中使用,而不會冒險耗盡固定池。* 這僅適用于非定時條件等待,不適用于定時版本。*/public final boolean isReleasable() {return status <= 1 || Thread.currentThread().isInterrupted();}public final boolean block() {while (!isReleasable()) LockSupport.park();return true;}
}// 等待隊列頭節點,延遲初始化
private transient volatile Node head;// 等待隊列尾節點。初始化后,僅通過 casTail 修改
private transient volatile Node tail;// 同步狀態
private volatile int state;/*** 返回同步狀態的當前值。* @return 當前狀態值*/
protected final int getState() {return state;
}

AQS 底層數據結構

state

state 是 AQS 的核心字段,表示共享資源的狀態。不同的同步器對 state 的解釋不同:

  • ReentrantLockstate 表示當前線程獲取鎖的可重入次數(如果發現可以可重入,則state+1; 執行完成則-1; 為0時可以釋放)
  • Semaphorestate 表示當前可用信號的個數
  • CountDownLatchstate 表示計數器當前的值

AQS 提供了一系列訪問 state 的方法:

// 獲取當前同步狀態
protected final int getState() {return state;
}// 設置同步狀態
protected final void setState(int newState) {state = newState;
}// 使用CAS原子性地設置同步狀態
protected final boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

同步隊列:Node 節點

AQS 內部維護了一個 FIFO 雙向隊列,隊列中的每個元素都是一個 Node 對象:

static final class Node {// 節點模式:共享模式static final Node SHARED = new Node();// 節點模式:獨占模式static final Node EXCLUSIVE = null;// 等待狀態:線程已取消static final int CANCELLED =  1;// 等待狀態:后繼節點的線程需要被喚醒static final int SIGNAL    = -1;// 等待狀態:節點在條件隊列中等待static final int CONDITION = -2;// 等待狀態:下一次acquireShared應無條件傳播static final int PROPAGATE = -3;// 當前節點的等待狀態volatile int waitStatus;// 前驅節點volatile Node prev;// 后繼節點volatile Node next;// 節點關聯的線程volatile Thread thread;// 指向下一個等待條件或共享模式的節點Node nextWaiter;// 判斷節點是否在共享模式下等待final boolean isShared() {return nextWaiter == SHARED;}// 獲取前驅節點final Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p;}// 構造方法Node() {    // Used to establish initial head or SHARED marker}Node(Thread thread, Node mode) {     // Used by addWaiterthis.nextWaiter = mode;this.thread = thread;}Node(Thread thread, int waitStatus) { // Used by Conditionthis.waitStatus = waitStatus;this.thread = thread;}
}

AQS 的整體結構

AQS 的整體結構如下圖所示:

在這里插入圖片描述

線程獲取鎖失敗時的 AQS 隊列變化

初始狀態

當沒有線程競爭鎖時,AQS 的同步隊列處于初始狀態,headtail 都為 null

// 初始狀態
head = null;
tail = null;

第一個線程獲取鎖失敗

當第一個線程嘗試獲取鎖失敗時,AQS 會初始化同步隊列:

  1. 創建空節點(dummy node)作為頭節點
  2. 將當前線程包裝成 Node 節點添加到隊列尾部
// addWaiter 方法:將當前線程包裝成Node并加入隊列
private Node addWaiter(Node mode) {// 將當前線程包裝成Node節點Node node = new Node(Thread.currentThread(), mode);// 嘗試快速入隊Node pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 快速入隊失敗,使用enq方法自旋入隊enq(node);return node;
}// enq方法:通過自旋CAS確保節點成功入隊
private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // 隊列未初始化// 創建空節點(dummy node)作為頭節點if (compareAndSetHead(new Node()))tail = head; // 頭尾都指向空節點} else {// 將新節點添加到隊列尾部node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}

初始化后的隊列結構:

head → [dummy node] ← tail↑新加入的節點

后續線程獲取鎖失敗

當后續線程嘗試獲取鎖失敗時,它們會被添加到隊列的尾部:

// acquireQueued方法:線程在隊列中自旋獲取資源或阻塞
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {// 獲取前驅節點final Node p = node.predecessor();// 如果前驅是頭節點,嘗試獲取資源if (p == head && tryAcquire(arg)) {// 獲取成功,設置當前節點為頭節點setHead(node);p.next = null; // help GC (方便垃圾回收)failed = false;return interrupted;}// 判斷是否應該阻塞,并在需要時安全地阻塞if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

多個線程競爭后的隊列結構 :
在這里插入圖片描述

線程被喚醒時的 AQS 隊列變化

鎖釋放與線程喚醒

當持有鎖的線程釋放資源時,會喚醒隊列中的下一個線程:

// release方法:釋放獨占模式下的資源
public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h); // 喚醒后繼節點return true;}return false;
}// unparkSuccessor方法:喚醒指定節點的后繼節點
private void unparkSuccessor(Node node) {int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0); // 清除狀態// 查找下一個需要喚醒的節點Node s = node.next;if (s == null || s.waitStatus > 0) {s = null;// 從尾部向前查找,找到隊列中最前面且未取消的節點for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null)LockSupport.unpark(s.thread); // 喚醒線程
}

4.2 線程被喚醒后的隊列變化

當隊列中的線程被喚醒后,它會嘗試獲取鎖,如果獲取成功,會將自己設置為新的頭節點:

// setHead方法:將節點設置為頭節點
private void setHead(Node node) {head = node;node.thread = null; // 清空線程引用node.prev = null;
}

原來的頭節點(dummy node)會被垃圾回收,新頭節點的 thread 字段被設置為 null,表示它不再關聯任何線程。

公平鎖與非公平鎖

AQS 支持兩種鎖獲取模式:公平鎖非公平鎖

非公平鎖(如 ReentrantLock 的默認實現):

final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 不管隊列中是否有等待線程,直接嘗試獲取鎖if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// 重入邏輯...
}

公平鎖

protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 只有隊列中沒有等待線程或當前線程是隊列中的第一個時才獲取鎖if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// 重入邏輯...
}

hasQueuedPredecessors() 方法檢查隊列中是否有比當前線程等待時間更長的線程,這是實現公平性的關鍵。

AQS 的設計模式

模板方法模式

AQS 使用了模板方法模式,定義了同步器的主要邏輯骨架,而將一些特定操作留給子類實現。以下是需要子類實現的方法:

方法名描述
boolean tryAcquire(int arg)嘗試以獨占方式獲取資源
boolean tryRelease(int arg)嘗試釋放獨占資源
int tryAcquireShared(int arg)嘗試以共享方式獲取資源
boolean tryReleaseShared(int arg)嘗試釋放共享資源
boolean isHeldExclusively()判斷當前線程是否獨占資源

以 ReentrantLock 為例的實現

ReentrantLock 中的同步器 Sync 繼承自 AQS,并實現了相關方法:

abstract static class Sync extends AbstractQueuedSynchronizer {// 嘗試獲取鎖abstract void lock();// 非公平嘗試獲取final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}// 嘗試釋放鎖protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}// 判斷當前線程是否獨占資源protected final boolean isHeldExclusively() {return getExclusiveOwnerThread() == Thread.currentThread();}
}

優勢

  1. 代碼復用:AQS 提供了通用的同步框架,多個同步器可以復用相同的隊列管理邏輯
  2. 職責分離:AQS 負責線程管理,子類專注于狀態管理

裝飾器模式

裝飾器模式動態地給一個對象添加一些額外的職責,就增加功能來說,裝飾器模式相比生成子類更為靈活。

在 AQS 中的實現

AQS 中的 Node 類可以看作是對 Thread 的裝飾,它添加了同步所需的額外信息:

static final class Node {// 節點模式:共享或獨占volatile Node nextWaiter;// 等待狀態volatile int waitStatus;// 前驅和后繼指針volatile Node prev;volatile Node next;// 被裝飾的線程對象volatile Thread thread;// 構造方法 - 裝飾線程Node(Thread thread, Node mode) {     // Used by addWaiterthis.nextWaiter = mode;this.thread = thread;}
}

優勢

  1. 功能擴展:在不修改 Thread 類的情況下為其添加同步功能
  2. 靈活性:可以根據需要為線程添加不同的同步屬性

狀態模式

狀態模式允許一個對象在其內部狀態改變時改變它的行為。

在 AQS 中的實現

AQS 中節點的 waitStatus 字段代表了不同的狀態,每種狀態對應不同的行為:

static final class Node {// 狀態常量static final int CANCELLED =  1;  // 取消狀態static final int SIGNAL    = -1;  // 需要喚醒后繼節點static final int CONDITION = -2;  // 在條件隊列中等待static final int PROPAGATE = -3;  // 傳播喚醒操作// 狀態字段volatile int waitStatus;
}

根據狀態的不同,AQS 采取不同的處理策略:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL) // 根據狀態決定行為return true;if (ws > 0) { // 處理取消狀態do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}

AQS 的使用

ReentrantLock 的實現

ReentrantLock 通過內部類 Sync 繼承 AQS,實現了可重入的獨占鎖:

public class ReentrantLock implements Lock, java.io.Serializable {private final Sync sync;abstract static class Sync extends AbstractQueuedSynchronizer {}// 非公平鎖實現static final class NonfairSync extends Sync {final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}// 公平鎖實現static final class FairSync extends Sync {final void lock() {acquire(1);}protected final boolean tryAcquire(int acquires) {// 公平獲取邏輯}}
}

CountDownLatch 的實現

CountDownLatch 通過內部類 Sync 繼承 AQS,實現了共享模式的同步器:

public class CountDownLatch {private static final class Sync extends AbstractQueuedSynchronizer {Sync(int count) {setState(count);}int getCount() {return getState();}protected int tryAcquireShared(int acquires) {return (getState() == 0) ? 1 : -1;}protected boolean tryReleaseShared(int releases) {for (;;) {int c = getState();if (c == 0)return false;int nextc = c - 1;if (compareAndSetState(c, nextc))return nextc == 0;}}}
}

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

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

相關文章

Dart → `.exe`:Flutter 桌面與純命令行雙軌編譯完全指南

Dart → .exe&#xff1a;Flutter 桌面與純命令行雙軌編譯完全指南 關鍵詞&#xff1a;Dart、Flutter、Windows、可執行文件、桌面端、CLI、交叉編譯 1. 前言 很多開發者以為 Dart 只能跑在 AOT 移動端或 Web 端&#xff0c;其實 官方工具鏈早已支持一鍵輸出 Windows 原生 .ex…

互聯網接入網中PPPoE和PPP協議

<摘要> PPPoE和PPP是寬帶接入網絡中至關重要的協議組合&#xff0c;其中PPP提供通用的點對點鏈路層解決方案&#xff0c;而PPPoE則是在以太網架構上擴展PPP應用的技術橋梁。本文從技術演進視角系統解析了兩者的內在關聯與本質區別&#xff1a;PPP作為成熟鏈路層協議&…

詳細解析SparkStreaming和Kafka集成的兩種方式的區別和優劣

spark streaming是基于微批處理的流式計算引擎&#xff0c;通常是利用spark core或者spark core與spark sql一起來處理數據。在企業實時處理架構中&#xff0c;通常將spark streaming和kafka集成作為整個大數據處理架構的核心環節之一。 針對不同的spark、kafka版本&#xff0…

Kite Compositor for Mac v2.1.2 安裝教程|DMG文件安裝步驟(Mac用戶必看)

Kite Compositor? 是一款專為 ?macOS? 設計的 ?輕量級界面設計 & 動畫制作工具&#xff0c;它可以讓你像拼圖一樣直觀地 ?創建、編輯和預覽用戶界面&#xff08;UI&#xff09;以及動畫效果。 一、下載文件 首先&#xff0c;你得先把這個 ?Kite Compositor for Mac …

【逆向】Android程序靜態+動態分析——去殼

對提供的 CrackmeTest.apk 進行逆向分析&#xff0c;程序含有反調試機制&#xff08;加殼&#xff09;&#xff0c;通過靜態補丁反反調試&#xff08;去殼&#xff09;&#xff0c;再動態調試獲取其中密碼。 目錄 環境 基礎 實驗內容 靜態分析 動態分析 反反調試 再動態…

Rust 開發環境安裝與 crates.io 國內源配置(Windows / macOS / Linux 全流程)

Rust 這幾年在系統編程、WebAssembly、區塊鏈、后端服務領域越來越火&#xff0c;很多開發者都在嘗試用它做一些新項目。 但是國內安裝 Rust 開發環境時&#xff0c;經常遇到 安裝慢、依賴拉不下來、crates.io 超時 等問題。本文結合個人踩坑經驗&#xff0c;整理了一份 跨平臺…

Nginx SSL/TLS 配置

Nginx SSL/TLS 配置指南&#xff1a;從入門到安全強化前言一、環境準備&#xff1a;Nginx安裝配置1.1. **EPEL倉庫配置**&#xff1a;1.2. **Nginx安裝**&#xff1a;1.3. **服務啟停管理**&#xff1a;1.4. **服務狀態驗證**&#xff1a;二、SSL/TLS證書獲取方案方案A&#xf…

Java ReentrantLock和synchronized的相同點與區別

1. 核心概念與定位synchronized&#xff1a;Java 內置的關鍵字&#xff0c;屬于 JVM 層面的隱式鎖。通過在方法或代碼塊上聲明&#xff0c;自動實現鎖的獲取與釋放&#xff0c;無需手動操作。設計目標是提供簡單易用的基礎同步能力&#xff0c;適合大多數常規同步場景。Reentra…

【npm】npm 包更新工具 npm-check-updates (ncu)

npm 包太多了&#xff0c;一個項目有那么多依賴包&#xff0c;它們的升級管理需要一個工具&#xff1a;npm-check-updates&#xff1a; 安裝&#xff1a; npm install -g npm-check-updates安裝之后&#xff0c;就可以使用它的命令&#xff1a;ncu 查看哪些包可以升級&#xff…

go資深之路筆記(一) Context

一、 Context 的正確使用與底層原理 1.結構體 type Context interface {// Deadline 返回此 Context 被取消的時間點。// 如果未設置截止時間&#xff0c;ok 為 false。Deadline() (deadline time.Time, ok bool)// Done 返回一個 channel。當 Context 被取消或超時后&#xff…

VS2022 + Qt5.9 中文亂碼/項目設置utf-8編碼

&#x1f6e0;? 解決QT5.9 VS2022中文亂碼的全面方案 &#x1f4c1; 1. 檢查文件編碼與編譯器設置 確保源文件是 帶BOM的UTF-8 編碼對MSVC編譯器很重要。VS2022默認可能使用本地編碼&#xff08;如GB2312&#xff09;解析源文件&#xff0c;即使文件以UTF-8保存。 查看和設置…

數據庫--MySQL數據管理

數據庫–MySQL數據管理 文章目錄數據庫--MySQL數據管理1.外鍵管理2.數據庫數據管理3.DML語言3.1添加數據3.2修改數據3.3刪除數據4.練習1.外鍵管理 外鍵概念 如果公共關鍵字在一個關系中是主關鍵字&#xff0c;那么這個公共關鍵字被稱為另一個關系的外鍵。由此可見&#xff0c;…

【C++練習】13.C++輸出九九乘法表的方法詳解

目錄 C++輸出九九乘法表的方法詳解 方法1:雙重for循環(最基礎) 思考: 代碼分析: 特點: 方法2:使用while循環 思考: 代碼分析: 特點: 方法3:使用遞歸實現 思考: 代碼分析: 特點: 方法4:格式化輸出(對齊美觀) 思考: 代碼分析: 特點: 方法5:使用函數封裝 思考…

MVC及其衍生

MVC 把軟件分成模型&#xff08;Model&#xff09;、視圖&#xff08;View&#xff09;、控制器&#xff08;Controller&#xff09;三個基本部分。 事實上對應著 Controller——輸入 用戶交互&#xff0c;將輸入處理成Controller能處理的形式 Model——處理 描述狀態、邏輯規律…

微碩WINSOK MOS管WSF3089,賦能汽車轉向系統安全升級

隨著汽車電子化程度不斷提高&#xff0c;轉向系統對高效功率器件的需求日益增長。微碩WINSOK推出的N溝道Trench MOS管WSF3089&#xff0c;以30 V/72 A大電流、4.5 mΩ超低導通電阻和TO-252-2L緊湊封裝&#xff0c;為EPS&#xff08;電動助力轉向&#xff09;電機驅動、電源管理…

淘寶拍立淘接口的接入與應用||item_search_img-按圖搜索淘寶商品(拍立淘)

淘寶拍立淘接口的接入與應用如下&#xff1a;接入流程注冊與認證&#xff1a;開發者賬號注冊&#xff1a;訪問淘寶開放平臺&#xff0c;進行開發者賬號注冊。創建應用&#xff1a;在控制臺創建新應用&#xff0c;獲取 App Key 和 App Secret&#xff0c;這是接口調用的憑證。申…

Python學習-day8 元組tuple

元組&#xff08;Tuple&#xff09;是Python中一種不可變的序列類型&#xff0c;用于存儲多個有序元素。與列表&#xff08;List&#xff09;類似&#xff0c;但元組一旦創建后不能修改&#xff08;不可添加、刪除或修改元素&#xff09;&#xff0c;這使得它在安全性、性能優化…

大數據畢業設計選題推薦-基于大數據的國家醫用消耗選品采集數據可視化分析系統-Hadoop-Spark-數據可視化-BigData

?作者主頁&#xff1a;IT畢設夢工廠? 個人簡介&#xff1a;曾從事計算機專業培訓教學&#xff0c;擅長Java、Python、PHP、.NET、Node.js、GO、微信小程序、安卓Android等項目實戰。接項目定制開發、代碼講解、答辯教學、文檔編寫、降重等。 ?文末獲取源碼? 精彩專欄推薦?…

二次學習C語言補充2

文章目錄表棧、隊列、二叉樹一、二叉樹二、表棧三、隊列鏈表一、單向鏈表二、循環鏈表、雙向鏈表和雙向循環鏈表預處理一、預處理二、宏定義文件文件操作補充本篇文章是對二次學習C語言12——文件操作 二次學習C語言14——預處理及模塊化 二次學習C語言15——鏈表 二次學習C語言…

2.9Vue創建項目(組件)的補充

1.再創建和引入vue的選擇2.VsCode插件 安裝Vue自己搜索最新的3.style自己的作用域在一個組件中引入另一個文件的子組件&#xff0c;給當前組件設置樣式&#xff0c;那么子組件的樣式也會改變的。為了解決這個問題 我們在自己的style中設置一個屬性4.另一種創建vue 的方式(主流…