Java并发编程实战之 线程安全性、对象的共享、对象的组合

线程安全性

当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式修复这个问题:

  • 不在线程之间共享该状态变量
  • 将状态变量修改为不可变类型
  • 在访问状态变量时使用同步

内置锁

Java 提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。

synchroized (lock) {
     
    // 访问或修改由锁保护的共享状态
}

每个 Java 对象都可以用做一个实现同步的锁,这些锁被称为 内置锁(Intrinsic Lock)监视器锁(Monitor Lock)

线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法

Java 的内置锁相当于一种互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁,当线程A尝试获取一个由线程B持有的锁,线程A必须等待或者阻塞,知道线程B释放这个锁。如果B不能释放,造成死锁,那么A将永远地等待下去。

由于每次只能有一个线程执行内置锁保护的代码块,因此,由这个锁保护的同步代码块会以原子方式执行,多个线程在执行该代码块时也不会互相干扰。并发环境中的原子性与事务应用程序中的原子性有着相同的含义——一组语句作为一个不可分割的单元被执行

重入

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。

“重入"意味着获取锁的操作粒度是"线程”,而不是"调用"。

重入的一种实现方法是为每个锁关联一个计数器和一个所有者线程。当计数器为0时,这个锁就被认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM 将记下锁的持有者,并且将获取计数值置为1。如果同一个线程再次获取这个锁,计数器将递增,而当线程退出同步代码块时,计数器会相应的递减。当计数器为0,这个锁将释放。

例如:

public class Widget {
     
    public synchronized void doSomething() {
     
        ...
    }
}

public class LoggingWidget extends Widget {
     
    public synchronized void doSomething() {
     
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();
    }
}

由于 Widget 和LoggingWidget 中 doSomething 方法都是 synchronized 方法,因此每个 doSomething 方法在执行前都会获取 Widget 上的锁。然而,如果内置锁是不可重入的,那么在调用 super.doSomething 方法时将无法获得 Widget 上的锁。

super.doSomething()的含义是,通过super引用调用从父类继承而来的doSomething()方法,那么锁的还是当前的子类对象,因此子类对象被锁了2次,说明内置锁是可重入的,否则会发生死锁。

用锁来保护状态

由于锁能使其保护的代码路径以串行(多个线程一次以独占的方式访问)来访问,因此可以通过锁来构造一些协议以实现对共享状态的独占访问。只要始终遵循这些协议,就能确保状态的一致性。

访问共享状态的符合操作,例如计数器的递增操作(读取-修改-写入)或者延迟初始化(先检查-后执行),都必须是原子操作以避免产生竞态条件。

**如果在符合操作的执行过程中持有一个锁,那么回时复合操作成为原子操作。**然而,仅仅将复合操作封装到一个同步代码块是不够的。

如果使用同步来协调对某个变量的访问,那么在访问这个变量的所有位置都需要使用同步。而且,当使用锁来协调对某个变量的范文时,在访问变量的所有位置都要使用同一个锁。

一种常见的错误认为只有在写入共享变量时才需要使用同步,但事实并非如此(下一节会提到)

对象的内置锁与其状态之间没有内在的关联。虽然大多数类都将内置锁用作一种有效的加锁机制,但对象的域并不一定要通过内置锁来保护。

当获取与对象关联的锁时,并不能阻止其他线程访问该对象,某个线程在获得对象的锁之后,只能阻止其他线程获得同一个锁。之所以每个对象都有一个内置锁,只是为了免去显式地创建锁对象。

每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。

并非所有数据都需要锁的保护,只有被多个线程同时访问的可变类型数据才需要通过锁来保护。当某个变量由锁来保护时,意味着每次访问这个变量都需要首先获得锁,这样就确保在同一时刻只有一个线程可以访问这个变量。

对象的共享

可见性

非原子的 64 位操作

当线程在没有同步的情况下读取变量时,可能会得到一个失效值(非最新),但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性(out-of-thin-air safety)

最低安全性适用于绝大多数变量,但存在一个例外:非 volatile 类型的 64 位数值变量(long 和 double)——long 和 double 的非原子协定

**Java 内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非 volatile 类型的 long 和 double 变量,JVM 允许将 64 位的读操作或写操作分解为两个 32 位的操作。**当读取一个非 volatile 类型的 long 变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。

