java并发编程实践学习(2)共享对象

一.可见性

当对象在从工作内存同步到主内存之前,那么它就是不可见的。若有其他线程在存取不可见对象就会引发可见性问题,看下面一个例子:

public class NoVisibility {  
private static boolean ready;  
private static int number;  
private static class ReaderThread extends Thread {  
    public void run() {  
        while (!ready)  
            Thread.yield();  
        System.out.println(number);  
    }  
}  
public static void main(String[] args) {  
    new ReaderThread().start();  
    number = 42;  
    ready = true;  
}  

}

按照正常逻辑,应该会输出42,但其实际结果会非常奇怪,可能会永远没有输出(因为ready为false)一直循环,可能会输出0(因为重排序问题导致ready=true先执行)。
关于重排序:Java存储模型允许编译器重排序操作,在寄存器中缓存数值;还允许cpu重排序,并在处理器特有的缓存中缓存数值。跟多细节在后面java存储模型中讲

(1) 过期数据

上面的例子就演示了过期数据,当读线程检查ready变量时,可能看到一个过期的值,而且跟坏的情况是:一个线程获取到了更新后的值,但是另一个线程获取到的是过期的值。
过期的数据可能导致严重的混乱错误,比如以外的异常,脏数据结构,错误的计算和无线循环。
非线程安全的可变整数访问器

@NotThreadSafe
public class MutableInteger{
private int value;

public int get(){
    return value;
}
public void set (int value) {
    this.value = value;
}

}
线程安全的可变整数访问器

@NotThreadSafe
public class MutableInteger{
@GuardedBy private int value;

public synchronized int get(){
    return value;
}
public synchronized void set (int value) {
    this.value = value;
}

}
(2) 非原子的64位操作

java存储模型要求获取和存储操作都是原子的,但在对没有声明为volatile的64位数值变量(double和long),jvm允许将64位的读或写操作划分为俩个32位的操作,这种情况下在多线程中获取数据你可能获取到一个变量值不同时的高32位和一个低32位。
因此,在多线程中共享的long和double变量也是不安全的,你需要用锁保护起来或者声明为volatile类型。

(3) 锁和可见性

为了保证所有线程都能看到共享的,可变的最新值,读取和写入必须使用公共的锁进行同步。
这里写图片描述
在上面例子中,线程A在同步块中或者之前所做的操作都是对b可见的,但是没有同步块的话就没有这样的保证。

(4) Volatile变量

java提供了一种同步的弱形式:Volatile变量。
当一个域声明为Volatile类型之后,编译器会监视这个变量:它是共享的,而且对它的操作不会与其他的内存操作仪器被重排序。Volatile变量不会缓存到寄存器或者在对于其他处理器隐藏的地方,所以读Volatile类型的变量时,总会返回由一个线程所写入的最新值。
Volatile比SynchronizedInteger更加轻量级。
加锁能保证可见性和原子性;volatile变量只能保证变量可见性。
只有满足下面标准才能使用volatile变量

写入变量时并不依赖变量的当前值;或者能够确保只有单一的线程修改变量的值
变量不需要与其他的状态变量共同参与不变约束
而且,访问变量时,没有其他的原因需要加锁
二. 发布和逸出

发布一个对象的意思是使它能够被当前范围之外的代码使用。
如果发布对象时他还没有完成构造,就会威胁到多线程环境下的安全。
一个对象在其尚未准备好时就将他发布,这种情况称作逸出。
发布对象

public static Set knownSecrets;

public void initalize(){
knownSecrets = new HashSet();
}

允许内部可变数据的逸出

class UnsafeStates{
private String[] states = new String[]{
“ak”,”al”,……
};
public String[] getStates(){
return states;
}
}

隐式的允许this引用逸出

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

(1) 安全建构建的实践

对象只有通过构造函数返回后,才处于可预言的、稳定的状态,所以从构造函数内部发布的对象,只是一个未完成构造的对象。
如果this引用在构造过程中逸出,这样的对象被认为“没有正确构建的”。
一个导致this引用在构造期间逸出的常见错误,实在构造函数中启动一个线程。当对象在构造函数中创建了一个线程时,无论是显示的(通过将它传给构造函数)还是隐式的(因为Thread或Runnable是所属对象的内部类),this引用几乎是被新线程共享。
在构造函数中创建线程并没有错误,但是最好不要立即启动它。取而代之的是发布一个start或initialize方法来启动对象拥有的线程 。
如果想在构造函数中注册监听器或者启动线程,可以使用一个私有的构造函数和一个公共的工厂方法,这样避免了不正确的构建。

