線程知識總結(一)

1、概述

1.1 進程與線程

進程是程序運行時,操作系統進行資源分配的最小單位,包括 CPU、內存空間、磁盤 IO 等。從另一個角度講,進程是程序在設備(計算機、手機等)上的一次執行活動,或者說是正在運行中的程序。

一個程序進入內存運行時,它就變成一個進程。進程是處于運行中的程序,它擁有自己獨立的資源和地址空間,在沒有經過進程本身允許的情況下,進程不可以直接訪問其他進程的地址空間。同時多個進程可以在單個處理器上并發執行,多個進程之間互不影響。

線程是進程的一個實體,是 CPU 調度的最小單位。自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器、一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。

使用多線程解決了多任務同時運行的問題,并且,共享變量使得線程間的通信要比進程間通信更有效、更容易。此外,在一些操作系統中,與進程相比,線程更“輕量級”,創建、撤銷一個線程比啟動新進程的開銷要小得多。

當然,線程太多,來回切換也會導致執行效率降低。

其實應用程序的執行都是 CPU 在做著快速的切換完成的,這個切換是隨機的。在某一個時刻,CPU 只在執行一個線程,但是由于它的執行速度非常快,在毫秒級別,因此人無法感知到它在一個時間片中其實只在執行一個任務,在時間片結束后又去切換執行另一個任務。

1.2 并行與并發

并發是指在同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得宏觀上具有多個進程同時執行的效果;而并行是指在同一時刻,有多條指令在多個處理器上同時執行。

1.3 JVM 中的多線程解析

JVM 啟動時就啟動了多個線程,至少有兩個線程可以分析的出來:

  1. 執行 main 方法的線程,即主線程。該線程的任務代碼都定義在 main 方法中。主線程執行完任務,其所在進程也就關閉了。
  2. 負責垃圾回收的線程。系統會自己決定何時回收垃圾,你也可以通過 System.gc() 通知垃圾回收器來回收。但是這個也不是立即回收垃圾,也是在調用之后的一定時間內。另外 Object 中還有一個 finalize() 方法進行垃圾回收。

實際上,JVM 啟動的完整線程有以下這些:

  1. main:main線程,用戶程序入口
  2. Reference Handler:清除Reference的線程
  3. Finalizer:調用對象finalize方法的線程
  4. Signal Dispatcher:分發處理發送給JVM信號的線程
  5. Attach Listener:內存 dump,線程 dump,類信息統計,獲取系統屬性等
  6. Monitor Ctrl-Break:監控 Ctrl-Break 中斷信號的

此外,多線程運行時的示意圖如下:

請添加圖片描述

說明:

  1. 當前有3個線程:main 線程、Thread-1、Thread-2,它們每個都維護了自己的方法棧(run 方法在棧底)。在哪個線程調用了方法,這個方法就會進入哪個線程。
  2. 如果 main 線程先于另外兩個線程執行完,JVM 不會結束,而是等所有線程都運行完。
  3. 在3個線程都運行的前提下,如果在某個線程中發生了異常導致該線程停止,不會影響其它線程,其它線程該怎么執行就怎么執行

2、使用

2.1 創建線程

創建線程的目的是為了開啟一條執行路徑,去運行指定的代碼(即該線程的任務)和其他代碼實現同時運行。

創建線程的方法有兩種:繼承 Thread 類和實現 Runnable 接口,實現 Callable 接口嚴格講是屬于第二種方式,不能單獨作為一種方法。

我們先來了解下 Callable 的基本用法再解釋上述觀點的原因。

Callable 的用法

Callable 是一個函數式接口,有泛型限制,該泛型參數類型與作為線程執行體的 call() 返回值類型相同。call() 比 Runnable 的 run() 功能要更強大,因為它可以有返回值,并且可以聲明拋出異常

雖然 call() 也是線程執行體,但是由于 Callable 接口不是 Runnable 的子接口,所以 Callable 并不能直接作為 Thread 的 target,而是要借助 Future 接口。

