1. 程式人生 > >Android進階——多執行緒系列之wait、notify、sleep、join、yield、synchronized關鍵字、ReentrantLock鎖

Android進階——多執行緒系列之wait、notify、sleep、join、yield、synchronized關鍵字、ReentrantLock鎖

前言

多執行緒一直是初學者最困惑的地方,每次看到一篇文章,覺得很有難度,就馬上叉掉,不看了,我以前也是這樣過來的。後來,我發現這樣的態度不行,知難而退,永遠進步不了。於是,我狠下心來看完別人的部落格,儘管很難但還是咬著牙,不懂去查閱資料,到最後弄懂整個過程。雖然花費時間很大,但這就是自學的精髓,別人學不會,而我卻學到了。很簡單的一個例子,一開始我對自定義View也是很抵觸,看到很難的圖就不去思考他,故意避開它,然而當我看到自己喜歡的雷達圖時,很有興趣的去查閱資料,不知不覺,自定義View對我已經沒有難度了。所以對於多執行緒我也是0基礎,不過我還是咬著牙皮,該學的還是得學。這裡先總結這幾個類特點和區別,讓大家帶著模糊印象來學習這篇文章

  1. Thread是個執行緒,而且有自己的生命週期
  2. 對於執行緒常用的操作有:wait(等待)、notify(喚醒)、notifyAll、sleep(睡眠)、join(阻塞)、yield(禮讓)
  3. wait、notify、notifyAll都必須在synchronized中執行,否則會丟擲異常
  4. synchronized關鍵字和ReentrantLock鎖都是輔助執行緒同步使用的
  5. 初學者常犯的誤區:一個物件只有一個鎖(正確的)

執行緒同步之synchronized關鍵字

火車搶票是一年中沸沸揚揚的事情,這也就好比我們的多執行緒搶奪資源是一個道理,下面我們通過火車搶票的案例

public
class SyncActivity extends AppCompatActivity { private int ticket = 10; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sync); for (int i = 0; i < 10; i++) { new Thread() { @Override
public void run() { //買票 sellTicket(); } }.start(); } } public void sellTicket() { ticket--; System.out.println("剩餘的票數:" + ticket); } }

這裡我們通過開啟十個執行緒來購買火車票,不過火車票只有十張,下面通過列印資訊來看一下搶票的情況

剩餘的票數:9
剩餘的票數:8
剩餘的票數:7
剩餘的票數:6
剩餘的票數:5
剩餘的票數:1
剩餘的票數:1
剩餘的票數:1
剩餘的票數:1
剩餘的票數:0

可以發現,票數出現了誤差,這明顯就是不行的,這也是因為開啟了十個執行緒,大家都搶著自己的票。上面這種情況是因為其中有四個執行緒都擠在一起了,然後一起執行了【ticket–;】,接著再一起執行【System.out.println(“剩餘的票數:” + ticket);】導致的。那麼該如何保證大家都是能夠自覺排隊,井然有序的搶票呢。這個時候就要用到synchronized關鍵字

一、方法上新增synchronized關鍵字

public class SyncActivity extends AppCompatActivity {

    private int ticket = 10;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sync);

        for (int i = 0; i < 10; i++) {
            new Thread() {
                @Override
                public void run() {
                    //買票
                    sellTicket();
                }
            }.start();
        }
    }

    //新增在這裡
    public synchronized void sellTicket() {
        ticket--;
        System.out.println("剩餘的票數:" + ticket);
    }
}

這樣就表示這個方法是同步的,只能由一個個執行緒來爭奪裡面的資源,下面通過列印資訊可以驗證

剩餘的票數:9
剩餘的票數:8
剩餘的票數:7
剩餘的票數:6
剩餘的票數:5
剩餘的票數:4
剩餘的票數:3
剩餘的票數:2
剩餘的票數:1
剩餘的票數:0

二、方法內新增synchronized關鍵字

public class SyncActivity extends AppCompatActivity {

    private int ticket = 10;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sync);

