Java并发学习之对象的组合(设计线程安全的类)

一.本章概述

本章将介绍一些组合模式,这些模式能够使一个类更容易成为线程安全的类,并且在维护这些类时不会无意的破坏类的安全性保证。

学习目标:
1.设计线程安全的类
2.实例封闭
3.线程安全性的委托
4.在现有的线程安全的类中添加功能

二.具体学习

1.设计线程安全的类
在设计线程安全的类的过程中,需要包含下面三个要素:

1)找出构成对象状态的所有变量
要分析对象的状态,首先从对象的域开始:

a.如果对象中所有的域都是基本类型的变量,那么这些域将构成对象的全部状态

b.如果在对象的域中引用了其他的对象,那么该对象的状态将包含它引用的对象的域。

2)找出约束状态变量的不可变条件
a.要确保类的线程安全性,就要确保它的不可变性条件不会再并发访问的情况下被破坏。

b.对象和变量都有一个状态空间(所有可能的取值),状态空间越小,就越容易判断线程的状态,故final类型的域使用的越多,就越能简化对象可能状态的分析过程。

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

3)建立对象状态的并发访问管理策略

同步策略:它定义了如何在不违背对象不变条件或后验条件的条件下对其状态的访问操作进行协同,同步策略规定了如何将不可变性,线程封闭与加锁机制等结合起来以维护线程的安全性,并且还规定了那些变量由那些锁保护。

总结:如果想要设计出一个线程安全的类,首先我们就要确保类中所有变量的封装性,然后确保类中方法对变量操作的安全性,总之就是确保在多个线程同时访问时的正确性。

2.实例封闭
如果某对象不是线程安全的,那么我们可以通过多种技术使其在多线程程序中安全的使用
我们可以确保该对象只能由单个线程访问,或者通过一个锁来保护该对象的所有访问。

封装简化了线程安全类的实现过程,它提供了一种实例封闭机制,当一个对象被封装到另一个对象中,能够访问被封装对象的所有代码路径都是已知的,并通过将封装机制与合适的加锁策略结合在一起,可以确保以线程安全的方式来使用非线程安全的类。

将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。

封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性时就无需检查整个程序。

例子:通过线程封闭机制来确保线程安全

public class PersonSet{
 private final Set<Person> mySet=new HashSet<Person>();
 
 public synchronized void addPerson(Person p){
  mySet.add(p);
 }
 
 public synchronized boolean containsPerson(Person p){
    return mySet.contains(p);
  }
}

从上面的代码我们可以看到,其中的Person的线程安全性未知,在PersonSet中使用了它的内置锁来保护它在mySet中的状态。

1)java监视器模式

什么是java监视器模式?
遵循java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁保护。

下面我们通过一个实例来学习它:

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

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 deepCopy(locations);
    }

    //获得指定车的位置对象
    public synchronized MutablePoint getLocation(String id){
        MutablePoint loc = locations.get(id);
        return loc == null ? null:new MutablePoint(loc);
    }

    //更新车的位置信息
    public synchronized void setLocations(String id,int x,int y){
        MutablePoint loc=locations.get(id);
        if(loc==null)
            throw new IllegalArgumentException("No such ID: "+id);
        loc.x=x;
        loc.y=y;
    }

    //返回深拷贝位置合集
    private static Map<String,MutablePoint> deepCopy(Map<String,MutablePoint>m){
        Map<String,MutablePoint> result=new HashMap<String,MutablePoint>();

        for(String id:m.keySet())
            result.put(id,new MutablePoint(m.get(id)));

        return Collections.unmodifiableMap(result);
    }

}


//位置类
class MutablePoint{
    public int x,y;

    public MutablePoint(){
        x=0;
        y=0;
    }
    public MutablePoint(MutablePoint p){
        this.x=p.x;
        this.y=p.y;
    }

}

上面代码中有两个类:MonitorVehicleTracker , MutablePoint,前者封装了后者,并用自己的锁对它进行了保护。
从代码中我们可以看到,前者是线程安全的,后者并不是线程安全的,MonitorVehicleTracker所包含的Map对象locations和可变的MutablePoint对象都没有发布。

当需要返回车辆的位置时,上面程序通过MutablePoint拷贝构造函数,或者deepCopy方法来深度复制正确的值,从而生成一个新的Map对象。

从某钟程度上,这种实现方式是通过在返回客户代码之前复制可变的数据来维持线程安全性的。

