什麼是線程安全


        線程安全定義:

       當多個線程訪問一個類的時候,如果不用考慮這些線程在運行時環境下的調度和交替執行,並且不需要額外的同步及在調用方法代碼不必做其他的協調,這個類的行爲任然是正確的,那麼稱這個類是線程安全的。

        所有的例子都是一個servlet用來進行因數分解,通過request傳入一個數字,然後service方法調用factor方法進行因數分解。

       1. 無狀態的對象永遠線程安全

public class StatelessFactorizer extends GenericServlet implements Servlet {

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(resp, factors);
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[] { i };
    }
}
        這個類沒有一個私有或公共的變量,是一個無狀態的類。因爲多個線程進入service方法,所有的service方法內的局部變量都封裝在各個線程的棧中,只有本線程可以訪問,所以,這個類是線程安全的。

        2. 原子性

@NotThreadSafe
public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
    private long count = 0;

    public long getCount() {
        return count;
    }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++count;
        encodeIntoResponse(resp, factors);
    }

    void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[] { i };
    }
}
          我們在第一個例子上,添加了一個功能計數。每調一次service方法,count加1。很可惜,這個類不是線程安全的,因爲++count不是一個原子操作,這是一個讀-改-寫三個步驟的操作。這樣我們就很容易理解這個類爲什麼不是線程安全的了,線程A讀到count值爲1,然後修改爲2。在線程A將count的值寫回去之前,線程B讀取了count的值。結果是線程A將count的值改爲2,線程B也將count的值改爲2。至於什麼是原子操作,我也沒看懂,定義如下:

        假設有操作A和B,如果從執行A的線程的角度看,當其他線程執行B的時候,要麼B全部執行完成,要麼一點也沒有執行,這樣A和B互爲原子操作。一個原子操作是指:該操作對於所有的操作,包括它自己,都滿足前面描述的狀態。(我已經暈了)

        讓count加一操作變成原子操作的代碼如下:

@ThreadSafe
public class CountingFactorizer extends GenericServlet implements Servlet {
	private final AtomicLong count = new AtomicLong(0);

	public long getCount() {
		return count.get();
	}

	public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = factor(i);
		count.incrementAndGet();
		encodeIntoResponse(resp, factors);
	}

	void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {
	}

	BigInteger extractFromRequest(ServletRequest req) {
		return null;
	}

	BigInteger[] factor(BigInteger i) {
		return null;
	}
}
        AtomicLong是什麼鬼,這個大家去問度娘吧。簡單說,它在讀-改-寫的第三步寫操作時還會回去查看值是否已經被改變,如果被改變,重新加載。(可以百度搜索java cas)


     3. 鎖

      我們上面所說的自增操作,通過atomic類變成了原子操作,因此類也就變成線程安全的了。但是如果有多個全局變量,並且這多個全局變量的值具有關聯性。場景如下:我們想實現應對連續兩個客戶請求相同的數字進行因數分解,於是我們緩存了最新的計算結果

@NotThreadSafe
public class UnsafeCachingFactorizer extends GenericServlet implements Servlet {
	private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
	private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();

	public void service(ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
		if (i.equals(lastNumber.get()))
			encodeIntoResponse(resp, lastFactors.get());
		else {
			BigInteger[] factors = factor(i);
			lastNumber.set(i);
			lastFactors.set(factors);
			encodeIntoResponse(resp, factors);
		}
	}

	void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
	}

	BigInteger extractFromRequest(ServletRequest req) {
		return new BigInteger("7");
	}

	BigInteger[] factor(BigInteger i) {
		// Doesn't really factor
		return new BigInteger[] { i };
	}
這個類不是線程安全的,因爲lastNumber和lastFactors這兩個變量是有關係的,不是獨立的。我們的atomic只能保證他們自身的操作是線程安全,無法保證兩個一起是線程安全的。假設,lastNumber被線程A修改爲了10,而lastFactors還是舊值9的因數分解結果,那線程B剛好請求數字10的因數分解結果,就拿到了9的因數分解結果。

        這裏我們很容易想到用synchronized關鍵字來給service方法加鎖,我就不貼代碼了,這樣做的確能夠解決問題,但是service方法一次只能進入一個線程了,性能存在很大問題。我們在做併發編程的時候,一定要兼顧性能,最好的方式如下所示:

@ThreadSafe
public class CachedFactorizer extends GenericServlet implements Servlet {
    @GuardedBy("this") private BigInteger lastNumber;
    @GuardedBy("this") private BigInteger[] lastFactors;
    @GuardedBy("this") private long hits;
    @GuardedBy("this") private long cacheHits;

    public synchronized long getHits() {
        return hits;
    }

    public synchronized double getCacheHitRatio() {
        return (double) cacheHits / (double) hits;
    }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = null;
        synchronized (this) {
            ++hits;
            if (i.equals(lastNumber)) {
                ++cacheHits;
                factors = lastFactors.clone();
            }
        }
        if (factors == null) {
            factors = factor(i);
            synchronized (this) {
                lastNumber = i;
                lastFactors = factors.clone();
            }
        }
        encodeIntoResponse(resp, factors);
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[]{i};
    }
}
          把鎖打碎在方法裏面,第一個synchronized塊保護着檢查再運行的操作,另一個保證緩存的number和factor同步更新。提醒一點,在加鎖期間,不要進行耗時操作,我們把鎖移到方法裏面,主要目的就是將那些耗時操作剔除到同步塊之外。






發佈了55 篇原創文章 · 獲贊 5 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章