对象的共享


重排序是指在没有同步的情况下,编译器,处理器可能对代码的执行顺序进行一些调整

例如如下代码,由于没有使用同步机制,读线程可能看不到ready的修改,而一直循环下去,也可能由于重排序,看到ready的修改而number没有修改二输出为0;


使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。

Java线程中有一个Thread.yield( )方法,很多人翻译成线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,让自己或者其它的线程运行。

失效数据:在缺少同步程序中产生错误的结果的一种情况,造成程序的不确定性

非原子的64位操作:即使是失效数据也是程序过去运行中产生的数据,但执行非原子的64位操作,jvm会分解为两个32位操作,从而可能造成错误的值,使用volatile关键字解决。

加锁与可见性:

加锁的作用不仅仅局限在互斥,还包括在内存可见性。

锁可以确保某个线程以一种可以预测的方式来查看另外一个线程的执行结果,即前一线程结果后一个线程可见。

为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一锁上同步。

Volatile变量:

比synchronized更加轻量级的同步机制,用来确保变量的更新操作通知到其他线程,编译器与运算时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile不会缓存在寄存器或者其他的处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

当且仅当满足所有条件时,才应该使用volatile变量:

对变量的写入操作不依赖变量的当前值,或者你能确保只有一个单线程更新变量的值

该变量不会与其他状态的变量一起纳入不变性条件中。

访问变量时不需要加锁

发布与逸出:

发布是指:对象能够在当前作用域之外的代码中使用。

逸出:某个不应该被发布的对象被发布时,这种情况就称为逸出。

使用封装的原因是:封装能够使对程序的正确性进行分析变得可能,并使得无意中破坏设计约束条件变得更加难。

不要在构造函数中使用this引用逸出。

在构造过程中使用this引用逸出的一个常见错误是,在构造函数中启动一个线程,当对象在其构造函数中创建一个线程时,无论是显式(通过将它传给构造函数)的还是隐式(由于Thread或者Runable是该对象的一个内部类)的创建,this引用都会被新创建的线程共享,在对象未完全构造之前,新的线程就可以看见它,在构造函数中创建线程并没有错误,但最好不立即启动它,二是通过一个start或者initalize方法来启动,在构造函数中调用一个可改写的实例方法时,同样会导致this引用在构造过程中逸出。

线程封闭:

当访问共享的可变数据时,通常需要同步,一种避免使用同步方式就是不共享数据,如果仅在单线程内访问数据,就不需要同步,这种技术称为线程封闭。

1:ad-hoc线程封闭

         这是完全靠实现者控制的线程封闭,他的线程封闭完全靠实现者实现。也是最糟糕的一种线程封闭。所以我们直接把他忽略掉吧。

2:栈封闭

        栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量。多个线程访问一个方法,此方法中的
局部变量都会被拷贝一分儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。

3:ThreadLocal封闭

      使用ThreadLocal是实现线程封闭的最好方法,有兴趣的朋友可以研究一下ThreadLocal的源码,其实ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。这里就不说ThreadLocal的使用方法了,度娘一下便知。

不变性:

满足同步需求的另外一种方法是使用不可变对象,不可变对象是线程安全的。

当满足以下条件时,对象才是不可变的:

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

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

对象是正确创建(在对象创建的过程中,this引用没有溢出)

我们来分析下面这个类。
[java]  view plain  copy
  1. @Immutable  
  2. public final class ThreeStooges {  
  3.     private final Set<String> stooges = new HashSet<String>();  
  4.   
  5.     public ThreeStooges() {  
  6.         stooges.add("Moe");  
  7.         stooges.add("Larry");  
  8.         stooges.add("Curly");  
  9.     }  
  10.   
  11.     public boolean isStooge(String name) {  
  12.         return stooges.contains(name);  
  13.     }  
  14. }  

        在不可变对象的内部仍可以使用可变对象来管理它们的状态, 如 ThreeStooges 所示。尽管保存姓名的Set对象是可变的,但从ThreeStooges的设计中可以看到,在Set对象构造完成后无法对其进行修改。stooges是一个final类型的引用变量,因此所有的对象状态都通过一个final域来访问。最后一个要求是“正确地构造对象”,这个要求很容易满足,因为构造函数能使该引用由除了构造函数及其调用者之外的代码来访问。

        由于程序的状态总在不断地变化,你可能会认为需要使用不可变对象的地方不多,但实际情况并非如此。在“不可变的对象”与“不可变的对象引用”之间存在着差异。保存在不可变对象中的程序状态仍然可以更新,即通过将一个保存新状态的实例来“替换”原有的不可变对象。        

