《Java并发编程实战》学习笔记 线程安全、共享对象和组合对象

第一章 介绍

线程的优势:
充分利用多处理器
简化模型
简化异步事件的处理
提供用户界面的响应(时间)
线程的风险:
安全的风险(不好的事情会发生),提高错误出现的几率
活性的风险(好的事情不会发生),如某些代码不会执行,出现死锁、活锁以及饥饿
性能的风险,不好的多线程编程可能会危害性能

第二章 线程安全

编写线程安全的代码,实质是管理对状态的访问,尤其是那些共享、可变的状态。对象的状态包括任何能影响它外部可见行为的数据。
当有过个线程能访问状态变量时,而且他们当中能对变量进行修改,则需要对他们进行同步管理。
在Java中实现同步的方式有:使用synchronized关键字,使用volatile变量,使用锁,使用原子变量。
在没有正确同步的情况下,如果多个线程访问了同一个可变变量,你的程序就存在隐患,有三种方法修复它:
把变量变为非共享
使变量变为不可变
使用合适的同步机制
一开始就将类设计为线程安全的,比之后修复它更简单
好的封装措施可以更简单的使我们的程序线程安全,同时有助于维护。因为封装后,外面的代码无法访问它的状态变量,我们只需要保存该对象本身时线程安全的就行。这对大型项目尤其重要。
不能为了些许的性能提升而损害代码的线程安全。因为这更得不偿失。
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。
线程安全的类封装了任何必要的同步,因此客户不需要自己提供。
一个类是无状态的,是指它既没有自己的状态域,也没有引用其他类的域。无状态对象永远是线程安全的。
竞争条件:当计算的正确性依赖于“幸运”的时序,会产生竞争条件
数据竞争:访问共享数据时没有采用同步措施,也就是多个线程会“不会控制”的使用数据
惰性初始化的目的是延迟对象的初始化,直到程序真正使用它,同时确保它只初始化一次。
假设操作A和B,如果从执行A的线程的角度看,当其他线程执行B时,要么B全部执行完成,要么一点都没有执行,这样A和B互为原子操作。一个原子操作是指:该操作对于所有的操作,包括它自己,都满足前面描述的状态。
为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量。
Synchronized方法包括两部分:一个对象的引用,充当的是锁的角色;该锁保护的代码段。Synchronized关键字充当锁的对象就是方法本身,也就是this关键字。
可重入锁的请求是基于“每个线程”,而不是“每次调用”。
一种常见得锁规则是:在对象内部封装所有的可变状态,通过对象的内部锁来同步任何访问可变状态的代码路径,保护它在并发访问中的安全。
对于每一个涉及多个变量的不变约束,需要同一个锁保护其所有的变量。
决定同步代码段大小的因素有:安全性、简单性和性能。
分析线程安全:首先要分析共享的可变变量是否是线程安全的,然后再分析涉及不变约束的多个变量是否被同步(如同一个锁保护)。
第三章 共享对象