        for (int i = 0; i < 10; i++) {
            new Thread() {
                @Override
                public void run() {
                    //買票
                    sellTicket();
                }
            }.start();
        }
    }

    //新增在這裡
    Object lock = new Object();
    public void sellTicket() {
        synchronized(lock){
            ticket--;
            System.out.println("剩餘的票數:" + ticket);
        }
    }
}

其實,synchronized關鍵字可以理解為一個鎖,而鎖就需要被鎖的東西,所以synchronized又分為類鎖和物件鎖,即可以鎖類又可以鎖物件,它們共同的作用就是保證執行緒的同步。就好比如我們上面中synchronized(lock),就是物件鎖,將Object物件鎖起來

類鎖和物件鎖的概念

物件鎖和類鎖在鎖的概念上基本上和內建鎖是一致的,但是在多執行緒訪問時,兩個鎖實際是有很大的區別的,物件鎖是用於物件例項方法,或者一個物件例項上的,類鎖是用於類的靜態方法或者一個類的class物件上的。我們知道,類的物件例項可以有很多個,但是每個類只有一個class物件,所以,結論是:1、不同物件例項的物件鎖是互不干擾的,但是每個類只有一個類鎖。2、而且類鎖和物件鎖互相不干擾。

一、物件鎖

類鎖建立如下兩種方法

public class SynchronizedDemo {
    //同步方法,物件鎖
    public synchronized void syncMethod() {

    }

    //同步塊,物件鎖
    public void syncThis() {
        synchronized (this) {

        }
    }
}

二、類鎖

物件鎖建立如下兩種方法

public class SynchronizedDemo {
    //同步class物件,類鎖
    public void syncClassMethod() {
        synchronized (SynchronizedDemo.class) {

        }
    }

    //同步靜態方法,類鎖
    public static synchronized void syncStaticMethod(){

    }
}

三、通過例子理解結論和概念

根據類鎖和物件鎖的概念,我們來通過例子驗證一下其正確性,這裡演示兩個物件鎖和一個類鎖,我們建立一個類

public class SynchronizedDemo {
     private int ticket = 10;
    //同步方法,物件鎖
    public synchronized void syncMethod() {
        for (int i = 0; i < 1000; i++) {
            ticket--;
            System.out.println(Thread.currentThread().getName() + "剩餘的票數:" + ticket);
        }
    }

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

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

1、同一個物件,使用兩個執行緒呼叫不同物件鎖

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_sync);

    final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();

    //執行緒一
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo.syncMethod();
        }
    }.start();
    //執行緒二
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo.syncThis();
        }
    }.start();
}

由於使用的是同一個物件的物件鎖,所以執行出來的結果是同步的(即先執行執行緒一,等執行緒一執行完後執行執行緒二,ticket有序的減少),這裡使用1000比較大的數字是為了一次能看出效果

Thread-1611剩餘的票數:7
Thread-1611剩餘的票數:6
Thread-1611剩餘的票數:5
Thread-1611剩餘的票數:4
Thread-1611剩餘的票數:3
Thread-1611剩餘的票數:2

2、不同物件,使用兩個執行緒呼叫同個物件鎖

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_sync);

    final SynchronizedDemo synchronizedDemo1 = new SynchronizedDemo();
    final SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();

    //執行緒一
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo1.syncMethod();
        }
    }.start();
    //執行緒二
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo2.syncMethod();
        }
    }.start();
}

由於是不同物件,所以執行的物件鎖都不是不同的,其結果是兩個執行緒互相搶佔資源的執行,即ticket偶爾會無序的減少

Thread-1667剩餘的票數:-1612
Thread-1667剩餘的票數:-1613
Thread-1668剩餘的票數:-1630
Thread-1668剩餘的票數:-1631
Thread-1668剩餘的票數:-1632

3、同一個物件,使用兩個執行緒呼叫一個物件鎖一個類鎖

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_sync);

    final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();

    //執行緒一
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo.syncMethod();
        }
    }.start();
    //執行緒二
    new Thread() {
        @Override
        public void run() {
            synchronizedDemo.syncClassMethod();
        }
    }.start();
}

由於物件鎖和類鎖互不干擾,所以也是執行緒不安全的

