Java併發程式設計:執行緒和鎖的使用與解析
執行緒的使用
新建執行緒
新建一個執行緒有兩種方法:繼承Thread類,然後重寫run方法;實現Runnable介面,然後實現run方法。實際上Thread類也是實現的Runnable介面,再加上類只能單繼承,所以推薦使用Runnable介面。示例如下:
class Demo1 implements Runnable{ @Override public void run() { //新建執行緒需要執行的邏輯 } }
class Demo2 extends Thread{ @Override public void run() { //新建執行緒需要執行的邏輯 } }
對於Thread類,當然可以使用匿名內部類來簡化寫法:
Thread thread=new Thread(){ public void run(){ //新建執行緒需要執行的邏輯 } }; //Lambda表示式簡化後 Thread thread=new Thread(()->{ //需要執行的邏輯 });
新建完一個執行緒後,就可以用物件例項來啟動執行緒,啟動後就會執行我們重寫後的run方法:
thread.start();
此外,Thread類有個非常重要的構造方法:
public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }
可見,傳入的是一個Runnable型別的引數。為什麼需要這個建構函式?因為Runnable介面只有一個run方法,如果我們直接例項化實現了這個介面的類,然後呼叫run方法,其實就和普通的類沒有區別,並沒有另外一個執行緒去執行run方法。說白了,Runnable並不是新建了一個執行緒,而只是執行緒裡面執行任務的一種型別。在Java併發程式設計裡,我們總是說的任務,很多時候就是Runnable型別的。所以我們還是需要把實現了Runnable介面的類的例項傳入Thread的建構函式,然後通過start方法去呼叫Runnable的run方法。
//新建一個任務(Demo1實現了Runnable介面) Demo1 task=new Demo1; //新建一個執行緒並傳入需要執行的任務 Thread thread=new Thread(task); //啟動執行緒執行任務 thread.start();
執行緒的其他方法
熟悉了執行緒的建立,再簡單瞭解一下操作執行緒的其他方法。
stop方法:作用是終止執行緒,但不推薦使用,因為它是強制結束執行緒,不管執行緒執行到了哪一步,很容易造成錯誤資料,引起資料不一致的問題。
interrupt方法:作用和stop類似,但是並不會那麼粗魯的終止執行緒,如果只調用這一個方法並不會中斷執行緒,它還需要配合一個方法使用:
class Demo implements Runnable { @Override public void run() { //通過isInterrupted方法判斷當前執行緒是否需要停止,不需要停止就執行邏輯程式碼 while (!Thread.currentThread().isInterrupted()){ //邏輯 } } } public class Use { public static void main(String[] args) throws InterruptedException { Demo task = new Demo (); Thread thread=new Thread(task); thread.start(); //通知thread可以終止了 thread.interrupt(); } }
wait方法和notify方法:這兩個方法放在一起說,是因為它們需要配合使用。簡單提一下synchronized ,這個會在在鎖裡面講。synchronized大概的作用就是:程式碼塊裡的程式碼,同時只能由一個執行緒去執行,如何確保只有一個執行緒去執行?誰擁有鎖誰就有資格執行。任何物件都可以呼叫wait方法,如obj.wait,它的意思就是讓當前執行緒在obj上等待並釋放當前執行緒佔用的鎖。obj.notify就是喚醒在obj上等待的執行緒並重新嘗試獲取鎖。下面演示一下簡單的使用:
public class Use { //一定要確保等待和喚醒是同一個物件,用類鎖也可以,至於什麼是類鎖可以看後面synchronized部分 static Object object=new Object(); static int i = 0; static class Demo1 implements Runnable { @Override public void run() { synchronized (object){ for(int j=0;j<10000;j++){ i++; if(i==5000){ //1.因為t1先啟動並進入同步程式碼塊,所以首先輸出5000 System.out.println(); try { //釋放鎖並等待 object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } //3.被喚醒後接著執行完剩餘的程式碼,輸出20000 System.out.println(i); } } } static class Demo2 implements Runnable{ @Override public void run() { synchronized (object){ for(int j=0;j<10000;j++){ i++; } //2.獲取到t1釋放的鎖,執行完程式碼後輸出15000並喚醒object上等待的執行緒 System.out.println(i); object.notify(); } } } public static void main(String[] args) throws InterruptedException { Demo1 task1 = new Demo1(); Demo2 task2 = new Demo2(); Thread thread1=new Thread(task1); Thread thread2=new Thread(task2); thread1.start(); thread2.start(); } }
需要注意的是,如果有多個執行緒在obj等待,只執行一次obj.notify的話,它是隨機從obj等待列表中選擇一個執行緒喚醒的,如果要喚醒所有等待執行緒,可以使用obj.notifyAll。不管wait、notify還是notifyAll只能在synchronized程式碼塊中使用,否則會報IllegalMonitorStateException異常。Java之所以這麼規定,是確保不會發生Lost Wake Up問題,也就是喚醒丟失。上面那個例子中使用了同步程式碼塊,所以不會發生這種問題。試想一種情況,如果沒有synchronized確保執行緒是有秩序執行的,當t2執行緒先喚醒了object上的物件,t1執行緒後暫停的,那麼t1是不是就永遠會暫停下去,t2的notify相當於丟失了,這就是Lost wake up。
join方法:作用是讓指定執行緒加入當前執行緒。為了節約篇幅還是以interrupt方法的程式碼為例,如果在main方法裡呼叫thread.join(),那麼主執行緒就會等待thread執行緒執行完才接著執行。其實這就和單執行緒的效果差不多了。如果有時候thread執行緒執行時間太長,為了不影響其他執行緒,我們可以在join方法裡傳入一個時間,單位是毫秒,當過了這個時間不管thread執行緒有沒有執行完,主執行緒都會接著執行。join方法其實是通過wait方法實現的,注意這個wait是被加入執行緒等待,而不是加入的執行緒等待。貼一下原始碼,邏輯很簡單就不復述了,如果join不傳入引數,millis預設就是0:
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
yield方法:這個方法會讓出當前執行緒的CPU,讓出後還會接著去爭奪,但是還能不能爭奪到就不一定了。一般優先順序比較低的執行緒,為了節約資源可以適當呼叫這個方法。
執行緒安全
如何保證執行緒安全?無非就是鎖的爭奪。誰擁有了鎖,誰才有資格執行。為什麼只讓一個執行緒執行程式碼?這就與Java的記憶體模型有關了。不瞭解的可以看其他資料,也可以看看我的另外一篇部落格:https://www.cnblogs.com/lbhym/p/12458990.html,最後一節就是講Java記憶體模型的。簡單的說:執行緒共享的資源是放在一個共享記憶體區域的。當執行緒A去操作一個共享變數時,它會先把這個變數拷貝到自己私有的記憶體空間,然後進行操作,最後把操作後的值賦值到共享記憶體中的變數。如果在賦值之前另外一個執行緒B剛剛更新了這個值,那麼執行緒A的操作就把執行緒B的操作給覆蓋了,而執行緒B渾然不知,接著執行它的邏輯,這就造成了資料不一致的情況。所以我們必須加上一把鎖,確保同一時間只能由一個執行緒來修改這個變數。
關鍵字synchronized
關鍵字synchronized的作用前面已經提到過了,下面給個簡單的示例:
class Demo implements Runnable { static int i = 0; @Override public void run() { //小括號中的Demo.class就是鎖,大括號內的程式碼同時只能由一個執行緒執行 synchronized (Demo.class){ for(int j=0;j<10000;j++) { i++; } } } } public class Use { public static void main(String[] args) throws InterruptedException { Demo task = new Demo(); Thread thread1=new Thread(task); Thread thread2=new Thread(task); thread1.start(); thread2.start(); //讓兩個執行緒加入主執行緒,這樣就可以輸出執行後的i了 thread1.join(); thread2.join(); System.out.println(Demo.i);//輸出20000,如果去掉同步程式碼塊,i絕對小於20000 } }
其實這個關鍵字的作用很好理解,關鍵在於,小括號裡面有什麼實際意義,它與Lock有什麼區別?
首先synchronized和Lock都是Java裡面的鎖機制,前者用起來更加方便,後者功能更多。方便在哪?進入程式碼塊前自動獲取鎖,如果鎖已經被佔,則會等待。執行完同步程式碼塊中的內容,自動釋放鎖。而Lock需要手動加鎖解鎖,接下來會講。
接著說說synchronized具體用法,小括號裡就是鎖物件。有一點需要注意,synchronized鎖的是物件,而不是裡面的程式碼,誰擁有指定的鎖誰就能執行裡面的程式碼。明白這一點有助於理解下面的內容。
synchronized的鎖分為類鎖和物件鎖。它們的區別就是作用域的不同。
首先說說物件鎖怎麼用以及它的特點:
//物件鎖: synchronized(this){...} synchronized(類的例項){...} //修飾在void前也是物件鎖 public synchronized void run(){...}
如果synchronized裡指定的是物件鎖,那麼在建立task時,不同的例項物件就是不同的鎖。大家可以在上面示例程式碼的基礎上,再用Demo類例項化一個task2,然後用thread去執行它,接著把synchronized小括號裡的鎖換成this,也就是物件鎖,會發現輸出的i小於20000。因為task和task2完全就是不同的鎖,兩個執行緒並不衝突,這就是為什麼上面強調,鎖的是物件,而不是裡面的程式碼。
再說說類鎖的用法和特點:
//類鎖 synchronized(類名.class){...} //修飾在靜態方法前也是類鎖,run方法裡直接呼叫handler就行 private synchronized static void handler(){...}
上面的示例程式碼就是一個類鎖,即使例項化兩個不同的物件,提交給兩個執行緒執行後,輸出結果肯定是20000,也就是說它們是同步的。
最後說一點,同一個類中,類鎖和物件鎖依舊是不同的鎖,它們之間互不干擾,不是同步的。舉個例子:
class Demo implements Runnable { static int i = 0; @Override public void run() { run2(); run3(); } //類鎖 private synchronized static void run2(){ for(int j=0;j<10000;j++) { i++; } } //物件鎖 private synchronized void run3(){ for(int j=0;j<10000;j++) { i++; } } }
main方法就不貼了,記得例項化一個task2給thread2執行。最後的輸出結果肯定小於40000,如果把run3改成靜態方法,也就是變成類鎖,輸出結果就是40000了。
介面Lock
Lock介面下提供了一套功能更完整的鎖機制。如果專案中執行緒的競爭並不激烈,使用synchronized完全足夠,如果競爭很激烈,還需要其他一些功能,這時候就可以嘗試一下Lock提供的鎖了。
ReentrantLock:可重入鎖
簡單的示例如下,說明也在註釋當中:
class ReenterLock implements Runnable { //可重入鎖,意思是:在同一個執行緒中,可以對lock多次加鎖,當然也必須解鎖對應次數 //那麼Lock下的鎖是類鎖還是物件鎖,取決於鎖物件是類變數還是普通的全域性變數,加上static就是類鎖,反之就是物件鎖 static ReentrantLock lock = new ReentrantLock(); static int i = 0; @Override public void run() { lock.lock(); for (int j=0;j<10000;j++){ i++; } lock.unlock(); } } public class 可重入鎖 { public static void main(String[] args) throws InterruptedException { ReenterLock task = new ReenterLock(); Thread thread1=new Thread(task); Thread thread2=new Thread(task); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(ReenterLock.i); } }
可重入鎖除了以上加鎖、解鎖的基本功能外,還有其他一些功能:
lockInterruptibly方法和interrupt方法:後者線上程中已經出現過一次了,雖然名字一樣,功能也差不多,但是作用物件不一樣。如果我們的執行緒在加鎖也就是獲取鎖時,用的是lockInterruptibly方法,如果在等待一段時間後,還沒獲取到鎖,那麼就可以通過interrupt方法通知這個執行緒不用等了。這兩個方法配合使用,在設定合理的等待時間後,可以避免死鎖的發生。但需要注意,被通知放棄獲取鎖的執行緒會釋放自己的資源,結束執行任務。
tryLock方法:除了上面那種外部通知放棄獲取鎖的方法外,還有一種限時等待的方法,tryLock有兩個引數,第一個是時間,第二個是時間型別。如果不傳入任何引數,獲取到鎖直接返回true,沒獲取到直接返回false。對的,tryLock和普通的lock方法不同,它返回的是Boolean型別,所以一般需要配合if判斷使用:
@Override public void run() { try { if (lock.tryLock(10, TimeUnit.SECONDS)) { //邏輯程式碼 } } catch (InterruptedException e) { e.printStackTrace(); } }
公平鎖和非公平鎖:公平鎖的分配是公平的,先到先得。非公平鎖則是隨機分配鎖的,你先等待的不一定能先獲取到鎖。具體的是在ReenterLock建構函式中進行設定:
//建構函式傳入true就是公平鎖,預設情況下是非公平鎖 static ReentrantLock lock = new ReentrantLock(true);
預設情況下采用非公平鎖,是因為公平鎖需要維護一個有序佇列,效能相較於非公平鎖是非常低的。
Condition:可重入鎖的搭檔
在synchronized程式碼塊中,可以使用wait方法讓當前執行緒釋放鎖並等待,然後通過notify方法喚醒執行緒並嘗試重新獲取鎖。但是這兩個方法是作用在synchronized中的,前面也說過了。在可重入鎖也有類似的功能,下面舉個簡單的例子,會發現和synchronized中的wait和notify差不都:
public class Use { static ReentrantLock lock = new ReentrantLock(); //建立的lock的condition物件 static Condition condition = lock.newCondition(); static int i = 0; static class Demo1 implements Runnable { @Override public void run() { //t1先進來加鎖(遇到一次特殊情況,t2後啟動的反而先獲取到鎖了) lock.lock(); for (int j = 0; j < 10000; j++) { i++; if (i == 5000) { //1.輸出5000 System.out.println(i); try { //釋放鎖並等待 condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } } } //被喚醒後接著執行完剩下的程式碼,並輸出20000 System.out.println(i); lock.unlock(); } } static class Demo2 implements Runnable { @Override public void run() { //獲取鎖 lock.lock(); for (int j = 0; j < 10000; j++) { i++; } System.out.println(i); //2.執行完後輸出15000,並喚醒等待的執行緒 condition.signal(); lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Demo1 task1 = new Demo1(); Demo2 task2 = new Demo2(); Thread thread1 = new Thread(task1); Thread thread2 = new Thread(task2); thread1.start(); thread2.start(); } }
await方法會使當前執行緒等待,同時釋放當前鎖,當其他執行緒中使用signal()方法或者signalAll()方法時,執行緒會重新獲得鎖並繼續執行。或者當執行緒被中斷時,也能跳出等待。這和Object.wait()方法相似。
awaitUninterruptibly方法與await方法基本相同,但是它並不會在等待過程中響應中斷。
singal方法用於喚醒一個在等待中的執行緒,singalAll方法會喚醒所有在等待中的執行緒。這和Obejct.notify()方法很類似。
Semaphore:允許多個執行緒同時訪問
前面提到的可重入鎖和同步程式碼塊一次只能讓一個執行緒進入,而Semaphore可以指定多個執行緒,同時訪問一個資源。
public class Use { static int i = 0; //一次允許兩個執行緒進入 static Semaphore sema = new Semaphore(2); static class Demo2 implements Runnable { @Override public void run() { try { //如果有多餘的名額就允許一個執行緒進入 sema.acquire(); for(int j=0;j<10000;j++){ i++; } } catch (InterruptedException e) { e.printStackTrace(); }finally { //當前執行緒執行完程式碼並釋放一個名額 sema.release(); } } } public static void main(String[] args) throws InterruptedException { //Demo1 task1 = new Demo1(); Demo2 task2 = new Demo2(); Thread thread1 = new Thread(task2); Thread thread2 = new Thread(task2); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(i);//輸出小於20000,說明同時有兩個執行緒進去了,互相干擾了。 } }
ReadWriteLock:讀寫鎖
很多時候執行緒只是執行讀操作,並不會互相干擾,其實這個時候並不需要執行緒之間相互排斥。在資料庫裡面讀寫鎖是比較常見的,在Java中,它們的邏輯其實是一樣的。只有讀和讀不會阻塞,有寫操作必然阻塞。
程式碼篇幅太多了,就不再演示邏輯程式碼了,下面是讀寫鎖的建立程式碼:
static ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock(); //讀鎖 static Lock readLock=readWriteLock.readLock(); //寫鎖 static Lock writLock=readWriteLock.writeLock();
CountDownLatch:倒計數器
一個實用的多執行緒工具類,說倒計數器可能有點不明白,其實就是等來指定數量的執行緒執行完後才執行接下來的程式碼,看示例更清楚:
public class Use { //需要兩個執行緒執行完任務 static CountDownLatch count = new CountDownLatch(2); static int i=0; static class Demo2 implements Runnable { @Override public void run() { synchronized (Use.class) { for (int j = 0; j < 10000; j++) { i++; } //當前執行緒執行完任務,計數器+1 count.countDown(); } } } public static void main(String[] args) throws InterruptedException { //Demo1 task1 = new Demo1(); Demo2 task2 = new Demo2(); Thread thread1 = new Thread(task2); Thread thread2 = new Thread(task2); thread1.start(); thread2.start(); //等待指定數量的執行緒都執行完任務後才接著執行,相當於阻塞了當前的主執行緒,從而實現了join的功能 count.await(); System.out.println(i);//輸出20000 } }
&n