Skip to content

Latest commit

 

History

History
251 lines (146 loc) · 10.1 KB

对象的共享.md

File metadata and controls

251 lines (146 loc) · 10.1 KB

对象的共享

一. 可见性

  • 可见性指当一个线程修改了对象状态之后,其他线程能看到发生的状态变化.

  • 在没有同步的情况下,编译器,处理器及运行时等都可能对操作的执行顺序进行一些意想不到的调整.

  • 只要有数据在多个线程之间共享,就得使用正确的同步.

失效数据

  • 读取某一变量时,可能会得到的一个已经失效的值(旧值,默认值)

  • 对set方法和get方法进行同步(仅仅set同步是不够的)

非原子的64位操作

  • Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的longdouble变量,JVM允许将64位的读取操作或写操作分解为两个32位的操作.在多线程中可能导致高32位和低32位在不同线程中.

  • 在多线程中共享可变的doublelong等类型,需要用volatile声明,或用锁保护起来.

加锁与可见性

  • 加锁的含义不仅仅局限于互斥行为,还包括内存可见性.为了确保所有线程都能看到共享变量的最新值,所有读写操作的线程都要在同一个锁上同步.

Volatile变量

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

  • volatile变量的三种正确使用方式:

    1. 确保自身状态可见性.
    2. 确保引用对象的状态可见性.
    3. 标识一些重要的程序生命周期事件的发生(初始化或关闭)
  • 当满足以下所有条件时,才该使用volatile变量:

    1. 对变量的写入操作不依赖变量的当前值,或者能确保只有单个线程更新变量值.
    2. 该变量不会与其他状态变量一起纳入不变性条件中.
    3. 访问变量时不需要加锁.
  • 加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性.

二. 发布与逸出

  • 发布(Publish): 对象能够在当前作用域之外的代码中使用.
    逸出(EScape): 当某个不该发布的对象被发布时,这种情况就称为逸出.

  • 发布内部状态可能会破坏封装性,并使得程序难以维持不变性条件.

  • 发布对象的方式:

    1. 将对象的应用保存到一个共有的静态变量中:

       public static Set<Secret> knownSerects;
      
       public void initialize(){
       knownSecrets = new HashSet<Secret>();
       }
      
    2. 间接发布其他对象:

      将一个Secret对象添加到knownSecret中,这个对象也会被发布.

    3. 发布一个对象时,该对象的非私有域中引用的所有对象都会被发布.

    4. 发布一个内部类的实例:

       public class ThisEscape{
      
           public ThisEscape(EventSource source){
               source.registerListener(
                   new EventListener(){
                       public void onEvent(Event e){
                           doSomething(e);
                       }
                   });
           }
       }
      

      ThisEscape发布EventListener时,也隐含地发布了ThisEscape实例本身.因为内部类实例包含对外部类实例的隐含引用.

安全的对象构造过程:

  • 不正确的对象构造过程: 当从对象的构造函数中发布该对象时,只发布了一个尚未构造完成的对象(即使发布语句位于最后一行).
    如果this引用在构造过程中逸出,那么这种对象就被认为是不正确构造.

  • this引用逸出:

    1. 在构造函数中创建并启动一个线程.
    2. 在构造函数中调用一个可改写的实例方法(既不是私有也不是终结方法).

三. 线程封闭

  • 一种避免使用同步的方式就是不共享数据,如果仅在单线程内访问数据,就不需要同步,这种技术被称为线程封闭(Thread Confinement).

Ad-hoc线程封闭

  • Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担.

  • Ad-hoc线程封闭技术较为脆弱,应尽量少用.

栈封闭

  • 栈封闭是线程封闭的一中特例,在栈封闭中,只能通过局部变量才能访问对象.

  • 局部变量的固有属性之一就是封闭在执行线程中,为与执行线程的栈中,其他线程无法访问这个栈.

