Java基礎——多線程

来源:https://www.cnblogs.com/yanghanwen/archive/2020/02/20/12337879.html
-Advertisement-
Play Games

Java基礎 多線程 多個線程一起做同一件事情,縮短時間,提升效率 提高資源利用率 加快程式響應,提升用戶體驗 創建線程 1. 繼承Thread類 步驟 繼承Thread類,重寫run方法 調用的時候,直接new一個對象,然後調start()方法啟動線程 特點 由於是繼承方式,所以不建議使用,因為J ...


Java基礎-多線程

多個線程一起做同一件事情,縮短時間,提升效率
提高資源利用率
加快程式響應,提升用戶體驗

創建線程

1. 繼承Thread類

  • 步驟

    • 繼承Thread類,重寫run方法

    • 調用的時候,直接new一個對象,然後調start()方法啟動線程

  • 特點

    • 由於是繼承方式,所以不建議使用,因為Java是單繼承的,不夠靈活

    • Thread類本質也是實現Runnable介面(public class Thread implements Runnable)

  

2. 實現Runnable介面

  • 步驟

    • 實現Runnable介面,重寫run()方法

    • 創建Runnable實現類的實例,並用這個實例作為Thread的target來創建Thread對象

    • 調用Thread類實例對象的start()方法啟動線程

  • 特點

    • 只是實現,保留了繼承的其他類的能力

    • 如果需要訪問當前線程,必須使用Thread.currentThread()方法

  

3. 實現 Callable介面

  • 步驟

    • 實現Callable介面,重寫call()方法

    • 創建Callable實現類的實例,使用FutureTask類來包裝Callable對象

    • 並用FutureTask實例作為Thread的target來創建Thread對象

    • 調用Thread類實例對象的start()方法啟動線程

    • 調用FutureTask類實例對象的get()方法獲取非同步返回值

  • 特點

    • call方法可以拋出異常

    • 只是實現,保留了繼承的其他類的能力

    • 如果需要訪問當前線程,必須使用Thread.currentThread()方法

    • 通過FutureTask對象可以瞭解任務執行情況,可取消任務的執行,還可獲取執行結果

  

4. 匿名內部類實現

  • 說明

    • 本質還是前面的方法,只是使用了匿名內部類來實現,簡化代碼

    • Callable介面之所以把FutureTask類的實例化寫出來,是因為需要通過task對象獲取返回值

  

參數傳遞

1. 通過構造方法傳遞數據

  通過前面的學習,可以看到,不管何種創建對象的方式,都需要新建立實例,所以我們可以通過構造函數傳入參數,並將傳入的數據使用類變數保存起來

  • 特點

    • 線上程運行之前,數據就已經傳入了

    • 使用構造參數,當參數較多時,使用不方便

    • 不同參數條件需要不同的構造方法,使得構造方法較多

2. 通過變數和方法傳遞數據

  線上程類裡面定義一些列的變數,然後定義set方法,在新建實例之後,調用set方法傳遞參數

  • 特點

    • 在參數較多時使用方便,按需傳遞參數

3. 通過回調函數傳遞數據

  使用線程方法自己產生的變數值作為參數,去調取外部的方法,獲取返回數據的方式

  • 特點

    • 擁有獲取數據的主動權
線程同步

  要跨線程維護正確的可見性,只要在幾個線程之間共用非 final 變數,就必須使用線程同步

1. ThreadLocal

  ThreadLocal利用空間換時間,通過為每個線程提供一個獨立的變數副本,避免了資源等待,解決了變數併發訪問的衝突問題,提高了併發量。實現了線程間的數據隔離,但是線程間無法共用同一個資源

public class StudyThread {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        SyncTest syncTest = new SyncTest();
        ConcurrentHashMap<String, String> testConMap = new ConcurrentHashMap<>();
        for (int i = 0; i < 10; i++) {
            ThreadTest2 threadTest2 = new ThreadTest2();
            threadTest2.setSyncTest(syncTest);
            Thread threadTest = new Thread(threadTest2);
            threadTest.start();
        }
    }
}

//實現Runnable
class ThreadTest2 implements Runnable {
    private SyncTest syncTest;

    public void setSyncTest(SyncTest syncTest) {
        this.syncTest = syncTest;
    }

    @Override
    public void run() {
        syncTest.threadLocalTest(Thread.currentThread().getName());
    }
}

class SyncTest {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();

