Java并发系列(二)线程安全与对象的发布

Author:Martin

E-mail:mwdnjupt@sina.com.cn

CSDN Blog:http://blog.csdn.net/ictcamera

Sina MicroBlog ID:ITCamera

Main Reference:

《Java并发编程实战》 Brian Goetz etc 童云兰等译

《Java并发设计教程》 温绍锦

1.        对象的发布与溢出

发布(publish)一个对象就是使对象能够在当前作用域之外的代码中使用。例如将一个指向该对象的引用保存到其他代代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其他类的方法中。在许多情况中,我们要确保对象及其内部状态不被发布,而某些情况下我们又需要发布某个对象。发布某个对象是时如果要确保线程安全,则可能需要同步。发布内部状态可能会破坏封装性,并且使得程序难以维持不变性条件。例如,如果对象构造完成之前就发布该对象,就会破坏线程安全性。当某个不应该发布的对象被发布时,这种情况就被称为逸出(Escape),逸出是一种发布,是一种错误的发布。

发布一个对象的简单方法就是将对象的引用保存到一个公用的静态变量中,以便任何类和线程都能看见该对象,如下所示,初始化方法中将对象的引用保存到knownSecrets中以发布改对象。当发布某个对象时,可能会间接的发布其他对象。这个例子中如果将一个Secret添加到集合knownSecrets中,那么同样会发布这个对象,因为任何代码可以遍历这个集合,并且获得对这个新的Secret对象的引用。

Public static Set<Secret> knownSecrets;

Public void initialize(){

    knownSecrets=new HashSet<Secret>();

}

    如果从非私有方法中返回一个引用,那么同样会发布返回的对象。下面UnsafeStates发布了本应为私有的状态数组。这样的方式发布states,就会出问题,因为任何调用者都能修改这个数组的内容。在这个示例中,数组states已经逸出了它所在的作用域,因为这个本应该是私有的变量已经发布了。

Class UnsafeStates{

Private String [] states=new String[]{“AK”,”AL”…};

Public String[] getStates(){return states;}

}

当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。一般来说,如果一个已经发布的对象能够通过非私有的变量引用和方法调用到达其他的对象,那么这些其他对象也都会被发布。

假定有一个类C,对C来说外部方法是指行为并不完全由C类规定的方法,包括其他类中定义的方法以及类C中可以被改写的方法(不是私有private方法,也不是终结final方法)。把一个对象传递给某个外部方法时,就相当于发布了这个对象。你无法知道哪些代码会执行,也不知道在外部方法中究竟会发布这个对象,还是会保留对象的引用并在随后由另一个线程使用。

当某个对象溢出后,你必须假设有某个类或线程可能会误用该独享。这正是需要使用封装的最主要原因:封装能够使得对程序的正确性进行分析变得可能,并且使得无意中破坏设计约束条件变得更难(可能性变小)。

最后一种发布对象(或者内部状态)的机制就是发布内部类实例。如下所示,当ThisEscape发布EventListener时,也隐含的发布了ThisEscape实例本身,因为在这个内部类的实例中包含了对ThisEscape实例的隐含引用即ThisEscape的this对象。

