关于设计线程安全的类你所要知道的一切

本文深入探讨设计线程安全类的方法,包括理解对象状态和约束、封闭实例机制、线程安全性的委托,以及如何安全地拓展线程安全类。通过实例分析了如何使用Java监视器模式、不可变视图和委托来实现并发访问策略,强调正确设计的重要性以避免并发编程中的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

设计线程安全的类

设计安全的类的过程,需要包含三个要素:

  • 找出构成对象状态的所有变量
  • 找出状态变量的约束
  • 建立对象状态的并发访问策略

这三条也是本篇文章的核心,以下将会逐一展开对象状态和约束都是什么,以及重点,如何结合三条要素,去设计并发访问策略。有三种常用的模式,封闭实例,委托,拓展。

状态和约束

状态就是对象中的状态变量,约束则是涉及到并发不可变性的种种约束。之所以放在一起讲,是因为本身约束就是作用于变量上。
找到状态和约束的过程,实际上也是收集同步需求的过程。

状态

对象的所有域都可能是状态,只要它们不是不可变的,如果域为引用类型,则引用对象的域也需要进行分析。

所以每个对象都会有一个状态变化空间,空间越小,这种分析越容易。从实践的角度来看,final的类型越多,不可变状态越多,这种分析越容易。

约束

状态之上的约束有三种:

  • 前置:先验条件。例如,判断集合为空才可以操作。
  • 后置:后验条件。例如,如果当前状态为14那么下一个状态必定是15。
  • 不变性条件。例如,NumberRange中low < high。

这部分,我认为简单理解为状态上的约束即可。不管是多个之间的不可变性条件,或者是集合不可变等等,通通都属于状态上的约束。

封闭实例机制

封闭实例机制,就不得不说到Java监视器模式。Java监视器模式是实现封闭实例机制最常见的方式。

所谓的封闭实例机制,就是把对象所有可变状态都封装起来,并由对象自己的内置锁来保护。

它非常好理解。简单来说就是封装状态,提供受限的同步接口进行访问。例如以下代码,只要Person是线程安全的,那么mySet就是线程安全的。

@ThreadSafe
public class PersonSet {
    @GuardedBy("this")
    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);
    }
}

我们尝试用开篇提到的三个要素来分析以下:

  1. 状态和约束
  • mySet,添加需要线程安全
  • Person,线程安全类(假设)
  1. 同步策略
  • 使用Java监视器模式,对mySet进行封装,提供同步的添加和访问接口
  • 用this锁保护状态的访问修改。

再来看一个更加复杂的例子,这个例子将会贯穿后文。

我们需要观察一个车辆位置的集合,跟踪车辆位置,例如绘图线程专门绘制这些点。我们需要获得某个点的位置,设置某个点的位置,获得所有车辆位置的视图。

这样的需求需要线程安全性,一种基于Java监视器的实现如下。
虽然MutablePoint不是线程安全的,但是MonitorVehicleTracker是线程安全的,它包含的map和point都没有发布(使用了保护性拷贝),并不会出现一致性问题(例如if 某个状态,进入该分支后被中断,切回来发现状态变动,导致一致性问题)。

@ThreadSafe
public class MonitorVehicleTracker {
    @GuardedBy("this")
    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 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<String, MutablePoint>();
        for (String id : m.keySet())
            result.put(id, new MutablePoint(m.get(id)));
        return Collections.unmodifiableMap(result);
    }
}

@NotThreadSafe
public class MutablePoint {
    public int x, y;

    public MutablePoint() {
        x = 0;
        y = 0;
    }
}

我们来分析一下:

  1. 状态和约束
  • locations:集合不得增减,访问修改需要线程安全
  • point:没有约束,但是Point的x,y更新获取要一致
  1. 同步策略,
  • 使用Java监视器模式
  • 提供locations的深拷贝,没有发布该状态,也不会发布Point
  • 使用synchronized,this锁保护状态修改访问的安全性

线程安全性的委托

事实上这个非常好理解,就是我们将这种线程安全性的需要,委托给其他线程安全性的类。
例如委托给AtomicLong,可以保证该变量的自增的线程安全性。

