Java并发编程实践

系统 1588 0

第一章   介绍

线程的优点
使用多处理器
对异步事件的处理
用户界面的更加响应性
线程的风险
  1.安全危险
  2.活跃度危险
  3.性能危险
线程无处不在
1.定时器
2.JSP
3.RMI
4.Swing和AWT

 

 

 


第二章   线程安全
编写线程安全的代码,本质上就是管理对状态的访问,而通常都是共享的、可变的状态 
无论何时,只要有多余一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。

在没有正确同步的情况下,如果多个线程访问了同一个变量,你的程序就存在隐患,有三种方法修复它:
1.不要跨线程共享变量
2.使状态变量为不可变的;或者
3.在任何访问状态变量的时候使用同步
 
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。

无状态对象永远是线程安全的

假设有操作A和B,如果从执行A的线程角度看,当其他线程执行B时,要么B全部执行完成,要么一点都没有执行,这样A和B互为原子操作,一个原子操作是指:该操作对于所有的操作,包括它自己,都满足前面的状态

利用AtomicLong这样已有的线程安全对象管理类的状态是非常实用的,相对于非线程安全对象,判断一个线程安全对象的可能状态和状态转换要容易的多,这简化了维护和验证线程安全性的工作

为了保护状态的一致性,要在单一的原子操作中更新相互关联的状态变量

对于每一个涉及多个变量的不变约束,需要同一个锁保护其所有的变量

通常简单性和性能之间是相互牵制的,实现一个同步策略时,不要过早地为了性能而牺牲简单性(这是对安全性潜在的妥协)

有些耗时的计算或操作,比如网络或者控制台IO,难以快速完成,执行这些操作期间不要占有锁


 

 


第三章 共享对象
在没有同步的情况下,编译器,处理器,运行时安排操作的顺序可能完全出人意料。在没有进行适当同步的多线程程序中,尝试推断那些“必然”
发生在内存中的动作时,你总是会判断错误。

JDK对于long和double类型来说,如果没有声明为volatile的话,则是非原子操作。也就是说对于一个int类型来说,如果没有赋值成功,那就是默认值,成功就是新的值,但非原子的long和double可能会出现一些中间值。

锁不仅仅是关于同步互斥的,也是关于内存可见的。为了保证所有线程都能够看到共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步。

只 有当volatile变量能够简化实现和同步策略的验证时,才使用它们。当验证正确性必须推断可见性问题时,应该避免使用volatile变量。正确使用 volatile变量的方式包括:用于确保它们所引起的对象状态的可见性,或者用于标识重要的生命周期事件(比如初始化或者关闭)的发生。

加锁可以保证可见性与原子性;volatile变量只能保证可见性。
只有满足了下面所有的标准后,你才能使用volatile变量:
1.写入变量时并不依赖变量的当前值;或者能够确保只有单一的线程修改变量的值;
2.变量不需要与其他的状态变量共同参与不变约束;
3.而且,访问变量时,没有其他原因需要加锁。

发布一个对象的意思是使它能够被当前范围之外的代码所使用
不要让this在构造期间逸出,甚至即使再构造函数的最后一行发布的引用也是如此,这样的对象为称为“没有正确的构建”

不可变对象永远是线程安全的
只有满足如下状态,一个对象才是不可变的:
1.它的状态不能再创建后再被修改
2.所有的域都是final类型;并且
3.它被正确创建(创建期间没有发生this引用逸出)

正如“将所有的域声明为私有的,除非它们需要更高的可见性“一样,将所有的域声明为final型,除非它们是可变的

不可变对象可以再没有额外同步的情况下,安全地用于任意线程;甚至发布它们不需要额外的同步

为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见,一个正确创建的对象可以通过下列条件安全发布:
1.通过静态初始化器初始化对象的引用;
2.将它们的引用存储到volatile域或AtomicReference;
3.将它们的引用存储到正确创建的对象的final域中;
4.或者将它的引用存储到由锁正确保护的域中

任何线程都可以再没有额外的同步下安全地使用一个安全发布的高效不可变对象。

发布对象的必要条件依赖于对象的可变性:
1.不可变对象可以通过任意机制发布
2.高效不可变对象必须要安全发布
3.可变对象必须要安全发布,同时必须要线程安全或者是被锁保护。