Future 接口代表 call() 的返回值,而且 Future 的實現類 FutureTask 也實現了 Runnable 接口,可以作為 Thread 的 target。Future 中定義了如下的公共方法控制與它關聯的 Callable 任務:

在這里插入圖片描述

創建并啟動有返回值的線程的步驟:

示例如下:

// Callable 后的參數類型為 String,意味著 call() 的返回值類型為 String
public class CallableTest implements Callable<String> {public static void main(String[] args) {new CallableTest().test();}/*** Callable 的 call() 類似于 Runnable 的 run(),只不過前者有返回值而前者沒有。** 此外,Future 接口可以控制 Runnable/Callable 取消執行任務、查詢任務是否完成、* 獲取任務執行結果(通過阻塞方法 get 獲取結果)。** 由于 Future 接口不能直接實例化,所以一般都是使用 FutureTask,它實現了 RunnableFuture* 接口,RunnableFuture 又繼承了 Runnable 和 Future,所以它既可以作為 Runnable 被線程執行,* 又可以作為Future得到Callable的返回值。*/private void test() {// 將 Callable 包裝進 FutureTask 后交給 ThreadFutureTask<String> futureTask = new FutureTask<>(this);new Thread(futureTask).start();try {System.out.println(futureTask.get());} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}// 任務是讀取文件內容,并發它轉換成字符串作為返回值@Overridepublic String call() throws Exception {StringBuffer stringBuffer = new StringBuffer();try (FileInputStream fileInputStream = new FileInputStream("filepath");FileChannel inChannel = fileInputStream.getChannel()) {ByteBuffer byteBuffer = ByteBuffer.allocate(256);while (inChannel.read(byteBuffer) != -1) {byteBuffer.flip();Charset charset = Charset.forName("GBK");CharBuffer charBuffer = charset.decode(byteBuffer);stringBuffer.append(charBuffer);byteBuffer.clear();}}return stringBuffer.toString();}
}

原因解釋

我們結合 Thread 的源碼解釋下原因。首先,Thread 的構造方法中并沒有 Callable 作為參數的:

只有空參和接收 Runnable 的構造方法:

public class Thread implements Runnable {/* What will be run. */private Runnable target;public Thread() {// nextThreadNum() 初始為 0,可以看到線程編號在創建線程時就已經確定init(null, null, "Thread-" + nextThreadNum(), 0);}public Thread(Runnable target) {// init() 會將參數 target 賦值給成員變量的 targetinit(null, target, "Thread-" + nextThreadNum(), 0);}public void run() {if (target != null) {target.run();}}
}

關注運行任務的 run():

  • 如果使用繼承 Thread 的方式創建線程,那么 run() 被重寫,執行任務時就按照 Thread 子類的 run() 去執行。
  • 如果使用實現 Runnable 的方式創建線程,run() 在判斷 target 不為空之后會運行 target 的 run()。可以認為 Runnable 就是對線程的任務進行了對象的封裝。

因為 FutureTask -> RunnableFuture -> Runnable & Future,而 Callable 需要交給 FutureTask 才能執行,所以實現 Callable 接口這種創建線程的方式在實現 Runnable 接口的范疇內,不能作為一種單獨的創建方式。

兩種創建線程的方式對比

兩種方式各有優缺點,通常還是使用實現 Runnable 接口的方式:

  • 繼承 Thread 類方式編寫簡單如果要訪問當前線程無須使用 Thread.currentThread(),直接使用 this 即可。劣勢是不能再繼承其它父類。
  • 實現 Runnable 接口方式則可以繼承其它類,并且多個線程可以共享同一個 target 對象,非常適合多個相同線程來處理同一份資源,從而將 CPU、代碼和數據分開,形成清晰的模型。缺點就是編程稍復雜,必須使用 Thread.currentThread() 訪問當前線程。

2.2 停止線程

線程會因為如下兩個原因之一被停止:

