zoukankan      html  css  js  c++  java
  • 《Java并发编程实践》(三)---- 组合对象

    一,设计一个线程安全的类

    一个线程安全的类的设计需要包括三个基本要素:

    • 组成对象状态的属性
    • 限制状态属性的不变性
    • 并发访问对象状态的管理策略

    同步策略规定了如何将不变性/线程封闭/加锁机制等结合起来以维护线程的安全性,并且规定了哪些变量由哪些锁来保护。

    1,收集同步需求

    要确保类的线程安全性,就需要确保它的不变性条件不会再并发访问时被破坏,这就需要对其状态进行推断。

    在许多类中都定义了一些不可变条件,用于判断状态是有效的还是无效的。同样,在操作中还包含一些后验条件来判断状态转换是否有效。当下一个状态需要依赖当前状态时,这个操作就必须是一个复合操作。

    如果不了解对象的不变性与后验条件,那么就不能确保线程安全性,要满足状态变量的有效值或状态转换上的各种约束条件,就需要借助于原子性与封装性。

    2,State-dependent Operations

    类的不变性已经方法post-conditon限制了对象的有效状态已经状态转换的有效性。有些对象包含一些基于状态的先验条件,例如,不能从空队列中移除一个元素。如果在操作中包含基于状态的先验条件,那么这个操作就叫做state-dependent操作。

    3,状态所有权(state Ownership)

    多数情况下,所有权与封装性是相互关联的:对象封装它拥有的状态,也对它封装的状态拥有所有权。状态变量的所有者将决定采用何种加锁协议来维持变量状态的完整性。所有权意味着控制权。如果发布了某个可变对象的引用,那么原来的所有者就不再独占控制权了,就变成共享控制权了。

    二,实例封闭

    封装简化了线程安全类的实现过程,它提供了一种实例封装机制,也简称为封闭。江数据封装在对象内部,就可以江数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。

    被封闭对象一定不能超出它们既定的作用域中。

    例如:

    package com.ivy.thread;
    
    import java.util.HashSet;
    import java.util.Set;
    
    import com.ivy.io.Person;
    
    @ThreadSafe
    public class PersonSet {
    
        @GuardedBy("this")
        private final Set<Person> mySet = new HashSet<>();
        
        public synchronized void addPerson(Person p) {
            mySet.add(p);
        }
        
        public synchronized boolean containsPerson(Person p) {
            return mySet.contains(p);
        }
    }

    PersonSet类说明了如何通过将mySet封闭在一个类属性中以及使用加锁机制使一个类成为线程安全的。PersonSet的状态由HashSet来管理,而HashSet不是线程安全的,但由于mySet是私有的并且不会逸出,因此HashSet被封闭在PersonSet中。唯一能访问mySet的代码路径是addPerson和containsPerson两个方法,在执行它们时都要获得PersonSet的内置锁,所以PersonSet的状态完全由它的内置锁保护,因而PersonSet是一个线程安全的类。

    封闭机制更易于构造线程安全的类,因为在分析线程安全性时可以只分析该类而不用检查整个程序。

    1,Java监视器模式

    Java的内置锁也成为监视器锁或监视器。所以使用内置锁来保证线程安全性的模式就叫做Java监视器模式。遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。

    三,Delegating Thread Safety(线程安全性的委托)

    在CountingFactorizer类中,我们在无状态的类中增加了一个AtomicLong类型的域,并且得到的组合对象仍然是线程安全的。由于CountingFactorizer的状态就是AtomicLong的状态,而AtomicLong是线程安全的,所以CountingFactorizer也是线程安全的,我们就可以说CountingFactorizer将它的线程安全性委托给AtomicLong来保证:之所以CountingFactorizer是线程安全的,是因为AtomicLong是线程安全的。

    还可以将线程安全性委托给多个状态变量,只要这些变量是彼此独立的,即组合而成的类并不会在其包含的多个状态变量上增加任何不变性条件。

    如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给这些状态变量。

    如果一个状态变量是线程安全的,也不参与任何不变性条件,也没有操作上的状态变换,那这个变量就可以发布出去。

    四,为线程安全类添加功能

    要添加一个新的原子操作,最安全的方法是修改原始类,但这通常无法做到,因为可能无法访问或修改类的源代码。如果直接将新方法添加到类中,那么意味着实现同步策略的所有代码仍然处于一个源文件中,从而更容易维护。

    另一种方法是用子类扩展这个类,但这样的话同步策略的实现就分布在了多个需要单独维护的源文件中,如果父类修改了同步策略选择不同的锁来保护它的状态变量,那子类也需要跟着变。

    1,客户端加锁机制

    第三种策略是扩展类的功能,但并不是扩展类本身,而是将扩展方法放在一个辅助类(Helper class)中。

    如下例,实现了一个若没有则添加的辅助类,用于对线程安全的List执行操作,但是这段代码并不是线程安全的:

    @NotThreadSafe
    public class ListHelper<E> {
        public List<E> list = Collections.synchronizedList(new ArrayList<E>());
        ...
        public synchronized boolean putIfAbsent(E x) {
            boolean absent = !list.contains(x);
            if(absent) {
                list.add(x);
            }
            return absent;
        }
    }

    putIfAbsent用的是ListHelper的内置锁,但list用的肯定不是ListHelper的锁,尽管所有的list操作都被声明为synchronized,但却是不一样的锁,这就无法确保当putIfAbsent执行时另一个线程不会修改这个list。

    要想使这个方法正确执行,必须使list在实现客户端加锁或外部加锁时使用同一个锁。客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户端代码。要使用客户端加锁,就必须知道对象X使用的是哪一个锁

    在Vector和同步封装器类的文档中指出,它们通过使用Vector或封装器容器的内置锁来支持客户端加锁。所以修改后的putIfAbsent如下: 

    @ThreadSafe
    public class ListHelper<E> {
        public List<E> list = Collections.synchronizedList(new ArrayList<E>());
        ...
        public  boolean putIfAbsent(E x) {
            synchronized(list) {
                boolean absent = !list.contains(x);
                if(absent) {
                    list.add(x);
                }
                return absent;
            }
        }
    }

    使用客户端加锁更脆弱,因为它将类C的加锁代码放到了与C完全无关的其他类中。客户端加锁机制与扩展类机制有许多共同点,二者都是将派生类的行为与基类的实现耦合在一起,正如扩展会破坏实现的封装性一样,客户端加锁同样会破坏同步策略的封装性。

    2,组合

    另一种更好地为现有类添加原子操作的方法是:组合。

    如下例: 

    @ThreadSafe
    public class ImprovedList<T> implements List<T> {
        private final list<T> list;
        public ImprovedList(List<T> list) {
            this.list = list;
        }
        public synchronized boolean putIfAbsent(T x) {
            boolean contains = list.contains(x);
            if(!contains) {
                list.add(x);
            }
            return !contains;
       }
    
       public synchronized void clear() {
           list.clear();
       }
    }

    ImprovedList通过自身的内置锁增加了一层额外的加锁。它并不关心List是否是线程安全的,即使List不是线程安全的或者修改了它的加锁实现,ImprovedList也会提供一致的加锁机制来实现线程安全性。事实上,我们使用了Java监视器模式来封装现有的List,并且只要在类中拥有指向底层List的唯一外部引用,就能确保线程安全性。

  • 相关阅读:
    浅谈聚类算法(K-means)
    多步法求解微分方程数值解
    本学期微分方程数值解课程总结(matlab代码)
    Stone Game
    Two Sum IV
    Insert into a Binary Search Tree
    Subtree of Another Tree
    Leaf-Similar Trees
    Diameter of Binary Tree
    Counting Bits
  • 原文地址:https://www.cnblogs.com/IvySue/p/6860165.html
Copyright © 2011-2022 走看看