Java并发编程实战 对象的共享总结

可见性
在没有同步的情况下共享变量(不要这么做)

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

在没有同步的情况下 编译器 处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整 在缺乏足够同步的多线程程序中 要想对内存操作的执行顺序进行判断 几乎无法得出正确的结论

有一种简单的方法能避免这些复杂的问题 只要有数据在多个线程之间共享 就使用正确的同步

失效数据
非线程安全的可变整数类

@NotThreadSafe
public class MutableInteger {
    private int value;

    public int get() {
        return value;
    }

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

如果某个线程调用了set 那么另一个正在调用get的线程可能会看到更新后的value值 也可能看不到

线程安全的可变整数类

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

    public synchronized int get() {
        return value;
    }

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

仅对set方法进行同步是不够的 调用get的线程仍然会看见失效值 这里的锁是当前对象 调用get或set首先要获得锁

非原子的64位操作
当读取一个非volatile类型的long变量时 如果对该变量的读操作和写操作在不同的线程中执行 那么很可能会读取到某个值的高32位和另一个值的低32位

加锁与可见性
加锁的含义不仅仅局限于互斥行为 还包括内存可见性 为了确保所有线程都能看到共享变量的最新值 所有执行读操作或者写操作的线程都必须在同一个锁上同步

Volatile变量
仅当volatile变量能简化代码的实现以及对同步策略的验证时 才应该使用它们 如果在验证正确性时需要对可见性进行复杂的判断 那么就不要使用volatile变量 volatile变量的正确使用方式包括:确保它们自身状态的可见性 确保它们所引用对象的状态的可见性 以及标识一些重要的程序生命周期事件的发生(例如 初始化或关闭)

volatile变量的一种典型用法 检查某个状态标记以判断是否退出循环
数绵羊

public class CountingSheep {
    volatile boolean asleep;

    void tryToSleep() {
        while (!asleep)
            countSomeSheep();
    }

    void countSomeSheep() {
        // One, two, three...
    }
}

加锁机制既可以确保可见性又可以确保原子性 而volatile变量只能确保可见性

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

  • 对变量的写入操作不依赖变量的当前值 或者你能确保只有单个线程更新变量的值
  • 该变量不会与其他状态变量一起纳入不变性条件中
  • 在访问变量时不需要加锁

发布与逸出
发布(Publish) 一个对象的意思是指 使对象能够在当前作用域之外的代码中使用
当某个不应该发布的对象被发布时 这种情况就被称为逸出(Escape)

发布一个对象

class Secrets {
    public static Set<Secret> knownSecrets;

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


class Secret {
}

使内部的可变状态逸出(不要这么做)

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

    void doSomething(Event e) {
    }


    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
    }
}

安全的对象构造过程
不要在构造过程中使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;
    }

    void doSomething(Event e) {
    }


    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
    }
}

线程封闭
当访问共享的可变数据时 通常需要使用同步 一种避免使用同步的方式就是不共享数据 如果仅在单线程内访问数据 就不需要同步 这种技术被称为线程封闭(ThreadConfinement) 它是实现线程安全性的最简单方式之一

Ad-hoc线程封闭
Ad-hoc线程封闭是指 维护线程封闭性的职责完全由程序实现来承担 Ad-hoc线程封闭是非常脆弱的 因为没有任何一种语言特性 例如可见性修饰符或局部变量 能将对象封闭到目标线程上

由于Ad-hoc线程封闭技术的脆弱性 因此在程序中尽量少用它 在可能的情况下 应该使用更强的线程封闭技术(例如 栈封闭或ThreadLocal类)

栈封闭
栈封闭是线程封闭的一种特例 在栈封闭中 只能通过局部变量才能访问对象

