并發(fā)編程中最常出現(xiàn)的情形就是多個線程共享一個資源嗤堰,這些共享的資源很可能導致錯誤或者數(shù)據(jù)不一致的情形戴质,需要想辦法來解決這種問題。
臨界區(qū)(critical section):最多只能有一個線程執(zhí)行的代碼塊來訪問某些共享資源踢匣。
一般鎖能夠防止多個線程同時訪問某些資源告匠,但是有些鎖可以允許多個線程并發(fā)的讀取共享資源,比如讀寫鎖离唬。
兩個基本的鎖機制:
- synchronized關(guān)鍵字后专,之前也提到過
- Lock接口
鎖的注意事項
鎖是最常用的同步方法之一。但是在高并發(fā)情況下鎖的競爭會導致程序的性能下降输莺。為了降低這種副作用戚哎,這里有一些使用鎖的建議。
- 減少鎖的持有時間嫂用。
- 減小鎖的粒度型凳,即縮小鎖作用的對象范圍。
- 鎖分離嘱函,如讀多寫少的場合甘畅,可以使用讀寫鎖。其余需要使用獨占鎖的時候,嘗試根據(jù)功能疏唾,分離鎖蓄氧。
- 鎖粗化:這個和減少鎖的持有時間相反,根據(jù)具體場景來衡量槐脏,假如某個線程不斷的請求喉童,同步和釋放鎖,也會浪費性能顿天,根據(jù)實際情況進行權(quán)衡泄朴。
在學習或者使用Java的過程中進程會遇到各種各樣的鎖的概念:公平鎖、非公平鎖露氮、自旋鎖、可重入鎖钟沛、偏向鎖畔规、輕量級鎖、重量級鎖恨统、讀寫鎖叁扫、互斥鎖等等。
這篇文章整理了各種常見的Java鎖:http://www.importnew.com/19472.html
Lock與synchronized的區(qū)別
在Lock接口出現(xiàn)前畜埋,Java都是依靠synchronized關(guān)鍵字的莫绣,在JavaSE5之后,新增了Lock接口以及相關(guān)類來實現(xiàn)類似功能悠鞍。
- Lock接口在使用時需要顯示的獲取和釋放鎖对室,synchronized關(guān)鍵字是隱式獲取和釋放鎖
- Lock接口更加靈活,在獲取鎖的時候可以指定更多的操作咖祭,可中斷鎖掩宜,超時獲取鎖,指定時間釋放鎖么翰,非阻塞的獲取鎖牺汤。
- Lock接口可以允許讀寫分離,多個讀浩嫌,但是只有一個寫
Lock可以說是synchronzed的增強版檐迟。
- 超時獲取鎖:在指定時間未獲取到鎖,則返回
- 非阻塞獲取鎖:當前線程嘗試獲取鎖码耐,如果未獲取到追迟,立刻返回,如果成功則獲取到鎖伐坏。
- 能被中斷的獲取鎖:獲取到鎖的線程可以響應中斷怔匣。
重入鎖-ReenterantLock
可重入鎖,也叫做遞歸鎖,指的是同一線程 外層函數(shù)獲得鎖之后 每瞒,內(nèi)層遞歸函數(shù)仍然有獲取該鎖的代碼金闽,但不受影響。
Java中重入鎖使用java.util.concurrent.locks.ReentrantLock
實現(xiàn)剿骨。syncrhonzed也是可重入鎖代芜。
// 一個簡單的案例
public class ReentrantLockTest implements Runnable{
private ReentrantLock lock = new ReentrantLock();
public void get() {
// get兩次加鎖,set又加鎖浓利。
lock.lock();
lock.lock();
System.out.println("線程當前ID:" + Thread.currentThread().getId());
set();
lock.unlock();
lock.unlock();
}
public void set() {
lock.lock();
System.out.println("線程當前ID:" + Thread.currentThread().getId());
lock.unlock();
}
@Override
public void run() {
get();
}
public static void main(String[] args) {
ReentrantLockTest test = new ReentrantLockTest();
new Thread(test,"A").start();
new Thread(test,"B").start();
new Thread(test,"C").start();
}
}
ReentrantLock有幾個重要方法:
- lock() : 獲得鎖挤庇,如果鎖已經(jīng)被占用,則等待
- lockInterruptibly() : 獲得鎖贷掖,但優(yōu)先響應中斷
- tryLock() : 無阻塞鎖嫡秕,如果成功返回true,失敗返回false苹威,該方法不等的昆咽,立刻返回
- tryLock(long time,TimeUnit unit) : 在給定時間內(nèi)嘗試獲得鎖
- unlock(): 釋放鎖
這幾個方法都比較簡單,可以自行嘗試牙甫。
讀寫鎖-ReadWriteLock
讀寫分離鎖可以有效的減少鎖競爭掷酗,以提升系統(tǒng)性能。
讀 | 寫 | |
---|---|---|
讀 | 非阻塞 | 阻塞 |
寫 | 阻塞 | 阻塞 |
- 讀讀不互斥
- 讀寫互斥
- 寫寫互斥
如果在系統(tǒng)中窟哺,讀操作的次數(shù)遠遠大于寫操作泻轰,則讀寫鎖就可以發(fā)揮最大的功效。JDK并發(fā)包中提供讀寫鎖的實現(xiàn)是ReentrantReadWriteLock且轨。
// 這個demo浮声,用來驗證讀寫鎖的性能比一般的鎖要好。
// 直接運行此demo殖告,程序幾秒鐘就可以運行完畢
// 如果注釋掉讀寫所阿蝶,讓sreadLock = sLock, sWriteLock = sLock。那么程序要運行20多秒才結(jié)束
public class ReadWriteLockDemo {
private static Lock sLock = new ReentrantLock();
private static ReadWriteLock sReadWriteLock = new ReentrantReadWriteLock();
// 分別獲取讀寫鎖
private static Lock sReadLock = sReadWriteLock.readLock();
// private static Lock sReadLock = sLock;
private static Lock sWriteLock = sReadWriteLock.writeLock();
// private static Lock sWriteLock = sLock;
private int value;
// 模擬讀操作
public int read() throws InterruptedException {
try {
sReadLock.lock();
Thread.sleep(1000);
return value;
}finally {
sReadLock.unlock();
}
}
// 模擬寫操作
public void write(int index) throws InterruptedException {
try {
sWriteLock.lock();
Thread.sleep(1000);
value = index;
}finally {
sWriteLock.unlock();
}
}
static class ReadRunnable implements Runnable{
private ReadWriteLockDemo mDemo;
public ReadRunnable(ReadWriteLockDemo demo) {
mDemo = demo;
}
@Override
public void run() {
try {
mDemo.read();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static class WriteRunnable implements Runnable{
private ReadWriteLockDemo mDemo;
public WriteRunnable(ReadWriteLockDemo demo) {
mDemo = demo;
}
@Override
public void run() {
try {
mDemo.write(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
ReadRunnable readRunnable = new ReadRunnable(demo);
WriteRunnable writeRunnable = new WriteRunnable(demo);
for (int i = 0; i < 20; i++) {
if (i < 18){
new Thread(readRunnable).start();
}else {
new Thread(writeRunnable).start();
}
}
}
}
鎖的公平性
重入鎖和讀寫鎖黄绩,構(gòu)造器都提供了一個參數(shù)fair
羡洁,允許你控制鎖的公平性,默認情況下是不公平的爽丹。
非公平鎖:在多個線程搶占鎖的時候筑煮,系統(tǒng)隨機挑選一個線程持有鎖。
公平鎖:多個線程搶占鎖的時候粤蝎,系統(tǒng)挑選等待時間最長的線程持有鎖真仲。
我們只需要在構(gòu)造鎖對象的時候,傳入true參數(shù)即可獲得公平鎖對象初澎。
new ReentrantLock(true);
new ReentrantReadWriteLock(true);
Condition
在JDK內(nèi)部秸应,重入鎖和Condition對象經(jīng)常一起用到虑凛。之前我們在并發(fā)基礎(chǔ)中提到過wait與notify要與synchronized關(guān)鍵字配合使用。Condition與重入鎖一起使用软啼,功能與wait和notify類似桑谍。
Lock接口的newCondition()方法可以生成一個與當前重入鎖綁定的Condition實例,利用Condition我們可以讓線程在特定的時間等待祸挪,在特定的時刻受到通知锣披。
以ArrayBlockingQueue為例:
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
//存儲數(shù)據(jù)元素
final Object[] items;
/** 主要的鎖 */
final ReentrantLock lock;
/** 等待taoke的條件 */
private final Condition notEmpty;
/** 等待put的條件 */
private final Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
//構(gòu)造鎖與condition
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
//如果隊列為空,等待隊列非空的信號
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
// 如果入隊成功贿条,發(fā)出不空的信號
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
//發(fā)信號
notEmpty.signal();
}
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
//如果隊列已滿雹仿,等待隊列有足夠的空間
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
//如果出隊成功,發(fā)出 不滿 的信號
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
//發(fā)信號
notFull.signal();
return x;
}
}
最后
這篇文章主要說了下使用鎖的時候需要注意的幾點整以,然后提了重入鎖與讀寫鎖胧辽。公平鎖,最后說了重入鎖的搭檔Condition公黑。
篇幅有限票顾,沒辦法面面俱到,感興趣的還望自己再摸索帆调。希望能幫助大家。
參考
- Java中synchronized的實現(xiàn)原理與應用
- Java鎖的種類以及辨析(四):可重入鎖
- 《Java高并發(fā)程序設(shè)計》
- 《并發(fā)編程的藝術(shù)》