    public void threadLocalTest(String name) {
        try {
            System.out.println(name + "進入了threadLocal方法!");
            threadLocal.set(name);
            Thread.currentThread().sleep(100);
            System.out.println(threadLocal.get() + "離開了threadLocal方法!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2. synchronized

  不管synchronized是用來修飾方法,還是修飾代碼塊,其本質都是鎖定某一個對象。修飾方法時,鎖上的是調用這個方法的對象,即this;修飾代碼塊時,鎖上的是括弧里的那個對象。每一個Java對象都有一個內置鎖,訪問synchronized代碼塊或synchronized方法的時候,線程都需要首先獲取到對象關聯的內置鎖,對於static方法,線程獲取的是類對象的內置鎖。

  • 特點

    • 鎖的對象越小越好
public class StudyThread {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        SyncTest syncTest = new SyncTest();
        ConcurrentHashMap<String, String> testConMap = new ConcurrentHashMap<>();
        for (int i = 0; i < 10; i++) {
            ThreadTest2 threadTest2 = new ThreadTest2();
            threadTest2.setSyncTest(syncTest);
            threadTest2.setTestConMap(testConMap);
            Thread threadTest = new Thread(threadTest2);
            threadTest.start();
        }
    }

}
//實現Runnable
class ThreadTest2 implements Runnable {
    private ConcurrentHashMap<String, String> testConMap;
    private SyncTest syncTest;

    public void setTestConMap(ConcurrentHashMap<String, String> testConMap) {
        this.testConMap = testConMap;
    }

    public void setSyncTest(SyncTest syncTest) {
        this.syncTest = syncTest;
    }

    @Override
    public void run() {
        //三個方法需要單獨測試,因為testConMap會相互影響

        //測試同步方法,鎖住的對象是syncTest
        //syncTest.testSyncMethod(testConMap,Thread.currentThread().getName());
        //測試同步代碼塊,鎖住的對象是testConMap
        //syncTest.testSyncObject(testConMap, Thread.currentThread().getName());
        //測試沒有鎖時執行請求是多麼的混亂!!!
        //syncTest.testNoneSyncObject(testConMap, Thread.currentThread().getName());
    }
}
//同步測試方法類
class SyncTest {
    public synchronized void testSyncMethod(ConcurrentHashMap<String, String> testConMap, String name) {
        try {
            System.out.println(name + "進入了同步方法!");
            testConMap.put("name", name);
            Thread.currentThread().sleep(10);
            System.out.println(testConMap.get("name") + "離開了同步方法!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void testSyncObject(ConcurrentHashMap<String, String> testConMap, String name) {
        synchronized (testConMap) {
            try {
                System.out.println(name + "進入了同步代碼塊!");
                testConMap.put("name", name);
                Thread.currentThread().sleep(10);
                System.out.println(testConMap.get("name") + "離開了同步代碼塊!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void testNoneSyncObject(ConcurrentHashMap<String, String> testConMap, String name) {
        try {
            System.out.println(name + "進入了無人管轄區域!");
            testConMap.put("name", name);
            Thread.currentThread().sleep(10);
            System.out.println(testConMap.get("name") + "離開了無人管轄區域!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3. volatile

  • 特點

    • 保證可見性,有序性,不保證原子性

    • 它會強制將對緩存的修改操作立即寫入主存

    • volatile不適合複合操作(對變數的寫操作不依賴於當前值),否則需要保證只有單一線程能夠修改變數的值

    • 使用volatile關鍵字,可以禁止指令重排序(單例雙重檢查鎖)

public class StudyThread {
    static int v = 1;//volatile能夠保證變數的可見性

    public static void main(String[] args) {

        //改動線程
        new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    v++;//確保只有一個線程修改變數值
                    try {
                        Thread.currentThread().sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        //檢測線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                int old = 0;
                while (old < 11) {
                    if (old != v) {
                        old = v;
                        System.out.println("檢測線程:v的值變動為" + old);
                    }
                }
            }
        }).start();
    }
}

4. ReentrantLock

  • 說明
    • 目前ReentrantLock和synchronized性能上沒有什麼差別

    • ReentrantLock需要手動加鎖和解鎖,且解鎖的操作儘量要放在finally代碼塊中,保證線程正確釋放鎖

    • ReentrantLock可以實現公平鎖,在鎖上等待時間最長的線程將獲得鎖的使用權,性能沒有非公平鎖性能好

    • ReentrantLock提供了一個可以響應中斷的獲取鎖的方法lockInterruptibly(),可以用來解決死鎖問題

    • ReentrantLock還提供了獲取鎖限時等待的方法tryLock(),使用該方法配合失敗重試機制來更好的解決死鎖問題

    • ReentrantLock結合Condition介面可以實現等待通知機制

public class StudyThread {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        //三段代碼逐一測試
        
        //測試tryLock
        Thread threadTest00 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while(!lock.tryLock()){
                        System.out.println(Thread.currentThread().getName() + "沒有拿到鎖,繼續等待!");
                        Thread.sleep(50);
                    }
                    System.out.println(Thread.currentThread().getName() + "進入了lock代碼塊!");
                    Thread.currentThread().sleep(300);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(Thread.currentThread().getName() + "準備釋放鎖,並離開lock代碼塊!");
                    lock.unlock();
                }
            }
        });
        threadTest00.start();
        Thread threadTest01 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while(!lock.tryLock()){
                        System.out.println(Thread.currentThread().getName() + "沒有拿到鎖,繼續等待!");
                        Thread.sleep(50);
                    }
                    System.out.println(Thread.currentThread().getName() + "進入了lock代碼塊!");
                    Thread.currentThread().sleep(300);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(Thread.currentThread().getName() + "準備釋放鎖,並離開lock代碼塊!");
                    lock.unlock();
                }
            }
        });
        threadTest01.start();

        //測試中斷鎖
        Thread threadTest02 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + "進入了lock代碼塊!");
                    Thread.currentThread().sleep(2000);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(Thread.currentThread().getName() + "準備釋放鎖,並離開lock代碼塊!");
                    lock.unlock();
                }
            }
        });
        threadTest02.start();
        Thread threadTest03 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + "進入了lock代碼塊!");
                    Thread.currentThread().sleep(2000);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(Thread.currentThread().getName() + "準備釋放鎖,並離開lock代碼塊!");
                    lock.unlock();
                }
            }
        });
        threadTest03.start();
        Thread.currentThread().sleep(20);
        threadTest02.interrupt();

        //測試condition
        Thread threadTest04 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + "進入了lock代碼塊,等待通知!");
                    condition.await();
                    System.out.println(Thread.currentThread().getName() + "收到通知,繼續執行!");
                    Thread.currentThread().sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(Thread.currentThread().getName() + "準備釋放鎖,並離開lock代碼塊!");
                    lock.unlock();
                }
            }
        });
        threadTest04.start();
        Thread threadTest05 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + "進入了lock代碼塊!");
                    Thread.currentThread().sleep(1000);
                    condition.signal();
                    System.out.println(Thread.currentThread().getName() + "發出通知!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println(Thread.currentThread().getName() + "準備釋放鎖,並離開lock代碼塊!");
                    lock.unlock();
                }
            }
        });
        threadTest05.start();
    }

}

