线程封闭

访问共享的、可变的数据要求使用同步。一个可以避免同步的方式就是不共享数据。如果数据仅在单线程中被访问,就不需要任何同步。线程封闭(Thread confinement)技术是实现线程安全的最简单的方式之一。当对象封闭在一个线程中时,这种做法会自动成为线程安全的,即使被封闭的对象本身并不是[CPJ 2.3.2]。

Swing发展了线程封闭技术。Swing的可视化组件和数据模型对象并不是线程安全的,它们是通过将它们限制到Swing的事件分发线程中,实现线程安全的。为了正确地使用Swing,运行在不同于事件线程(event thread)的其他线程中的代码不应该访问这些对象(为了简化这些,Swing提供了invokeLater机制,用于在事件线程中安排执行Runnable实例)。

很多Swing应用中的并发错误都滋生于从其他线程中错误地使用这些被限制的对象。

另一种常见的使用线程限制的应用程序是应用池化的JDBC(Java Database Connectivity)Connection对象。JDBC规范并没有要求Connection对象是线程安全的9。然而在典型的服务器应用中,线程总是从池中获得一个Connection对象,并且用它处理一个单一的请求,最后把它归还。每个线程都会同步地处理大多数请求(比如Servlet请求或者EJB(Enterprise java Bean)调用),而且在Connection对象在被归还前,池不会将它再分配给其他线程,因此,这种连接管理模式隐式地将Connection对象限制在处于请求处理期间的线程中。

正如语言并未提供强迫变量被锁保护的机制一样,语言也没有办法将对象限制在某一线程中。线程限制是你在程序设计中需要考虑的一个元素,它是在程序的实现中完成的。语言自身以及核心库提供了某些机制(本地变量和ThreadLocal类)有助于维护线程限制,尽管如此,程序员仍然要自己负责确保线程限制对象不会从它所在的线程中逸出。

3.3.1  Ad-hoc线程限制

Ad-hoc线程限制10是指维护线程限制性的任务全部落在实现上的这种情况。因为没有可见性修饰符与本地变量等语言特性协助将对象限制在目标线程上,所以这种方式是非常容易出错的。事实上,对于像GUI应用中的可视化组件或者数据模型这些线程限制对象,对它们的引用通常是公用域。

如果决定将一个像GUI这样特定的子系统实现为“单线程化”的子系统,通常就要使用线程限制技术。单线程化子系统有时所带来的简便性的好处远远胜过ad-hoc线程限制的易损性11。

线程限制的一种特例是将它用于volatile变量。只要你确保只通过单一线程写入共享的volatile变量,那么在这些volatile变量上执行“读-改-写”操作就是安全的。在这种情况下,你就将修改操作限制在单一的线程中,从而阻止了竞争条件。并且,可见性保证volatile变量能够确保其他线程能看到最新的值。

鉴于ad-hoc线程限制固有的易损性,因此应该有节制地使用它。如果可能的话,用一种线程限制的强形式(栈限制或者Thread Local)取代它。

3.3.2  栈限制

栈限制是线程限制的一种特例,在栈限制中,只能通过本地变量才可以触及对象。正如封装使不变约束更容易被保持,本地变量使对象更容易被限制在线程本地中。本地变量本身就被限制在执行线程中;它们存在于执行线程栈。其他线程无法访问这个栈。栈限制(也称线程内部或者线程本地用法,但是不要与核心库类的ThreadLocal混淆)与ad-hoc线程限制相比,更易维护,更健壮。

像清单3.9中loadTheArk方法的numPairs,对于这些基本(primitive)类型的本地变量,你无法尝试去利用栈限制。由于无法获得基本类型的引用,所以语言语义确保了基本本地变量总是线程封闭的。

清单3.9  本地的基本类型和引用类型的变量的线程限制

