4. 对象的组合

4.1 设计线程安全的类

在设计线程安全的类的过程中,需要包含以下的三个基本的要素:

  • 找出构成对象状态的所有变量
  • 找出约束变量的不可变性
  • 建立对象状态的并发访问管理
public final class Counter{
    private long value = 0;

    public synchronized long getValue(){
        return value;
    }

    public synchronized long setValue(){
        if (value == long.MAX_VALUE) 
            throw new IllegalStateException("Counter overflow");
        return ++value;
    }
}

如上,我们构造了一个线程安全的类,我们来找出它所满足的三个条件:

  • 变量:value变量
  • 约束:改变value前,判断value是否达到最大值```if (value == long.MAX_VALUE)
- 并发管理:通过synchronized关键字

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

我们先来理解这两个名词:
- 不可性条件:在许多类中都定义了一些不可变条件,用于判断状态是否有效。比如,在上面的例子中,value的值必须满足在```Long.MIN_VALUE```和```Long.MAX_VALUE```之间。

- 后验条件:比如上面counter中当前状态为17,那么下一个有序状态只能为18。当下一个状态需要依赖上一个状态时,这个操作就必须是一个复合操作。

##### 4.1.2 依赖状态的操作
> 如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为“依赖状态的操作”。比如,在不能从空队列中删除元素,因此,在删除元素之前需要检查队列是否为空。

##### 4.1.3 状态的所有权
>状态变量的所有者将决定采用何种加锁协议来维持状态的完整性。所有权意味着控制权。但,如果发布了某个可变对象的引用,那么就不再拥有独立的控制权,最多是“共享控制权”。

#### 4.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);
}

}

如上,我们将PersonSet进行了实例封闭,将数据mySet设置为私有成员,并通过方法来对数据进行访问。

##### 4.2.1 Java监视器模式
> 对于任何一种锁对象,只要自始自终都使用该锁对象,都可以用来保护对象的状态。

public class privateLock{
private final Object myLock = new Object();
Widget widget;

void someMethod(){
    synchronized(myLock){
        //访问或修改Widget的状态
    }
}

}

如上为通过一个私有锁来保护状态

###### 示例:基于监视器模式的车辆追踪
我们要求视图线程和更新线程并发地访问数据模型,因此该模型必须是线程安全的。

- 记录轨迹的点:

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 = locations;
}
//返回数据的拷贝对象
public synchronized Map<String,MutablePoint> getLocations(){
return deepCopy(locations);
}
//对数据进行更新
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 Map<String,MutablePoint> deepCopy(Map<String, MutablePoint> map){
Map<String,MutablePoint> result = new HashMap<>();
for (String id : map.keySet())
result.put(id,new MutablePoint(map.get(id)));
return Collections.unmodifiableMap(result);
}
}

- 模拟更新和访问数据:

public static void main(String[] args){
//初始化数据
Map<String,MutablePoint> map = new HashMap<>();
map.put("test1",new MutablePoint());
map.put("test2",new MutablePoint());
map.put("test3",new MutablePoint());

MonitorVehicleTracker tracker = new MonitorVehicleTracker(map);
//设置更新线程,每隔10s更新一次数据
Thread set_thread = new Thread(){
    @Override
    public void run() {
        while (true){
            Map<String,MutablePoint> locations = tracker.getLocations();
            for (String key : locations.keySet()){
                MutablePoint p = locations.get(key);
                int dx = (int) (Math.random() * 10);
                int dy = (int) (Math.random() * 10);
                tracker.setLocations(key, p.x+dx, p.y+dy);
            }
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
};
//设置视图线程,每隔10s获取一次数据,并显示
Thread get_thread = new Thread(){
    @Override
    public void run() {
        while(true){
            Map<String,MutablePoint> locations = tracker.getLocations();
            for (String key : locations.keySet()){
                MutablePoint p = locations.get(key);
                System.out.println("key: " + key + " value: (" + p.x
                    + ","+p.y + ")");
            }
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
};

set_thread.start();
get_thread.start();

}


- 总结:对于上面的模式,类MutablePoint不是线程安全的,但追踪器类是线程安全的。因为它所包含的map对象和可变的Point对象都未曾发布过。当需要返回车辆的位置时,通过MutablePoint拷贝构造函数来复制正确的值,从而生成一个新的对象。

- 评价:优点是location集合上内部的数据是一致的,缺点是每次调用getLocation都需要复制数据,这将影响到性能。

#### 4.3 线程安全性的委托

public class CountingFactorizer implements Servlet{
private final AtomicLong count = new AtomicLong(0);

public long getCount() {return count.get();}

public void service(ServletRequest req, ServletResponse resp){
    BigInteger i = extractFromRequest(req);
    BigInteger[] factors = factors(i);
    count.incrementAndGet();
    encodeIntoResponse(resp,factors);
}

}

>  如上,我们将CountingFactorizer类的线程安全性委托给AtomicLong来保证:之所以CountingFactorizer是安全的,是因为AtomicLong是安全的。

###### 4.3.1 示例:基于委托的车辆追踪器
- 我们用不可变的Point类代替MutablePoint类:

public class Point{
public final int x,y;
public Point(int x, int y){
this.x = x;
this.y = y;
}
}

这样能保证在返回location时,不需要复制。因为不可变的值是可以被自由地共享。

- 将线程安全委托给ConcurrentHashMap

public class DelegatingVehicleTracker{
private final ConcurrentHashMap<String,Point> locations;
private final Map<String,Point> unmodifiableMap;

public DelegatingVehicleTracker(Map<String,Point> map){
    locations = new ConcurrentHashMap<String,Point>(map);
    unmodifiableMap = Collections.unmodifiableMap(map);
}

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

}

我们可以看到将线程安全性交给了安全容器ConcurrentHashMap,从而免去了对方法的加锁。

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

public class VisualComponent{
private final List<KeyListener> keyListeners
= new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners
= new CopyOnWriteArrayList<MouseListener>();

public void addKeyListener(KeyListener listener){
    keyListeners.add(listener);
}

public void addMouseListener(MouseListener listener){
    mouseListeners.add(listener);
}

public void removeKeyListener(KeyListener listener){
    keyListeners.remove(listener);
}

public void removeMouseListener(MouseListener listener){
    mouseListeners.remove(listener);
}

}

如上,我们将键盘和鼠标监听器列表都委托给CopyOnWriteArrayList,因为两者之间是相互独立的,因此不会增加不变性条件。

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

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");
    lower.set(i);
}

public boolean isInRange(int i){
    return (i >= lower.get() && i <= upper.get());
}

}

如上,虽然AtomicInteger是线程安全的,但经过组合得到的类却不安全。由于状态变量lower和upper并不是彼此独立的,因此NumberRange不能将线程安全性委托给它的安全状态变量。

如果某个类含有复合操作,仅靠委托并不足以实现线程安全性时,需要提供自己的加锁机制以保证这些复合操作都是原子性。

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

##### 4.3.5 发布状态的车辆追踪器
- 定义一个安全且可变的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 synchronized int[] get(){
    return new int[] {x,y};
}

public synchronized void set(int x,int y){
    this.x = x;
    this.y = y;
}

}

注意:我们这里将x和y绑定在一起,因为如果分别为x和y提供get方法,那么在获得这两个不同坐标的操作之间,x和y的值发生变化,从而导致调用者看到不一致的值。

- 安全发布底层状态的车辆追踪器:

public class PublishingVehicleTracker{
private final Map<String,SafePoint> locations;
private final Map<String,SafePoint> unmodifiableMap;

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

}

注意:PublishingVehicleTracker将其线程安全性委托给底层的ConcurrentHashMap,只是Map的元素是线程安全且可变的Point。getLocation方法返回底层Map对象的一个不可变副本。调用者不能增加或删除车辆,但却可以通过修改返回Map中的SafePoint值来改变车辆的位置。

#### 4.4 在现有的线程安全类中添加功能
> 假设需要一个线程安全的链表,它需要提供一个原子的“若没有则添加”的操作。

- 对原有的类进行扩展:

public class BetterVector<E> extends Vector<E>{
public synchronized boolean putIfAbsent(E x){
boolean absent = !contains(x);
if (absent)
add(x);
return absent;
}
}

评价:如果底层的类改变了同步策略并选择了不同的锁来保护它的状态量,那么子类会被破坏。因为在同步策略改变后它无法再使用正确的锁来控制对基类状态的并发访问。

- 扩展类的功能:

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

}

评价:该中方式并不能保证线程的安全性,因为List所用的锁和ListHelper所用的锁不是同一个锁。这意味着putIfAbsent相对于list的其他操作来说不是原子的。


- 客户端加锁:对于使用某个对象X的客户端代码,使用X本身用于保护其状态的锁来保护这段客户代码。

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

}

如此,就能保证ListHelper所用的锁和list所用的锁是一致的。

##### 4.4.2 组合:实现接口的方式

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 absent = !list.contains(x);
    if (absent)
        list.add(x);
    return absent;
}

}

ImprovedList通过自身的内置锁增加了一层额外的加锁来实现线程的安全性。













最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,628评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,209评论 11 349
  • (一)Java部分 1、列举出JAVA中6个比较常用的包【天威诚信面试题】 【参考答案】 java.lang;ja...
    独云阅读 7,079评论 0 62
  • 最近回头一看,发现我们的项目现在对图片处理都是用YYWebImage 的处理方式方式的,用了不短时间了,却没有好好...
    炸街程序猿阅读 1,251评论 0 1