《JAVA并发编程实战》第四章 对象的组合

4.1 设计线程安全的类

程序清单4-1 使用Java监视器模式的线程安全计数器
@ThreadSafe
public final class Counter {
    @GuardedBy("this")private long value = 0;
    
    public synchronized long getValue() {
        return value;
    }
    
    public synchronized long increment() {
        if(value == Long.MAX_VALUE)
            throw new IllegalStateException("counter overflow");
        return ++value;
    }
}

4.2 实例封闭

程序清单4-2 通过封闭机制来确保线程安全
/**
  1.PersonSet的状态由HashSet来管理,而HashSet并非线程安全的。
  2.但由于mySet是私有的并且不会逸出,因此HashSet被封闭在PersonSet中。
  3. 唯一能访问的路径是addPerson 与containsPerson方法,在执行他们时都需要获得PersonSet的内置锁
  4. PersonSet完全由他的内置锁保护,因此他是一个线程安全类
*/
@ThreadSafe
public class PersonSet {
    private final Set mySet = new HashSet<>();
    
    public synchronized void addPerson(Person p) {
        mySet.add(p);
    }
    
    public synchronized boolean containsPerson(Person p) {
        return mySet.contains(p);
    }
}

4.2.1Java监视器模式

遵循Java监视器模式的对象会把对象所有可变状态都封装起来,并由对象自己的内置锁来保护。eg: Vector,HashTable。
Java监视器模式仅仅是一种代码约定,只有自始至终都使用该锁对象,都可以用来保护对象的状态。
程序清单4-3 通过一个私有锁来保护状态

私有锁对象可以将锁封装起来,使用客户代码无法获得锁,但可以通过public方法来访问锁,以便参与到他的同步策略中

public class PrivateLock {
    private final Object myLock = new Object();
    
    Widget widget;
    
    void someMethod() {
        synchronized (myLock) {
            //访问或修改widget的状态
        }
    }
}

4.2.2 示例:车辆追踪器

程序清单4-4 基于监视器模式车辆追踪
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import lombok.Data;
import lombok.experimental.Accessors;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.NotThreadSafe;
import net.jcip.annotations.ThreadSafe;
/**
 虽然MutablePoint不是线程安全的,但是追踪器类是线程安全的
 它所包含的Map对象和可变的Point对象都未曾发布。
 当需要返回车辆的位置时,返回的是copy的位置对象,在位置容器特别大的情况下会极大地降低性能
 */
@ThreadSafe
public class MonitorVehicleTracker {    //vehicle   英[ˈvi:əkl] n.   车辆; 交通工具;

    @GuardedBy("this")
    private final Map locations;

    public MonitorVehicleTracker(Map locations) {
        this.locations = locations;
    }
    //获取监视到的所有车辆的不可修改的副本信息
    public synchronized Map getLocations(){
        return deepCopy(locations);
    }
    //通过车辆id获取车辆位置信息的一个副本
    public synchronized MutablePoint getLocation(String id){
        MutablePoint loc = locations.get(id);
        return loc == null ? null : new MutablePoint(loc);
    }
    
    //修改某id车辆的位置
    public synchronized void setLocation(String id,int x, int y){
        MutablePoint loc = locations.get(id);
        if(loc == null)
            throw new IllegalArgumentException("no such ID : " + id);
        loc.setX(x).setY(y);
    }
    //拷贝参数传进来的所有车辆的位置信息并返回一个副本
    private static Map deepCopy(Map m) {
         Map result = new HashMap<>(); //参数副本
         m.keySet().forEach(id->result.put(id, new MutablePoint(m.get(id))));//给副本赋值
         return Collections.unmodifiableMap(result);//返回不可修改的map
    }
    
}
//程序清单4-5  与Java.awt.Point类似的可变的Point类(不要这么做)
/**

车辆位置
*/
@Data
@NotThreadSafe
@Accessors(chain = true)
class MutablePoint{ //mutable   英[ˈmju:təbl] adj.   易变的,性情不定的;
    private int x;
    private int y;
    public MutablePoint(MutablePoint mutalbePoint) {
        this.x = mutalbePoint.getX();
        this.y = mutalbePoint.getY();
    }
}

4.3 线程安全性的委托

4.3.1 基于委托的车辆追踪器

