一个线程写入的数据可能被另一个线程所覆盖
如果两个或多个线程同时等待共享资源,而这些共享资源又必须由彼此来释放。以下四个条件同时满足时就会造成死锁:
以下机制可以预防死锁发生:
活锁发生在当系统中有两个线程因为对方的某种操作或行为不断改变自己的状态。其结果是它们都处于改变状态的循环中而无法继续执行。
例如有两个任务,任务1和任务2,它们同时需要两个资源:资源1和资源2。假设任务1拥有资源1,任务2拥有资源2。它们都无法获得所需的全部资源,它们释放了各自的资源然后开始新一轮的资源持有,释放循环。这种状态会一直持续,因此这两个任务永远没法终止。
?
资源饥饿发生在当系统中的一个任务永远无法获得所需的资源时。当一个资源可用时但同时有多个任务正在等待该资源,如果分配资源的算法不够优化,有可能一个任务永远无法分配到资源。一种解决办法是考虑公平性,资源分配算法在决定把资源分配给哪个等待的任务时考虑每个任务已等待该资源的时间。然而实现公平性可能导致额外的开销从而降低系统运行效率。
?
优先级倒置发生在当一个优先级较低的任务持有另一个优先级较高任务所需的资源时,优先级较低的任务会先于优先级高的任务被执行。
?
?
当开始实现一个算法时,我们可以从串行算法入手,这样做的好处是:
通过分析串行版本的算法来决定代码中的哪一部分可以用并行方法代替。需要特别关注那些运行时间较长的代码。代码中相互独立的循环部分是很有可能被改为并行处理的。例如一个系统需要初始化数据库连接池,加载配置文件,初始化一些对象。这些都是相互独立的步骤,可以用并行方法处理。
?
当你知道哪些代码可以被修改为并发处理,你就需要决定如何实现它。代码的修改一般会影响系统中以下两个主要部分:
设计并行算法的方法有以下两种:
并发处理的目的是为了提高处理的效率,所以你必须使用计算机所有可用的处理器或核。另一方面,当你使用同步机制时,你需要引入一些额外的必须执行的代码。一旦你把算法分解为许许多多细小的任务时,那些由于同步机制引入的额外代码有可能影响程序的运行效率。但如果你分解的任务数少于处理器或核的数量,程序则没有最大化地利用硬件资源。同时,你还需要考虑每一个线程需要做的工作量,如果一个线程的工作量大于其它的线程,这个线程就会决定程序的整体运行时间。因此你需要寻找一个相对的平衡点。
?
?
可以对比并行和串行两个版本的算法运行的结果以及所需的时间
?
如果并行算法没有达到预期,则需要重新审核代码找出影响性能的部分再做调整。
?
?
Java中最重要的同步机制:
Executor框架允许你把线程创建和管理并发任务执行区分开来。它让你只需要关心线程的创建。该框架包括以下主要的类:
Fork/Join 框架定义了一种特殊的executor,专门用于解决 divide and conquer 技术中遇到的问题。Fork/Join 专门针对细颗粒度的并行处理,它能够往任务队列中添加新的任务,同时也能从任务队列中取任务来执行。框架包含的主要的类和接口有:
大体可以把这些数据结构分为两大类:
以下是其中一些数据结构:
如同普通的设计模式,并发系统也有自己的设计模式:
?
一个线程在一定时间通知另外一个线程。最简单实现该设计模式是用 ReentrantLock 或者 Semaphore 类。示例代码:
class="java" name="code">public void task1() { section1(); commonObject.notify(); } public void task2() { commonObject.wait(); section2(); }
?以上代码中,section2()方法总是在section1()方法后被执行。
?
这种设计模式如同Signaling,线程1等待线程2触发某一事件,线程2同时等待线程1触发某一事件。
public void task1() { section1_1(); commonObject1.notify(); commonObject2.wait(); section1_2(); } public void task2() { section2_1(); commonObject2.notify(); commonObject1.wait(); section2_2(); }
?
Mutex实现一段关键代码的互斥,即一次只有一个线程允许访问此关键代码。在Java中,你可以使用 synchronized 关键字(可以用于保护一部分代码或整个方法),ReentrantLock类,或者Semaphore类。
public void task() { preCriticalSection(); lockObject.lock() // The critical section begins criticalSection(); lockObject.unlock(); // The critical section ends postCriticalSection(); }
?
这种设计模式如同Mutex,不同点是Mutex一次只允许一个线程访问关键资源,而Multiplex允许多个线程同时访问。例如当你有多个不同拷贝的资源可用时,多个线程就可以被允许同时访问。在Java里可以用Semaphore实现。
public void task() { preCriticalSection(); semaphoreObject.acquire(); criticalSection(); semaphoreObject.release(); postCriticalSection(); }
?
这种设计模式用来实现当你需要在一个公共点来同步多个线程时。线程到达同步点时必须等待,直到参与的所有线程都到达那个同步点时才能继续。Java使用CyclicBarrier类来实现。
public void task() { preSyncPoint(); barrierObject.await(); postSyncPoint(); }
?
读写锁设计模式是为了提高系统的性能,读写锁具有以下特性:
Java中使用ReentrantReadWriteLock来实现该设计模式。但你需要小心读操作线程和写操作线程的优先级,如果有太多读操作的线程,那么写操作的线程将需要长时间等待。
?
线程池由一组线程以及一个任务队列组成。通常线程池中线程的数量是固定的。如果任务队列中有任务需要处理,线程就会取任务执行,否则就等待直到有新的任务添加到任务队列中。Java并发包中包含了一系列实现了ExecutorService接口的类。
?
?该设计模式定义了如何使用全局变量或线程本地静态变量。当你使用线程本地存储时,每个线程将访问不同的变量。Java使用 ThreadLocal 类来实现这种模式。
当并发程序运行在多核或多处理器的计算机中。内存缓存可能会带来一些问题。内存缓存在一定程度上提高了运行效率,但是也带来了数据不一致性的问题。当一个线程修改了一个变量的值,它是修改了内存缓存中的变量值,而不是主内存里的变量值,这可能导致其它线程读取到的值是主内存里修改之前的旧值。
另外一个问题是编译器或代码优化器有可能对某些代码进行重新排序,以达到更优的运行效率,这在串行程序里是不会有问题的,但对并行系统来说,可能带来不可预见的运行结果。
为了解决这些问题,各个编程语言都引入了各自的内存模型。内存模型定义了一个任务修改了一个值后如何通知其它任务。Java的内存模型定义了以下几点:
?
你只能对那些相互独立的任务作并行处理。例如一个循环中,下一个循环需要上一个循环得出的数据,那你就不能使用并发处理该循环,因为任务之间是相互依赖的。
?
你能够使用底层的类来实现并发系统,但是应该尽量避免使用它们。Java提供了很多高级类使程序员更好地进行并发编程。例如你可以使用 Thread 或者 Lock 类来实现线程的创建和线程之间的同步。但是Java还提供了更方便的类诸如 executors 或者 Fork/Join 框架。
?
程序可能需要在不同的计算机中运行,因此必须考虑程序的可扩展性。
当你使用数据分解法设计并发系统时,不要假设运行程序的计算机核与处理器的数量。因为这些都是可变因素。你需要动态获取这些信息,例如使用Java的 Runtime.getRuntime().availableProcessors()来动态获取这些信息然后计算线程池中所需的线程数。虽然这种方法会有额外的开销,但是它让你的程序具有更强的可扩展性。
当你使用任务分解法设计并发系统时,考虑的问题将会更多。取决于算法中独立任务的数量,强制性增加任务的数量可能因为同步机制导致更多的开销。程序整体运行性能也许更加糟糕。认真分析算法确定你是否能够有一个动态的任务数量。
?
当需要使用Java类库时,阅读文档确定使用的类是否是线程安全,如果不是则需要:
例如当你需要在并行系统中使用 List 时,你不能使用ArrayList,因为它不是线程安全的,你需要使用诸如ConcurrentLinkedDeque,CopyOnWriteArrayList 或者 LinkedBlockingDeque。
?
如果几个线程同时需要使用相同的资源,那么代码中获取这些资源的顺序应该一致,例如任务1和任务2同时需要占有资源1和资源2以完成操作,那么这两个任务都必须按照相同的顺序获取这两个资源,比如先获取资源1,后获取资源2,如果顺序不同,则有可能造成死锁。
?
有些时候,你应该使用volatile,而不是同步机制。例如只有一个线程修改变量,其它线程都是读这个变量,你应该选择使用volatile。其它情况你需要使用锁,synchronized关键字,或者synchronization方法。
Java 5 开始出现了一些原子变量,这些变量是一些支持单个变量原子性操作的类。它们引入了一个先对比后设值的机制。如果变量的值等于旧的值,那么则更新变量值并返回 true,否则返回 false,该类型变量的更新不需要锁或者同步机制,因此性能优于任何的同步机制,Java 提供了以下原子变量:
锁允许实现一次只有一个线程访问锁控制的代码,所以锁机制控制的代码要尽可能小,它应该只包含一些共享数据的操作。而且锁机制控制的代码里不应该有一些不可控的代码,例如在代码里调用Callable,因为Callable里或许有一些阻碍当前线程的操作,例如IO操作
?