Public class ThisEscape{

Public ThisEscape (EventSource source){

    Source.registerListener(

        new EventListener(){

            public void onEvent(Event e){

                doSomething(e);

}

}

);

}

在上面的例子中还出现了构造函数中使引用逸出问题,上面例子中构造函数中将隐含的this引用逸出了。当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态,所以当从对象的构造函数中发布对象时,发布了一个尚未构造完成的对象。即使发布对象语句位于构造函数的额最后一行也是存在问题。如果this引用在构造过程中逸出,那么这种对象被认为是不正确的构造。再例如构造函数中创建线程,this被线程引用,这时候立即启动线程,即构造函数未完全构造之间线程就能看到他(这种情况最好是增加start方法或initial方法等构造完后再启动线程)。当然在构造函数中调一个可改写的实例方法(不是私有private方法,也不是终结final方法),同样会导致this引用在构造过程中逸出。

如果想在构造函数中注册一个事件监听器或启动线程,那么可以用一个私有的构造函数和一个公共的工厂方法(Factory Method),从而避免不正确的构造过程,如下所示:

Public class SafeListener{

Private final EventListener;

Private SafeListener(){

    Listerner=new EventListener(

        Public void onEvent(){

            doSomethin(e);

}

);

}

Public static SafeListener newInstance(EventSource source){

    SafeListener safe=new SafeListener();

    source.registerListener(safe.listener);

    return safe;

}

2.        线程封闭

如果需要线程安全的对象出现线程安全问题时(某个时刻还是会出现了不正确的结果),解决问题的办法要么使得对象的访问改为单线程的,即不在线程之间共享该状态变量,要么使得对象的状态是不可变的,还有就是提供同步机制。也就是说如果在访问共享可变数据时通常需要使用同步,如果不同步任要保证线程安全,就需要不共享数据,在单线程内访问数据就能实现不共享数据,就不需要同步,这种技术被称为线程封闭(Thread Confinement),他是实现线程安全的最简单方法之一。当某个对象封闭在一个线程中时,这个用法将自动实现线程的安全性,即使被封闭的对象本身不是线程安全的。

例如Swing通过将可视化组件和数据模型对象封闭到Swing的时间分发线程中,从而实现线程安全,因为要想正确地使用Swing,那么在除了事件线程之外的其他线程中就不能访问这些对象。Swing应该用程序的许多并发错误都是由于错误的在另外一个线程中使用了这些被封闭的对象。另外一个封闭技术的应用就是JDBC的Connection对象(JDBC规范并不要求Connection是对象安全的)。大多数请求都是单个线程采用同步的方式来处理,并且在Connection对象返回之前,连接池不会再将它分配给其他线程,因此,这种链接管理模式在处理请求时隐含地将Connection对象封闭在线程中。

Java语言无法强制线程封闭,线程封闭只是我们程序设计的一个考虑,但是Java语言极其核心类库提供了一些机制来维持线程的封闭性,比如局部变量(栈封闭)和ThreadLocal,但即便如此,程序员任然需要负责确保封闭在线程中的对象不会从线程中逸出。

2.1.        Ad-hoc线程封闭

Ad-Hoc线程封闭是指维护线程封闭性的职责完全有程序实现来承担。Ad-Hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能够将对象封闭到目标线程上。

在volatile变量上存在一种特殊的线程封闭。只要你能够确保只有单个线程对共享的volatile变量只写写入操作,那么就可以安全的在这些共享的volatile变量上执行“读取—修改—写入”的操作。在这种情况下,相当于将修改操作封闭在单个线程中以防止发生竞态条件,并且volatile变量的可见性还确保了其他线程能看到最新的值。

由于Ad-Hoc线程封闭技术的脆弱性,在实际程序中尽量少用,在可能的情况下,应该使用更强的线程封闭技术(例如后面介绍的栈封闭和ThreadLocal)。

2.2.        栈封闭

栈封闭(使用局部变量)是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。正如封装使得代码更容易维持不变性条件那样,局部变量也能使对象更易于封闭在线程中。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。栈封闭也被称为线程内部使用或者线程局部使用(不要和ThreadLocal混淆),栈封闭比Ad-Hoc线程封闭更易于维护,更加健壮。

2.3.        ThreadLocal

维持线程封闭性的一种更规范的方法是使用ThreadLocal,这个类能使线程的某个值与保存值的对象联系起来。基本思想是:将线程作为key,将要保存、更新的对象作为value维护在一个同步Map中,从而达到各个线程分隔(因为每个使用变量的线程都一份独立的副本)。这些特定于某个线程值,当线程终止后,这些会作为垃圾回收。下面是简单的示例实现:

publicclass ThreadLocal{

    privateMapvalueMap =Collections.synchronizedMap(new HashMap());

    publicvoid set(Object newValue){

        valueMap.put(Thread.currentThread(), newValue);

    }

    public Object get(){

        Thread currentThread = Thread.currentThread();

        Object o = valueMap.get(currentThread);

        if(o ==null && !valueMap.containsKey(currentThread)){

            o = initialValue();

            valueMap.put(currentThread, o);

        }

        return o;

    }

    publicvoid remove(){

        valueMap.remove(Thread.currentThread());

    }

    public Object initialValue(){

        returnnull;

    }

}

当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免每次执行时都重新分配该临时对象,就可以使用这项技术。在实际应用程序框架时大量使用了ThreadLocal。例如EJB调用期间,J2EE容器需要将一个事务上下文与某个执行中的线程关联起来。通过将事务上下文保存在静态的ThreadLocal对象中,可以很容易的实现这样的功能:当框架代码需要判断当前运行的是哪个事务时,只需要从这个ThreadLocal中读取事务上下文即可。这种机制很方便,因为他避免了在调用每个方法是都需要传递执行上下文信息。然而这也将使用该机制的代码和框架耦合在一起。ThreadLocal变量类似于局部变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此使用是应该格外小心。下面给出HibernateSessionFactory是如何使用ThreadLocal管理Session的例子。

publicclass HibernateSessionFactory {

    privatestatic StringCONFIG_FILE_LOCATION ="/hibernate.cfg.xml";

    privatestaticfinalThreadLocalthreadLocal =newThreadLocal();

    privatestaticfinalConfigurationcfg =newConfiguration();

    privatestaticorg.hibernate.SessionFactorysessionFactory;

    publicstaticSession currentSession()throwsHibernateException {

        Session session = (Session)threadLocal.get();

        if (session ==null) {

            if (sessionFactory ==null) {

                try {

                    cfg.configure(CONFIG_FILE_LOCATION);

                    sessionFactory =cfg.buildSessionFactory();

                }

                catch (Exception e) {

                    System.err.println("%%%% Error Creating SessionFactory %%%%");

                    e.printStackTrace();

                }

            }

            session = sessionFactory.openSession();

            threadLocal.set(session);

        }

        return session;

    }

    publicstaticvoid closeSession()throwsHibernateException {

        Session session = (Session)threadLocal.get();

        threadLocal.set(null);

        if (session !=null) {

            session.close();

        }

    }

    private HibernateSessionFactory() {

    }

}

3.        对象的不变性

解决线程安全问题的办法除了线程封闭(不在线程之间共享该状态变量)提供同步机制之外,还有就是使得对象的状态是不可变的。如果某个对象在被创建之后其状态就不能被修改,那么这个对象就称为不可变对象。线程的安全性是不可变对象的固有属性之一,不可变对象一定是线程安全的。

3.1.        不变性条件

Java中的关键字final用于构造不可变性对象。final类型域是不可修改的,但是如果final与所引用的对象是可变的,那么这些被引用的对象是可以修改的,即如果final修饰的是基本类型那么这些值是不可变的,如果修饰的是引用,那么这个引用被赋值后不能改为引用其他对象,但是如果引用的对象本身可变,那么对象本身还是可以修改的。在Java语言规范和Java内存模型中都没有给出不可变性的正式定义,但是不可变性并不等于将对象中的所有域都声明为final类型,即使队形中的所有域都是final类型的,这个对象也可能是可变的,因为final类型域中可以保存对可变对象的引用。良好的编程习惯就是尽可能的将域修饰为private和final。当满足一下条件是对象才是不可变的:

l  对象创建后其状态就不能修改。

l  对象的所有域类型都是final

l  对象是正确创建的(在对象创建期间,this引用没有逸出)。

3.2.        不变性举例

下面举例说明如何用final构造不可变对象,同时结合volatile机制来解决同步问题的。为了说明这个问题还是以servlet缓存为例子,假设我们希望提升Servlet的性能:将最近的计算结果缓存起来,当两个连续的请求对相同的数值进行因数分解时,可以直接使用上一次的计算结果,而无须从新计算。要实现缓存策略,需要保存两个状态:最近执行因素分解的值以及分解结果。很容想到如下实现

Public class UnsafeAtomicCachingFactorizer implements Servlet{

Private final AtomicReference<BigInteger> lastNumber

=new AtomicReference<BigInteger>();

Private final AtomicReference<BigInteger[]> lastFactors

=new AtomicReference<BigInteger[]>();

Public void service (ServletRequest req,ServletResponse resp){

    BigInteger i=extractFromRequest(req);

    If(i.eaquals(lastNumber.get())){

         encodeIntoResponse(resp,foctors.get());

}else{

         BigInteger[] factors=factor(i);

lastNumber.set(i);

lastFactors.set(factors);

         encodeIntoResponse(resp,foctors);

}

}

这种方法并不正确。尽管这些原子应用本身都是线程安全的,但是在这个实现中存在竞态条件,可能产生错误的结果。这里的不变条件之一是:在lastFactors中缓存的因素之积应该等于lastNumber中缓存的值。在不变条件中设计多个变量时,多个变量之间并不是彼此独立的,而是某个变量值会对其他变量的值产生约束。因此,更新某一个变量时,需要在同一个原子操作中对其它变量同时进行更新。现在看下面是如何不用同步锁机制解决这个问题的:

    class oneValueCache{

        privatefinal BigIntegerlastNumber;

        privatefinal BigInteger[]lastFactors;

        public oneValueCache(BigInteger i,BigInteger[] factors){

            lastNumber=i;

            lastFactors=Arrays.copyOf(factors, factors.length);

        }

        public BigInteger[] getFactors(BigInteger i){

            if(lastNumber==null||!lastNumber.equals(i)){

                returnnull;

            }else{

                return Arrays.copyOf(lastFactors,lastFactors.length);

            }

        }

    }

    publicclass VolatileCacheFactorizerimplementsServlet{

        privatevolatile oneValueCachecache=new oneValueCache(null,null);

        publicvoid service(ServletRequest req,Servlet resp){

            BigInteger i=extractFromRequest(req);

            BigInteger[] factors=cache.getFactors(i);

            if(factors==null){

                factors=factor(i);

                cache=new oneValueCache(i,factors);

            }

            encodeIntoResponse(resp,faxtors);

        }

    }

代码中使用了oneValueCache来保存缓存的数值以及因素。当一个线程将volatile类型的cache设置为一个新的oneValueCache时,其他线程就会立即看到新缓存的数据。与cache相关的操作互不干扰,因为oneValueCache是不可变的,并且在每条相应的代码路径中智只会访问它一次。通过使用包含多个状态变量的容器对象来维持不变性条件,并且使用一个volatile类型的引用来确保可见性,使得VolatileCacheFactorizer在没有显式锁的情况下任然是线程安全的。

4.        对象的安全发布

1部分我们讨论了对象发布的逸出情况和简单的规避措施; 2部分我们说明了如何确保对象不对外发布(线程封闭),即让对象封闭在线程或者对象的内部;在3部分讨论了不可变对象在保证初始化安全的情况下即使在没有同步的情况下,发布不可变对象也是安全的(可以安全的访问该对象),但是不可变对象必须满足不可变性的所有需求:【状态不可修改,所有域都是final类型的,以及正确的构造过程】,当然同步的情况下发布不可变对象也是安全的。实际中很多情况是需要使得对象被多个线程共享,即需要发布对象,并且发布不可变的情况也很少,因此必须确保安全地进行共享。可变对象必须通过其他安全的方式来发布和使用,这通常意味着在发布和使用该对象的线程时都必须使用同步。

4.1.        安全发布的方式

一个正确构造的对象可以通过一下方式安全的发布(包括可变对象和不可变对象):

在静态初始化函数中初始化一个对象的引用。

将对象的引用保存到volatile类型的域或者AtomicReference对象中。

将对象的引用保存到某个正确构造对象的final类型域中。

将对象的引用保存到一个由锁保护的域中。

例如通过public static Holder holder=new Holder(4);静态初始化器由JVM在类的初始化阶段执行,由于JVM内部存在着同步机制,因此这种方式初始化的任何对象都可以被安全的发布。线程安全容器内部的同步意味着,在将对象放入到某个容器(例如VectorSynchronizedList)时将满足上述最后一条。如果线程A将对象X放入一个线程安全的容器,然后线程B读取这个对象,那么可以确保B能到A设置的X状态,即便在这段读、写X的应用程序代码中没有包含显式的同步。Java线程安全库中的容器提供了以下的安全发布保证:

通过将一个键或值放入HashTableSynchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这个容器中访问它的线程,无论是直接访问还是通过迭代器访问。

通过将某个元素放入VectorCopyOnWriteArrayListCopyOnWriteArraySetSynchronizedList或者SynchronizedSet,可以将元素安全地发布到任何从这个容器中访问该元素的线程。

通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将元素安全地发布到任何从这些队列中访问该元素的线程。

4.2.        事实不可变对象

如果对象发布是足够安全的(注意足够的含义),并且对象在发布后不会被修改,即足够安全的发布了不可变对象,那么其他没有额外同步的线程任然可以安全的访问。更进一步说,所有安全发布机制都能确保,当前引用的对象对所有访问该对象的线程可见时,对象发布时的状态对于所有线程也将是可见的,并且对象的状态不会再改变的时候,那么就足以确保任何访问都是安全的。

如果对象是可变的,但是其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象“(Effectively Immutable Object)。这些对象不需要满足3部分定义的不可变性的严格定义。这些对象被安全的发布后,程序只要将它们视为不可变对象即可,也就是说在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。通过事实不可变对象,不仅可以简化开发过程,而且还能由于减少了同步提高性能。

    例如Date本身是可变的,但是如果将它作为不可变对象来使用,那么多个线程之间共享Date时就可以省去对锁的使用。假设需要维护一个Map对象,其中保存了每位用户的最近登入时间:Map<String ,Date> lastLogin=new Collections.synchronizedMap(new HashMap(String ,Date));如果Date对象的值在被放入Map后就不会改变,那么SynchronizedMap中的同步机制就足以使Date值被安全的发布,并且访问这些Date值时不需要额外的同步。

4.3.        可变对象

如果可变对象在构造后可以修改,那么即使安全的被发布,也只能确保“发布时”状态的可见性。对于可变对象,不仅在发布时需要使用同步,而且每次访问对象时同样需要同步来确保后续修改操作的可见性。也就是说,安全地共享可变对象,这些对象必须安全地被发布,并且线程安全的或者某个锁保护起来的,同时每次访问时也需要同步。

4.4.        安全发布总则

对象的发布需求取决于它的可见性:

不可变对象可以通过任意机制来发布;

事实不可变对象必须通过安全的方式来发布;

可变对象必须通过安全的方式发布,并且是线程安全的或者某个锁保护起来的。

5.        发布和使用对象的策略

在并发程序中共享和使用对象时,可以使用以下的一些策略:

线程封闭:线程封闭的对象只能由一个线程拥有,对象封闭在该线程中,并且只能由这个线程修改。

只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发的访问,但是任何线程都不能修改它。共享只读对象包括不可变对象和事实不可变对象。

线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。

保护对象:保护对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护起来的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值