java并发编程实践笔记一

目录

不可变对象

JMM中final域的内存语义

实例限制

委托线程安全

向线程安全类添加功能


不可变对象

满足不可变对象的有三点:

1.它的状态(对象关联的成员属性)不能在构造后被修改,除构造方法外没有提供修改状态的代码途径。

2.所有域都是final的。

3.它被正确地创建,即:构造期间没有发生过this引用的逸出。

下面的讲解摘自外网:http://jeremymanson.blogspot.com/2008/07/immutability-in-java-part-2.html

1.the object is transitively reachable from a final field  表示状态的对象(final字段)可以通过final字段来传递式地获取到。

可以通过跟随零个或多个引用从final字段到达您想要的不可变的数据。如下类你可以bytes字段访问字节数组:

class String {
  private final byte[] bytes;
  public String(byte[] value) {
    byte[] newArray = new byte[value.length];
    System.arraycopy(value, 0, newArray, 0, value.length);
    bytes = newArray;
  }
}

 而如下类,仍然可以通过final的bytes字段来传递式地访问字节数组。

class String {
  		static class BytesHolder {
    		byte[] bytes;
  		}
  		final BytesHolder holder;
}

实际上,您可以根据需要添加任意数量的间接层 - 只要您可以通过跟踪字段中的引用传递式地访问那里,它仍然可以从字段中被传递式地访问到。

2.the object has not changed since the final field was set 自从final字段被设置以来,表示状态的对象(final字段)从没有改变过。

这里有个String类,其字节不再是不可变的。如果两个线程调用setZerothElt方法,则可能上演各种糟糕的线程马戏。

class String {
  private final byte[] bytes;
  public String(byte[] value) {
    byte[] newArray = new byte[value.length];
    System.arraycopy(value, 0, newArray,
        0, value.length);
    bytes = newArray;
  }
  public setZerothElt(byte b) {
    byte[0] = b;
  }
}

 事实证明,如果您希望数据是线程安全的,那么在设置final字段后就不应该更改它,不应该提供修改final字段的代码途径。而且,在设置final字段后还可以更改其状态,不可变对象在语义上只是在构造函数结束后无法更改其final字段。

class String {
  // Absolutely fine.
  private final byte[] bytes;
  public String(byte[] value) {
    bytes = new byte[value.length];
    System.arraycopy(value, 0, 
      bytes, 0, value.length);
  }
}

您可以将构造函数的结尾视为“冻结”操作,对象的final字段的值必须在构造函数的末尾冻结。 

3.A reference to the object containing the final field did not escape the constructor 对包含final字段的对象的引用没有在构造函数里逸出。

逸出构造函数意味着在构造函数中,你存储了一个对正在构造的对象的引用,而另一个线程获取了这个引用,并且正在使用它。例如:

class String {
  // Don't do this.
  static Set allStrings =
  Collections.synchronizedSet(new HashSet());
  private final byte[] bytes;
  public String(byte[] value) {
    allStrings.add(this);
    bytes = new byte[value.length];
    System.arraycopy(value, 0, 
      bytes, 0, value.length);
  }
}

另一个线程使用allStrings中的”this”时,bytes这个final字段可能还没准备好数据(还没达到想要的状态),你可能想到把allStrings.add(this);放到构造方法的结尾处,所以我们又有如下逸出构造方法的例程: 

class String {
// Don't do this, either.
  static String lastConstructed;
  private final byte[] bytes;
  public String(byte[] value) {
    bytes = new byte[value.length];
    System.arraycopy(value, 0, 
      bytes, 0, value.length);
    lastConstructed = this;
  }
}

 当读到lastConstructed=this时,你会一直认为它总会给你最后构造的String实例。设想一下多线程来使用这个lastConstructed的情景,在实际测试中线程有可能会看到未初始化的bytes数据,这是编译器愚弄了你,编译器把lastConstructed = this;这句移动到了bytes = new byte[value.length];之前。

所以,我们要避免不可变对象的引用逸出构造方法。

JMM中final域的内存语义

不可变对象构造方法中为什么不能“逃逸”this?其实这是final域的内存语义要求(JMM的规范),它有一套读写final域的重排序规则,如下:

1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

也就是:在写final域之后构造方法返回之前编译器会插入StoreStore内存屏障指令,在读不可变对象引用之后读final域之前编译器会插入LoadLoad内存屏障指令。

至于什么是内存屏障指令,什么是重排序,话题会很长,这里不展开,具体参见《并发编程的艺术》一书。

实例限制

封装简化了类的安全化工作,封装就是一种限制,因为当一个对象被另一个对象封装时,所有访问被封装对象的代码途径全部在封装对象中可知,而对外部整个系统来说暴露只是访问被封装对象的接口,并没有暴露被封装对象,这样地限制在类实例中,再与适当的锁策略相结合,就可以确保程序以线程安全的方式访问其他非线程安全对象。

