在多线程环境中安全的共享对象
来源:互联网 发布: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) 不可变对象的共享
· 不可变类可以安全的在多线程坏境下共享
- 在多线程环境中安全的共享对象
- 多线程中共享对象,创建线程安全类
- 解决htmlunit的webclient对象在多线程环境下的共享问题
- 解决htmlunit的webclient对象在多线程环境下的共享问题
- 多线程中共享对象,创建线程安全类(线程安全又控制不好咧~~~)
- 在非线程安全的数据库中使用多线程
- 多线程环境中安全使用集合API
- 多线程环境中安全使用集合API
- 在 Linux 中处理来自共享对象的同步事件
- 多线程系列提高(3)--对象的共享
- java多线程(三) 之 对象的共享
- 【多线程】共享对象和数据的解决方案
- 在VS2010中使用pthread多线程库的环境搭建
- 在Flex中使用本地共享对象
- 在 Linux 中使用共享对象
- 在 Linux 中使用共享对象
- springsecurity3 登录后在extjs中使用安全实体的信息,不同页面共享数据
- 当析构函数遇到多线程──C++ 中线程安全的对象回调
- 在同一个页面使用多个不同的jQuery版本,让它们并存而不冲突
- 如何在不同编程语言中获取现在的Unix时间戳(Unix timestamp)?
- No module named yum 错误
- 康托展开和康托展开的逆运算
- 解析oracle的rownum
- 在多线程环境中安全的共享对象
- jsp基础
- 修改注册表权限
- 字符串转换知识
- iPhone开源项目大全
- 一篇应该天天看的文章
- 我的安卓学习之路--Java多线程--线程简介
- @autoreleasepool in Loop 和 Loop in @autoreleasepool
- char,wchar_t,TCHAR 3者的区别与联系