Synchronized的两个作用:一是保护临界区,二是内部可见性。我们不仅希望能够避免一个线程修改其他线程正在使用的对象的状态,而且希望确保当一个线程修改了对象的状态后,其他线程能够真正看到改变。
为了确保跨线程写入的内存可见性,你必须使用同步机制。
在没有同步的情况下,编译器、处理器,运行时安排操作的执行顺序可能完全出人意料(重排序)。在没有进行适当同步的多线程程序中,尝试推断那些“必然”发生在内存中的动作时,你总是会判断错误。
在多线程中,没有使用同步机制的“读”操作可能会引发一种错误:读到过期的数据。因而在多线程中,只要有“写”共享变量,读写共享变量都要使用同步机制。
JVM允许将64位的读或写划分为两个32位的操作,因为在多程序中使用共享的、可变的long和double变量时,必须将它们声明为volatile类型,或者用锁保护起来。
锁不仅仅是关于同步与互斥的,也是关于内存可见的。一个线程在同步块之中或之前所做的每一件事,当其他线程处于同步块时都是可见的。故某些操作不一定要放到同步块中,之前也行。
Volatile可以解决可见性,而且如同同步块一样,某个线程在写volatile变量前的操作,在其它线程读volatile变量后,也都变成可见的了。相当于“栅栏”,栅栏前和后的操作只会分别重排序,而不会一起重排序。然而,我们不应该过度依赖volatile的栅栏作用,因为这比使用锁的代码更脆弱,更难以理解。正确使用volatile的方式包括:用于确保它们所引用的对象状态的可见性,或者用于表示重要的生命周期事件(比如初始化或关闭)的发生。
加锁可以保证原子性和可见性,volatile只能保证可见性。
发布一个对象的意思是指使它能够被当前范围之外的代码所使用,比如将它的引用存储到其他代码可以访问的地方,在一个非私有的方法中返回这个对象,或者传递它到其它类的方法中。发布了一个不该发布的对象或没准备好的对象(的现象)称为逸出。
在构造函数内部发布的对象,只是一个未完成构造的对象。不要让this引用在构造函数中逸出。
线程封闭是指把变量限制在单线程中,仅仅在单线程中被访问,这样就不需要任何同步。线程封闭实例:Swing的可视化组件和数据模型,JDBC connection对象。
线程封闭的三种实现方式:
Ad-hoc线程限制:是指维护线程限制性的任务全部落在实现上,而不需要经过设计。如使用volatile修饰单写多读的共享变量。这种方式是非常容易出错的。
栈限制:在线程中定义本地变量(对象),此时必须保证该对象不能被其他线程访问。
threadLocal:把一个全局共享的变量设置为threadlocal,这样每个线程都会保存一个该变量的副本,而不会相互冲突。使用threalocal还可以频繁执行的操作每次都重新分配临时对象(相对于栈限制)。
不可变性:创建后状态不能被修改的对象叫做不可变对象。不可变对象永远是线程安全的。只有满足如下条件,一个对象才是不可变的:
它的状态不能在创建后再被修改;
所有域都是final类型;并且(final域可能是可变的,因为它可以获得一个可变对象的引用)
它被正确创建(创建期间没有发生this引用的逸出)。
“将所有的域声明为final型,除非它们是可变的”,是一条良好的时间,可以减少对象的复杂度。
不可变对象可以在没有额外同步的情况下,安全的用于任意线程;甚至发布它们时亦不需要同步。
为了安全的发布对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确创建的对象可以通过下列条件安全地发布:
通过静态初始化器初始化对象的引用;
将它的引用存储到volatile域或AtomicReference;
将它的引用存储到正确创建的对象的final域中;
或者将它的引用存储到由锁正确保护的域中。
线程安全库中的容器提供了如下的线程安全保证:
直入Hashtable、synchronizedMap、ConcurrentMap中的主键以及键值,会安全的发布到可以从Map获得他们的任一线程中,无论是直接获得还是通过迭代器(iterator)获得;
置入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中的元素,会安全的发布到可以从容器中获得它的任意线程中;
置入BlockingQueue或者ConcurrentLinkedQueue的元素,会安全的发布到可以从队列中获得它的任意线程中。
一个对象在技术上不是不可变的,但是它的状态不会在发布后被修改,这样的对象称作有效不可变对象。任何线程都可以在没有额外的同步下安全的使用一个安全发布的有效不可变对象。
可变对象安全发布仅仅可以保证“发布当时”状态的可见性。发布对象的必要条件依赖与对象的可变性:
不可变对象可以通过任意机制发布;
有效不可变对象必须要安全的发布;
可变对象必须要安全发布,同时不需要线程安全或者是被锁保护。
在并发程序中,使用和共享对象的一些最有效的策略如下:
线程限制:一个线程限制的对象,通过限制在线程中,而被线程独占,且只能被堵占有它的线程修改。
共享只读(share read-only):一个共享的只读对象,在没有额外同步的情况下,可以被多个线程并发的访问,但是任何线程都不能修改它,共享只读对象包括不可变对象和有效不可变对象。
共享线程安全(shared thread-safe):一个线程安全的对象在内部进行同步,所以其他线程无须额外同步,就可以通过公共接口随意的访问它。
被守护的(Guarded):一个被守护的对象只能通过特定的锁来访问。被守护的对象包括那些被线程安全对象封装的对象,和已知被特定的锁保护起来的已发布对象。
可见性是指在某个线程中修改了变量,其他线程可以“发觉”——读到;发布是指在某个线程中定义了变量,可以被其他线程“发觉”。

 

