乾貨: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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...