新聞中心
線程是編程中常用而且強(qiáng)大的手段,在使用過(guò)程中,我們經(jīng)常面對(duì)的就是線程安全問(wèn)題了。對(duì)于Java中常見(jiàn)的數(shù)據(jù)結(jié)構(gòu)而言,一般的,ArrayList是非線程安全的,Vector是線程安全的;HashMap是非線程安全的,HashTable是線程安全的;StringBuilder是非線程安全的,StringBuffer是線程安全的。

然而,判斷代碼是否線程安全,不能夠想當(dāng)然,例如Java 中的構(gòu)造函數(shù)是否是線程安全的呢?
自己從***感覺(jué)來(lái)看,構(gòu)造函數(shù)應(yīng)該是線程安全的,如果一個(gè)對(duì)象沒(méi)有初始化完成,怎么可能存在競(jìng)爭(zhēng)呢? 甚至在Java 的語(yǔ)言規(guī)范中也談到,沒(méi)有必要將constructor 置為synchronized,因?yàn)樗跇?gòu)建過(guò)程中是鎖定的,其他線程是不可能調(diào)用還沒(méi)有實(shí)例化好的對(duì)象的。
但是,當(dāng)我讀過(guò)了Bruce Eckel 的博客文章,原來(lái)構(gòu)造函數(shù)也并不是線程安全的,本文中的示例代碼和解釋全部來(lái)自Bruce Eckel 的那篇文章。
演示的過(guò)程從 定義一個(gè)接口開(kāi)始:
- // HasID.java
- public interface HasID {
- int getID();
- }
有各種方法可以實(shí)現(xiàn)這個(gè)接口,先看看靜態(tài)變量方式的實(shí)現(xiàn):
- // StaticIDField.java
- public class StaticIDField implements HasID {
- private static int counter = 0;
- private int id = counter++;
- public int getID() { return id; }
- }
這是一個(gè)簡(jiǎn)單而無(wú)害的類(lèi),再構(gòu)造一個(gè)用于并行調(diào)用的測(cè)試類(lèi):
- // 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
> {
- private Supplier
gen; - public MakeObjects(Supplier
gen) { - this.gen = gen;
- }
- @Override
- public List
get() { - return
- Stream.generate(gen)
- .limit(SIZE)
- .map(HasID::getID)
- .collect(Collectors.toList());
- }
- }
- public static void test(Supplier
gen) { - CompletableFuture
>
- 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 是一個(gè) Supplier 通過(guò)get()方法產(chǎn)生一個(gè) List. 這個(gè) List 從 每個(gè)HasID 對(duì)象中得到一個(gè)ID。test() 方法創(chuàng)建了兩個(gè)并行的CompletableFutures 來(lái)運(yùn)行MakeObjects suppliers, 然后就每個(gè)結(jié)果使用Guava庫(kù)的Sets.intersection() 來(lái)找出兩個(gè)List中有多少個(gè)共有的ID?,F(xiàn)在,測(cè)試一下多個(gè)并發(fā)任務(wù)調(diào)用這個(gè)StaticIDField類(lèi)的結(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
- */
通過(guò)構(gòu)造函數(shù)的參數(shù)來(lái)共享狀態(tài)同樣是對(duì)線程安全敏感的:
- // 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 中并不支持對(duì)構(gòu)造函數(shù)synchronized,但實(shí)際上可以實(shí)現(xiàn)一個(gè)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ù)的集成,通過(guò)一個(gè)靜態(tài)工廠的方法來(lái)生成對(duì)象:
- // 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
- */
這樣通過(guò)工廠方法來(lái)實(shí)現(xiàn)加鎖就可以安全了。
這樣的結(jié)果對(duì)于老碼農(nóng)來(lái)說(shuō),并不意外,因?yàn)榫€程安全取決于那三競(jìng)爭(zhēng)條件的成立:
- 兩個(gè)處理共享變量
- 至少一個(gè)處理會(huì)對(duì)變量進(jìn)行修改
- 一個(gè)處理未完成前另一個(gè)處理會(huì)介入進(jìn)來(lái)
示例程序中主要是用鎖來(lái)實(shí)現(xiàn)的,這一點(diǎn)上,erlang實(shí)際上具有著先天的優(yōu)勢(shì)。紙上得來(lái)終覺(jué)淺,終于開(kāi)始在自己的虛擬機(jī)上開(kāi)始安裝Java 8 了,否則示例程序都跑不通了。對(duì)完成線程安全而言————
規(guī)避一,沒(méi)有共享內(nèi)存,就不存在競(jìng)態(tài)條件了,例如利用獨(dú)立進(jìn)程和actor模型。
規(guī)避二,比如C++中的const,scala中的val,Java中的immutable
規(guī)避三, 不介入,使用協(xié)調(diào)模式的線程如coroutine等,也可以使用表示不便介入的標(biāo)識(shí)——鎖、mutex、semaphore,實(shí)際上是使用中的狀態(tài)令牌。
***,簡(jiǎn)單粗暴地說(shuō), share nothing 基本上可以從根本上解決線程安全吧。
【本文來(lái)自專欄作者“老曹”的原創(chuàng)文章,作者微信公眾號(hào):喔家ArchiSelf,id:wrieless-com】
網(wǎng)站欄目:老曹:從構(gòu)造函數(shù)看線程安全
文章鏈接:http://www.dlmjj.cn/article/ccchdec.html


咨詢
建站咨詢