第四章 组合对象

设计线程安全类的过程应该包括下面3个基本要素:
确定对象状态是由哪些变量构成的;
确定限制状态变量的不变约束;
制定一个管理并发访问对象状态的策略。
对象的同步策略:定义了对象如何协调对其状态的访问,并且不会违反它的不变约束或后验条件。应该将类的同步策略写入文档。
对象与变量拥有一个状态空间:即它们可能处于的状态范围。不可变对象是一种极限情况,它只可能处于唯一的状态。类的不变约束与方法的后验条件约束了对象合法的状态和合法状态转换。不理解对象的不变约束和后验条件,你就不能保证线程安全性。要约束状态变量的有效值或者状态转换,就需要原子性与封装性。
若一个操作存在基于状态的先验条件,则把它称为是状态依赖的(state-dependent)。在单线程化的程序中,操作如果无法满足先验条件,必然失败;但在多线程中,可以选择:持续等待,直到先验条件为真,再继续处理操作。这可以使用java的内置高效机制wait和notify。
一个线程不安全的对象也可以应用于多线程,因为它可以被其他安全的对象封装。这称为实例限制。将数据封装在对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据时总能获得正确的锁。限制对象时,要防止对象逸出它的期望范围,即防止外部不通过方法直接访问对象。
限制性使构造线程安全的类变得更容易,因为类的状态被限制后,分析它的线程安全性时,就不必检查完整的程序。
使用实例限制最好的例子是Java监视器模式。遵循Java监视器模式的对象封装了所有的可变状态,并由对象自己的内部锁保护。Vector和Hashtable都使用了Java监视器模式。
线程安全委托:类自己不解决线程安全的问题,让类中的变量来解决,这种现象叫做线程安全委托。如果一个类由多个彼此独立的线程安全的状态变量组成,并且类的操作不包含任何无效状态转换时,可以将线程安全委托给这些状态变量。
如果一个状态变量是线程安全的,没有任何不便约束限制它的值,并且没有任何状态转换限制它的操作,那么它可以被安全发布。
向已有的线程安全类添加功能有三种方式:
向原始类中加入方法,需要原始代码支持和理解同步策略
扩展类,需要了解类的状态,一般同步方式是使用synchronized()就行
扩展类的功能,要确保类的内部锁和我们加的锁是同一个锁,一般是使用synchronized(object),object为原始类。
还有一种更健壮的方式——组合。其实就是使用另一个类封装原始类,而且新类中引入一个新的锁层(包括原有的方法和新方法),新的锁和原始类的锁不需要有任何关系。虽然这种方式会稍微的影响性能,但它安全。
在维护线程安全性的过程中,文档是最强大的工具之一。为类的用户编写类的线程安全性担保文档;为类的维护者编写类的同步策略文档。技巧:使用@GuardedBy标签。

时间: 2024-08-03 18:25:00

《Java并发编程实战》学习笔记 线程安全、共享对象和组合对象的相关文章

Java并发编程示例(十):线程组_java

对线程分组是Java并发API提供的一个有趣功能.我们可以将一组线程看成一个独立单元,并且可以随意操纵线程组中的线程对象.比如,可以控制一组线程来运行同样的任务,无需关心有多少线程还在运行,还可以使用一次中断调用中断所有线程的执行. Java提供了ThreadGroup类来控制一个线程组.一个线程组可以通过线程对象来创建,也可以由其他线程组来创建,生成一个树形结构的线程. 根据<Effective Java>的说明,不再建议使用ThreadGroup.建议使用Executor. --D瓜哥特此

《Java并发编程实战》学习笔记 任务执行和取消关闭

第六章 任务执行 大多数并发应用程序是围绕执行任务进行管理的.设计任务时,要为任务设计一个清晰的任务边界,并配合一个明确的任务执行策略.任务最好是独立的,因为这会提高并发度.大多数服务器应用程序都选择了下面这个自然的任务边界:单个客户请求. 任务时逻辑上的工作单元,线程是使任务异步执行的机制. 应用程序内部的任务调度,存在多种可能的调度策略: 其中,最简单的策略是在单一的线程中顺序的执行任务.但它的吞吐量和响应性很差,一般只在特殊情况下使用:任务的数量很少但生命周期很长时,或者服务器只服务于唯一

