老曹:从构造函数看线程安全

开发 开发工具
线程是编程中常用而且强大的手段,在使用过程中,我们经常面对的就是线程安全问题了。对于Java中常见的数据结构而言,一般的,ArrayList是非线程安全的,Vector是线程安全的;HashMap是非线程安全的,HashTable是线程安全的;StringBuilder是非线程安全的,StringBuffer是线程安全的。

线程是编程中常用而且强大的手段,在使用过程中,我们经常面对的就是线程安全问题了。对于Java中常见的数据结构而言,一般的,ArrayList是非线程安全的,Vector是线程安全的;HashMap是非线程安全的,HashTable是线程安全的;StringBuilder是非线程安全的,StringBuffer是线程安全的。

[[188681]]

然而,判断代码是否线程安全,不能够想当然,例如Java 中的构造函数是否是线程安全的呢?

自己从***感觉来看,构造函数应该是线程安全的,如果一个对象没有初始化完成,怎么可能存在竞争呢? 甚至在Java 的语言规范中也谈到,没有必要将constructor 置为synchronized,因为它在构建过程中是锁定的,其他线程是不可能调用还没有实例化好的对象的。

[[188682]]

但是,当我读过了Bruce Eckel 的博客文章,原来构造函数也并不是线程安全的,本文中的示例代码和解释全部来自Bruce Eckel 的那篇文章。

