乾貨:Java多線程詳解(內附源碼)

来源:https://www.cnblogs.com/lfs2640666960/archive/2018/04/06/8729456.html
-Advertisement-
Play Games

線程是程式執行的最小單元,多線程是指程式同一時間可以有多個執行單元運行(這個與你的CPU核心有關)。 在java中開啟一個新線程非常簡單,創建一個Thread對象,然後調用它的start方法,一個新線程就開啟了。 那麼執行代碼放在那裡呢?有兩種方式:1. 創建Thread對象時,覆寫它的run方法, ...



 

線程是程式執行的最小單元,多線程是指程式同一時間可以有多個執行單元運行(這個與你的CPU核心有關)。

在java中開啟一個新線程非常簡單,創建一個Thread對象,然後調用它的start方法,一個新線程就開啟了。

那麼執行代碼放在那裡呢?有兩種方式:1. 創建Thread對象時,覆寫它的run方法,把執行代碼放在run方法里。2. 創建Thread對象時,給它傳遞一個Runnable對象,把執行代碼放在Runnable對象的run方法里。

如果多線程操作的是不同資源,線程之間不會相互影響,不會產生任何問題。但是如果多線程操作相同資源(共用變數),就會產生多線程衝突,要知道這些衝突產生的原因,就要先瞭解java記憶體模型(簡稱JMM)。

一. java記憶體模型(JMM)

1.1 java記憶體模型(JMM)介紹

java記憶體模型決定一個線程對共用變數的寫入何時對另一個線程可見。從抽樣的角度來說:線程之間的共用變數存儲在主記憶體(main memory)中,每個線程都有一個私有的本地記憶體(local memory),本地記憶體中存儲了該線程以讀/寫共用變數的副本。

存在兩種記憶體:主記憶體和線程本地記憶體,線程開始時,會複製一份共用變數的副本放在本地記憶體中。

線程對共用變數操作其實都是操作線程本地記憶體中的副本變數,當副本變數發生改變時,線程會將它刷新到主記憶體中(並不一定立即刷新,何時刷新由線程自己控制)。

當主記憶體中變數發生改變,就會通知發出信號通知其他線程將該變數的緩存行置為無效狀態,因此當其他線程從本地記憶體讀取這個變數時,發現這個變數已經無效了,那麼它就會從記憶體重新讀取。

1.2 可見性

從上面的介紹中,我們看出多線程操作共用變數,會產生一個問題,那就是可見性問題: 即一個線程對共用變數修改,對另一個線程來說並不是立即可見的。

