在分布式開發中,鎖是線程控制的重要途徑。Java為此也提供了2種鎖機制,synchronized和lock。做為Java愛好者,自然少不了對比一下這2種機制,也能從中學到些分布式開發需要注意的地方。
我們先從最簡單的入手,逐步分析這2種的區別。
一、synchronized和lock的用法區別
synchronized:在需要同步的對象中加入此控制,synchronized可以加在方法上,也可以加在特定代碼塊中,括號中表示需要鎖的對象。
lock:需要顯示指定起始位置和終止位置。一般使用ReentrantLock類做為鎖,多個線程中必須要使用一個ReentrantLock類做為對象才能保證鎖的生效。且在加鎖和解鎖處需要通過lock()和unlock()顯示指出。所以一般會在finally塊中寫unlock()以防死鎖。
用法區別比較簡單,這里不贅述了,如果不懂的可以看看Java基本語法。
二、synchronized和lock性能區別
synchronized是托管給JVM執行的,而lock是java寫的控制鎖的代碼。在Java1.5中,synchronize是性能低效的。因為這是一個重量級操作,需要調用操作接口,導致有可能加鎖消耗的系統時間比加鎖以外的操作還多。相比之下使用Java提供的Lock對象,性能更高一些。但是到了Java1.6,發生了變化。synchronize在語義上很清晰,可以進行很多優化,有適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導致在Java1.6上synchronize的性能并不比Lock差。官方也表示,他們也更支持synchronize,在未來的版本中還有優化余地。
說到這里,還是想提一下這2中機制的具體區別。據我所知,synchronized原始采用的是CPU悲觀鎖機制,即線程獲得的是獨占鎖。獨占鎖意味著其他線程只能依靠阻塞來等待線程釋放鎖。而在CPU轉換線程阻塞時會引起線程上下文切換,當有很多線程競爭鎖的時候,會引起CPU頻繁的上下文切換導致效率很低。
而Lock用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。樂觀鎖實現的機制就是CAS操作(Compare and Swap)。我們可以進一步研究ReentrantLock的源代碼,會發現其中比較重要的獲得鎖的一個方法是compareAndSetState。這里其實就是調用的CPU提供的特殊指令。
現代的CPU提供了指令,可以自動更新共享數據,而且能夠檢測到其他線程的干擾,而 compareAndSet() 就用這些代替了鎖定。這個算法稱作非阻塞算法,意思是一個線程的失敗或者掛起不應該影響其他線程的失敗或掛起的算法。
我也只是了解到這一步,具體到CPU的算法如果感興趣的讀者還可以在查閱下,如果有更好的解釋也可以給我留言,我也學習下。
三、synchronized和lock用途區別
synchronized原語和ReentrantLock在一般情況下沒有什么區別,但是在非常復雜的同步應用中,請考慮使用ReentrantLock,特別是遇到下面2種需求的時候。
1.某個線程在等待一個鎖的控制權的這段時間需要中斷
2.需要分開處理一些wait-notify,ReentrantLock里面的Condition應用,能夠控制notify哪個線程
3.具有公平鎖功能,每個到來的線程都將排隊等候
下面細細道來……
先說第一種情況,ReentrantLock的lock機制有2種,忽略中斷鎖和響應中斷鎖,這給我們帶來了很大的靈活性。比如:如果A、B2個線程去競爭鎖,A線程得到了鎖,B線程等待,但是A線程這個時候實在有太多事情要處理,就是一直不返回,B線程可能就會等不及了,想中斷自己,不再等待這個鎖了,轉而處理其他事情。這個時候ReentrantLock就提供了2種機制,第一,B線程中斷自己(或者別的線程中斷它),但是ReentrantLock不去響應,繼續讓B線程等待,你再怎么中斷,我全當耳邊風(synchronized原語就是如此);第二,B線程中斷自己(或者別的線程中斷它),ReentrantLock處理了這個中斷,并且不再等待這個鎖的到來,完全放棄。(如果你沒有了解java的中斷機制,請參考下相關資料,再回頭看這篇文章,80%的人根本沒有真正理解什么是java的中斷,呵呵)
這里來做個試驗,首先搞一個Buffer類,它有讀操作和寫操作,為了不讀到臟數據,寫和讀都需要加鎖,我們先用synchronized原語來加鎖,如下:
查看源代碼打印幫助
1
public class Buffer {
2
3
private Object lock;
4
5
public Buffer() {
6
lock = this;
7
}
8
9
public void write() {
10
synchronized (lock) {
11
long startTime = System.currentTimeMillis();
12
System.out.println("開始往這個buff寫入數據…");
13
for (;;)// 模擬要處理很長時間
14
{
15
if (System.currentTimeMillis()
16
- startTime > Integer.MAX_VALUE)
17
break;
18
}
19
System.out.println("終于寫完了");
20
}
21
}
22
23
public void read() {
24
synchronized (lock) {
25
System.out.println("從這個buff讀數據");
26
}
27
}
28
}
接著,我們來定義2個線程,一個線程去寫,一個線程去讀。
1
public class Writer extends Thread {
2
3
private Buffer buff;
4
5
public Writer(Buffer buff) {
6
this.buff = buff;
7
}
8
9
@Override
10
public void run() {
11
buff.write();
12
}
13
14
}
15
16
public class Reader extends Thread {
17
18
private Buffer buff;
19
20
public Reader(Buffer buff) {
21
this.buff = buff;
22
}
23
24
@Override
25
public void run() {
26
27
buff.read();//這里估計會一直阻塞
28
29
System.out.println("讀結束");
30
31
}
32
33
}
好了,寫一個Main來試驗下,我們有意先去“寫”,然后讓“讀”等待,“寫”的時間是無窮的,就看“讀”能不能放棄了。
1
public class Test {
2
public static void main(String[] args) {
3
Buffer buff = new Buffer();
4
5
final Writer writer = new Writer(buff);
6
final Reader reader = new Reader(buff);
7
8
writer.start();
9
reader.start();
10
11
new Thread(new Runnable() {
12
13
@Override
14
public void run() {
15
long start = System.currentTimeMillis();
16
for (;;) {
17
//等5秒鐘去中斷讀
18
if (System.currentTimeMillis()
19
- start > 5000) {
20
System.out.println("不等了,嘗試中斷");
21
reader.interrupt();
22
break;
23
}
24
25
}
26
27
}
28
}).start();
29
30
}
31
}
我們期待“讀”這個線程能退出等待鎖,可是事與愿違,一旦讀這個線程發現自己得不到鎖,就一直開始等待了,就算它等死,也得不到鎖,因為寫線程要21億秒才能完成 T_T ,即使我們中斷它,它都不來響應下,看來真的要等死了。這個時候,ReentrantLock給了一種機制讓我們來響應中斷,讓“讀”能伸能屈,勇敢放棄對這個鎖的等待。我們來改寫Buffer這個類,就叫BufferInterruptibly吧,可中斷緩存。
查看源代碼打印幫助
1
import java.util.concurrent.locks.ReentrantLock;
2
3
public class BufferInterruptibly {
4
5
private ReentrantLock lock = new ReentrantLock();
6
7
public void write() {
8
lock.lock();
9
try {
10
long startTime = System.currentTimeMillis();
11
System.out.println("開始往這個buff寫入數據…");
12
for (;;)// 模擬要處理很長時間
13
{
14
if (System.currentTimeMillis()
15
- startTime > Integer.MAX_VALUE)
16
break;
17
}
18
System.out.println("終于寫完了");
19
} finally {
20
lock.unlock();
21
}
22
}
23
24
public void read() throws InterruptedException {
25
lock.lockInterruptibly();// 注意這里,可以響應中斷
26
try {
27
System.out.println("從這個buff讀數據");
28
} finally {
29
lock.unlock();
30
}
31
}
32
33
}
當然,要對reader和writer做響應的修改
查看源代碼打印幫助
1
public class Reader extends Thread {
2
3
private BufferInterruptibly buff;
4
5
public Reader(BufferInterruptibly buff) {
6
this.buff = buff;
7
}
8
9
@Override
10
public void run() {
11
12
try {
13
buff.read();//可以收到中斷的異常,從而有效退出
14
} catch (InterruptedException e) {
15
System.out.println("我不讀了");
16
}
17
18
System.out.println("讀結束");
19
20
}
21
22
}
23
24
/**
25
* Writer倒不用怎么改動
26
*/
27
public class Writer extends Thread {
28
29
private BufferInterruptibly buff;
30
31
public Writer(BufferInterruptibly buff) {
32
this.buff = buff;
33
}
34
35
@Override
36
public void run() {
37
buff.write();
38
}
39
40
}
41
42
public class Test {
43
public static void main(String[] args) {
44
BufferInterruptibly buff = new BufferInterruptibly();
45
46
final Writer writer = new Writer(buff);
47
final Reader reader = new Reader(buff);
48
49
writer.start();
50
reader.start();
51
52
new Thread(new Runnable() {
53
54
@Override
55
public void run() {
56
long start = System.currentTimeMillis();
57
for (;;) {
58
if (System.currentTimeMillis()
59
- start > 5000) {
60
System.out.println("不等了,嘗試中斷");
61
reader.interrupt();
62
break;
63
}
64
65
}
66
67
}
68
}).start();
69
70
}
71
}
這次“讀”線程接收到了lock.lockInterruptibly()中斷,并且有效處理了這個“異常”。
至于第二種情況,ReentrantLock可以與Condition的配合使用,Condition為ReentrantLock鎖的等待和釋放提供控制邏輯。
例如,使用ReentrantLock加鎖之后,可以通過它自身的Condition.await()方法釋放該鎖,線程在此等待Condition.signal()方法,然后繼續執行下去。await方法需要放在while循環中,因此,在不同線程之間實現并發控制,還需要一個volatile的變量,boolean是原子性的變量。因此,一般的并發控制的操作邏輯如下所示:
1
volatile boolean isProcess = false;
2
ReentrantLock lock = new ReentrantLock();
3
Condtion processReady = lock.newCondtion();
4
thread: run() {
5
lock.lock();
6
isProcess = true;
7
try {
8
while(!isProcessReady) { //isProcessReady 是另外一個線程的控制變量
9
processReady.await();//釋放了lock,在此等待signal
10
}catch (InterruptedException e) {
11
Thread.currentThread().interrupt();
12
} finally {
13
lock.unlock();
14
isProcess = false;
15
}
16
}
17
}
18
}
這里只是代碼使用的一段簡化,下面我們看Hadoop的一段摘取的源碼:
查看源代碼打印幫助
1
private class MapOutputBuffer<K extends Object, V extends Object>
2
implements MapOutputCollector<K, V>, IndexedSortable {
3
...
4
boolean spillInProgress;
5
final ReentrantLock spillLock = new ReentrantLock();
6
final Condition spillDone = spillLock.newCondition();
7
final Condition spillReady = spillLock.newCondition();
8
volatile boolean spillThreadRunning = false;
9
final SpillThread spillThread = new SpillThread();
10
...
11
public MapOutputBuffer(TaskUmbilicalProtocol umbilical, JobConf job,
12
TaskReporter reporter
13
) throws IOException, ClassNotFoundException {
14
...
15
spillInProgress = false;
16
spillThread.setDaemon(true);
17
spillThread.setName("SpillThread");
18
spillLock.lock();
19
try {
20
spillThread.start();
21
while (!spillThreadRunning) {
22
spillDone.await();
23
}
24
} catch (InterruptedException e) {
25
throw new IOException("Spill thread failed to initialize", e);
26
} finally {
27
spillLock.unlock();
28
}
29
}
30
31
protected class SpillThread extends Thread {
32
33
@Override
34
public void run() {
35
spillLock.lock();
36
spillThreadRunning = true;
37
try {
38
while (true) {
39
spillDone.signal();
40
while (!spillInProgress) {
41
spillReady.await();
42
}
43
try {
44
spillLock.unlock();
45
sortAndSpill();
46
} catch (Throwable t) {
47
sortSpillException = t;
48
} finally {
49
spillLock.lock();
50
if (bufend < bufstart) {
51
bufvoid = kvbuffer.length;
52
}
53
kvstart = kvend;
54
bufstart = bufend;
55
spillInProgress = false;
56
}
57
}
58
} catch (InterruptedException e) {
59
Thread.currentThread().interrupt();
60
} finally {
61
spillLock.unlock();
62
spillThreadRunning = false;
63
}
64
}
65
}
代碼中spillDone 就是 spillLock的一個newCondition()。調用spillDone.await()時可以釋放spillLock鎖,線程進入阻塞狀態,而等待其他線程的 spillDone.signal()操作時,就會喚醒線程,重新持有spillLock鎖。
這里可以看出,利用lock可以使我們多線程交互變得方便,而使用synchronized則無法做到這點。
最后呢,ReentrantLock這個類還提供了2種競爭鎖的機制:公平鎖和非公平鎖。這2種機制的意思從字面上也能了解個大概:即對于多線程來說,公平鎖會依賴線程進來的順序,后進來的線程后獲得鎖。而非公平鎖的意思就是后進來的鎖也可以和前邊等待鎖的線程同時競爭鎖資源。對于效率來講,當然是非公平鎖效率更高,因為公平鎖還要判斷是不是線程隊列的第一個才會讓線程獲得鎖。
原文地址:http://www.365doit.com/all/news/synchronizeandlock.html/4
- JVM
- 深入理解Java內存模型
- 深入理解Java內存模型(一)——基礎
- 深入理解Java內存模型(二)——重排序
- 深入理解Java內存模型(三)——順序一致性
- 深入理解Java內存模型(四)——volatile
- 深入理解Java內存模型(五)——鎖
- 深入理解Java內存模型(六)——final
- 深入理解Java內存模型(七)——總結
- Java內存模型
- Java內存模型2
- 堆內內存還是堆外內存?
- JVM內存配置詳解
- Java內存分配全面淺析
- 深入Java核心 Java內存分配原理精講
- jvm常量池
- JVM調優總結
- JVM調優總結(一)-- 一些概念
- JVM調優總結(二)-一些概念
- VM調優總結(三)-基本垃圾回收算法
- JVM調優總結(四)-垃圾回收面臨的問題
- JVM調優總結(五)-分代垃圾回收詳述1
- JVM調優總結(六)-分代垃圾回收詳述2
- JVM調優總結(七)-典型配置舉例1
- JVM調優總結(八)-典型配置舉例2
- JVM調優總結(九)-新一代的垃圾回收算法
- JVM調優總結(十)-調優方法
- 基礎
- Java 征途:行者的地圖
- Java程序員應該知道的10個面向對象理論
- Java泛型總結
- 序列化與反序列化
- 通過反編譯深入理解Java String及intern
- android 加固防止反編譯-重新打包
- volatile
- 正確使用 Volatile 變量
- 異常
- 深入理解java異常處理機制
- Java異常處理的10個最佳實踐
- Java異常處理手冊和最佳實踐
- Java提高篇——對象克隆(復制)
- Java中如何克隆集合——ArrayList和HashSet深拷貝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常見正則表達式
- 類
- 理解java類加載器以及ClassLoader類
- 深入探討 Java 類加載器
- 類加載器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之實現細節
- java.util.concurrent 之ConcurrentHashMap 源碼分析
- HashMap的實現原理和底層數據結構
- 線程
- 關于Java并發編程的總結和思考
- 40個Java多線程問題總結
- Java中的多線程你只要看這一篇就夠了
- Java多線程干貨系列(1):Java多線程基礎
- Java非阻塞算法簡介
- Java并發的四種風味:Thread、Executor、ForkJoin和Actor
- Java中不同的并發實現的性能比較
- JAVA CAS原理深度分析
- 多個線程之間共享數據的方式
- Java并發編程
- Java并發編程(1):可重入內置鎖
- Java并發編程(2):線程中斷(含代碼)
- Java并發編程(3):線程掛起、恢復與終止的正確方法(含代碼)
- Java并發編程(4):守護線程與線程阻塞的四種情況
- Java并發編程(5):volatile變量修飾符—意料之外的問題(含代碼)
- Java并發編程(6):Runnable和Thread實現多線程的區別(含代碼)
- Java并發編程(7):使用synchronized獲取互斥鎖的幾點說明
- Java并發編程(8):多線程環境中安全使用集合API(含代碼)
- Java并發編程(9):死鎖(含代碼)
- Java并發編程(10):使用wait/notify/notifyAll實現線程間通信的幾點重要說明
- java并發編程-II
- Java多線程基礎:進程和線程之由來
- Java并發編程:如何創建線程?
- Java并發編程:Thread類的使用
- Java并發編程:synchronized
- Java并發編程:Lock
- Java并發編程:volatile關鍵字解析
- Java并發編程:深入剖析ThreadLocal
- Java并發編程:CountDownLatch、CyclicBarrier和Semaphore
- Java并發編程:線程間協作的兩種方式:wait、notify、notifyAll和Condition
- Synchronized與Lock
- JVM底層又是如何實現synchronized的
- Java synchronized詳解
- synchronized 與 Lock 的那點事
- 深入研究 Java Synchronize 和 Lock 的區別與用法
- JAVA編程中的鎖機制詳解
- Java中的鎖
- TreadLocal
- 深入JDK源碼之ThreadLocal類
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的內存泄露
- 多線程設計模式
- Java多線程編程中Future模式的詳解
- 原子操作(CAS)
- [譯]Java中Wait、Sleep和Yield方法的區別
- 線程池
- 如何合理地估算線程池大小?
- JAVA線程池中隊列與池大小的關系
- Java四種線程池的使用
- 深入理解Java之線程池
- java并發編程III
- Java 8并發工具包漫游指南
- 聊聊并發
- 聊聊并發(一)——深入分析Volatile的實現原理
- 聊聊并發(二)——Java SE1.6中的Synchronized
- 文件
- 網絡
- index
- 內存文章索引
- 基礎文章索引
- 線程文章索引
- 網絡文章索引
- IOC
- 設計模式文章索引
- 面試
- Java常量池詳解之一道比較蛋疼的面試題
- 近5年133個Java面試問題列表
- Java工程師成神之路
- Java字符串問題Top10
- 設計模式
- Java:單例模式的七種寫法
- Java 利用枚舉實現單例模式
- 常用jar
- HttpClient和HtmlUnit的比較總結
- IO
- NIO
- NIO入門
- 注解
- Java Annotation認知(包括框架圖、詳細介紹、示例說明)