• 03组合对象


    前面对线程安全与同步的基础知识已经具备基本的了解,但是不希望为了获得线程安全而去分析每次内存的访问,而希望线程的组件能够以安全的方式组合成更大的组件或程序。

    1.设计线程安全的类

    设计线程安全的过程应该包括下面3个基本要素:
    1.确定对象状态是由哪些变量构成
    2.确定限制状态变量的不变约束
    3.指定一个管理并发对象状态的策略
    对象的状态的从域说起,如果对象的域是基本类型(primitive),那么这些域就组成了对象的完成状态。如下:Counter只有一个value域,如果一个对象有n个基本域(primitive fields),它的状态就是域值组成的n元组(n-tuple).
    public final class Counter{
    private long value;
    public synchronized long getValue(){
    return value;
    }
     
    public synchronized long increment() {
    if(value == Long.MAX_VALUE){
    throw new IllegalStateException("counter overflow");
    }
    return ++value;
    }
    }
    同步策略定义了对象如何协调对其状态的访问,并且不会违反它的不变约束或后验条件。它规定了如何把不可变性,线程限制和锁结合起来,从而维护线程的安全性。还指明了哪些锁保护哪些便利。为了保证开发者与维护者可以分析并维护类,应该将类的同步策略写入文档。
    同步需求
    维护类的线程:确保在并发的时候,保护它的不变约束,即对其状态进行判断。对象与变量拥有一个状态空间(state space),即可能处于某种范围,状态空间越小,越容易判断它们。尽量使用final类型的域,可简化我们对对象的可能状态进行分析。
    不可变约束:很多类可通过不可变约束来判断一种状态是合法还是非法。如上面例子中的value,Lon类型范围Long.MIN_VALUE,Long.MAX_VALUE的范围,但是Counter约束了value的取值,不允许有负数。
    操作的后验条件会指出状态转换时非法的。如Counter当前值17,下一个唯一合法值18,如果下一个值的状态的确源自当前状态,那么这个操作必须是复合操作。不是所有操作都限制于状态转换约束,如温度就不受前一个值影响。
    状态依赖操作
    类的不变约束与方法的后验条件约束了对象合法的状态和合法状态转换。某些对象的方法也基于状态的先验条件(preconditions)。如无法再空队列中移除一个条目。若一个操作基于状态的先验条件,则称之为状态依赖。
    在单线程中,如果无法满足先验条件,则必然失败。但是在并发程序中,原本假的先验条件可能会处于其他线程的活动而变成真的,并发程序中有这种可能,持续等待,直到先验条件为真,再继续处理操作。
    在java中,等待特定条件成立的内置高效机制——wait与notify.与内部锁紧密地绑在一起。但是使用起来其实并不容易。当创建一个操作,让它在执行前必须等待到先验条件为真,不如使用现有类库来提供期望状态行为更容易。比如阻塞队列(blocking queue)或信号量(semaphore),以及其他同步工具(Synchronizer)
    状态所有权
    对象的状态实际为以该对象为根的所有域的一个子集。所有权与封装性总是出现在一起的,对象封装它拥有的状态,且拥有它封装的状态,拥有给定状态的所有者决定了锁协议。该协议用于维护变量的完整性。所有权意味着控制权,一旦你将引用发布到一个可变对象上,你就不再拥有独占的控制器。充其量只能有‘共享控制权’。类通常不会拥有由构造函数或方法传递进来的对象,除非该方法被明确设计用来转换传递对象的所有权(如同步容器的包装工厂方法)
     
    容器类通常表现出一种‘所有权分离’的形式,这是指容器拥有容器框架的状态。而客户代码拥有存储在容器中的对象的状态。如servlet框架中的ServletContext.它为Servlet提供了类似Map的对象容器服务,每个Servlet可以通过setAttribute与getAttribute在ServletContext中注册或重获应用程序对象。由于Servlet容器实现的ServletContext对象一定被多个线程访问,因此ServletContext必须是线程安全的。调用setAttribute与getAttribute不必同步,但是使用存储在ServletContext中的对象必须同步,这些对象属于应用程序。Servlet容器只是帮忙存储并替程序保管他们。正如所有共享对象那样,它们必须安全地共享,为防止多线程并发访问同一对象所带来的干扰,这些对象应该是线程安全对象高效不可变对象由锁明确保护的对象

    2.实例限制

    虽然一个对象可能不是线程安全的,但是任然可以使用许多技术让它安全地用于多线程。例如使用线程限制确保只被一个线程锁访问。或者确保所有的访问都被正确地被锁保护。
    通过使用实例限制(instance confinement),封装简化了类的线程安全工作。将数据封装在对象内部,把对数据的访问限制在对象的方法上,更容易确保线程在访问数据时总能获得正确的锁。
     
    被限制对象一定不能逸出到它的期望可用范围之外,可以吧对象限制在类实例(如私有的类成员),语汇范围(本地变量)或线程(比如对象在线程内部从一个方法到另一个方法,前提是该对象不能被跨线程调用)。对象不能自发地逸出自己。
     
    在下面的PersonSet中,师范了限制与锁如何协同确保一个类的线程安全性。即使它的组件状态变量不是线程安全的,非现场安全的HashSet管理者PersonSet类的状态,但是mySet是私有的,不会逸出,因此HashSet只被限制在PersonSet中,唯一可访问的是set与get方法,但是执行时还需要获得PersonSet的锁。因此内部锁保护了它的状态,确保了线程安全。
     
    public class PersonSet {
     
    private final Set<Person> mySet = new HashSet<>();
     
    public synchronized boolean getMySet(Person p) {
    return mySet.contaions(p);
    }
     
    public synchronized void setMySet(Person p) {
    myset.add(p);
    }
     
    }
    但是上面的例子如果Person是可变的,那么在PersonSet中获取Person时候,还需要额外的同步。为了安全使用Person,z则最可靠的方法就是让Person自身是线程安全的。对Person加锁并不可靠,因为还需要所有的用户遵守访问Person首先需要获得正确的锁的协议规则。
     
    平台类库中有很多线程限制的实例,包括有些类,它的存在就是把非线程安全类转化为线程安全的类,如ArrayList用于HashMap是非线程安全的,,但是类库提供了包装器工厂方法(Collections.synchronizedList及同族的方法),使这些非线程安全的类可以用于多线程中。这些工厂方法利用Decorator模式,使用一个同步的包装其对象包装容器,只要包装器对象占有着对下一层容器唯一的可触及的引用,包装器对象就是线程安全的
    Collections的synchronizedList方法:
     
    public static <T> List<T> synchronizedList(List<T> list)返回由指定列表支持的同步(线程安全的)列表。为了保证按顺序访问,必须通过返回的列表完成对底层列表的所有访问。
    在返回的列表上进行迭代时,强制用户手工在返回的列表上进行同步:
     
    List list = Collections.synchronizedList(new ArrayList());
    ...
    synchronized(list) {
    Iterator i = list.iterator(); // Must be in synchronized block
    while (i.hasNext())
    foo(i.next());
    }
    不遵从此建议将导致无法确定的行为。
    如果指定列表是可序列化的,则返回的列表也将是可序列化的。
     
     
    参数:
    list - 被“包装”在同步列表中的列表。
    返回:
    指定列表的同步视图。
    限制性使用构造线程安全的类变得更容易,因为类的状态被限制后,分析它的线程安全性时,就不必检查完整的程序。
    JAVA监视器模式
    java监视器模式是线程限制原则的直接推论之一,只要遵循了java监视器模式,则该对象所有的可变状态都被封装在对象中,并且由对象内部的锁保护。
    上面的Counter类就是该模式,其拥有一个value值,所有访问该变量都需要经过Counter的方法,而这些方法都是同步的。
    当前,其实锁不一定必须是该类的字节码,还可以使用自己的一个私有锁保护,如下:
    public class StringSet {
    private Object object = new Object();
    private final Set<String> mySet = new HashSet<>();
     
    public synchronized void setMySet(String p) {
    synchronized(object){
    mySet.add(p);
    }
    }
    }
    使用私有锁(非内部锁),可以封装锁,客户端无法得到,非内部锁是允许客户端代码访问。当使用可公共访问的锁,需要检查完整的程序,而非单独的类。
    public class StringSet {
    private String tag ;
    private Map<String, String> locations = new HashMap<String, String>();
    }

    3委托线程安全

    几乎所有的对象都是组合对象,下面是机动车最终其的实现,如MutablePoint描述了机动车的位置,虽然该类不是安全的,但是MonitorVehicleTracker是,程序没有将map或者其他包含的任何可变点发布出去,当我们需要将机动车位置返回给调用者时,正确返回值从MutablePoint执行拷贝的构造函数或deepCopy方法拷贝而来的。deepCopy会创建一个新的Map,它的值是从就Map的key和value而来。
    虽然先复制在返回给用户,维护者线程安全,但是当数据量相当大的时候,并且数据要求必须及时最新的时候,则这样会出现问题,就需要更频繁地刷新location集合。
    public class MutablePoint {
    public int x,y;
    public MutablePoint() {
    x = 0 ;
    y = 0;
    }
     
    public MutablePoint(MutablePoint p) {
    this.x = p.x;
    this.y = p.y;
    }
    }
    public class MonitorVehicleTracker {
    private final Map<String, MutablePoint> locations ;
     
    public MonitorVehicleTracker(Map<String, MutablePoint> locations) {
    this.locations = deepCopy(locations);
    }
     
    public synchronized Map<String, MutablePoint> getLocations() {
    return locations;
    }
     
    public synchronized MutablePoint getLocation(String id) {
    MutablePoint point = locations.get(id);
    return point == null ?null : new MutablePoint(point);
    }
     
    public synchronized void setLocation(String id ,int x , int y) {
    MutablePoint point = locations.get(id);
    if (point == null) {
    throw new IllegalArgumentException("no such id:" + id);
    }
    point.x = x;
    point.y = y;
    }
     
     
    /**
    * 深度克隆,不返回原有的数据
    */
    private Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> loMap) {
    Map<String, MutablePoint> lo = new HashMap<String, MutablePoint>();
    for (String id : loMap.keySet()) {
    lo.put(id, new MutablePoint(loMap.get(id)));
    }
    return Collections.unmodifiableMap(lo);
    }
    }
    下面换一种方式来追踪:使用委托的机动车追踪器
    下面使用了线程安全的point,基于‘监视器’的代码范湖location的筷快照,基于‘委托‘的代码返回一个不可变的,但是确实‘现场’的location视图,意味着如果线程A调用getLocation时,线程B修改了一些Point的location,这些变回会及时更新到A的map中。
    public class MutablePoint {
    public final int x,y;
    public MutablePoint(int x, int y) {
    this.x = x;
    this.y = y;
    }
    }
    public class MonitorVehicleTracker {
    private final ConcurrentHashMap<String, MutablePoint> locations ;
    private final Map<String, MutablePoint> unmodifiableMap ;
     
    public MonitorVehicleTracker(Map<String, MutablePoint> points) {
    //创建一个与给定映射具有相同映射关系的新映射。使用给定映射中映射关系数两倍的容量或 11(选更大的那一个)、默认加载因子和 concurrencyLevel 来创建该映射。
    locations = new ConcurrentHashMap<>(points);
    // 返回指定映射的不可修改视图。
    unmodifiableMap = Collections.unmodifiableMap(locations);
    }
     
    public Map<String, MutablePoint> getLocations() {
    return unmodifiableMap;
    }
     
    public MutablePoint getLocation(String id) {
    return locations.get(id);
    }
     
    public void setLocation(String id ,int x , int y) {
    if (locations.replace(id, new MutablePoint(x,y)) == null) {
    throw new IllegalArgumentException("invalid vehicle name:" + id);
    }
    }
    }
    如果需要一个不可变的瞬时视图,getLocation可以返回一个locationMap的灰拷贝(复制对象的引用,复制的对象与原始对象是同一个对象,深拷贝deepCopy则复制对象所有成员,与原始对象不是同一个对象),因为Map的内容不可变,因此需要复制的只有Map的结构,而不是它的内容。
    public Map<String, MutablePoint> getLocations(){
    return Collections.unmodifiableMap(new HashMap<>(locations));
    }
    非状态依赖变量
    上面的例子是委托了一个单一的线程安全的变量,也可以将线程安全委托到多个隐含的状态变量上,它们都是彼此独立。
    下面是允许客户注册鼠标键盘时间监听的图形组件,为每个类型维护一个已注册监听器的清单,但是这两个清单之间并没有关系,彼此独立,因此VisualComponet  可将它的线程安全委托到着两个线程安全的清单上。
    public class VisualComponet {
    //CopyOnWriteArrayList:
    //这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。
    private final List<KeyListener> keyListener = new CopyOnWriteArrayList<>();
    private final List<MouseListener> mouseListener = new CopyOnWriteArrayList<>();
     
    public void addKeyListener(KeyListener listener) {
    keyListener.add(listener);
    }
    public void addMouseListener(MouseListener listener) {
    mouseListener.add(listener);
    }
     
    public void removeKeyListener(KeyListener listener) {
    keyListener.remove(listener);
    }
    public void removeMouseListener(MouseListener listener) {
    mouseListener.remove(listener);
    }
    }
    当委托无法胜任时
    当对象的不变约束与组件的状态变量相联系,就不能完整地保护它的不变约束。
    public class NumberRange {
    private final AtomicInteger lower = new AtomicInteger();
    private final AtomicInteger upper = new AtomicInteger();
     
    public void setLower(int i){
    if (i > upper.get()) {
    throw new IllegalArgumentException("cant't set lower to " + i + "> upper");
    }
    lower.set(i);
    }
     
     
    public void setUpper(int i){
    if (i < lower.get()) {
    throw new IllegalArgumentException("cant't set upper to " + i + " < lower");
    }
    upper.set(i);
    }
     
    public boolean isInRange (int i) {
    return (i >= lower.get() && i <= upper.get());
    }
    }
    NumberRange不是线程安全的,它没有保护好用于约束lower和upper的不变约束,setLower与setUpper试图保护不变约束,但是都是‘检查再运行’的操作,它们没有适当加锁以保证其原子性。当一个线程调用setLower(5),而另一个线程调用setUpper(4),在一些偶发时段里,都能满足set方法中的检查,使修改全部生效,结果值可能变为(5,4)的范围
    虽然AtomicInteger是线程安全的,但是组合的类却不是,因为状态变量lower和upper不是彼此独立,不能简单地将线程安全委托给线程安全的状态变量上。可以通过加锁维护不变约束,如一把公共锁保护lower和upper。
     
    如果一个类由多个彼此独立的线程安全的状态变量组成,并且类的操作不包含任何无效状态转换时,可以将线程安全委托给这些变量,但是如果线程安全的状态之间有相互约束,则需要加额外的锁进行控制。
     
    如果一个状态变量时线程安全的,没有任何不变约束限时它的值,并且没有任何状态转换它的操作,那么可以被安全发布。
     
    可变但是线程安全的状态依赖(实现上面的机动车追踪器)
    可变单线程安全的point
    public class SafePoint {
     
    private int x,y;
     
    private SafePoint(int[] a) {
    this(a[0],a[1]);
    }
     
    public SafePoint(int x, int y) {
    this.x = x;
    this.y = y;
    }
     
    public SafePoint (SafePoint point) {
    this(point.get());
    }
     
    public synchronized int[] get() {
    return new int[]{x,y};
    }
     
    public synchronized void set(int x,int y){
    this.x = x;
    this.y = y;
    }
    }
    通过修改point来实现:
    public class MonitorVehicleTracker {
    private final ConcurrentHashMap<String, SafePoint> locations ;
    private final Map<String, SafePoint> unmodifiableMap ;
     
    public MonitorVehicleTracker(Map<String, SafePoint> points) {
    //创建一个与给定映射具有相同映射关系的新映射。使用给定映射中映射关系数两倍的容量或 11(选更大的那一个)、默认加载因子和 concurrencyLevel 来创建该映射。
    locations = new ConcurrentHashMap<>(points);
    // 返回指定映射的不可修改视图。
    unmodifiableMap = Collections.unmodifiableMap(locations);
    }
     
    public Map<String, SafePoint> getLocations() {
    return unmodifiableMap;
    }
     
    public SafePoint getLocation(String id) {
    return locations.get(id);
    }
    public void setLocation(String id ,int x , int y) {
    if (!locations.contains(id)) {
    throw new IllegalArgumentException("invalid vehicle name:" + id);
    }
    locations.get(id).set(x, y);
    }
    }
     

    4.向已有的线程安全类中添加功能

    客户端加锁
    非现场安全的“缺少即加入”实现,这样当我们将putIfAbsent方法加了锁,看是正确,但是list受保护的锁与该锁并不是同一把锁。因此putIfAbsent的操作对于List而言并不是原子化的,因此不能保证另一个线程不会修改list.
    public class Test{
    public List<String> list = Collections.synchronizedList(new ArrayList<String>());
     
    public synchronized void putIfAbsent(String str ){
    if (!list.contains(str)) {
    list.add(str);
    }
    }
    }
    因此我们必须保证方法所使用的锁,与list用于客户端加锁与外部加锁时使用的锁是同一个锁。下面是正确测处理
    public class Test{
    public List<String> list = Collections.synchronizedList(new ArrayList<String>());
     
    public synchronized void putIfAbsent(String str ){
    synchronized (list) {
    if (!list.contains(str)) {
    list.add(str);
    }
    }
    }
    }
    组合,自己创建锁
    向已有的类中添加一个原子操作,还有更健壮的选择:组合,ImproveList通过将操作委托给底层的List实例,实现了List的操作,同时还添加了一个原子的putIfAbsent方法。通过使用内部锁,Improve引入了一个新的锁层,它并不关心底层的List是否线程安全,及时不是安全的,或者会改变ImproveList的锁实现,它都有自己兼容的锁可提供线程安全。
    public class ImproveList<T> implements List<T> {
    private final List<T> list = new ArrayList<T>();
     
    public ImproveList(List<T> list ) {
    this.list = list;
    }
     
    public synchronized void putIfAbsent(T t ){
    if (!list.contains(t)) {
    list.add(t);
    }
    }
    }
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
  • 相关阅读:
    领料单取整
    财务应付金额对不上的
    销售订单计算交期
    辅助单位启用
    K3CLOUD日志目录
    QLIKVIEW-日期格式,数字格式写法
    MRP运算报错-清除预留
    整单折扣后 财务、暂估应付价税合计对不上的问题处理
    BZOJ 2976: [Poi2002]出圈游戏 Excrt+set
    BZOJ 3060: [Poi2012]Tour de Byteotia 并查集
  • 原文地址:https://www.cnblogs.com/yeziTesting/p/4983646.html
Copyright © 2020-2023  润新知