因此,即使不考虑是小数据问题,在多线程中使用共享且可变的 long 和 double 等类型的变量也是不安全的,除非使用 volatile 来声明它们,或者用锁保护起来

加锁与可见性

内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。如下图所示。当线程A执行某个同步代码块时,线程B随后进入一个由同一个锁保护的同步代码块,在这种情况下可以保证,在锁被释放之前,A 看到的变量值在 B 获得锁后同样可以由 B看到。

Java并发编程实战之 线程安全性、对象的共享、对象的组合_第1张图片

**之所以要在访问某个共享且可变的变量时要求所有线程在同一个锁上同步,就是因为要确保某个线程写入该变量的值对于其他线程来说都是可见的。**否则,如果一个线程在未持有正确锁的情况下读取某个变量,那么读到的就可能是一个失效值。

Volatile 变量

Java 提供了一种稍弱的同步机制——即 volatile 变量,用来确保将变量的更新操作通知到其他线程。把变量声明为 volatile 类型之后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。

虽然 volatile 变量很方便,但也存在一些局限性。volatile 变量通常用作某个操作完成、发生中断或者表示状态的标志。尽管 volatile 可以表示状态等其他信息,但在使用时要非常小心。volatile 的语义不足以确保递增操作 (count++) 的原子性,除非你能确保只有一个线程对变量执行写操作。

加锁机制既能确保可见性又能确保原子性,而 volatile 变量只能确保可见性

当且仅当满足以下所有条件时,才应该使用 volatile 变量:

  • 对变量的写入操作不依赖当前遍历的当前值,或者你能确保只有单个线程更新变量的值
  • 该变量不会与其他状态量一起纳入不变性条件中
  • 在访问变量时不需要加锁

发布与溢出

发布(Publish)” 一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。例如:

  • 将一个指向该对象的引用保存到其他代码可以访问的地方
  • 或者在某一个非私有的方法中返回该引用
  • 又或者将引用传递到其他类的方法中

当某个不应该发布的对象被发布时,这种情况就被称为 “**溢出 (Escape) **”。

发布一个对象

public static Set<Sercret> knownSercets;

public void initialize() {
     
    knowSercets = new HashSet<Sercet>();
}

当某个对象发布时,可能会间接发布其他对象,就比如例子中的 knownSercets 对象,因为任何代码都可以遍历这个集合。

线程封闭

当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。这种技术被称为 线程封闭(Thread Confinement)

例如一种常见的应用是 JDBC(Java Database Connectivity)的 Connection 对象。JDBC 规范并不要求 Connection 对象是线程安全的。

Ad-hoc 线程封闭

Ad-hoc 线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。它是非常脆弱的,因为没有任何一种语言特性,例如可见性修饰符或局部变量,能将对象封闭到目标线程上。

当决定使用线程封闭技术时,通常是因为要将某个特定的子系统实现为一个单线程子系统。在程序中尽量少使用。

栈封闭

在栈封闭中,只能通过局部变量才能访问变量

栈封闭(也被称为线程内部使用或者线程局部使用)比Ad-hoc线程封闭更易于维护,也更加健壮。

Java语言确保了基本类型的局部变量始终封闭在线程内。下面代码是表示:基本类型的局部变量与引用变量的线程封闭性。

public int loadTheArk(Collection<Animal> candidates) {
     
		SortedSet<Animal> animals;
		int numPairs = 0;
		Animal candidate = null;
		
		// animals被封装在方法中,不要使它们溢出!!
		animals = new TreeSet<Animal>(new SpeciesGenderComparator());
		animals.addAll(candidates);
		for(Animal a:animals){
     
			if(candidate==null || !candidate.isPotentialMate(a)){
     
				candidate = a;
			}else{
     
				ark.load(new AnimalPair(candidate,a));
				++numPairs;
				candidate = null;
			}
		}
		return numPairs;
}

在 loadTheArk 中实例化一个TreeSet对象,并将该对象的一个引用保存到animals中。此时,只有一个引用指向集合animals,这个引用被封闭到局部变量中,因此也被封闭到局部变量中。

然而,如果发布了对集合animals(或者该对象中的任何内部数据)的引用,那么封闭性将被破坏,并导致对象animals的逸出。

ThreadLocal 类

维持线程封闭性的一种更规范方法是使用 ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。

ThreadLocal 提供了 get 与 set 等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此 get 总是返回由当前执行线程在调用 set 时设置的最新值。

ThreadLocal 对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。

