Java 線程和多線程執行過程分析

来源:https://www.cnblogs.com/f-ck-need-u/archive/2018/01/05/8197547.html
-Advertisement-
Play Games

本文目錄:1.幾個基本的概念2.創建線程的兩種方法3.線程相關的常用方法4.多線程安全問題和線程同步 4.1 多線程安全問題 4.2 線程同步 4.3 同步代碼塊和同步函數的區別以及鎖是什麼 4.4 單例懶漢模式的多線程安全問題5.死鎖(DeadLock) 1.幾個基本的概念 本文涉及到的一些概念, ...


本文目錄:
1.幾個基本的概念
2.創建線程的兩種方法
3.線程相關的常用方法
4.多線程安全問題和線程同步
 4.1 多線程安全問題
 4.2 線程同步
 4.3 同步代碼塊和同步函數的區別以及鎖是什麼
 4.4 單例懶漢模式的多線程安全問題
5.死鎖(DeadLock)

1.幾個基本的概念

本文涉及到的一些概念,有些是基礎知識,有些在後文會展開詳細的說明。

  1. 進程(Process):一個程式運行起來時在記憶體中開闢一段空間用來運行程式,這段空間包括heap、stack、data segment和code segment。例如,開一個QQ就表明開了一個QQ進程。
  2. 線程(Thread):每一個進程中都至少有一個線程。線程是指程式中代碼運行時的運行路徑,一個線程表示一條路徑。例如QQ進程中,發送消息、接收消息、接收文件、發送文件等各種獨立的功能都需要一個線程來執行。
  3. 進程和線程的區別:從資源的角度來考慮,進程主要考慮的是CPU和記憶體,而線程主要考慮的是CPU的調度,某進程中的各線程之間可以共用這個進程的很多資源。
    從粒度粗細來考慮,進程的粒度較粗,進程上下文切換時消耗的CPU資源較多。線程的粒度要小的多,雖然線程也會切換,但因為共用進程的上下文,相比進程上下文切換而言,同進程內的線程切換時消耗的資源要小的多的多。在JAVA中,除了java運行時啟動的JVM是一個進程,其他所有任務都以線程的方式執行,也就是說java應用程式是單進程的,甚至可以說沒有進程的概念。
  4. 線程組(ThreadGroup):線程組提供了一些批量管理線程的方法,因此通過將線程加入到線程組中,可以更方便地管理這些線程。
  5. 線程的狀態:就緒態、運行態、睡眠態。還可以分為存活和死亡,死亡表示線程結束,非死亡則存活,因此存活包含就緒、運行、睡眠。
  6. 中斷睡眠(interrupt):將線程從睡眠態強制喚醒,喚醒後線程將進入就緒隊列等待cpu調度。
  7. 併發操作:多個線程同時操作一個資源。這會帶來多線程安全問題,解決方法是使用線程同步。
  8. 線程同步:讓線程中的某些任務原子化,即要麼全部執行完畢,要麼不開始執行。通過互斥鎖來實現同步,通過監視這個互斥鎖是否被誰持有來決定是否從睡眠態轉為就緒態(即從線程池中出去),也就是是否有資格去獲取cpu的執行權。線程同步解決了線程安全的問題,但降低了程式的效率。
  9. 死鎖:線程全睡眠了無法被喚醒,導致程式卡死在某一處無法再執行下去。典型的是兩個同步線程,線程1持有A鎖,且等待B鎖,但線程2持有B鎖且等待A鎖,這樣的僵局會造成死鎖。但需要註意的是,死鎖並非都是因為僵局,只要兩邊的線程都無法繼續向下執行代碼(或者兩邊的線程池都無法被喚醒,這是等價的概念,因為鎖等待也會讓進程進入睡眠態),則都是死鎖

還需需要明確的一個關鍵點是:CPU對就緒隊列中每個線程的調度是隨機的(對我們人類來說),且分配的時間片也是隨機的(對人類來說)。

2.創建線程的兩種方法

Java中有兩種創建線程的方式。