classData{inta =0;intb =0;intx =0;inty =0;// a線程執行publicvoidthreadA(){        a =1;        x = b;    }// b線程執行publicvoidthreadB(){        b =2;        y = a;    }}

如果有兩個線程同時分別執行了threadA和threadB方法。可能會出現x==y==0這個情況(當然這個情況比較少的出現)。

因為a和b被賦值後,還沒有刷新到主記憶體中,就執行x = b和y = a的語句,這個時候線程並不知道a和b還已經被修改了,依然是原來的值0。

1.3 有序性

為了提高程式執行性能,Java記憶體模型允許編譯器和處理器對指令進行重排序。重排序過程不會影響到單線程程式的執行,卻會影響到多線程併發執行的正確性。

classReorder{intx =0;booleanflag =false;publicvoidwriter(){        x =1;        flag =true;    }publicvoidreader(){if(flag) {inta = x * x;            ...        }    }}

例如上例中,我們使用flag變數,標誌x變數已經被賦值了。但是這兩個語句之間沒有數據依賴,所以它們可能會被重排序,即flag = true語句會在x = 1語句之前,那麼這麼更改會不會產生問題呢?

在單線程模式下,不會有任何問題,因為writer方法是一個整體,只有等writer方法執行完畢,其他方法才能執行,所以flag = true語句和x = 1語句順序改變沒有任何影響。

在多線程模式下,就可能會產生問題,因為writer方法還沒有執行完畢,reader方法就被另一線程調用了,這個時候如果flag = true語句和x = 1語句順序改變,就有可能產生flag為true,但是x還沒有賦值情況,與程式意圖產生不一樣,就會產生意想不到的問題。

1.4 原子性

在Java中,對基本數據類型的變數的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要麼執行,要麼不執行。

x =1;// 原子性y = x;// 不是原子性x = x +1;// 不是原子性x++;// 不是原子性System.out.println(x);// 原子性

公式2:有兩個原子性操作,讀取x的值,賦值給y。公式3:也是三個原子性操作,讀取x的值,加1,賦值給x。公式4:和公式3一樣。

所以對於原子性操作就兩種:1. 將基本數據類型常量賦值給變數。2. 讀取基本數據類型的變數值。任何計算操作都不是原子的。

1.5 小結

多線程操作共用變數,會產生上面三個問題,可見性、有序性和原子性。

可見性: 一個線程改變共用變數,可能並沒有立即刷新到主記憶體,這個時候另一個線程讀取共用變數,就是改變之前的值。所以這個共用變數的改變對其他線程並不是可見的。

有序性: 編譯器和處理器會對指令進行重排序,語句的順序發生改變,這樣在多線程的情況下,可能出現奇怪的異常。

原子性: 只有對基本數據類型的變數的讀取和賦值操作是原子性操作。

要解決這三個問題有兩種方式:

volatile關鍵字:它只能解決兩個問題可見性和有序性問題,但是如果volatile修飾基本數據類型變數,而且這個變數只做讀取和賦值操作,那麼也沒有原子性問題了。比如說用它來修飾boolean的變數。

加鎖:可以保證同一時間只有同一線程操作共用變數,當前線程操作共用變數時,共用變數不會被別的線程修改,所以可見性、有序性和原子性問題都得到解決。分為synchronized同步鎖和JUC框架下的Lock鎖。

二. volatile關鍵字

volatile關鍵字作用

1.可見性: 對一個volatile變數的讀取,總是能看到(任意線程)對這個volatile變數最後的寫入。

有序性: 禁止指令重排序,即在程式中在volatile變數進行操作時,在其之前的操作肯定已經全部執行了,而且結果已經對後面的操作可見,在其之後的操作肯定還沒有執行。

這個的具體解釋,大家請看《深入理解Java記憶體模型》裡面關於happens-before規則的講解。

classVolatileFeaturesExample{//使用volatile聲明一個基本數據類型變數vlvolatilelongvl =0L;//對於單個volatile基本數據類型變數賦值publicvoidset(longl){        vl = l;    }//對於單個volatile基本數據類型變數的複合操作publicvoidgetAndIncrement(){        vl++;    }//對於單個volatile基本數據類型變數讀取publiclongget(){returnvl;    }}classVolatileFeaturesExample{//聲明一個基本數據類型變數vllongvl =0L;// 相當於加了同步鎖publicsynchronizedvoidset(longl){      vl = l;    }// 普通方法publicvoidgetAndIncrement(){longtemp = get();        temp +=1L;        set(temp);    }// 相當於加了同步鎖publicsynchronizedlongget(){returnvl;    }}

如果volatile修飾基本數據類型變數,而且只對這個變數做讀取和賦值操作,那麼就相當於加了同步鎖。

三. synchronized同步鎖

synchronized同步鎖作用是訪問被鎖住的資源時,只要獲取鎖的線程才能操作被鎖住的資源,其他線程必須阻塞等待。

所以一個線程來說,可以阻塞等待,可以運行,那麼線程到底有哪些狀態呢?

3.1 線程狀態


 

狀態轉換圖

線程分為5種狀態:

新建狀態(New):創建一個Thread對象,那麼該thread對象就是新建狀態。

可運行狀態(Runnable):表示該thread線程隨時都可以運行,只要獲取CPU的執行權。 

註: 該狀態可以由新建狀態轉換而來(通過調用thread的start方法),也可以由阻塞狀態轉換而來

運行狀態(Running):表示該線程正在運行,註意運行狀態只能從可運行狀態到達。

阻塞狀態(Blocked):表示該線程當前停止運行,主要分為三種情況: 

1). 同步阻塞狀態:線程獲取同步鎖失敗,就會進入同步阻塞狀態。 

2). 等待阻塞狀態:線程調用wait方法,進入該狀態。註:join方法本質也是通過wait方法實現的。 

3). 其他阻塞狀態:通過Thread.sleep方法讓線程睡眠,開啟IO流讓線程等待阻塞。

死亡狀態(Dead):當thread的run方法運行完畢,那麼線程就進入死亡狀態。該狀態不能再轉換成其他狀態。

3.2 synchronized同步方法或者同步塊

synchronized同步方法或者同步塊具體是怎樣操作的呢?

