03-对象的共享

一、概述

  要编写正确的并发程序,关键问题在于:在访问共享的可变状态时需要进行正确的管理。
  我们已经知道了同步代码块和同步方法可以确保以原子的方式执行操作,但一种常见的误解是,认为关健字synchronized只能用于实现原子性或者确定“临界区(Critical Section)”。同步还有另一个重要的方面:内存可见性(Memory Visibility)。我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态化。如果没有同步,那么这种情况就无法实现。你可以通过显式的同步或者类库中内置的同步来保证对象被安全地发布。

二、可见性

  • 通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
  • 如下代码,Novisibility可能会持续循环下去,因为读线程可能永远看不到ready的值;另一种现象是NoVisiblity可能会输出0 ,因为读线程可能看到写入了ready值但没有看到写入的number值 ,这种现象被称为“重排序
  • 很幸运有一种方法可避免:只要有数据在多个线程之间共享就使用正确的同步
  • 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;
        }
    }

    2.1 失效数据

  •   上述代码展示了在缺乏同步的程序中可能产生错误结果的一种情况:失效数据
  •   当读线程查看ready变量时,可能得到一个已失效数据。除非在每次访问变量时都使用同步,否则很可能获得该变量的一个失效值。更糟糕的是失效值可能会同时出此:一个线程可能获得某个线程的最新值,而获得另一个变量的失效值。

  2.2 非原子的64位操作

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

    2.2.2 最低安全性适用于绝大多数变量。但是存在一个例外:非volatile类型的64位数值变量(double long) jvm将64位的读写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,很可能会读到某个值的高32位和另一个值的低32位。因此即使不考虑失效数据问题,在多线程中使用共享且可变的long和double等类型的变量也是不安全的,除非使用volatile关键字声明或者使用锁保护起来。

  2.3 加锁与可见性

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

  

 

 

 

 

  

  • 如上图,A看到的值在B获得锁后同样可以看到。因此我们可以理解为什么在访问某个共享且可变的变量要求所有线程在同一个锁上加锁
  • 加锁的含义不仅仅局限于互斥行为,还包括内存的可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。

  2.4 volatile变量

    声明成volatile时,编译器和运行时都会注意到这个变量时共享的,因此不会将该变量上的操作与其他操作一起重排序,不会被缓存在寄存器或对其它处理器不可见的地方。读volatile会返回最新写入的值。

    不会造成线程阻塞,是一种比Synchronized更轻量级的锁机制

    仅当volatile变量能够简化代码的实现以及对同步策略的验证时,才应该使用它们

    加锁机制既能保证原子性又能保证可见性,而volatile只能保证可见性   

当且仅当满足以下条件时,才使用volatile

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

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

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

  

 三、发布与逸出

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

  当某个不应该被发布的对象被发布时,就叫做逸出

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

public static Set<Secret> knownSecrets;

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

  3.2 当发布某个对象时,可能会间接地发布其他对象。

  3.3 当发布某个对象时,该对象的非私有域中的所有对象同样会被发布

  3.4 发布一个内部的类实例也可以发布对象或内部状态

    因为在这个内部类的实例中包含了对外部类的隐含引用。如下代码

// 隐式地使this引用逸出
public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListenner(
            new EventListener() {
                public void onEvent(Event e) {
                    doSomething(e);
                }
            }
        )
    }
}

  3.5 安全的对象构造过程

    不要在构造过程中使this引用逸出

    在构造过程中使this引用逸出的一个常见错误是,在构造函致中启动一个线程。当对象在其构造函数中创建一个线程时,无论是显式创建(通过将它传给构造函数)还是隐式创建(由于Thread或Runnable是该对象的一个内部类),this引用都会被新创建的线程共享。在对象尚未完全构造之前,新的线程就可以看见它。在构造函数中创建线程并没有错误,但最好不要立即启动它,而是通过一个start或initialize方法来启动(请参见第7章了解更多关于服务生命周期的内容)。在构造函数中调用一个可改写的实例方法时(既不是私有方法,也不是终结方法),同样会导致this引用在构造过程中逸出。

    如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法(Factory Method),从而避免不正确的构造过程,如程序清单3・8中SafeListener 所示 

// 使用工厂方法来防止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;
    }
}

四、线程封闭

  

 

   

posted @ 2021-10-20 14:46  微~粒  阅读(52)  评论(0)    收藏  举报