synchronize與lock

1. synchronize的做用

  synchronize是java最原始的同步關鍵字,經過對方法或者代碼塊進行加鎖實現對臨界區域的保護.線程每次進去同步方法或者代碼塊都須要申請鎖,若是鎖被佔用則會等待鎖的釋放,值得注意的是,等待鎖的線程不會響應中斷.synchronize的鎖分爲對象所和類鎖,當synchronize修飾靜態方法或者synchronize(Object.class)這樣寫時是類鎖,當synchronize修飾普通方法或者synchronize(this)這樣寫時是對象鎖(this能夠替換成其餘對象的引用).synchronize是官方推薦使用的同步工具,synchronize主要是在JVM層面實現的同步,官方已經對synchronize的性能進行了屢次優化,有興趣能夠自行百度.html

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 }

輸出結果:java

Thread-0進入了代碼塊
Thread-0準備退出代碼塊
Thread-1進入了代碼塊
Thread-1準備退出代碼塊

  synchronize的基本使用如上,其在使用和理解上很是容易理解這裏不作多餘解釋.咱們在使用synchronize要注意同步代碼範圍不能太大,而且耗時操做最好不要在同步中進行,這樣會極大程度的影響程序效率.node

3. Lock的做用

  Lock一樣是實現同步的工具,可是他的實現與synchronize有本質上的差異,synchronize基於JVM的同步,Lock是一個接口,他是基於AQS的實現,底層是使用CAS和volatile變量結合實現.一樣在進入臨界區以前須要申請鎖,退出臨界區域須要手動釋放鎖,Lock主要實現類是ReetrantLock.ide

4. 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.優化

5. 公平鎖與非公平鎖在ReetrantLock的實現

  在ReentrantLock類中有一個Sync內部類,他繼承自AbstractQueuedSynchronizer(即AQS,介紹看這裏).Sync子類就是公平鎖(FairSync)和非公平鎖(NonfairSync),這兩個類也是ReentrantLock的內部類.ui

  首先來看非公平鎖,非公平鎖是指後來線程具備極大的機率得到鎖,來看看他的代碼實現.this

  

 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     }
以上是公平鎖與非公平鎖的實現.

6. 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異常.

7. 可重入性

  synchronize具備可重入性,當一個線程獲取到鎖時,鎖會將當前線程設置爲擁有線程,而且狀態值加1表示該鎖被獲取了1次,當該線程再次獲取同一個鎖對象時,鎖會判斷線程是否爲擁有線程,若是是則容許獲取,而且狀態加1,不然拒絕獲取,釋放時必須一層一層釋放資源,直到狀態值爲0,表示該鎖被徹底釋放.

  Lock與synchronize同理,咱們從上面的代碼就能夠看出來,Lock在獲取鎖資源時都會判斷是否爲鎖擁有線程.

相關文章
相關標籤/搜索