总结一句,将数据封装在对象内部,把对数据的访问限制在对象的方法上,更容易确保线程在访问数据时总能获得正确的锁。即,封装更易于做线程安全化工作

可以把对象(数据)限制在类实例(比如私有类成员)、语汇范围(比如本地变量)、线程(比如对象在线程内部从一个方法传递到另一个方法,该对象不被跨线程共享)中。下面的例子示范了限制和内部锁一起确保了一个类的线程安全性:

/**
 * PersonSet
 * 

* Using confinement to ensure thread safety * * @author Brian Goetz and Tim Peierls */ @ThreadSafe public class PersonSet { @GuardedBy("this") 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); } interface Person { } }

封装或者可以说是包装。平台类库中有许多限制和锁结合的例子,就是Collections.synchronized…这样的方法产生的包装器对象,只要包装器对象有着对被包装对象的唯一引用,那么包装器对象就是线程安全的类。

发布其他对象也可能间接地发布了本应该受限制的被封装(包装)对象,比如你发布了被封装对象的迭代器,或者你发布了一个内部类实例(内部类实例有使用被封装对象的引用),这样被封装对象就逸出了。

委托线程安全

看举例:

基于监视器(内部锁)的实现类:

/**
 * MonitorVehicleTracker
 * 

* Monitor-based vehicle tracker implementation * * @author Brian Goetz and Tim Peierls */ @ThreadSafe public class MonitorVehicleTracker { @GuardedBy("this") private final Map locations; public MonitorVehicleTracker(Map locations) { this.locations = deepCopy(locations); } public synchronized Map 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 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 deepCopy(Map m) { Map result = new HashMap(); for (String id : m.keySet()) result.put(id, new MutablePoint(m.get(id))); return Collections.unmodifiableMap(result); } }

 以及非不可变对象:

/**
 * MutablePoint
 * 

* Mutable Point class similar to java.awt.Point * * @author Brian Goetz and Tim Peierls */ @NotThreadSafe 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; } }

为了配合讲解委托是什么,我们修改MutablePoint为不可变对象Point(它是线程安全的): 

/**
 * Point
 * 

* Immutable Point class used by DelegatingVehicleTracker * 这个类在构造完成后,没有提供修改其状态的接口(代码途径) * @author Brian Goetz and Tim Peierls */ @Immutable public class Point { public final int x, y; public Point(int x, int y) { this.x = x; this.y = y; } }

然后我们再把MonitorVehicleTracker的线程安全问题委托给ConcurrentMap和unmodifiableMap去考虑: 

/**
 * DelegatingVehicleTracker
 * 

* Delegating thread safety to a ConcurrentHashMap * * @author Brian Goetz and Tim Peierls */ @ThreadSafe public class DelegatingVehicleTracker { private final ConcurrentMap locations; private final Map unmodifiableMap; public DelegatingVehicleTracker(Map points) { locations = new ConcurrentHashMap(points); unmodifiableMap = Collections.unmodifiableMap(locations); } public Map 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); } // Alternate version of getLocations (Listing 4.8) public Map getLocationsAsStatic() { return Collections.unmodifiableMap( new HashMap(locations)); } }

 注:如果我们还用MutablePoint去代替Point,那么getLocations和getLocation就发布了一个非线程安全的可变的引用,破坏了封装性,其实就是暴露了对被封装对象的一部分访问,也就是没有封装好,也就是实例限制没做到位。

也可以将线程安全委托到多个状态变量上。例如:

/**
 * VisualComponent
 * 

* Delegating thread safety to multiple underlying state variables * * @author Brian Goetz and Tim Peierls */ public class VisualComponent { private final List keyListeners = new CopyOnWriteArrayList(); private final List mouseListeners = new CopyOnWriteArrayList(); 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); } }

注:在这个类中,2个list都是线程安全的类,而且2个状态之间没有不变约束的耦合,所以该类的委托行为是线程安全的,该类是线程安全的。然而如下例:  