public int loadTheArk(Collection<Animal> candidates) {

    SortedSet<Animal> animals;

    int numPairs = 0;

    Animal candidate = null;

    // animals被限制在方法中, 不要让它们逸出!

    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中,我们实例化一个TreeSet对象animals,并且保存了一个到animals中一个元素的引用。此时只有一个引用指向集合animals,因此它被限制在保存本地变量的执行线程中。但是,倘若我们发布了到集合animals(或者其他任何内部数据)的引用,那么将会破坏限制性,也导致了animals对象的逸出。

在线程内部上下文使用非线程安全的对象仍然可以保证线程安全性。不过请小心:无论是对象被限制于执行线程的设计需求,还是对“被限制的对象并非线程安全”这一情况

的明确知晓,都只是存在于一线开发人员编码的那一刻。如果线程内部用法的设定没有清楚地文档化,那么后期维护人员会错误放任对象的逸出。

3.3.3  ThreadLocal

一种维护线程限制的更加规范的方式是使用ThreadLocal,它允许你将每个线程与持有数值的对象关联在一起。ThreadLocal提供了get与set访问器,为每个使用它的线程维护一份单独的拷贝。所以get总是返回由当前执行线程通过set设置的最新值。

线程本地(Thread Local)变量通常用于防止在基于可变的单体(Singleton)或全局变量的设计中,出现(不正确的)共享。比如说,一个单线程化的应用程序可能会维护一个全局的数据库连接,这个Connection在启动时就已经被初始化了。这样就可以避免为每个方法都传递一个Connection。因为JDBC规范并未要求Connection自身一定是线程安全的,因此,如果没有额外的协调时,使用全局变量的多线程应用程序同样不是线程安全的。通过利用ThreadLocal存储JDBC连接,如同清单3.10的ConnectionHolder,每个线程都会拥有属于自己的Connection。

清单3.10  使用Thread Local确保线程封闭性

private static ThreadLocal<Connection> connectionHolder

    = new ThreadLocal<Connection>() {

        public Connection initialValue() {

            return DriverManager.getConnection(DB_URL);

        }

    };

public static Connection getConnection() {

    return connectionHolder.get();

}

这项技术还用于下面的情况:一个频繁执行的操作既需要像buffer这样的临时对象,同时还需要避免每次都重分配(realloate)该临时对象。举例来说,在Java 5.0以前,Integer.toString()方法使用ThreadLocal存储一个12-byte的缓冲区来格式化结果,而不是使用共享的静态缓冲区(这需要用到锁)或者在每次调用前都分配一个新的缓冲12。

线程首次调用ThreadLocal.get方法时,会请求initialValue 提供一个初始值。概念上,你可以将Thread Local<T>看作map< Thread,T>它存储了与线程相关的值,不

过事实上它并非这样实现的。与线程相关的值存储在线程对象自身中,线程终止后,这些值会被垃圾回收。

假设你正在将一个单线程的应用迁移到多线程环境中,你可以将共享的全局变量都转换为ThreadLocal类型,这样可以确保线程安全。前提是全局共享(shared globals)的语义允许这样。如果将应用级的缓存变成一堆线程本地缓冲,它将毫无价值。

实现一个应用程序框架会广泛地使用ThreadLocal。举例来说,在EJB调用期间,J2EE容器把一个事务上下文与一个可执行线程关联起来。下面是一种简单的实现,它利用静态ThreadLocal持有事务上下文:当框架代码需要获知当前正在运行的是哪个事务时,只要从ThreadLocal中获得事务的上下文即可。这样很方便,因为它降低了为每个方法传递执行上下文信息的需要,不过却增加了任何使用该机制的代码与框架间的耦合。

ThreadLocal很容易被滥用:比如将它们所封闭的属性作为使用全局变量的许可证,或者是创建一种将方法的参数“隐藏”起来的方法。如同全局变量,线程本地变量会降低重用性,引入隐晦的类间的耦合。因此应该谨慎地使用它们。

 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值