不可变性(Immutability)在设计模式中是指一个对象在创建后其状态就不能改变。这是一种编程思想和设计原则。在某些情况下,使用不变对象可以带来许多好处:
-
简化代码 make things very simple:不可变对象在创建后状态不会改变,因此无需考虑对象状态的变化,这使得代码更简单,更容易理解和维护。
-
线程安全 Inherently thread-safe:不可变对象是线程安全的,因为它们不会在多个线程之间共享可变状态。这消除了同步和锁定的需要,提高了性能和可靠性。
-
支持共享 No risks in sharing:不可变对象可以被多个客户端安全地共享,因为它们不会修改共享的状态。这可以节省内存和计算资源。
-
降低错误风险:由于不可变对象的状态在创建后就不会发生变化,因此可以减少因状态改变而导致的错误,提高系统的稳定性。
有以下几种方式来创建immutability的对象:
- 将类的所有成员变量设为私有(private)和只读(final)。Make all fields final and private.
- 在构造函数中初始化所有成员变量,并确保它们在对象创建后不会改变。Ensure that no methods may be overridden.
- 不提供任何修改成员变量的方法(setter)。Don’t provide any mutators.
- 如果类包含可变对象的引用,确保在返回这些引用时创建它们的副本,以避免客户端代码修改内部状态。Ensure security of any mutable components.
举个例子,下面是一个不具备immutable的代码:
public class Complex {
double re, im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
public double getRealPart() { return re; }
public double getImaginaryPart() { return im; }
public double setRealPart(double re) { this.re = re; }
public double setImaginaryPart(double im) { this.im = im; }
进行immutable的修改后为:
public final class Complex {
private final double re, im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
// Getters without corresponding setters
public double getRealPart() { return re; }
public double getImaginaryPart() { return im; }
这里可能会有疑问,如果把set方法去掉了,怎么满足修改的需求呢?此时可以加一个修改方法只返回修改后的副本,以避免影响原来的对象,比如:
// subtract, multiply, divide similar to add
public Complex add(Complex c) {
return new Complex(re + c.re, im + c.im);
}
同理,如果非得写set方法,只要返回a new copy of an object即可。
不变性(Immutability)的局限:
immutability只是一种设计思想,并不是唯一的标准,因为一方面写immutable的代码有时会导致额外的内存分配和垃圾回收开销,从而降低程序的性能;另一方面因为每次修改都需要创建一个新对象,这可能导致许多相似的对象同时存在于内存中。最重要的是,但在需要频繁修改对象的状态情况下,使用可变对象会更加简洁和高效(比如要记录一个人的银行账户,难道每次用户每次交易都新建一个账户对象吗?显然此时要做的是最小化mutable的部分,对于mutable的部分做好线程保护),就不要非得写immutable的代码了。
其他保实现程安全的方法
正如上面所说,能写immutable的部分就写immutable,但如果不得不需要变化,我们就通过一些其他方式来实现线程安全。
首先来看一个“线程不安全”的例子:
@NotThreadSafe
public class UnsafeSequence {
private int value;
public int getNext() {
return value++;
}
}
这段代码不是线程安全的,因为在getNext()
方法中对value
变量进行自增操作(value++
)时可能发生竞争条件(race condition)。value++
操作实际上包含了三个步骤:
- 读取
value
的当前值。 - 将
value
的值加1。 - 将新的值写回
value
。
在多线程环境中,如果两个或多个线程同时执行getNext()
方法,这些步骤可能会交错进行,导致value
的更新丢失。
在这种情况下,我们明明通过两个线程分别给变量增加了1,应该一共加2,却最终只增加了1。对于这个特定的例子,无法仅通过使用immutability使其线程安全。这是因为UnsafeSequence
类的主要目的是产生一个递增的序列,这意味着它需要在内部维护一个可变的状态(在这里是value
变量)。不过,可以使用其他方法来使该代码线程安全,如同步或原子操作。
synchronized关键字
synchronized
关键字是Java中用于实现同步的一种机制,它用于确保在并发环境中,共享资源的访问和修改是互斥的。当一个线程正在执行一个被synchronized
关键字修饰的方法或代码块时,其他线程必须等待,直到当前线程完成对共享资源的操作。这有助于防止多个线程同时访问和修改共享资源,从而避免竞争条件和数据不一致的问题。下面是使用这个关键字解决上面线程不安全的例子:
public class SafeSequence {
private int value;
public synchronized int getNext() {
return value++;
}
}
对于synchronization我们也会分Coarse-grained粗粒度同步(锁住整个对象)和fine-grained细粒度同步(锁住部分资源),取决于实际情况,锁住的东西越多往往对性能影响会越大(会变慢)。
AtomicInteger
另一种替代方案是使用java.util.concurrent.atomic
包中的AtomicInteger
类,它提供了原子操作,可以确保自增操作的原子性:
import java.util.concurrent.atomic.AtomicInteger;
public class SafeSequence {
private AtomicInteger value = new AtomicInteger();
public int getNext() {
return value.getAndIncrement();
}
}
volatile
关键字
volatile
关键字比synchronized功能上弱一点,它提供了一种同步机制,但它并不提供互斥访问。换句话说,使用volatile
关键字可以确保变量的可见性,即当一个线程修改了volatile
变量的值后,其他线程可以立即看到这个变化。然而,它并不能确保原子性,即在并发环境下,多个线程仍然可以同时访问和修改volatile
变量,可能导致不一致的状态。没有synchronized强大。在这里volatile
关键字并不能解决上面的线程不安全的问题,仅仅只是提出来做介绍。
private static volatile boolean stopRequested;
Thread Confinement线程封闭
刚刚讲的volatile
和synchronized
是用于处理在多个线程间共享变量时的同步问题,而线程封闭则是直接通过避免在多个线程间共享变量来消除同步问题,直接从根本上解决问题。
以下是实现线程封闭的一些方法:
-
使用局部变量Local variables:局部变量仅在声明它们的方法中可见,因此它们天然地属于拥有该方法的线程。当方法调用结束时,局部变量会从栈上移除,不会影响其他线程。
-
防御性拷贝defense copying:当你需要将一个对象从一个线程传递到另一个线程时,可以创建该对象的拷贝,这样每个线程都有自己的副本,避免了多线程访问同一对象的问题。(这其实又回到了immutability上了。)
-
使用ThreadLocal(针对Java):ThreadLocal是一个特殊的Java类,它允许你为每个线程存储一个单独的值。当你需要在线程之间共享数据时,可以使用ThreadLocal来确保每个线程都有自己的私有副本。
-
适应其他编程语言的特性:
- JavaScript:由于JavaScript在单线程环境中运行,不需要考虑线程封闭问题。
- Python:Python在多线程和多进程之间进行了明确的区分。多进程无法共享状态,除非通过特殊对象来实现。
通过使用这些方法就可以实线线程封闭的效果,但可以发现,只要实现了immutability就大大帮助实现线程封闭了,Immutability Simplifies Thread Confinement!由于不可变对象的状态不会改变,因此它们在多线程环境中是天然线程安全的。一旦一个对象被创建,任何线程都可以安全地访问它,而无需担心其他线程对其进行修改。
小结:
这篇文章首先讲了immutability的优缺点和实现例子, 它的其中一个优点就是线程安全。但对于不适合写成immutable的变量来说,为了实现线程安全我们也可以采用线程封闭或者是通过如synchronized的关键字或其他同步机制(如显式锁)来确保对共享资源的访问是原子的和有序的。