并发编程_03 对象的共享

3.1  可见性

当一个线程修改了对象状态后,其他线程能够看到状态的变化。

为了确保多个线程之间对写入操作的内存可见性,必须使用同步机制。

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; 
    }
}

在上面代码中,主线程和读线程都将访问共享变量ready和number,主线程启动读线程,然后将number设为42,并将ready设为true。读线程一直循环指导发现ready变为true,然后输出number的值。虽然NoVisibility看起来会输出42,但事实上很可能输出0,或者根本无法终止。这是因为在代码中没有使用足够的同步机制,因此无法保证主线程写入的ready值和number值对于读线程来说是可见的。

3.1.1  失效数据

//非线程安全的可变整数类
@NotThreadSafe
public class MutableInteger{
    private int value;
    
    public int get(){
        return value;
    }
    public void set(int value){
        this.value = value;
    }
}

上面的程序不是线程安全的,因为get和set都是在没有同步的情况下访问value的。与其他问题相比,失效值问题更容易出现:如果某个线程调用了set,那么另一个正在调用get的线程可能会看到更新后的value值,也可能看不到。

//线程安全的可变整数类

@ThreadSafe
public class SynchronizedInteger{
    @GuardBy("this") private int value;
    public synchronized int get(){
        return value;
    }
    public synchronized void set(int value){
        this.value = value;
    }
}

上面的程序中,通过对get和set等方法进行同步可以使其成为一个线程安全的类。仅对set方法进行同步是不够的,调用get的线程仍然会看到失效值。

3.1.2  非原子的64位操作

当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性。

最低安全性适用于大多数变量,但存在一个意外:非volatile类型的64位数值变量(double和long)。

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

3.1.3  加锁与可见性

内置锁用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。

3.1.4  volatile变量

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译与运行时都会注意到这个变量时共享的。因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

  当且仅党满足以下所有条件时,才应该使用volatile变量:

1.对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。

2.该变量不会与其他变量一起纳入到不变性条件中。

3.在访问变量时不需要加锁。

3.2  发布与逸出

发布一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。例如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其它类的方法中。

当某个不应该发布的对象被发布时吗,这种情况被称为逸出。

//发布一个对象

public static Set<Secret> knownSecrets;

public void initialize(){
    knownSecrets = new HashSet<Secret>();
}

不要在构造过程中使this引用逸出。如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法,从而避免不正确的构造过程。

//使用工厂方法来防止this引用在构造过程中逸出

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

猜你喜欢

转载自blog.csdn.net/strawqqhat/article/details/92008969