前言
今天看java并發程式設計實踐時,看到了線程安全這一塊,講到了java自帶的一個原子計數器,AtomicInteger 。我就很好奇它是怎麼實作線程安全的。我查詢了一下源碼:
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the updated value
*/
public final int addAndGet(int delta) {
for (;;) {
int current = get();
int next = current + delta;
if (compareAndSet(current, next))
return next;
}
}
這個代碼很簡單,但是有一個方法 compareAndSet(current,next),這是什麼東東?恩,再往下找~
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return true if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
unsafe是什麼東東,當我再次點選查詢源碼時,出現source not found。原來這是sun包下的。我就bing了一下 unsafe.compareAndSwapInt。找到了一篇好文:Java Magic. Part 4: sun.misc.Unsafe 。看完之後,感覺發現了新大陸。以下講的所有都是參考這個文章。
unsafe類簡介
java是一個安全的程式設計語言,它幫助程式猿避免犯一些愚蠢的錯誤,這些錯誤一般都是基于記憶體管理。但是,如果你就是想故意的犯一些這樣的錯誤,使用Unsafe類就對了。
Unsafe初始化
在使用前,我們需要建立Unsafe類的一個執行個體。建立Unsafe執行個體不能像建立普通類執行個體new一下就行,因為Unsafe類的構造方法是私有化的。它雖然有靜态的方法getUnsafe(),但是調用Unsafe.getUnsafe()這個方法時,有可能出現安全異常 SecurityException.
比較簡單的建立Unsafe類執行個體的方法
Unsafe類包含一個執行個體theUnsafe,它是private私有化的。我們可以通過反射的機制把它偷出來:
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
并發
Unsafe.compareAndSwap方法是原子的并且可以用來實作高性能不使用鎖的資料結構。
舉例,在多線程中使用共享對象來增加值。
首先我們定義一個簡單的接口:Counter
interface Counter {
void increment();
long getCounter();
}
然後我們定義一個工作線程CounterClient來使用Counter:
class CounterClient implements Runnable {
private Counter c;
private int num;
public CounterClient(Counter c, int num) {
this.c = c;
this.num = num;
}
@Override
public void run() {
for (int i = 0; i < num; i++) {
c.increment();
}
}
}
下面是測試代碼:
int NUM_OF_THREADS = 1000;
int NUM_OF_INCREMENTS = 100000;
ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
Counter counter = ... // creating instance of specific counter
long before = System.currentTimeMillis();
for (int i = 0; i < NUM_OF_THREADS; i++) {
service.submit(new CounterClient(counter, NUM_OF_INCREMENTS));
}
service.shutdown();
service.awaitTermination(1, TimeUnit.MINUTES);
long after = System.currentTimeMillis();
System.out.println("Counter result: " + c.getCounter());
System.out.println("Time passed in ms:" + (after - before));
首先實作一個非同步的計數器:
class StupidCounter implements Counter {
private long counter = 0;
@Override
public void increment() {
counter++;
}
@Override
public long getCounter() {
return counter;
}
}
輸出:
Counter result: 99542945
Time passed in ms: 679
程式運作時間非常短,但是沒有線程管理,是以結果有問題。第二種嘗試,使用簡單的java方式的同步:
class SyncCounter implements Counter {
private long counter = 0;
@Override
public synchronized void increment() {
counter++;
}
@Override
public long getCounter() {
return counter;
}
}
輸出:
Counter result: 100000000
Time passed in ms: 10136
激進的同步做法總是有效的,但是消耗時間非常長。我們接着試一下使用讀寫鎖:
class LockCounter implements Counter {
private long counter = 0;
private WriteLock lock = new ReentrantReadWriteLock().writeLock();
@Override
public void increment() {
lock.lock();
counter++;
lock.unlock();
}
@Override
public long getCounter() {
return counter;
}
}
輸出:
Counter result: 100000000
Time passed in ms: 8065
結果依然正确,耗時也比較短。使用原子類怎麼樣?
class AtomicCounter implements Counter {
AtomicLong counter = new AtomicLong(0);
@Override
public void increment() {
counter.incrementAndGet();
}
@Override
public long getCounter() {
return counter.get();
}
}
輸出:
Counter result: 100000000
Time passed in ms: 6552
AtomicCounter甚至更好。
最後,重頭戲來了,我們試一下使用Unsafe原生的compareAndSwapLong來實作計數器。
public class CASCounter implements Counter{
private volatile long counter = 0;
private Unsafe unsafe;
private long offset;
public CASCounter() throws Exception {
unsafe = getUnsafe();
offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
}
public Unsafe getUnsafe() {
Field f;
try {
f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
@Override
public void increment() {
long before = counter;
while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
before = counter;
}
}
@Override
public long getCounter() {
return counter;
}
}
輸出:
Counter result: 100000000
Time passed in ms: 6454
和原子實作差不多。是不是原子方式就是用Unsafe實作的?YES
事實上這個例子很簡單,但是展示出Unsafe一些能力。
就像之前說的,CAS原生可以用作實作不使用鎖的資料結構。實作原理很簡單:
- 擁有一些狀态
- 建立一個副本
- 修改它
- 執行CAS
- 如果失敗重複執行
老實說,在實際應用中,你遇到的困難超乎你的想象。比如很多類型ABA Problem。
如果你真的感興趣,可以參考一下lock-free HashMap的精彩實作。
note: 在counter變量前增加volatile關鍵字為了避免死循環。
總結
即使,Unsafe很有意思,但千萬不要使用。
另外,我之前說想看一下 Unsafe原生的compareAndSwapLong 源碼,我查完知道了這個方法使用c寫的... -_-||
個人感受
并發程式開發的确非常考驗技術,比如怎麼避免使用鎖,鎖會增加程式的運作時間,如果使用不好,會出現死鎖,導緻程式卡死。我實際工作中就遇到過死鎖的問題:
我寫的是一個用spring實作一個簡單的排程程式,其中有一個排程的功能是從遠端伺服器拉檔案到本地伺服器,定時器頻率是5分鐘一次。部署上線後,總是隔三差五的程式假死:查詢程式程序id存在,但是檢視業務日志,就是沒有列印。找了好幾天,才定位到問題:
1 由于網絡環境不穩定,從伺服器拉檔案這個動作可能會消耗很長時間。
2 代碼中控制拉檔案的一些參數:連接配接時間,讀取時間,逾時時間沒有起作用。
3 拉檔案這個方法加鎖了。
這樣問題就明晰了:
當定時器啟動一個線程執行拉檔案操作時,耗時非常長,過了五分鐘之後,定時器又啟動了另一個線程,因為拉檔案操作加鎖了,這個線程隻能等待...随着時間的流逝,定時器起的線程全部在等待第一個線程釋放鎖,無法再建立線程執行其他的排程任務,是以程式假死。
解決方案很簡單:
修複了代碼中拉檔案的參數,保證五分鐘内 要麼拉完檔案,要麼失敗,中斷網絡連接配接,釋放鎖。
轉載于:https://my.oschina.net/huaxiaoqiang/blog/2051483