Thinking In Java 读书小结

并发

2017-02-07  本文已影响34人  CodingHou
  1. 并发的多面性
    用并发解决的问题大体上可以分为“速度”和“设计可管理性”两种

1.1 更快的执行

速度提高是以多核处理器的形式而不是更快的芯片的形式出现的。
阻塞:程序中的某个任务因为该程序控制范围之外的某些条件而导致不能继续执行。
并发通常是提高运行在单处理器上的程序的性能。
实现并发最直接的方式就是在操作系统级别使用进程。
编写多线程程序最基本的困难在于协调不同任务之间对内存和IO资源的使用。

1.2 改进代码的设计

Java的线程机制是抢占式的, 调度机制会周期性的中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到合理的时间去驱动他的任务。
协作式系统:每个任务都会自动地放弃控制,这要求程序员要有意识地在每个任务中插入某种类型的让步语句。

  1. 基本的线程控制
    并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立任务中的每一个都将由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流。
    因此,单个进程可以拥有度多个并发执行的任务,但是你的程序使得每个任务都好像有其自己的cpu一样。
    其底层机制是切分CPU时间。

2.1 定义任务

描述任务的方式由Runnable接口来提供。要想定义任务,只需实现Runnable接口并编写run()方法,使得该任务可以执行你的命令。
Tread.yield()是对线程调度器的一种建议,表示可以切换到其他线程。
当从Runnable导出一个类时,它必须具有run()方法,但是这个方法并无特殊之处——他不会产生任何内在的线程能力,要实现线程行为,你必须显式地将一个任务附着到线程上。

2.2 Thread类

将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器。
Thread t = new Thread(new LiftOff());
t.start();
Thread构造器只需要一个Runnable对象。调用Thread对象的start()方法为该线程执行必须的初始化操作,然后调用Runnable的run()方法,以便在这个新线程中启动该任务。
调用start()是main()线程,调用run()方法是新线程。

2.3 使用Executor

Executor将为你管理Thread对象。
Executor在客户端和任务执行之间提供了一个间接层,与客户端直接执行任务不同,这个中介对象将执行任务。
Executor允许你管理异步任务的执行, 而无须显式地管理线程的生命周期。是启动任务的优选方法。 可以用Executor来代替显示的创建Thread对象。
CashedThreadPool:在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程。
FixedThreadPool:固定数量的线程池。可以一次性先创建固定数量的线程,然后需要线程的事件处理器,直接从池中获取线程,这样可以节省时间。
SingleThreadPool:线程数量为1的FixedThreadPool。如果像SingleThreadExecutor提交了多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前结束运行。所有的任务将使用相同的线程。在这种方式中,不需要在共享资源上处理同步。

2.4 从任务中产生返回值

Runnable是执行工作的独立任务,但是他不返回任何值。
Callable接口可以在任务完成时返回一个值。必须用ExecutorService.submit()调用call()方法。

2.5 休眠

影响任务行为的一种简单方法是调用Sleep(),这值得任务中止执行给定的时间。

2.6 优先级

线程的优先级将该线程的重要性传递给了调度器。
一般使用的时候,只使用MAX_PRIORITY,NORM_PRIORITY,MIN_PRIORITY。

2.7 让步

Thread.yield() 表示让步,建议具有相同优先级的其他线程可以运行。

2.8 后台线程

后台线程:指在程序运行的时候在后台提供的一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。当所有的非后台线程结束时,程序也就中止了,同时会杀死所有的后台线程。
也就是说,只要任何非后台线程还在运行,程序就不会中止。

2.9 编码的变体

在非常简单的情况下,可以直接从Thread继承。

2.10 术语

我们创建任务,通过某种方式将一个线程附着到任务上,以使得这个线程可以驱动任务。
Java的线程机制基于来自C的低级P线程方式。
线程不是任务。

2.11 加入一个线程

一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。
如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复。
也可以在调用join()时带上一二超时参数,这样如果目标线程在这段时间到期时还没有结束的话,join()方法总能返回。
join()方法可以通过调用interrupt()方法中断。

2.12 创建有响应的用户界面

2.13 线程组

线程组持有一个线程集合。

2.14 捕获异常

由于线程的本质特性,使得不能捕获从线程中逃逸的异常。
可以通过Executor来解决这个问题。
将产生异常的线程放到try catch块里面是没用的。
Thread.UncaughtExceptionHandler允许在每个Thread对象上都附着一个异常处理器。

  1. 共享受限资源
    3.1 不正确的访问资源