5. 線程安全的類

  Java中很多類說的線程安全指的是,它的每個方法單獨調用(即原子操作)都是線程安全的,但是代碼總體的互斥性並不受控制

  • 線程安全的類有以下幾類

    • Concurrentxxx

    • ThreadPoolExecutor

    • BlockingQueue和BlockingDeque

    • 原子類Atomicxxx—包裝類的線程安全類

    • CopyOnWriteArrayList和CopyOnWriteArraySet

    • 通過synchronized 關鍵字給方法加上內置鎖來實現線程安全:Timer,TimerTask,Vector,Stack,HashTable,StringBuffer

    • Collections中的synchronizedCollection(Collection c)方法可將一個集合變為線程安全:

      Map m=Collections.synchronizedMap(new HashMap());

線程池

線程池只能放入實現Runable或callable類線程,不能直接放入繼承Thread的類

1. Executors線程池的實現

  • 要點

    • 可能導致資源耗盡,OOM問題出現

    • 線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式(阿裡巴巴java開發)

public class StudyThread {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //創建一個線程池,該線程池重用固定數量的從共用無界隊列中運行的線程
        //ExecutorService threadPool = Executors.newFixedThreadPool(20);
        //創建一個維護足夠的線程以支持給定的並行級別的線程池,線程的實際數量可以動態增長和收縮,工作竊取池不保證執行提交的任務的順序
        //ExecutorService threadPool = Executors.newWorkStealingPool(8);
        //創建一個使用從無界隊列運行的單個工作線程的執行程式。
        //ExecutorService threadPool = Executors.newSingleThreadExecutor();
        //創建一個根據需要創建新線程的線程池,但在可用時將重新使用以前構造的線程。如果沒有可用的線程,將創建一個新的線程並將其添加到該池中。未使用六十秒的線程將被終止並從緩存中刪除
        ExecutorService threadPool = Executors.newCachedThreadPool();
        //放入Runnable類線程
        for (int i = 0; i < 10; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("線程名:" + Thread.currentThread().getName());
                }
            });
        }
        //Thread.currentThread().sleep(1000);
        //放入Callable類線程
        List<Future<String>> futures = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Future<String> future = threadPool.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    System.out.println("線程名:" + Thread.currentThread().getName());
                    return Thread.currentThread().getName();
                }
            });
            futures.add(future);
        }
        threadPool.shutdown();
        for (Future future : futures) {
            System.out.println(future.get());
        }
    }
}

