java并发编程(二): 对象的共享

对象的共享:

  • 要编写并发程序,关键在于:在访问共享可变状态时须要进行正确的管理

可见性:     

/**
 * 可见性问题致使,程序运行结果不正确
 * 有可能因为编译器,处理器及运行时作一些重排序
 */
public class Novisibility {
	private static boolean ready;
	private static int number;
	
	private static class ReaderThread extends Thread{
		@Override
		public void run() {
			while (!ready){
				Thread.yield(); //主动让出cpu, 进入就绪队列
			}
			System.out.println(number);
		}
	}
	
	public static void main(String[] args) {
		new ReaderThread().start();
		number = 42;
		ready = true;
	}
}
  • 失效数据:就如同上面的代码,没有同步的状况下可能产生错误的结果。

又如: java

/**
 * get操做可能与最近set值不一致,产生数据失效
 */
@NotThreadSafe
public class MutableInteger {
	private int value;

	public int getValue() {
		return value;
	}

	public void setValue(int value) {
		this.value = value;
	}
}
可作以下修改:
/**
 * 将get, set同步化,可防止数据失效
 */
@ThreadSafe
public class MutableInteger {
	private int value;

	public synchronized int getValue() {
		return value;
	}

	public synchronized void setValue(int value) {
		this.value = value;
	}
}
  • 非原子的64位操做:对于非volatile类型的long和double, jvm会将64位的读写操做分解为2个32位操做(java虚拟机栈中的操做数栈每一个slot为32位),对于这种变量在多线程读写操做下,有可能读取到某个值的高32位或某个值的低32位,建议在多线程环境下,对共享可变的long或double变量进行volatile声明。
  • 加锁与可见性:加锁的含义不只局限于互斥行为,还包括内存可见性。为了确保全部线程能看到共享可变变量的最新值,全部执行读写操做的线程必须在同一个锁上同步

      

  • Volatile变量:以前转载过一篇相关文章(http://my.oschina.net/indestiny/blog/208541), 当变量声明为volatile后,那么编译器或运行时会主要这个变量是共享的不会将该变量上的操做与其余内存操做一块儿重排序。volatile变量不会被缓存在寄存器或其余处理器不可见的地方,所以读取volatile变量总会返回最新的值

      比较典型的用法: 数据库

private volatile boolean asleep;
...
while (!asleep){
    // do sth.
}
  • 加锁机制既能够确保可见性又能够确保原子性,而volatile变量只可确保可见性,因此说volatile是一种轻量级同步机制;
  • 知足如下全部条件时,可用volatile:

       1. 对变量的写入操做不依赖变量的当前值,或者能确保只有一个线程更新变量的值; 缓存

       2. 该变量不会与其余状态变量一块儿归入不变性条件; 安全

       3. 访问该变量不须要加锁。 多线程

发布与逸出:

  • 发布对象:使对象可以在当前做用域以外被使用, 如:
//对共有静态变量的发布,集合内部的变量也会被发布
public static Set<Object> publishedObject;
public void init(){
    publishedObject = new HashSet<>();
}
//经过共有方法发布
private Object publishedObject;
public Object get(){
	return publishedObject;
}
/**
 * 经过发布类的内部实例
 * this引用被逸出
 */
public class ThisEscape {
	public ThisEscape(EventSource source){
		source.registerListener(new EventListener() {
			@Override
			public void onEvent(Event e) {
				// ThisEscape.this实例逸出,但此时该实例并无构造完成
			}
		});
	}
}

安全的对象构造过程:

  • 不要在构造过程当中使this引用逸出,如上面的ThisEscape;

      可经过工厂方法避免this逸出: 并发

/**
 * 经过工厂方法防止this逸出
 */
public class SafeListener {
	private final EventListener listener;
	private SafeListener(){
		listener = new EventListener() {
			@Override
			public void onEvent(Event e) {
				//do sth.
			}
		};
	}
	
	public static SafeListener newInstance(EventSource source){
		SafeListener safe = new SafeListener();
		source.registerListener(safe.listener);
		return safe;
	}
}

线程封闭:

  • 线程封闭:在单线程内访问数据,不须要同步,这是实现线程安全最简单的方式。
  • Ad-hoc线程封闭:维护线程封闭性的职责彻底由程序实现来承担,它很脆弱,由于没有一种语言特性能将对象封闭到目标线程上。
  • 栈封闭:是线程封闭的一种特例,在栈封闭中,只能经过局部变量才能访问对象。比Ad-hoc线程封闭更易维护,强壮。
  • ThreadLocal类:一种维持线程封闭更规范的方法,它会为每一个使用ThreadLocal变量的线程存放一份独立的副本,所以对于该变量线程之间不会相互干扰,你能够想ThreadLocal<T>想成Map<Thread, T>, 当线程终止时 ,这些值会做为垃圾回收。好比,多线程环境下,咱们能够这样获取数据库链接:

private static ThreadLocal<Connection> connectionHolder = 
	new ThreadLocal<Connection>(){
		@Override
		protected Connection initialValue() {
			try {
				return DriverManager.getConnection("DB_URL");
			} catch (SQLException e) {
				e.printStackTrace();
			} 
			return null;
		}
	};
	
public static Connection getConnection(){
	//不一样线程每次获得的Connection, 都是独立的备份
	return connectionHolder.get();
}

不变性:

  • 不可变对象必定是线程安全的;
  • 对象不可变应知足:

      1.对象建立后其状态不能修改; jvm

      2.对象的全部域都是final类型; ide

      3.对象是正确建立的(在对象建立期间,this未逸出)。 函数

  • Final域:final类型的域不能修改(但其指向的引用对象可修改),从新改造以前文章的CachedFactorizer:
/**
 * 不可变类:
 * 全部域都是final
 */
public class OneValueCache {
	private final BigInteger lastNumber;
	private final BigInteger[] lastFactors;
	
	public OneValueCache(BigInteger lastNumber, BigInteger[] lastFactors) {
		this.lastNumber = lastNumber;
		this.lastFactors = lastFactors;
	}
	
	public BigInteger[] getFactors(BigInteger i){
		if (lastNumber == null || ! lastNumber.equals(i)){
			return null;
		} else{
			return Arrays.copyOf(lastFactors, lastFactors.length);
		}
	}
}
/**
 * 使用Volatile类型发布不可变对象
 */
@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {
	private volatile OneValueCache cache = new OneValueCache(null, null); //volatile保证每次写后最新值对其余线程可见
	@Override
	public void service(ServletRequest req, ServletResponse repo) {
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = cache.getFactors(i);
		if (factors == null) {
			factors = factor(i);
			cache = new OneValueCache(i, factors);
		}
		reponseTo(i, factors);
	}
}

安全发布:

  • 不正确的发布:正确的对象被破坏:
/**
 * 多线程访问下,有可能出错,问题不在Holder自己,而在于未正确地发布,可将n声明为final,避免不正确发布
 */
public class Holder {
	private int n;
	
	public Holder(int n){
		this.n = n;
	}
	
	public void assertSanity(){
		if (n != n){
			throw new AssertionError("");
		}
	}
}

不可变对象与初始化安全性:

  • 任何线程均可以在不须要额外同步地状况下安全地访问不可变对象,即便在发布这些对象时没有使用同步。

安全发布地经常使用模式:

  • 一个正确构造的对象能够经过如下方式来安全地发布:

      1.在静态初始化函数中初始化一个对象引用; 性能

      2.将对象的引用保存到volatile类型地域或AtomicReference对象中;

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

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

事实不可变对象:

  • 若是对象从技术上来看是可变的,但其状态在发布后不会再改变,称这种对象为"事实不可变对象"。
  • 在没有额外的同步的状况下,任何线程均可以安全地使用被安全发布的事实不可变对象。

可变对象:

  • 可变对象:对象构造后,其状态能够发生改变;
  • 对象的发布需求取决于它的可变性

       1.不可变对象能够经过任意机制来发布;

       2.事实不可变对象必须经过安全方式来发布;

       3.可变对象必须经过安全方式来发布,而且必须是线程安全的或由某个锁保护起来。

安全地共享对象:

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

      1.线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,只容许这个线程修改;

      2.只读共享。在没有同步的状况下,共享的只读对象能够由多个线程并发访问,但任何线程不能修改它。共享的只读对象包括不可变对象和事实不可变对象。

      3.线程安全共享。线程安全的对象在其内部实现同步,多个线程能够经过公有接口对其访问而不需进一步同步;

      4.保护对象。被保护对象只能经过持有特定锁来访问。保护对象包括封装在其余线程安全对象中的对象,以及已发布的而且由某个特定锁保护的对象。

不吝指正。

相关文章
相关标签/搜索