3.2 解决共享资源竞争

防止资源冲突:上锁。

要想控制对共享资源的访问,得先把他包装进一个对象,然后把所有要访问这个资源的方法标记为synchronized。

synchronized void f(){}

当在对象上调用任意synchronized的方法的时候,此对象会被加锁,这时候该对象上其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。

Lock对象必须被显式的创建,锁定和释放。
使用Lock对象时,必须将unlock()放在try——finally的finally子句中。这样可以抛出异常,维护系统处于良好状态。

3.3 原子性与易变性

这些类被调整为可以使用在某些现代处理器上的可获得的,并且是在机器级别上的原子性,对性能调优有帮助。
可以用来替换synchronized关键字。但是依赖锁更安全。

3.5 临界区

临界区:希望防止过个线程同时访问方法内部的部分代码而不是整个方法。通过这种方法被分离出来的代码段被称为临界区,使用synchronized关键字建立。
也称为同步控制块,在进入此段代码前,必须得到syncObject对象的锁,如果其他线程已经得到这个锁,那么就得等到锁被释放以后,才能进入临界区。
优点:通过同步控制块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到显著提高。可以把一个非保护型的类,在其他类的保护和控制之下,应用于多线程环境。 也可以通过lock来创建临界区。

3.6 在其他对象上同步

synchronized块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象synchronized(this)。
有时候需要在另一个对象上同步,就必须确保所有相关任务都是在同一个对象上同步的。

3.7 线程本地存储

防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。
线程本地存储是一种自动化机制,可以为使用相同变量的每个线程都创建不同的存储。

  1. 终结任务
    有些情况下,任务必须更加突然的中止。

4.1 装饰性花园

ExecutorService.awaitTermination()等待每个任务结束,如果所有的任务在超时时间到达之前全部结束,就返回true,否则返回false。

4.2 在阻塞时终结

sleep()将任务从执行转台变为被阻塞状态,有时候必须中止被阻塞的任务。

线程状态:

4.3 中断

被互斥阻塞

如果尝试着在一个对象上调用其synchronized方法,而这个对象的锁已经被其他任务获得。那么调用任务将被挂起,直至这个锁可获得。

4.4 检查中断

  1. 线程之间的协作
    5.1 wait()与notifyAll()

wait()使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。
wait()会在等待外部世界产生变化的时候将任务挂起,并且只有在notify()或notifyAll()发生的时候,这个任务才会被唤醒去检查所产生的变化。
wait()提供了一种在任务之间对活动同步的方式。
调用sleep()和yield()都不会释放锁。
当一个任务在方法里遇到对wait()的调用的时候,线程的执行执行将被挂起,对象上的锁被释放,因为wait()将释放锁,这就意味着另一个任务可以获得这个锁,因此在该对象中的其他synchronized方法可以在wait()期间被调用。
“我已经刚刚做完能做的所有事情,因此我要在这里等待,但是我希望其他的synchronized操作在条件适合的情况下能够执行”
两种形式的wait()

只能在同步控制方法或同步控制块里调用wait(),notify(),notifyAll()。

错失的信号

当两个线程使用notify()/wait()或者notifyAll()/wait()进行协作时,有可能会错过某个信号。
解决方法是应该再synchronized块中进行条件判断。

5.2 notify()&&notifyAll()

使用显示的Lock和Condition对象

wait()与notifyAll()通过一种非常低级的方式解决了任务互操作问题,即每次交互时都握手。
更高级的方法:使用同步队列来解决问题。同步队列在任何时候都只允许有一个任务插入或移除元素。
通常可以使用

这里用了队列来实现面包的制作和消费。还是不太理解Executor到底是产生了几个线程,为什么会一直循环

5.5 任务间使用管道进行输入输出

通过输入输出在线程间进行通信很有用。提供线程功能的类库以“管道”的形式对线程间的输入输出提供了支持。PipedWriter&&PipedReader
PipedReader是可以中断的。

  1. 死锁
    死锁:一个任务之间相互等待的连续循环,没有哪一个线程可以继续。
    当下面四个条件同时满足的时候会发生死锁:
  1. 新类库中的构件
    Java SE5引入了大量用来解决并发问题的新类。

7.1 CountDownLatch