程序清单4-7将线程安全委托给ConcurrentHashMap
/**
 * 没有任何显示的同步,所有对状态的访问都由ConcurrentMap管理,而且map所有的键值都是不可变的
 * ConcurrentMap的key 是final修饰的
 * 
 * 由于Point类是不可变的,因此它是线程安全的。不可变的值可以被自由地分享与发布,因此返回location时不需要复制。
 * 
 * 注释1、2意味着:
 *  如果线程A调用【getLocations】而线程B在随后修改了【setLocation】某些点的位置,
 *  那么在返回给线程A的Map中将反映出这些变化
 *  优点:更新数据
 *  缺点:可能导致不一致的车辆位置视图
 *
 */
@ThreadSafe
public class DelegatingVehicleTracker {
    private final ConcurrentMap locations;
    
    private final Map unmodifiableMap;
    
    public DelegatingVehicleTracker(Map points) {
        locations = new ConcurrentHashMap<>(points);
        unmodifiableMap = Collections.unmodifiableMap(points);
    }
    
    //1、在使用监视器模式的车辆追踪器中 返回的是 车辆的位置的快照
    public Map getLocations() {
//      return Collections.unmodifiableMap(new HashMap(locations)); //返回locations的静态拷贝
        return unmodifiableMap; //返回实时拷贝
    }
    
    //2、而在使用委托的车辆追踪器中 返回的是 一个不可修改 但却是实时的车辆位置视图
    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 : ");
    }
}

4.3.2 独立的状态变量

程序清单4-9 将线程安全性委托给多个状态变量
/**
 * keyListeners,mouseListeners每个状态都是线程安全的,并且两个状态之间不存在耦合关系
 * 所以VisualComponent可以将他的线程安全性委托给keyListeners和mouseListeners
 */
public class VisualComponent {
    //CopyOnWriteArrayList是一个线程安全列表,特别适合于管理监听器列表
    private final List keyListeners = new CopyOnWriteArrayList<>();
    private final List mouseListeners = new CopyOnWriteArrayList<>();
    
    public void addKeyListener(KeyListener listener) {
        keyListeners.add(listener);
    }
    
    public void addMouseListener(MouseListener mouseListener) {
        mouseListeners.add(mouseListener);
    }
    
    public void removeKeyListener(KeyListener keyListener) {
        keyListeners.remove(keyListener);
    }
    
    public void removeMouseListener(MouseListener mouseListener) {
        mouseListeners.remove(mouseListener);
    }
}

4.3.3 当委托失效时

程序清单4-10 NumberRange类并不足以保护它的不变性条件(不要这么做)
/**
 * 假设:取值范围是(0,10),如果一个线程调用setLower(5), 一个线程调用setUpper(4),
 * 那么:在一些错误的时序中,这两个调用都将通过检查,并且都能设置成功。
 * 结果:得到的取值范围就是(5,4),这是一个无效的状态
 * 所以:虽然AtomicInteger是线程安全的,但经过组合得到的类确不是。
 * 由于:lower和upper不是彼此独立的,
 * 因此:NumberRange不能将线程安全性委托给它的线程安全状态变量。
 */
import java.util.concurrent.atomic.AtomicInteger;
public class NumberRange {
    //不变性条件 lower < upper
    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("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 lower to " + i + " < lower");
         lower.set(i);
    }
    
    public boolean isInRange(int i) {
        return (i >= lower.get() && i <= upper.get());
    }
}

4.3.5 示例:发布状态的车辆追踪器

程序清单4-11 线程安全且可变的Point类
/**
 * 【发布底层的可变状态】
 * 当把线程安全性委托给某个对象的底层状态变量时,
 * 在什么情况下才可以发布这些变量从而使其它类可以修改他们?
 * 答案仍然取决于在类中对这些变量施加了哪些不变性条件
 */
@ThreadSafe
public class SafePoint {
    private int x,y;

    public SafePoint(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    private SafePoint(int[] a) { this(a[0],a[1]); }

    /*如果将拷贝的构造函数改为this.(p.x,p.y),则会产生竞态条件,因为其是public
     *而private的构造函数则可以避免这种竞态条件
     */
    public SafePoint(SafePoint p) { this(p.get()); }
    
    /*
     * 如果为x和y分别提供get 方法,那么获得这2个不同坐标的操作之间,
     * x,y的值发生变化,从而导致调用者看到不一致的值:车辆从来没有到达过(x,y)。
     * 
     * 通过使用SafePoint,可以构造一个发布其底层可变状态的车辆追踪器,
     * 还能确保其线程安全性不被破坏。
     */
    public synchronized int[] get() {
        return new int[] {x,y};
    }
    
    public synchronized void set(int x,int y) {
        this.x = x;
        this.y = y;
    }   
}
程序清单4-12 安全发布底层状态的车辆追踪器
/**
 * 程序清单4-7的差别:
 * 1、底层状态类不同 
 *      4-7: Point:状态不可变的
 *      4-12:SafePoint:线程安全且底层状态可变的
 * 2.
 *      4-7:setLocation()中replace()
 *      4-12:setLocation()中containKey()
 */
@ThreadSafe
public class PublishingVehicleTracker {
    private final Map locations;
    private final Map unmodifyMap;
    public PublishingVehicleTracker(Map locations) {
        this.locations = new ConcurrentHashMap(locations);
        this.unmodifyMap = Collections.unmodifiableMap(this.locations);
    }
    
