(1)線程系列 - 線程、多線程 相關概念 1. 線程

我們經常用的okhttp和rxjava等,都是基於線程進行封裝,我們從java最基礎上了解線程對於以後是有幫助的,那麼直接進入主題

相關概念

在說線程之前,我們先了解一下進程。

什麼是進程
我們平日裏打開的微信、簡書App,都是一個進程。
什麼是線程
線程是比進程更小的執行單位。一個程序只可以有一個進程,但這個進程可以包含多個線程
什麼是多線程
這些線程可以同時存在,同時運行,一個進程可能包含多個同時執行的線程
併發和並行
併發:併發是指一個處理器同時處理多個任務
並行:並行是指多個處理器或者是多核的處理器同時處理多個不同的任務
打比方:併發是一個人同時喫三個包子,而並行是三個人同時喫三個包子
什麼是線程池
創建並銷燬線程的過程勢必會消耗內存,如果創建多個線程對於Java來說是不合適的,Java的內存資源是極其寶貴的,所有就有了這個線程池重複利用線程

1. 線程

自定義Thread
/**
 * 自定義線程類
 * @author zhongjh
 * @date 2021/5/7
 */
public class MyThread extends Thread {

    private static final int COUNT = 10;

    /**
     * 線程名稱
     */
    private final String threadName;

    public MyThread(String name) {
        this.threadName = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < COUNT; i++) {
            System.out.println(threadName + ": " + i);
        }
    }

}
                // 並行執行多個線程
                MyThread myThread1 = new MyThread("線程1");
                MyThread myThread2 = new MyThread("線程2");
                myThread1.start();
                myThread2.start();

打印日誌


實現Runnable接口
/**
 * @author zhongjh
 * @date 2021/5/7
 */
public class MyThreadImpl implements Runnable {

    private static final int COUNT = 10;

    /**
     * 線程名稱
     */
    private final String threadName;

    public MyThreadImpl(String name) {
        this.threadName = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < COUNT; i++) {
            System.out.println(threadName + ": " + i);
        }
    }

}
                // 並行執行多個線程
                MyThreadImpl myThreadImpl1 = new MyThreadImpl("線程1");
                MyThreadImpl myThreadImpl2 = new MyThreadImpl("線程2");
                Thread myThreadOne = new Thread(myThreadImpl1);
                Thread myThreadTwo = new Thread(myThreadImpl2);
                myThreadOne.start();
                myThreadTwo.start();

打印日誌的內容跟繼承Thread是一樣的,Thread的源碼也是實現Runnable接口,兩者區別就是接口跟繼承的區別,在很多場景中接口比繼承靈活多了,當然,這是接口繼承的另一篇文章了。

線程流程

創建
new Thread()創建線程後,此時已經有了相應的內存空間和其他資源。
準備
調用線程的start()方法後,線程將進入線程隊列排隊,等待 CPU 服務,此時的線程已經具備了運行條件。
運行
當就緒狀態被調用並獲得處理器資源時,線程就進入了運行狀態。此時該線程自動它的 run() 方法。
阻塞
線程在運行過程中,如果人爲調用sleep(),suspend(),wait() 等方法或者別的因素,線程將進入阻塞狀態,發生阻塞時線程不能進入排隊隊列,只有當引起阻塞的原因被消除後,線程纔可以轉入就緒狀態。
運行
線程調用 stop() 方法時或 run() 方法執行結束後,即處於死亡狀態。處於死亡狀態的線程將不會有繼續運行的能力。

定義線程名稱

Thread.currentThread().getName(); // 取得當前線程的名稱
也可以通過new Thread(Runnable, "線程1");這種方式自定義賦值名稱

join

join方法的功能就是使異步執行的線程變成同步執行。
平常的調用線程實例的start方法後,這個方法會立即返回,如果後面的代碼想得到這個線程返回的值才能計算,那麼就必須使用join方法。

public class MyThreadJoin extends Thread {

