美文网首页
3对象共享

3对象共享

作者: WFitz | 来源:发表于2018-10-24 10:07 被阅读0次

    同步的作用

    • 确保复合操作的原子性(复合操线程间作互斥)
    • 内存可见性

    volatile

    1. 作用:将当前线程对volatile的改变立即通知给其他线程;保证了volatile变量对线程的可见性;volatile是一种比synchronizyed稍弱的同步机制
    2. 对可见性的影响:volatile变量对可见性的影响比volatile变量本身更为重要。当线程A首先写入一个volatile变量并且线程B随后读取该变量时,在写入volatile变量之前对A可见的所有变量(包括volatile变量)的值,在B读取了volatile变量后,对B也是可见的。因此,从内存可见性的角度来看,写入volatile变量相当于退出同步代码块,而读取volatile变量就相当于进入同步代码块
    3. 典型用法:检查某个状态标记以判断是否退出循环
    4. 注意:volatile的语义不足以确保递增操作(count++)的原子性,除非你能确保只有一个线程对变量执行写操作;原子变量提供了“读-改-写”的原子操作,并且尝尝做一种“更好的volatile变量”;即volatile只能确保可见性,而加锁机制既可以确保可见性又可以确保原子性
    5. 使用volatile变量的条件
      • 对变量的写入操作不依赖变量的当前值,或者你能确定只有单个线程更新变量的值
      • 该变量不会与其他变量一起纳入不变性条件中
      • 在访问变量时不需要加锁

    对象发布与溢出

    1. 发布(Publish):使对象能够在当前作用于之外的代码中使用
    2. 溢出(Escape):当某个不应该发布的对象被发布时就成为溢出
    3. 发布方式
      //方式一:将指向对象的引用保存到其他代码能访问到的地方
      //发布了new HashSet<Secret>()对象
      public static Set<Secret> knowSecrets;
      public void initialize() {
          konwnSecretets = new HashSet<Secret>();
      }
      
      //方式二:在某一个非私有的方法中返回对象引用
      //发布了new HashSet<Secret>()对象
      public Set<Secret> getSecrets() {
          return new HashSet<Secret>();
      }
      
      //方式三:将对象的引用传递到其他类的方法中
      //发布了new User()对象
      public class Caculate {
          public static Object caculate(Object o) {
              System.out.println(o);
              return o;
          }
      }
      Caculate.caculate(new User());
      
      //方式四:在已发布的对象中的非私有域中引用对象
      //发布了"AK","AL"
      class UnsafeStates {
          private String[] states = new String[] {"AK", "AL"};
          public String[] getStates() {
              return states;
          }
      }
      
      //方式五:在类的方法内发布匿名内部类;因为匿名内部类包含了当前对象的隐含引用this,随意发布匿名内部类时也发布了自己
      //发布了new EventListener(),在该对象内部包含自己的隐试引用this,即当前ThisEscape对象
      public class ThisEscape {
          private int value;
          public ThisEscape(EventSource source) {
              //此处已经将ThisEscape发布给了外部的source,存在逃逸现象
              source.registerListener(new EventListener() {
                  public void onEvent(Event e) {
                      doSomething(e);
                  }
              });
              //尚未构造完成
              value = 10;
          }
          private void doSomething(Object o) {
              System.out.println(value);
          }
      }
      

    4.对象发布的风险:无论其他线程会对已发布的引用执行何种操作,其实都不重要,因为误用该引用的风险始终存在。当某个对象逸出后,你必须假设有某各类或者线程可能会误用该对象。这正是需要使用封装的最主要原因:封装能够使得对程序的正确性分析变得可能,并使得无意中破坏设计约束条件变得更难

    安全的对象构造过程

    1. 不正确构造
      • 概念:如果this引用在对象构造过程中逸出,那么这种对象就被认为是不正确构造
      • 原因:当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态。因此,当从构造函数中发布对象时,只是发布了一个尚未构造完成的对象;即使发布对象的语句位于构造函数的最后一行也是如此
      • 经验:不要在构造过程中使this引用逸出;即在构造中不要发布对象
    2. 常见不正确构造
      • 在构造函数中发布对象
      • 在构造函数中启动线程
      • 在构造函数中调用当前类可改写的实例方法(既不是私有方法,也不是最终方法)
    3. 解决不安全构造的方法
      /**
       * 用一个私有的构造函数和一个工友的工厂方法
       * 构造函数用来实例化对象
       * 工厂方法发布已经构造完的this,并返回这个构造完的实例
       */
      public class SafeListener {
          private final EventListener listener;
          
          private SafeListener() {
              listener = new EventListener() {
                  public void onEvent(Event e) {
                      doSomething(e);
                  }
              };
          }
          
          public static SafeListener newInstance(EventSource source) {
              SafeListener safe = new SafeListener();
              source.registerListener(safe.listener);
              return safe;
          }
      }
      

    线程封闭

    1. 概念:当访问共享的可变数据时,通常需要同步;一种避免使用同步的方式就是不共享数据;如果仅在单线程内访问数据,就不需要同步;这就叫线程封闭
    2. 作用:当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性,即使被封装的对象本身不是线程安全的;线程封闭是实现线程安全的最简单方式之一
    3. 线程封闭的方式
      1. Ad-hoc线程封闭:维护线程封闭性的职责完全由程序实现来承担
        • 由与Ad-hoc线程封闭技术的脆弱性,因此在程序中尽量少用它,在可能的情况下,应该使用更强的线程封闭技术(例如,栈封闭或ThreadLocal类)
      2. 栈封闭:栈封闭是一种特殊的线程封闭,在栈封闭中将对象定义为局部变量,局部变量的固有属性之一就是封闭在执行线程中
        • 基本类型的局部变量始终封闭在线程内,因为任何方法都无法获得基本类型的引用
        • 引用类型的局部变量,为了维持线程封闭,需要多做一些工作以确保被引用的对象不会溢出当前线程
      3. ThreadLocal

    不变性

    1. 不可变对象:如果某个对象在被创建后其状态就不能被修改,那么这个对象就成为不可变对象
    2. 不可变对象固有属性:线程安全性是不可变对象的固有属性之一,他们的不变性条件是由构造函数创建的,只要他们的状态不改变,那么这些不变性条件就能得以维持
    3. 不可变对象的条件
      1. 对象创建以后其状态就不能修改
      2. 对象的所有域都是final类型
      3. 对象是正确构造的(在对象构造期间,this引用没有逸出)
    4. 习惯:正如“除非需要更高的可见性,否则应将所有的域都声明为私有域”是一个良好的编程习惯,“除非需要某个域是可变的,否则应将其声明为final域”也是一个良好的编程习惯
    5. final域可见性详解: http://www.infoq.com/cn/articles/java-memory-model-6

    使用volatile类型来发布不可变对象

    1. 应用场景:需要对一组相关的数据以原子的方式执行某个操作;不适用当前状态依赖上一个操作时的状态的场景,例如i++操作
    2. 方式:通过使用包含多个相关状态变量的容器对象来维持不变性条件,并使用volatile类型的引用来确保可见性,就能使该操作在没有显示地使用锁的情况下仍然是线程安全的
    3. 示例
      /**
       * 注意:在构造和返回不变状态对象时要和线程局部变量断开关联,尤其是引用变量
       * 下面注释的两个Arrays.copyOf操作就是
       */
      class OneValueCache {
          private final BigInteger lastNumber;
          private final BigInteger[] lastFactors;
          
          /**
           * 状态由构造函数创建
           */
          public OneValueCache(BigInteger i, BigInteger[] factors) {
              lastNumber = i;
              //复制原因:防止factors被别的线程修改
              //防止线程用factors构造完OneValueCache之后修改factors,不会影响缓存数据lastFactors
              lastFacotrs = Arrays.copyOf(factors, factors.length);
          }
          
          public BigInteger[] getFactors(BigInteger i) {
              if(lastNumber == null || !lastNumber.equals(i))
                  return null;
              else
                  //复制原因:防止lastFacors被别的线程修改
                  //返回一个新的结果数组给线程,线程想怎么处理怎么处理,不会影响缓存lastFactors
                  return Arrays.copyOf(lastFacors, lastFactors.length);
          }
      }
      
      public class VolatileCachedFactorizer implements Servlet {
          private volatile OneValueCache cache = new OneValueCache(null, null);
          
          public void service(ServletRequest req, ServletResponse resp) {
              BigInteger i = exractFromRequest(req);
              //结果从不可变状态对象获取
              BigInteger factors = cache.getFactors(i);
              if(factors == null) {
                  factors = factor(i);
                  //构造新的不可变状态对象
                  cache = new OneValueCache(i, factors);
              }
              encodeIntoResponse(resp, factors);
          }
      }
      

    安全发布对象的方法

    1. 前提:对象被正确构造
    2. 安全发布机制:
      1. 在静态代码块或者静态域中初始化一个对象的引用
        • 静态初始化器有JVM在类的初始化阶段执行。由与在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布
      2. 将对象的引用保存到volatile类型的域
      3. 将对象的引用保存到AtomicReference类型的域中
      4. 将对象的引用保存到final类型的域中
      5. 将对象的引用保存到一个由锁保护的域中
      6. 通过线程安全库的容器类发布对象
    3. 线程安全库提供的安全发布保证
      1. 通过将一个键或者值放入Hashtable、synchronizecdMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)
      2. 通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将元素安全地发布到任何从这些容器中访问元素的线程
      3. 通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将元素安全地发布到任何从这些队列中访问元素的线程
      4. 类库中的其他数据传递机制(例如Futrue和Exchanger)同样能实现安全发布

    事实不可变对象

    1. 概念:如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么这种对象称为“事实不可变对象(Effectively Immutable Object)”
    2. 结论1:在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象
    3. 结论2:通过使用事实不可变对象,不仅可以简化开发过程,而且还能由于减少了同步而提高性能

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

    1. 不可变对象可以通过任意机制来发布
    2. 事实不可变对象必须通过安全的发布机制来发布
    3. 可变对象必须通过安全的发布机制来发布,并且必须是线程安全的或者有某个锁保护起来

    并发编程中共享对象的一些使用策略

    1. 线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改
    2. 只读共享:在没有额外同步的情况下,共享的只读对象可以有多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象
    3. 线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口进行访问而不需要进一步的同步
    4. 保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他安全对象中的对象,以及已发布的并且有某个特定的锁保护的对象

    相关文章

      网友评论

          本文标题:3对象共享

          本文链接:https://www.haomeiwen.com/subject/qzrttqtx.html