java并发编程读书笔记(1)-- 对象的共享

1. 一些原则

  1. RIM(Remote Method Invocation):远程方法调用
  2. Race Condition:竞态条件
  3. Servlet要满足多个线程的调用,必须是线程安全的
  4. 远程对象,即通过远程方法调用将对象放入字节流中传给其他jvm的对象,要特别注意对象中的共享状态
  5. Shared:共享的
  6. Mutable:可变的
  7. 当设计线程安全的类时,良好的面向对象技术、不可修改性,以及明晰的不变性规范都能起到一定的帮助作用;
  8. 无状态对象是线程安全的:没有任何域也不包含任何对其他类中域的引用(比如StatelessFactory implements Servlet),多个线程访问并没有共享状态,不会影响其正确性。
  9. 最常见的一个静态类型是:先检查后执行(Check-Then-Act)操作,即通过一个可能失效的观测结果来决定下一步的动作。
  10. 要保持状态的一致性,就需要自单个原子操作中更新所有相关的状态变量
  11. 内置锁(Intrinsic Lock),监视锁(Monitor Lock):每个对象都可以当做。
  12. 重入:当某个线程请求一个由其他线程持有的锁时会被阻塞,但请求他自己持有的锁就会成功。内置锁是可重入锁。
  13. 子类继承了父类,重写了父类的synchronized方法,访问子类的这个方法时要先获得父类的锁,然后获取自身的锁。如果在这个方法里又调用了父类的这个方法(super.xx),可以继续说明,获取的父类锁可以重入。
  14. 对于每个包含多个变量的不变条件,其中涉及的多有变量都需要由同一个锁来保护
  15. java内存模型要求变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的64位数值变量(double,long),jvm允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非使用volatile来声明他们或者用锁保护起来。
  16. 加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有的线程都能看到共享变量的最新值,所有的执行读操作或写操作的线程都必须在同一个锁上同步。
  17. 把变量声明为volatile类型后,编译与运行时都会注意到这个变量是共享的,因此不会讲该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存到寄存器或者对其他处理器不可见的地方,因此在读取volatile变量时总会返回最新写入的值。
  18. volatile的一个用法:while的条件变量,为保证可见性。
  19. 加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
  20. 发布(Publish)一个对象:是对象能够在当前作用域之外的代码中使用。
  21. 逸出(Escape):当某个不应发布的对象被发布时。
  22. 不要再构造过程中使this逸出。

2. 对象的共享

2.1 volatile确保可见性

java内存模型要求变量的读取操作和写入操作都必须是原子操作,但对于非volatile类型的64位数值变量(double,long),jvm允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非使用volatile来声明他们或者用锁保护起来。

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

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

2.2 volatile可见性的应用

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  • 该变量不会与其他状态变量一起纳入不变性条件中
  • 在访问变量时不需要加锁

比如,while循环的条件变量,如果监视是否改变,需要设置为volatile,否则访问while的线程会将asleep放到工作区,一直循环直到某刻去内存读取。

volatile boolean asleep;
...
   while(!asleep)
          countSomeSheep();

  2.3发布与逸出

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

在许多情况,我们要确保对象及其内部状态不被发布。而在某些情况,我又需要发布该对象,但如果在发布时要确保线程安全性,则可能需要同步。发布内部状态可能会破坏封装性,使线程难以维持不变的状态。例如,如果在对象构造完成之前就发布该对象,就会破坏线程安全性。

发布对象的最简单的方法是将对象的引用保存到一个共有的静态变量中。

逸出(Escape):当某个不应该发布的对象呗发布时。

class UnsafeStates{
   private String[] states = new String[]{"as","dsf",...};  
   public String[] getStates(){return states;}
}

  如果按上述的方式发布states,则任何调用者都可以修改数组,这是不安全的。

 2.4 构造函数中this逸出

 上述构造函数好像没啥问题,至少我看本书之前看不出。构造函数中的对象是this,其他譬如引用类属性的拥有者为this,如果将这哥匿名类传递给source,source如果对其进行了引用,而这时候构造函数还没结束即没有创建ThisEscape的对象,这个匿名类也还没构造,即空。当且仅当对象的构造函数返回时,对象才处于一个可预测和一致的状态。

参考http://bruce008.iteye.com/blog/1461345

文章指出,匿名内部类编译的结果表明ThisEscape这个类会作为一个final成员变量放到匿名内部类中,即这个ThisEscape被逃逸出去了。因此不要再构造函数中发布匿名类和起线程。

 2.5线程封闭

 当某个对象封闭在一个线程中时,这种方法将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。

public int loadTheArk(Collection<Animal> candidates){
        SortedSet<Animal> animals;
        int numPairs  = 0;
        Animal candidate = null;

        //animals被封闭在方法中,不要使他们逸出
        animals = new TreeSet<Animal>(new SpeciesGenderComparator());
        animals.addAll(candidates);
        for (Animal a:animals) {
            if(candidate==null || !candidate.isPotentialMate(a))
                candidate = a;
            else{
                ark.load(new AnimalPair(candidate,a));
                ++numPairs;
                candidate = null;
            }
        }
        return numPairs;
    }

  如果在线程内部(Within-Thread)上下文中使用非线程安全的对象,那么该对象仍然是线程安全的。但要注意后期维护的时候对象逸出。

2.6.ThreadLocal类

维持线程封闭性的一种更规范方法是使用ThreadLocal。ThreadLocal对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。比如,在单线程应用中可能会维持一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个connection对象。由于jdbc的连接对象不一定是线程安全的,因此当多线程应用程序在没有协同的情况下使用全局变量时,就不是线程安全的。通过将jdbc的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接:

 private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>(){
        public Connection initialValue(){
            try {
                return DriverManager.getConnection("url","username","password");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    };

    public static Connection getConnection(){
        return connectionHolder.get();
    }

  当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。

当某个线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值。

ThreadLocal变量类似于全局比阿娘,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。

 2.7. 不变性

满足同步需求的另一种方法是使用不可变对象(Immutable Object)。如果某个对象在被创建后其状态就不能修改,那么这个对象就成为不可变对象。

  • 对象创建后其状态就不能修改。final
  • 对象的所有域都是final
  • 对象是正确创建的(在创建对象期间,this没有逸出)

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

2.8 安全的发布

在某些情况下,我们希望在多个线程间共享对象,此时必须确保安全地进行共享。以下是错误的:

//不安全的发布
public Holder holder;
public void initialize(){
    holder = new Holder(22);
}

  看起来应该没什么问题才对,实际上多线程访问的时候可能得到一个尚未创建的对象。

再详细些:

public class Holder{
 private int n;
public Holder(int n){this.n = n}
public void assertSanity(){
   if(n!=n){
    throw new AssertionError("This statement is false.");
}
}
}

  除了创建hoder的线程,其他线程获取holder的状态是不一定的。也许是创建前获得为null,也许是创建了还没赋值,等等。这是不安全不正确的发布。

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

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

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

  • 不可变对象可以通过任意机制来发布。
  • 事实不可变对象(对象从技术上来看是可变的,但其状态在发布后不会再改变)必须通过安全方式来发布
  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。

2.9安全地共享对象

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

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

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

 

 

 

 

posted @ 2016-03-03 18:07  Ryan.Miao  阅读(1077)  评论(0编辑  收藏  举报