对象的共享
Posted 竹马今安在
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对象的共享相关的知识,希望对你有一定的参考价值。
1.可见性:我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态 变化。
NoVisibility可能会持续循环下去,因为读线程可能永远都看不到ready的值。还有一种情况下,可能会输出0,他看到了ready,却没有看到number,这种情况叫做“重排序”
也就是说,在没有同步的情况下,编译器、处理器以及运行时都可能对操作的执行顺序进行一些意想不到的调整。
1.1 失效数据
NoVisibility展现了一种可能产生错误结果的一种情况:失效数据。
这个类不是现成安全的,get和set都是在没有同步的情况下访问value的。如果某个线程调用了set,那么另一个正在调用get的线程可能会看到set以后的数据,也可能看不到
通过使用同步方法,就成了一个线程安全的类
1.2 非原子的64位操作
线程在没有同步的情况下,读取的一个失效值,再怎么也是之前某个线程设置的,而不是随机的,这叫做最低安全性,不过有一个例外,非volatile类型的64位数值变量(double和long).JVM允许将64的读操作或写操作分解为两个32位的操作。如果读取一个非volatile类型的long变量,读和写在不同的线程内执行,很可能得到一个值得高32位和另一个值得低32位。因此,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的!除非用volatile来声明他们,或者用锁保护起来
1.3 加锁与可见性
加锁的含义不仅仅局限于互斥行为,还包括内存可见性,为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步!!!!
1.4 Volatile变量
一种稍弱的同步机制,用来确保将变量的更新通知到其他线程。
当把一个变量声明为volatile类型之后,编译器和运行时都会注意到这个变量时共享的,因此不会将该变量上的操作与其他内存操作一起重排序,因此在读取volatile类型的变量时总会返回最新写入的值。
仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。Volatile变量的正确使用方式是:确保它们自身状态的可见性,确保他们所引用对象的状态的可见性,以及表示一些重要的程序生命周期事件的发生。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
当且仅当满足以下所有条件时,才可以使用volatile变量:
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量不会与其他状态变量一起纳入不变性条件中
- 在访问变量时不需要加锁
2.发布和逸出
“发布”一个对象指的是使对象能够在当前作用域之外的代码中使用。当某个不该发布的对象被发布时,这种情况就叫做“逸出”
将一个指向该对象的引用保存到其他代码可以访问的地方
发布对象的最简单方法就是将对象的引用保存到一个公有的静态变量中。在initialize方法中实例一个新的HashSet对象,并发布。当发布某个变量时,可能会间接的发布其他对象如果将一个Sectet对象添加到knownSecrets中,那么同样会发布这个对象。
在一个非私有的方法中返回该引用
如果按照上述方式来发布states,就会使内部的可变状态逸出。任何调用者都可以改变这个数组的内容
将引用传递到其他类的方法中
发布一个内部的类实例,当发布EventListener()时,也隐含的发布了ThisEscape实例本身。
安全的对象构造过程
上面从构造函数中发布对象时,只是发布了一个尚未构造完成的对象。即使发布对象位于构造函数的最后一行也是如此。因此不要在构造过程中使this引用逸出。
如果想在构造函数中注册一个事件监听器或者启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法。
3 线程封闭
为了避免使用同步的方式就是不共享数据,如果仅在单线程内访问数据,就不需要同步,这种技术叫做线程封闭
当某个被封闭的对象本身不是线程安全的封闭在一个线程中时,也将自动实现线程安全性
3.1 Ad—hoc线程封闭
指的是:维护线程封闭性的职责完全由程序来实现承担!
在volatile中存在一种特殊的线程封闭。只要能确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全的在这些共享的volatile变量上执行“读取-修改-写入”操作。在这种情况下,相当于将修改操作封闭在当个线程中以防止发生竞态条件,而volatile的可见性还保证了其他线程可以看到最新的值
3.2栈封闭
一种特例。在栈封闭中,只能通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。栈封闭也被称为线程内部使用或者线程局部使用
3.3 ThreadLocal类
这个类能使线程中的某个值与保存值得对象关联起来。ThreadLocal提供了get与set等访问接口或方法
这个类为提供了get和set方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程再调用set时设置的最新值。
ThreadLocal对象通常用于防止对可变的单实例变量或全局变量进行共享
JDBC的连接对象不一定是安全的,因此,当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过把JDBC的连接保存到ThreadLocal中,每个线程都会拥有属于自己的连接
4 不变性
不可变对象一定是线程安全的!
当满足一下条件,对象才是不可变得:
1.对象创建以后他的状态就不能改变
2.对象的所有域都是final类型
3.对象时正确创建的(对象创建期间,this引用没有逸出)
在不可变对象的内部可以使用可变对象来管理它们的状态
4.1 Final域(域是一种属性,可以是一个类变量,一个对象变量,一个对象方法变量或者是一个函数的参数)
final类型的域是不能修改的(如果final域引用的对象时可变的,那么这些被引用的对象时可以修改的)
4.2 示例:使用Volatile类型来发布不可变对象
创建一个对数值和因数分解结果进行缓存的不可变容器类
因式分解Servlet里面有两个原子操作:更新缓存的结果,以及通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的因数分解结果。
如果是一个不可变得对象,一个线程获得了该对象的引用时,不必担心另一个线程修改,如果想要更新这些变量,那么久可以创建一个新的容器对象,其他使用原有对象的线程因为volatile仍然会看到最新的结果
public class VolatileCachedFactorizer implements Servlet{
//volatile对象确保可见性! private volatile OneValueCache cache=new OneValueCache (null,null);
public void service(ServletRequest req,ServletResponse resq){ BigInteger i=extractFromRequest(req);
//判断是否有缓存,OneValueCache是不可变的,所以不怕其他线程干扰 BigInteger[] factors=cache.getFactors(i); if(factors==null){ factros=factor(i);
//更新cache变量,创建新的容器对象 cache=new OneValueCache(i,factros); } encodeIntoResponse(resp,factors); } }
5 安全发布
public Holder holder; public void initialize(){ holder=new Holder(42); }
5.1 不正确的发布:正确的对象被破坏
如果使用上面的方式发布Holder,由于没有使用同步来确保Holder对象对其他线程可见,因此Holder被称为“未被正确发布”。
未被正确发布的对象有两个问题:除了发布对象的线程外,其他线程可能看到的Holder域是一个失效值。也可能看到Holder引用的值是最新的,但是Holder状态的值却是失效的。
5.2 不可变对象与初始化安全性
任何线程都可以在不需要额外同步的情况下安全的访问不可变对象,即使在发布这些对象时没有使用同步。
如果上面的Holder对象时不可变得,那么即使Holder没有被正确的发布,也不会抛出异常。
5.3 安全发布的常用模式
安全发布的方式:
1.在静态初始化函数中初始化一个对象引用
2.将对象的引用保存到volatile类型的域或者AtomicReferance对象中
3.将对象的引用保存到某个正确构造对象的final类型域中
4.将对象的引用保存到一个由锁保护的域中
要发布一个静态够早的对象,最简单和最安全的方式是使用静态的初始化块
public static Holder holder=new Holder(42);
静态初始化块由JVM在类的初始化阶段执行,由于在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全的发布!
5.4 事实不可变对象
如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么就把这种对象叫做“事实不可变对象”
例如:Date本身是可变的,如果视作不可变对象,那么在多个线程之间共享Date对象时,就可以省去对锁的使用。
public Map<String,Date> lastLogin=Collections.synchronizedMap(new HashMap<String,Date>());
如果Date对象的值在被放入Map后就不会改变,那么synchronizedMap中的同步机制就足以使Date值被安全的发布,并且在访问这些Date值时不需要额外的同步。
5.5 可变对象
对于可变对象,不仅在发布时需要使用同步,在每次对象访问的时候也得使用同步。
对象的发布需求取决于它的可变性:
1.不可变对象可以通过任意机制来发布
2.事实不可变对象必须通过安全方式来发布
3.可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来
5.6 安全地共享对象
在并发程序中使用和共享对象时,可以使用一些实用的策略
1.线程封闭 线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改
2.只读共享 在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象
3.线程安全共享 线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步
4.保护对象 被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象
以上是关于对象的共享的主要内容,如果未能解决你的问题,请参考以下文章
SnippetsLab for Mac 1.9 中文共享版 – 强大的代码收藏管理工具