我们考虑如何使用委托设计上述车辆位置跟踪案例。

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

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

@ThreadSafe
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);
        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);
    }
}
  1. 状态和约束
  • locations:集合不得增减,集合访问修改需要线程安全
  • point:没有约束,但point的x,y两个状态的获取和更新需要一致
  1. 同步策略
  • 使用Java监视器模式
  • locations委托给线程安全的ConcurrentMap,保证修改访问的安全性
  • 提供不可变视图unmodifiableMap,保证集合不会增减(不可变视图后文会讲解)
  • Point改写为不可变类,使得unmodifiableMap获得的Point引用也不可变

状态安全发布

审视上面例子,我们思考一下,Point随着unmodifiableMap被发布了,但是Point是没有约束的(在本例的需求中),是否有必要不可变?
答案是否定的。

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

所以当前例子可以改为以下代码,它获得的位置集合不可变,但是具体的位置却可以自由更新,这因为这Point可以变动。

@ThreadSafe
public class PublishingVehicleTracker {
    private final Map<String, MutablePoint> locations;
    private final Map<String, MutablePoint> unmodifiableMap;

    public PublishingVehicleTracker(
            Map<String, MutablePoint> locations) {
        this.locations = new ConcurrentHashMap<String, MutablePoint>(locations);
        this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
    }

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

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

Point不可变的情况下,它没有任何约束,但当它可变的时候,它有一个隐含的约束,就是Point本身x,y两个状态的更新要一致——不能跟新了一个被中断。所以正确的设计是这样的,你能按照三要素分析一下这个案例的线程安全设计吗?

@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.x, p.y)那就会产生竞态条件,需要同时获取
        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 BetterVector<E> extends Vector<E> {
    public synchronized boolean putIfAbsent(E x) {
        boolean absent = !contains(x);
        if (absent)
            add(x);
        return absent;
    }
}

这个案例是线程安全的,并且实现也很简单,它给Vector类添加了线程安全的putIfAbsent方法。

为何不好

拓展方法比直接将代码添加到类中更加脆弱,因为同步策略被分布到多个单独维护的类中,如果底层类改变了同步策略,例如选用了不同的锁来维护,那么子类就会被破坏。

客户端加锁

客户端加锁说的是,当我们知道源代码使用什么锁的时候,获取这个锁,利用这个锁编写helper类,来拓展我们的方法。

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

为什么需要获取原来的锁,我们可以理解下面的错误实现来理解这个问题。
下面的实现不是线性安全的,因为它和原来的List使用了不同的锁,所以它根本没办法使得添加和获取操作同步。

@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;
    }
}
为何不好

同样,客户端加锁比起继承的方式甚至更加脆弱,因为它把List策略放到了一个和List完全无关其他类中,这样一来,所有地方都必须由客户端保证遵守客户端的策略(这可能也是一种Ad-hoc)。
例外,客户端加锁和继承方式有很多共同点,二者都是将派生类的行为与基类的实现耦合在一起,正如继承会破坏封装性,客户端加锁也会破坏封装性。

组合

一种更好的方式是使用组合,具体而言,就是实现相同的接口,并在原来的接口上进行拓展。

@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();
    }
    // ... similarly delegate other List methods
}
好在哪里

它并不关心底层的List是否是线程安全的,即使它修改了它的实现或者同步策略,ImprovedList也提供了一致性的加锁机制。事实上,这也是Java监视器模式的一个实例。

不可变视图

使用这种方式,还可以为已有集合提供不可变的视图,例如上文我们经常使用的Collections.synchronizedList,就是JDK提供的工具类实现。
源码如下:

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E> {
    private static final long serialVersionUID = -7754090372962971524L;

    final List<E> list;

    SynchronizedList(List<E> list) {
        super(list);
        this.list = list;
    }

    public E get(int index) {
        synchronized (mutex) {
            return list.get(index);
        }
    }

    public E set(int index, E element) {
        synchronized (mutex) {
            return list.set(index, element);
        }
    }
    
    // ...

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值