例如通过将JDBC的连接保存到ThreadLocal对象中,每个线程都会拥有属于自己的连接。

private static ThreadLocal<Conection> connectionHolder = new ThreadLocal<Connection>(){
     
	public Connection initialValue(){
     
		return DriverManager.getConneciton(DB_URL);
	}
}
	
public static Connection getConnection(){
     
	return connectionHolder.get();
}

当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而又希望避免每次执行时都分配该临时对象,就可以使用这项技术。

当某个线程初次调用 ThreadLocal.get 时,就会调用 initialValue 来获取初始值。从概念上看,你可以将 ThreadLocal 视为包含了 Map 对象,其中保存了特定于该线程的值,但 ThreadLocal 并非如此。这些特定于线程的值保存在 Thread 对象中,当线程终止后,这些值将被当作垃圾回收。

不变性

满足同步需求的另一种方法是使用不可变对象(Immutable Object)

不可变对象一定是线程安全的!!

当满足以下条件时,对象才是不可变的:

  • 对象创建以后其状态就不能修改
  • 对象的所有域都是 final 类型
  • 对象是正确创建的(在对象的创建期间,没有 this 溢出)

例:在可变对象基础上构建构建不可变类

@Immutable
public final class ThreeStooges {
     
    private final Set<String> stooges = new HashSet<String>();
    
    public ThreeStooges() {
     
        stooges.add("Michael");
        stooges.add("George");
        stooges.add("Amy");
    }
    
    public boolean isStooge(String name) {
     
        return stooges.contains(name);
    }
}

final 域

“除非需要某个域是可变的,否则应将其声明为 final 域”——一个良好的编程习惯

安全发布

在某些情况下我们希望在多个线程共享对象,此时必须确保安全地进行共享。如果只是想如下程序一样保存到共有域中,那么还不足以安全地发布这个对象。

// 不安全的发布
public Holder holder;

public void initialize() {
     
    holder = new Holder(42);
}

为什么没有安全发布? 由于可见性问题,其他线程看到的 Holder 对象将处于不一致的状态,即便在该对象的构造函数已经正确地构建了不变性条件。这种不正确的发布将导致其他线程看到尚未创建完成的对象

因为没有足够的同步来确保 Holder 对象对其他线程可见,因此将 Holder 成为“未被正确发布”。其他线程看到 Holder 的域可能是一个失效值。

不可变对象

任何线程都可以在不需要额外同步的情况下访问不可变对象,即使在发布这些对象时没有使用同步。

安全发布的常用模式

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:

  • 在静态初始化函数中初始化一个对象引用
  • 将对象的引用保存到 volatile 类型的域或者 AtomicReference 对象中
  • 将对象的引用保存到某个正确构造对象的 final 类型域中
  • 将对象的引用保存到一个由锁保护的域中

线程安全容器内部的同步意味着,将对象放入到某个容器,例如 Vector 或 synchronizedList 时,将满足最后一条需求

例如线程A将对象X放入一个线程安全的容器中,随后线程B读取这个对象,那么可以确保B看到A设置的X对象是最新的,即使在读 / 写 X的代码中没有显式的同步。

在线程安全库中的容器类提供了以下的安全发布保证:

  • 通过将一个键或者值放入 HashTable、synchronizedMap、ConcurrentMap 中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是迭代器访问)。

  • 通过将某个元素放入 Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或是 synchronizedSet 中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程。

  • 通过将某个元素放入 BlockingQueue、ConcurrentLinkedQueue 中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程

通常,要发布一个静态构造的对象,最安全和最简单的方式是使用静态的初始化器:

public static Holder holder = new Holder(42);

静态初始化器由 JVM 在类的初始化阶段进行。由于在 JVM 内部存在着同步机制,因此通过这种方式初始化任何对象都可以被安全地发布。

事实不可变对象

如果对象在发布后不会再被修改,那么对于在没有额外同步的情况下安全地访问这些对象的线程来说,安全发布是足够的。

如果对象从技术上看是可变的,但其状态在发布后不会再改变,那么把这种对象成为**“事实不可变对象(Effectively Immutable Object)”**。

在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

可变对象

对象对于安全发布的需求取决于它的可变性:

  • 不可变对象可以通过任意机制来发布
  • 事实不可变对象必须通过安全方式来发布
  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来

安全地共享对象

当获得一个对象的引用时,你需要知道在这个引用上可以执行哪些操作?!!