在并发程序中,使用和共享对象的一些最有效的策略如下:
1.线程限制:一个线程限制的对象,通过限制在线程中,而被线程独占,且只能被占有它的线程修改。
2.共享只读:一个共享的只读对象,在没有额外同步的情况下,可以被多个线程并发地访问,但是任何线程不能修改它。共享只读对象包括可变对象与
   高效不可变对象
3.共享线程安全:一个线程安全的对象在内部进行同步,所以其他线程无需额外同步,就可以通过公共接口随意的访问它。
4.被守护的:一个被守护的对象只能通过特定的锁来访问。被守护的对象包括哪些被线程安全对象封装的对象,和已知被特定的锁保护起来的已发布对象。

 

 

 


第四章   组合对象
设计线程安全类的过程应该包括下面三个基本要素:
1.确定对象状态是由哪些变量构成的
2.确定限制状态变量的不变约束
3.制定一个管理并发访问对象状态的策略

不理解对象的不变约束和后验条件,你就不能保证线程安全性。要约束状态变量的有效值或者状态转换,就需要原子性与封装性。

将数据封装在对象内部,把对数据的访问限制在对象的方法上,更易确保线程在访问数据时总能获得正确的锁

限制性使构造线程安全的类变得更容易。因为类的状态被限制后,分析它的线程安全性时,就不必检查完整的程序。
java监视器模式

如果一个累由多个彼此独立的线程安全的状态变量组成,并且类的操作不包含任何无效状态转换时,可以将线程安全委托给这些状态变量。

如果一个状态变量是线程安全的,没有任何不变约束限制它的值,并且没有任何状态转换限制它的操作,那么它可以被安全发布。

为类的用户编写类线程安全性担保的文档;为类的维护者编写类的同步策略文档。

 

 

 


第五章   构建块
同步容器,同步容器的组合操作(可能需要外部加锁)
迭代器和ConcurrentModificationException
并发容器
ConcurrentHashMap,采用分离锁机制
CopyOnWriteArrayList
CopyOnWriteArraySet

有界队列是强大的资源管理工具,用来简历可靠的应用程序:它们歇制那些可以产生过多工作量、具有威胁的活动,从而让你的程序在面对超负荷
工作时更加健壮。
虽然生产者-消费者模式可以把生产者和消费者的代码互相解耦合,但是它们的行为还是间接地通过共享工作队列耦合在一起了。它理想地假定消费者
会持续工作,所以你不需要为工作队列的大小划定边界,但是这将成为日后需要重新架构系统的预兆。在你的设计初期就是用阻塞队列建立对资源的
管理--提早做这件事情会比日后再修复容易得多。

双端队列和窃取工作
阻塞和可中断的方法

闭锁是一种Synchronizer,它可以延迟线程的进度直到线程到达终止状态。一个闭锁工作起来就像一道大门:直到闭锁达到终点状态之前门一直
是关闭的,没有线程能够通过,在终点状态到来的时候,门开了,允许所有的线程通过。
FutureTask
信号量:用来控制能够同时访问某特定资源的活动数量,或者同时执行某一给定操作的数量。
关卡

 

 

 


第一部分  总结
1.可变状态,伙计们  所有并发问题都归结为如何协调访问并发状态。可变状态越少,保证线程安全就越容易。
2.尽量将域声明为final类型,除非它们的需要是可变的。
3.不可变状态天生是线程安全的  不可变对象极大地减轻了并发编程的压力。它们简单而安全,可以在没有锁或者防御性复制的情况下自由地共享。
4.封装使管理复杂度变得更可行   你固然可以用存储于全局变量的数据来写一个线程安全类。但是你为什么要这么做?在对象中封装数据,让它们
     能够更加容易地保持不变;在对象中封装同步,使它们能够更容易地遵守同步策略。
5.用锁来守护每一个可变变量
6.对同一不变约束中的所有变量都使用相同的锁。
7.在运行复合操作期间持有锁。
8.在非同步的多线程情况下,访问可变变量的程序是存在隐患的。
9.不要依赖于可以需要同步的小聪明
10.在设计过程中就要考虑线程安全性。或者在文档中明确地说明它不是线程安全的。
11.文档话你的同步策略。

 

 

 

 

 

