天天看點

unsafe類在java中的應用舉例:原子計數器的實作

前言

今天看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