我们知道,大多数对象都是组合对象,当从头开始构建一个类,或者将多个非线程安全的类组合为一个类时,java监视器模式是非常有用的。

3.线程安全性的委托

1)我们以下面的例子学习,将上面的程序用线程安全委托实现:

public class Point{
 public final int x,y;//声明不可变变量
 public Point(int x,int y){
   this.x=x;
   this.y=y;
 }
}

很明显,上面是个不可变类,线程安全。

然后将下面的线程安全委托给ConcurrentHashMap:

static <K,V> Map<K,V> unmodifiableMap(Map<? extends K,? extends V> m)
返回指定map的不可修改map

public class DelegatingVehicleTracker{

  private final ConcurrentMap<String,Point> locations;
  private final Map<String,Point> unmodifiableMap;

  public DelegatingVehicleTracker(Map<String,Point> points){
     locations=new ConcurrentHashMap<String,Point>(points);//创建线程安全的HashMap对象
     unmodifiableMap = Collections.unmodifiableMap(locations);//返回locations的不可变对象
  }

  public Map<String,Point> getLocations(){
      return unmodifiableMap;
  }

  public Point getLocation(String id){
   return locations.get(id);
 }
 public void setLocation(String id,int x,int y){
   if(locations.replace(id,new Point(x,y))==null)
     throw new IllegalArgumentException("invalid vehicle name: "+id);
 }
}

显然,上面的方法没有用内置锁来修饰,因为它把线程安全性委托给其中的对象了。
如果上面程序使用MutablePoint,那么上面的getLocation方法会发布一个线程不安全的引用。

2)独立的状态变量
只要这些变量彼此独立,我们可以将线程安全性委托给这多个状态变量。

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

什么是无效状态转换?我们看下下面代码:

public class NumberRange{
  private final AtomicInteger lower=new AtomicInteger(0);
  private final AtomicInteger upper=new AtomicInteger(0);
  
  public void setLower(int i){
     if(i>upper.get())
        throw new IllegalArgumentException("can't set lower to" + i+">upper");
      lower.set(i);
  }
  public void setUpper(int i){
    if(i<lower.get())
         throw new IllegalArgumentException("can't set upper to" + i+"<lower");
    upper.set(i);
  }
  public boolean isInrange(int i){
     return (i>=lower.ger() && i<=upper.get());
  }
}

执行上面的程序时,如果我们的setLower(5)和setUpper(2)同时进行的话,那么就会得到(5,2)很明显,它是一个无效的状态,而上面的操作就叫做无效状态转换。

3)发布底层的状态变量

如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全的发布这个变量。

例如:

 public class SafePoint{
  private int x, y;
  private SafePoint(int[] a){
     this(a[0],a[1]);
  }
  public SafePoint(SafePoint p){
     this(p.get());
  }
  public SafePoint(int x,int y){
     this.x=x;
     this.y=y;
  }
  public synchronized void set(int x,int y){
  this.x=x;
  this.y=y;
  }
  public synchronized int[] get(){
    return new int[]{x,y};
  }
}

上面的类是线程安全的,下面我们将它的对象作为状态变量进行下面的类的编写:

public class PublishingVehicleTracker{
  private final Map<String,SafePoint> locations;
  private final Map<String,SafePoint> unmodifiableMap;
  
  public PublishingVehicleTracker(Map<String,SafePoint>locations){
    this.locations=new ConcurrentHashMap<String,SafePoint>(locations);
    this.unmodifiableMap=Collections.unmodifiableMap(this.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.containsKey(id))
         throw new IllegalArgumentException("invalid vehicle name:"+ id);
     locations.get(id).set(x,y);
  }
}

4.在现有线程安全的类中添加功能

在编程的过程中,更多的时候现有的类支持大部分操作,但有些操作需要我们在不破坏线程安全性的情况下添加。
一般我们采用通过客户端加锁来实现:
例如,假设需要一个线程安全的链表,它需要提供一个原子操作:“若没有则添加”

public class ListHelper<E>{
 public List<E> list=Collections.sychronizedList(new ArrayList<E>());

 public boolean pubIfAbsent(E x){
     synchronized (list){
       boolean absent=!list.contains(x);
       if(absent) list.add(x);
       return absent;
     }
  }
}

我们可以看到上面的实现方式采用外部操作来实现所需的功能。
但是这种实现方式是非常脆弱的,我们还有更好的方法
通过组合实现:

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

上面通过组合的方式以特别的技巧采用内部的方式来添加这个原子操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小牧之

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值