public class SafeListener{
private final EventListener listener;

private SafeListener(){
    listener = new EventListener(){
         public void onEvent(){
             doSomething(e);
         }
    }
};
public static SafeListener newInstance(EventSource source){
    SafeListener safe = new SafeListener();
    source.registerListener(safe.listener);
    return safe;
}

}

三.线程封闭

访问共享的,可变的数据要求使用同步。一个可以避免同步的方式就是不共享数据。线程封闭技术是实现线程安全的最简单的方式之一。
常用的是swing的线程封闭技术和应用池化的JDBC Connection对象。
线程封闭是指运行在不同事件线程的其他线程中的代码不应该访问这些对象。
java自身和其核心库提供了某些机制(本地变量和ThreadLocal类)有助于维护线程限制,但程序员自己也要负责确保。

(1)Ad-hoc线程限制

Ad-hoc线程限制指维护线程封闭性的职责完全由程序实现来承担。
Ad-hoc线程封闭是非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。
当决定使用线程封闭技术时,通常是因为要将某个特定的子系统实现为一个单线程子系统。

(2)栈限制

在栈限制中,只能通过本地变量才能触及对象。本地变量被限制到执行线程中,其他线程无法访问这个栈。
栈限制比ad-hoc线程更易维护,更健壮。

public int loadTheArk(Collection candidates) {
SortedSet 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;  

}
(3)ThreadLocal

一种维护线程限制的更加规范的方式是使用ThreadLocal.
ThreadLocal提供了get和set访问器,为每个使用它的线程维护一份单独的拷贝,所以get总是返回当前执行线程通过set设置的最新值。而且ThreadLocal变量有自己的初始化方法。 当某个线程初次调用ThreadLocal.get方法时,就会调用initialValue来获取初始值。
使用ThreadLocal确保线程封闭性

private static ThreadLocal<Connection> connectionHoder = new ThreadLocal<Connection>(){
    public Connection initialValue(){
        return DriverManager.getConnection(DB_URL);
    } 
};
public static Connection getConnection(){
    return connectionHolder.get();
}

上面的例子通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。
这项技术还用于频繁执行的操作既需要buffer这样的零时对象,同时还需要避免每次都冲分配该零时对象。

四.不可变性

只有满足如下状态一个对象才是不可变的

它的状态不能再创建之后在修改
所有的域都是final类型
他被 正确的创建(在创建期间没有发生this引用的逸出)
(1) final域

final域是不能修改的(尽管如果final域指向的对象是可变的,这个对象任然可以修改)
将所有域设置为私有的,除非他们需要更高的可见性
将所有域设置为final,除非他们是可变的

(2) 使用volatile发布不可变对象

五.安全发布

我们让对象限制在线程中或者另一个对象内部,我们又的确希望跨线程共享对象,这时我们必须安全共享它。
————————-安全发布对象的条件:—————————-
1、通过静态初始化对象的引用;
2、将引用存储到volatile变量或AutomaticReference;
3、将引用存储到final域字段中;
4、将引用存储到由锁正确保护的域中;
Java中支持线程安全保证的类:
1、置入HashTable、synchronizedMap、ConcurrentMap中的主键以及值,会安全地发布到可以从Map获取他们的任意线程中,无论是直接还是通过迭代器(Iterator)获得。
2、置入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中的元素,会安全地发布到可以从容器中获取它的任意线程中。
3、置入BlockingQueue或ConcurrentLinkedQueue的元素,会安全地发布到可以从容器中获取它的任意线程中。
发布对象必要条件依赖于对象的可变性:
1、不可变对象可以依赖任何机制发布;
2、高效不可变对象必须安全地发布;
3、可变对象必须要安全发布,同时必须要线程安全或被锁保护。
安全的共享对象
线程限制:一个线程限制的对象,通过限制在线程中,而被线程独占,且只能被占有它的线程修改。
共享只读(read-only):一个共享的只读对象,在没有额外同步的情况下,可以被多个线程并发地访问,但任何线程都不能修改它。共享只读对象包括可变对象与高效不可变对象。
共享线程安全(thread-safe)————-
一个线程安全的对象在内部进行同步,所以其它线程无额外同步,就可以通过公共接口随意地访问它。
被守护(Guarded):
一个被守护的对象只能通过特定的锁来访问。被守护的对象包括哪些被线程安全对象封装的对象,和已知特定的锁保护起来的已发布对象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章