相當於有一個大房間,房間門上有一把鎖lock,房間裡面存放的是所有與這把鎖lock關聯的同步方法或者同步塊。

當某一個線程要執行這把鎖lock的一個同步方法或者同步塊時,它就來到房間門前,如果發現鎖lock還在,那麼它就拿著鎖進入房間,並將房間鎖上,它可以執行房間中任何一個同步方法或者同步塊。

這時又有另一個線程要執行這把鎖lock的一個同步方法或者同步塊時,它就來到房間門前,發現鎖lock沒有了,就只能在門外等待,此時該線程就在synchronized同步阻塞線程池中。

等到拿到鎖lock的線程,同步方法或者同步塊代碼執行完畢,它就會從房間中退出來,將鎖放到門上。

這時在門外等待的線程就爭奪這把鎖lock,拿到鎖的線程就可以進入房間,其他線程則又要繼續等待。

註:synchronized 鎖是鎖住所有與這個鎖關聯的同步方法或者同步塊。

synchronized的同步鎖到底是什麼呢?

其實就是java對象,在Java中,每一個對象都擁有一個鎖標記(monitor),也稱為監視器,多線程同時訪問某個對象時,線程只有獲取了該對象的鎖才能訪問。

3.3 wait與notify、notifyAll

這三個方法主要用於實現線程之間相互等待的問題。

調用對象lock的wait方法,會讓當前線程進行等待,即將當前線程放入對象lock的線程等待池中。調用對象lock的notify方法會從線程等待池中隨機喚醒一個線程,notifyAll方法會喚醒所有線程。

註:對象lock的wait與notify、notifyAll方法調用必須放在以對象lock為鎖的同步方法或者同步塊中,否則會拋出IllegalMonitorStateException異常。

wait與notify、notifyAll具體是怎麼操作的呢?

前面過程與synchronized中介紹的一樣,當調用鎖lock的wait方法時,該線程(即當前線程)退出房間,歸還鎖lock,但並不是進入synchronized同步阻塞線程池中,而是進入鎖lock的線程等待池中。

這時另一個線程拿到鎖lock進行房間,如果它執行了鎖lock的notify方法,那麼就會從鎖lock的線程等待池中隨機喚醒一個線程,將它放入synchronized同步阻塞線程池中(記住只有拿到鎖lock的線程才能進行房間)。調用鎖lock的notifyAll方法,即喚醒線程等待池所有線程。

註:當被wait阻塞的線程再次進入synchronized同步代碼塊時,會從wait方法調用之後的地方繼續執行。

在鎖lock的線程等待池中的線程,只有四種方式喚醒:

通過notify()喚醒

通過notifyAll()喚醒

通過interrupt()中斷喚醒

如果是通過調用wait(long timeout)進入等待狀態的線程,當時間超時的時候,也會被喚醒。

註意wait、notify和notifyAll方法必須先獲取鎖才能調用,否則拋出IllegalMonitorStateException異常。而只有synchronized模塊才能讓當前線程獲取鎖,所以wait方法只能在synchronized模塊中執行。

四. 其他重要方法

4.1 join方法

讓當前線程等待另一個線程執行完成後,才繼續執行。

