synchronized同步方法、ReentranLock、ReentranReadWriteLock對比分析

首先,synchronized是java關鍵字,用來做線程同步,ReentranLock和ReentranReadWriteLock則是為了更加靈活的處理同步而出現的兩種鎖。

一、synchronized同步方法
synchronized同步方法,有兩種表現形式,一種是以內置鎖對象作為對象監視器,另一種則是用Class作為對象監視器,這兩者的使用情況有一定的區別,后面我們分析;首先我們先學習synchronized的使用方式。

public class SyncTest {

    private final SyncTest lock = new SyncTest();

    /**
     * 使用當前對象內置鎖
     */
    public synchronized void test1() {
        // TODO 同步代碼
    }

    /**
     * 使用當前對象內置鎖
     */
    public void test2() {
        synchronized (this) {
            // TODO 同步代碼
        }
    }

    /**
     * 使用了lock對象內置鎖
     */
    public void test3() {
        synchronized (lock) {
            // TODO 同步代碼
        }
    }

    /**
     * 使用了SyncTest的Class對象的內置鎖
     */
    public synchronized static void test4() {
        // TODO 同步代碼
    }

    /**
     * 使用了SyncTest的Class對象的內置鎖
     */
    public synchronized static void test5() {
        // TODO 同步代碼
    }
}

以上列出了synchronized的使用情況,同時說明了其對象監視器是什么,那么我們就可以很好的分析出它們之間的競爭關系。

        // 具有競爭關系
        SyncTest syncTest = new SyncTest();
        syncTest.test1();
        syncTest.test2();

上面兩個方法都會競爭syncTest對象的內置鎖。

        // 三者具有競爭關系
        lock.test1();
        lock.test2();
        lock.test3();

上面三個方法都會競爭lock對象的內置鎖。(lock是SyncTest類中SyncTest類型的成員變量)

        // 具有競爭關系
        SyncTest.test4();
        SyncTest.test5();

上面兩個方法競爭SyncTest的Class對象中的內置鎖。(Class對象是單例的)

對于分析synchronized同步代碼之間是否有競爭關系,我們只需要關注它的對象監視器是否是同一個就能很清楚的知道。
當然有一個比較特別情況,就是String常量池,由于相同的String常量是同一個對象,所以在作為對象監視器的時候也是同一個。

public class StringSyncTest {

    private final String lock1 = "lock";
    private final String lock2 = "lock";

    public void test1() {
        synchronized(lock1) {
            // TODO 同步代碼
        }
    }

    public void test2() {
        synchronized (lock2) {
            // TODO 同步代碼
        }
    }

}

上面兩個方法就有競爭關系。

synchronized可以保證同一時刻,只有一個線程能獲取對象監視器,去執行一個方法或者某個代碼塊,所以它具有互斥性和可見性。
以下代碼,如果在server模式下運行,那么就會出現死循環。

public class VolatileSyncTest {

    private boolean loop = true;

    public static void main(String... args) {
        VolatileSyncTest app = new VolatileSyncTest();
        new Thread(app.new LoopTask()).start();
        new Thread(app.new LoopController()).start();
    }

    private class LoopTask implements Runnable {
        @Override
        public void run() {
            // 在-server模式下進入死循環
            while (loop) {
                // TODO something
            }
            System.out.println("stop...");
        }
    }

    private class LoopController implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            loop = false;
        }
    }
}

出現的原因其實很簡單,就是因為LoopController更新了loop,但是LoopTask不可見,依舊使用的是緩存于線程中的緩存值。我們在循環中加上同步代碼塊,就能保證其可見性。

public class VolatileSyncTest {

    private boolean loop = true;

    public static void main(String... args) {
        VolatileSyncTest app = new VolatileSyncTest();
        new Thread(app.new LoopTask()).start();
        new Thread(app.new LoopController()).start();
    }

