Android進階——多執行緒系列之wait、notify、sleep、join、yield、synchronized關鍵字、ReentrantLock鎖
前言
多執行緒一直是初學者最困惑的地方,每次看到一篇文章,覺得很有難度,就馬上叉掉,不看了,我以前也是這樣過來的。後來,我發現這樣的態度不行,知難而退,永遠進步不了。於是,我狠下心來看完別人的部落格,儘管很難但還是咬著牙,不懂去查閱資料,到最後弄懂整個過程。雖然花費時間很大,但這就是自學的精髓,別人學不會,而我卻學到了。很簡單的一個例子,一開始我對自定義View也是很抵觸,看到很難的圖就不去思考他,故意避開它,然而當我看到自己喜歡的雷達圖時,很有興趣的去查閱資料,不知不覺,自定義View對我已經沒有難度了。所以對於多執行緒我也是0基礎,不過我還是咬著牙皮,該學的還是得學。這裡先總結這幾個類特點和區別,讓大家帶著模糊印象來學習這篇文章
- Thread是個執行緒,而且有自己的生命週期
- 對於執行緒常用的操作有:wait(等待)、notify(喚醒)、notifyAll、sleep(睡眠)、join(阻塞)、yield(禮讓)
- wait、notify、notifyAll都必須在synchronized中執行,否則會丟擲異常
- synchronized關鍵字和ReentrantLock鎖都是輔助執行緒同步使用的
- 初學者常犯的誤區:一個物件只有一個鎖(正確的)
執行緒同步之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
......
結語
好了,關於執行緒的介紹就這麼多,可能知識點有點多,我自己也學習了好幾天來掌握執行緒,這裡的分享我都是測試過的。學習一遍才知道原來是這麼一回事,沒學習之前看別人的文章還是懂的,當自己碼一遍的時候會發現寫不出來,原因是沒有真正理解執行緒。現在理解了執行緒之後,寫出來會根據它的作用和思路來寫,根本不用記程式碼