它被用来同步一个或者多个任务,强制他们等待由其他任务执行的一组操作完成。
可以向CountDownLatch对象设置一个初始值,任何在这个对象上调用wait()的方法都将阻塞,直至这个计数达到0。在其他任务结束工作时,可以在该对象上调用countDown()来减小这个计数值。
CountDownLatch被设计为只触发一次,计数值不能被重置。
CountDownLatch的典型用法是将一个程序分为n个互相独立的可解决任务,并创建值为0的CountDownLatch。

类库的线程安全

7.2 CyclicBarrier

CyclicBarrier适用于这样的情况:你希望创建一组任务,他们并行的执行工作,然后在进行下一个步骤之前等待,直到所有任务都完成。它使得所有的并行任务都将在栅栏处队列,因此可以一致地向前移动。与CountDownLatch不同的是,CyclicBarrier可以多次重用。

HorseRace利用CyclicBarrier执行每匹马向前移动的工作,然后在栅栏处等待所有马集合完毕。

7.3 DelayQueue

这是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。
这种队列是有序的即队头对象的延迟到期时间最长。

7.4 PriorityBlockingQueue

这是一个很基础的优先级队列,它具有可阻塞的读取操作。

7.5 使用ScheduledExecutor的温室控制器

正常的锁在任何时候都只允许一个任务访问一项资源,而计数信号量允许n个任务同时访问这个资源。

7.7 Exchanger

Exchanger是在两个任务之间交换对象的栅栏。当这些任务进入栅栏时, 他们各自拥有一个对象,当他们离开时,他们都拥有之前由对象持有的对象。
典型应用场景:一个任务在创建对象,这些对象的生产代价很高昂,而另一个任务在消费这些对象。

  1. 仿真
    通过使用并发,仿真的每个构件都可以成为其自身的任务,这使得仿真更容易编程。

8.1 银行出纳员仿真

9.性能调优
9.1 比较各类互斥技术

SynchronizationComparions用了模板方法设计模式,将所有共用代码都放置到基类中,并将所有不同的代码隔离在导出类的accumulate()和read()的实现中。
结论: 很明显,使用Lock通常会比使用synchronized要高效很多,而且synchronized的开销看起来变化范围太大, 而lock相对比较一致。
但是synchronized关键字的代码更具有可读性,所以应该以synchronized关键字入手,只有在性能调优时才替换为Lock对象。

9.2 免锁容器

Java SE5特别添加了新的容器,通过更加灵巧的技术来消除锁,从而提高线程安全的性能。
免锁容器背后的通用策略:对容器的读取和修改操作可以同时发生,只要读取者能看到完成修改的结果即可。修改是在容器数据结构的某个部分的一个单独副本上执行的,并且这个副本在修改过程中是不可视的。只有当修改完成时,被修改的结构才会自动的与主数据结构进行交换,之后读取者就可以看到这个修改了。

synchronized ArrayList无论读取者和写入者的数量是多少,都具有大致相同的性能——读取者与其他读取者竞争锁的方式与写入者相同。
但是,CopyOnWriteArrayList在没有写入者时,速度会快很多,并且在有五个写入者时,速度仍然明显的快。

向ConcurrentHashMap添加写入者的影响甚至还不如CopyOnWriteArrayList,这是因为ConcurrentHashMap使用了一种不同的技术,它可以明显地最小化写入所造成的影响。

9.3 乐观加锁

某些Atomic类允许执行所谓的乐观加锁。
意味着当你执行某项计算时,实际上没有使用互斥,但是在这项计算完成,并且你准备更新这个Atomic对象时,你需要使用一个名叫ccompareAndSet()方法,将新值和旧值一起提交给这个方法,如果旧值与他在Atomic对象中发现的值不一致,那么这个操作就失败,意味着某个其他任务已经于此操作期间修改了这个对象。

9.4 ReadWriteLock

ReadWriteLock对 向数据结构相对不平凡的写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化。
ReadWriteLock使得你可以同时拥有多个读取者,只要他们都不是如写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直到这个写锁被释放为止。

  1. 活动对象
    有一种可以替换多线程机制的方法叫做“活动对象”,因为每个对象都维护着它自己的工作器线程和消息队列,并且所有这种对象的请求都将进入队列排队,任何时刻只能运行其中的一个。

有了活动对象:

上一篇 下一篇

猜你喜欢

热点阅读