Thread-1667剩餘的票數:-1612
Thread-1667剩餘的票數:-1613
Thread-1668剩餘的票數:-1630
Thread-1668剩餘的票數:-1631
Thread-1668剩餘的票數:-1632

溫習結論:1、不同物件例項的物件鎖是互不干擾的,但是每個類只有一個類鎖。2、而且類鎖和物件鎖互相不干擾。

執行緒同步之ReentrantLock鎖

Java6.0增加了一種新的機制:ReentrantLock,下面看ReentrantLock的使用

public class RenntrantLockActivity extends AppCompatActivity {

    Lock lock;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_renntrant_lock);

        lock = new ReentrantLock();
        doSth();
    }

    public void doSth() {
        lock.lock();
        try {
            //這裡執行執行緒同步操作

        } finally {
            lock.unlock();
        }
    }
}

使用ReentrantLock很好理解,就好比我們現實的鎖頭是一樣道理的。使用ReentrantLock的一般組合是lock與unlock成對出現的,需要注意的是,千萬不要忘記呼叫unlock來釋放鎖,否則可能會引發死鎖等問題。如果忘記了在finally塊中釋放鎖,可能會在程式中留下一個定時炸彈,隨時都會炸了,而是用synchronized,JVM將確保鎖會獲得自動釋放,這也是為什麼Lock沒有完全替代掉synchronized的原因

執行緒的生命週期的介紹

執行緒也有屬於自己的生命週期,這裡使用我畫的一張圖來理解,在下面我們會講解這個有關生命週期的一些方法的使用

執行緒的等待喚醒機制之wait()、notify()、notifyAll()

一開始我們也提到了wait、notify、notifyAll都必須在synchronized中執行,否則會丟擲異常。所以下面以一個簡單的例子來介紹執行緒的等待喚醒機制

public class WaitAndNotifyActivity extends AppCompatActivity {

