1. 程式人生 > 其它 >【Java】MultiThread 多執行緒 Re01

【Java】MultiThread 多執行緒 Re01

學習參考:

https://www.bilibili.com/video/BV1ut411T7Yg

一、執行緒建立的四種方式:

1、整合執行緒類

    /**
     * 使用匿名內部類實現子執行緒類,重寫Run方法
     */
    static class CustomThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("custom subThread2 is running " +  new
Date().getTime()); } } }

執行

Thread thread2 = new CustomThread();
thread.start();

2、實現Runnable介面

    /**
     * 或者重寫Runnable介面完成
     */
    static class RunnableImpl implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) System.out.println("custom RunnableImpl is running " +  new
Date().getTime()); } }

執行

Thread thread2 = new Thread(new RunnableImpl(), "custom RunnableThread");
thread2.start();

3、實現Callable介面

    /**
     * 實現Callable介面方式
     */
    static class CustomCallable implements Callable<String> {
        @Override
        public String call() {
            
for (int i = 0; i < 10; i++) System.out.println(Thread.currentThread().getName() + " execute" + new Date().getTime() + " loopCount " + i); return "CustomCallable execute finish"; } }

執行程式碼

Callable介面

        FutureTask<String> futureTask = new FutureTask<>(new CustomCallable());
        Thread thread = new Thread(futureTask, "futureTask");
        thread.start();
        for (int i = 0; i < 10; i++) System.out.println("main thread is running " +  System.currentTimeMillis());
        boolean stopFlag = false;
        if (stopFlag) {
            // 可以中斷執行
            futureTask.cancel(stopFlag);
        }
        // boolean cancelled = futureTask.isCancelled();
        try {
            // 判斷是否執行完成
            if (futureTask.isDone()) {

                // 如果執行完成,可以獲取Callable返回的結果
                String s = futureTask.get();
                System.out.println("thread execute finish, result => " + s);
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }

4、使用執行緒池,只需要提供Runnable介面的實現例項

        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
        fixedThreadPool.execute(() -> {
            for (int i = 0; i < 10; i++) System.out.println("custom Runnable-N-Impl is running " +  new Date().getTime());
        });

四種執行緒方式的總結:

介面 和 繼承Thread類比較:
- 介面適合多個相同的程式程式碼的執行緒 共享一個資源
- 介面避免了Java單繼承的侷限性
- 介面程式碼可以被多個執行緒共享,程式碼和執行緒是獨立的
- 執行緒池只能放入實現Runnable或者Callable介面的執行緒,不能直接放入繼承Thread的類

Java程式執行至少有幾個執行緒?

至少兩個執行緒,一個Main主執行緒,一個GC垃圾回收執行緒
3、Runnable 和 Callable介面的比較
相同點:
- 都是介面定義
- 都可以用來編寫多執行緒程式
- 都需要呼叫Thread.start()來啟動執行緒

不同點:
- Runnable是重寫run方法,該方法的返回型別是void,和Thread一樣,不能返回結果 - Callable是重寫call方法,該方法的返回型別通過泛型決定,預設Object 在FutureTask物件的get方法返回 - Callable允許使用FutureTask物件的cancel方法,取消執行,Runnable則沒有對應的方法可以取消 注意事項:   FutureTask物件的get方法會阻塞Main執行緒,直到獲取方法結果獲取
4、執行緒生命週期
- 建立階段 Thread物件已經被new分配記憶體資源
- 就緒階段 Thread物件呼叫start()方法 JVM為執行緒物件建立方法棧和程式計數器,等待執行緒排程器排程
- 執行階段 就緒狀態的執行緒獲取CPU資源 開始執行run方法
- 阻塞階段
  1、主動呼叫sleep方法 主動放棄CPU資源,該執行緒執行緒阻塞
  2、呼叫了阻塞式的IO方法 知道該方法返回之前該執行緒一直阻塞
  3、獲取同步鎖(同步監視器)但是該同步鎖被其他執行緒所持有
  4、執行緒正在等待某個通知 (呼叫了notify方法)
  5、呼叫執行緒掛起方法suspend(),該方法容易造成死鎖,避免使用
- 死亡階段
  1、run() 或 call() 方法執行完成,執行緒結束
  2、執行緒丟擲未捕獲的Exception或者Error
  3、呼叫該執行緒stop()方法結束該執行緒,但是容易早曾死鎖,避免使用

執行緒安全問題:

售票程式碼例項:

public class ThreadSecurity {
    public static final String TICKET_WINDOW_1 = "12306 TicketWindow1";
    public static final String TICKET_WINDOW_2 = "12306 TicketWindow2";
    public static final String TICKET_WINDOW_3 = "12306 TicketWindow3";

    static class Ticket implements Runnable {
        private int ticketTotal = 100;
        @Override
        public void run() {
            while (true) {
                if (ticketTotal > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (Exception exception) {
                        exception.printStackTrace();
                    }
                    String name = Thread.currentThread().getName();
                    System.out.println(name + " 剩餘票數 " + ticketTotal);
                    ticketTotal --;
                }
            }
        }
    }
/** * 執行緒安全問題 * 1、多個執行緒同事執行寫操作,需要考慮執行緒同步 * 2、對於全域性變數不需要寫入操作,僅讀取,該變數執行緒安全 * * - 程式碼執行過程,同一個變數值多個執行緒執行好幾回 * - 出現小於1的情況 */ public void threadProblem() { Ticket ticket = new Ticket(); Thread ticketThread1 = new Thread(ticket, TICKET_WINDOW_1); Thread ticketThread2 = new Thread(ticket, TICKET_WINDOW_2); Thread ticketThread3 = new Thread(ticket, TICKET_WINDOW_3); ticketThread1.start(); ticketThread2.start(); ticketThread3.start(); }

  
public static void main(String[] args) { new ThreadSecurity().threadProblem();
  }
}

執行後,會出現三個視窗都在售賣同一張票的情況

或者票已經賣完了,視窗任然在售賣的問題

二、解決執行緒安全的方式:

    /**
     * 執行緒安全問題的解決辦法:
     *  執行緒修改共享資源的時候其他執行緒必須等待當前執行的執行緒修改完逼同步之後,才能搶奪CPU資源執行對應的操作
     *  這樣保證資料的同步性,解決執行緒不安全的現象
     *
     * Java提供了7種執行緒同步機制
     *  1、同步程式碼塊
     *  2、同步方法
     *  3、同步鎖 ReenreantLock
     *  4、特殊域變數 volatile
     *  5、區域性變數  ThreadLocal
     *  6、阻塞佇列 LinkedBlockingQueue
     *  7、原子變數 Atomic
     */

1、使用同步程式碼塊來解決:

    static class TicketLocked implements Runnable {
        private int ticketTotal = 100;
        private Object lockKey = new Object();
        @Override
        public void run() {
            while (true) {
                // 使用同步程式碼塊 需要一個鎖物件,執行緒執行必須通過【開鎖】的方式執行程式碼塊內容,以保證變數寫入正確
                synchronized (lockKey) {
                    if (ticketTotal > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (Exception exception) {
                            exception.printStackTrace();
                        }
                        String name = Thread.currentThread().getName();
                        System.out.println(name + " 剩餘票數 " + ticketTotal);
                        ticketTotal --;
                    }
                }
            }
        }
    }

執行部分:

    public void threadSync() {
        TicketLocked ticket = new TicketLocked();
        Thread ticketThread1 = new Thread(ticket, TICKET_WINDOW_1);
        Thread ticketThread2 = new Thread(ticket, TICKET_WINDOW_2);
        Thread ticketThread3 = new Thread(ticket, TICKET_WINDOW_3);
        ticketThread1.start();
        ticketThread2.start();
        ticketThread3.start();
    }

2、使用同步方法解決

原理和同步程式碼塊的原理是一致的

鎖物件不再使用隨意變數,而是this當前物件

    static class TicketLocked2 implements Runnable {
        private int ticketTotal = 100;
        @Override
        public void run() {
            while (true) {
                saleTicket();
            }
        }

        /**
         * 使用 synchronized 修飾方法
         * 原理和 synchronized程式碼塊是一樣的
         * 只是同步鎖這個物件直接使用的是當前物件 this
         * 如果是static方法,則是這個類物件 this.getClass()
         */
        private synchronized void saleTicket() {
            if (ticketTotal > 0) {
                try {
                    Thread.sleep(100);
                } catch (Exception exception) {
                    exception.printStackTrace();
                }
                String name = Thread.currentThread().getName();
                System.out.println(name + " 剩餘票數 " + ticketTotal);
                ticketTotal --;
            }
        }
    }

執行部分

    public void threadSyncBySyncMethod() {
        TicketLocked2 ticket = new TicketLocked2();
        Thread ticketThread1 = new Thread(ticket, TICKET_WINDOW_1);
        Thread ticketThread2 = new Thread(ticket, TICKET_WINDOW_2);
        Thread ticketThread3 = new Thread(ticket, TICKET_WINDOW_3);
        ticketThread1.start();
        ticketThread2.start();
        ticketThread3.start();
    }

3、使用鎖物件實現

    static class TicketLocked3 implements Runnable {
        private int ticketTotal = 100;
        /**
         * 引數:是否公平
         *  true 公平鎖 多個執行緒公平擁有執行權
         *  false 獨佔鎖 當執行緒佔用時直到釋放,其他執行緒才能使用
         */
        private final Lock lock = new ReentrantLock(true);
        @Override
        public void run() {
            while (true) {
                // 上鎖和解鎖必須都要執行
                lock.lock();
                try {
                    if (ticketTotal > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (Exception exception) {
                            exception.printStackTrace();
                        }
                        String name = Thread.currentThread().getName();
                        System.out.println(name + " 剩餘票數 " + ticketTotal);
                        ticketTotal --;
                    }
                } catch (Exception exception) {
                    exception.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }

執行部分:

    /**
     * 使用同步鎖解決
     * 發現同步鎖執行緒執行是均等機會的,
     */
    public void threadSyncBySyncLock() {
        TicketLocked3 ticket = new TicketLocked3();
        Thread ticketThread1 = new Thread(ticket, TICKET_WINDOW_1);
        Thread ticketThread2 = new Thread(ticket, TICKET_WINDOW_2);
        Thread ticketThread3 = new Thread(ticket, TICKET_WINDOW_3);
        ticketThread1.start();
        ticketThread2.start();
        ticketThread3.start();
    }

安全問題的總結:

/**
 * 執行緒安全問題
 * 總結:
 *  synchronized & lock物件的區別:
 *      1、synchronized 關鍵字基於JVM層面上的控制, Lock鎖物件是程式編碼上的控制
 *      2、synchronized自動上鎖解鎖,Lock鎖物件需要手動編寫,且解鎖必須釋放(存在死鎖的風險), 同樣,使用Lock物件可以在編碼上靈活調整
 *      3、synchronized屬於獨佔鎖,可重入,不可中斷, Lock鎖可重入,可判斷 可公平
 *      4、Lock適用場景更為靈活,例如部分需要同步的程式碼,非全部程式碼都需要同步
 *
 */

三、死鎖問題:

/**
 * 死鎖問題
 *
 * 死鎖產生的條件
 * 1、互斥
 * 2、不可剝奪
 * 3、請求與保持關係
 * 4、迴圈等待條件
 *
 * 處理死鎖問題
 * 1、預防 通過限制條件破壞上述條件的產生
 * 2、避免
 * 3、檢查死鎖
 * 4、解除死鎖
 *
 */

死鎖案例:

package cn.cloud9.test.multithread;

/**
 * 死鎖問題
 *
 * 死鎖產生的條件
 * 1、互斥
 * 2、不可剝奪
 * 3、請求與保持關係
 * 4、迴圈等待條件
 *
 * 處理死鎖問題
 * 1、預防 通過限制條件破壞上述條件的產生
 * 2、避免
 * 3、檢查死鎖
 * 4、解除死鎖
 *
 */
public class DeadLock {

    private static class DeadRunnable implements Runnable {

        private boolean flag;
        // 是靜態的
        private static Object lockObj1 = new Object();
        private static Object lockObj2 = new Object();

        public DeadRunnable(boolean flag) {
            this.flag = flag;
        }

        @Override
        public void run() {
            if (flag) {
                // 執行緒1執行
                synchronized (lockObj1) {
                    System.out.println(Thread.currentThread().getName() + " 獲取當前資源lockObj1 開始請求lockObj2");
                    try {
                        Thread.sleep(1000);
                    } catch (Exception exception) {
                        exception.printStackTrace();
                    }
                    synchronized (lockObj2) {
                        System.out.println(Thread.currentThread().getName() + " 獲取當前資源lockObj2 & lockObj1");
                    }
                }
            } else {
                // 執行緒2執行
                synchronized (lockObj2) {
                    System.out.println(Thread.currentThread().getName() + " 獲取當前資源lockObj2 開始請求lockObj1");
                    try {
                        Thread.sleep(1000);
                    } catch (Exception exception) {
                        exception.printStackTrace();
                    }
                    synchronized (lockObj1) {
                        System.out.println(Thread.currentThread().getName() + " 獲取當前資源lockObj1 & lockObj2");
                    }
                }
            }
        }
    }
}

執行部分:

    public static void main(String[] args) {
        DeadRunnable deadRunnable1 = new DeadRunnable(true);
        DeadRunnable deadRunnable2 = new DeadRunnable(false);

        Thread thread1 = new Thread(deadRunnable1, "deadRunnable1");
        Thread thread2 = new Thread(deadRunnable2, "deadRunnable2");

        thread1.start();
        thread2.start();

    }

執行後的結果可以看到,鎖物件1和鎖物件2線上程1和2都上過鎖了,

執行到二次上鎖的程式碼塊的時候,鎖物件沒有釋放,則執行緒1和2都發生阻塞,程式既不停止也不結束執行