ThreadLocal类

  • 维持线程封闭的更规范的方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来.

  • ThreadLocal将特定于线程的值保存在Thread对象中,当线程终止后,这些值会作为垃圾回收.

  • ThreadLocal变量类似于全局变量,能降低代码的可重用性,并在类之间引入隐含的耦合性.

四. 不变性

  • 不可变对象: 如果某个对象在被创建后其状态不能被修改,那么这个对象就称为不可变对象.

  • 不可变对象一定是线程安全的

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

    1. 对象创建后其状态就不能修改.
    2. 对象的所有域都是final类型.
    3. 对象是正确创建的(创建期间this没有逸出)
  • 不可变对象的内部仍可以使用可变对象来管理他们的状态:

      // 可变对象的基础上构件不可变类
      public finall class ThreeStooges{
          private final Set<String> stooges = new HashSet<String>();
    
          public ThreeStooges(){
              stooges.add("janke");
              stooges.add("wususu");
          }
          public boolean isStooge(String name){
              return stooges.contains(name);
          }
      }
    

Final域:

  • final类型的域是不可修改的.
    final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步.

除非需要更高的可见性,否则所有域都应声明为私有.
除非需要某个域是可变的,否则应将其声明为final.

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

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

      // 不可变容器类
      class OneValeCache{
          private final BigInteger lastNumber;
          private final BigInteger[] lastFactors;
    
          public OneValueCache(BigInteger i, BigInteger[] factors){
              lastNumber = i;
              lastFactors = Arrays.copyOf(factors, factors.length);
          }
    
          public BigInteger[] getFactors(BigInteger i){
              if(lastNumber == nul || !lastNumber.equals(i))
                  return null;
              else
                  return Arrays.copyOf(lastFactors, lastFactors.length);
          }
      }
    
      public class VolatileCachedFactorizer implements Servlet{
          private volatile OneValueCache cache = new OneValueCache(null, null);
    
          public void service(ServletRequest req, ServletResponse reso){
              BigInteger i = extractFromRequest(req);
              BigInteger[] factors = cache.getFactors(i);
              if(factors == null){
                  factors = factor(i);
                  cache = new OneValueCache(i, factors);
              }
              encodeIntoResponse(resp, factors);
          }
      }
    
    • 通过使用包含多个状态变量的容器对象来维持不变性条件,并使用一个volatile类型引用来确保可见性.

五. 安全发布

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

  • 初始化安全性保证:

    1. 为了确保对象状态能呈现出一致的视图,必须使用同步.
    2. 若不同步,必须满足不可变性的三个要求

 在"准备"阶段,如果存在被final修饰的域,则一开始就初始化为其指定的值(而不是默认值)

  • 如果final类型的域指向的是可变对象,那么在访问这些域所指向的对象的状态时仍需同步.

安全发布的常用模式:

  • 一个正确构造的对象可以通过以下方式来安全发布:

    1. 在静态初始化函数中初始化一个对象引用.(静态初始化器)
    2. 将对象的引用保存到volatile类型的域或AtomicReference对象中.
    3. 将对象的引用保存到某个正确构造对象的final类型域中.
    4. 将对象的引用保存到一个由锁保护的域中.

+要发布一个对象,最简单安全的方式是使用静态的初始化器:

    public static Holder holder = new Holder(42);

静态初始化器由JVM在类初始化阶段执行.由于JVM内部存在同步机制,所以这种方式初始化的任何对象都可以被安全发布.

事实不可变对象:

  • 对象在技术上来看是可变的,但其状态在发布后不会再改变,这种对象称为事实不可变对象(Effectivelt Immutable Object).(例如,Date())

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

可变对象:

  • 对象在构造后可以修改,安全发布只能确保"发布当时"的状态可见性.

  • 对于可变对象,不仅在发布时需要同步,每次在对象访问时都需要同步来确保后续修改操作的可见性.

安全地共享对象:

  1. 线程关闭: 线程封闭的对象只能由一个线程拥有.

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

  3. 线程安全共享: 线程安全的对象在其内部实现同步,多个线程可通过对象的共有接口来进行访问.

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