    private static Object lockObject = new Object();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_wait_and_notify);

        System.out.println("主執行緒執行");
        //建立子執行緒
        Thread thread = new WaitThread();
        thread.start();

        long start = System.currentTimeMillis();
        synchronized (lockObject) {
            try {
                System.out.println("主執行緒等待");
                lockObject.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("主執行緒繼續 --> 等待的時間:" + (System.currentTimeMillis() - start));
        }
    }

    class WaitThread extends Thread {
        @Override
        public void run() {
            synchronized (lockObject) {
                try {
                    //子執行緒等待了2秒鐘後喚醒lockObject鎖
                    Thread.sleep(2000);
                    lockObject.notifyAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

可以看到,我們使用的是同一個物件的鎖,和同一個物件執行的wait()和notify()才會保證了我們的執行緒同步。當主執行緒執行到wait()方法時,代表主執行緒等待,讓出使用權讓子執行緒執行,這個時候主執行緒等待這一事件會被加進到【等待喚醒的佇列】中。然後子執行緒則是兩秒鐘後執行notify()方法喚醒等待【喚醒佇列中】的第一個執行緒,這裡指的是主執行緒。而notifyAll()方法則是喚醒整個【喚醒佇列中】的所有執行緒,這裡就不多加演示了

下面採用一道經典的Java多執行緒面試題來讓大家練習熟悉熟悉:子執行緒迴圈10次,接著主執行緒迴圈15次,接著又回到子執行緒迴圈10次,接著再回到主執行緒又迴圈15次,如此迴圈50次

//子執行緒
new Thread() {
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            for (int j = 0; j < 10; j++) {
                System.out.println("子迴圈迴圈第" + (j + 1) + "次");
            }
            System.out.println("--> 子執行緒迴圈了" + (i + 1) + "次");
        }
    }
}.start();
//主執行緒
for (int i = 0; i < 50; i++) {
    for (int j = 0; j < 15; j++) {
        System.out.println("主迴圈迴圈第" + (j + 1) + "次");
    }
    System.out.println("--> 主執行緒迴圈了" + (i + 1) + "次");
}

首先是主要思路的搭建,現在的問題就是如何讓子執行緒和主執行緒有序的執行呢,那肯定是我們的等待喚醒機制

//子執行緒
new Thread() {
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            synchronized (lock){

                for (int j = 0; j < 10; j++) {
                    System.out.println("子迴圈迴圈第" + (j + 1) + "次");
                }
                //喚醒
                lock.notify();
                //等待
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}.start();
//主執行緒
for (int i = 0; i < 50; i++) {
    synchronized (lock){
        //等待
        try {
            lock.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int j = 0; j < 15; j++) {
            System.out.println("主迴圈迴圈第" + (j + 1) + "次");
        }
        //喚醒
        lock.notify();
    }
}

不管是主執行緒先執行還是子執行緒執行,兩個執行緒只能同時進入synchronized (lock)一個鎖中。由於是子執行緒先執行:1、當主執行緒先進入synchronized (lock)鎖時,它就必須是等待,而子執行緒開始執行輸出,輸出後就喚醒主執行緒。2、當子執行緒先執行的話,那就直接輸出,然後等待主執行緒的執行輸出

執行緒的sleep()、join()、yield()

一、sleep()

sleep()作用是讓執行緒休息指定的時間,時間一到就繼續執行,它的使用很簡單

try {
    Thread.sleep(2000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

二、join()

join()作用是讓指定的執行緒先執行完再執行其他執行緒,而且會阻塞主執行緒,它的使用也很簡單

public class JoinActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_join);

        //啟動執行緒一
        try {
            MyThread myThread1 = new MyThread("執行緒一");
            myThread1.start();
            myThread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("主執行緒需要等待");

        //啟動執行緒二
        try {
            MyThread myThread2 = new MyThread("執行緒二");
            myThread2.start();
            myThread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("主執行緒繼續執行");
    }

    class MyThread extends Thread {

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

        @Override
        public void run() {
            System.out.println(getName() + "在執行");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

這裡就不解釋了,看列印資訊,你就能發現它的作用了

執行緒一在執行
主執行緒需要等待
執行緒二在執行
主執行緒繼續執行

三、yield()

yield()的作用是指定執行緒先禮讓一下別的執行緒的先執行,就好比公交車只有一個座位,誰禮讓了誰就坐上去。特別注意的是:yield()會禮讓給相同優先順序的或者是優先順序更高的執行緒執行,不過yield()這個方法只是把執行緒的執行狀態打回準備就緒狀態,所以執行了該方法後,有可能馬上又開始執行,有可能等待很長時間

public class YieldActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_yield);

        MyThread myThread1 = new MyThread("執行緒一");
        MyThread myThread2 = new MyThread("執行緒二");

        myThread1.start();
        myThread2.start();
    }

    class MyThread extends Thread {

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

        @Override
        public synchronized void run() {
            for (int i = 0; i < 100; 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();
                    }
                }
            }
        }
    }
}

這裡我們通過Thread.sleep()的方式,讓執行緒強行延遲一秒回到準備就緒狀態,這樣在列印資訊上就能看到我們想要的結果了

執行緒二在執行,i的值為:0 優先順序為:5
執行緒二在執行,i的值為:1 優先順序為:5
執行緒二在執行,i的值為:2 優先順序為:5
執行緒二禮讓
執行緒一在執行,i的值為:0 優先順序為:5
執行緒一在執行,i的值為:1 優先順序為:5
執行緒一在執行,i的值為:2 優先順序為:5
執行緒一禮讓
執行緒二在執行,i的值為:3 優先順序為:5
執行緒二在執行,i的值為:4 優先順序為:5
執行緒二在執行,i的值為:5 優先順序為:5
執行緒二在執行,i的值為:6 優先順序為:5
......

結語

好了,關於執行緒的介紹就這麼多,可能知識點有點多,我自己也學習了好幾天來掌握執行緒,這裡的分享我都是測試過的。學習一遍才知道原來是這麼一回事,沒學習之前看別人的文章還是懂的,當自己碼一遍的時候會發現寫不出來,原因是沒有真正理解執行緒。現在理解了執行緒之後,寫出來會根據它的作用和思路來寫,根本不用記程式碼