創建線程方式一:

  1. 繼承Thread類(在java.lang包中),並重寫該類的run()方法,其中run()方法即線程需要執行的任務代碼。
  2. 然後new出這個類對象。這表示創建線程對象。
  3. 調用start()方法開啟線程來執行任務(start()方法會調用run()以便執行任務)。

例如下麵的代碼中,在主線程main中創建了兩個線程對象,先後並先後調用start()開啟這兩個線程,這兩個線程會各自執行MyThread中的run()方法。

class MyThread extends Thread {
    String name;
    String gender;

    MyThread(String name,String gender){
        this.name = name;
        this.gender = gender;
    }

    public void run(){
        int i = 0;
        while(i<=20) {
            //除了主線程main,其餘線程從0開始編號,currentThread()獲取的是當前線程對象
            System.out.println(Thread.currentThread().getName()+"-----"+i+"------"+name+"------"+gender);
            i++;
        }
    }
}

public class CreateThread {
    public static void main(String[] args) {
        MyThread mt1 = new MyThread("malong","Male");
        MyThread mt2 = new MyThread("Gaoxiao","Female");

        mt1.start();
        mt2.start();
        System.out.println("main thread over");
    }
}

上面的代碼執行時,有三個線程,首先是主線程main創建2個線程對象,並開啟這兩個線程任務,開啟兩個線程後主線程輸出"main thread over",然後main線程結束。在開啟兩個線程任務後,這兩個線程加入到了就緒隊列等待CPU的調度執行。如下圖。因為每個線程被cpu調度是隨機的,執行時間也是隨機的,所以即使mt1先開啟任務,但mt2可能會比mt1線程先執行,也可能更先消亡。

創建線程方式二:

  1. 實現Runnable介面,並重寫run()方法。
  2. 創建子類對象。
  3. 創建Thread對象來創建線程對象,並將實現了Runnable介面的對象作為參數傳遞給Thread()構造方法。
  4. 調用start()方法開啟線程來執行run()中的任務。
class MyThread implements Runnable {
    String name;
    String gender;

    MyThread(String name,String gender){
        this.name = name;
        this.gender = gender;
    }

    public void run(){
        int i = 0;
        while(i<=200) {
            System.out.println(Thread.currentThread().getName()+"-----"+i);
            i++;
        }
    }
}

public class CreateThread2 {
    public static void main(String[] args) {
        //創建子類對象
        MyThread mt = new MyThread("malong","Male");
        //創建線程對象
        Thread th1 = new Thread(mt);
        Thread th2 = new Thread(mt);

        th1.start();
        th2.start();
        System.out.println("main thread over");
    }
}

這兩種創建線程的方法,無疑第二種(實現Runnable介面)要好一些,因為第一種創建方法繼承了Thread後就無法繼承其他父類。

3.線程相關的常用方法

Thread類中的方法:

  • isAlive():判斷線程是否還活著。活著的概念是指是否消亡了,對於運行態、就緒態、睡眠態的線程都是活著的狀態。
  • currentThread():返回值為Thread,返回當前線程對象。
  • getName():獲取當前線程的線程名稱。
  • setName():設置線程名稱。給線程命名還可以使用構造方法Thread(String thread_name)Thread(Runnable r,String thread_name)
  • getPriority():獲取線程優先順序。優先順序範圍值為1-10(預設值為5),相鄰值之間的差距對cpu調度的影響很小。一般使用3個欄位MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY分別表示1、5、10三個優先順序,這三個優先順序可較大地區分cpu的調度。
  • setPriority():設置線程優先順序。
  • run():封裝的是線程開啟後要執行的任務代碼。如果run()中沒有任何代碼,則線程不做任何事情。
  • start():開啟線程並讓線程開始執行run()中的任務。
  • toString():返回線程的名稱、優先順序和線程組。
  • sleep(long millis):讓線程睡眠多少毫秒。
  • join(t1):將線程t1合併到當前線程,並等待線程t1執行完畢後才繼續執行當前線程。即讓t1線程強制插隊到當前線程的前面並等待t1完成。
  • yield():將當前正在執行的線程退讓出去,以讓就緒隊列中的其他線程有更大的幾率被cpu調度。即強制自己放棄cpu,並將自己放入就緒隊列。由於自己也在就緒隊列中,所以即使此刻自己放棄了cpu,下一次還是可能會立即被cpu選中調度。但畢竟給了機會給其它就緒態線程,所以其他就緒態線程被選中的幾率要更大一些。

