Java并发编程实战——基础知识2

4 对象的组合

4.1 设计线程安全的类

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

  • 找出构成对象状态的所有变量
  • 找出约束状态变量的不变性条件
  • 建立对象状态的并发访问管理策略。

4.1.1 收集同步需求

public final class Counter {
	private long value = 0;

	public synchronized long getValue() {
		return value;
	}

	public syschronized long increment() {
		if (value == Long.MAX_VALUE) {
			throw new IllegalStateException("counter overflow");
		}

		return ++value;
	}
}

许多类中都定义了一些不可变条件,用于判断状态是有效的还是无效的。Counter中的value域是long类型的变量,其状态空间从Long.MIN_VALUE到Long.MAX_VAlUE。但是value有一个限制,即不能是负值。

在操作中还会包含一些后验条件来判断状态迁移是否是有效的。如果Counter的当前状态为17,那么下一个有效状态只能是18。当下一个状态需要依赖当前状态时,这个操作就必须是一个复合操作。

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

4.1.2 依赖状态的操作

在某些对象的方法中还包含一些基于状态的先验条件(PreCondition)。例如,不能从空队列中移除一个元素。如果在某个操作中包含基于状态的先验条件,那么这个操作就称为依赖状态的操作。

4.1.3 状态的所有权

许多情况下,所有权与封装性总是相互关联的:对象封装它拥有的状态,反之也成立,即对它封装的状态有所有权。状态变量的所有者决定采用何种加锁协议来维持变量状态的完整性。所有权意味着控制权。如果发布了某个可变对象的引用, 那么就不再拥有独占的控制权,最多是“共享控制权”。对于从构造函数或者从方法中传递进来的对象,类通常并不拥有这些对象,除非这些方法是被专门设计为转移传递进来的对象的所有权(例如,同步容器封装器的工厂方法)。

容器类通常表现出一种“所有权”分离的形式,其中容器类拥有其自身的状态,而客户代码则拥有容器中各个对象的状态。

4.2 实例封闭

当一个对象被封装到另一个对象中时,能够访问被封装的对象的所有代码路径都是已知的。

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

程序清单 4-2-1 通过封闭机制确保线程安全

@ThreadSafe
public class PersonSet {
	@GaurdedBy("this")
	private final Set<Person> mySet = new HashSet<Persion>();

	public synchronized void addPerson(Person p) {
		mySet.add(p);
	}

	public synchronized boolean containsPerson(Person p) {
		return mySet.contains(p);
	} 
}

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

4.2.1 Java监视器模式

程序清单4-2-1-1 通过一个私有锁来保护状态

public class PrivateLock {
	private final Object myLock = new Object();
	@GuardedBy("myLock")
	Widget widget;

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

使用私有的锁对象而不是对象的内置锁,有诸多优点:(1)私有的锁对象可以将锁封装起来,使客户代码无法得到锁,但客户代码可以通过公有方法来访问锁,以便参与到它的同步策略中。

4.2.2 示例:车辆追踪

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

@ThreadSafe
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(this.locations);
    }
    
    public synchronized MutablePoint getLocation(String id) {
        MutablePoint loc = locations.get(id);
        return loc == null ? null : new MutablePoint(loc);
    }
    
    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.x = x;
        loc.y = y;
    }
    
    private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> m) {
        Map<String, MutablePoint> result = new HashMap<>();
        for (String id : m.keySet()) {
            result.put(id, m.get(id));
        }
        
        return Collections.unmodifiableMap(result);
    }
}
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;
    }
}

4.3 线程安全性的委托

4.3.1 示例:基于委托的车辆追踪器

@Immutable 
public class Point {
	public final int x, y;

	public Point(int x, int y) {
		this.x = x;
		this.y = y;
	}
}
@TreadSafe
pubilc class DelegatingVehicleTracker {
	private final ConcurrentMap<String, Point> locations;
	private final Map<String, Point> unmodifiableMap;

	public DelegatingVehicleTracker(Map<String, Point> points) {
		locations = new ConcurrentHashMap(points);
		unmodifiableMap = Collections.unmodifiableMap(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);
		}
	}
}

4.3.2 独立的状态变量

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

	public void addKeyListener(KeyListener keyListener) {
		keyListeners.add(keyListener);
	}
	public void addMouseListener(MouseListener listener) {
		mouseListeners.add(listener); 
	}
	public void removeKeyListener(KeyListener keyListener) {
		keyListeners.remove(keyListener);
	}
	public void removeMouseListener(MouseListener listener) {
		mouseListeners.remove(listener); 
	}	
}

CopyOnWriteArrayList是一个线程安全的集合类,特别适合管理监听器列表

4.3.3 当委托失效时

如果一个类的状态变量之间存在着某些不变性条件,那么就需要额外的同步机制,即使每一个状态变量都是线程安全的。

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("can not set lower to " + i + " > upper.");
		}
		lower.set(i);
	}

	public void setUpper(int i) {
		// 注意:不安全的先检查后执行
		if (i < lower.get()) {
			throw new IllegalArgumentException("can not set upper to " + i + " < lower.");
		}
		upper.set(i);
	}

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

由于状态变量lower和upper不是彼此独立的,因此NumberRange 不能将线程安全性委托给它的线程安全的状态变量。

如果某个类含有复合操作,那么仅靠委托不足以实现线程安全性。在这种情况下,这个类必须提供自己的加锁机制以保证这些复合操作都是原子操作,除非整个复合操作都可以委托给状态变量。

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

4.3.4 发布底层的状态变量

不建议这么做。

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

例如,VisualComponent 中的两个监听器列表就可以被安全发布。VisualComponent 没有在两个列表的合法状态上施加任何约束。

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

@ThreadSafe
public class SafePoint {
	@GuardedBy("this")
	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 int[] get() {
		return new int[] {x, y};
	}

	public synchronized void set(int x, int y) {
		this.x = x;
		this.y = y;
	}
}
@ThreadSafe
public class PublishingVehicleTrachker {
	private final Map<String, SafePoint> locations;
	private final Map<String, SafePoint> unmodifiedMap;

	pubilc PublishingVehicleTracker(Map<String, SafePoint> locations) {
		this.locations = new ConcurrentHashMap(locations);
		this.unmodifiedMap = Collections.unmonifiedMap(this.locations);
	}

	public Map<String, SafePoint> getLocations() {
		return unmodifiedMap;
	}

	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.4.1 客户端加锁机制

程序清单 4-4-1-1 非线程安全的“若没有则添加”(不要这么做)

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

ListHelper只是带来了同步的假象,尽管所有的链表操作都被声明为了synchronized,但是使用了不同的锁,这意味着putIfAbsent相对于List的其他操作来说并不是原子的,因此无法确保putIfAbsent执行时另一个线程不会修改链表。

程序清单 4-4-1-2 通过客户端加锁来实现“若没有则添加”

@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无关的其他类中。当在那些并不承诺遵循加载策略的类上使用客户端加锁时,要特别小心。

客户端加锁机制与扩展类机制有许多共同点,二者都是将派生类的行为与基类的实现耦合在一起。正如扩展会破坏实现的封装性,客户端加锁同样会破坏同步策略的封装性。

4.4.2 组合

程序清单 4-4-2-1 通过组合实现“若没有则添加”

@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();
	}
	// ... 按照类似的方式委托List的其他方法。 
}

4.5 将同步策略文档化

在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值