第六章 执行任务
围绕执行任务来管理应用程序时,第一步要指明一个清晰的任务边界。理想情况下,任务是独立的活动:它的工作并不依赖于其他任务的状态、结果
或者边界效应。
无限创建线程的缺点:
1.线程生命周期的开销
2.资源消耗量
3.稳定性

Executor框架
无论何时当你看到这种形式的代码:
new Thread(runnable).start()
并且你可能最终希望获得一个更加灵活的执行策略时,请认真考虑使用Executor代替Thread。
1.newFixedThreadPool()
2.newCachedThreadPool()
3.newSingleThreadExecutor()
4.newScheduleThreadPool()
Executor的生命周期

可携带结果的任务:Callable和Future
当一些线程执行了耗时的任务,需要等待,我们可以将这些任务线程全部放到一个ExecutorService中,当所有的线程都
执行完后,就会返回一个结果集,我们就可以操作这个集了,但是它的响应性不太高。

CompletionService 内部使用了BlockQueue,它可以返回一个Future,这个Future可以一直阻塞等待(获取CompletionService
中的BlockQueue元素),也可以设置等待超时。
有了CompletionService,若干个耗时线程,只要有一个完成,就可以返回,我们拿到Future就可以执行了。


 

 


第七章 取消和关闭 
当外部代码能够在活动自然完成之前,把它更改为完成状态,那么这个活动被称为可取消的。
1.用户请求取消
2.限制活动
3.应用程序事件
4.错误
5.关闭

在API和语言规范中,并有把中断与任何取消的语意绑定起来,但是,实际上使用中断来处理取消之外的任何事情都是不明智的,并且很难支撑
起更大的应用。

调用interrupt并不意味着必然停止目标线程正在进行的工作;它仅仅是传递了请求中断的消息。

中断通常是实现取消最明智的选择。

支持中断取消的代码:
public void run() {
    try {
        BingInteger p = BigInteger.ONE;
        while(!Thread.currentThread().isInterrupted()) {
            queue.put(p=p.nextProbalePrime());
        }
    } catch(InterruptedException e) {
        //允许线程退出
    }
}

因为每一个线程都其自己的中断策略,所以你不应用中断线程,除非你想知道中断对这个线程意味着什么。
有两种处理InterruptedException的实用策略:
1.传递异常(很可能发生在特定任务的清除时),使你的方法也成为可中断的阻塞方法
2.或者保存中断状态,上层调用栈中的代码能够对其进行处理

只有实现了线程中断策略的代码才可以接受中断请求。通用目的的任务和库的代码决不应该接受中断请求。

当Feture.get()抛出InterruptedException或TimeoutException时,如果你知道不再需要结果时,就可以调用Future.cancel()来取消任务了。

处理不可中断的阻塞:
1.java.io中的同步Socket I/O
2.java.nio中的同步I/O
3.Selector的异步I/O
4.获得锁

对于线程的持有的服务,只要服务的存在时间大于创建线程的方法存在的时间,那么久应该提供声明周期方法。
不支持关闭的线程是不太好的,因为可能会错过一些日志
可以通过线程池来管理关闭相关的服务
增加致命药丸关闭服务

在一个长时间运行的应用程序中,所有的线程都要给未捕获异常设置一个处理器,这个处理器至少要将异常信息记入日志中。
可以在一个函数中执行ExecutorService,这样它的生命周期就限定在一个函数内
在关闭ExecutorService时,可以保存任务待下次继续执行

处理未捕获的异常,JDK1.5之前只能通过线程组,可以实现UncaughtExceptionHandler接口,然后赋给Thread
    Thread#setUncaughtExceptionHandler()
    默认情况下线程出现未捕获的异常,最后是由JVM,也就是本地代码捕获到,然后调用线程组的UncaughtExceptionHandler实现
    最终是调用System.err,打印出来。

JVM关闭钩子 Rumtime#addShutdownHook(),最终通过Runtime#halt()关闭虚拟机的

应用程序中,精灵线程不能替代对服务的生命周期恰当、良好的管理。

避免使用Finalizer

 

 

 


第八章 应用线程池
任务执行与执行策略间的隐性耦合
一些任务具有这样的特征:需要或者排斥某种特定的执行策略。对其他任务具有依赖性的任务,就会要求线程池足够大,来保证它所依赖任务不必
排队或者不被拒绝;采用线程限制的任务需要顺序地执行。把这些需求都写入文档,这样将来的维护者就不会使用一个与原先相悖的执行策略,而
破坏安全性或活跃度了。