Object類中的方法:

  • wait():線程進入某個線程池中併進入睡眠態。等待notify()或notifyAll()的喚醒。
  • notify():從某個線程池中隨機喚醒一個睡眠態的線程。
  • notifyAll():喚醒某個線程池中所有的睡眠態線程。

這裡的某個線程池是由鎖對象決定的。持有相同鎖對象的線程屬於同一個線程池。見後文。

一般來說,wait()和喚醒的notify()或notifyAll()是成對出現的,否則很容易出現死鎖。

sleep()和wait()的區別:(1)所屬類不同:sleep()在Thread類中,wait()則是在Object中;(2)sleep()可以指定睡眠時間,wait()雖然也可以指定睡眠時間,但大多數時候都不會去指定;(3)sleep()不會拋異常,而wait()會拋異常;(4)sleep()可以在任何地方使用,而wait()必須在同步代碼塊或同步函數中使用;(5)最大的區別是sleep()睡眠時不會釋放鎖,不會進入特定的線程池,在睡眠時間結束後自動蘇醒並繼續往下執行任務,而wait()睡眠時會釋放鎖,進入線程池,等待notify()或notifyAll()的喚醒。

java.util.concurrent.locks包中的類和它們的方法:

  • Lock類中:

    • lock():獲取鎖(互斥鎖)。
    • unlock():釋放鎖。
    • newCondition():創建關聯此lock對象的Condition對象。
  • Condition類中:

    • await():和wait()一樣。
    • signal():和notify()一樣。
    • signalAll():和notifyAll()一樣。

4.多線程安全問題和線程同步

4.1 多線程安全問題

線程安全問題是指多線程同時執行時,對同一資源的併發操作會導致資源數據的混亂。

例如下麵是用多個線程(視窗)售票的代碼。

class Ticket implements Runnable {
    private int num;    //票的數量

    Ticket(int num){
        this.num = num;
    }

    //售票
    public void sale() {
        if(num>0) {
            num--;
            System.out.println(Thread.currentThread().getName()+"-------"+remain());
        }
    }

    //獲取剩餘票數
    public int remain() {
        return num;
    }

    public void run(){
        while(true) {
            sale();
        }
    }
}

public class ConcurrentDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket(100);
        //創建多個線程對象
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);

        //開啟多個線程使其執行任務
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

執行結果大致如下:

以上代碼的執行過程大致如下圖:

共開啟了4個線程執行任務(不考慮main主線程),每一個線程都有4個任務:

  • ①判斷if條件if(num>0);
  • ②票數自減num--;
  • ③獲取剩餘票數return num;
  • ④列印返回的num數量System.out.println(Thread.currentThread().getName()+"-------"+remain())

這四個任務的共同點也是關鍵點在於它們都操作同一個資源Ticket對象中的num,這是多線程出現安全問題的本質,也是分析多線程執行過程的切入點

當main線程開啟t1-t4這4個線程時,它們首先進入就緒隊列等待被CPU隨機選中。(1).假如t1被先選中,分配的時間片執行到任務②就結束了,於是t1進入就緒隊列等待被CPU隨機選中,此時票數num自減後為99;(2).當t3被CPU選中時,t3所讀取到的num也為99,假如t3分配到的時間片在執行到任務②也結束了,此時票數num自減後為98;(3).同理t2被選中執行到任務②結束後,num為97;(4).此時t3又被選中了,於是可以執行任務③,甚至是任務④,假設執行完任務④時間片才結束,於是t3的列印語句列印出來的num結果為97;(5).t1又被選中了,於是任務④列印出來的num也為97。

顯然,上面的代碼有幾個問題:(1)有些票沒有賣出去了但是沒有記錄;(2)有的票重覆賣了。這就是線程安全問題。

4.2 線程同步

java中解決線程安全問題的方法是使用互斥鎖,也可稱之為"同步"。解決思路如下:

(1).為待執行的任務設定給定一把鎖,擁有相同鎖對象的線程在wait()時會進入同一個線程池睡眠。
(2).線程在執行這個設了鎖的任務時,首先判斷鎖是否空閑(即鎖處於釋放狀態),如果空閑則去持有這把鎖,只有持有這把鎖的線程才能執行這個任務。即使時間片到了,它也不是釋放鎖,只有wait()或線程結束時才會安全地釋放鎖。
(3).這樣一來,鎖被某個線程持有時,其他線程在鎖判斷後就繼續會線程池睡眠去了(或就緒隊列)。最終導致的結果是,(設計合理的情況下)某個線程一定完整地執行完一個任務,其他線程才有機會去持有鎖並執行任務。

換句話說,使用同步線程,可以保證線程執行的任務具有原子性,只要某個同步任務開始執行了就一定執行結束,且不允許其他線程參與。

讓線程同步的方式有兩種,一種是使用synchronized(){}代碼塊,一種是使用synchronized關鍵字修飾待保證同步的方法。

class Ticket implements Runnable {
    private int num;    //初始化票的數量
    private Object obj = new Object();

    Ticket(int num){
        this.num = num;
    }

    //售票
    public void sale() {
        synchronized(obj) {   //使用同步代碼塊封裝需要保證原子性的代碼
            if(num>0) {
                num--;
                System.out.println(Thread.currentThread().getName()+"-------"+remain());
            }
        }
    }

    //獲取剩餘票數
    public int remain() {
        return num;
    }

    public void run(){
        while(true) {
            sale();
        }
    }
}
class Ticket implements Runnable {
    private int num;    //初始化票的數量

    Ticket(int num){
        this.num = num;
    }

    public synchronized void sale() {  //使用synchronized關鍵字,方法變為同步方法
        if(num>0) {
            num--;
            System.out.println(Thread.currentThread().getName()+"-------"+remain());
        }
    }

    //獲取剩餘票數
    public int remain() {
        return num;
    }

    public void run(){
        while(true) {
            sale();
        }
    }
}

使用同步之後,if(num>0)num--return numprint(num)這4個任務就強制具有原子性。某個線程只要開始執行了if語句,它就一定會繼續執行直到執行完print(num),才算完成了一整個任務。只有完成了一整個任務,線程才會釋放鎖(當然,也可能繼續判斷while(true)併進入下一個迴圈)。

4.3 同步代碼塊和同步函數的區別以及鎖是什麼

前面的示例中,同步代碼塊synchronized(obj){}中傳遞了一個obj的Object對象,這個obj可以是任意一個對象的引用,這些引用傳遞給代碼塊的作用是為了標識這個同步任務所屬的鎖。

synchronized函數的本質其實是使用了this作為這個同步函數的鎖標識,this代表的是當前對象的引用。但如果同步函數是靜態的,即使用了static修飾,則此時this還沒出現,它使用的鎖是"類名.class"這個位元組碼文件對象,對於java來說,這也是一個對象,而且一個類中一定有這個對象。

使用相同的鎖之間會互斥,但不同鎖之間則沒有任何影響。因此,要保證任務同步(原子性),這些任務所關聯的鎖必須相同。也因此,如果有多個同步任務(各自保證自己的同步性),就一定不能都使用同步函數。

例如下麵的例子中,寫了兩個相同的sale()方法,並且使用了flag標記讓不同線程能執行這兩個同步任務。如果出現了多線程安全問題,則表明synchronized函數和同步代碼塊使用的是不同對象鎖。如果將同步代碼塊中的對象改為this後不出現多線程安全問題,則表明同步函數使用的是this對象。如果為sale2()加上靜態修飾static,則將obj替換為"Ticket.class"來測試。

class Ticket implements Runnable {
    private int num;    //初始化票的數量
    boolean flag = true;
    private Object obj = new Object();

    Ticket(int num){
        this.num = num;
    }