    public Map getLocations() {
        return unmodifyMap;
    }
    
    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.4 在现有的线程安全类中添加功能

程序清单4-13 扩展Vector并增加一个 “若没有则添加”方法
//并非所有的类都像Vector一样将状态向子类公开,因此也就不适合采用这种办法
public class BetterVector extends Vector {
    /**
     * absent   英[ˈæbsənt] 
     * adj. 缺席的,不在场的; 缺少的,缺乏的; 不在意的,茫然的;
     * vt.  缺席,不参加; 不在;
     */
    public synchronized boolean putIfAbsent(E x) {
        boolean absent = !contains(x);
        if(absent)
            add(x);
        return absent;
    }
}

4.4.1 客户端加锁机制

程序清单4-14 非线程安全的“若没有则添加”(不要这么做)
/**
 * 【非线程安全】因为在错误的锁上进行了同步
 * 方法上的synchronized 是内置锁
 * 而Collections.synchronizedList(List list) 的锁是:
 *      Collectons.SynchronizedCollection.mutex (final)
 *  不同的锁意味着 putIfAbsent相对于List的其它操作来说并不是原子的
 */
@NotThreadSafe
public class ListHelper {
    public List list = Collections.synchronizedList(new ArrayList());
    
    /**
     * absent   英[ˈæbsənt] 
     * adj. 缺席的,不在场的; 缺少的,缺乏的; 不在意的,茫然的;
     * vt.  缺席,不参加; 不在;
     */
    public synchronized boolean putIfAbsent(E x) {
        boolean absent = !list.contains(x);
        if(absent)
            list.add(x);
        return absent;
    }
}
程序清单4-15 通过客户端加锁来实现”若没有则添加“

客户端加锁是指:
对于使用某个对象X的客户端代码,使用X对象本身用于保护其状态的锁来保护这段客户代码。
要使用客户端加锁,你必须要知道对象X使用的是哪一个锁

/** 
 * 通过添加一个原子操作类拓展类是脆弱的(BetterVector),因为它类的加锁代码分布到多个类中,
 * 然而,客户端加锁却更加脆弱,因为它将客户端类的加锁代码放到了与客户端完全无关的其它类中
 * 当在那些并不曾诺遵循加锁策略的类(本例中的list)上使用客户端加锁时,要特别小心
 */
@ThreadSafe
public class ListHelper {
    public List list = Collections.synchronizedList(new ArrayList());
    
    public boolean putIfAbsent(E x) {
        synchronized (list) { //使用list自身的锁,可能是list的内置锁,也可能是其类中定义的其它对象
            boolean absent = !list.contains(x);
            if(absent)
                list.add(x);
            return absent;
        }
    }
}

4.4.2 组合

为现有的类添加一个原子操作时,有一种更好的办法:【组合】

程序清单4-16 通过组合实现”若没有则添加“
/**
 * ImprovedList通过自身的内置锁增加了一层额外的加锁。
 * 它并不关心底层的List是否是线程安全的,即使List不是线程安全的或者修改了它的加锁实现,
 * ImprovedList也会提供一致的加锁机制来实现线程安全性。
 * 虽然额外的同步层可能导致轻微的性能损失,但与模拟另一个对象的加锁策略相比,【ImprovedList更为健壮】。
 * 事实上,我们使用了Java监视器模式来封装现有的List,并且只要在类中拥有指向底层List的唯一外部引用,就能确保安全。
 */
public class ImprovedList implements List {
    
    private final List list;

    public ImprovedList(List list) {
        this.list = list;
    }
    
    public synchronized boolean putIfAbsent(E x) {
        boolean absent = !list.contains(x);
        if(absent)
            list.add(x);
        return absent;
    }
    
    @Override
    public synchronized void clear() {
        list.clear();
    }
    //......按照类似的方式委托List的其它方法
}

你可能感兴趣的:(《JAVA并发编程实战》第四章 对象的组合)