使用它之前是否需要获得锁?是否可以修改它的状态?或者是只读对象?许多并发错误都是由于没有了解共享对象的这些”既定规则“导致的。

在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:

  • 线程封闭,例如 ThreadLocal 等方式
  • 只读共享
  • 线程安全共享,线程安全的对象在其内部实现
  • 保护对象,例如只能通过特定的锁访问

对象的组合

设计线程安全的类

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

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

要分析对象的状态,首先从对象的域开始。如果对象中所有的域都是基本类型的变量,那么这些域将构成对象的全部状态。如果在对象的域中引用了其他对象,那么该对象的状态将包含被引用对象的域。

使用 Java 监视器模式的线程安全计数器

public class Counter {
     
    private long value = 0;
    public synchronized long getValue() {
     
        return value;
    }
    public synchronized long increment() {
     
        if (value == Long.MAX_VALUE) {
     
            throw new IllegalArgumentException("");
        }
        return ++value;
    }
}

同步策略(Synchronization Policy)定义了如何在不违背对象不变条件后验条件(接下来情况的检验)的情况下对其状态的访问操作进行协同。同步策略规定了如何将不可变性线程封闭加锁机制等结合起来以维护线程的安全性,并且还规定了那些变量由那些锁来保护。

收集同步需求

要确保类的线程安全性,就需要确保它的不变性条件不会在并发访问的情况下被破坏,这就需要对其状态进行推断。

同样,在操作中还会包含一些后验条件来判断状态迁移是否有效的。如自增值。

依赖状态的操作

类的不变性条件与后验条件约束了在对象上有哪些状态和状态转换是有效的。如果在某个操作中包含有基于状态的先验条件,那么这个操作就称为依赖的操作。

等待某个条件为真的各种内置机制(包括等待和通知等机制)都与内置加锁机制紧密关联,要想正确地使用它们并不容易。要想实现某个等待先验条件为真时才执行的操作,一种更简单的方法是通过现有库中的类(例如阻塞队列Blocking Queue 或信号量 Semaphore)来实现依赖状态的行为。

实例封闭

如果某对象不是线程安全的,那么可以通过多种技术使其在多线程程序中安全地使用。你可以确保该对象只能由单个线程访问(线程封闭),或者通过一个锁来保护对该对象的所有访问。

封装简化了线程安全类的实现过程,它提供了一种实例封闭机制(instance Confienement)。当一个对象被封装到另一个对象中时,能够访问被封装对象的所有代码路径都是已知的。

通过封闭机制来确保线程安全

@ThreadSafe
public class PersonSet {
     
    private final Set<Person> mySet = new HashSet<Person>();
    public synchronized void addPerson(Person p) {
     
        mySet.add(p);
    }
    public synchronized boolean containPerson(Person p) {
     
        return mySet.contains(p);
    }
}

实例封装是构建线程安全类的一个最简单方式,它还使得在锁策略的选择上拥有了更多的灵活性。

当然,如果将一个本该本封闭的对象发布出去,那么也会破坏封闭性。如果一个对象本应该封闭在特定的作用域内,那么让该对象逸出作用域就是一个作物。当发布其他对象时,例如迭代器或内部的类实例,可能会间接地发布被封闭的对象,同样会使本封闭的对象逸出。

Java 监视器模式

从线程封闭原则及其逻辑推理可以得出Java监视器模式。遵循Java监视器模式的对象会把对象的所有可变状态都封装起来,并由对象自己的内置锁来保护。

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

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

使用私有的锁对象而不是对象的内置锁(任何其他可通过公有方式访问的锁)有许多优点:

私有的锁对象可以将锁封装起来,是客户代码无法得到锁,但客户代码可以通过公有方法来访问,以便参与到它的同步策略中。如果客户代码错误地获得了另一个对象的锁,那么可能会产生活跃性问题。

此外,要想验证某个公有访问的锁在程序中是否被正确地使用,则需要检查整个程序,而不是单个的类。

示例:车辆调度

以下程序清单中,我们看一个示例: 一个用于调度车辆的“车辆追踪器”。首先使用监视器模式来构建车辆追踪器,然后尝试放宽某些封装性需求同时又保持线程安全性。

public class MonitorVehicleTracker {
     
    @GuardBy("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 lo = locations.get(id);
        return lo == null ? null : new MutablePoint(lo);    //返回拷贝信息
    }

