JUC系列二:对象的发布与共享

原创
2015/08/01 17:28
阅读数 60

系列一讲了如何通过同步来避免多个线程访问共享的变量,这一节讲如何发布与共享对象,使它能够安全的被多个线程所访问。

###发布和逸出

所谓发布一个对象就是使对象能够被当前作用域外的代码所访问,例如在一个非私有方法中返回这个对象的引用

public class Test{
	private Student o=new Student();
	//....
	public Student getStudent(){
		return o;
	}
}

本来对象o是无法在Test类外被访问的,但getObject方法返回了这个对象的引用,这样就使得在任何一个类中,都可以通过这个方法来获取对象o,从而修改对象o的属性。我们称这种行为叫发布了对象o。

在程序开发过程中,虽然大多数情况下都需要确保对象不被外部访问,但在某些情况下确实需要发布一个对象,这时就可能带来两个线程的安全性问题:第一是当你发布一个对象时,其它的线程就能够修改这个对象的属性,就会破坏封装性,使得程序难以维持不变性条件;在上面的例子中,对于任何一个线程,都能够修改对象o的状态,如果对象o与Test的其它属性存在不变性关系时,这种关系就容易被打破。二是在发布过程中,可能会发布一个还没有正确构造完的对象,这种情况叫逸出,像下面的例子

public class Escape{
	private int thisCanBeEscape = 0;
	public Escape(){
	    new InnerClass();
	}
	private  class InnerClass {
		public InnerClass() {
			//这里可以在Escape对象完成构造前提前引用到Escape的private变量
			System.out.println(Escape.this.thisCanBeEscape);
		}
	}
}

很显然,在Escape对象没有构造完全时。InnerClass的构造方法中就能够访问到这个还没有构造完全的对象,从而产生线程安全问题,这种情况叫this引用逸出

###可变与不可变

如果某个对象在被创建后其状态不能被修改,那么这个对象就称为不可变对象,而不可变对象一定是线程安全的。虽然Java语法规范和内存模型并没有给出不可变的定义,但不可变并不等于将所有的域(状态)都声明为final,而且,就算全为final,对象也可能是可变的,因为在final类型的域中,可以保存对可变对象的引用。

当一个对象满足以下所有的条件时,该对象才是不可变的:

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

  • 对象的所有域都被final修饰(并不是十分准确,比如String类型是不可变的,但其hash域就没有被final修饰)

  • 对象是正确创建的(创建是没有发生上面的this引用逸出)

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

还有一种情况是,如果对象从技术上来看是可变的,但其状态在发布后就不会再改变,那么这种对象称之为事实不可变对象,例如,Date本身是可变的,但如果将它作为不可变对象来使用,那么在多个线程之间共享Date对象时,就可以省去对锁的使用。假设需要维护一个Map对象,其中保存了每位用户的最近登录时间:

public Map<String, Date> lastLogin = Collections.synchronizedMap(new HashMap<String, Date>());

如果Date对象的值在被放入Map后就不会改变,那么synchronizedMap中的同步机制就足以使Date值被安全地发布,并且在访问这些Date值时不需要额外的同步。 (这里开始有点不明白,难道放在ArrayList中就不是被安全的发布么?后来想,放在ArrayList中并不能保证Date的可见性,所以不一定能被安全的发布,应该是这个道理)

###安全发布 要安全的发布一个对象,对象的引用以及对象的状态必须同时保持对其它线程可见,一个正确创建的对象(没有this引用逸出)可以通过以下方式来安全的发布,

  • 在静态初始化函数中初始化一个对象引用(也就是使用静态变量)

  • 将对象的引用使用volatile修饰或者保存到AtomicReference对象中

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

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

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

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

  • 对于事实不可变对象必须通过以上安全的方式来发布(比如上面的Date)

  • 对于可变的对象必须通过以上安全的方式来发布,并且该对象要么是线程安全的,要么被某个锁保护起来

###共享对象

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

  • 线程封闭

  • 只读共享

  • 线程安全共享

  • 保护对象

####线程封闭 当访问共享的可变数据时,一种避免使用同步的方式就是不共享数据。对于发布的对象只能由一个线程所拥有,也只能由这个线程修改。这样就不存在线程安全的问题。比较常用的应用就是JDBC的Connection对象,Connection并不是线程安全的,当服务器收到一个请求时,请求所属的线程就向连接池申请一个Connection对象,并且在使用完Connection对象时,连接池不会将它分配给其它的线程,这样,Connection对象就被封闭在线程中,从而保证了Connection的线程安全性。

在Java的核心库中,也提供了一些机制来帮助维持线程的封闭性,例如栈封闭(局部变量)和ThreadLocal类。栈封闭自然不用说,在JVM系列中就有说明,栈是线程独有的,不会共享。所以我们来看看ThreadLocal类。

import java.util.concurrent.atomic.*;
import java.util.*;
public class ThreadLocalDemo{
	private static final AtomicInteger count=new AtomicInteger(0);
	public static ThreadLocal<Integer> integerHolder=new ThreadLocal<Integer>(){
		public Integer initialValue(){
			return count.incrementAndGet();
		}
	};
	public static void main(String []args){
		for(int i=0;i<2;i++){
			Thread thread=new Thread(new MyThread());
			thread.start();
		}
	}
}
class MyThread implements Runnable{
	private static int count=0;
	private static final Random rand=new Random();
	public void run(){
		try{
			while(true){
				Thread.sleep(1000);
				System.out.println(Thread.currentThread().getName()+" value is "+ThreadLocalDemo.integerHolder.get());
				count++;
				if(count%5==0){
					ThreadLocalDemo.integerHolder.set(count);
				}
			}
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}

从结果可以看的出来,同一个线程的value获取多少次都是同一个值,当某个线程初次调用ThreadLocal.get()方法时,就会调用initialValue来获取初始值,可以把ThreadLocal当作一个Map<Thread,T>对象来看,其键是当前线程,其值为特定于该线程的值。

####只读共享

在没有同步的情况下,共享只读对象可以保证线程的安全性,因为任何线程都不能修改对象的状态,也就不会存在线程安全问题。共享的只读对象包括不可变对象和事实不可变对象(需要使用安全的方式发布)。

####线程安全共享

线程安全的对象在其内部实现了对所有可变状态的操作进行了同步,从而使得多线程环境中可以通过对象的共有接口来进行访问而不需要外加同步处理。JavaAPI中很多这样的类,比如Vector。但这种对象遇到判断添加的情况时就可能会遇到原子性的问题。

####保护对象

保护对象就是在访问时通过同步来限制对象在同一时刻只能被一个线程访问,在访问对象时,每个线程都需要获取特定的锁。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部