演示的过程从 定义一个接口开始:

  1. // HasID.java  
  2. public interface HasID { 
  3.   int getID(); 

有各种方法可以实现这个接口,先看看静态变量方式的实现:

  1. // StaticIDField.java 
  2.  
  3. public class StaticIDField implements HasID { 
  4.   private static int counter = 0; 
  5.   private int id = counter++; 
  6.   public int getID() { return id; } 

这是一个简单而无害的类,再构造一个用于并行调用的测试类:

  1. // IDChecker.java 
  2. import java.util.*; 
  3. import java.util.function.*; 
  4. import java.util.stream.*; 
  5. import java.util.concurrent.*; 
  6. import com.google.common.collect.Sets; 
  7.  
  8. public class IDChecker { 
  9.   public static int SIZE = 100000; 
  10.   static class MakeObjects 
  11.   implements Supplier<List<Integer>> { 
  12.     private Supplier<HasID> gen; 
  13.     public MakeObjects(Supplier<HasID> gen) { 
  14.       this.gen = gen; 
  15.     } 
  16.     @Override 
  17.     public List<Integer> get() { 
  18.       return 
  19.         Stream.generate(gen) 
  20.           .limit(SIZE
  21.           .map(HasID::getID) 
  22.           .collect(Collectors.toList()); 
  23.     } 
  24.   } 
  25.   public static void test(Supplier<HasID> gen) { 
  26.     CompletableFuture<List<Integer>> 
  27.       groupA = CompletableFuture 
  28.         .supplyAsync(new MakeObjects(gen)), 
  29.       groupB = CompletableFuture 
  30.         .supplyAsync(new MakeObjects(gen)); 
  31.     groupA.thenAcceptBoth(groupB, (a, b) -> { 
  32.       System.out.println( 
  33.         Sets.intersection( 
  34.           Sets.newHashSet(a), 
  35.           Sets.newHashSet(b)).size()); 
  36.     }).join(); 
  37.   } 

其中 MakeObjects 是一个 Supplier 通过get()方法产生一个 List. 这个 List 从 每个HasID 对象中得到一个ID。test() 方法创建了两个并行的CompletableFutures 来运行MakeObjects suppliers, 然后就每个结果使用Guava库的Sets.intersection() 来找出两个List中有多少个共有的ID。现在,测试一下多个并发任务调用这个StaticIDField类的结果:

  1. // TestStaticIDField.java 
  2.  
  3. public class TestStaticIDField { 
  4.   public static void main(String[] args) { 
  5.     IDChecker.test(StaticIDField::new); 
  6.   } 
  7. /* Output
  8. 47643 
  9. */ 

有大量的重复值,显然 static int 不是线程安全的,需要用AtomicInteger 尝试一下:

  1. // GuardedIDField.java 
  2. import java.util.concurrent.atomic.*; 
  3.  
  4. public class GuardedIDField implements HasID { 
  5.   private static AtomicInteger counter = 
  6.     new AtomicInteger(); 
  7.   private int id = counter.getAndAdd(1); 
  8.   public int getID() { return id; } 
  9.   public static void main(String[] args) { 
  10.     IDChecker.test(GuardedIDField::new); 
  11.   } 
  12. /* Output
  13. */ 

通过构造函数的参数来共享状态同样是对线程安全敏感的:

  1. // SharedConstructorArgument.java 
  2. import java.util.concurrent.atomic.*; 
  3.  
  4. interface SharedArg { 
  5.   int get(); 
  6.  
  7. class Unsafe implements SharedArg { 
  8.   private int i = 0; 
  9.   public int get() { return i++; } 
  10.  
  11. class Safe implements SharedArg { 
  12.   private static AtomicInteger counter = 
  13.     new AtomicInteger(); 
  14.   public int get() { 
  15.     return counter.getAndAdd(1); 
  16.   } 
  17.  
  18. class SharedUser implements HasID { 
  19.   private final int id; 
  20.   public SharedUser(SharedArg sa) { 
  21.     id = sa.get(); 
  22.   } 
  23.   @Override 
  24.   public int getID() { return id; } 
  25.  
  26. public class SharedConstructorArgument { 
  27.   public static void main(String[] args) { 
  28.     Unsafe unsafe = new Unsafe(); 
  29.     IDChecker.test(() -> new SharedUser(unsafe)); 
  30.     Safe safe = new Safe(); 
  31.     IDChecker.test(() -> new SharedUser(safe)); 
  32.   } 
  33. /* Output
  34. 47747 
  35. */ 

这里,SharedUser的构造函数共享了相同的参数,SharedUser 理所当然的使用了这些参数,构造函数引起了冲突,而自身并不知道失控了。

Java 中并不支持对构造函数synchronized,但实际上可以实现一个synchronized 块的,例如:

  1. // SynchronizedConstructor.java 
  2. import java.util.concurrent.atomic.*; 
  3.  
  4. class SyncConstructor implements HasID { 
  5.   private final int id; 
  6.   private static Object constructorLock = new Object(); 
  7.   public SyncConstructor(SharedArg sa) { 
  8.     synchronized(constructorLock) { 
  9.       id = sa.get(); 
  10.     } 
  11.   } 
  12.   @Override 
  13.   public int getID() { return id; } 
  14.  
  15. public class SynchronizedConstructor { 
  16.   public static void main(String[] args) { 
  17.     Unsafe unsafe = new Unsafe(); 
  18.     IDChecker.test(() -> new SyncConstructor(unsafe)); 
  19.   } 
  20. /* Output
  21. */ 

这样,就是线程安全的了。另一种方式是避免构造函数的集成,通过一个静态工厂的方法来生成对象:

  1. // SynchronizedFactory.java 
  2. import java.util.concurrent.atomic.*; 
  3.  
  4. class SyncFactory implements HasID { 
  5.   private final int id; 
  6.   private SyncFactory(SharedArg sa) { 
  7.     id = sa.get(); 
  8.   } 
  9.   @Override 
  10.   public int getID() { return id; } 
  11.   public static synchronized 
  12.   SyncFactory factory(SharedArg sa) { 
  13.     return new SyncFactory(sa); 
  14.   } 
  15.  
  16. public class SynchronizedFactory { 
  17.   public static void main(String[] args) { 
  18.     Unsafe unsafe = new Unsafe(); 
  19.     IDChecker.test(() -> 
  20.       SyncFactory.factory(unsafe)); 
  21.   } 
  22. /* Output
  23. */ 

这样通过工厂方法来实现加锁就可以安全了。

[[188683]]

这样的结果对于老码农来说,并不意外,因为线程安全取决于那三竞争条件的成立:

  1. 两个处理共享变量
  2. 至少一个处理会对变量进行修改
  3. 一个处理未完成前另一个处理会介入进来

示例程序中主要是用锁来实现的,这一点上,erlang实际上具有着先天的优势。纸上得来终觉浅,终于开始在自己的虚拟机上开始安装Java 8 了,否则示例程序都跑不通了。对完成线程安全而言————

规避一,没有共享内存,就不存在竞态条件了,例如利用独立进程和actor模型。

规避二,比如C++中的const,scala中的val,Java中的immutable

规避三, 不介入,使用协调模式的线程如coroutine等,也可以使用表示不便介入的标识——锁、mutex、semaphore,实际上是使用中的状态令牌。

***,简单粗暴地说, share nothing 基本上可以从根本上解决线程安全吧。

【本文来自51CTO专栏作者“老曹”的原创文章,作者微信公众号:喔家ArchiSelf,id:wrieless-com】

戳这里,看该作者更多好文

责任编辑:武晓燕 来源: 51CTO专栏
相关推荐

2016-12-01 14:16:18

GitSCM配置

2016-12-01 15:03:36

缓存技术客户端

2016-12-19 09:43:59

软件开发架构

2017-05-18 14:11:22

CRM图解交付

2016-12-01 14:47:05

负载均衡DNS

2016-12-02 08:55:18

Linux系统

2016-12-02 08:54:18

Lambda代码云计算

2017-02-05 16:51:35

网络编程网络系统

2016-12-08 15:52:09

互联网数据计算

2016-12-02 09:09:18

MySQL调优数据库

2016-12-06 20:01:56

数据架构数据机器学习

2016-12-01 13:53:41

2013-06-09 13:24:57

SQL请求

2020-02-04 09:53:05

数据安全数据泄漏信息安全

2017-03-27 08:45:47

全栈技术管理

2015-01-06 10:47:11

国家政策信息安全明朝万达

2012-12-28 09:53:22

网络安全电子商务

2019-11-27 10:11:22

勒索病毒网络安全

2010-01-28 10:29:44

2009-09-17 10:24:49

点赞
收藏

51CTO技术栈公众号