前面提到过,将对象限制在线程中,可以达到线程安全的目的。当然,这的确是一个解决共享对象线程安全性的一个方案。接下来,介绍另一种方案。
不可变对象
我们前面学到过,保证线程安全的两个核心要点就是操作原子性和内存可见性的问题处理。而这两方面导致的问题就是获取到一个处于不一致状态的变量或者得到过期数据。因此,自然而然地就想到了另外一种方案。当我们的对象是不可变对象的时候,那岂不是不会发生这些问题?不可变对象及其引用,在何时何地都是线程安全的。因此,我们可以放心的进行发布。
所谓“发布”,在并发编程实践一书中有专门的讲解,也即是将创建的对象在外部供其他客户端使用。也就是说,我们发布过的对象有可能在其他任何地方使用。
而不可变对象即使发布,仍然是不可变的。也就是只有读操作而没有写权限。这自然而然的就是线程安全的了。而什么是不可变对象呢?其实,并不是简单的将变量设置为final类型的即可,而且还需要被正确的创建。当然了,在一个拥有多个属性的对象中,要尽可能的使用final来修饰不可更改的属性,这样有助于帮助我们判断一个对象的“值域”。而且,基于不可变对象的重要性,Java存储模型为共享不可变对象提供了特殊的初始化安全性的保证,也就是说,即使发布时没有同步操作,不可变对象仍然可以被安全的访问。
可变对象
如果我们想要发布可变对象,要怎么办呢?这就要考虑我们前面所说的操作原子性和内存可见性的问题了。
当然了,在强大的Java工具集中,自然提供了一些用于安全发布的容器工具。当我们使用这些工具时,无需考虑操作原子性和内存可见性问题时,依然可以安全的发布我们的对象。
例如有目前很少使用的Hashtable,对Hashtable进行优化之后的ConcurrentMap等等;还有Vector,CopyOnWriteArrayList,CopyOnWrite:写时复制,是JUC包引入的一个特别重要的特性,即当我们进行写操作时,才会创建一个新的副本。这个以后有机会,会详细介绍。以及用作队列的BlockingQueue等等,后面的这些都是JUC包中的关键点。
还有一点,是在业务设计中的不可变对象,这被称作为“高效不可变对象”。所谓高效不可变对象,就是一个对象,即使在技术上可以实现对该对象的更新,但是我们在将它发布之后不会再更新它,这就是高效不可变对象。这需要一个详细的文档,供客户端使用。
封装可以轻松的检验线程安全性
好了,线程安全与同步的基础知识已经差不多了。我们知道,将所有的变量都放在公共静态方法中,可以写出线程安全的程序。但是,我们如何验证线程安全程序呢?或者说,随着程序的扩展,我们如何验证新的程序的线程安全性呢?
自然而然地,我们就考虑到了面向对象的三大特性之一----封装。封装可以轻松的验证类的线程安全性。
首先,我们来看对象的状态包括哪些。如果对象的属性全部是由基本属性(设为n)组成的,那么这个对象的状态的的域值就是n元组。如果一个对象引用了其他的对象,那么被引用对象的状态也被包含在这个对象中了。
在一个对象中,为其设置一个同步策略是非常必要的。所谓同步策略,即是对象如何协调对它的各个属性的访问,并且不会违反对应类的不变约束与方法的后验条件。相应的不变约束与后验条件,其实也就是该类的需求。当我们了解了一个对象的各个属性对应的业务含义之后,才能正确设计它的同步策略。
方法的先验条件
在众多的方法中,有些方法需要有先验条件,例如我们不能在一个为空的对象中获取他的属性信息等。如果这个方法有先验条件,那么我们称它是状态依赖的。如果在单线程中,在一个为空的对象中获取它的信息肯定是会报错的。但是,如果在多线程环境中,这个对象可能会因为其他的线程的操作而被赋予了一个值。因此,这个操作就可以继续下去了。
看到这些,我们就自然地会想到wait和notify。但是,如果我们想等待一个信号再进行操作的话,我们可以使用JUC包中的阻塞队列,就是刚刚提到的线程安全的阻塞队列BlockingQueue或者信号量Semaphore等方式。接下来我们会继续学习对象的状态。
下面,有一个消息队列BlockingQueue中的实现类ArrayBlockingQueue的简单用法。
阻塞队列的简单使用
/**
* @Author: huangpf
* @Date: 2021/8/15
* @Des: 队列的简单使用
*/
public class BlockQueueDemo {
public static void main(String[] args) {
ArrayBlockingQueue queue = new ArrayBlockingQueue(5); //创建一个容量为5的队列数组,该类是BlockingQueue接口的一个实现类
queue.add("hello world");//往队列中加入数据
System.out.println("queue size:" + queue.size());//获取队列的长度 我们加入了一条数据,则该值为1
System.out.println("queue poll:" + queue.poll());//弹出队列的最后一个值
System.out.println("queue size:" + queue.size());//弹出队列的值,则该队列是空的,所以值为0
}
}