当前位置: 首页 > news >正文

建设网站要多久到账常平网站

建设网站要多久到账,常平网站,东莞主页网站制作,目前最火的大型网络游戏要编写正确的并发程序#xff0c;关键问题在于#xff1a;在访问共享的可变状态时需要进行正确的管理。第2章介绍了如何通过同步来避免多个线程在同一时刻访问相同的数据#xff0c;而本章将介绍如何共享和发布对象#xff0c;从而使它们能够安全地由多个线程同时访问。这两…要编写正确的并发程序关键问题在于在访问共享的可变状态时需要进行正确的管理。第2章介绍了如何通过同步来避免多个线程在同一时刻访问相同的数据而本章将介绍如何共享和发布对象从而使它们能够安全地由多个线程同时访问。这两章合在一起就形成了构建线程安全类以及通过java. util. concurrent 类库来构建并发应用程序的重要基础。 我们已经知道了同步代码块和同步方法可以确保以原子的方式执行操作但一种常见的误解是,认为关键字synchronized 只能用于实现原子性或者确定“临界区(Critical Section)”。同步还有另一个重要的方面内存可见性(Memory Visibility)。我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态而且希望确保当一个线程修改了对象状态后其他线程能够看到发生的状态变化。如果没有同步那么这种情况就无法实现。你可以通过显式的同步或者类库中内置的同步来保证对象被安全地发布。 3.1    可见性 可见性是一种复杂的属性因为可见性中的错误总是会违背我们的直觉。在单线程环境中如果向某个变量先写入值然后在没有其他写入操作的情况下读取这个变量那么总能得到相同的值。这看起来很自然。然而当读操作和写操作在不同的线程中执行时情况却并非如此这听起来或许有些难以接受。通常我们无法确保执行读操作的线程能适时地看到其他线程写入的值有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性必须使用同步机制。 在程序清单3-1中的NoVisibility 说明了当多个线程在没有同步的情况下共享数据时出现的错误。在代码中主线程和读线程都将访问共享变量ready 和number。主线程启动读线程然后将number 设为42,并将ready设为true。读线程一直循环直到发现ready 的值变为true,然后输出number的值。虽然NoVisibility 看起来会输出42但事实上很可能输出0或者根本无法终止。这是因为在代码中没有使用足够的同步机制因此无法保证主线程写入的ready值和number 值对于读线程来说是可见的。 private static class ReaderThread extends Thread { public void run(){ while (!ready). Thread. yield(); System. out. println(number); public static void main(String[]args}{ new ReaderThread(). start(); number 42; ready true; NoVisibility可能会持续循环下去因为读线程可能永远都看不到ready的值。一种更奇怪的现象是NoVisibility可能会输出0因为读线程可能看到了写入ready 的值但却没有看到之后写入number的值这种现象被称为“重排序(Reordering)”。只要在某个线程中无法检测到重排序情况(即使在其他线程中可以很明显地看到该线程中的重排序)那么就无法确保线程中的操作将按照程序中指定的顺序来执行。当主线程首先写入number然后在没有同步的情况下写入ready那么读线程看到的顺序可能与写入的顺序完全相反。 在没有同步的情况下编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中要想对内存操作的执行顺序进行判断几乎无法得出正确的结论。 No Visibility 是一个简单的并发程序只包含两个线程和两个共享变量但即便如此在判断程序的执行结果以及是否会结束时仍然很容易得出错误结论。要对那些缺乏足够同步的并发程序的执行情况进行推断是十分困难的。 这听起来有点恐怖但实际情况也确实如此。幸运的是有一种简单的方法能避免这些复杂的问题只要有数据在多个线程之间共享就使用正确的同步。 3.1.1    失效数据 NoVisibility 展示了在缺乏同步的程序中可能产生错误结果的一种情况失效数据。当读线程查看ready 变量时可能会得到一个已经失效的值。除非在每次访问变量时都使用同步否则很可能获得该变量的一个失效值。更糟糕的是失效值可能不会同时出现一个线程可能获得某个变量的最新值而获得另一个变量的失效值。 通常当食物过期(即失效)时还是可以食用的只不过味道差了一些。但失效的 这看上去似乎是一种失败的设计但却能使JVM充分地利用现代多核处理器的强大性能。例如在缺少同步的情况下 Java 内存模型允许编译器对操作顺序进行重排序并将数值缓存在寄存器中。此外它还允许CPU对操作顺序进行重排序并将数值缓存在处理器特定的缓存中。更多细节请参阅第16章。 数据可能导致更危险的情况。虽然在Web应用程序中失效的命中计数器可能不会导致太糟糕的情况但在其他情况中失效值可能会导致一些严重的安全问题或者活跃性问题。在NoVisibility中失效数据可能导致输出错误的值或者使程序无法结束。如果对象的引用.(例如链表中的指针)失效那么情况会更复杂。失效数据还可能导致一些令人困惑的故障例如意料之外的异常、被破坏的数据结构、不精确的计算以及无限循环等。 程序清单3-2 中的Mutablelnteger不是线程安全的,因为get 和set都是在没有同步的情况下访问value的。与其他问题相比失效值问题更容易出现如果某个线程调用了set那么另一个正在调用get的线程可能会看到更新后的value值也可能看不到。 public void set(int value){this. value value;} } 在程序清单3-3 的SynchronizedInteger中,通过对get 和set等方法进行同步,可以使MutableInteger 成为一个线程安全的类。仅对set 方法进行同步是不够的调用get的线程仍然会看见失效值。 public class SynchronizedInteger { GuardedBy(this) private int value; public synchronized int get(){return value;} public synchronized void set(int value){this. value value;} } 3.1.2    非原子的64位操作 当线程在没有同步的情况下读取变量时可能会得到一个失效值但至少这个值是由之前某个线程设置的值而不是一个随机值。这种安全性保证也被称为最低安全性(out-of-thin-air safety)。 最低安全性适用于绝大多数变量但是存在一个例外非volatile 类型的64位数值变量(double 和long请参见3.1.4节)。Java 内存模型要求变量的读取操作和写入操作都必须是原子操作,但对于非volatile 类型的long 和double变量,JVM允许将64位的读操作或写操作 在没有同步的情况下读取数据类似于在数据库中使用READ_UNCOMMITTED隔离级别在这种级别上将牺牲准确性以获取性能的提升。然而在非同步的读取操作中则牺牲了更多的准确度因为线程看到的共享变量值很容易失效。 分解为两个32位的操作。当读取一个非volatile 类型的long变量时如果对该变量的读操作和写操作在不同的线程中执行那么很可能会读取到某个值的高32位和另一个值的低32位。因此即使不考虑失效数据问题在多线程程序中使用共享且可变的long 和double等类型的变量也是不安全的除非用关键字volatile 来声明它们或者用锁保护起来。 3.1.3    加锁与可见性 内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果如图3-1所示。当线程A执行某个同步代码块时线程B随后进入由同一个锁保护的同步代码块在这种情况下可以保证在锁被释放之前A看到的变量值在B获得锁后同样可以由B看到。换句话说当线程B 执行由锁保护的同步代码块时可以看到线程A之前在同一个同步代码块中的所有操作结果。如果没有同步那么就无法实现上述保证。 现在我们可以进一步理解为什么在访问某个共享且可变的变量时要求所有线程在同一个锁上同步就是为了确保某个线程写入该变量的值对于其他线程来说都是可见的。否则如果一个线程在未持有正确锁的情况下读取某个变量那么读到的可能是一个失效值。 加核的含义不仅仅局限于五压行为还包括内存可易结、为了确保所有线程亦能增到共享变量的最新值所有执行读操作或者写操作的线程都必须在同一个锁上同步。 3.1.4    Volatile 变量 Java 语言提供了一种稍弱的同步机制即volatile变量用来确保将变量的更新操作通知          在编写Java 虚拟机规范时许多主流处理器架构还不能有效地提供64位数值的原子操作。 到其他线程。当把变量声明为volatile 类型后编译器与运行时都会注意到这个变量是共享的因此不会将该变量上的操作与其他内存操作一起重排序。volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方因此在读取volatile 类型的变量时总会返回最新写入的值。 理解volatile 变量的一种有效方法是,将它们的行为想象成程序清单3-3 中SynchronizedInteger 的类似行为并将volaLile 变量的读操作和写操作分别替换为get方法和set 方法。然而在访问volatile 变量时不会执行加锁操作因此也就不会使执行线程阻塞因此volatile 变量是一种比sychronized 关键字更轻量级的同步机制。 volatile 变量对可见性的影响比volatile 变量本身更为重要。当线程A 首先写入一个volatile 变量并且线程B随后读取该变量时在写入volatile 变量之前对A可见的所有变量的值在B读取了volatile 变量后对B也是可见的。因此从内存可见性的角度来看写入volatile 变量相当于退出同步代码块而读取volatile 变量就相当于进入同步代码块。然而我们并不建议过度依赖volatile 变量提供的可见性。如果在代码中依赖volatile 变量来控制状态的可见性通常比使用锁的代码更脆弱也更难以理解。 仅当volatile 变量能简化代码的实现以及对同步策略的验证时才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断那么就不要使用volatile变量。volatile 变量的正确使用方式包括确保它们自身状态的可见性确保它们所引用对象的状态的可见性以及标识一些重要的程序生命周期事件的发生(例如初始化或关闭)。 程序清单3-4 给出了volatile 变量的一种典型用法检查某个状态标记以判断是否退出循环。在这个示例中线程试图通过类似于数绵羊的传统方法进入休眠状态。为了使这个示例能正确执行, asleep 必须为volatile 变量。否则,当asleep 被另一个线程修改时,执行判断的线程却发现不了。我们也可以用锁来确保asleep 更新操作的可见性但这将使代码变得更加复杂。 volatile boolean asleep; while (!asleep) countSomeSheep(); ◎   这种类比并不准确,SynchronizedInteger在内存可见性上的作用比volatile 变量更强。请参见第16章。. ◎   在当前大多数处理器架构上读取volatile 变量的开销只比读取非volatile 变量的开销略高一些。 ②调试小提示对于服务器应用程序无论在开发阶段还是在测试阶段当启动JVM时一定都要指定-server 命令行选项。server模式的JVM 将比client模式的JVM 进行更多的优化例如将循环中未被修改的变量提升到循环外部因此在开发环境(client模式的JVM)中能正确运行的代码可能会在部署环境(server模式的JVM)中运行失败。例如如果在程序清单3-4中“忘记”把asleep 变量声明为volatile 类型那么server模式的JVM会将asleep 的判断条件提升到循环体外部(这将导致一个无限循环)但client模式的JVM不会这么做。在解决开发环境中出现无限循环问题时解决这个问题的开销远小于解决在应用环境出现无限循环的开销。 虽然volatile 变量很方便但也存在一些局限性。volatile 变量通常用做某个操作完成、发生中断或者状态的标志例如程序清单3-4中的asleep标志。尽管volatile变量也可以用于表示其他的状态信息但在使用时要非常小心。例如 volatile 的语义不足以确保递增操作(count)的原子性除非你能确保只有一个线程对变量执行写操作。(原子变量提供了“读-改-写”的原子操作并且常常用做一种“更好的volatile 变量”。请参见第15章)。 加锁机制既可以确保可见性又可以确保原子性而volatile变量只能确保可见性。 当且仅当满足以下所有条件时才应该使用volatile变量 ·对变量的写入操作不依赖变量的当前值或者你能确保只有单个线程更新变量的值。 ·该变量不会与其他状态变量一起纳入不变性条件中。 ·在访问变量时不需要加锁。 3.2    发布与逸出 “发布(Publish)”一个对象的意思是指使对象能够在当前作用域之外的代码中使用。例如将一个指向该对象的引用保存到其他代码可以访问的地方或者在某一个非私有的方法中返回该引用或者将引用传递到其他类的方法中。在许多情况中我们要确保对象及其内部状态不被发布。而在某些情况下我们又需要发布某个对象但如果在发布时要确保线程安全性则可能需要同步。发布内部状态可能会破坏封装性并使得程序难以维持不变性条件。例如如果在对象构造完成之前就发布该对象就会破坏线程安全性。当某个不应该发布的对象被发布时这种情况就被称为逸出(Escape)。3.5节介绍了如何安全发布对象的一些方法。现在我们首先来看看一个对象是如何逸出的。 发布对象的最简单方法是将对象的引用保存到一个公有的静态变量中以便任何类和线程都能看见该对象如程序清单3-5所示。在initialize 方法中实例化一个新的HashSet对象并将对象的引用保存到knownSecrets中以发布该对象。 public static SetSecretknownSecrets; public void initialize(){ knownSecrets new HashSetSecret(); } 当发布某个对象时可能会间接地发布其他对象。如果将一个Secret 对象添加到集合knownSecrets中那么同样会发布这个对象因为任何代码都可以遍历这个集合并获得对这个新Secret 对象的引用。同样如果从非私有方法中返回一个引用那么同样会发布返回的对象。程序清单3-6 中的UnsafeStates发布了本应为私有的状态数组。 public String[]getstack(){return states;} ﹞ 如果按照上述方式来发布states就会出现问题因为任何调用者都能修改这个数组的内容。在这个示例中数组states已经逸出了它所在的作用域因为这个本应是私有的变量已经被发布了。 当发布一个对象时在该对象的非私有域中引用的所有对象同样会被发布。一般来说如果一个已经发布的对象能够通过非私有的变量引用和方法调用到达其他的对象那么这些对象也都会被发布。 假定有一个类C对于C来说“外部(Alien)方法”是指行为并不完全由C来规定的方法包括其他类中定义的方法以及类C中可以被改写的方法(既不是私有[private]方法也不是终结[final]方法)。当把一个对象传递给某个外部方法时就相当于发布了这个对象。你无法知道哪些代码会执行也不知道在外部方法中究竟会发布这个对象还是会保留对象的引用并在随后由另一个线程使用。 无论其他的线程会对已发布的引用执行何种操作其实都不重要因为误用该引用的风险始终存在。当某个对象逸出后你必须假设有某个类或线程可能会误用该对象。这正是需要使用封装的最主要原因封装能够使得对程序的正确性进行分析变得可能并使得无意中破坏设计约束条件变得更难。 最后一种发布对象或其内部状态的机制就是发布一个内部的类实例如程序清单3-7的ThisEscape所示。当ThisEscape发布Even*Listener时,也隐含地发布了ThisEscape实例本身,因为在这个内部类的实例中包含了对ThisEscape实例的隐含引用。 public void onEvent(Event e){ doSomething(e); } } )  ; } } 如果有人窃取了你的密码并发布到alt. free-passwords 新闻组上那么你的信息将“逸出”无论是否有人会(或者尚未)恶意地使用这些个人信息你的账户都已经不再安全了。发布一个引用同样会带来类似的风险。 安全的对象构造过程 在ThisEscape 中给出了逸出的一个特殊示例即this 引用在构造函数中逸出。当内部的EventListener 实例发布时在外部封装的ThisEscape实例也逸出了。当且仅当对象的构造函数返回时对象才处于可预测的和一致的状态。因此当从对象的构造函数中发布对象时只是发布了一个尚未构造完成的对象。即使发布对象的语句位于构造函数的最后一行也是如此。如果this 引用在构造过程中逸出那么这种对象就被认为是不正确构造日。 不要在构造过程中使this 引用远出。 在构造过程中使this 引用逸出的一个常见错误是在构造函数中启动一个线程。当对象在其构造函数中创建一个线程时无论是显式创建(通过将它传给构造函数)还是隐式创建(   由于Thread 或Runnable 是该对象的一个内部类) this引用都会被新创建的线程共享。在对象尚未完全构造之前新的线程就可以看见它。在构造函数中创建线程并没有错误但最好不要立即启动它而是通过一个start 或initialize 方法来启动(请参见第7章了解更多关于服务生命周期的内容)。在构造函数中调用一个可改写的实例方法时(既不是私有方法也不是终结方法)同样会导致this 引用在构造过程中逸出。 如果想在构造函数中注册一个事件监听器或启动线程那么可以使用一个私有的构造函数和一个公共的工厂方法(Factory Method)从而避免不正确的构造过程如程序清单3-8中SafeListener所示。 public class SafeListener { private final EventListener listener; private 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引用保存到某个地方只要其他线程不会在构造函数完成之前使用它。在程序清单3-8 的SafeListener中就使用了这种技术。 3.3    线程封闭 当访问共享的可变数据时通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据就不需要同步。这种技术被称为线程封闭(Thread Confinement)它是实现线程安全性的最简单方式之一。当某个对象封闭在一个线程中时这种用法将自动实现线程安全性即使被封闭的对象本身不是线程安全的[CPJ 2.3.2]。 在Swing 中大量使用了线程封闭技术。Swing的可视化组件和数据模型对象都不是线程安全的 Swing 通过将它们封闭到Swing的事件分发线程中来实现线程安全性。要想正确地使用Swing那么在除了事件线程之外的其他线程中就不能访问这些对象(为了进一步简化对Swing.的使用, Swing 还提供了invokeLater机制,用于将一个Runnable 实例调度到事件线程中执行)。Swing 应用程序的许多并发错误都是由于错误地在另一个线程中使用了这些被封闭的对象。 线程封闭技术的另一种常见应用是JDBC(Java Database Connectivity)的Connection 对象。JDBC规范并不要求Connection对象必须是线程安全的。在典型的服务器应用程序中线程从连接池中获得一个Connection对象并且用该对象来处理请求使用完后再将对象返还给连接池。由于大多数请求(例如Servlet请求或EJB 调用等)都是由单个线程采用同步的方式来处理并且在Connection对象返回之前连接池不会再将它分配给其他线程因此这种连接管理模式在处理请求时隐含地将Connection 对象封闭在线程中。” 在Java 语言中并没有强制规定某个变量必须由锁来保护同样在Java 语言中也无法强制将对象封闭在某个线程中。线程封闭是在程序设计中的一个考虑因素必须在程序中实现。Java 语言及其核心库提供了一些机制来帮助维持线程封闭性例如局部变量和ThreadLocal类但即便如此程序员仍然需要负责确保封闭在线程中的对象不会从线程中逸出。 3.3.1    Ad-hoc线程封闭 Ad-hoc 线程封闭是指维护线程封闭性的职责完全由程序实现来承担。Ad-hoc线程封闭是非常脆弱的因为没有任何一种语言特性例如可见性修饰符或局部变量能将对象封闭到目标线程上。事实上对线程封闭对象(例如GUI应用程序中的可视化组件或数据模型等)的引用通常保存在公有变量中。 当决定使用线程封闭技术时通常是因为要将某个特定的子系统实现为一个单线程子系统。在某些情况下单线程子系统提供的简便性要胜过Ad-hoc线程封闭技术的脆弱性。 在volatile 变量上存在一种特殊的线程封闭。只要你能确保只有单个线程对共享的volatile 变量执行写入操作那么就可以安全地在这些共享的volatile 变量上执行“读取-修改-写入”的操作。在这种情况下相当于将修改操作封闭在单个线程中以防止发生竞态条件并且volatile 变量的可见性保证还确保了其他线程能看到最新的值。 ○应用程序服务器提供的连接池是线程安全的。连接池通常会由多个线程同时访问因此非线程安全的连接池是毫无意义的。 使用单线程子系统的另一个原因是为了避免死锁这也是大多数GUI框架都是单线程的原因。第9章将进一步介绍单线程子系统。 由于Ad-hoc 线程封闭技术的脆弱性因此在程序中尽量少用它在可能的情况下应该使用更强的线程封闭技术(例如栈封闭或ThreadLocal类)。 3.3.2    栈封闭 栈封闭是线程封闭的一种特例在栈封闭中只能通过局部变量才能访问对象。正如封装能使得代码更容易维持不变性条件那样同步变量也能使对象更易于封闭在线程中。局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中其他线程无法访问这个栈。栈封闭(也被称为线程内部使用或者线程局部使用不要与核心类库中的ThreadLocal混淆)比Ad-hoc线程封闭更易于维护也更加健壮。 对于基本类型的局部变量例如程序清单3-9 中loadTheArk方法的numPairs无论如何都不会破坏栈封闭性。由于任何方法都无法获得对基本类型的引用因此Java语言的这种语义就确保了基本类型的局部变量始终封闭在线程内。  public int loadTheArk(CollectionAnimalcandidates){ SortedSetAnimalanimals; int numPairs 0; Animal candidate null; //   animals被封闭在方法中,不要使它们逸出! animals new TreeSetAnimal(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 的逸出。 如果在线程内部(Within-Thread)上下文中使用非线程安全的对象那么该对象仍然是线程安全的。然而要小心的是只有编写代码的开发人员才知道哪些对象需要被封闭到执行线程中以及被封闭的对象是否是线程安全的。如果没有明确地说明这些需求那么后续的维护人员很容易错误地使对象逸出。 3.3.3    ThreadLocal类 维持线程封闭性的一种更规范方法是使用ThreadLocal这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal 提供了get 与set 等访问接口或方法这些方法为每个使用该变量的线程都存有一份独立的副本因此get总是返回由当前执行线程在调用set时设置的最新值。 ThreadLocal 对象通常用于防止对可变的单实例变量(Singleton)或全局变量进行共享。例如在单线程应用程序中可能会维持一个全局的数据库连接并在程序启动时初始化这个连接对象从而避免在调用每个方法时都要传递一个Connection对象。由于JDBC 的连接对象不一定是线程安全的因此当多线程应用程序在没有协同的情况下使用全局变量时就不是线程安全的。通过将JDBC的连接保存到ThreadLocal 对象中每个线程都会拥有属于自己的连接,如程序清单3-10中的ConnectionHolder所示。 private static ThreadLocalConnectionconnectionHolder new ThreadLocalConnection(){ public Connection initialValue(){ }  ; public static Connection_getConnection(   ){ return connectionHolder. get(); } 当某个频繁执行的操作需要一个临时对象例如一个缓冲区而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。例如,在Java 5.0之前, Integer. toString()方法使用ThreadLocal 对象来保存一个12字节大小的缓冲区用于对结果进行格式化而不是使用共享的静态缓冲区(这需要使用锁机制)或者在每次调用时都分配一个新的缓冲区。 当某个线程初次调用ThreadLocal. get方法时,就会调用initialValue 来获取初始值。从概念上看,你可以将ThreadLocalT视为包含了MapThread,T对象,其中保存了特定于该线程的值但ThreadLocal的实现并非如此。这些特定于线程的值保存在Thread 对象中当线程终止后这些值会作为垃圾回收。 假设你需要将一个单线程应用程序移植到多线程环境中通过将共享的全局变量转换为ThreadLocal 对象(如果全局变量的语义允许)可以维持线程安全性。然而如果将应用程序范围内的缓存转换为线程局部的缓存就不会有太大作用。 在实现应用程序框架时大量使用了ThreadLocal。例如在EJB 调用期间J2EE 容器需要将一个事务上下文(Transaction Context)与某个执行中的线程关联起来。通过将事务上下文保存在静态的ThreadLocal对象中可以很容易地实现这个功能当框架代码需要判断当前运行的是哪          除非这个操作的执行频率非常高或者分配操作的开销非常高否则这项技术不可能带来性能提升。在Java 5.0中这项技术被一种更直接的方式替代即在每次调用时分配一个新的缓冲区对于像临时缓冲区这种简单的对象该技术并没有什么性能优势。 一个事务时只需从这个ThreadLocal对象中读取事务上下文。这种机制很方便因为它避免了在调用每个方法时都要传递执行上下文信息然而这也将使用该机制的代码与框架耦合在一起。 开发人员经常滥用ThreadLocal例如将所有全局变量都作为ThreadLocal对象或者作为一种“隐藏”方法参数的手段。ThreadLocal变量类似于全局变量它能降低代码的可重用性并在类之间引入隐含的耦合性因此在使用时要格外小心。 3.4    不变性 满足同步需求的另一种方法是使用不可变对象(Immutable Object)[EJ Item 13]。到目前为止我们介绍了许多与原子性和可见性相关的问题例如得到失效数据丢失更新操作或者观察到某个对象处于不一致的状态等等都与多线程试图同时访问同一个可变的状态相关。如果对象的状态不会改变那么这些问题与复杂性也就自然消失了。 如果某个对象在被创建后其状态就不能被修改那么这个对象就称为不可变对象。线程安全性是不可变对象的固有属性之一它们的不变性条件是由构造函数创建的只要它们的状态不改变那么这些不变性条件就能得以维持。 不可变对象一定是线程安全的。 不可变对象很简单。它们只有一种状态并且该状态由构造函数来控制。在程序设计中一个最困难的地方就是判断复杂对象的可能状态。然而判断不可变对象的状态却很简单。 同样不可变对象也更加安全。如果将一个可变对象传递给不可信的代码或者将该对象发布到不可信代码可以访问它的地方那么就很危险——不可信代码会改变它们的状态更糟的是在代码中将保留一个对该对象的引用并稍后在其他线程中修改对象的状态。另一方面不可变对象不会像这样被恶意代码或者有问题的代码破坏因此可以安全地共享和发布这些对象,而无须创建保护性的副本[EJ Item 24]。 虽然在Java 语言规范和Java 内存模型中都没有给出不可变性的正式定义但不可变性并不等于将对象中所有的域都声明为final 类型即使对象中所有的域都是final 类型的这个对象也仍然是可变的因为在final类型的域中可以保存对可变对象的引用。 当满足以下条件时对象才是不可变的 ·对象创建以后其状态就不能修改。 ·对象的所有域都是final类型目。 ·对象是正确创建的(在对象的创建期间 this引用没有逸出)。 -从技术上来看不可变对象并不需要将其所有的域都声明为final类型例如String就是这种情况这就要对类的良性数据竞争(Benign Data Race)情况做精确分析因此需要深入理解Java 内存模型。(注意String 会将散列值的计算推迟到第一次调用hash Code时进行并将计算得到的散列值缓存到非final类型的城中但这种方式之所以可行是因为这个城有一个非默认的值并且在每次计算中都得到相同的结果[因为基于一个不可变的状态]。自己在编写代码时不要这么做。) 在不可变对象的内部仍可以使用可变对象来管理它们的状态如程序清单3-11中的ThreeStooges 所示。尽管保存姓名的Set对象是可变的,但从ThreeStooges的设计中可以看到,在Set 对象构造完成后无法对其进行修改。stooges 是一个final类型的引用变量因此所有的对象状态都通过一个final 域来访问。最后一个要求是“正确地构造对象”这个要求很容易满足因为构造函数能使该引用由除了构造函数及其调用者之外的代码来访问。             public final class ThreeStooges { private final SetStringstooges new HashSetString(); public ThreeStooges(){ stooges. add(Moe); stooges. add(Larry); stooges. add(Curly); } public boolean isStooge(String name){ return stooges. contains(name); } } 由于程序的状态总在不断地变化你可能会认为需要使用不可变对象的地方不多但实际情况并非如此。在“不可变的对象”与“不可变的对象引用”之间存在着差异。保存在不可变对象中的程序状态仍然可以更新即通过将一个保存新状态的实例来“替换”原有的不可变对象。下一节将给出使用这项技术的示例。 3.4.1    Final 域 关键字final 可以视为C中const机制的一种受限版本用于构造不可变性对象。final 类型的域是不能修改的(但如果final 域所引用的对象是可变的那么这些被引用的对象是可以修改的)。然而在Java 内存模型中 final 域还有着特殊的语义。final 域能确保初始化过程的安全性从而可以不受限制地访问不可变对象并在共享这些对象时无须同步。 即使对象是可变的通过将对象的某些域声明为final 类型仍然可以简化对状态的判断因此限制对象的可变性也就相当于限制了该对象可能的状态集合。仅包含一个或两个可变状态的“基本不可变”对象仍然比包含多个可变状态的对象简单。通过将域声明为final 类型也相当于告诉维护人员这些域是不会变化的。 许多开发人员都担心这种方法会带来性能问题但这是没有必要的。内存分配的开销比你想象的还要低并且不可变对象还会带来其他的性能优势例如减少了对加锁或者保护性副本的需求以及降低对基于“代”的垃圾收集机制的影响。 正如“除非需要更高的可见性否则应将所有的域都声明为私有域”[E]ltem 12]是一个良好的编程习惯。除非需要某个成是可变的否则应将其声明为final城”也是一个良好的编程习惯。 3.4.2    示例:使用Volatile 类型来发布不可变对象 在前面的UnsafeCachingFactorizer类中,我们尝试用两个AtomicReferences 变量来保存最新的数值及其因数分解结果但这种方式并非是线程安全的因为我们无法以原子方式来同时读取或更新这两个相关的值。同样用volatile 类型的变量来保存这些值也不是线程安全的。然而在某些情况下不可变对象能提供一种弱形式的原子性。 因式分解Servlet将执行两个原子操作更新缓存的结果以及通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的因数分解结果。每当需要对一组相关数据以原子方式执行某个操作时就可以考虑创建一个不可变的类来包含这些数据例如程序清单3-12 中的OneValueCache⊖。 class OneValueCache { private final BigInteger lastNumber; private final BigInteger[]lastFactors; public OneValueCache(BigInteger i, BigInteger[]factors){ lastNumber i; lastFactors Arrays. copyOf(factors, factors. length); } public BigInteger[]getFactors(BigInteger i){ if (lastNumber null||!lastNumber. equals(i)) return null; else return Arrays. copyOf(lastFactors,lastFactors. length); } } 对于在访问和更新多个相关变量时出现的竞争条件问题可以通过将这些变量全部保存在一个不可变对象中来消除。如果是一个可变的对象那么就必须使用锁来确保原子性。如果是一个不可变对象那么当线程获得了对该对象的引用后就不必担心另一个线程会修改对象的状态。如果要更新这些变量那么可以创建一个新的容器对象但其他使用原有对象的线程仍然会看到对象处于一致的状态。 如果在OneValueCache和构造函数中没有调用copyOf,那么OneValueCache 就不是不可变的。Arrays. copyOf 是在Java 6 中引入的,同样还可以使用clone。 程序清单3-13 中的VolatileCachedFactorizer 使用了OneValueCache来保存缓存的数值及其因数。当一个线程将volatile 类型的cache设置为引用一个新的OneValueCache时,其他线程就会立即看到新缓存的数据。 程序清单3-13使用指向不可变容器对象的 volatile 类型引用以缓存最新的结果                ThreadSafe public class VolatileCachedFactorizer implements Servlet{ private volatile OneValueCache cache new OneValueCache(null, null); public void service (ServletRequest req,ServletResponse resp){ BigInteger i extractFromRequest(req); BigInteger[]factors cache. getFactors(i); if (factors null){ factors factor(i); cachenew OneValueCache(i, factors); } encodeIntoResponse(resp, factors); } } 与cache相关的操作不会相互干扰因为OneValueCache是不可变的并且在每条相应的代码路径中只会访问它一次。通过使用包含多个状态变量的容器对象来维持不变性条件并使用一个volatile 类型的引用来确保可见性,使得Vołatile Cached Factorizer 在没有显式地使用锁的情况下仍然是线程安全的。 3.5    安全发布 到目前为止我们重点讨论的是如何确保对象不被发布例如让对象封闭在线程或另一个对象的内部。当然在某些情况下我们希望在多个线程间共享对象此时必须确保安全地进行共享。然而如果只是像程序清单3-14 那样将对象引用保存到公有域中那么还不足以安全地发布这个对象。 }holder new Holder(42); 你可能会奇怪这个看似没有问题的示例何以会运行失败。由于存在可见性问题其他线程看到的Holder 对象将处于不一致的状态即便在该对象的构造函数中已经正确地构建了不变性条件。这种不正确的发布导致其他线程看到尚未创建完成的对象。 3.5.1    不正确的发布正确的对象被破坏 你不能指望一个尚未被完全创建的对象拥有完整性。某个观察该对象的线程将看到对象处于不一致的状态然后看到对象的状态突然发生变化即使线程在对象发布后还没有修改过它。事实上如果程序清单3-15中的Holder 使用程序清单3-14 中的不安全发布方式那么另一个线程在调用assertSanity时将抛出AssertionError。 public void assertSanity(){ if    ( n   !  n) throw new AssertionError(This statement is false.); } } 由于没有使用同步来确保Holder对象对其他线程可见因此将Holder称为“未被正确发布”。在未被正确发布的对象中存在两个问题。首先除了发布对象的线程外其他线程可以看到的Holder 域是一个失效值因此将看到一个空引用或者之前的旧值。然而更糟糕的情况是线程看到Holder 引用的值是最新的但Holder 状态的值却是失效的。情况变得更加不可预测的是某个线程在第一次读取域时得到失效值而再次读取这个域时会得到一个更新值这也是assertSainty抛出AssertionError的原因。 如果没有足够的同步那么当在多个线程间共享数据时将发生一些非常奇怪的事情。 3.5.2    不可变对象与初始化安全性 由于不可变对象是一种非常重要的对象因此Java 内存模型为不可变对象的共享提供了一种特殊的初始化安全性保证。我们已经知道即使某个对象的引用对其他线程是可见的也并不意味着对象状态对于使用该对象的线程来说一定是可见的。为了确保对象状态能呈现出一致的视图就必须使用同步。 另一方面即使在发布不可变对象的引用时没有使用同步也仍然可以安全地访问该对象。为了维持这种初始化安全性的保证必须满足不可变性的所有需求状态不可修改所有域都是final类型以及正确的构造过程。(如果程序清单3-15中的Holder 对象是不可变的那么即使Holder 没有被正确地发布,在assertSanity 中也不会抛出AssertionError。) ⊙   题并不在于Holder 类本身而是在于Holder 类未被正确地发布。然而如果将n声明为final 类型那么Holder将不可变从而避免出现不正确发布的问题。请参见3.5.2节。 ②尽管在构造函数中设置的域值似乎是第一次向这些域中写入的值因此不会有“更旧的”值被视为失效值但Object 的构造函数会在子类构造函数运行之前先将默认值写入所有的域。因此某个域的默认值可能被视为失效值。 任何线程都可以在不需要额外同步的情况下安全地访问不可变对象即使在发布这些对象时没有使用同步。 这种保证还将延伸到被正确创建对象中所有final 类型的域。在没有额外同步的情况下也可以安全地访问final 类型的域。然而如果final类型的域所指向的是可变对象那么在访问这些域所指向的对象的状态时仍然需要同步。 3.5.3    安全发布的常用模式 可变对象必须通过安全的方式来发布这通常意味着在发布和使用该对象的线程时都必须使用同步。现在我们将重点介绍如何确保使用对象的线程能够看到该对象处于已发布的状态并稍后介绍如何在对象发布后对其可见性进行修改。 要安全地发布一个对象对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布 ·在静态初始化函数中初始化一个对象引用。 ·将对象的引用保存到volatile 类型的域或者AtomicReferance对象中。 ·将对象的引用保存到某个正确构造对象的final 类型域中。 ·将对象的引用保存到一个由锁保护的域中。 在线程安全容器内部的同步意味着在将对象放入到某个容器例如Vector 或synchronizedList 时将满足上述最后一条需求。如果线程A将对象X放入一个线程安全的容器随后线程B 读取这个对象那么可以确保B看到A设置的X状态即便在这段读/写X的应用程序代码中没有包含显式的同步。尽管Javadoc在这个主题上没有给出很清晰的说明但线程安全库中的容器类提供了以下的安全发布保证 ·通过将一个键或者值放入Hashtable、synchronizedMap 或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)。 ·通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或synchronizedSet中可以将该元素安全地发布到任何从这些容器中访问该元素的线程。 ·通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。 类库中的其他数据传递机制(例如Future 和Exchanger)同样能实现安全发布在介绍这些机制时将讨论它们的安全发布功能。 通常要发布一个静态构造的对象最简单和最安全的方式是使用静态的初始化器 public static Holder holder new Holder(42); 静态初始化器由JVM在类的初始化阶段执行。由于在JVM 内部存在着同步机制因此通过这种方式初始化的任何对象都可以被安全地发布[JLS 12.4.2]。 3.5.4   事实不可变对象 如果对象在发布后不会被修改那么对于其他在没有额外同步的情况下安全地访问这些对象的线程来说安全发布是足够的。所有的安全发布机制都能确保当对象的引用对所有访问该对象的线程可见时对象发布时的状态对于所有线程也将是可见的并且如果对象状态不会再改变那么就足以确保任何访问都是安全的。 如果对象从技术上来看是可变的但其状态在发布后不会再改变那么把这种对象称为“事实不可变对象(Effectively Immutable Object)”。这些对象不需要满足3.4节中提出的不可变性的严格定义。在这些对象发布后程序只需将它们视为不可变对象即可。通过使用事实不可变对象不仅可以简化开发过程而且还能由于减少了同步而提高性能。 在没有额外的同步的情况下任何线程都可以安全地使用被安全发布的事实不可变对象。 例如 Date 本身是可变的但如果将它作为不可变对象来使用那么在多个线程之间共享Date 对象时就可以省去对锁的使用。假设需要维护一个Map 对象其中保存了每位用户的最近登录时间 public MapString, DatelastLogin Collections. synchronizedMap(new HashMapString, Date()); 如果Date 对象的值在被放入Map 后就不会改变那么synchronizedMap中的同步机制就足以使Date 值被安全地发布并且在访问这些Date 值时不需要额外的同步。 3.5.5    可变对象 如果对象在构造后可以修改那么安全发布只能确保“发布当时”状态的可见性。对于可变对象不仅在发布对象时需要使用同步而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。要安全地共享可变对象这些对象就必须被安全地发布并且必须是线程安全的或者由某个锁保护起来。 对象的发布需求取决于它的可变性 ·不可变对象可以通过任意机制来发布。 ·事实不可变对象必须通过安全方式来发布。 ·可变对象必须通过安全方式来发布并且必须是线程安全的或者由某个锁保护起来。 3.5.6    安全地共享对象 当获得对象的一个引用时你需要知道在这个引用上可以执行哪些操作。在使用它之前是否需要获得一个锁是否可以修改它的状态或者只能读取它许多并发错误都是由于没有理 这或许是类库设计中的一个错误。 解共享对象的这些“既定规则”而导致的。当发布一个对象时必须明确地说明对象的访问方式。 在并发程序中使用和共享对象时可以使用一些实用的策略包括 线程封闭。线程封闭的对象只能由一个线程拥有对象被封闭在该线程中并且只能由这个线程修改。 只读共享。在没有额外同步的情况下共享的只读对象可以由多个线程并发访问但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。 线程安全共享。线程安全的对象在其内部实现同步因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。 保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象以及已发布的并且由某个特定锁保护的对象。
http://www.dnsts.com.cn/news/104200.html