publicfinalvoidjoin()throwsInterruptedException {join(0);    }publicfinalsynchronizedvoidjoin(longmillis)throwsInterruptedException {// 獲取當前系統毫秒數longbase = System.currentTimeMillis();longnow =0;// millis小於0,拋出異常if(millis <0) {thrownewIllegalArgumentException("timeout value is negative");        }if(millis ==0) {// 通過isAlive判斷當前線程是否存活while(isAlive()) {// wait(0)表示當前線程無限等待wait(0);            }        }else{// 通過isAlive判斷當前線程是否存活while(isAlive()) {longdelay = millis - now;if(delay <=0) {break;                }// 當前線程等待delay毫秒,超過時間,當前線程就被喚醒wait(delay);                now = System.currentTimeMillis() - base;            }        }    }

join方法是Thread中的方法,synchronized方法同步的鎖對象就是Thread對象,通過調用Thread對象的wait方法,讓當前線程等待

註意:這裡是讓當前線程等待,即當前調用join方法的線程,而不是Thread對象的線程。那麼當前線程什麼時候會被喚醒呢?

當Thread對象線程執行完畢,進入死亡狀態時,會調用Thread對象的notifyAll方法,來喚醒Thread對象的線程等待池中所有線程。

示例:

publicstaticvoidjoinTest(){        Thread thread =newThread(newRunnable() {            @Overridepublicvoidrun(){for(inti =0; i <10; i++) {try{                        Thread.sleep(100);                    }catch(InterruptedException e) {                        e.printStackTrace();                    }                    System.out.println(Thread.currentThread().getName()+":  i==="+i);                }            }        },"t1");        thread.start();try{            thread.join();        }catch(InterruptedException e) {            e.printStackTrace();        }        System.out.println(Thread.currentThread().getName()+": end");    }

4.2 sleep方法

只是讓當前線程等待一定的時間,才繼續執行。

4.3 yield方法

將當前線程狀態從運行狀態轉成可運行狀態,如果再獲取CPU執行權,就繼續執行。

4.4 interrupt方法

中斷線程,它會中斷處於阻塞狀態下的線程,但是對於運行狀態下的線程不起任何作用。

示例:

publicstaticvoidinterruptTest(){// 處於阻塞狀態下的線程Thread thread =newThread(newRunnable() {            @Overridepublicvoidrun(){try{                    System.out.println(Thread.currentThread().getName()+" 開始");                    Thread.sleep(1000);                    System.out.println(Thread.currentThread().getName()+" 結束");                }catch(InterruptedException e) {                    System.out.println(Thread.currentThread().getName()+" 產生異常");                }            }        },"t1");        thread.start();// 處於運行狀態下的線程Thread thread1 =newThread(newRunnable() {            @Overridepublicvoidrun(){                System.out.println(Thread.currentThread().getName()+" 開始");inti =0;while(i < Integer.MAX_VALUE -10) {                    i = i +1;for(intj =0; j < i; j++);                }                System.out.println(Thread.currentThread().getName()+" i=="+i);                System.out.println(Thread.currentThread().getName()+" 結束");            }        },"t2");        thread1.start();try{            Thread.sleep(10);        }catch(InterruptedException e) {            e.printStackTrace();        }        System.out.println(Thread.currentThread().getName()+" 進行中斷");        thread.interrupt();        thread1.interrupt();    }

4.5 isInterrupted方法

返回這個線程是否被中斷。註意當調用線程的interrupt方法後,該線程的isInterrupted的方法就會返回true。如果異常被處理了,又會將該標誌位置位false,即isInterrupted的方法返回false。

4.6 線程優先順序以及守護線程

在java中線程優先順序範圍是1~10,預設的優先順序是5。

在java中線程分為用戶線程和守護線程,isDaemon返回是true,表示它是守護線程。當所有的用戶線程執行完畢後,java虛擬機就會退出,不管是否還有守護線程未執行完畢。

當創建一個新線程時,這個新線程的優先順序等於創建它線程的優先順序,且只有當創建它線程是守護線程時,新線程才是守護線程。

當然也可以通過setPriority方法修改線程的優先順序,已經setDaemon方法設置線程是否為守護線程。

五. 實例講解

5.1 不加任何同步鎖

importjava.util.Collections;importjava.util.List;importjava.util.concurrent.CopyOnWriteArrayList;importjava.util.concurrent.CountDownLatch;classData {intnum;publicData(intnum){this.num = num;    }publicintgetAndDecrement(){returnnum--;    }}classMyRun implements Runnable {privateData data;// 用來記錄所有賣出票的編號privateListlist;privateCountDownLatch latch;publicMyRun(Data data, Listlist, CountDownLatch latch){this.data = data;this.list=list;this.latch = latch;    }    @Overridepublicvoidrun(){try{            action();        }  finally {// 釋放latch共用鎖latch.countDown();        }    }// 進行買票操作,註意這裡沒有使用data.num>0作為判斷條件,直到賣完線程退出。// 那麼做會導致這兩處使用了共用變數data.num,那麼做多線程同步時,就要考慮更多條件。// 這裡只for迴圈了5次,表示每個線程只賣5張票,並將所有賣出去編號存入list集合中。publicvoidaction(){for(inti =0; i <5; i++) {try{                Thread.sleep(10);            }catch(InterruptedException e) {                e.printStackTrace();            }intnewNum = data.getAndDecrement();            System.out.println("線程"+Thread.currentThread().getName()+"  num=="+newNum);list.add(newNum);        }    }}publicclassThreadTest {publicstaticvoidstartThread(Data data, String name, Listlist,CountDownLatch latch){        Thread t =newThread(newMyRun(data,list, latch), name);        t.start();    }publicstaticvoidmain(String[] args){// 使用CountDownLatch來讓主線程等待子線程都執行完畢時,才結束CountDownLatch latch =newCountDownLatch(6);longstart = System.currentTimeMillis();// 這裡用併發list集合Listlist=newCopyOnWriteArrayList();        Data data =newData(30);        startThread(data,"t1",list, latch);        startThread(data,"t2",list, latch);        startThread(data,"t3",list, latch);        startThread(data,"t4",list, latch);        startThread(data,"t5",list, latch);        startThread(data,"t6",list, latch);try{            latch.await();        }catch(InterruptedException e) {            e.printStackTrace();        }// 處理一下list集合,進行排序和翻轉Collections.sort(list);        Collections.reverse(list);        System.out.println(list);longtime = System.currentTimeMillis() - start;// 輸出一共花費的時間System.out.println("\n主線程結束 time=="+time);    }}

輸出的結果是

線程t2num==29線程t6num==27線程t5num==28線程t4num==28線程t1num==30線程t3num==30線程t2num==26線程t4num==24線程t6num==25線程t5num==23線程t1num==22線程t3num==21線程t4num==20線程t6num==19線程t5num==18線程t2num==17線程t1num==16線程t3num==15線程t4num==14線程t5num==12線程t6num==13線程t1num==9線程t3num==10線程t2num==11線程t1num==8線程t6num==5線程t2num==7線程t5num==3線程t3num==4線程t4num==6[30,30,29,28,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3]主線程結束 time==62

從結果中發現問題,出現了重覆票,所以30張票沒有被賣完。最主要的原因就是Data類的getAndDecrement方法操作不是多線程安全的。

首先它不能保證原子性,分為三個操作,先讀取num的值,然後num自減,在返回自減前的值。

因為num不是volatile關鍵字修飾的,它也不能保證可見性和有序性。

所以只要保證getAndDecrement方法多線程安全,那麼就可以解決上面出現的問題。那麼保證getAndDecrement方法多線程安全呢?最簡單的方式就是在getAndDecrement方法前加synchronized關鍵字。

這是synchronized關鍵鎖就是這個data對象實例,所以保證了多線程調用getAndDecrement方法時,只有一個線程能調用,等待調用完成,其他線程才能調用getAndDecrement方法。

因為同一時間只有一個線程調用getAndDecrement方法,所以它在做num--操作時,不用擔心num變數會發生改變。所以原子性、可見性和有序性都可以得到保證。

5.2 使用最小同步鎖

classData{intnum;    public Data(intnum) {this.num=num;    }// 將getAndDecrement方法加了同步鎖public synchronizedintgetAndDecrement() {returnnum--;    }}

輸出結果

線程t1num==30線程t2num==29線程t6num==28線程t4num==26線程t3num==27線程t5num==25線程t6num==22線程t2num==21線程t3num==23線程t1num==24線程t4num==20線程t5num==19線程t2num==18線程t3num==17線程t5num==13線程t4num==14線程t6num==16線程t1num==15線程t2num==12線程t4num==9線程t1num==7線程t5num==10線程t3num==11線程t6num==8線程t4num==6線程t2num==3線程t1num==2線程t3num==4線程t5num==5線程t6num==1[30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1]主線程結束 time==61

我們只是將Data的getAndDecrement方法加了同步鎖,發現解決了多線程併發問題。主要是因為我們只在一處使用了共用變數num,所以只需要將這處加同步就行了。而且你會發現最後花費的總時間與沒加同步鎖時幾乎一樣,那麼因為我們同步代碼足夠小。

相反地,我們加地同步鎖不合理,可能也能實現多線程安全,但是耗時就會大大增加。

5.3 不合理地使用同步鎖

@Overridepublicvoidrun(){try{synchronized(data){                action();            }        }finally{// 釋放latch共用鎖latch.countDown();        }    }

輸入結果:

線程t1num==30線程t1num==29線程t1num==28線程t1num==27線程t1num==26線程t6num==25線程t6num==24線程t6num==23線程t6num==22線程t6num==21線程t5num==20線程t5num==19線程t5num==18線程t5num==17線程t5num==16線程t4num==15線程t4num==14線程t4num==13線程t4num==12線程t4num==11線程t3num==10線程t3num==9線程t3num==8線程t3num==7線程t3num==6線程t2num==5線程t2num==4線程t2num==3線程t2num==2線程t2num==1[30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1]主線程結束 time==342

在這裡我們將整個action方法,放入同步代碼塊中,也可以解決多線程衝突問題,但是所耗費的時間是在getAndDecrement方法上加同步鎖時間的幾倍。

所以我們在加同步鎖的時候,那些需要同步,就是看那些地方使用了共用變數。比如這裡只在getAndDecrement方法中使用了同步變數,所以只要給它加鎖就行了。

但是如果在action方法中,使用data.num>0來作為迴圈條件,那麼在加同步鎖時,就必須將整個action方法放在同步模塊中,因為我們必須保證,在data.num>0判斷到getAndDecrement方法調用這些代碼都是在同步模塊中,不然就會產生多線程衝突問題。

福利:

想要瞭解更多多線程知識點的,可以關註我一下,我後續也會整理更多關於多線程這一塊的知識點分享出來,另外順便給大家推薦一個交流學習群:650385180,裡面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分散式、多線程、微服務架構的原理,JVM性能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多。


 
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 迭代器在STL運用廣泛,類似容器的迭代已經成為其重要特性,而迭代器模式則是利用迭代器概念進行的抽象運用,迭代器模式運用廣泛和有用,因為其能夠不考慮數據的存儲方式,而是直接面對數據進行迭代,也就是說我們不用考慮集合是數組(或vector)、鏈表、棧還是隊列,而是通過統一的介面進行順序的訪問。 作用 迭 ...
  • 我有點像瘋子,一個人開了一天酒店,來寫這個。我發現我寫這個系列,閱讀的人很少。也許是程式員不重視思想的東西,也許是感覺我寫的很Low,無所謂,我只想告訴同行,程式員重在編程思想,有了編程思想技術的路才能走的更長更遠。我很孤獨,在自己的小世界里存活著。但是我也要耐著孤獨,向更好的方向發展需要孤獨,孤獨 ...
  • 進程的三態模型 細分進程狀態圖 進程的通信 互斥:一次只能供一個進程使用的資源。 同步:多個進程併發進行,可能需要等待。 生產者與消費者 PV操作 PV操作是實現進程同步與互斥的常用方法,在執行期間不可分割。P代表申請一個資源,V代表釋放一個資源。 P操作定義 :S1:=S1-1,若S>=0,則P操... ...
  • 觀察者模式通常的叫法叫做訂閱 發佈模式,類似於報刊雜誌的訂閱,觀察者和被觀察者就是讀者和郵局的關係,讀者先要在郵局訂閱想要的報刊,當報刊發行時,郵局會將報刊郵寄到讀者家裡。觀察者(Observer)和被觀察者(Listener)也是這種關係,Observer將自己attach到Listener中,當 ...
  • 服務拆分具體拆分到多細,業內沒有一個統一的標準。當然也不能為了拆分而拆分,還要依據具體的業務場景應用情況而定,讀過《淘寶技術這十年》的朋友,相信對淘寶的技術演進有一個很直觀的感受。雖然當時微服務的概念並不今天這般火熱,但實際已經在生產環境中運行。 simplemall項目的業務背景基於簡單的購物場景 ...
  • 編譯過程 詞法分析:對源程式從前到後(從左至右)逐個字元地掃描,從而識別出一個個"單詞"符號。 語法分析:判斷語法是否出錯,如表達式、迴圈語句、程式等。 語義分析:檢查如賦值語句左右是否匹配,是否有零除數等。 文法 G={Vt*Vn*S*P} Vt是一個非空有限的符號集合,它的每個元素稱為終結符。 ... ...
  • 題目鏈接 "簡單的數學題" 題目描述 輸入一個整數n和一個整數p,你需要求出 $$\sum_{i=1}^n\sum_{j=1}^n (i\cdot j\cdot gcd(i,j))\ mod\ p$$ 其中$gcd(a,b)$表示$a$與$b$的最大公約數 輸入 一行兩個整數$p,n$ 輸出 一行一 ...
  • 參考資料:網易雲網課地址 http://study.163.com/course/courseMain.htm?courseId=1455026 一、String類的兩種實例化方法 (1)直接賦值 以上代碼可以輸出str的值,說明str 已被實例化(未實例化會為null)。我們知道String類並不 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...