    int m = (int) (Math.random() * 10000);

    @Override
    public void run() {
        try {
            System.out.println("我在子線程中會隨機睡上0-9秒,時間爲="+m);
            Thread.sleep(m);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
    /**
     * join方法示例
     */
    private void testJoin() {
        MyThreadJoin myThread =new MyThreadJoin();
        myThread.start();
        try {
            myThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("正常情況下肯定是我先執行完,但是加入join後,main主線程會等待子線程執行完畢後才執行");
    }

打印日誌,可以看到6秒後才顯示


sleep

線程常用方法之一,sleep(xx毫秒),線程的休眠。顧名思義,暫停線程xx毫秒之後繼續執行,直接看示例代碼

public class MyThreadSleep extends Thread {
    private static final int COUNT = 3;

    @Override
    public void run() {
        for (int i = 0; i < COUNT; i++) {
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}
    /**
     * Sleep示例
     */
    private void testSleep() {
        MyThreadSleep myThread1 = new MyThreadSleep();
        myThread1.start();
    }

打印日誌,可以看到相隔1秒才顯示一句日誌



sleep還有一種寫法,Thread.sleep(long millis),這是針對所有線程的睡眠

yield

yield()會禮讓給相同優先級的或者是優先級更高的線程執行,yield()這個方法只是把線程的狀態打回準備狀態,他會繼續跑起來,可以看到代碼例子有個停住1秒的,可以嘗試把1秒暫停看看打印出來的文字

    /**
     * Yield示例
     */
    private void testYield() {
        MyThreadYield myThread1 = new MyThreadYield("線程一");
        MyThreadYield myThread2 = new MyThreadYield("線程二");
        myThread1.start();
        myThread2.start();
    }

/**
 * 禮讓線程
 * @author zhongjh
 * @date 2021/5/14
 */
public class MyThreadYield extends Thread {

    public MyThreadYield(String name) {
        super(name);
    }

    @Override
    public synchronized void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + "在運行,i的值爲:" + i + " 優先級爲:" + getPriority());
            if (i == 2) {
                System.out.println(getName() + "禮讓");
                Thread.yield();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

打印文字


synchronized

在更深入的講解線程其他機制前,我們先講另一個關鍵字,synchronized。
這是一個同步關鍵字,不管多少個線程調用該關鍵字修飾的方法,都是一個一個的按照順序執行完。假設我們多個線程(人)買火車票

    private int ticket = 10;

    /**
     * Synchronized購買火車票的示例
     */
    private void testSynchronized() {
        for (int i = 0; i < 10; i++) {
            new Thread() {
                @Override
                public void run() {
                    // 買票
                    sellTicket();
                }
            }.start();
        }
    }

    /**
     * 減少票,同步synchronized
     */
    public synchronized void sellTicket() {
        ticket--;
        System.out.println("剩餘的票數:" + ticket);
        if (ticket == 0) {
            // 重新填充票數用於測試
            ticket = 10;
        }
    }

打印日誌,可以看到票數順序減少,如果去掉synchronized,可以發現亂序的


synchronized 對象鎖和類鎖

不同對象之間的對象鎖是互不影響的,而類鎖只有一個。但是同時對象鎖和類鎖又不互不影響,接着會通過代碼分別加深鎖的印象
首先創建一個實體類,包含了對象鎖和類鎖的方法

public class SynchronizedEntity {

    private int ticket = 10;

    /**
     * 同步方法,對象鎖
     */
    public synchronized void syncMethod() {
        for (int i = 0; i < 1000; i++) {
            if (ticket > 0) {
                ticket--;
                System.out.println(Thread.currentThread().getName() + "剩餘的票數:" + ticket);
            }
        }
    }

    /**
     * 同步塊,對象鎖
     */
    public void syncThis() {
        synchronized (this) {
            for (int i = 0; i < 1000; i++) {
                if (ticket > 0) {
                    ticket--;
                    System.out.println(Thread.currentThread().getName() + "剩餘的票數:" + ticket);
                }
            }
        }
    }

    /**
     * 同步class對象,類鎖
     */
    public void syncClassMethod() {
        synchronized (SynchronizedEntity.class) {
            for (int i = 0; i < 50; i++) {
                if (ticket > 0) {
                    ticket--;
                    System.out.println(Thread.currentThread().getName() + "剩餘的票數:" + ticket);
                }
            }
        }
    }

    /**
     * 同步靜態方法,類鎖
     */
    public static synchronized void syncStaticMethod(){
        // 暫不演示該方法
    }

}
多個線程調用同一個對象鎖
    /**
     * 多個線程調用同一個對象鎖
     */
    private void testSynchronized2() {
        final SynchronizedEntity synchronizedEntity = new SynchronizedEntity();

        // 線程一
        new Thread() {
            @Override
            public void run() {
                synchronizedEntity.syncMethod();
            }
        }.start();
        // 線程二
        new Thread() {
            @Override
            public void run() {
                synchronizedEntity.syncThis();
            }
        }.start();
    }

打印日誌可以看到有效的順序執行


兩個線程分別調用不同對象鎖
    /**
     * 兩個線程分別調用不同對象鎖
     */
    private void testSynchronized3() {
        final SynchronizedEntity synchronizedEntity1 = new SynchronizedEntity();
        final SynchronizedEntity synchronizedEntity2 = new SynchronizedEntity();

        // 線程一
        new Thread() {
            @Override
            public void run() {
                synchronizedEntity1.syncMethod();
            }
        }.start();
        // 線程二
        new Thread() {
            @Override
            public void run() {
                synchronizedEntity2.syncMethod();
            }
        }.start();
    }

打印日誌可以看到票數順序亂了


兩個線程分別調用對象鎖、類鎖
    /**
     * 兩個線程分別調用對象鎖、類鎖
     */
    private void testSynchronized4() {
        final SynchronizedEntity synchronizedDemo = new SynchronizedEntity();

        // 線程一
        new Thread() {
            @Override
            public void run() {
                synchronizedDemo.syncMethod();
            }
        }.start();

        // 線程二
        new Thread() {
            @Override
            public void run() {
                synchronizedDemo.syncClassMethod();
            }
        }.start();
    }

打印日誌如圖,他們互不影響,所以也是線程不安全的


總結:不同對象之間的對象鎖是互不影響的,而類鎖只有一個。但是同時對象鎖和類鎖又不互不影響

wait()、notify()、notifyAll()

wait、notify、notifyAll都必須在synchronized中執行,否則會拋出異常。所以爲什麼會先講synchronized
notify跟notifyAll區別是notify會喚醒等待喚醒隊列中的第一個線程,而notifyAll()方法則是喚醒整個喚醒隊列中的所有線程
直接上代碼,先創建一個線程,該線程是sleep自身2秒後再喚醒所有線程

public class MyThreadWait extends Thread {

    private final Object lockObject;

    public MyThreadWait(Object lockObject) {
        this.lockObject = lockObject;
    }

    @Override
    public void run() {
        synchronized (lockObject) {
            try {
                // 子線程等待了2秒鐘後喚醒lockObject鎖
                sleep(2000);
                System.out.println("lockObject喚醒");
                lockObject.notifyAll();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
    /**
     * wait示例
     */
    private void testWait() {
        // 創建子線程
        Thread thread = new MyThreadWait(lockObject);
        thread.start();

        long start = System.currentTimeMillis();
        synchronized (lockObject) {
            try {
                System.out.println("lockObject等待");
                lockObject.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("lockObject繼續 --> 等待的時間:" + (System.currentTimeMillis() - start));
        }
    }
wait()、notify()、notifyAll() 進階

接着是java經典題目,子線程循環2次,接着主線程循環3次,接着又回到子線程循環2次,接着再回到主線程又循環3次,如此循環10次

    /**
     * 鎖對象
     */
    private final Object lock = new Object();
    /**
     * 是否執行子線程標誌位
     */
    boolean beShouldSub = true;

    /**
     * wait和notify示例
     * 子線程循環2次,接着主線程循環3次,接着又回到子線程循環2次,接着再回到主線程又循環3次,如此循環10次
     */
    private void testWaitNotify() {
        // 子線程
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    testWaitNotifyThread();
                }
            }
        }.start();
        // 主線程
        for (int i = 0; i < 10; i++) {
            testWaitNotifyMain();
        }
    }

    /**
     * 子線程循環兩次
     */
    private void testWaitNotifyThread() {
        synchronized (lock) {
            if (!beShouldSub) {
                // 等待
                try {
                    Log.d("testWaitNotify","子線程等待lock");
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            for (int j = 0; j < 2; j++) {
                Log.d("testWaitNotify","子循環第" + (j + 1) + "次");
            }
            // 子線程執行完畢,子線程標誌位設爲false
            beShouldSub = false;
            // 喚醒
            Log.d("testWaitNotify","子線程喚醒lock");
            lock.notify();
        }
    }

    /**
     * 主線程循環3次
     */
    private void testWaitNotifyMain() {
        synchronized (lock) {
            if (beShouldSub) {
                // 等待
                try {
                    Log.d("testWaitNotify","主線程等待lock");
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            for (int j = 0; j < 3; j++) {
                Log.d("testWaitNotify","主循環第" + (j + 1) + "次");
            }
            // 主線程執行完畢,子線程標誌位設爲true
            beShouldSub = true;
            // 喚醒
            Log.d("testWaitNotify","主線程喚醒lock");
            lock.notify();
        }
    }
volatile進階

一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之後,那麼就具備了兩層語義:
  1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
  2)禁止進行指令重排序。
該鏈接超級詳細的講解了volatile
Java併發編程:volatile關鍵字解析 - Matrix海子 - 博客園 (cnblogs.com)
在DEMO中我也詳細的寫了一個錯誤示範例子

    /**
     * 這是Volatile的一個錯誤示範
     * 事實上運行它會發現每次運行結果都不一致,都是一個小於10000的數字。
     * 可見性只能保證每次讀取的是最新的值,但是volatile沒辦法保證對變量的操作的原子性
     * 自增操作是不具備原子性的,它包括讀取變量的原始值、進行加1操作、寫入工作內存
     * 那麼就是說自增操作的三個子操作可能會分割開執行,就有可能導致下面這種情況出現:
     * 假如某個時刻變量inc的值爲10
     * 線程1對變量進行自增操作,線程1先讀取了變量inc的原始值,然後線程1被阻塞了
     * 然後線程2對變量進行自增操作,線程2也去讀取變量inc的原始值,增加1變成11,並把11寫入工作內存,最後寫入主存
     * 然後線程1接着進行加1操作,由於已經讀取了inc的值,注意此時在線程1的工作內存中inc的值仍然爲10,所以線程1對inc進行加1操作後inc的值爲11,然後將11寫入工作內存,最後寫入主存。
     * 那麼兩個線程分別進行了一次自增操作後,inc只增加了1。
     */
    private void testVolatileNo() {
        final Test test = new Test();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        test.increase();
                    }
                    if (finalI ==9) {
                        System.out.println("test.inc: " + test.inc);
                    }
                }
            }.start();
        }
    }

參考學習文章:
媽媽再也不用擔心你不會使用線程池了(ThreadUtils) - 簡書 (jianshu.com)
Android-多線程 - 簡書 (jianshu.com)
深入理解線程和線程池(圖文詳解)weixin_40271838的博客-CSDN博客線程池
安卓Thread的運用 Thread.join()_ruiruiddd的博客-CSDN博客
Android進階——多線程系列之wait、notify、sleep、join、yield、synchronized關鍵字、ReentrantLock鎖_點擊置頂文章查看博客目錄(全站式導航)-CSDN博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章