Java并发编程实战笔记(2)-对象的共享

Posted by zhidaliao on February 1, 2016

核心知识点

  • 同步不仅能够保证原子性; 还能实现内存可见性: 当一个线程修改了对象状态后,其他线程能够看到发生的状态变化

  • 如何让多个线程安全的访问可变状态 ?

可见性

重排序
class Test{
	private static boolean visible;
	private static int number;

	static class Reader extends Thread{
		void run(){
			while(!visible){
				Thread.yield();
			}
			system.out.println(number);
		}
	}

	static void main(String[]  args){
		new Reader().start();
		number = 12;
		visible = true;
	}
}

Reader线程可能看到了visible的值,却没有看到number的,这种现象称为重排序 Jvm为了充分利用现代多核处理器,可能对操作的执行顺序进行调整

如何避免 : 只要有数据在线程之间共享,就使用正确的同步

Volatile
  • 变量不会被缓存在寄存器或者其他处理器不可见的地方,在读取valatile变量时总会返回最新写入的值
  • 变量声明为 volatile,编译器和运行时都会注意到这个变量是共享的,不会将该变量的操作与其他内存操作一起重排序

使用场景:

检查某个状态标记以判断是否退出循环:(因为所有线程修改对其他线程都是可见)

while(!asleep){
	// TODO
}

发布与逸出

核心知识点
  • 发布的概念: 指对象能够在当前作用域之外的代码中使用.
  • 发布内部的状态,会破坏封装性,会影响线程安全
  • 逸出: 某个不应该被发布的对象被发布,就成为逸出(Escape)
  • 当对象在其构造函数中创建一个线程时,this引用都会被新创建的线程共享;解决方案:在构造函数中创建但不是马上启动它,而是通过一个方法来启动。
  • 避免逸出: 要返回一个对象A的时候,拷贝对应的值创建一个对象B,然后返回对象B。(可能在拷贝的时候A对象发生了改变,造成数据不一致,取决于需求是否这样使用)

逸出常见场景1: 将对象保存在共有的静态变量.

public static Set<Object>  hello;

public void init(){
	hello = new HashSet<Object>()
}

逸出常见场景2: 非私有方法中返回一个引用.

class A{
	private Object obj = new Object()''

	public get Obj(){
		return this.obj;
	}
}

线程封闭

核心知识点
  • 避免使用同步的方式就是不共享数据: 如果仅在单线程内访问数据,就不需要同步。
  • 线程封闭: 在数据封闭在一个线程中不共享
栈封闭

定义:栈封闭?其实就是把同步变量写在局部方法中,当然就线程安全了,根本不会共享变量。线程私有。

注意事项:确保栈内对象不会逸出

ThreadLocal

概念:ThreadLocal 提供了get /set等访问接口或方法,这些方法为每个使用该变量的线程都保存一份独立的副本, 因此Get方法总是返回由当前执行线程在调用set时设置的最新值。

使用ThreadLocal,能使线程中的某个值所有改变只在该当前线程中变动,不会被其他线程操作,保证了当前线程的数据不共享,避免竞态条件的发生。

场景: Spring中的事务上下文(Transaction Context),需要对当前线程进行 回滚、提交 操作,为了当前事务不会和其他线程串了,就通过将事务上下文与某个执行线程关联起来。

缺点: 耦合性会更强,并且降低代码的可重用性

不变性

核心知识点
  • 如果某个对象被创建之后就不能被改变(不变性条件是由构造函数创建的),就一定是线程安全的。

  • 即使对象中所有的域都是final类型的,这个对象仍然可能是可变的, 因为final类型的域可以保存对 可变对象的引用

  • 不可变对象需满足的条件,代码示例符合以下三个条件: 1、对象创建以后其所有状态都不能修改。(没有修改状态的方法) 2、对象的所有域都是final类型的。 3、对象是正确创建的(创建期间,this引用没有逸出)。

class Demo{
	private final Object demoObj;

	public Demo(Object obj){
		demoObj = obj;
	}

	public Object getObj(){
		return demoObj;
	}
}
  • 保存在不可变对象中的程序状态仍然可以更新,即通过将一个保存新状态的实例来”替换”原有的不可变对象
class ImmutableObject{
	private final BigDecimal bd;

	ImmutableObject(String val){
		bd = new BigDecimal(val);
	}
}

...

ImmutableObject io = new ImmutableObject("2");
io = new ImmutableObject("3");

io是不可变对象,但它的程序状态 bd仍然可以更新, 因为我创建了一个保存新状态的实例来替换原有的不可变对象io;

Final域
  • final类型的域是不能被修改的(但是所引用的对象可能是可变的)。
  • 除非需要更高的可见性,否则应该将所有的变量声明为私有的。
  • 除非需要某个域是可变的,否则变量应该声明为final类型的
  • 对于在访问和更新 多个相关变量时出现的竞态条件问题,可以通过将这些变量全部保存在一个不可变对象中来消除。

final类型的域初始化之后就不能更改赋值。但如果引用的对象是可变的,比如:

public static final Set<String> se = new HashSet();		
se.add("hello");
se.add("world");

安全的发布

前面讨论的三种方式 栈封闭、ThreadLocal、对象中不可变final变量。前两种都是为了使对象不发布不共享,保存在单个线程中。现在要讨论如何安全的共享变量。

错误示例:即使在构造函数中正确的创建不可变对象,也很难保证其他线程获取的时候构造函数没有执行完成

class Demo{
	public Object obj;

	Demo(){
		obj = new Object("Hello");
	}
}
安全发布常用模式

要安全的发布一个对象,对象的引用和对象的状态必须同时对其他线程可见,(安全的发布重点在于发布的过程中不会存在竞态条件,发布之后还是要有相应的同步机制 !) 以下是安全发布的方式:

  • 在静态初始化函数中,初始化一个对象引用 : 静态初始化器由JVM在类的初始化阶段执行,JVM内部存在着同步机制,所以任何对象都可以被安全的发布。
  • 将对象的引用保存到 volatile 类型的域 或者 AtomicReference对象中。
  • 将对象的引用保存到 某个正确构造对象(没有逸出)的 final 域中。(对象不可变)
  • 将对象的引用保存到 一个由锁保护的域中。
  1. 线程安全的容器(“装饰器设计模式”)满足上述最后一条需求。例如:Vetor / synchronizedList / Hashtable / BlockingQueue / ConcurrentMap .e.g ;

  2. 类库中的其他数据传递机制(例如Future和Exchanger)同样能实现安全发布。

  3. 事实不可变对象:对象在技术中可以改变,但是业务中其状态在发布后不会被改变。只读

总结

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

  • 不可变对象可以通过任意方式发布
  • 事实不可变对象发布需要通过安全方式
  • 可变对象必须通过安全方式发布,发布后的改变需要同步机制

重点:在并发程序中使用和共享对象,可以采用以下策略

  • 线程封闭:栈封闭、ThreadLocal
  • 只读共享:将对象修改为不可变对象。(包括事实不可变对象)
  • 线程安全共享: 线程安全的对象在其内部实现同步,多个线程通过公有接口访问,比如线程安全类
  • 保护对象: 使用同步机制来保证访问。(包括封装在线程安全对象中的对象)