  1. run() 正常退出而自然死亡
  2. 因為一個沒有捕獲的的遺產終止了 run() 而意外死亡

推薦使用 Thread 的 interrupt() 配合 isInterrupted()/interrupted() 來停止線程。

interrupt()

interrupt() 會請求線程停止,注意是請求,而不是立即停止線程。該方法會將線程中的中斷狀態標記位置位,等待線程通過 isInterrupted()/interrupted() 檢查該標記位來進行響應。isInterrupted() 和 interrupted() 都會在標記位被置位的情況下返回 true,不同點在于前者是對象方法,而后者是一個靜態方法,并且在調用后會將標記位改寫為 false。

在使用上述方法時需要注意,如果在中斷標記位為 true 的情況下執行阻塞方法(如 Thread.sleep()、Thread.join()、Object.wait()),這些阻塞方法會拋出 InterruptedException,并且在拋出異常后立即將線程的中斷標示位清除重置為 false:

    public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {while (!Thread.currentThread().isInterrupted()) {System.out.println("Thread is running...");try {Thread.sleep(500);} catch (InterruptedException e) {System.out.println(Thread.currentThread().isInterrupted());e.printStackTrace();// Thread.currentThread().interrupt();}}}});thread.start();Thread.sleep(1000);thread.interrupt();System.out.println("interrupt in main!");}

如果不打開 catch 中被注釋掉的 Thread.currentThread().interrupt(),線程是無法被中斷的,因為 sleep() 如果發現中斷標記位為 true 會拋出異常并將其清除為 false:

Thread is running...
Thread is running...
Thread is running...
Thread is running...
interrupt in main!
false
Thread is running...
java.lang.InterruptedException: sleep interruptedat java.lang.Thread.sleep(Native Method)at com.demo.thread.multi.InterruptDemo$1.run(InterruptDemo.java:13)at java.lang.Thread.run(Thread.java:748)
Thread is running...
Thread is running...
// 線程繼續運行輸出 Thread is running...

從這里也能看到子線程其實是在主線程結束后才消亡的,一定注意要讓線程能執行完,否則這個線程會阻止已經運行完的主線程所在的進程結束

在拋出 InterruptedException 的 catch 代碼塊中調用 interrupt() 中斷線程是基本操作。

自定義標記位

interrupt() 基本上是我們推薦的,唯一的中斷線程的方法。或許有人會問,自己在線程中定義一個中斷標記位不是也能實現線程中斷嘛,像這樣:

class StopThread implements Runnable {private boolean flag = true;public void run() {while (flag) {System.out.println(Thread.currentThread().getName() + "......++++");}}// 外部調用注入方法控制標記位進而停止線程public void setFlag(boolean flag) {this.flag = flag;}
}

一般情況下確實可以,但如果線程中執行的代碼有類似 wait() 這樣的阻塞方法,那么該線程就會進入等待狀態,在線程池中等待喚醒,在沒有其它線程喚醒它的情況下,它就無法通過標記位的方式結束線程:

	public synchronized void run() {while (flag) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "......++++");}}

在這種情況下,使用 interrupt() 會更好,因為:

  1. 一般的阻塞方法,如 sleep()、wait() 等本身就支持中斷的檢查
  2. 檢查中斷標記位和自定義的標志位沒什么區別,用中斷標記位還可以避免聲明自定義的標志位,減少資源的消耗
  3. interrupt() 方法會將線程從阻塞狀態強制恢復到運行狀態中來,讓線程具備 cpu 的執行資格,但是強制動作會發生 InterruptedException,需要處理

中斷異常是如何被拋出的

我們以 Thread.sleep() 為例,進入源碼看下中斷異常是如何被拋出的:

    /*** sleep 期間,線程不會失去已經獲取到的同步鎖。** @throws  InterruptedException*          如果任何線程中斷了當前線程,那么會拋出這個異常并且*          清除掉當前線程的中斷狀態。*/public static native void sleep(long millis) throws InterruptedException;

想要查看 sleep() 的 native 源碼,要先在 src/share/native/java/lang/Thread.c 文件中,找到 sleep() 在 JVM 中對應的方法 JVM_Sleep:

#include "jni.h"
#include "jvm.h"#include "java_lang_Thread.h"#define THD "Ljava/lang/Thread;"
#define OBJ "Ljava/lang/Object;"
#define STE "Ljava/lang/StackTraceElement;"
#define STR "Ljava/lang/String;"#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))static JNINativeMethod methods[] = {{"start0",           "()V",        (void *)&JVM_StartThread},{"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},{"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},{"suspend0",         "()V",        (void *)&JVM_SuspendThread},{"resume0",          "()V",        (void *)&JVM_ResumeThread},{"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},{"yield",            "()V",        (void *)&JVM_Yield},{"sleep",            "(J)V",       (void *)&JVM_Sleep},{"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},{"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},{"interrupt0",       "()V",        (void *)&JVM_Interrupt},{"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},{"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},{"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},{"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},{"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};#undef THD
#undef OBJ
#undef STE
#undef STR// 注冊 methods[] 中的 native 方法
JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

然后去 jvm.cpp 文件中找到這個方法:

可以看到如果線程的中斷標記位已經為 true,調用 sleep 方法就會拋出 InterruptedException。在拋出 InterruptedException 之前,中斷標記位會被清除為 false。

3、線程的狀態

3.1 狀態定義與狀態轉移

線程狀態也被稱為生命周期,指的是 JVM 中的線程狀態,而不是操作系統的。Thread 中定義的枚舉類 State 規定了線程的 6 種狀態:

  1. 初始(NEW):新創建了一個線程對象,但還沒有調用 start()。這時僅僅由虛擬機分配內存并初始化變量值。如果此時錯誤地調用了 run(),該線程就不再處于初始狀態,不能再調用 start()。
  2. 可運行(RUNNABLE):Java 線程中將就緒(ready)和運行中(running)兩種狀態統稱為“可運行”。
    線程對象創建后,其他線程(如主線程)調用了該對象的 start() 后會進入就緒狀態,虛擬機會創建方法調用棧和程序計數器。該狀態的線程位于可運行線程池中,等待被線程調度選中,獲取 CPU 的使用權。即線程可以運行,但是尚未運行。
    就緒狀態的線程在獲得 CPU 時間片后,開始執行 run() 就變為運行中狀態(running)。
  3. 阻塞(BLOCKED):表示線程阻塞于鎖。
  4. 等待(WAITING):進入該狀態的線程需要等待其他線程做出一些特定動作(如通知或中斷)。
  5. 等待超時(TIMED_WAITING):該狀態不同于 WAITING,它可以在指定的時間后自行返回,也就是計時等待。
  6. 終止(TERMINATED):表示該線程已經執行完畢,包括 run() 或 call() 執行完成,線程正常結束;線程拋出未捕獲的 Exception 或 Error;直接調用了 stop() 結束線程(容易死鎖,不推薦)。

狀態轉移圖如下所示:

說明:

  1. 主線是初始->運行->終止,new 創建的新線程要調用 start() 才能進入運行狀態。運行狀態內部又分為運行中(具備執行權)和就緒(具備執行資格但沒有執行權)兩種狀態。注意 Java 中是把運行中和就緒統一視為運行狀態,但是在操作系統的觀點中,認為這兩個狀態是分開的、兩個獨立的狀態
  2. 運行狀態的線程可以通過調用 Object.wait() 或 Thread.sleep() 等方法進入等待狀態,方法上加時間參數的會進入等待超時狀態。等待狀態下的線程沒有執行資格,需要通過 notify()、notifyAll() 等方法喚醒。
  3. 阻塞狀態(具備執行資格但無執行權)只有一種情況,就是等待獲取 synchronized 鎖,拿到鎖之后就變成了運行狀態(阻塞式 IO 方法應該是這種情況?)。注意使用顯式鎖 Lock 等待鎖時,進入的是等待/等待超時狀態,因為其底層使用的是 LockSupport 類實現的。因此系統中能讓線程進入阻塞狀態的有且僅有 synchronized 關鍵字。
  4. 阻塞狀態是一種被迫的等待(因為拿不到鎖只能等著),而等待狀態是一種主動的等待(主動調用 wait() 或 sleep())。

此外還有幾點注意事項:

  1. 主線程結束并不會影響其它線程。
  2. isAlive() 可以測試某個線程是否存活,就緒、運行、阻塞狀態返回 true,新建、死亡返回 false。
  3. 不要對已經死亡的線程調用 start(),也不要對新建的線程調用兩次 start(),否則會引發 ILLegalThreadStateException。

針對以上情況,當發生如下特定情況時可以解除上面的阻塞,使線程重新進入就緒狀態:

3.2 涉及的方法介紹

join()

可以通過 join() 控制線程的執行順序,哪個線程執行到了 join() 就釋放執行權并凍結在線程池中,等調用了 join() 的線程執行完后,才恢復可執行狀態,與其它線程爭奪執行權。比如說:

public static void main(String[] args) throws Exception{Demo d = new Demo();Thread t1 = new Thread(d);Thread t2 = new Thread(d);t1.start();t2.start();t1.join();//t1線程要申請加入進來,運行。臨時加入一個線程運算時可以使用join方法。for(int x=0; x<50; x++){System.out.println(Thread.currentThread()+"....."+x);}}
}

主線程開啟了 t1、t2 兩個線程,在執行 t1.join() 之前,是三個線程在輪番運行的。在主線程中執行了 t1.join() 后,主線程釋放執行權,凍結在線程池。等待 t1 執行完畢后,恢復可執行狀態,與 t2 爭奪執行資格,輪番運行。

如果在 A 線程中調用了 B 線程的 join() 方法,那么 A 線程將被阻塞直到 B 線程執行完。它有三種重載形式:

第三種形式很少被用到,因為程序、操作系統和計算機硬件都無法精確到納秒。

sleep()

sleep() 用于線程睡眠,調用該方法可以讓線程暫停一段時間進入等待狀態,它有兩種重載形式:

同樣是因為程序、操作系統和硬件設備無法精確到納秒,因此第二個方法很少被使用。

處于睡眠時間內的線程不會獲得執行的機會,即使系統中沒有其它可執行的線程,處于 sleep() 中的線程也不會執行;已經獲得鎖的線程如果執行了 sleep(),只會釋放執行權,但不會釋放鎖。

yield()

yield() 用于線程讓步,它也可以讓當前正在運行的線程暫停,但它不會使線程進入等待狀態,只是將該線程轉入可運行狀態,然后讓系統的線程調度器重新調度一次。完全可能的情況是:某個線程調用了 yield() 暫停之后,調度器又將其調度出來重新執行。

實際上,當 A 線程調用了 yield() 之后,只有優先級大于等于 A 的處于可運行狀態的線程才會獲得執行機會。

暫停當前正在執行的線程對象,釋放執行權,然后讓包括自己在內的所有線程再次爭奪執行權。這樣做會更和諧,不會一直在執行同一個線程。

sleep() 與 yield() 的區別:

其它方法

Object.wait() 與 Thread.sleep() 的對比:

  1. wait() 可以指定時間也可以不指定,sleep() 必須指定時間。
  2. 在同步中時,對 cpu 的執行權和鎖的處理不同。wait() 釋放執行權,釋放鎖;sleep() 釋放執行權,不釋放鎖。
class Demo {void show() {synchronized(this) {wait();//t0 t1 t2}}void method() {synchronized(this)//t4{notifyAll();}//t4}
}

同步中誰有鎖誰執行,如果 t0 t1 t2 都卡在 wait() 并被 t4 喚醒,雖然 3 個都活了,但是只有一個能持鎖執行代碼。

此外,我們經常會調用 Thread 的 toString() 輸出線程信息,線程的字符串表現形式,包括線程名稱、優先級和線程組:

    public String toString() {ThreadGroup group = getThreadGroup();if (group != null) {return "Thread[" + getName() + "," + getPriority() + "," +group.getName() + "]";} else {return "Thread[" + getName() + "," + getPriority() + "," +"" + "]";}}

4、線程屬性

4.1 優先級

每個 Java 線程都有一個優先級,可以通過 Thread.setPriority() 將線程優先級設置在 MIN_PRIORITY(數值為1)和 MAX_PRIORITY(數值為10)之間,默認優先級為 MIN_PRIORITY(數值為5)。

線程調度器會優先選擇優先級高的線程來運行。但是線程優先級是高度依賴于系統的。當 JVM 依賴于宿主機平臺的線程實現機制時,Java 線程的優先級會先被映射到宿主機平臺的優先級上。例如 Windows 有 7 個優先級,而在 Oracle 為 Linux 提供的 JVM 中,線程的優先級被忽略——所有線程具有相同的優先級。

如果確實要使用優先級,需要注意,如果高優先級的線程沒有進入非活動狀態(阻塞或等待),低優先級的線程可能永遠也得不到執行,發生線程饑餓的情況(線程饑餓就是指低優先級的線程,總是拿不到執行時間)。

4.2 守護線程

守護線程也稱為后臺線程、精靈線程,它是在后臺運行的線程,任務是為其它線程提供服務,JVM 的垃圾回收線程就是典型的守護線程。此外,守護線程也可用于發送計時信號或清空過時的高速緩存。

可以使用 Thread 的 setDeamon(true) 將一個線程設置為守護線程(但是必須在該線程啟動之前,否則會引發 ILLegalThreadStateException),isDeamon() 用來判斷是否是守護線程。

主線程默認是前臺線程,但不是所有線程默認都是前臺線程,規則是:前臺線程創建的子線程默認是前臺線程,守護線程創建的線程默認是后臺線程

守護線程也會去爭搶同步鎖。

守護線程的特征為當所有前臺線程死亡后,虛擬機會退出并通知后臺線程死亡。假如在主線程中開啟了一個執行耗時操作的守護線程,那么很有可能守護線程的任務并不會執行完,因為主線程不會等待守護線程,只要主線程跑完了,守護線程也會自動消亡。但如果在主線程中啟動一個非守護線程,那么主線程會等待該子線程執行完任務。

永遠不要讓守護線程去訪問文件、數據庫這樣的固有資源,因為他會在任何時候甚至在一個操作的中間被中斷。

4.3 線程組與未處理的異常

Java 允許程序直接對 ThreadGroup 進行控制。如果沒有顯式指定一個線程屬于哪個線程組,那么它就屬于默認線程組,即與創建它的線程在同一線程組。線程中途不能改變它所屬的線程組

以下構造方法用來指定新創建的線程屬于哪個線程組:

在這里插入圖片描述

以上方法也可以指定線程組的名字,這個名字也不能中途更改。

常用的操作線程組的方法:

ThreadGroup 實現了一個接口 Thread.UncaughtExceptionHandler,該接口用于處理未捕獲的異常。

線程的 run() 不能拋出任何受查異常(刨去非受查異常剩余的異常),而非受查異常(所有派生于 Error 或 RuntimeException 的異常)會導致線程終止。

線程因為異常終止之前,會將異常傳遞到未捕獲異常的處理器中,該處理器必須實現 Thread.UncaughtExceptionHandler 接口,并且通過 Thread 的 setUncaughtExceptionHandler() 為某一個線程設置處理器,或者用靜態的 setDefaultUncaughtExceptionHandler() 為所有線程設置一個默認的處理器。如果沒有調用以上方法給線程設置處理器,那么線程的處理器就是該線程的 ThreadGroup 對象。

UncaughtExceptionHandler 接口的唯一方法 uncaughtException() 會按照如下優先順序操作:

  1. 如果該線程組有父線程組,則調用父線程組的 uncaughtException()
  2. 如果 Thread 的 getDefaultUncaughtExceptionHandler() 返回一個非空處理器,則調用該處理器
  3. 如果 Throwable 是 ThreadDeath 的一個實例,就什么都不做,否則,就將線程名字以及 Throwable 的棧軌跡輸出到 System.err 上。

其中,最后一點的棧軌跡就是應用發生崩潰時我們看到的調用棧信息。

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

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

相關文章

深入QML語法

文章目錄 深入了解 QML 文檔的結構和語法什么是 QML 文檔&#xff1f;導入語句導入語句的格式示例 對象聲明基本語法示例更復雜的對象聲明 QML 對象類型詳解1. Rectangle&#xff08;矩形&#xff09;2. Gradient&#xff08;漸變&#xff09;3. Text&#xff08;文本&#xff…

【Python】使用Selenium 操作瀏覽器 自動化測試 記錄

【自動化】Python SeleniumUtil 工具 開啟開發者模式 自動安裝油猴用戶腳本等-CSDN博客文章瀏覽閱讀389次。【自動化】Python SeleniumUtil 工具。https://blog.csdn.net/G971005287W/article/details/144565691?spm1001.2014.3001.5501【學習記錄】瀏覽器指紋相關學習記錄&am…

Linux應用軟件編程-文件操作(標準io)

在Linux下一切皆文件&#xff0c;比如&#xff1a;.txt&#xff0c;.c&#xff0c;.h&#xff0c;.jpg&#xff0c;目錄&#xff0c;鍵盤&#xff0c;鼠標&#xff0c;顯示器、硬盤等等都是文件&#xff0c;即IO。文件操作的統一思想&#xff1a;打開文件&#xff0c;讀、寫文件…

【Rust自學】4.4. 引用與借用

4.4.0 寫在正文之前 這一節的內容其實就相當于C的智能指針移動語義在編譯器層面做了一些約束。Rust中引用的寫法通過編譯器的約束寫成了C中最理想、最規范的指針寫法。所以學過C的人對這一章肯定會非常熟悉。 喜歡的話別忘了點贊、收藏加關注哦&#xff08;加關注即可閱讀全文…

深入解析 StarRocks 物化視圖:全方位的查詢改寫機制

小編導讀&#xff1a; 本文將重點介紹如何利用物化視圖進行查詢改寫。文章將全面介紹物化視圖的基本原理、關鍵特性、應用案例、使用場景、代碼細節以及主流大數據產品的物化視圖改寫能力對比。 物化視圖在 StarRocks 中扮演著至關重要的角色&#xff0c;它是進行數據建模和加速…

2. petalinux-build失敗

NOTE 解決因為網絡原因產生的編譯錯誤分享詳細的解決步驟 報錯的情況 因為網絡原因產生編譯錯誤 現象 找不到適合的包文件(No suitable stageing package found) 不能發現文件(Fetcher failure for URL) 解決方法 采用本地加載本地文件的方式&#xff0c;步驟如下 進入…

sql server msdb數據庫備份恢復

備份 BACKUP DATABASE [msdb] TO DISK ND:\liyuanshuai\test\sqlserver_bakfile\msdb20241219.bak WITH NOFORMAT, NOINIT, NAME Nlys-完整 數據庫 備份, SKIP, NOREWIND, NOUNLOAD, COMPRESSION, STATS 10 GO然后刪除2個測試的job&#xff0c;停止 SQL Server 代理…

web實驗二

web實驗二 2024.12.19 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>青島理工大學</title>&l…

bain.js(十二):RNN神經網絡實戰教程 - 音樂樂譜生成 -人人都是作曲家~

系列文章&#xff1a; &#xff08;一&#xff09;&#xff1a;可以在瀏覽器運行的、默認GPU加速的神經網絡庫概要介紹&#xff08;二&#xff09;&#xff1a;項目集成方式詳解&#xff08;三&#xff09;&#xff1a;手把手教你配置和訓練神經網絡&#xff08;四&#xff09…

WebSocket入門與結合redis

WebSocket是什么 WebSocket 是一種用于在客戶端和服務器之間建立雙向通信的協議&#xff0c;它能實現實時、持久的連接。與傳統的 HTTP 請求響應模式不同&#xff0c;WebSocket 在建立連接后允許客戶端和服務器之間相互發送消息&#xff0c;直到連接關閉。由于 WebSocket 具有…

Hive是什么,Hive介紹

官方網站&#xff1a;Apache Hive Hive是一個基于Hadoop的數據倉庫工具&#xff0c;主要用于處理和查詢存儲在HDSF上的大規模數據?。Hive通過將結構化的數據文件映射為數據庫表&#xff0c;并提供類SQL的查詢功能&#xff0c;使得用戶可以使用SQL語句來執行復雜的?MapReduce任…

OpenHarmony和OpenVela的技術創新以及兩者對比

兩款有名的國內開源操作系統&#xff0c;OpenHarmony&#xff0c;OpenVela都非常的優秀。本文對二者的創新進行一個簡要的介紹和對比。 一、OpenHarmony OpenHarmony具有諸多有特點的技術突破和重要貢獻&#xff0c;以下是一些主要方面&#xff1a; 架構設計創新 分層架構…

Electron-Vue 開發下 dev/prod/webpack server各種路徑設置匯總

背景 在實際開發中&#xff0c;我發現團隊對于這幾個路徑的設置上是純靠猜的&#xff0c;通過一點點地嘗試來找到可行的路徑&#xff0c;這是不應該的&#xff0c;我們應該很清晰地了解這幾個概念&#xff0c;以下通過截圖和代碼進行細節講解。 npm run dev 下的路徑如何處理&…

前端-處理數據的函數

判斷數據是否為空,對象是否存在某屬性,屬性值是否為空,對大數據進行換算,對單位進行轉換. 目錄 1.格式化數據 2.判斷值是否為空(包括對象、數組、字符串、數值類型) &#xff08;1&#xff09;值是0不表示空 &#xff08;2&#xff09;值是0表示空 3. 檢查對象是否具有指定名…

基礎入門-Web應用蜜罐系統堡壘機運維API內外接口第三方拓展架構部署影響

知識點&#xff1a; 1、基礎入門-Web應用-蜜罐系統 2、基礎入門-Web應用-堡壘機運維 3、基礎入門-Web應用-內外API接口 4、基礎入門-Web應用-第三方拓展架構 一、演示案例-Web-拓展應用-蜜罐-釣魚誘使 蜜罐&#xff1a;https://hfish.net/ 測試系統&#xff1a;Ubuntu 20.04 …

Android運行低版本項目可能遇到的問題

Android運行低版本項目可能遇到的問題 低版本項目總是遇到各種問題的&#xff0c;耐心點 一、gradle-xxx.xxx.xxx.zip一直下載不下來 在gradle-wrapper.properties可以試下 distributionBaseGRADLE_USER_HOME distributionPathwrapper/dists zipStoreBaseGRADLE_USER_HOME …

springboot中Controller內文件上傳到本地以及阿里云

上傳文件的基本操作 <form action"/upload" method"post" enctype"multipart/form-data"> <h1>登錄</h1> 姓名&#xff1a;<input type"text" name"username" required><br> 年齡&#xf…

智慧城市工程:相關學點、優勢、未來發展

目錄 相關學點&#xff1a; 智慧城市的優勢 挑戰與未來發展 智慧城市工程是利用現代信息技術和數據分析手段&#xff0c;提升城市管理和服務水平&#xff0c;實現城市運行的智能化、便捷化和高效化的一種新型城市發展模式。智慧城市通過整合物聯網&#xff08;IoT&#xff0…

授權模型MAC

MAC&#xff08;Mandatory Access Control&#xff09;是一種授權模型&#xff0c;用于實現對系統資源訪問的強制控制。在MAC模型中&#xff0c;授權是基于預先定義的安全策略&#xff0c;且該策略由系統管理員來配置和管理。 在MAC模型中&#xff0c;每個用戶和每個資源都被賦…

看板工具助力餐飲與酒店行業實現數字化轉型,提升管理與運營效率

在餐飲與酒店行業&#xff0c;服務質量和客戶體驗是衡量企業成功的關鍵因素。隨著客戶需求的不斷多樣化以及市場競爭的加劇&#xff0c;傳統的管理模式逐漸難以滿足高效運營的需求。尤其在高峰期&#xff0c;如何優化內部流程、提高服務效率和響應速度&#xff0c;成為了許多餐…