final 字段一旦被初始化完成,并且构造器没有把 this 引用传递出去,那么在其他线程中就能看到 final 字段的值(域内变量可见性,和 volatile 类似),而且其外部可见状态永远也不会改变。它所带来的安全性是最简单最纯粹的。

即使对象是可变的,通过将对象的某些域声明为final类型,仍然可以简化对状态的判断,因此限制对象的可变性也就相当于限制了该对象可能的状态集合。仅包含一个或两个可变状态的“基本不可变”对象仍然比包含多个可变状态的对象简单。通过将域声明为final类型,也相当于告诉维护人员这些域是不会变化的。

        正如“除非需要更高的可见性,否则应将所有的域都声明为私有域”是一个良好的变成习惯,“除非需要某个域是可变的,否则应将其声明为final域”也是一个良好的变成习惯。

示例:使用 Volatile 类型来发布不可变对象

        之前我们讲过, volatile 可以用来保证域的可见性而不能保证变量操作的原子性,更为准确的讲,只能保证读写操作具有原子性,而不能保证自增 i++ 等运算操作的原子性。

        在前面的UnsafeCachingFactorizer类中,我们尝试用两个AtomicReferences变量来保存最新的数值及其因数分解结果,但这种方式并非是线程安全的,因为我们无法以原子方式来同时读取或更新这两个相关的值。同样,用volatile类型的变量来保存这些值也不是线程安全的。然而,在某些情况下,不可变对象能提供一种弱形式的原子性。

        因式分解Servlet将执行两个原子操作:更新缓存的结果,以及通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的因数分解结果。每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据,例如 OneValueCache。

[java]  view plain  copy
  1. @Immutable  
  2. class OneValueCache {  
  3.     private final BigInteger lastNumber;  
  4.     private final BigInteger[] lastFactors;  
  5.   
  6.     /** 
  7.      * 如果在构造函数中没有使用 Arrays.copyOf()方法,那么域内不可变对象 lastFactors却能被域外代码改变 
  8.      * 那么 OneValueCache 就不是不可变的。 
  9.      */  
  10.     public OneValueCache(BigInteger i,  
  11.                          BigInteger[] factors) {  
  12.         lastNumber  = i;  
  13.         lastFactors = Arrays.copyOf(factors, factors.length);  
  14.     }  
  15.   
  16.     public BigInteger[] getFactors(BigInteger i) {  
  17.         if (lastNumber == null || !lastNumber.equals(i))  
  18.             return null;  
  19.         else  
  20.             return Arrays.copyOf(lastFactors, lastFactors.length);  
  21.     }  
  22. }  

        对于在访问和更新多个相关变量时出现的竞争条件问题,可以通过将这些变量全部保存在一个不可变对象中来消除。如果是一个可变的对象,那么就必须使用锁来确保原子性。如果是一个不可变对象,那么当线程获得了对该对象的引用后,就 不必担心另一个线程会修改对象的状态 。如果要更新这些变量,那么可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。

        在 VolatileCachedFactorizer使用了OneValueCache来保存缓存的数值及其因数。我们将 OneValueCache 声明为 volatile,这样当一个线程将cache设置为引用一个新的OneValueCache时,其他线程就会立即看到新缓存的数据。

[java]  view plain  copy
  1. @ThreadSafe  
  2. public class VolatileCachedFactorizer implements Servlet {  
  3.     private volatile OneValueCache cache =  
  4.         new OneValueCache(nullnull);  
  5.   
  6.     public void service(ServletRequest req, ServletResponse resp) {  
  7.         BigInteger i = extractFromRequest(req);  
  8.         BigInteger[] factors = cache.getFactors(i);  
  9.         if (factors == null) {  
  10.             factors = factor(i);  
  11.             cache = new OneValueCache(i, factors);//声明为 volatile ,防止指令重排序,保证可见性  
  12.         }  
  13.         encodeIntoResponse(resp, factors);  
  14.     }  
  15. }  

        与cache相关的操作不会相互干扰,因为OneValueCache是不可变的,并且在每条相应的代码路径中只会访问它一次。通过使用包含多个状态变量的容器对象来维持不变性条件,并使用一个volatile类型的引用来确保可见性,使得Volatile Cached Factorizer在没有显式地使用锁的情况下仍然是线程安全的。

 

