在多线程环境中安全的共享对象

来源:互联网 发布:java生成csv文件 编辑:程序博客网 时间:2024/06/04 22:12

1.      可见性

1.1      多线程环境中共享变量的可见性问题

(1)    单线程环境下,对一个共享变量的修改很自然的是有序的

在t1时刻,修改了一个变量的值

那么在t2时候,一定会读到这个变量的最新的值,不会说读到过期的数据。

(2)    多线程环境下,对一个线程中对一个共享变量的修改,对其他线程来说不一定是可见的。

public class NoVisibility {

    private staticboolean ready;

    private staticint number;

 

    private staticclass ReaderThread extends Thread {

        public voidrun() {

            while(!ready)

               Thread.yield();

           System.out.println(number);

        }

    }

 

    public staticvoid main(String[] args) {

        newReaderThread().start();

        number =42;

        ready =true;

    }

}

在主线程中对ready设置true,不一定在ReaderThread中一定是马上可见的。所以ReaderThread线程中loop会一直执行。以下几个因素可能或导致这种情况的发生。

· 因为有缓存存在,主线程对ready设置true,,不一定会store到主内存中去,所以ReaderThread线程还是读到的过期数据

· 主线程对ready的修改保存到主内存中去了,但是ReaderThread线程因为其缓存了过期值,导致读到的还是false

· 主线程中源代码在编译成机器指令的时候,可能reorder了机器指令,ready=true先执行,然后是number=42。所以ReaderThread可能直接打印number=0;

· 线程之间的执行是乱序的,并不能保证主线程一定先执行,然后才是ReaderThread线程,而ready又是对ReaderThread线程是不可见的,所以ReaderThread线程会一直执行。

可见,在缺乏适当同步的情况下,去分析机器是如何执行是很困难的。

 

单线程环境下,编译器、cpu、缓存可能都会优化代码的执行;前提条件只要不影响最终结果就行了。

而在多线程环境下,如果缺乏同步,那么正是这些优化使得最后很难预测代码是如何执行的。

(3)过期数据

@NotThreadSafe
public class MutableInteger {
    private int value;
 
    public int  get() { return value; }
    public void set(int value) { this.value = value; }
}
缺乏适当同步的情况下,会导致在线程中读到的数据是过期数据。
代码分析:
·  原来value值是0
·  Thread B调用set方法把value值改为2
·  Thread A调用get方法读value的值
这时候Thread A读到的值可能是0,也可能是2。这样是Thread A是不是先与Thread B执行;Thread B对value值的修改是不是store到主内存中去了;Thread A是不是读的是工作内存中的初始值,还是Thread B修改后的值。这些情况多会发生。
 
1.2  用lock来保证可见性。
(1)Lock的性质
·  原子性,由锁保护的代码块中所有操作可以视作是一个不可分割的unit
·  线程执行的有序性,由锁保护的代码块某个时间段只能由一个线程执行,线程执行完释放lock,其他线程才能获取lock执行由这个lock保护的代码块。
·  可见性,线程开始执行的时候要获取lock并且强制load主内存中的共享变量的最新值;线程释放锁的时候,必须要把工作内存中对共享变量的修改刷新到主内存中去。
 
(2)Lock在多线程中的作用
在多线程中,不管在哪里对共享变量的读和写,都需要由同一个锁来保护。
 
1.3  volatile 变量
(1)volatile变量的性质
·  只能保证可见性
·  不能保证原子性
意味着复合操作需要用lock
·  不能保证有序性
 
(2)Volatile 与happens-before
·  对volatile变量的写一定是发生在读之间。
·  新的JMM强化了volatile变量的语意
对非volatile变量的写,如果发生在对某个volatile变量的写之前;那么随后在其他线程中读volatile变量,也能读到非volatile变量的最新值。所以volatile变量可以这么用:

MapconfigOptions;

char[]configText;

volatileboolean initialized = false;

以上是一些共享变量

      

// In Thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
 
// In Thread B
while (!initialized) 
  sleep();
// use configOptions 
线程A中对configOptions初始化后,对于线程B是可见的。
(3)  Volatile变量的使用场景
·   写一个volatile变量的值,不会依赖于当前值
·   Volatile变量不会参与对象的约束
 

2.      对象的发布和逸出

2.1      发布一个对象的引用

(1)    发布对象应用意味着,把一个对象从当前的scope发布到其他地方,比如:

·  把它保存到hashMap这个数据结构中去,以便以后的代码能迭代hashMap访问到hashMap中对对象

Public static Map<Secret> maps;

Public void init() {

         Maps= new HashMap<Secret>();

}

这样其他方法中能拿到这个maps,然后迭代它,拿到每个Secret对象做操作。

·  通过一个public 的方法把一个原本私有的域发布出去。

Private String[] names = {“hua”,”zhang”,”liu”};

 

Public String[] getNames() {

         Returnnames;

}

·  在构造器中,把this逸出

public class ThisEscape {

    publicThisEscape(EventSource source) {

       source.registerListener(

            newEventListener() {

               public void onEvent(Event e) {

                   doSomething(e);

                }

            });

    }

}

(2)    带来的问题。

·  无法预测对象的状态空间,不安全的发布了对象的引用。意味了代码对于对象的状态失去了控制;只要对象被不安全的发布出去,那么在任何线程中都可能会修改对象的内部状态。如果一个对象状态只有满足某种约束才是合理的话,那么不安全的发布对象可能会破坏对象的内在约束。比如:

Public classNumberRange {

         Private int lower;

         Private int upper;

 

         Public NumberRange(int lower,int upper){

                   If(low > upper)

throw new IllegalArgumentException("lower: " +lower +" > " +"upper: "+upper);

                   This.lower = lower;

                   This.upper = upper;

}

 

Public void setLower(int lower) {

         This.lower = lower;

}

 

Public void setUpper(int upper){

         This.upper = upper;

}

}

如果NumberRange初始化完成后是(0,5),然后这个对象被不安全的发布了,那么其他线程就可能调用setLower,setUpper使得这个对象违反对象的约束。

 

2.2      如何正确的发布一个对象的引用

2.2.1         多使用线程安全的不变类

·  不变类的对象的域,一旦调用构造器完成初始化工作后,其状态就能在整个生命周期内保持不变。也就是不变类的状态空间只有一个。所以,不变类的对象可以在线程中安全的共享。

·  另外不变类的对象可以很好的作为散列存储结构的键值。因为不变类的状态只有一个,所以其hashcode也就只有一个值,在不变类对象的整个生命周期中都不可以改变。所以可以很好的作为key值。(不会破坏HashMap的内在约束)。

 

2.2.2         对于可变对象,用锁发布可变对象的引用

在多线程坏境下,去分析可变对象的状态空间比较复杂。因为只要不正确的发布了对象的引用,那么谁知道别人会怎么使用这个对象呢?所以,为了保护可变对象内部的约束,必须恰当使用lock。例如上面的NumberRange

Public classNumberRange {

         Private int lower;

         Private int upper;

 

         Public NumberRange(int lower,int upper){

                   If(low > upper)

throw new IllegalArgumentException("lower: " +lower +" > " +"upper: "+upper);

                   This.lower = lower;

                   This.upper = upper;

}

 

Public synchronized void setLower(int lower) {

         If(low > upper)

throw new IllegalArgumentException("lower: " +lower +" > " +"upper: "+upper);

 

         This.lower = lower;

}

 

Public synchronized void setUpper(int upper){

If(low > upper)

throw new IllegalArgumentException("lower: " +lower +" > " +"upper: "+upper);

         This.upper = upper;

}

}

                   这样,线程A调用setLower(4)时候看到的最新的NumberRange的范围例如(0,5),由于某个时间段只能有一个线程进入同步块,那么在线程A执行的时候,线程B不能调用setUpper(3)使得NumberRange的状态处于违反约束的情况下。(锁性质:排斥性,可见性)。

 

2.2.3         在构造对象的时候,不要发布对象的引用

具体看:http://www.ibm.com/developerworks/java/library/j-jtp0618/index.html

 

3.      线程限制

可以用线程限制来避免不安全的共享可变对象。可变对象如果想在多线程坏境中共享,必须使用lock。

如果对象只在一个线程中使用,就不需要用锁来保护对象。这样的一种方式称为线程限制;也就说,不能再线程中对外发布这个对象的引用。

实际上线程限制是一种用内存空间来换取线程安全性的方式,请看下面两种线程限制。

(1)    栈限制

把对可变对象的访问限制在单个限制中,并且不对外发布这个对象的引用,我们称为栈限制。

public int loadTheArk(Collection<Animal> candidates) {
    SortedSet<Animal> animals;
    int numPairs = 0;
    Animal candidate = null;
 
    // animals confined to method, don't let them escape!
    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;
}

Animals这个引用指向的对象只能在loadTheArk这个方法中访问到,并且animals引用不会对外发布,所以其他对象是不能拿到animals引用的。每个线程进入到这个方法中来的时候,都为得到一个新的animals对象;也就是说animals限制在线程的工作内存中是不对外共享的(主内存中没有animals)。

 

(2)    用ThreadLocal

为每一个使用该变量线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。

对于ThreadLocal的说明请看:

 

4.      不可变类和volatile

当需要对一组相关的变量做一个原子性操作的时候,考虑把这些相关变量封装在一个不可变类中。


 

@Immutable
class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;
 
    public OneValueCache(BigInteger i,
                         BigInteger[] factors) {
        lastNumber  = i;
        //对于可变域做保护性copy
        lastFactors = Arrays.copyOf(factors, factors.length);
    }
 
    public BigInteger[] getFactors(BigInteger i) {
        if (lastNumber == null || !lastNumber.equals(i))
            return null;
        else
            //可变域做保护性copy
            return Arrays.copyOf(lastFactors, lastFactors.length);
    }
}

 

使用不可变类构建的新类是线程安全的。

@ThreadSafe
//因为OneValueCache是不可变类,而不可变类是线程安全的
public class VolatileCachedFactorizer implements Servlet {
    private volatile OneValueCache cache =
        new OneValueCache(null, null);
 
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = cache.getFactors(i);
        if (factors == null) {
            factors = factor(i);
            cache = new OneValueCache(i, factors);
        }
        encodeIntoResponse(resp, factors);
    }
}

 

用volatile来保证对cache指向的不可变类的对象,一旦有变化,那么对于其他线程就是可见的。

 

5.      安全发布共享对象

(1)    可变对象的共享

一旦要共享,必须用锁来保证对象状态的一致性。

(2)    不可变对象的共享

·  不可变类可以安全的在多线程坏境下共享

 

原创粉丝点击