前一章讲到要编写正确的并发程序,关键问题在于:在访问共享的可变状态时需要进行正确的管理。以前,我们知道如何通过同步来避免多个线程同一时刻访问相同的数据,本章我们将介绍如何发布共享和发布对象,从而使得它们能够安全的由多个线程访问。。。这两章结合一起,进行成了构建安全类以及通过java current包来构建并发应用程序的基础。
我们已经知道同步代码块以及同步方法可以保证原子操作,一种常见的误区就是:我们总觉得synchronized关键字只能确保原子性或者确定临界区,其实同步还有一个重要的方面:内存可见性。我们不仅希望防止某个线程正在使用对象而另一个线程同时修改状态,而且希望当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。。如果没有同步,那么这种情况就无法实现,你可以通过显式的同步或者类库中内置的同步来保证对象被安全的发布。。
一、可见性
可见性是一种复杂的属性,因为可见性的错误总是会违背我们的直觉,在单线程环境中,如果向某个变量先写入值,然后在没有其他写入操作的情况下读取这个变量,那么总能得到相同的值,这看起来很自然!然而,当读和写操作在不同的线程下执行时,情况却并非如此。。通常,我们无法保证执行读操作的线程能适时的看到其他线程写入的值。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
案例:
public class NoVisibility {
private static boolean ready;//默认值为false
private static int number;//默认值为0
private static class ReaderThread extends Thread {
public void run(){
while(!ready){
System.out.println(number);
}
}
}
public static void main(){
new ReaderThread().start();
number = 42;
ready = true;
}
}
上面案例中的NoVisibility说明了当多个线程没有同步的情况下共享数据时出现的错误。在代码中,主线程和读线程都将访问共享变量ready和number。主线程启动读线程,然后将number设为42,将ready设置为true。读线程一直循环直到读到的ready值为true。。虽然看起来结果会输出42,但事实上很可能输出0,或者循环根本无法终止。。这是因为在代码中没有使用足够的同步机制,因此无法保证主线程写入的ready值和number值对于读线程来说是可见的!
NoVisibility可能会持续执行下去,因为读线程可能永远都看不到ready的新值。一种更奇怪的现象是,NoVisibility可能会输出0,因为读线程可能看到了ready的值,但却没有看到之后写入的值,这种现象称为“重排序”。。
注:在没有同步的情况下,编译器和处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,想要对内存操作的执行顺序进行判断,几乎无法得出正确的结论。
1、失效数据
NoVisibility中展示了在缺乏同步的程序中可能产生错误结果的一种情况:失效的数据。。当读线程查看ready变量时,可能会得到一个已经失效的值。除非在每次访问变量的时候都使用同步,否则很可能获得该变量的失效值。更糟糕的是,失效值可能不会同时出现:一个线程可能得到某个变量的最新值,而获得另一个变量的失效值。。失效值可能会导致一些严重的安全问题或者活跃性问题。在NoVisibility中,失效的数据可能导致输出错误的值、或者程序无法结束。如果对象的引用(例如链表的指针)失效,那么情况就会非常复杂。。
案例:非线程安全的可变整数类
@NotThreadSafe
public class MutableInteger {
private int value;
public int get(){return value;}
public void set(int value){this.value = value;}
......
}
上面这个类不是线程安全的,因为get和set都是在没有同步的情况下访问value的。
通过对get set等方法进行同步,即可使之称为线程安全的类。。
案例:线程安全的可变整数类
@ThreadSafe
public class SynchronizedInteger {
@GuardedBy("this") private int value;
public synchronized int get(){return value;}
public synchronized void set(int value){this.value = value;}
}
2、非原子性的64位操作
当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机的值。这种安全性保证也被称之为最低安全性(out-of-thin-airsafety)。
最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位变量(double、long)。Java内存模型要求:变量的读取操作和写操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。。 当读取一个非volatile类型的long变量时,如果对该变量的读和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。除非使用volatile来声明它们,或者使用锁保护起来!
3、加锁与可见性
额...加锁很明显的,保证了原子性和可见性,就不多敲了。。
注:加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有的线程都能看到共享变量的最新值,所有执行读操作或者写操作都必须在同一个锁上同步。。
4、volatile变量
Java语言提供一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作到其他线程。当把变量声明为volatile类型后,编译器运行时都会注意到这个变量是共享的,因此不会将该变量的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此读取volatile类型的变量时总会返回最新写入的值。。然而,在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile是一种比sychronized关键字更轻量级的同步机制。
由于依赖volatile控制可见性会导致程序更脆弱和难以理解,所以,当且仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,就不要使用volatile变量!volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(如初始化或者关闭)。。
比如,检查某个状态标记以判断是否退出循环,在这个例子中,线程试图通过类似于数绵羊的传统方法进入休眠状态。为了使这个例子能正确执行,asleep必须为volatile变量,否则,当asleep被另一个线程修改时,执行判断的线程却发现不了。我们可以使用锁来确保asleep更新操作的可见性,但是这将使代码变得更加复杂。。如下:
volatile boolean asleep;
......
while(!asleep){
countSomeSheep();
}
虽然volatile变量很方便,但是也存在一些局限性。volatile变量通常用做某个操作完成、发生中断或者状态的标志。。尽管volatile变量可以表示其他的状态信息,但在使用时一定要非常小心。例如,volatile语义不足以确保递增操作(count++)的原子性,除非你能确保只有一个线程对变量执行写操作。。
注:加锁机制可以确保原子性和可见性,而volatile只能确保可见性
二、发布与溢出
“发布(publish)”一个对象的意思是指,使对象能够在当前作用域外的代码使用,如,将一个指向该对象的引用保存到其他代码可以访问的地方,或者在某一个非私有的方法中返回该引用,或者将引用传递到其它类的方法中。在许多情况下,我们要确保对象及其内部状态不被发布。而在某些情况下,我们又需要发布某个对象,但如果在发布时要确保线程安全性,则可能需要同步。发布内部状态可能会破坏封装性,并使得程序难以维持不变性条件,如,如果在对象构造完成之前就发布该对象,就会破坏线程安全性。当某个不应该发布的对象被发布时,称为“逸出”。
发布对象最简单的方法是将对象的引用保存到一个公有的静态变量中,以便任何类和线程都能看见该对象,如下:在initialize方法中实例化一个新的hashset对象,并将对象的引用保存到knownSrcrets中以发布该对象。
案例:
public static Set<Secret> knownSecrets;
public void initialize() {knownSecrets = new HashSet<Secret>();}
当发布一个对象时,可能会间接的发布其它对象。如果将一个Secret对象添加到knownSecrets中,那么同样会发布这个Secret对象,因为任何代码都会遍历这个集合,并获得对这个新Secret对象的引用。同样,如果从非私有方法中返回一个引用,那么同样会发布所返回的对象。如下:UnsafeStates发布了本应为私有的状态数组:
案例:
class UnsafeStates {
private String[] states = new String[] {
"AK","AL","AZ"........
};
public String[] getStates() {return states}
}
如果按照上述发布states,就会出现问题,因为任何调用着都能修改这个数组的内容。在此示例中数组states已经逸出了它所在的作用域,因为这个本应该是私有的变量已经被发布了。
当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。一般来说,如果一个已经发布的对象能够通过非私有的变量引用和方法到达其它对象,那么这些对象也都会被发布。
假定有一个类C,对于C来说,“外部(alien)方法”是指:行为并不完全由类C来规范的方法,包括其他类中定义的方法以及类c中可以被改写的方法(既不是私有方法,也不是final方法)。当把一个对象传递给某个外部方法时,就相当于发布了这个对象。你无法知道哪些代码会执行,也不知道在外部方法中究竟会发布这个对象还是会保留对象的引用并在随后由另一个线程使用。。。
无论其它线程会对已发布的对象执行何种操作,其实都不重要,因为误用该引用的风险始终存在。当某个对象逸出后,必须假设有某个类或线程可能会误用该对象。这正是需要使用封装的最主要原因:封装能够使得对程序的正确性进行分析变得可能,并使得无意中破坏约束条件变得更难。
还有一种发布对象或其内部状态的机制就是发布一个内部的类实例,如下案例中ThisEscape所示,当ThisEscape发布EventListener时,也隐含地发布了ThisEscape本身,因为在这个内部类的实例中包含了对ThisEscape的引用。。。,如下:
案例:
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener(){
public void onEvent(Event e){
doSomething(e);
}
});
}
}
1、安全对象的构造过程
在ThisEscape中给出了溢出的一个特殊示例,即this引用在构造函数中溢出。当内部的EventListener实例发布时,在外部封装的ThisEscape实例也溢出了。。当且仅当对象的构造函数返回时,对象才处于一个尚未构造完成的对象。即使发布对象的语句位于构造函数的最后一行也是如此。如果this引用在构造函数中溢出,那么这种对象被认为是不正确构造!
注:不要在构造过程中使this引用溢出
在构造函数中使this引用溢出的一个常见错误是,在构造函数中启动一个线程。当对象在其构造函数中创建一个线程时,无论是显式创建(通过将其传给构造函数)还是隐式创建(由于thread或runnable是该对象的一个内部类),,this引用都会被新创建的线程共享。在对象未完全构造之前,新的线程就可以看见它。在构造函数中创建线程并没有错误,但最好不要立即启动它,而是通过一个start或initialize方法来启动。在构造函数中调用一个可改写的实例方法时(既不是私有方法,也不是final方法),同样会导致this引用在构造过程中溢出。
如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法,从而避免不正确的构造过程,如下:
案例:
public class SafeListener {
private final EventListener listener;
private final SafeListener() {
listener = new EventListener(){
public void onEvent(Event e){
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
注:具体来说,只有当构造函数返回时,this引用才应该从线程中溢出。构造函数可以将this保存到某个地方,只要其它线程不会在构造函数完成之前使用它!
三、线程封闭
当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭(thread confinement),它是实现线程安全性的最简单方式之一。当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性,即使封闭的对象本身不是线程安全的!
在swing中大量使用了线程封闭技术。swing的可视化组件和视图模型对象都不是线程安全的swing通过将它们封闭到swing的事件分发线程中来实现线程安全性。要想正确的使用swing,那么除了事件线程以外的其它线程中就不能访问这些对象。呵呵...swing和我有毛的关系。
线程封闭的另一种常见的应用是:JDBC(java database connectivity)的connection对象。 JDBC规范不要求connection对象必须是线程安全的。。在典型的服务器应用程序中,线程从连接池中获得一个connection对象,并且使用该对象处理请求,使用完后再将对象返还给连接池。由于绝大多数请求(例如servlet请求活着ejb调用等)都是由单个线程采用同步来处理,并且在connection对象返回之前,连接池不会再将它分配给其它线程,因此,这种连接管理模式在处理请求时,隐含的将connection对象封闭在线程中。。。
我们并不强制规定某个变量必须由锁来保护,同样也没有强制将对象封闭在某个线程中。线程封闭是程序设计的一个考虑因素,必须在程序中实现。java语言及核心库提供了一些机制来维持线程封闭性,例如局部变量和threadlocal类,但即便如此,程序员仍需要负责确保封闭在线程中的对象不会从线程中溢出!!
1、Ad-hoc线程封闭
Ad-hoc线程封闭是指:维护线程封闭的职责完全由程序来承担。Ad-hoc线程封闭是很脆弱的,因为没有任何一种语言特性,例如可见性修饰符或者局部变量,能将对象封闭到目标线程上。。事实上,对于线程封闭对象(例如,gui应用程序中的可视化组件或数据模型等)的引用通常保存在共有变量中。
当决定使用线程封闭技术时,通常是因为要将某个特定的子系统实现为一个单线程子系统。在某些情况下,单线程子系统提供的简便性要胜过Ad-hoc线程封闭技术的脆弱性。
在volatile变量上存在一种特殊线程封闭。只要你能确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全的在这些共享的volatile变量上执行“读取-修改-写入”的操作。。在这种情况下,相当于将修改操作封闭在单个线程中以防止发生竞态条件,并且volatile变量的可见性还确保了其它线程能看到最新的值。
由于Ad-hoc线程封闭技术的脆弱性,因此在程序中尽量少用它,在可能的情况下,应使用更强的线程封闭技术(如栈封闭或threadlocal类)。
2、栈封闭
栈封闭是线程封闭的特例,在栈封闭中,只能通过局部变量才能访问对象。正如封装能使代码更容易维持不变形条件那样,同步变量也能使对象更易于封闭在线程中。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其它线程无法访问这个栈。栈封闭(也被称为线程内部使用或者线程局部使用,不要与类库中的threadlocal混淆)比Ad-hoc线程封闭更容易维护,也更加健壮。
对于基本类型的局部变量,如下案例中的loadTheArk方法的numPairs,无论如何都不会破坏栈封闭性,由于任何方法都无法获得对基本类型的引用,因此java语言的这种语义确保了基本类型的局部变量始终封闭在线程内。
案例:基本类型的局部变量与引用变量的线程封闭性
public int loadTheArk (Collection<Animal> candidates) {
SortSet<Animal> animals;
int numPairs = 0;
Animal candidate = null;
//animals被封装在方法中,不能使它们溢出!
animals = new TreeSet<Animals>(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;
}
}
}
在维持对象引用的栈封闭性时,程序员需要多做一些工作以确保被引用的对象不会溢出。在loadTheArk中实例化一个TreeSet对象,并将指向该对象的一个引用保存到animals中。此时,只有一个引用指向该集合,并且这个引用被封装在局部变量中。然而,如果发布了对集合animals(或该对象中的任何内部数据)的引用,那么封闭性将被破坏,并导致对象animals溢出。
如果在线程内部(within-thread)上下文中使用非线程安全的对象,那么该对象仍然是线程安全的。然而,要小心的是,只有编写代码的开发人员才知道哪些对象需要被封闭到执行线程中,以及被封闭的对象是否是线程安全的。如果没有明确的说明这些需求,那么后续的维护人员很容易使对象溢出。
3、ThreadLocal类
维持线程封闭性更规范的做法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,一次get总是返回由当前执行线程在调用set时设置的最新值。
ThreadLocal对象通常用于防止对可变的单实例(singleton)或全局变量进行共享。例如,在单线程应用中可能维护一个全局的数据库连接,并在程序启动时初始化这个连接对象,从而避免在调用每个方法时都要传递一个connection对象。。由于JDBC的连接对象不一定是线程安全的。通过对jdbc的连接保存到ThreadLocal对象中,,每个线程都会拥有属于自己的连接,如下案例:
案例:使用ThreadLocal来维持线程封闭性
private static ThreadLocal<Connection> connectionHolder =
new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection(){
return connectionHolder.get();
}‘
当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。。例如在java5.0之前,Integer.toString()方法使用ThreadLocal对象来保存一个12字节大小的缓冲区,用于对结果进行格式化,而不是使用共享的静态缓冲区(这需要使用锁机制)或者在每次调用时都分配一个新的缓冲区。。。当某个线程初次调用ThreadLocal.get()方法时,就会调用initialValue()来获取初始值。从概念上看,你可以将ThreadLocal<T>视为包含了Map<Thread,T>对象,其中保存了特定于该线程的值,当ThreadLocal的实现并非如此。这些特定于线程的值保存在Thread对象中,当线程终止后,这些值会被垃圾回收!
假设需要将一个单线程应用程序移植到多线程环境中,通过将共享的全局变量转换为ThreadLocal对象(如果全局变量的语义允许),可以维持线程安全性。然而,如果将应用程序范围内的缓存转换为线程局部的缓存,就不会与太大的作用!
在实现应用框架时大量使用了ThreadLocal。例如,在ejb调用期间,j2ee容器需要将一个事物上下文(transaction context)与某个执行中的线程关联起来。通过事物上下文保存在静态的ThreadLocal对象中,可以很容易的实现这个功能:当框架需要判断当前线程运行的是哪个事物时,只需要从这个ThreadLocal对象中读取事物上下文。这种机制很方便,因为它避免了在调用每个方法时都需要传递执行上下文信息,然而这也将使用该机制的代码与框架耦合在一起!
开发人员经常滥用ThreadLocal,例如将所有的全局变量都作为ThreadLocal对象,或者作为一种“隐藏”方法参数的手段。ThreadLocal变量类似于全局变量,它能降低代码的可充用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心!
四、不变性
前面一直在说可见性和原子性相关的问题,如得到失效数据、丢失更新操作或者观察到某个对象处于不一致的状态等,都与多线程访问一个可变状态有关。。。如果对象的状态不会改变,那这些问题也就消失了。
如果一个对象在被创建之后,其状态就不能被修改,那么这个对象就称为不可变性对象。线程安全性是不可变对象的固有属性之一,它们的不可变性条件是由构造函数创建的,只要它们的状态不改变,那么这些不可变性条件就能得到维持!
注:不可变对象一定是线程安全的。
虽然java语言规范和java内存模型都没有给出可变性的正确定义,但我们要知道,不可变性不等于将对象中的所有域都声明为final类型,即使对象中所有域都是final类型的,这个对象也是可变的,因为final类型的域中可以保存对可变对象的引用。
当满足一下条件,对象才是不可变的:
对象创建以后,其状态就不能修改
对象的所有域都是final类型
对象是正确创建的(在对象创建期间,this引用没有逸出)
1、final域
关键字final可以视为c++中const的受限版本,用于构造不可变对象。final修饰的域是不能修改的(但如果final所引用的对象是可变的,那么这些被引用的对象是可以修改的)。然而,在java内存模型中还有特殊的语义:final能确保初始化的安全性,从而可以不受限制的访问不可变对象,并在共享这些对象时无需同步。
除非需要更高的可见性,否则应将所有的域都声明为私有域。除非需要某个域是可变的,否则应将其声明为final域。
五、安全发布
到目前为止,我们讨论的是如何确保对象不被发布,例如让对象封闭在线程或者另一个对象的内部。当然,有时,我们希望在多个线程间共享对象,此时必须确保安全的进行共享。如果只是简单的将对象的引用保存到公有域中,还不足以安全的发布这个对象,如下:
案例:在没有足够同步的情况下发布对象(不要这么做)
public Holder holder;
public void initialize(){ holder = new Holder(42); }
你可能会奇怪,看似没有问题的示例何以会失败?由于存在可见性问题,其它线程看到的hodler对象将处于不一致的状态,即便在该对象的构造函数中已经明确地构建了不变性条件。这种不正确的发布导致其它线程看到尚未创建完成的对象!
1、不正确的发布:正确的对象被破坏
你不能指望一个尚未完全创建的对象拥有完整性。某个观察该对象的线程将看到对象处于不一致的状态,然后看到对象的状态突然发生变化,即使线程在对象发布后还没有修改过它。如下:不安全的发布方式,那么另一个线程在调用assertSanity时将抛出AssertionError。
案例:由于未正确发布,因此这个类可能出错
public class Holder {
private int n;
public Holder(int n) {this.n = n;}
public void assertSanity(){
if(n != n) throw new AssertionError("this statement is false.");
}
}
由于没有使用同步来确保Holder对象对其他线程可见,因此将holder对象称为“未正确发布的对象”。在未正确发布的对象中存在两个问题:首先,除了发布对象的线程以外,其它线程可以看到的holder域是一个失效值,因此将看到一个空引用或者之前的旧指(构造函数执行前会有个默认值)。然而,更糟糕的是,线程看到的holder引用是最新的,但holder状态的值却是实效的。情况变得更加不可预测的是:某个线程在第一次读取域时得到实效值,而再次读取这个域的值时会得到一个更新值,这也是抛出AssertionError的原因。
2、安全发布的常用模式
可变对象必须通过安全的方式来发布,这通常意味着在发布和使用该对象的线程时都必须使用同步。
要安全的发布一个对象,对象的引用以及对象的状态必须同时对其它线程可见。
一个正确构造的对象可以通过以下方式来安全的发布:
在静态初始化函数中初始化一个对象的引用,
将对象的引用保存到volatile类型的域或者AutomicReferance中。
将对象的引用保存到某个正确构造对象的final类型域中。
将对象的引用保存到一个由锁保护的域中。
在线程安全容器内部的同步,意味着,在将对象放入到某个线程同步的容器,例如Vector或synchronizedList时,将满足上述最后一个情况。javadoc提供了安全容器库,以保证安全的发布:
~:通过将一个键或者值放入Hashtable、sychronizedMap或者ConcurrentMap中,可以安全的将它发布给任何从这些容器访问它的线程(无论是直接访问还是迭代器访问)
~:通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList、synchronizedSet中,可以将该元素安全的发布到任何从这些容器中访问该元素的线程。
~:通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全的发布到任何从这些队列中访问该元素的线程。
通常,要发布一个静态构造的对象,最简单最安全的方式是使用静态的初始化器:
public static Holder holder = new Holder(42);
静态初始化器由JVM在类的初始化阶段执行,由于在JVM内部存在着同步机制,,因此通过这种方式初始化的任何对象都可以安全的发布。
3、事实不可变对象和可变对象
(1)事实不可变对象
如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象(effective immutable object)”。。在没有额外同步的情况下,任何线程都可以安全的使用被安全发布的事实不可变对象
(2)可变对象
如果对象在构造后可以修改,那么安全发布只能确保“发布当时”状态的可见性。对于可变对象,不仅在发布对象时需要使用同步,而且在每次对象访问时同样需要使用同步来确保修改操作的可见性。。要安全的共享可变对象,这些对象就必须被安全的发布,并且必须是线程安全的或者由某个锁保护起来。
对象的发布需求(条件)取决于它的可变性:
不可变对象可以通过任意机制来发布
事实不可变对象必须通过安全方式发布
可变对象必须通过安全方式发布,并且必须是线程安全的或者由某个锁保护起来
4、安全的共享对象
当获得一个对象的引用时,你需要知道在这个引用上可以执行哪些操作。在使用它之前是否需要获得一个锁?是否可以修改它的状态,或者只能读取它?许多并发错误都是没有理解共享对象的这些“既定规则”而导致的。当发布一个对象时,必须明确地说明对象的访问方式。。
在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
只读共享:在没有额外的同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的共有接口来进行访问,而不需要进一步的同步。
保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其它线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。