【不安全发布的示例】

 

可见性问题;其他线程看到的是不一致的状态;对象尚未创建完成就发布出去了。

 

不正确的发布:正确的对象被破坏

 

上被完全创建的对象还没有完整性,此时发布出去将使其他线程看到的是不一致的状态,然后看到对象的状态突然发生变化,即使线程在对象发布后还没有修改过它。

 

【示例代码】

 

未正确发布对象可能有两种情况:

1. 其他线程看到的对象的域可能是一个空引用,或者之前的旧值;

2. 更糟糕的情况是,线程看到的对象的引用是最新的,但是对象状态的值却是失效;

3. 线程在第一次读取域时得到的失效值,再次读取的时候会得到一个更新值。

 

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

 

由于不可变对象是一种非常重要的对象,因此Java内存模型为不可变对象的共享提供了一种特殊的初始化安全性保证。(啥保证)

 

为了确保对象状态能呈现出一致性的视图,就必须使用同步。

 

另一方面,即使在发布不可变对象的引用时没有使用同步,也仍然可以安全地访问该对象。

 

为了维持这种初始化安全性的保证,必须满足不可变性的所有需求:状态不可修改,所有域有是final类型,以及正确的构造过程。

 

Object的构造函数会在子类构造函数运行之前先将默认值写入所有的域,因此某个域的默认值可能被视为失效值。

 

任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。

 

这种保证还将延伸到被正确创建对象中所有fianl类型的域。在没有额外同步的情况下,也可以安全地访问fianl类型的域。然而,如果fianl类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态仍然需要同步。

 

安全发布的常用模式

 

可变对象必须通过安全的方式来发布,这通常意味着在发布和使用该对象的线程时都必须使用同步。

 

使用对象的线程能够看到该对象处于已发布的状态,并稍后介绍如何在对象发布后对其可见性进行修改。

 

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布。

 

(以前从未考虑过对象的引用和对象的状态的关系,引用不是一定可以拿到对象的状态么?原来不是这样的)

 

  • 在静态初始化函数中初始化一个对象的引用。
  • 将对象的引用保持到volatile类型的域或者AtomicReference对象中。
  • 将对象的引用保存到某个正确构造对象的final类型域中。
  • 将对象的引用保存到一个由锁保护的域中。

 

在线程安全容器内部的同步意味着,将对象放入到某个容器,例如Vector或synchronizedList时,将满足最后一条需求。如果线程A将对象X放入到一个线程安全的容器,随后线程B读取这个对象,那么可以确保B看到A设置的X状态,即便在这段读/写X的应用程序代码中没有包含显式的同步。

 

通过讲一个键或者值放入Hashtable,synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问,还是通过迭代器访问)。通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程。通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。

 

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

 

通常要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化:

【代码演示】

 

静态初始化由JVM在类的初始化阶段执行。由于在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布。

事实不可变对象

如果对象在发布之后不会被修改,那么对于其他在没有额外同步的情况下安全地访问这些对象的线程来说,安全发布时足够的。如果一个对象从技术上来说的可变的,但是其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象(Effectively Immutable Object)“。

 

在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

 

比如Date本身是可变的,但如果将它作为不可变对象来使用,那么在多个线程之间共享Date对象时,就可以省去对锁的使用。

 

可变对象

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

 

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

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

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

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

 

安全地共享对象

 

当获得一个对象的引用时,你需要知道这个引用上可以执行哪些操作。在使用它之前是否需要获得一个锁?是否可以修改它的状态,或者只能读取它?许多并发错误都是由于没有理解共享对象的这些“既定规则”而导致的。当发布一个对象时,必须明确说明对象的访问方式。

 

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

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

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

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

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



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值