无论何时,你提交了一个非独立的Executor任务,要明确出现线程饥饿死锁的可能性,并且在代码或者配置文件以及其他可以配置Executor的地方,
任何有关池的大小和配置约束都要写入文档。

定制线程池的大小:
Ncpu = CPU的数量
Ucpu = 目标CPU的使用率,0<=Ucpu<=1
W/C = 等待时间与计算时间的比率
为保持处理器达到期望的使用率,最优的池大小等于:
Nthreads = Ncpu * Ucpu * (1+W/C)

newCacheThreadPool()工厂提供了比定长的线程池更好的队列等待性能,(这个性能差异源自于使用SynchronousQueue取代了
LinkedBlockingQueue)它是Executor的一个很好的默认选择。处于资源管理的目的,当你需要限制当前任务的数量,一个定长的线程池就是很好
的选择。就像一个接受网络客户端请求的服务器应用程序,如果不进行限制,就会很容易因为过载而遭受攻击。

饱和策略
线程工厂
扩展ThreadPoolExecutor

 

 

 


第九章 GUI应用程序

 

 

 

 

 

第十章 避免活跃度危险
锁顺序死锁    资源死锁

两个线程试图通过不同的顺序获得多个相同的锁
如果所有线程以通用的固定秩序获得锁,程序就不会出现顺序死锁问题了
动态的锁顺序死锁   加时赛锁

在持有锁的时候调用外部方法是在挑战活跃度的问题。外部方法可能会获得其他锁(产生死锁的风险),或者遭遇严重的超时阻塞。当你持有
锁的时候会延迟其他试图获得该锁的线程。

当调用的方法不需要持有锁时,这被称为开放调用。
在程序中尽量使用开放调用。依赖于开放调用的程序,相比于那些在持有锁的时候还调用外部方法的程序,更容易进行死锁自由度分析。
线程饥饿死锁  单线程化的Executor可能发生

避免死锁
1.尝试定时的锁
2.通过线程转储分析死锁

其他活跃度问题
1.当线程访问它所需要的资源时却被永久拒绝,以至于不能再继续进行,这样就发生了饥饿
   抵制使用线程优先级的诱惑,因为这会增加平台依赖性,并且可能引起活跃度问题。大多数并发应用程序可以对所有线程使用相同的优先级。
2.弱响应性
3.活锁是线程中活跃度失败的另一种形式,尽管没有被阻塞,线程却仍然不能继续,因为它不断重试相同的操作,却总是失败。

 

 

 


第十一章 性能和可伸缩性
可伸缩性指的是:当增加计算资源的时候(比如增加额外CPU数量、内存、存储器、I/O带宽),吞吐量和生产量能够相应地得到改进。

避免不成熟的优化,首先使程序正确,然后再加快--如果他运行的还不够快。
测试,不要臆测

Amdahl定律:大多数并发程序农耕有阵很多相似之处,有一系列并发和串行化的片段组成。Amdahl定律描述了在一个系统中,基于可并行话和
可串行化的组件各自所占的比重,程序通过额外的计算资源,理论上能够加速多少。如果F是必须串行化执行的比重,那么Amdahl定律告诉我们,
在一个N处理器的机器中,我们最多可以加速:
Speedup <= 1/(F+ (1-F)/N )

所有的并发程序都有一些串行源,如果你认为你没有,那么去仔细检查吧。

不要过分担心非竞争的同步带来的开销。基础的机制已经足够快了,在这个基础上,JVM能够进行额外的优化,大大减少或者消除了开销,关注那些
真正发生了锁竞争的区域中性能的优化。

并发程序中,对可伸缩性首要的威胁是独占的资源锁。

有三种方式来减少锁的竞争:
1.减少持有锁的时间
2.减少请求锁的频率
3.或者用协调机制取代独占锁,从而允许更强的并发性。

减小锁的粒度:
分拆锁
分离锁(ConcurrentHashMap)

检测CPU利用率
1.不充足的负载
2.I/O限制
3.外部资源
4.锁竞争

对象分配通常比同步要更“便宜”

 

 

 


