- 并发的多面性
用并发解决的问题大体上可以分为“速度”和“设计可管理性”两种
1.1 更快的执行
速度提高是以多核处理器的形式而不是更快的芯片的形式出现的。
阻塞:程序中的某个任务因为该程序控制范围之外的某些条件而导致不能继续执行。
并发通常是提高运行在单处理器上的程序的性能。
实现并发最直接的方式就是在操作系统级别使用进程。
编写多线程程序最基本的困难在于协调不同任务之间对内存和IO资源的使用。
1.2 改进代码的设计
Java的线程机制是抢占式的, 调度机制会周期性的中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到合理的时间去驱动他的任务。
协作式系统:每个任务都会自动地放弃控制,这要求程序员要有意识地在每个任务中插入某种类型的让步语句。
- 基本的线程控制
并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立任务中的每一个都将由执行线程来驱动。一个线程就是在进程中的一个单一的顺序控制流。
因此,单个进程可以拥有度多个并发执行的任务,但是你的程序使得每个任务都好像有其自己的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对象上都附着一个异常处理器。
- 共享受限资源
3.1 不正确的访问资源
3.2 解决共享资源竞争
防止资源冲突:上锁。
- 基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。
- 这意味着在给定时刻只允许一个任务访问共享资源。通常是通过在代码前加上一条锁语句来实现的。
- Java以synchronized关键字,为防止资源冲突提供了内置支持。当任务要执行被synchronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁。
要想控制对共享资源的访问,得先把他包装进一个对象,然后把所有要访问这个资源的方法标记为synchronized。
synchronized void f(){}
当在对象上调用任意synchronized的方法的时候,此对象会被加锁,这时候该对象上其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。
- 在使用并发时,将域设置为private,这样synchronized可以防止其他任务直接访问域。
- 一个任务可以多次获得对象的锁,也就是说锁可以叠加。
- 对象,方法和类都有锁。
使用显示的lock对象
Lock对象必须被显式的创建,锁定和释放。
使用Lock对象时,必须将unlock()放在try——finally的finally子句中。这样可以抛出异常,维护系统处于良好状态。
3.3 原子性与易变性
- 原子操作是不能被调度机制中断的操作,是简化的机制,有时候不安全。
- 原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。
- 当定义long或double变量时,如果使用volatile关键字,就会获得原子性。
- 如果将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作就都可以看到这个修改。确保了应用中的可视性。
- 如果多个任务在同时访问某个域,那么这个域就应该是volatile的,否则就只能通过同步来访问。
- 因此如果一个域完全由synchronized方法或语句块来防护,那就不必设置为volatile。
- 一个任务的任何写入操作对这个任务来说都是可视的,因此如果只需要在这个任务内部可视,就不需要设置volatile。
- 原子操作:对域中的值做赋值和返回操作通常都是原子性的
3.4 原子类
这些类被调整为可以使用在某些现代处理器上的可获得的,并且是在机器级别上的原子性,对性能调优有帮助。
可以用来替换synchronized关键字。但是依赖锁更安全。
3.5 临界区
临界区:希望防止过个线程同时访问方法内部的部分代码而不是整个方法。通过这种方法被分离出来的代码段被称为临界区,使用synchronized关键字建立。
也称为同步控制块,在进入此段代码前,必须得到syncObject对象的锁,如果其他线程已经得到这个锁,那么就得等到锁被释放以后,才能进入临界区。
优点:通过同步控制块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到显著提高。可以把一个非保护型的类,在其他类的保护和控制之下,应用于多线程环境。 也可以通过lock来创建临界区。
3.6 在其他对象上同步
synchronized块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象synchronized(this)。
有时候需要在另一个对象上同步,就必须确保所有相关任务都是在同一个对象上同步的。
3.7 线程本地存储
防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。
线程本地存储是一种自动化机制,可以为使用相同变量的每个线程都创建不同的存储。
- 终结任务
有些情况下,任务必须更加突然的中止。
4.1 装饰性花园
ExecutorService.awaitTermination()等待每个任务结束,如果所有的任务在超时时间到达之前全部结束,就返回true,否则返回false。
4.2 在阻塞时终结
sleep()将任务从执行转台变为被阻塞状态,有时候必须中止被阻塞的任务。
线程状态:
新建:被创建时的短暂状态,此时已经分配了必须的系统资源和执行了初始化。之后调度器会将它转变成可运行状态或阻塞状态。
就绪:只要调度器把时间片分配给线程,线程就可以运行。
阻塞:线程能够运行,但是有个条件阻止他的运行。
死亡:处于死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间片。
进入阻塞状态1.通过调用sleep使任务进入休眠状态。
2.通过调用wait()是线程挂起,知道线程得到了notify()或者notifyAll()消息,线程才会进入就绪状态。
3.任务在等待某个输入或输出完成。
4.任务驶入在某个对象上调用其同步控制方法,但是对象锁不可用。
如果有时候需要中止处于阻塞状态的任务,不能等待其跳出,而是要强制这个任务跳出阻塞状态。
4.3 中断
- 在Runnable.run()方法的中间打断它要棘手的多,可能需要清理资源,所以更像是抛出异常,需要仔细编写catch子句来正确清除所有事物。
- Thread包含interrupt()方法,因此可以终止被阻塞的任务,这个方法将设置线程的中断状态。
- 设置中断状态将抛出InterruptedEXception。
- 当抛出该异常或者任务调用了Thread.interrupted()时,中断状态将被复位。
- Thread.interrupted()提供了离开run()循环而不抛出异常的第二种方式。
可以中断对sleep()的调用,但是不能中断正在试图获取synchronized锁或者试图执行IO操作的线程。
解决方法是关闭低层资源。或者被阻塞的nio通道会自动中断。
被互斥阻塞
如果尝试着在一个对象上调用其synchronized方法,而这个对象的锁已经被其他任务获得。那么调用任务将被挂起,直至这个锁可获得。
4.4 检查中断
- 线程之间的协作
5.1 wait()与notifyAll()
wait()使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。
wait()会在等待外部世界产生变化的时候将任务挂起,并且只有在notify()或notifyAll()发生的时候,这个任务才会被唤醒去检查所产生的变化。
wait()提供了一种在任务之间对活动同步的方式。
调用sleep()和yield()都不会释放锁。
当一个任务在方法里遇到对wait()的调用的时候,线程的执行执行将被挂起,对象上的锁被释放,因为wait()将释放锁,这就意味着另一个任务可以获得这个锁,因此在该对象中的其他synchronized方法可以在wait()期间被调用。
“我已经刚刚做完能做的所有事情,因此我要在这里等待,但是我希望其他的synchronized操作在条件适合的情况下能够执行”
两种形式的wait()
- 第一个版本接受毫秒作为参数,与sleep()差不多,表示在此期间暂停,不同的是
- 1)在wait()期间锁是释放的
- 2)可以通过notify(),notifyAll(),或者令时间到期,从wait()中恢复执行。
- 第二个更常用的版本,不接受任何参数,表示将无限等待下去,知道线程接受到notify()或者notifyAll()消息。
wait(),notify(),notifyAll()是基类object()的一部分,而不是thread的一部分。因为这些方法操作的锁也是所有对象的一部分。
只能在同步控制方法或同步控制块里调用wait(),notify(),notifyAll()。
错失的信号
当两个线程使用notify()/wait()或者notifyAll()/wait()进行协作时,有可能会错过某个信号。
解决方法是应该再synchronized块中进行条件判断。
5.2 notify()&¬ifyAll()
- 使用notify()时,就必须被唤醒的是恰当的任务。
- 为了使用notify(),所有任务必须等待相同的条件。
- 如果使用了notify(),当条件发生变化时,必须只有一个任务能够从中受益。
- 如果这些条件有一条不满足,就必须使用notifyAll()
- notifyAll()并不会将任何处于wait()状态中的任务都唤醒
- 当notifyAll()因某个特定锁而被调用时,只有等待这个锁的任务才会被唤醒。
5.3 生产者与消费者
使用显示的Lock和Condition对象
- 使用互斥并允许任务挂起的基本类是Condition,你可以通过在Condition上调用await()来挂起一个任务。
- 当外部条件发生变化,意味着某个任务应该继续执行时,你可以通过调用signal()来通知这个任务。从而唤醒一个任务,或者调用signalAll()来唤醒所有在这个Condition上被其自身挂起的任务。
5.4 生产者--消费者与队列
wait()与notifyAll()通过一种非常低级的方式解决了任务互操作问题,即每次交互时都握手。
更高级的方法:使用同步队列来解决问题。同步队列在任何时候都只允许有一个任务插入或移除元素。
通常可以使用
- LinkedBlockingQueue 无界队列
- ArrayBlockingQueue 具有固定尺寸
吐司BlockingQueue
这里用了队列来实现面包的制作和消费。还是不太理解Executor到底是产生了几个线程,为什么会一直循环
5.5 任务间使用管道进行输入输出
通过输入输出在线程间进行通信很有用。提供线程功能的类库以“管道”的形式对线程间的输入输出提供了支持。PipedWriter&&PipedReader
PipedReader是可以中断的。
- 死锁
死锁:一个任务之间相互等待的连续循环,没有哪一个线程可以继续。
当下面四个条件同时满足的时候会发生死锁:
- 1)互斥条件,任务使用的资源中至少有一个是不能共享的。
- 2)至少有一个任务它必须持有一个资源并且正在等待获取一个被别的任务占有的资源
- 3)资源不能被任务抢占,任务必须把资源释放当做普通事件。
- 4)必须有循环等待。
防止死锁最容易的办法是破坏第四个条件。
- 新类库中的构件
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的温室控制器
- schedule():运行一次任务
- scheduleAtFixedRate():每隔规则的时间重复执行任务
7.6 Semaphore
正常的锁在任何时候都只允许一个任务访问一项资源,而计数信号量允许n个任务同时访问这个资源。
7.7 Exchanger
Exchanger是在两个任务之间交换对象的栅栏。当这些任务进入栅栏时, 他们各自拥有一个对象,当他们离开时,他们都拥有之前由对象持有的对象。
典型应用场景:一个任务在创建对象,这些对象的生产代价很高昂,而另一个任务在消费这些对象。
- 仿真
通过使用并发,仿真的每个构件都可以成为其自身的任务,这使得仿真更容易编程。
8.1 银行出纳员仿真
9.性能调优
9.1 比较各类互斥技术
SynchronizationComparions用了模板方法设计模式,将所有共用代码都放置到基类中,并将所有不同的代码隔离在导出类的accumulate()和read()的实现中。
结论: 很明显,使用Lock通常会比使用synchronized要高效很多,而且synchronized的开销看起来变化范围太大, 而lock相对比较一致。
但是synchronized关键字的代码更具有可读性,所以应该以synchronized关键字入手,只有在性能调优时才替换为Lock对象。
9.2 免锁容器
Java SE5特别添加了新的容器,通过更加灵巧的技术来消除锁,从而提高线程安全的性能。
免锁容器背后的通用策略:对容器的读取和修改操作可以同时发生,只要读取者能看到完成修改的结果即可。修改是在容器数据结构的某个部分的一个单独副本上执行的,并且这个副本在修改过程中是不可视的。只有当修改完成时,被修改的结构才会自动的与主数据结构进行交换,之后读取者就可以看到这个修改了。
- CopyOnWriteArrayList:写入将导致创建整个低层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全的执行。当修改完成时,一个原子性的操作将把新的数组换入,使得新的读取操作可以看到这个修改
- CopyOnWriteSet:将使用CopyOnWriteArrayList来实现其免锁行为。
- ConcurrentHashMap和ConcurrentLinkedQueue使用了类似的技术,允许并发的读取和写入。但是容器中只有部分内容而不是整个容器可以被复制和修改。
乐观锁
synchronized ArrayList无论读取者和写入者的数量是多少,都具有大致相同的性能——读取者与其他读取者竞争锁的方式与写入者相同。
但是,CopyOnWriteArrayList在没有写入者时,速度会快很多,并且在有五个写入者时,速度仍然明显的快。
向ConcurrentHashMap添加写入者的影响甚至还不如CopyOnWriteArrayList,这是因为ConcurrentHashMap使用了一种不同的技术,它可以明显地最小化写入所造成的影响。
9.3 乐观加锁
某些Atomic类允许执行所谓的乐观加锁。
意味着当你执行某项计算时,实际上没有使用互斥,但是在这项计算完成,并且你准备更新这个Atomic对象时,你需要使用一个名叫ccompareAndSet()方法,将新值和旧值一起提交给这个方法,如果旧值与他在Atomic对象中发现的值不一致,那么这个操作就失败,意味着某个其他任务已经于此操作期间修改了这个对象。
9.4 ReadWriteLock
ReadWriteLock对 向数据结构相对不平凡的写入,但是有多个任务要经常读取这个数据结构的这类情况进行了优化。
ReadWriteLock使得你可以同时拥有多个读取者,只要他们都不是如写入即可。如果写锁已经被其他任务持有,那么任何读取者都不能访问,直到这个写锁被释放为止。
- 活动对象
有一种可以替换多线程机制的方法叫做“活动对象”,因为每个对象都维护着它自己的工作器线程和消息队列,并且所有这种对象的请求都将进入队列排队,任何时刻只能运行其中的一个。
有了活动对象:
- 1.每个对象可以拥有自己的工作器线程。
- 2.每个对象都将维护对它自己的域的全部控制权
- 3.所有在活动对象之间的通信都将以在这些对象之间的消息形式发生。
- 4.活动对象之间的所有消息都要排队。