    public synchronized void setLocations(String id, int x, int y) {
     
        MutablePoint lo = locations.get(id);
        if (lo == null) {
     
            throw new IllegalArgumentException("");
        }
        lo.x = x;
        lo.y = y;
    }

    private static Map<String, MutablePoint> deepCopy(Map<String, MutablePoint> locations) {
     
        Map<String, MutablePoint> result = new HashMap<String, MutablePoint>();
        for (String id : locations.keySet()) {
     
            result.put(id, new MutablePoint(locations.get(id)));
        }
        return Collections.unmodifiableMap(result);
    }
}

虽然 MutablePoint 不是线程安全的,但追踪器类是线程安全的。它包含的 MutablePoint 对象都未曾发布

当需要返回车辆位置时,通过 MutablePoint 拷贝构造函数或者 deepCopy 方法来复制正确的值,从而生成一个新的 Map 对象。

@NotThreadSafe
public class MutablePoint {
       // don't do it!
    public int x, y;
    public MutablePoint() {
     
        x = 0; y = 0;
    }
    public MutablePoint(MutablePoint p) {
     
        this.x = p.x;
        this.y = p.y;
    }
}

线程安全性的委托

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

下面将介绍一个更实际的委托示例,构造一个委托给线程安全类的车辆追踪器。我们将车辆位置保存到一个 实现线程安全的Map 对象中,还可以用一个不可变的 Point 类来代替 MutablePoint 以保存位置。

在DelegatingVehicleTracker 中使用的不可变 Point 类

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

由于Point 类时不可变的,因而它是线程安全的。  将线程安全委托给 ConcurrentHashMap。

public class DelegatingVehicleTrack {
     
    
    private final ConcurrentMap<String, Point> locations;
    private final Map<String, Point> unmodifiableMap;
    
    public DelegatingVehicleTrack(Map<String, Point> pointMap) {
     
        locations = new ConcurrentHashMap<String, Point>(pointMap);
        // Collections.unmodifiableMap 返回的是 locations 的只读对象
        unmodifiableMap = Collections.unmodifiableMap(locations);
    }
    
    public Map<String, Point> getLocations() {
     
        return unmodifiableMap;
    }
    
    public Point getLocation(String id) {
     
        return locations.get(id);
    }
    
    public void setLocations(String id, int x, int y) {
     
        if (locations.replace(id, new Point(x, y)) == null) {
     
            throw new IllegalArgumentException("");
        }
    }
}

ConcurrentHashMap本身是一个线程安全的HashMap,所以无需进行深度拷贝,直接在线程间共享该数据结构即可。从上面的实现可以看到,位置信息使用ImmutablePoint而不是MutablePoint,这是因为位置信息也会发布出去,也可能会在线程间共享,而ConcurrentHashMap只能保证自身操作的线程安全。ConcurrentHashMap的key、value都需要是线程安全的,ImmutablePoint使用不变性提供了线程安全,String可以认为是常量,同样支持线程安全。与第一种实现发放不同的是,每个线程拿到的位置信息视图是一个变化的,并非快照,如果需要快照,通过浅拷贝即可实现。

独立的状态变量

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 mouseListener) {
     
        mouseListeners.add(mouseListener);
    }
    
    public void removeKeyListener(KeyListener keyListener) {
     
        keyListeners.remove(keyListener);
    }
    
    public void removeMouseListener(MouseListener mouseListener) {
     
        mouseListeners.remove(mouseListener);
    }
}

VisualComponent 使用 CopyOnWriteArrayList 来保存各个监听器列表。它是一个线程安全的链表,特别适用于管理监听器列表。

copyonwrite机制

和单词描述的一样,他的实现就是写时复制, 在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,然后用现在的数组去替换成员变量的数组(就是get等读取操作读取的数组)。这个机制和读写锁是一样的,但是比读写锁有改进的地方,那就是读取的时候可以写入的 ,这样省去了读写之间的竞争,看了这个过程,你也发现了问题,同时写入的时候怎么办呢,当然果断还是加锁。

委托失效时

大多数组合对象都不会像 VisualComponent 这样简单:在它们的状态变量之间存在着某些不变性条件。

NumberRange 类并不足以保护它的不变性条件

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()) {
       //  不安全的 先检查后执行
            System.out.println("lower > upper");
            return;
        }
        lower.set(i);
    }
                          
    public void setUpper(int i) {
     
        if (i < lower.get()) {
       //  不安全的 先检查后执行
            System.out.println("lower > upper");
            return;
        }
        upper.set(i);
    }
                       
    public boolean isInRange(int i) {
     
        return (i >= lower.get() && i <= upper.get());
    }
}