2. ThreadPoolExecutor創建線程池

  

  • 要點

    • 線程池空閑大小和最大線程數根據實際情況確定

    • keepAliveTime一般設置為0

    • unit一般設置為TimeUnit.SECONDS(其他的也行,反正是0)

    • 任務隊列需要指定大小,不要使用無界隊列,容易造成OOM-> new ArrayBlockingQueue<>(512)

    • ThreadFactory threadFactory使用系統預設的

    • 拒絕策略:

      • AbortPolicy:拋出RejectedExecutionException(該異常是非受檢異常,要記得捕獲)

      • DiscardPolicy:什麼也不做,直接忽略

      • DiscardOldestPolicy:丟棄執行隊列中最老的任務,嘗試為當前提交的任務騰出位置

      • CallerRunsPolicy:直接由提交任務者執行這個任務

public class StudyThread {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        int poolSize = Runtime.getRuntime().availableProcessors() * 2;
        BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(512);
        RejectedExecutionHandler policy = new ThreadPoolExecutor.DiscardPolicy();
        ExecutorService executorService = new ThreadPoolExecutor(poolSize, poolSize,
                0, TimeUnit.SECONDS,
                queue,
                policy);
        //放入Runnable類線程
        for (int i = 0; i < 10; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("線程名:" + Thread.currentThread().getName());
                }
            });
        }

        //放入Callable類線程
        List<Future<String>> futures = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Future<String> future = executorService.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    System.out.println("線程名:" + Thread.currentThread().getName());
                    return Thread.currentThread().getName();
                }
            });
            futures.add(future);
        }
        for (Future future:futures) {
            System.out.println(future.get());
        }

        //放入Callable類線程
        //使用CompletionService簡化獲取結果的操作,執行完一個任務,獲取一個結果,結果順序和執行順序相同
        CompletionService<String> ecs = new ExecutorCompletionService<String>(executorService);
        for (int i = 0; i < 10; i++) {
            Future<String> future = ecs.submit(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    System.out.println("線程名:" + Thread.currentThread().getName());
                    return Thread.currentThread().getName();
                }
            });
        }
        for (int i = 0; i < 10; i++) {
            System.out.println(ecs.take().get());
        }
    }
}


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

-Advertisement-
Play Games
更多相關文章
  • jQuery是JS的工具庫,對原生JS中的DOM操作、事件處理、包括數據處理和Ajax技術等進行封裝,使用 . 鏈式寫法,提供更完善,更便捷的方法。 再使用jquery之前,我們需要先引入jquery文件,才能使用jquery語法,導入jQ文件的方法有兩種。 從 jquery.com 下載 jQue ...
  • 1. 滿足高可用,唯一的辦法是副本鏡像方案。 2. 滿足可伸縮,唯一的辦法是分片方案。 3. 業界實踐證明,先將全量數據分片,再對分片做多副本鏡像這樣的組合方案,在資源利用率、可用性、伸縮性各方面都很均衡。如HDFS、RedisCluster、RocketMQ等都是採用這種組合方案來存儲數據的。 4 ...
  • 面向對象三大特性 封裝: Encapsulation是指一種將抽象性函式介面的實現細節部份包裝、隱藏起來的方法。封裝可以被認為是一個保護屏障,防止該類的代碼和數據被外部類定義的代碼隨意訪問。要訪問該類的代碼和數據,必須通過嚴格的介面控制。封裝最主要的功能在於我們能修改自己的實現代碼,而不用修改那些調 ...
  • 一、List集合 1.List集合存儲元素的特點: (1)有序(List集合中存儲有下標)​:存進去是這樣的順序,取出來還是按照這個順序取出​。 (2)可重覆 2.深入ListJ集合 ArrayList集合底層是數組,數組​是有下標的;所以ArrayList集合有很多自己的特性​;ArrayList ...
  • 開發環境: Windows操作系統開發工具: MyEclipse+Jdk+Tomcat+MySQL資料庫運行效果圖 源碼及原文鏈接:https://javadao.xyz/forum.php?mod=viewthread&tid=45 ...
  • 開發環境: Windows操作系統開發工具: Myeclipse+Jdk+Tomcat+MySQL資料庫運行效果圖 源碼及原文鏈接:https://javadao.xyz/forum.php?mod=viewthread&tid=44 ...
  • 原創聲明 本文作者:黃小斜 轉載請務必在文章開頭註明出處和作者。 本文思維導圖 什麼是演算法 上回我們有一篇文章,講述了作為一個新人程式員,如何學習數據結構這門課程,其實呢,數據結構和演算法是息息相關的,為什麼這麼說呢,因為數據結構本身只是一個載體,而在數據結構之上產生作用和輸出價值的東西其實是演算法。 ...
  • 關註公眾號:CoderBuff,回覆“redis”獲取《Redis5.x入門教程》完整版PDF。 《Redis5.x入門教程》目錄 "第一章 · 準備工作" "第二章 · 數據類型" 第三章 · ​命令 第四章 ​· 配置 第五章 · Java客戶端(上) 第六章 · 事務 第七章 · 分散式鎖 第 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...