第十二章 测试并发程序
与活跃度测试相关的是性能测试。性能可以通过很多种方式来测量,其中包括:
1.吞吐量:在一个并发任务集里,已完成任务所占的比例。
2.响应性:从请求到完成一些动作之间的延迟(也被称作等待时间)
3.可伸缩性:增加更多的资源(通常是指CPU),就能提高(或者缓解短缺)吞吐量。

为并发类创建有效的安全测试;其挑战在于:如何在程序出问题并导致某些属性极度可能失败时,简单地识别出这些受检测的属性来,同时不要
人为地让检查找错误的代码限制住的并发性。最好能做到在检测测试的属性时,不需要任何的同步。

测试应该在多处理器系统上运行,以提高在交替运行的多样性。但是,多个CPU未必会使测试更加高效。为了能够最大程度地检测到时序敏感的
数据竞争的发生机会,应该让测试中的线程数多于CPU数,这样在任何给定的时间里,都有一些线程在运行,一些被交换出执行队列,这样可以
增加线程间交替行为的随机性。

编写有效的性能测试,就需要哄骗优化器不要把你的基准测试当做死代码而优化掉。这需要每一个计算的结果都要应用在你的程序中--以一种不需要
的同步或真实计算的方式。

测试正确性
  1.基本的单元测试
  2.测试阻塞操作
  3.测试安全性
  4.测试资源管理 (对JVM内存消耗是否有影响)
  5.测试性能
  6.测试响应性  

避免性能测试的陷阱
1.垃圾回收
2.动态编译(加入一些热身时间)
3.代码路径的非真实取样
4.不切实际的竞争程度
5.死代码的消除

使用一些静态工具如FindBugs来检测代码

 

 

 

 

 

第十三章 显示锁
Lock、ReentrantLock

性能是一个不断变化的目标;昨天的基准显示X比Y更快,这可能已经过时了。

在内部锁不能够满足使用时,ReentrantLock才被作为更高级的工具。当你需要以下高级特性时,才应该使用:可定时的、可轮询的与
可中断的锁获取操作,公平队列,或者非块结构的锁。则否,请使用synchronized。
读写锁

 

 

 


第十四章 构建自定义的同步工具
条件谓词是先验条件的第一站,它再一个操作与状态转行之间建立起依赖关系。
将条件谓词和与之关联的条件队列,以及在条件队列中等待的操作,都写入文档。

每次调用wait都会隐式地与特定的条件谓词相关联。当调用特定条件谓词的wait时,调用者必须已经持有了与条件队列相关的锁,
这个锁必须同时还保护着组成条件谓词的状态变量。
一个单独的内部条件队列可以与多个条件谓词共同使用。

当使用条件等待(Object.wait()或者Condition.await()):
1.永远设置一个条件谓词---一些对象状态的测试,线程执行前必须满足它;
2.永远在调用wait前测试条件谓词,并且从wait中返回后再次测试;
3.永远在循环中调用wait;
4.确保构成条件谓词的状态变化被锁保护住,而这个锁正是条件队列相关联的;
5.当调用wait,notify或者notifyAll时,要持有与条件队列相关的锁;并且,
6.在检查条件谓词之后、开始执行被保护的逻辑之前,不要释放锁。

无论何时,当你在等待一个条件,一定要确保有人会在条件谓词变为真时通知你。

只要同时满足下述条件后,才能用单一的notify取代notifyAll;
相同的等待者,只有一个条件谓词与条件队列相关,每个线程从wait返回后执行相同的逻辑,并且,一进一出,一个队条件变量的通知,
至多激活一个线程执行。

危险警告:wait、notify和notifyAll在Condition对象的对等体是await、signal和signalAll。但是Condition继承与Object,这意味着它也有
wait和notify。一定要确保使用了正确的版本-----await和singal。

AbstractQueuedSynchronizer(简称AQS)
使用到AQS的类:
ReentrantLock
Semaphore    CountDownLatch
FutureTask
ReentrantReadWriteLock

当访问一些有限资源时,就需要同步
1.根据先验条件,isFull(), isEmpty()将失败抛给调用者
2.利用轮询加休眠实现阻塞
3.使用条件队列来完成阻塞