相关文章:

  • 网站运营适合什么样的人做企业宣传片制作拍摄电话
  • WordPress node企业为什么做网站优化推广
  • 网址查询站长工具做网站常用图标
  • 小说网站80电子书怎么做换服务器wordpress升级
  • 顺德网站建设公司有哪些郑州模板网站制作
  • 公司网站优化哪家好网址广告
  • 江门市网站建设帮别人做视频剪辑的网站
  • 网站网店建设wordpress404错误
  • 汉字叔叔花了多少钱做网站赶集招聘网
  • 免费建站系统wordpress电子商务论文3000字
  • 郑州响应式网站建设广告公司网站建设方案
  • 广州在建火车站在哪里网络推广培训学校
  • 手机网站 app网站开发外包项目网站
  • 泉州专门做网站网站推广排名怎么做
  • 设计类网站策划书睡不着来个网址2022
  • 淮安制作网站在那里网站采编队伍建设
  • 做自媒体搬运文章的网站vs2010 网站开发
  • 网站首页 栏目页 内容页行业类网站模板
  • 网站开发 技术方案软件开发app开发定制外包11
  • 大丰企业做网站多少钱南京cms建站系统
  • 做卖衣服网站源代码网站建设网站网页模板
  • 绘制网站结构图网页设计作业制作个人网站
  • 佳木斯网站建设公司把网站内容全删掉 在重新建立会不会被k
  • 白酒网站设计杭州网站设计询问蓝韵网络
  • 泉州市住房与城乡建设局网站做淘宝客网站性质
  • 网站 首页 关键词个人无网站怎样做cps广告
  • 外贸建站公司中装建设千股千评
  • 岳阳网站开发网站运营网站 建设 基本 数据库
  • 东莞外贸网站建设公司微博的网站连接是怎么做的
  • 做网站linux主机国外网站建设软件有哪些方面