1. synchronize的作用
synchronize是java最原始的同步關鍵字,通過對方法或者代碼塊進行加鎖實現對臨界區域的保護.線程每次進去同步方法或者代碼塊都需要申請鎖,如果鎖被占用則會等待鎖的釋放,值得注意的是,等待鎖的線程不會響應中斷.synchronize的鎖分為對象所和類鎖,當synchronize修飾靜態方法或者synchronize(Object.class)這樣寫時是類鎖,當synchronize修飾普通方法或者synchronize(this)這樣寫時是對象鎖(this可以替換成其他對象的引用).synchronize是官方推薦使用的同步工具,synchronize主要是在JVM層面實現的同步,官方已經對synchronize的性能進行了多次優化,有興趣可以自行百度.
2. synchronize的使用
1 package main; 2 3 public class Service implements Runnable { 4 5 @Override 6 public void run() { 7 8 synchronized (this) { 9 System.out.println(Thread.currentThread().getName() + "進入了代碼塊"); 10 try { 11 Thread.sleep(1000); 12 } catch (InterruptedException e) { 13 e.printStackTrace(); 14 } 15 System.out.println(Thread.currentThread().getName() + "准備退出代碼塊"); 16 } 17 } 18 19 public static void main(String[] args) { 20 Service service=new Service(); 21 new Thread(service).start(); 22 new Thread(service).start(); 23 } 24 }
輸出結果:
Thread-0進入了代碼塊 Thread-0准備退出代碼塊 Thread-1進入了代碼塊 Thread-1准備退出代碼塊
synchronize的基本使用如上,其在使用和理解上非常容易理解這里不做多余解釋.我們在使用synchronize要注意同步代碼范圍不能太大,並且耗時操作最好不要在同步中進行,這樣會極大程度的影響程序效率.
3. synchronize鎖機制
https://blog.csdn.net/chenssy/article/details/54883355
https://tech.meituan.com/2018/11/15/java-lock.html
4. Lock的作用
Lock同樣是實現同步的工具,但是他的實現與synchronize有本質上的差別,synchronize基於JVM的同步,Lock是一個接口,他是基於AQS的實現,底層是使用CAS和volatile變量結合實現.同樣在進入臨界區之前需要申請鎖,退出臨界區域需要手動釋放鎖,Lock主要實現類是ReetrantLock.
5. Lock的使用
1 public class Service implements Runnable { 2 3 private Lock lock=new ReentrantLock(); 4 5 @Override 6 public void run() { 7 lock.lock(); 8 System.out.println("進入臨界資源"); 9 try { 10 Thread.sleep(1000); 11 } catch (InterruptedException e) { 12 e.printStackTrace(); 13 } 14 System.out.println("准備提出臨界資源"); 15 lock.unlock(); 16 } 17 18 public static void main(String[] args) { 19 Service service=new Service(); 20 new Thread(service).start(); 21 new Thread(service).start(); 22 } 23 }
輸出結果
進入臨界資源
准備提出臨界資源
進入臨界資源
准備提出臨界資源
以上代碼使用的是ReetrantLock,他默認是使用非公平鎖,要使用公平鎖就給構造函數傳一個true.上面的代碼先調用lock方法獲取鎖,如果獲取到鎖則進入到臨界資源區沒有獲取到則阻塞,操作完后調用unlock方法釋放鎖並喚醒在鎖上等待的線程.
Lock中獲取鎖的主要方法有lock(),lockInterruptibly(),tryLock().
lock()方法不會響應中斷,lockInterruptibly()會響應中斷,tryLock()方法是嘗試獲取鎖,如果沒獲取到則返回false,否則true.
6. 公平鎖與非公平鎖在ReetrantLock的實現
在ReentrantLock類中有一個Sync內部類,他繼承自AbstractQueuedSynchronizer(即AQS,介紹看這里).Sync子類就是公平鎖(FairSync)和非公平鎖(NonfairSync),這兩個類也是ReentrantLock的內部類.
首先來看非公平鎖,非公平鎖是指后來線程具有極大的概率獲得鎖,來看看他的代碼實現.
1 static final class NonfairSync extends Sync { 2 private static final long serialVersionUID = 7316153563782823691L; 3 4 final void lock() { 5 if (compareAndSetState(0, 1)) 6 setExclusiveOwnerThread(Thread.currentThread());//將當前線程設置為鎖的擁有者 7 else 8 acquire(1); 9 } 10 11 protected final boolean tryAcquire(int acquires) { 12 return nonfairTryAcquire(acquires); 13 } 14 }
lock()就是ReentrantLock類中的lock()方法具體調用的方法,compareAndSetState方法是一個CAS操作,它是AQS中的方法,它的功能是判斷鎖的狀態是否為0,如果為0則為1返回true,否則返回false.
acquire()方法是AQS的方法,他的功能是嘗試獲取鎖,下面是該方法的代碼
1 public final void acquire(int arg) { 2 if (!tryAcquire(arg) && //嘗試獲取鎖 3 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //將當前線程放入等待隊列 4 selfInterrupt(); //中斷自己 5 }
該方法首先會調用tryAcquire()方法,這個方法就是NonfairSync類中的tryAcquire().下面是nonfairTryAcquire方法代碼
1 final boolean nonfairTryAcquire(int acquires) { 2 final Thread current = Thread.currentThread(); 3 int c = getState(); 4 if (c == 0) { //如果鎖未被占用 5 if (compareAndSetState(0, acquires)) { //CAS操作獲取鎖 6 setExclusiveOwnerThread(current); 7 return true; 8 } 9 } 10 else if (current == getExclusiveOwnerThread()) { //如果鎖被占用且申請鎖的是鎖的擁有線程 11 int nextc = c + acquires; 12 if (nextc < 0) // overflow 13 throw new Error("Maximum lock count exceeded"); 14 setState(nextc);//改變鎖狀態值 15 return true; 16 } 17 return false; 18 }
從上面代碼邏輯來看,非公平鎖是具有可重入性,如果獲取鎖失敗就返回false,否則返回true.我們在返回來看acquire()方法中的acquireQueued(addWaiter(Node.EXCLUSIVE), arg)這一句,addWaiter(Node.EXCLUSIVE)方法是將當前線程放入等待隊列中,acquireQueued()方法再次嘗試獲取鎖,如果再次獲取失敗,則將當前線程阻塞,代碼如下
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor();//獲取等待隊列前一個節點 if (p == head && tryAcquire(arg)) { //如果前一個節點是頭結點則再次獲取鎖 setHead(node); p.next = null; // help GC failed = false; return interrupted;//返回中斷標記 } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node);//取消當前線程 } }
其中shouldParkAfterFailedAcquire(p, node)這個方法,是在再次獲取鎖失敗之后的處理,由於等待隊列中可能會有線程被取消,所以當前線程要去尋找自己前面的節點,直到找到一個沒有被取消的線程為止,這樣能夠保證自己能夠被喚醒.
parkAndCheckInterrupt()是將當前線程阻塞的方法,他調用了Unsafe類的本地方法.
以上就是非公平鎖獲取鎖的過程,值得注意的是,在線程阻塞階段,是不會響應中斷的,代碼中的響應中斷是線程被喚醒之后才響應,響應手段是通過執行selfInterrupt()方法,該方法就是調用了Thread類的interrupt()方法.也就是說,只有會響應中斷的方法才會被中斷,以第4節的代碼為例,線程被中斷的話,依然會輸出兩句話,只是線程不會睡眠而已.
接下來是鎖資源的釋放,AQS中已經實現好了鎖資源釋放方法release(),但是tryRelease()方法沒有實現,一下是ReetrantLock類的實現
1 protected final boolean tryRelease(int releases) { 2 int c = getState() - releases; 3 if (Thread.currentThread() != getExclusiveOwnerThread()) //是否為當前線程 4 throw new IllegalMonitorStateException(); 5 boolean free = false; 6 if (c == 0) { //如果狀態變為0即鎖變為未被擁有 7 free = true; 8 setExclusiveOwnerThread(null); 9 } 10 setState(c); 11 return free; 12 }
接下來是release()方法
1 public final boolean release(int arg) { 2 if (tryRelease(arg)) {//嘗試釋放資源 3 Node h = head; 4 if (h != null && h.waitStatus != 0) 5 unparkSuccessor(h);//喚醒線程 6 return true; 7 } 8 return false; 9 }
unparkSuccessor()代碼如下
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) //如果線程狀態為小於0,表示有效狀態,大於0表示取消狀態 compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread);//喚醒線程 }
這里會喚醒后繼節點,如果后繼節點為null或者被取消,則從隊尾開始向前回溯,為什么從隊尾開始?博主也無法理解.
以上便是非公平鎖的實現原理,非公平鎖在獲取鎖時如果有新線程進來,那么新線程有很大可能性會獲取到鎖資源,因為等待隊列中的線程被喚醒到重新請求鎖會消耗相當大的時間,公平鎖就能夠解決這個問題.
公平鎖與非公平鎖的唯一區別在於,公平鎖的tryAcquire()方法與非公平鎖不同,看代碼
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && //唯一不同點 compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
hasQueuedPredecessors()方法是用於判斷等待隊列是否存在,如果等待隊列中有節點,那么等待隊列肯定存在,那么線程就不能直接獲取鎖資源,必須去排隊,以下是源碼
1 public final boolean hasQueuedPredecessors() { 2 3 Node t = tail; // Read fields in reverse initialization order 4 Node h = head; 5 Node s; 6 return h != t && 7 ((s = h.next) == null || s.thread != Thread.currentThread()); 8 }
以上是公平鎖與非公平鎖的實現.
7. condition
我們現在有個問題是:一個線程要進行下去,就必須一個條件滿足.我們這里有兩種有兩種實現.
1:無限循環,一直循環訪問條件,這樣顯然是極大程度浪費CPU資源
2:使用wait()方法,當條件滿足時被其他線程喚醒,這個方法是常用方法,但是這種方法依賴於synchronize
condition就是用來解決上面這個問題的.看代碼
1 public class ServiceCondition implements Runnable { 2 3 private static Lock lock = new ReentrantLock(); 4 private static boolean flag = false; 5 private static Condition condition = lock.newCondition(); 6 7 @Override 8 public void run() { 9 try { 10 lock.lock(); 11 while (!flag) { 12 System.out.println("條件為假,等待"); 13 condition.await(); 14 } 15 System.out.println("條件為真,執行"); 16 lock.unlock(); 17 } catch (InterruptedException e) { 18 e.printStackTrace(); 19 } 20 } 21 22 public static void main(String[] args) { 23 ServiceCondition serviceCondition=new ServiceCondition(); 24 new Thread(serviceCondition).start(); 25 try { 26 Thread.sleep(1000); 27 } catch (InterruptedException e) { 28 e.printStackTrace(); 29 } 30 lock.lock(); 31 flag=true; 32 condition.signal(); 33 lock.unlock(); 34 } 35 }
condition.await()相當於wait()方法,singal()相當於notify()方法.必須獲取到鎖才能調用這兩個方法,原因是調用await()方法時,會釋放鎖資源,要釋放必須先要獲得;調用signal()方法時會判斷鎖的擁有者是否是當前線程,如果是才會允許調用,這兩個方法在未獲取到鎖時調用會拋出IllegalMonitorStateException異常.
8. 可重入性
synchronize具有可重入性,當一個線程獲取到鎖時,鎖會將當前線程設置為擁有線程,並且狀態值加1表示該鎖被獲取了1次,當該線程再次獲取同一個鎖對象時,鎖會判斷線程是否為擁有線程,如果是則允許獲取,並且狀態加1,否則拒絕獲取,釋放時必須一層一層釋放資源,直到狀態值為0,表示該鎖被完全釋放.
Lock與synchronize同理,我們從上面的代碼就可以看出來,Lock在獲取鎖資源時都會判斷是否為鎖擁有線程.
9. 內存可見性
內存可見性涉及到java內存模型,建議不了解的朋友看一下我的另一篇博文:理解JVM之java內存模型。
當線程進入synchronize代碼塊時,會將共享變量全部置為失效,后續在操作共享變量時會從主存中獲取最新的數據;當退出代碼塊時,線程會把共享變量的最新值寫回主存,保證了內存可見性。synchronize的內存可見性直接由JVM提供支持,但是Lock只是一個實現了同步鎖的工具類,他是如何實現內存可見性的呢?
我們回過頭看一下上面的tryAcquire()方法,其中在第二行 int c = getState(); 獲取state變量的值,這個值是AQS中表示線程當前同步狀態的變量,該值是一個volatile變量。在后續的獲取鎖、重入鎖時都是在操作這個變量,而在java內存模型中的規則對於變量讀/寫有這樣的規定:
1.讀普通變量時,如果跟隨在讀volatile變量之后,將會從主存刷新到工作區,讀到最新值
2.普通變量隨同volatile變量,在同一個線程中的賦值,將跟隨volatile變量被刷新到主存
我們在進入調用lock(),unlock()的時候其實對volatile變量進行了操作,所以鎖住的代碼塊同樣具有線程可見性。
10. 是否響應中斷
當使用synchronize進行同步時,如果線程未獲取到鎖將會掛起自己等待喚醒,線程再被喚醒之前不會響應中斷,也就是說即使調用Thread.Interrupt()方法也不會使掛起的線程中斷。但是Lock就可以響應中斷,我們可以回過頭看一下 public final void acquire(int arg); 方法,在 acquireQueued() 方法中會進行中斷標志位判斷,如果中斷則會中斷自己。
可以看出來,synchronize使用比起Lock更加方便,但是不夠靈活,一旦未獲取到鎖就只能夠進入掛起等待喚醒;而Lock靈活,但是不夠方便,需要手動進行加鎖、解鎖等操作。