第10章并发

第10章并发

线程机制允许进行多个活动并发程序设计比单线程程序设计有更多的东西可能出错,也难以重现失败。但是并发能从多核的处理器中获得好的性能。

66. 同时访问共享的可变数据

关键字synchronized, 保证同一个时刻,只有一个线程可以执行某一个方法,或者某一个代码块。同步不仅是指互斥的一种方式,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。

java的语言规范保证读取或者写一个变量是原子的,除非这个变量的类型是long或者double。换句话说:读取一个非long 和double类型的变量,可以保证返回的值是某个线程保存在该变量中的,即使多个线程在没有同步的情况下并发地修改这个变量也是如此。

 

由于java语言规范并不保证一个线程写入的值对于另一个线程将是可见的,因此为了在线程间进行可靠的通信,也为了互斥访问,同步是必要的

要阻止一个线程妨碍另一个线程,建议做法是让一个线程去轮询(poll)一个boolean域,这个域一开始为false,但是可以通过第二个线程设置为true,以表示第一个线程将终止自己。由于boolean域的读和写操作都是原子的,程序员在访问这个域的时候不再使用同步。具体代码如下:

运行结果如下:

这个程序设计运行的初衷是运行一秒以后,主线程将stopRequested设为true,导致后台线程终止循环。然而由于没有同步,就不能保证后台线程何时看到主线程对stopRequested的值所做的改变。

修正这个问题的一种方式是同步访问stopRequested域。这个程序就会如预期般在大约1秒钟内结束。具体代码如下:

运行结果:

注意,写方法和读方法都被同步了。实际上,如果读写操作没有都被同步的话,同步是不会起作用的。这些方法的同步只是为了它的通信效果。上面的方式虽然在在循环的每个迭代中同步开小很小,但是还有更简洁,性能更好的方法。

另一种方式是将stopRequested声明为volatile,则上面StopThread的锁就可以省略。Volatile虽然不执行互斥访问,但它可以保证任何一个线程在读取该域的时候,都将看到最近刚刚被写入的值。具体代码如下:

扩展:增量操作符(++是原子的。它执行两项操作:首先读取旧值,然后+1,最后写回一个新值。可以用类AtomicXXX代替.

67.  避免过度同步

过度同步会造成性能降低、死锁,甚至不确定的行为。

为了避免活性失败和安全性失败,再一个被同步的方法或者代码块中,永远不要放弃对客户端的控制。即在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。因为这个类不知道该方法会做什么事情,也无法控制它。根据外来方法的作用,从同步区域中调用它会导致异常、死锁或者数据损坏。

在同步区域之外被调用的外来方法被称作开放调用。除了可以避免死锁之外,开放调用还可以极大地增加并发性。外来方法的运行时间可能会任意长。如果在同步区域内调用外来方法,其他线程对受保护资源的访问就会遭到不必要的拒绝。

通常,应该在同步区域内做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后放掉锁。

如果一个可变的类并发使用,应该使这个类变成是线程安全的,通过内部同步,可以获得比从外部锁定整个对象更高的并发性。否则,就要在内部同步。让客户在必要的时候从外部同步。Java违背了这条方针的类StringBuffer,StringBuffer几乎用于单线程,但是执行的是内部同步。

68.executorstask优先于线程

在java1.5之前,Thread类既充当工作单元,又是执行机制。现在两者分离,工作单元称作任务(task),包括Runnable和Callable。Executors是执行机制。

Executor Service简单使用:

ExecutorService创建的几种类型及使用场景

  1. Executor.newCachedThreadPool需要配置,被提交的任务没有排成队列,而是直接交给线程执行。如果没有线程可用,就创建一个新的线程。适用于小程序轻载服务器
  2. Executor.newFixedThreadPool,提供了包含固定线程数目的线程池。适用于一个重负载的服务器。
  3. 为了最大限度控制直接使用ThreadPoolExecutor
  4. Executors.newScheduledThreadPool()代替java.util.Timer。线程池executor支持多个线程,并且从抛出未受检异常的任务中恢复

69. 并发工具优先于wait和notify

从 Java 5 发行版本开始, Java 平台就提供了更高级的并发工具,它们可以完成以前必须在 wait 和 notify 上手写代码来完成的各项工作。 既然正确地使用 wait 和 notify 比较困难,就应该用更高级的并发工具来代替。

java.util.concurrent 中更高级的工具分成三类: Executor Framework 、并发集合(Concurrent Collection)以及同步器(Synchronizer)

有些并发集合接口将几个基本操作合并到了单个原子操作中。事实证明,这些操作在并发集合中已经够用,它们通过缺省方法被加到了 Java 8 对应的集合接口中。

例如, Map 的 putIfAbsent(key, value) 方法,当键没有映射时会替它插入一个映射,并返回与键关联的前一个值,如果没有这样的值,则返回 null 。这样就能很容易地实现线程安全的标准 Map 了。具体代码如下:

并发集合导致同步的集合大多被废弃了。比如, 应该优先使用 ConcurrentHashMap ,而不是使用 Collections.synchronizedMap 。 只要用并发 Map 替换同步 Map ,就可以极大地提升并发应用程序的性能。

有些集合接口已经通过阻塞操作(blocking operation )进行了扩展,它们会一直等待(或者阻塞)到可以成功执行为止。

同步器(Synchronizer)是使线程能够等待另一个线程的对象,允许它们协调动作。最常用的同步器是 CountDownLatchSemaphore 。较不常用的是 CyclicBarrier 和 Exchanger 。功能最强大的同步器是 Phaser 。

倒计数锁存器(Countdown Latch)是一次性的障碍,允许一个或者多个线程等待一个或者多个其他线程来做某些事情。CountDownLatch 的唯一构造器带有一个 int 类型的参数,这个 int 参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用 countDown 方法的次数。

 

70线程安全性的文档化

类的线程安全性级别:

1.不可变的:这个类的实例是不可变的。需要外部的同步。例:String,Long和BigInteger。

2.无条件的线程安全:这个类的实例可变的,但是这个类有着足够内部同步,所以它的实例可以被并发使用,无需任何外部同步。例:Random和ConcurrentHashMap。

3.有条件的线程安全:除了有些方法为进行安全的并发使用需要外部同步之外,这种线程安全级别与无条件的线程安全相同。例:Collections.synchronized包装返回的集合,它们的迭代器要求外部同步。

4.非线程安全:这个实例可变的。为了并发地使用它们,客户必须利用自己选择的外部同步包围每个方法调用(或者调用序列)。例:通用的集合实现ArrayList、HashMap等。

5.线程对立的:这个类能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。线程对立的根源通常在于,没有同步地修改静态数据。在Java平台类库中,线程对立的类或者方法非常少。例:System.runFinalizersOnExit方法,但已经被废除。

 

对于有条件的线程安全类,必须指明哪个调用序列需要外部同步,还要指明为了执行这些序列,必须获得哪一把(极少的情况是指哪几把锁)。

 

当一个类使用一个公有可访问的锁对象时,就允许客户端以原子的方式执行一个方法调用序列。客户端可以超时地持有公有可访问锁,发起拒绝服务攻击,导致其它客户端无法访问该对象。可以使用私有锁对象来解决这个问题,把锁对象封装在它所同步的对象中。具体代码如下:

私有锁对象只能用在无条件的线程安全类上。有条件的线程安全类不能使用,因为要说明调用方法序列时要获得哪把锁。私有锁对象模式特别适用于那些为继承设计的类。

 

71慎用延迟初始化

延迟初始化的最佳建议是「除非需要,否则不要这样做」(详见第 67 条)。原因是它增加了访问延迟初始化字段的成本。根据这些字段中最终需要初始化的部分、初始化它们的开销以及初始化后访问每个字段的频率延迟初始化实际上会损害性能。在大多数情况下,常规初始化优于延迟初始化

延迟初始化的用途:如果一个字段只在类的一小部分实例上访问,并且初始化该字段的代价很高,那么延迟初始化可能是值得的。唯一确定的方法是以使用和不使用延迟初始化的效果对比来度量类的性能。

延迟初始化的具体使用原则:

  1. 对于实例字段,使用双重检查模式具体代码如下:

这个模式用volatile避免了初始化后访问字段时的锁定成本。

注意该字段可以容忍重复初始化,则可以使用单检查模式。具体代码如下:

      此时,只有一次检查,可以重复初始化。

2. 对于静态字段,使用 lazy initialization holder class 模式进行延迟初始化来提高性能。具体代码如下:

 

72 不要依赖于线程调度器

当许多线程可以运行时,线程调度器决定哪些线程可以运行以及运行多长时间。线程调度器调度线程的策略在各个操作系统是不一样的,任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。因此,编写良好的程序不应该依赖于此策略的细节。

要编写健壮的、响应良好的、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。而保持可运行线程数量尽可能少的主要方法是,让每个线程做些有意义的工作,然后等待更多有意义的工作。

线程不应该处于忙等的状态,即反复地检查一个共享对象,以等待某些事情发生。这种做法会使程序容易受到调度器的变化影响,也会极大地增加处理器负担,,还影响其他人完成工作。

当面对一个几乎不能工作的程序时,而原因是由于某些线程相对于其他线程没有获得足够的 CPU 时间,那么 通过调用 Thread.yield 来「修复」程序 你也许能勉强让程序运行起来,但它是不可移植的,因为在不同的jvm上表现不同。

另外,通过调整线程优先级来“修正“一个原本不能运行的程序也是不可移植的。它只能少量地用于提高已经工作的程序的服务质量

73 避免使用线程组

除了线程、锁、监视器,线程系统还提供了一个基本的抽象——线程组。

线程组并没有提供太多有用的功能,而且它们提供的许多功能还都是有缺陷的。不要使用它们如果要处理线程的逻辑组,可以使用线程池executor。

参考资料

https://blog.csdn.net/xavi_2010/category_9900596.html

https://www.cnblogs.com/itlivemore/p/7103414.html

《Effective Java》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值