    //售票
    public void sale1() {
        synchronized(obj) {  //使用的是obj標識鎖
            if(num>0) {
                num--;
                try{Thread.sleep(1);} catch (InterruptedException i){}  //為了確保num--和println()分開,加上sleep
                System.out.println(Thread.currentThread().getName()+"===sale1==="+remain());
            }
        }
    }

    public synchronized void sale2() {   //使用this標識鎖
        if(num>0) {
            num--;
            try{Thread.sleep(1);} catch (InterruptedException i){}
            System.out.println(Thread.currentThread().getName()+"===sale2==========="+remain());
        }
    }

    //獲取剩餘票數
    public int remain() {
        return num;
    }

    public void run(){
        if(flag){
            while(true) {
                sale1();
            }
        } else {
            while(true) {
                sale2();
            }
        }
    }
}

public class Mytest {
    public static void main(String[] args) {
        Ticket t = new Ticket(200);
        //創建多個線程對象
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        //開啟多個線程使其執行任務
        t1.start();
        try{Thread.sleep(1);} catch (InterruptedException i){}
        t.flag = false;
        t2.start();
    }
}

以下是執行結果中的一小片段,出現了多線程安全問題。而如果將同步代碼塊中的obj改為this,則不會出現多線程安全問題。

Thread-0===sale1===197
Thread-1===sale2===========197
Thread-0===sale1===195
Thread-1===sale2===========195
Thread-1===sale2===========193
Thread-0===sale1===193
Thread-0===sale1===191
Thread-1===sale2===========191

4.4 單例懶漢模式的多線程安全問題

單例餓漢式:

class Single {
    private static final Single s = new Single();
    private Single(){};
    public static Single getInstance() {
        return s;
    }
}

單例懶漢式:

class Single {
    private static Single s = null;
    private Single(){};
    public static getInstance(){
        if(s==null) {
            s = new Single();
        }
        return s;
    }
}

當多線程操作單例餓漢式和懶漢式對象的資源時,是否有多線程安全問題?

class Demo implements Runnable {
    public void run(){
        Single.getInstance();
    }
}

以上面的代碼為例。當多線程分別被CPU調度時,餓漢式中的getInstance()返回的s,s是final屬性修飾的,因此隨便哪個線程訪問都是固定不變的。而懶漢式則隨著不同線程的來臨,不斷new Single(),也就是說各個線程獲取到的對象s是不同的,存在多線程安全問題。

只需使用同步就可以解決懶漢式的多線程安全問題。例如使用同步方法。

class Single {
    private static Single s = null;
    private Single(){};
    public static synchronized getInstance(){
        if (s == null){
            s = new Single();
        }
        return s;
    }
}

這樣一來,每個線程來執行這個任務時,都將先判斷Single.class這個對象標識的鎖是否已經被其他線程持有。雖然解決了問題,但因為每個線程都額外地判斷一次鎖,導致效率有所下降。可以採用下麵的雙重判斷來解決這個效率降低問題。

class Single {
    private static Single s = null;
    private Single(){};
    public static getInstance(){
        if (s == null) {
            synchronized(Single.class){
                if (s == null){
                    s = new Single();
                }
                return s;
            }
        }
    }
}

這樣一來,當第一個線程執行這個任務時,將判斷s==null為true,於是執行同步代碼塊並持有鎖,保證任務的原子性。而且,即使在最初判斷s==null後切換到其他線程了,也沒有關係,因為總有一個線程會執行到同步代碼塊並持有鎖,只要持有鎖了就一定執行s= new Single(),在這之後,所有的線程在第一階段的"s==null"判斷都為false,從而提高效率。其實,雙重判斷的同步懶漢式的判斷次數和餓漢式的判斷次數幾乎相等。

5.死鎖(DeadLock)

最典型的死鎖是僵局問題,A等B,B等A,誰都不釋放,造成僵局,最後兩個線程都無法執行下去。

例如下麵的代碼示例,sale1()中,obj鎖需要持有this鎖才能完成任務整體,而sale2()中,this鎖需要持有obj鎖才能完成任務整體。當兩個線程都開始執行任務後,就開始產生死鎖問題。

class Ticket implements Runnable {
    private int num;    
    boolean flag = true;
    private Object obj = new Object();

