第3章 对象的共享

可见性

eg.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;
    }
}
  • 1.main线程将number设为42,read设为true
  • 2.ReaderThread循环判断 ready,输出number的值
  • 3.但是NoVisibility看起来会输出42,但事实上可能是0,也可能无法终止(这种现象称之为"重排序")

eg.2 数据失效

@NotThreadSafe
public class MutableInteger {
    private int value;

    public int get() {
        return value;
    }

    public void set(int value) {
        this.value = value;
    }
}
  • A、B两个线程,A set时 B同时在get,此时B可能读取到的值是更新前的也可能是更新后的;为此我们进行加锁改造

eg.2.1 蜜汁加锁

public class SynchronizedInteger {
    @GuardedBy("this") private int value;

    public synchronized int get() {
        return value;
    }

    public synchronized void set(int value) {
        this.value = value;
    }
}
  • 调用get仍然有可能会看见失效值
  • 因为set get操作可能不是在同一个线程

Volatile变量

  • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

基于volatile变量的特性,通常只需要对写入操作进行适当的加锁来控制并发

发布与逃逸

发布: 表示对象能够在当前作用域之外的代码中使用

  • eg.3 错误的发布
class UnsafeStates {
    private String[] states = new String[]{
        "AK", "AL" /*...*/
    };

    public String[] getStates() {
        return states;
    }
}

任何调用者都能修改数组的内容,states对象已经逸出了他所在的作用域

  • eg.4 this引用逃逸
public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        });
    }
    void doSomething(Event e) {
    }
    interface EventSource {
        void registerListener(EventListener e);
    }
    interface EventListener {
        void onEvent(Event e);
    }
    interface Event {
    }
}

当我们new ThisEscape对象的时候,该线程会持有doSomething的this引用,此时ThisEscape还没有实例完成(没有返回引用),导致this引用逸出

  • eg.5 工厂方法防止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;
    }
    void doSomething(Event e) {
    }
    interface EventSource {
        void registerListener(EventListener e);
    }
    interface EventListener {
        void onEvent(Event e);
    }
    interface Event {
    }
}

线程封闭

当访问共享的可变数据时,通常需要使用同步.一种避免使用同步方式就是不共享数据.如果再单线程内访问数据,就不需要同步,这种技术被称之为线程封闭

Ad-hoc线程封闭(脆弱)

指维护线程封闭性的职责完全有程序实现来承担

栈封闭

只能通过局部变量才能访问对象
eg.7

public class Animals {
    Ark ark;
    Species species;
    Gender gender;

    public int loadTheArk(Collection candidates) {
        SortedSet animals;
        int numPairs = 0;
        Animal candidate = null;

        // animals confined to method, don't let them escape!
        animals = new TreeSet(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;
    }


    class Animal {
        Species species;
        Gender gender;

        public boolean isPotentialMate(Animal other) {
            return species == other.species && gender != other.gender;
        }
    }

    enum Species {
        AARDVARK, BENGAL_TIGER, CARIBOU, DINGO, ELEPHANT, FROG, GNU, HYENA,
        IGUANA, JAGUAR, KIWI, LEOPARD, MASTADON, NEWT, OCTOPUS,
        PIRANHA, QUETZAL, RHINOCEROS, SALAMANDER, THREE_TOED_SLOTH,
        UNICORN, VIPER, WEREWOLF, XANTHUS_HUMMINBIRD, YAK, ZEBRA
    }

    enum Gender {
        MALE, FEMALE
    }

    class AnimalPair {
        private final Animal one, two;

        public AnimalPair(Animal one, Animal two) {
            this.one = one;
            this.two = two;
        }
    }

    class SpeciesGenderComparator implements Comparator {
        public int compare(Animal one, Animal two) {
            int speciesCompare = one.species.compareTo(two.species);
            return (speciesCompare != 0)
                    ? speciesCompare
                    : one.gender.compareTo(two.gender);
        }
    }

    class Ark {
        private final Set loadedAnimals = new HashSet();

        public void load(AnimalPair pair) {
            loadedAnimals.add(pair);
        }
    }
}

new TreeSet引用保存在animals中,此时只有一个引用指向animals,该引用封闭在局部变量中,因此被封闭在执行线程中。如果发布了对animals的引用,就会造成逃逸。

ThreadLocal类

提供get和set方法,每个使用该变量的线程都存一份独立的副本,get总是返回当前执行线程在调用set时设置的最新值

  • ThreadLocal对象通常用于防止对可变的单实例变量或全局变量进行共享。
    假设你需要将一个单线程应用程序移植到多线程环境中,通过将共享的全局变量转换为ThreadLocal对象(如果全局变量的语义允许),可以维持线程安全性。

  • ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。

不变性

  • 1、对象创建完之后其状态就不能修改
  • 2、对象的所有与都是 final 类型
  • 3、对象时正确创建的(创建期间没有 this 的逸出)

安全发布

安全地发布一个对象,对象的应用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:

  • 1、在静态初始化函数中初始化一个对象引用
  • 2、将对象的应用保存到volatile类型的域或者AtomicReferance对象中
  • 3、将对象的引用保存到某个正确构造对象的final类型域中
  • 4、将对象的引用保存到一个由锁保护的域中。

在线程安全容器内部的同步意味着,在将对象放入到某个容器,例如Vector或synchronizedList时,将满足上述最后一条需求。如果线程A将对象X放入一个线程安全的容器,随后线程B读取这个对象,那么可以确保B看到A设置的X状态,即便在这段读/写X的应用程序代码中没有包含显式的同步。尽管Javadoc在这个主题上没有给出很清晰的说明,但线程安全库中的容器类提供了以下的安全发布保证:

  • 1、通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)
  • 2、通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程
  • 3、通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。

类库中的其他数据传递机制(例如Future和Exchanger)同样能实现安全发布。

通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器:

public static Holder holder = new Holder(42);

静态初始化器由JVM在类的初始化阶段执行。由于在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布[JLS 12.4.2]。

可变对象

对象的发布需求取决于它的可变性:

  • 1、不可变对象可以通过任意机制来发布
  • 2、事实不可改变必须通过安全方式发布
  • 3、可变对象必须通过安全方式发布,并且必须是线程安全的或者由某个锁保护起来

小结

在并发程序中使用和共享对象时,遵循如下实用策略

  • 1.线程封闭 对象封闭在线程中,只能由该线程修改
  • 2.只读共享 多线程并发只读对象,不能修改
  • 3.线程安全共享 线程安全的对象在其内部实现同步,多线程可以通过对象公有接口进行访问而不是进一步同步
  • 4.保护对象 被保护的对象只能通过持有特地的锁来访问

你可能感兴趣的:(Java,Java并发编程实战)