一个内部条件队列只能有一个与之相关联的锁,
一个Condition和一个单独的Lock相关联,而一个Lock可以关联多个Condition
并且比内部条件队列有更丰富的特性,中断/不可中断,基于时限的等待,公平和非公平


 

 


第十五章 原子变量与非阻塞同步机制
非阻塞算法,比较并交换CAS
硬件的支持: 比较并交换(compare-and-set)
                加载链接/存储条件(load-linked/store-conditional)

非阻塞计数器,非阻塞栈(Treiber算法),非阻塞链表(Michael-scott算法)
原子变量是“更佳的volatile”
ABA问题


 

 


第十六章 Java存储模型
重排序
happens-before的法则包括:
程序次序法则:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都出现在动作A之后。
监视器锁法则:对一个监视器锁的解锁happens-before于每一个后续对同一监视器锁的加锁。
volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一域的读操作。
线程启动法则:在一个线程里,对Thread.start()的调研会happens-before于每一个启动线程中的动作。
线程终结法则:线程中任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join()调用中成功返回,或者
Thread.isAlive()返回false。
中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断(通过抛出InterruptedException,或者调用
isInterruped和interruped。)
终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C。

除了不可变对象以外,使用被另一个线程初始化的对象,是不安全的,除非对象的发布是happens-before于对象的消费线程使用它。

初始化安全可以保证,对于正确的创建对象,无论它是如何发布的,所有线程都将看到构造函数设置的final域的值。更进一步,一个正确
创建的对象中,任何可以通过其final域触及到的变量(比如一个final数组中的元素,或者一个final引用的HashMap里面的内容),也可以保证
对其他线程都是可见的。

DCL失败的原因
初始化安全性保证只有以通过final域触及的值,在构造函数完成时是可见的。对于通过非final域触及的值,或者创建完成后可能改变的值,

必须使用同步来确保可见性。

 

 

 

 

 

网友写的java并发编程实践笔记

1, 保证线程安全的三种方法 :
a, 不要跨线程访问共享变量
b, 使共享变量是 final类型的
c, 将共享变量的操作加上同步

2, 一开始就将类设计成线程安全的 , 比在后期重新修复它 ,更容易 .

3, 编写多线程程序 , 首先保证它是正确的 , 其次再考虑性能 .

4, 无状态或只读对象永远是线程安全的 .

5, 不要将一个共享变量裸露在多线程环境下 (无同步或不可变性保护 )

6, 多线程环境下的延迟加载需要同步的保护 , 因为延迟加载会造成对象重复实例化

7, 对于 volatile 声明的数值类型变量进行运算 , 往往是不安全的 (volatile 只能保证可见性 , 不能保证原子性 ).
详见 volatile 原理与技巧中 , 脏数据问题讨论 .

8, 当一个线程请求获得它自己占有的锁时 ( 同一把锁的嵌套使用 ), 我们称该锁为可重入锁 .
在 jdk1.5 并发包中 , 提供了可重入锁的 java 实现 -ReentrantLock.

9, 每个共享变量 , 都应该由一个唯一确定的锁保护 .
创建与变量相同数目的 ReentrantLock, 使他们负责每个变量的线程安全 .

10,虽然缩小同步块的范围 , 可以提升系统性能 .
但在保证原子性的情况下 , 不可将原子操作分解成多个 synchronized块 .

11, 在没有同步的情况下 , 编译器与处理器运行时的指令执行顺序可能完全出乎意料 .
原因是 , 编译器或处理器为了优化自身执行效率 , 而对指令进行了的重排序 (reordering).

12, 当一个线程在没有同步的情况下读取变量 , 它可能会得到一个过期值 , 但是至少它可以看到那个
线程在当时设定的一个真实数值 . 而不是凭空而来的值 . 这种安全保证 , 称之为 最低限的安全性 (out-of-thin-air safety)

在开发并发应用程序时 , 有时为了大幅度提高系统的吞吐量与性能 , 会采用这种无保障的做法 .
但是针对 , 数值的运算 , 仍旧是被否决的 .

13, volatile 变量 , 只能保证可见性 , 无法保证原子性 .

14, 某些耗时较长的网络操作或 IO, 确保执行时 , 不要占有锁 .

15, 发布 (publish) 对象 , 指的是使它能够被当前范围之外的代码所使用 .( 引用传递 )
对象逸出 (escape), 指的是一个对象在尚未准备好时将它发布 .