基本类型的局部变量与引用变量的线程封闭性

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

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


    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<Animal> {
        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<AnimalPair> loadedAnimals = new HashSet<AnimalPair>();

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

ThreadLocal类
维持线程封闭性的一种更规范方法是使用ThreadLocal 这个类能使线程中的某个值与保存值的对象关联起来 ThreadLocal提供了get与set等访问接口或方法 这些方法为每个使用该变量的线程都存有一份独立的副本 因此get总是返回由当前执行线程在调用set时设置的最新值

使用ThreadLocal来维持线程封闭性

public class ConnectionDispenser {
    static String DB_URL = "jdbc:mysql://localhost/mydatabase";

    private ThreadLocal<Connection> connectionHolder
            = new ThreadLocal<Connection>() {
                public Connection initialValue() {
                    try {
                        return DriverManager.getConnection(DB_URL);
                    } catch (SQLException e) {
                        throw new RuntimeException("Unable to acquire Connection, e");
                    }
                };
            };

    public Connection getConnection() {
        return connectionHolder.get();
    }
}

不变性
不可变对象一定是线程安全的

当满足以下条件时 对象才是不可变的:

  • 对象创建以后其状态就不能修改
  • 对象的所有域都是final类型
  • 对象是正确创建的(在对象的创建期间 this引用没有逸出)

在可变对象基础上构建的不可变类

@Immutable
 public final class ThreeStooges {
    private final Set<String> stooges = new HashSet<String>();

    public ThreeStooges() {
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
    }

    public boolean isStooge(String name) {
        return stooges.contains(name);
    }

    public String getStoogeNames() {
        List<String> stooges = new Vector<String>();
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Curly");
        return stooges.toString();
    }
}

Final域
正如 除非需要更高的可见性 否则应将所有的域都声明为私有域是一个良好的编程习惯 除非需要某个域是可变的 否则应将其声明为final域 也是一个良好的编程习惯

示例:使用Volatile类型来发布不可变对象
对数值及其因数分解结果进行缓存的不可变容器类

@Immutable
public class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;

    public OneValueCache(BigInteger i,
                         BigInteger[] factors) {
        lastNumber = i;
        lastFactors = Arrays.copyOf(factors, factors.length);
    }

    public BigInteger[] getFactors(BigInteger i) {
        if (lastNumber == null || !lastNumber.equals(i))
            return null;
        else
            return Arrays.copyOf(lastFactors, lastFactors.length);
    }
}

使用指向不可变容器对象的volatile类型引用以缓存最新的结果

@ThreadSafe
public class VolatileCachedFactorizer extends GenericServlet 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);
    }

    void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
    }

    BigInteger extractFromRequest(ServletRequest req) {
        return new BigInteger("7");
    }

    BigInteger[] factor(BigInteger i) {
        // Doesn't really factor
        return new BigInteger[]{i};
    }
}

安全发布
在没有足够同步的情况下发布对象(不要这么做)

public class StuffIntoPublic {
    public Holder holder;

    public void initialize() {
        holder = new Holder(42);
    }
}

不正确的发布:正确的对象被破坏
由于未被正确发布 因此这个类可能出现故障

public class Holder {
    private int n;

    public Holder(int n) {
        this.n = n;
    }

    public void assertSanity() {
        if (n != n)
            throw new AssertionError("This statement is false.");
    }
}

不可变对象与初始化安全性
任何线程都可以在不需要额外同步的情况下安全地访问不可变对象 即使在发布这些对象时没有使用同步

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

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

事实不可变对象
如果对象从技术上来看是可变的 但其状态在发布后不会再改变 那么把这种对象称为 事实不可变对象
在没有额外的同步的情况下 任何线程都可以安全地使用被安全发布的事实不可变对象

可变对象
对于可变对象 不仅在发布对象时需要使用同步 而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性 要安全地共享可变对象 这些对象就必须被安全地发布 并且必须是线程安全的或者由某个锁保护起来
对象的发布需求取决于它的可变性:

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

安全地共享对象
在并发程序中使用和共享对象时 可以使用一些实用的策略 包括:
线程封闭 线程封闭的对象只能由一个线程拥有 对象被封闭在该线程中 并且只能由这个线程修改
只读共享 在没有额外同步的情况下 共享的只读对象可以由多个线程并发访问 但任何线程都不能修改它 共享的只读对象包括不可变对象和事实不可变对象
线程安全共享 线程安全的对象在其内部实现同步 因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步
保护对象 被保护的对象只能通过持有特定的锁来访问 保护对象包括封装在其他线程安全对象中的对象 以及已发布的并且由某个特定锁保护的对象

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值