    private class LoopTask implements Runnable {
        @Override
        public void run() {
            while (loop) {
                // TODO something
                // 保證可見性
                synchronized (this) {}
            }
            System.out.println("stop...");
        }
    }

    private class LoopController implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            loop = false;
        }
    }
}

上述代碼就能夠正常結束。其實這個和volatile關鍵字的效果一樣,保證內存對線程可見。

二、ReentranLock同步鎖
ReentranLock相對synchronized提高了擴展性以及使用靈活性,比如具有嗅探鎖定、多路分支通知等功能。
ReentranLock單純作為鎖的使用還是很簡單的,比如:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
    private final Lock lock = new ReentrantLock();
    /**
     * 無限制等待獲取鎖資源,不會被中斷
     */
    public void test1() {
        try {
            lock.lock();
            // TODO
            System.out.println("lock");
        } finally {
            lock.unlock();
        }
    }
    /**
     * 有超時時間獲取鎖資源,會被中斷
     */
    public void test2() {
        boolean hasLock = false;
        try {
            hasLock = lock.tryLock(5000L, TimeUnit.MILLISECONDS);
            if (!hasLock) {
                System.out.println("not has Lock");
                return;
            }
            // TODO
            System.out.println("tryLock#timed");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (hasLock) {
                lock.unlock();
            }
        }
    }
    /**
     * 立即返回試獲取鎖資源
     */
    public void test3() {
        boolean hasLock = false;
        try {
            hasLock = lock.tryLock();
            if (!hasLock) {
                System.out.println("not has Lock");
                return;
            }
            // TODO
            System.out.println("tryLock");
        } finally {
            if (hasLock) lock.unlock();
        }
    }
    /**
     * 無限制等待鎖資源,但是會被中斷
     */
    public void test4() {
        try {
            lock.lockInterruptibly();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

上面代碼給出了四種方式,第一種無時間限制的等待獲取鎖資源,第二種則是有時間限制的等待,如果超過時間限制,則不再競爭鎖資源,第三種是立即返回式獲取鎖資源,不論資源是否獲取到,都會立馬返回,第四種也是無限制時間等待獲取鎖資源,但是能夠被中斷,其中第二種和第三種都會返回是否獲取到鎖,所以我們需要根據其返回來判斷是否已經獲取到鎖資源;因為是使用的同一個lock對象調用的,所以這三個方法之間存在競爭關系;使用的時候一定要注意finally里面的unlock方法,一定不能丟,不然很容易出現死鎖。
ReentranLock有兩個構造函數,一個是不帶參,一個是帶boolean類型參數:

/**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

從源碼中我們知道,它有公平鎖和非公平鎖兩種;公平鎖是按照FIFO先進先出的順序來的,而非公平鎖則是搶占式,和線程的優先級有一定關系;由于保證獲取鎖的公平性存在性能損耗,所以不是很必要的情況,不用去追求公平,非公平鎖在多線程的性能表現上更加優秀;線程從掛起到真正運行的中間存在較大延時,那么在公平鎖的情況下幾乎每次鎖被釋放,讓下一個申請鎖的線程都會經歷這個過程,而非公平鎖則能在一定程度上規避這個問題,因為很有可能在某個線程A處理完釋放鎖,然后立馬另外一個線程C去申請鎖,這樣就規避了C從掛起到真正運行的損耗。
然后我們再看一下其他方法:

// 獲取持有數
public int getHoldCount() {
     return sync.getHoldCount();
}
// 判斷是否被當前線程持有
public boolean isHeldByCurrentThread() {
     return sync.isHeldExclusively();
}
// 判斷是否被任意一個線程持有 
public boolean isLocked() {
    return sync.isLocked();
}
// 獲取擁有鎖的線程
protected Thread getOwner() {
    return sync.getOwner();
}
// 判斷是否有線程在等待鎖資源
public final boolean hasQueuedThreads() {
    return sync.hasQueuedThreads();
}
// 判斷線程是否在等待隊列中
public final boolean hasQueuedThread(Thread thread) {
    return sync.isQueued(thread);
}
// 獲取等待隊列的長度
public final int getQueueLength() {
    return sync.getQueueLength();
}
// 獲取等待隊列
protected Collection<Thread> getQueuedThreads() {
    return sync.getQueuedThreads();
}

以上就是ReentranLock中的一些方法,在設計程序的時候可以參考使用。
三、ReentranReadWriteLock讀寫同步鎖
在真實場景中,并不是所有操作都對同一個共享資源都是互斥的,比如讀取,也就是說兩個讀取操作可以并行執行;只有在讀寫、寫寫的時候保證數據線程安全,那么為了應對這種情況就產生了讀寫鎖。
以下是讀寫鎖的簡單使用

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {

    private ReadWriteLock lock = new ReentrantReadWriteLock();

    /**
     * 無限制時間等待寫鎖,不會被中斷
     */
    public void test1() {
        try {
            lock.readLock().lock();
        } finally {
            lock.readLock().unlock();
        }
    }

    /**
     * 無限制時間等待讀鎖,不會被中斷
     */
    public void test2() {
        try {
            lock.writeLock().lock();
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * 立即返回式獲取讀鎖
     */
    public void test3() {
        boolean hasReadLock = false;
        try {
            hasReadLock = lock.readLock().tryLock();
            if (!hasReadLock) return;
            // TODO
        } finally {
            if (hasReadLock) {
                lock.readLock().unlock();
            }
        }
    }

    /**
     * 立即返回式獲取寫鎖
     */
    public void test4() {
        boolean hasWriteLock = false;
        try {
            hasWriteLock = lock.writeLock().tryLock();
            if (!hasWriteLock) return;
        } finally {
            if (hasWriteLock) {
                lock.writeLock().unlock();
            }
        }
    }

    /**
     * 有時間限制的等待讀鎖
     */
    public void test5() {
        boolean hasReadLock = false;
        try {
            hasReadLock = lock.readLock().tryLock(5000L, TimeUnit.MILLISECONDS);
            if (!hasReadLock) return;
            // TODO
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (hasReadLock) {
                lock.readLock().unlock();
            }
        }
    }

    /**
     * 有時間限制的等待寫鎖
     */
    public void test6() {
        boolean hasWriteLock = false;
        try {
            hasWriteLock = lock.writeLock().tryLock(5000L, TimeUnit.MILLISECONDS);
            if (!hasWriteLock) return;
            // TODO
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (hasWriteLock) {
                lock.writeLock().unlock();
            }
        }
    }

    /**
     * 無限制時間的等待讀鎖,但是會被中斷
     */
    public void test7() {
        try {
            lock.readLock().lockInterruptibly();
            // TODO
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.readLock().unlock();
        }
    }

    /**
     * 無限制時間的等待寫鎖,但是會被中斷
     */
    public void test8() {
        try {
            lock.writeLock().lockInterruptibly();
            // TODO
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.writeLock().unlock();
        }
    }
}

我們可以看到其實和ReentranLock的使用方法大同小異,只不過內部實現是有一定區別的。

總的來說,使用最為簡單的是synchronized同步方法,在JDK1.5之前synchronized的性能確實跟不上ReentranLock,但是后面對它改造之后,性能上已經不再是瓶頸;而相對來說ReentranLock、ReentranReadWriteLock的使用則更加靈活,但是需要注意的點也相對多一些。
這篇文章僅僅只是對這三者的用法做了簡單的對比分析,關于同步還有另外很多知識點,后續會介紹等待通知、線程間通信等等,這些都和這三者有關系,尤其是等待通知,這也是Lock比synchronized更為靈活的地方,JDK很多基礎庫都用到了這點;然后還有必要去分析一下Lock的源碼實現,能夠想到的是肯定與CAS有關。

如果有不正確的地方,請幫忙指正,謝謝!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容