/**
 * NumberRange
 * 

* Number range class that does not sufficiently protect its invariants * * @author Brian Goetz and Tim Peierls */ public class NumberRange { // INVARIANT: lower <= upper private final AtomicInteger lower = new AtomicInteger(0); private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i) { // Warning -- unsafe check-then-act if (i > upper.get()) throw new IllegalArgumentException("can't set lower to " + i + " > upper"); lower.set(i); } public void setUpper(int i) { // Warning -- unsafe check-then-act 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.get() && i <= upper.get()); } }

假设当前值域是(0,5),一个线程setLower(5),另一个线程setUpper(4),在一些偶发的时段里,这两个方法都通过了检查,并且使值域转换成了(5,4)这些无效的状态。所以,虽然AtomicInteger是线程安全的,但组合起来不是安全的,因为lower和upper之间有不变约束的耦合,所以可以通过枷锁来维护状态变量之间的不变约束。

总结一句话,如果一个类由多个彼此独立的线程安全的状态变量组成,并且类的方法不包括任何无效状态的转换,则该类可以线程安全地委托给这些状态变量。

另外,如果一个状态变量是线程安全的,且没有任何的不变约束限制它,那么它可以安全地发布。例如,VisualComponent中的2个keyListeners列表。

向线程安全类添加功能

重用能够降低开发的难度、风险(因为已有的组件已经经过测试)和维护成本。而一个类有时候只支持我们的大部分操作,没法满足我们需要的全部操作,这时候,我们要在不破坏线程安全的前提下,向他添加一个操作。

例如我们向Vector添加一个操作:

/**
 * BetterVector
 * 

* Extending Vector to have a put-if-absent method * * @author Brian Goetz and Tim Peierls */ @ThreadSafe public class BetterVector extends Vector { // When extending a serializable class, you should redefine serialVersionUID static final long serialVersionUID = -3963416950630760754L; public synchronized boolean putIfAbsent(E x) { boolean absent = !contains(x); if (absent) add(x); return absent; } }

有时候我们添加的安全操作,会出现同步幻象,例如:

/**
 * ListHelder
 * 

* Examples of thread-safe and non-thread-safe implementations of * put-if-absent helper methods for List * * @author Brian Goetz and Tim Peierls */ @NotThreadSafe class BadLsitHelper { public List list = Collections.synchronizedList(new ArrayList()); public synchronized boolean putIfAbsent(E x) { boolean absent = !list.contains(x); if (absent) list.add(x); return absent; } }

注:putIfAbsent操作和list的操作使用了不同的锁,所以也没法保证线程安全。然而如下:

@ThreadSafe
class GoodListHelper  {
    public List list = Collections.synchronizedList(new ArrayList());

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

总结,扩展安全方法和客户端加锁(GoodListHelper)他们所得类的行为与基类(Vector和SynchronizedList)的实现之间都存在耦合,实现时一定要小心谨慎。而且,扩展会破坏基类的封装性,客户端加锁会破坏同步策略的封装性(实例限制)。

然而为了更好地添加一个安全操作,我们引入如下的组合模式的实现:

/**
 * ImprovedList
 *
 * Implementing put-if-absent using composition
 *
 * @author Brian Goetz and Tim Peierls
 */
@ThreadSafe
public class ImprovedList implements List {
    private final List list;

    /**
     * PRE: list argument is thread-safe.
     */
    public ImprovedList(List list) { this.list = list; }

    public synchronized boolean putIfAbsent(T x) {
        boolean contains = list.contains(x);
        if (contains)
            list.add(x);
        return !contains;
    }

    // Plain vanilla delegation for List methods.
    // Mutative methods must be synchronized to ensure atomicity of putIfAbsent.
    
    public int size() {
        return list.size();
    }

    public boolean isEmpty() {
        return list.isEmpty();
    }

    public boolean contains(Object o) {
        return list.contains(o);
    }

    public Iterator iterator() {
        return list.iterator();
    }

    public Object[] toArray() {
        return list.toArray();
    }

    public  T[] toArray(T[] a) {
        return list.toArray(a);
    }

    public synchronized boolean add(T e) {
        return list.add(e);
    }

    public synchronized boolean remove(Object o) {
        return list.remove(o);
    }

    public boolean containsAll(Collection c) {
        return list.containsAll(c);
    }

    public synchronized boolean addAll(Collection c) {
        return list.addAll(c);
    }

    public synchronized boolean addAll(int index, Collection c) {
        return list.addAll(index, c);
    }

    public synchronized boolean removeAll(Collection c) {
        return list.removeAll(c);
    }

    public synchronized boolean retainAll(Collection c) {
        return list.retainAll(c);
    }

    public boolean equals(Object o) {
        return list.equals(o);
    }

    public int hashCode() {
        return list.hashCode();
    }

    public T get(int index) {
        return list.get(index);
    }

    public T set(int index, T element) {
        return list.set(index, element);
    }

    public void add(int index, T element) {
        list.add(index, element);
    }

    public T remove(int index) {
        return list.remove(index);
    }

    public int indexOf(Object o) {
        return list.indexOf(o);
    }

    public int lastIndexOf(Object o) {
        return list.lastIndexOf(o);
    }

    public ListIterator listIterator() {
        return list.listIterator();
    }

    public ListIterator listIterator(int index) {
        return list.listIterator(index);
    }

    public List subList(int fromIndex, int toIndex) {
        return list.subList(fromIndex, toIndex);
    }

    public synchronized void clear() { list.clear(); }
}

ImprovedList使用自己的内部锁,引入了一个新的锁层,它并不关心被封装的list是否线程安全,即使额外的一层锁会带来一些微弱的性能损失,但相比于客户端加锁去模拟另一个对象的锁策略而言,ImprovedList并不那么脆弱。只要使用监视器模式封装一个已有的list,并且唯一持有list的引用,那么就能保证线程安全。

转载笔记请注明出处。

你可能感兴趣的:(并发编程)