原则 : 为防止逸出 , 对象必须要被完全构造完后 , 才可以被发布 ( 最好的解决方式是采用同步 )

this 关键字引用对象逸出
例子 : 在构造函数中 , 开启线程 , 并将自身对象 this 传入线程 , 造成引用传递 .
而此时 , 构造函数尚未执行完 , 就会发生对象逸出了 .

16, 必要时 , 使用 ThreadLocal变量确保线程封闭性 (封闭线程往往是比较安全的 , 但一定程度上会造成性能损耗 )
封闭对象的例子在实际使用过程中 , 比较常见 , 例如 hibernate openSessionInView机制 , jdbc的 connection机制 .

17, 单一不可变对象往往是线程安全的 (复杂不可变对象需要保证其内部成员变量也是不可变的 )
良好的多线程编程习惯是 : 将所有的域都声明为 final, 除非它们是可变的

18, 保证共享变量的发布是安全的 
a, 通过静态初始化器初始化对象 (jls 12.4.2 叙述 , jvm 会保证静态初始化变量是同步的 )
b, 将对象申明为 volatile 或使用 AtomicReference
c, 保证对象是不可变的
d, 将引用或可变操作都由锁来保护

19, 设计线程安全的类 , 应该包括的基本要素 :
a, 确定哪些是可变共享变量
b, 确定哪些是不可变的变量
c, 指定一个管理并发访问对象状态的策略

20, 将数据封装在对象内部 , 并保证对数据的访问是原子的 .
建议采用 volatile javabean 模型或者构造同步的 getter,setter.

21, 线程限制性使构造线程安全的类变得更容易 , 因为类的状态被限制后 , 分析它的线程安全性时 , 就不必检查完整的程序 .

22, 编写并发程序 , 需要更全的注释 , 更完整的文档说明 .

23, 在需要细分锁的分配时 , 使用 java监视器模式好于使用自身对象的监视器锁 .
前者的灵活性更好 .

Object target = new Object();
// 这里使用外部对象来作为监视器 , 而非 this
synchronized(target) {
// TODO
}

针对 java monitor pattern, 实际上 ReentrantLock的实现更易于并发编程 .
功能上 , 也更强大 .

24,  设计并发程序时 , 在保证伸缩性与性能折中的前提下 , 优先考虑将共享变量委托给线程安全的类 .
由它来控制全局的并发访问 .

25, 使用普通同步容器 (Vector, Hashtable) 的迭代器 , 需要外部锁来保证其原子性 .
原因是 , 普通同步容器产生的迭代器是非线程安全的 .

26, 在并发编程中 , 需要容器支持的时候 , 优先考虑使用 jdk 并发容器 

(ConcurrentHashMap, ConcurrentLinkedQueue, CopyOnWriteArrayList…).

27, ConcurrentHashMap, CopyOnWriteArrayList
并发容器的迭代器 , 以及全范围的 size(), isEmpty() 都表现出弱一致性 .
他们只能标示容器当时的一个数据状态 . 无法完整响应容器之后的变化和修改 .

28,  使用有界队列 , 在队列充满或为空时 , 阻塞所有的读与写操作 .  ( 实现生产 – 消费的良好方案 )
BlockQueue 下的实现有 LinkedBlockingQueue 与 ArrayBlockingQueue, 前者为链表 , 可变操作频繁优先考虑 , 后者为数组 , 读取操作频繁优先考虑 .
PriorityBlockingQueue 是一个按优先级顺序排列的阻塞队列 , 它可以对所有置入的元素进行排序 ( 实现 Comparator 接口 )

29, 当一个方法 , 能抛出 InterruptedException, 则意味着 , 这个方法是一个可阻塞的方法 , 如果它被中断 , 将提前结束阻塞状态 .
当你调用一个阻塞方法 , 也就意味着 , 本身也称为了一个阻塞方法 , 因为你必须等待阻塞方法返回 .

 

如果阻塞方法抛出了中断异常 , 我们需要做的是 , 将其往上层抛 , 除非当前已经是需要捕获异常的层次 .
如果当前方法 , 不能抛出 InterruptedException, 可以使用 Thread.currentThread.interrupt() 方法 , 手动进行中断 .

 
 

Java并发编程实践


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论