NumberRange 不是线程安全的,它没有维持对下界和上界进行约束的不变性条件。setLowersetUpper 都是“先检查后执行”,但他们都没有足够的加锁机制保证这些操作的原子性

因此,虽然 AtomicInteger 是线程安全的,但经过组合得到的类却不是线程安全的。

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

发布底层状态变量

当线程安全性委托给某个对象的底层状态变量时,在什么条件下才可以发布这些变量从而使其他类能修改它们? 答案仍然取决于在类中对这些变量施加了那些不变性条件。

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

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

线程安全且可变的 Point 类

public class SafePoint {
     
    private int x, y;
    
    public SafePoint(SafePoint sp) {
     
        this.x = sp.x;
        this.y = sp.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;
    }
}

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

public class PublishingVehicleTracker {
     
    private final Map<String, SafePoint> locations;
    private final Map<String, SafePoint> unmodifiableMap;
    
    public PublishingVehicleTracker(Map<String, SafePoint> locations) {
     
        this.locations = new ConcurrentHashMap<String, SafePoint>(locations);
        this.unmodifiableMap = Collections.unmodifiableMap(locations);
    }
    public Map<String, SafePoint> getLocations() {
     
        return unmodifiableMap;
    }
    public SafePoint getLocations(String id) {
     
        return locations.get(id);
    }
    public void setLocations(String id, int x, int y) {
     
        if (!locations.containsKey(id)) {
     
            throw new IllegalArgumentException("");
        }
        locations.get(id).set(x, y);
    }
}

在现有的线程安全类中添加功能

Java 类库包含许多有用的“基础模块”类。通常,我们应该优先选择重用这些现有的类而不是创建新的类:重用能降低开发工作量、开发风险以及维护成本。

有时候,某个现有的线程安全类能支持我们需要的所有操作,但更多时候,现有的类只能支持大部分的操作,此时就需要在不破坏线程安全性的情况下添加一个新操作。

扩展 Vector 并增加一个“若没有则添加”方法

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

“扩展”方法比直接将代码添加到类中更加脆弱,因为现在的同步策略实现被分布到多个单独维护的源代码文件中。如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类会被破坏,因为在同步策略改变后它无法再使用正确的锁来控制对基类状态的并发访问。

客户端加锁机制

错误示范:非线程安全的“若没有则添加”

public class ListHelper<E> {
           // 不要这样做
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    
    public synchronized boolean putIfAbsent(E e) {
     
        boolean absent = !list.contains(e);
        if (absent) list.add(e);
        return absent;
    }
}

为什么这种方式不能实现线程安全性?毕竟,putIfAbsent 已经声明为 synchronized 类型的变量,对不对?问题在于在错误的锁上进行了同步。无论List 使用哪一个锁来保护它的状态,可以确定的是,这个锁并不是 ListHelper 上的锁

要想使这个方法能正确执行,必须使List 在实现客户端加锁或外部加锁时使用同一个锁。

客户端加锁是指,对于使用某个对象X的客户端代码,使用X本身来作为用于保护其状态的锁来保护这段客户代码

通过客户端加锁来实现“若没有则添加”

public class ListHelper<E> {
     
    public List<E> list = Collections.synchronizedList(new ArrayList<E>());
    
    public boolean putIfAbsent(E e) {
     
        synchronized (list) {
     
            boolean absent = !list.contains(e);
            if (absent) list.add(e);
            return absent;
        }
    }
}

组合

当为现有的类添加一个原子操作时,有一种更好的方法:组合(Composition)。

通过组合实现“若没有则添加

public class ImprovedList<E> {
     
    private final List<E> list;
    
    public ImprovedList(List<E> list) {
     
        this.list = list;
    }
    public synchronized boolean putIfAbsent(E e) {
     
            boolean absent = !list.contains(e);
            if (absent) list.add(e);
            return absent;
    }
    public synchronized void clear() {
     
        list.clear();
    }
    // 按照类似的方式委托List的其他方法
}

将同步策略文档化

在维护线程安全性时,文档是最强大的(同时也是最未充分利用的)工具之一。

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

你可能感兴趣的:(Java,多线程,多线程,java)