Java并发编程示例(一):线程的创建和执行_java

开门见山 在IT圈里,每当我们谈论并发时,必定会说起在一台计算机上同时运行的一系列线程.如果这台电脑上有多个处理器或者是一个多核处理器,那么这时是实实在在的"同时运行":但是,如果计算机只有一个单核处理器,那么这时的"同时运行"只是表象而已. 所有的现代操作系统全部支持任务的并发执行.你可以边听音乐,边上网看新闻,还不耽误首发电子邮件.我们可以说,这种并发是 进程级并发 .在进程内部,我也可以看到有许许多多的并发任务.我们把运行在一个进程里面的并发任务称 线程. 和

Java并发编程示例(三):线程中断_java

一个多线程的Java程序,直到所有线程执行完成,整个程序才会退出.(需要注意的是,是所有非后台线程(non-daemon thread)执行完成:如果一个线程执行了System.exit()方法,程序也会退出.)有时,你想中止一个线程的执行,例如你想退出程序,或者你想取消一个正在执行的任务等. Java提供了中断机制,可以让我们显式地中断我们想中止执行的线程.中断机制的一个特征就是我们可以检查线程是否已经被中断,进而决定是否响应中止请求.线程也可以忽略中止请求,继续执行. 在本节,我们所开发的示

Java多线程编程实战之实现线程

Java编程语言使多线程如此简单有效,以致于某些程序员说它实际上是自然的.尽管在 Java 中使用线程比在其他语言中要容易得多,仍然有一些概念需要掌握.要记住的一件重要的事情是 main() 函数也是一个线程,并可用来做有用的工作.程序员只有在需要多个线程时才需要创建新的线程. Thread 类 Thread 类是一个具体的类,即不是抽象类,该类封装了线程的行为.要创建一个线程,程序员必须创建一个从 Thread 类导出的新类.程序员必须覆盖 Thread 的 run() 函数来完成有用的工作.

Java 并发编程学习笔记之核心理论基础_java

并发编程是Java程序员最重要的技能之一,也是最难掌握的一种技能.它要求编程者对计算机最底层的运作原理有深刻的理解,同时要求编程者逻辑清晰.思维缜密,这样才能写出高效.安全.可靠的多线程并发程序.本系列会从线程间协调的方式(wait.notify.notifyAll).Synchronized及Volatile的本质入手,详细解释JDK为我们提供的每种并发工具和底层实现机制.在此基础上,我们会进一步分析java.util.concurrent包的工具类,包括其使用方式.实现源码及其背后的原理.本

Java并发编程之性能、扩展性和响应_java

本文讨论的重点在于多线程应用程序的性能问题.我们会先给性能和扩展性下一个定义,然后再仔细学习一下Amdahl法则.下面的内容我们会考察一下如何用不同的技术方法来减少锁竞争,以及如何用代码来实现. 1.性能 我们都知道,多线程可以用来提高程序的性能,背后的原因在于我们有多核的CPU或多个CPU.每个CPU的内核都可以自己完成任务,因此把一个大的任务分解成一系列的可彼此独立运行的小任务就可以提高程序的整体性能了.可以举个例子,比如有个程序用来将硬盘上某个文件夹下的所有图片的尺寸进行修改,应用多线程技

Java并发编程总结——慎用CAS详解_java

一.CAS和synchronized适用场景 1.对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源:而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能. 2.对于资源竞争严重的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized.以java.util.concurrent.atomic包中AtomicInteger类为例,其getAn

Java 并发编程内部分享PPT分享

.NET程序员转向JAVA领域,必备技术首当其冲就是JAVA Concurrency 并发编程. 最近系统性的学习了 Doug Lea <JAVA并发编程实战>一书.这书很有嚼劲,进入JAVA技术体系必看书籍之一. 看完之后,在公司内部做了一个简单的分享,主要是普及下.NET程序员转向Java技术后对于并发的基本认识. PPT中有大量的实例代码,可以自己debug. http://files.cnblogs.com/files/wangiqngpei557/java-concurrency%E