    Ticket(int num){
        this.num = num;
    }


    public void sale1() {
        synchronized(obj) {   //obj鎖
            sale2();          //this鎖
        }
    }

    public synchronized void sale2() {   //this鎖
        synchronized(obj){               //obj鎖
            if(num>0) {
                num--;
                try{Thread.sleep(1);} catch (InterruptedException i){}
                System.out.println(Thread.currentThread().getName()+"========="+remain());
            }
        }
    }

    //獲取剩餘票數
    public int remain() {
        return num;
    }

    public void run(){
        if(flag){
            while(true) {
                sale1();
            }
        } else {
            while(true) {
                sale2();
            }
        }
    }
}

public class DeadLockDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket(200);
        //創建多個線程對象
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        //開啟多個線程使其執行任務
        t1.start();
        try{Thread.sleep(1);} catch (InterruptedException i){}
        t.flag = false;
        t2.start();
    }
}

為了避免死鎖,儘量不要在同步中嵌套同步,因為這樣很容易造成死鎖。

 

註:若您覺得這篇文章還不錯請點擊右下角推薦,您的支持能激發作者更大的寫作熱情,非常感謝!


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

-Advertisement-
Play Games
更多相關文章
  • ajaxJson('POST', url, JSON.stringify(param), function(err, result){ //result or rsp // if(SUCCESS){ //....... }else{ //....... } }); ————————————————— ...
  • 本文地址:http://www.cnblogs.com/aiweixiao/p/8202365.html 原文地址: 歡迎關註微信公眾號 程式員的文娛情懷 一、主要內容: 1️⃣php擴展的概念和底層實現 2️⃣編寫一個php擴展的步驟 3️⃣php底層,Zend 引擎API的介紹 ,HashTab ...
  • A代碼編輯器,線上模版編輯,仿開發工具編輯器,pdf線上預覽,文件轉換編碼B 集成代碼生成器 [正反雙向](單表、主表、明細表、樹形表,快速開發利器)+快速表單構建器freemaker模版技術 ,0個代碼不用寫,生成完整的一個模塊,帶頁面、建表sql腳本,處理類,service等完整模塊C 集成阿裡 ...
  • nuget包也要自動化部署了,想想確實挺好,在實施過程中我們要解決的問題有版本自動控制,nuget自動打包,nuget自動上傳到服務端等。 一 參數化構建 二 環境變數的k/v參數,存儲類庫的初始版本,當根目錄version.txt生成後,這個k/v就不需要了 三 這個構建跳轉到哪台節點伺服器 四 ...
  • 架構師寫的軟體指南 《 程式員必讀之軟體架構 》筆記 語境 意圖 這個軟體項目/產品/系統是關於什麼的? 構建的是什麼? 它如何融入現有環境? 誰在使用? 功能性概覽 意圖 系統實際上做什麼? 哪些特性、功能、用例、用戶故事是重要的?原因? 重要用戶是誰?系統如何滿足他們的需求? 上述已經用於塑造和 ...
  • 高可用的兩大目的:數據備份,數據分片 1、FastDFS安裝配置 先配置一臺,將其中的配置文件打包,下載,然後配置其他機器時只需要解壓即可, 打包命令 然後下載,上傳到其他機器相對應的/etc目錄下 將其他機器中的fdfs文件夾刪除 解壓上傳文件 2、 伺服器列表 伺服器IP 組 角色 192.16 ...
  • 預覽數據 這次我們使用 Artworks.csv ,我們選取 100 行數據來完成本次內容。具體步驟: DataFrame 是 Pandas 內置的數據展示的結構,展示速度很快,通過 DataFrame 我們就可以快速的預覽和分析數據。代碼如下: 統計日期數據 我們仔細觀察一下 Date 列的數據, ...
  • 原文地址: 本文地址:http://www.cnblogs.com/aiweixiao/p/8202360.html Original 2018-01-02 關註 微信公眾號 程式員的文娛情懷 1.概述 常見的排序演算法,雖然很基礎,但是很見功力,如果能思路清晰,很快寫出來各個演算法的代碼實現,還是需要 ...
一周排行
    -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 ...