Java并发编程 - 线程封闭

保证并发安全性的方式有三:

不共享、不可变、同步 

 

前两种方式相对第三种要简单一些。

这一篇不说语言特性和API提供的相关同步机制,主要记录一下关于共享的一些思考。

共享(shared),可以简单地认为多个线程可以同时访问某个对象。

如果仅仅在单线程内进行访问则不存在同步的问题。

保证数据的单线程访问称为线程封闭(thread confinement)。

 

线程封闭有三种方式:

·Ad-hoc线程封闭

·栈封闭

·ThreadLocal

  

Ad-hoc线程封闭

通过程序实现来进行线程封闭,也就是说我们无法利用语言特性将对象封闭到特定的线程上,这一点导致这种方式显得不那么可靠。

 

举个例子,假设我们保证只有一个线程可以对某个共享的对象进行写入操作,那么这个对象的"读取-修改-写入"(比如自增操作)在任何情况下都不会出现竟态条件。

如果我们为这个对象加上volatile修饰则可以保证该对象的可见性,任何线程都可以读取该对象,但只有一个线程可以对其进行写入。

这样,仅仅通过线程封闭+volatile修饰就适当地保证了其安全性,相比直接使用synchoronized修饰,虽然更适合,但实现起来稍微复杂。

而对于线程封闭方式的选择,这种方式是最不被推荐的。

 

栈封闭

这个方式理解起来比较简单,封闭在执行线程是局部变量本身固有的特性,封闭在执行线程的栈里,其他线程无法访问是理所当然的。

 

对于基本类型的局部变量,我们不用考虑任何事情,因为Java语言特性本身就保证了任何方法都无法获得基本类型的引用。

而对于引用类型的局部变量,我们需要稍微注意一些问题来保证其栈封闭。

参考下面的装载方舟的代码,现在我们要保护animals,则需要保证该方法的参数、调用的外来方法、返回值都不会引用到animals:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
     public  int  loadTheArk(Collection<Animal> candidates) {
         SortedSet<Animal> animals;
         int  numPairs =  0 ;
         Animal candidate =  null ;
 
         animals =  new  TreeSet<Animal>( new  SpeciesGenderComparator());
         animals.addAll(candidates);
         for  (Animal a : animals) {
             if  (candidate ==  null  || !candidate.isPotentialMate(a))
                 candidate = a;
             else  {
                 ark.load( new  AnimalPair(candidate, a));
                 ++numPairs;
                 candidate =  null ;
             }
         }
         return  numPairs;
     }

先说说loadTheArk的参数candidates,我们将它的元素进行筛选后装载到了方舟中,方法结束后无法通过该参数影响方舟中的动物夫妇。

其次是外来方法,我们使用了"种类性别比较器"对animals进行排序,但它是一个concrete,不会有不确定的行为对animals的状态产生影响。

最后是返回值,显然我们是想报告装载了多少对动物夫妇,返回类型是个基本类型,无法引用animals。

好了,这就是个成功的栈封闭。

 

 

ThreadLocal

给人一种亲切感,这几乎是很常见的方式,而且也是最规范的方式。

 

我们通常用ThreadLocal保证可变的单例变量和全局变量不被多线程共享。

先让我们想想单线程场景中使用Connection对象连接数据库,鉴于Connection对象的初始化开销,整个应用中会维护一个全局的Connection对象。

如果我们想将这个应用改为多线程的,鉴于Connection对象本身不是线程安全的,我们需要对其进行线程封闭,此时我们可以使用ThreadLocal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public  class  ConnectionDispenser {
     static  String DB_URL =  "jdbc:mysql://localhost/mydatabase" ;
 
     private  ThreadLocal<Connection> connectionHolder
             new  ThreadLocal<Connection>() {
                 public  Connection initialValue() {
                     try  {
                         return  DriverManager.getConnection(DB_URL);
                     catch  (SQLException e) {
                         throw  new  RuntimeException( "Unable to acquire Connection, e" );
                     }
                 };
             };
 
     public  Connection getConnection() {
         return  connectionHolder.get();
     }
}

不仅是Connection这种场景,如果我们的很多操作频繁地用到某个对象,而我们又需要考虑它的线程封闭又需要考虑它的初始化开销,ThreadLocal几乎是最好的选择。

虽然这看起来有点像一个全局的Map<Thread,T>,事实上也可以这样理解,但其实现并不是这样你懂的。

当然,这种方式很方便,但这并不代表ThreadLocal可以滥用, 比如仅仅是考虑到应用的并发安全性就把全局变量一律变成ThreadLocal。

而这种做法会导致全局变量难以抽象,并降低其可重用性,而且也增加了耦合。

 

你可能感兴趣的:(Java并发编程)