首先,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有關。
如果有不正確的地方,請幫忙指正,謝謝!