老曹:從構(gòu)造函數(shù)看線程安全
線程是編程中常用而且強大的手段,在使用過程中,我們經(jīng)常面對的就是線程安全問題了。對于Java中常見的數(shù)據(jù)結(jié)構(gòu)而言,一般的,ArrayList是非線程安全的,Vector是線程安全的;HashMap是非線程安全的,HashTable是線程安全的;StringBuilder是非線程安全的,StringBuffer是線程安全的。
然而,判斷代碼是否線程安全,不能夠想當(dāng)然,例如Java 中的構(gòu)造函數(shù)是否是線程安全的呢?
自己從***感覺來看,構(gòu)造函數(shù)應(yīng)該是線程安全的,如果一個對象沒有初始化完成,怎么可能存在競爭呢? 甚至在Java 的語言規(guī)范中也談到,沒有必要將constructor 置為synchronized,因為它在構(gòu)建過程中是鎖定的,其他線程是不可能調(diào)用還沒有實例化好的對象的。
但是,當(dāng)我讀過了Bruce Eckel 的博客文章,原來構(gòu)造函數(shù)也并不是線程安全的,本文中的示例代碼和解釋全部來自Bruce Eckel 的那篇文章。
演示的過程從 定義一個接口開始:
- // HasID.java
- public interface HasID {
- int getID();
- }
有各種方法可以實現(xiàn)這個接口,先看看靜態(tài)變量方式的實現(xiàn):
- // StaticIDField.java
- public class StaticIDField implements HasID {
- private static int counter = 0;
- private int id = counter++;
- public int getID() { return id; }
- }
這是一個簡單而無害的類,再構(gòu)造一個用于并行調(diào)用的測試類:
- // IDChecker.java
- import java.util.*;
- import java.util.function.*;
- import java.util.stream.*;
- import java.util.concurrent.*;
- import com.google.common.collect.Sets;
- public class IDChecker {
- public static int SIZE = 100000;
- static class MakeObjects
- implements Supplier<List<Integer>> {
- private Supplier<HasID> gen;
- public MakeObjects(Supplier<HasID> gen) {
- this.gen = gen;
- }
- @Override
- public List<Integer> get() {
- return
- Stream.generate(gen)
- .limit(SIZE)
- .map(HasID::getID)
- .collect(Collectors.toList());
- }
- }
- public static void test(Supplier<HasID> gen) {
- CompletableFuture<List<Integer>>
- groupA = CompletableFuture
- .supplyAsync(new MakeObjects(gen)),
- groupB = CompletableFuture
- .supplyAsync(new MakeObjects(gen));
- groupA.thenAcceptBoth(groupB, (a, b) -> {
- System.out.println(
- Sets.intersection(
- Sets.newHashSet(a),
- Sets.newHashSet(b)).size());
- }).join();
- }
- }
其中 MakeObjects 是一個 Supplier 通過get()方法產(chǎn)生一個 List. 這個 List 從 每個HasID 對象中得到一個ID。test() 方法創(chuàng)建了兩個并行的CompletableFutures 來運行MakeObjects suppliers, 然后就每個結(jié)果使用Guava庫的Sets.intersection() 來找出兩個List中有多少個共有的ID。現(xiàn)在,測試一下多個并發(fā)任務(wù)調(diào)用這個StaticIDField類的結(jié)果:
- // TestStaticIDField.java
- public class TestStaticIDField {
- public static void main(String[] args) {
- IDChecker.test(StaticIDField::new);
- }
- }
- /* Output:
- 47643
- */
有大量的重復(fù)值,顯然 static int 不是線程安全的,需要用AtomicInteger 嘗試一下:
- // GuardedIDField.java
- import java.util.concurrent.atomic.*;
- public class GuardedIDField implements HasID {
- private static AtomicInteger counter =
- new AtomicInteger();
- private int id = counter.getAndAdd(1);
- public int getID() { return id; }
- public static void main(String[] args) {
- IDChecker.test(GuardedIDField::new);
- }
- }
- /* Output:
- 0
- */
通過構(gòu)造函數(shù)的參數(shù)來共享狀態(tài)同樣是對線程安全敏感的:
- // SharedConstructorArgument.java
- import java.util.concurrent.atomic.*;
- interface SharedArg {
- int get();
- }
- class Unsafe implements SharedArg {
- private int i = 0;
- public int get() { return i++; }
- }
- class Safe implements SharedArg {
- private static AtomicInteger counter =
- new AtomicInteger();
- public int get() {
- return counter.getAndAdd(1);
- }
- }
- class SharedUser implements HasID {
- private final int id;
- public SharedUser(SharedArg sa) {
- id = sa.get();
- }
- @Override
- public int getID() { return id; }
- }
- public class SharedConstructorArgument {
- public static void main(String[] args) {
- Unsafe unsafe = new Unsafe();
- IDChecker.test(() -> new SharedUser(unsafe));
- Safe safe = new Safe();
- IDChecker.test(() -> new SharedUser(safe));
- }
- }
- /* Output:
- 47747
- 0
- */
這里,SharedUser的構(gòu)造函數(shù)共享了相同的參數(shù),SharedUser 理所當(dāng)然的使用了這些參數(shù),構(gòu)造函數(shù)引起了沖突,而自身并不知道失控了。
Java 中并不支持對構(gòu)造函數(shù)synchronized,但實際上可以實現(xiàn)一個synchronized 塊的,例如:
- // SynchronizedConstructor.java
- import java.util.concurrent.atomic.*;
- class SyncConstructor implements HasID {
- private final int id;
- private static Object constructorLock = new Object();
- public SyncConstructor(SharedArg sa) {
- synchronized(constructorLock) {
- id = sa.get();
- }
- }
- @Override
- public int getID() { return id; }
- }
- public class SynchronizedConstructor {
- public static void main(String[] args) {
- Unsafe unsafe = new Unsafe();
- IDChecker.test(() -> new SyncConstructor(unsafe));
- }
- }
- /* Output:
- 0
- */
這樣,就是線程安全的了。另一種方式是避免構(gòu)造函數(shù)的集成,通過一個靜態(tài)工廠的方法來生成對象:
- // SynchronizedFactory.java
- import java.util.concurrent.atomic.*;
- class SyncFactory implements HasID {
- private final int id;
- private SyncFactory(SharedArg sa) {
- id = sa.get();
- }
- @Override
- public int getID() { return id; }
- public static synchronized
- SyncFactory factory(SharedArg sa) {
- return new SyncFactory(sa);
- }
- }
- public class SynchronizedFactory {
- public static void main(String[] args) {
- Unsafe unsafe = new Unsafe();
- IDChecker.test(() ->
- SyncFactory.factory(unsafe));
- }
- }
- /* Output:
- 0
- */
這樣通過工廠方法來實現(xiàn)加鎖就可以安全了。
這樣的結(jié)果對于老碼農(nóng)來說,并不意外,因為線程安全取決于那三競爭條件的成立:
- 兩個處理共享變量
- 至少一個處理會對變量進行修改
- 一個處理未完成前另一個處理會介入進來
示例程序中主要是用鎖來實現(xiàn)的,這一點上,erlang實際上具有著先天的優(yōu)勢。紙上得來終覺淺,終于開始在自己的虛擬機上開始安裝Java 8 了,否則示例程序都跑不通了。對完成線程安全而言————
規(guī)避一,沒有共享內(nèi)存,就不存在競態(tài)條件了,例如利用獨立進程和actor模型。
規(guī)避二,比如C++中的const,scala中的val,Java中的immutable
規(guī)避三, 不介入,使用協(xié)調(diào)模式的線程如coroutine等,也可以使用表示不便介入的標(biāo)識——鎖、mutex、semaphore,實際上是使用中的狀態(tài)令牌。
***,簡單粗暴地說, share nothing 基本上可以從根本上解決線程安全吧。
【本文來自51CTO專欄作者“老曹”的原創(chuàng)文章,作者微信公眾號:喔家ArchiSelf,id:wrieless-com】