并发思想

  从性能的角度看,如果没有任务会阻塞,那么在单处理器机器上使用并发就没有任何意义。


线程机制分为:协作式和抢占式。Java的线程机制是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。在协作式系统中,每个任务都会自动地放弃控制,这要求程序员要有意识地在每个任务中插入某种类型的让步语句。协作式系统的优势是双重的:上下文切换的开销通常比抢占式系统要低廉的许多,并且对可以同时执行的线程数量在理论上没有任何限制。当你处理大量的仿真元素时,这可以一种理想的解决方案。但是注意,某些协作式系统并未设计为可以在多个处理器之间分布任务,这可能会非常受限。


非常常见的情况是,单个的Executor被用来创建和管理系统中所有的任务。对shutdown()方法的调用可以防止新任务被提交给这个Executor,当前线程将继续运行在shutdown()被调用之前提交的所有任务。


线程创建和管理类:CachedThreadPool, SingleThreadExecutor, FixedThreadPool


CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选。只有当这种方式会引发问题时,你才需要切换到FixedThreadPool.如果向SingleThreadExecutor提交了多个任务,那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有的任务将使用相同的线程。


Runnable是执行工作的独立任务,但是它并不返回任何值。如果你希望任务在完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一种具有类型参数的范性,它的类型参数表示的是从方法call()中返回的值,并且必须使用ExecutorService.submit()方法调用它。


你可以在一个任务的内部,通过调用Thread.currentThread()来获得对驱动该任务的Thread对象的引用。


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


可以通过调用isDaemon()方法来确定线程是否是一个后台线程。如果是一个后台线程,那么它创建的任何线程将被自动设置成后台线程。后台线程在不执行finally子句的情况下就会终止其run()方法。


在构造器中启动线程可能会变得很有问题,因为另一个任务可能会在构造器结束之前开始执行,这意味着该任务能够访问处于不稳


定状态的对象。这是优选Executor而不是显式地创建Thread对象的另一个原因。



一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复。


当另一个线程在该线程上调用interrupt()时,将给该线程设定一个标志,表明该线程已经被中断。然而,异常被捕获时将清理这个标志,所以在catch子句中,在异常被捕获的时候这个标志总是为假。除异常之外,这个标志还可用于其他情况,比如线程可能会检查其中断状态。



Thread.UncaughtExceptionHandler是Java SE5中的新接口,它允许你在每个Thread对象上都附着一个异常处理器。Thread.UncaughtExceptionHandler.uncaughtException()会在线程因未捕获的异常而临近死亡时被调用。为了使用它,我们创建了一个新类型的ThreadFactory,它将在每个新创建的Thread对象上附着一个Thread.UncaughtExceptionHandler.


class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {


public void uncaughtException(Thread t, Throwable e) {


    //to do some functions.


}


}


class HandlerThreadFactory implements ThreadFactory {


public Thread newThread(Runnable r) {


  Thread t = new Thread(r);


  t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());


  return t;


}


}


ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());


exec.execute(...);





在Java中,递增不是原子性操作。因此,如果不保护任务,即使单一的递增也不是安全的。





关于锁:所有对象都自动含有单一的锁(也称为监视器)。当在对象上调用其任意synchronized方法的时候,此对象都被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。


针对每个类,也有一个锁(作为类的Class对象的一部分),所以synchronized static 方法可以在类的范围内防止对static数据的并发访问。





每个访问临界共享资源的方法都必须被同步,否则他们就不会正确地工作。





private Lock lock = new ReentrantLock();





public int next(){


lock.lock();


try{


   //to do some work here!


   return 123;


} finally {


  lock.unlock();


}


}


当你在使用Lock对象时,将这里所示的惯用法内部化是很重要的:紧接着的对lock()的调用,你必须放置在finally子句中带有unlock()的try-finally语句中。注意:return语句必须在try子句中出现,以确保unlock()不会过早发生,从而将数据暴露给第二个任务。





原子性与易变性:


原子性可以应用于除long和double之外的所有基本类型之上的“简单操作”。对于读取和写入除long和double之外的基本类型变量这样的操作,可以保证它们会被当作不可分(原子)的操作来操作内存。但是,当你定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作的)原子性。


在多处理器系统上,相对于单处理器系统而言,可视性问题远比原子性问题多得多。一个任务做出的修改,即使在不中断的意义上讲是原子性的,对其他任务也可能是不可视的(例如,修改只是暂时性地存储在本地处理器的缓存中),因此不同的任务对应用的状态有不同的视图。另一方面,同步机制强制在处理器系统中,一个任务做出的修改必须在应用中是可视的。如果没有同步机制,那么修改时可视将无法确定。


volatile关键字还确保了应用中的可视性。如果你将一个域声明为volatile的,那么只要对这个域产生了写操作,那么所有的读操作都可以看到这个修改。即便使用了本地缓存,情况也确实如此,volatile域会立即被写入到主存中,而读取操作就发生在主存中。


理解原子性和易变性是不同的概念这一点很重要。在非volatile域上的原子操作不必刷新到主存中去,因此其他读取改域的任务也不必看到这个新值。如果多个任务在同时访问某个域,那么这个域就应该是volatile的,否则,这个域就应该只能经由同步来访问。同步也会导致向主存中刷新,因此如果一个域完全由synchronized方法或语句块来防护,那就不必将其设置为是volatile的。


一个任务所作的任何写入操作对这个任务来说都是可视的,因此如果它只需要在这个任务内部可视,那么你就不需要将其设置为volatile的。


当一个域的值依赖于它之前的值时(例如递增一个计数器),volatile就无法工作了。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,例如Range类的lower和upper边界就必须遵循lower<upper的限制。


使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。


synchronized关键字不属于方法特征签名的组成部分,所以可以在覆盖方法的时候加上去。


使用同步控制块而不是对整个方法进行同步控制的典型原因:使得其他线程能更多地访问(在安全的情况下尽可能多)。





中断:


Thread类包含interrupt()方法,因此你可以终止被阻塞的任务,这个方法将设置线程的中断状态。如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的终端状态将抛出InterruptedException。当抛出异常或者改任务调用Thread.interrupted()时,中断状态将被复位。


如果你在Executor上调用shutdownNow(),那么它将发送一个Interrupt()调用给它启动的所有线程。这么做是有意义的,因为当你完成工程中的某个部分或者整个程序时,通常会希望同时关闭某个特定Executor的所有任务。然而,你有时也会希望只中断某个单一任务。如果使用Executor,那么通过调用submit()而不是executor()来启动任务,就可以持有改任务的上下文。submit()将返回一个范性Future<?>,其中有一个未修饰的参数,因为你永远都不会在其上调用get()-持有这种Future的关键在于你可以在其上调用cancel(),并因此可以使用它来中断某个特定任务。如果你将true传递给cancel(),那么它就会拥有在改线程上调用interrupt()以停止这个线程的权限。


因此,cancel()是一种由Executor启动的单个线程的方式。


你能够中断对sleep()的调用,但是,你不能中断正在试图获取synchronized锁或者试图执行I/O操作的线程。


Java SE5并发类库中添加了一个特性,即在ReentrantLock上阻塞的任务具备可以被中断的能力,这与在synchronized方法或临界区上阻塞的任务完全不同:


public void f(){


try{


   lock.lockInterruptibly();


   //do something


  } catch(InterruptedException e) {


  }


}





被设计用来响应interrupt()的类必须建立一种策略,来确保它将保持一致的状态。这通常意味着所有需要清理的对象创建操作的后面,都必须紧跟try-finally子句,从而使得无论run()循环如何退出,清理都会发生。





使用notify()时,在众多等待同一个锁的任务中只有一个会被唤醒,因此如果你希望使用notify(),就必须保证被唤醒的是恰当的任务。另外,为了使用notify(),所有任务必须等待相同的条件,因为你有多个任务在等待不同的条件,那么你就不会知道是否唤醒了恰当的任务。如果使用notify(),当条件发生变化时,必须只有一个任务能够从中受益。最后,这些限制对所有可能存在的子类都必须总是起作用的。如果这些规则中有任何一条不满足,那么你就必须使用notifyAll()而不是notify();





Interface BlockingQueue<E>


A Queue that additionally supports operations that wait for the queue to become non-empty when retrieving an element, and wait for space to become available in the queue when storing an element.





BlockingQueue methods come in four forms, with different ways of handling operations that cannot be satisfied immediately, but may be satisfied at some point in the future: one throws an exception, the second returns a special value (either null or false, depending on the operation), the third blocks the current thread indefinitely until the operation can succeed, and the fourth blocks for only a given maximum time limit before giving up. These methods are summarized in the following table:











               Throws exception     Special value        Blocks            Times out


Insert               add(e)           offer(e)          put(e)       offer(e, time, unit)


Remove        remove()              poll()          take()           poll(time, unit)


Examine      element()           peek()           not applicable     not applicable


你通常可以使用LinkedBlockingQueue, 它是一个无界队列,还可以使用ArrayBlockingQueue,它具有固定的尺寸,因此你可以在它被阻塞之前,向其中放置有限数量的元素。





任务间使用管道进行输入/输出:PipedReader与普通I/O之间最重要的差异——PipedReader是可中断的。





CountDownLatch: 它被用来同步一个或多个任务,强制他们等待由其他任务执行的一组操作完成。可以向CountDownLatch对象设置一个初始计数值,任何在这个对象上调用wait()的方法都将阻塞,直至这个计数值到达。其他任务在结束其工作时,可以在改对象


上调用countDown()来减小这个计数值。CountDownLatch被设计为只触发一次,计数值不能被重置。


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


DelayQueue:这是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即队头对象的延迟到期的时间最长。如果没有任何延迟到期,那么就不会由任何头元素,并且poll()将返回null(正因为这样,你不能将null放置到这种队列中)。


Class PriorityBlockingQueue<E>


An unbounded blocking queue that uses the same ordering rules as class PriorityQueue and supplies blocking retrieval operations.


While this queue is logically unbounded, attempted additions may fail due to resource exhaustion (causing OutOfMemoryError). This class does not permit null elements. A priority queue relying on natural ordering also does not permit insertion of non-comparable objects (doing so results in ClassCastException).





Class ScheduledThreadPoolExecutor


A ThreadPoolExecutor that can additionally schedule commands to run after a given delay, or to execute periodically. This class is preferable to Timer when multiple worker threads are needed, or when the additional flexibility or capabilities of ThreadPoolExecutor (which this class extends) are required.


Delayed tasks execute no sooner than they are enabled, but without any real-time guarantees about when, after they are enabled, they will commence. Tasks scheduled for exactly the same execution time are enabled in first-in-first-out (FIFO) order of submission.


Semaphore:正常的锁在任何时刻都只允许一个任务访问一项资源,而计数信号量允许n个任务同时访问这个资源。你还可以将信号量看作是在向外分发使用资源的“许可证”, 尽管实际上没有使用任何许可证对象。


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





所有的控制系统都具有稳定性问题,如果它们队变化反映过快,那么他们就是不稳定的,而如果他们放映过慢,则系统会迁移到它的某种极端情况。





使用Lock通常会比使用synchronized要高效许多,而且synchronized的开销看起来变化范围太大,而Lock相对比较一致。Atomic对象只有在非常简单的情况下才有用,这些情况通常包括你只有一个要被修改的Atomic对象,并且这个对象独立与其他所有的对象。更安全的做法是:以更加传统的互斥方式入手,只有在性能方面的需求能够明确指示时,再替换为Atomic.免锁容器 免锁容器背后的通用策略是:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。修改是在容器数据结构的某个部分的一个单独的副本(有时是整个数据结构的副本)上执行的,并且这个副本在修改过程中是不可视的。只有当修改完成时,被修改的结构才会自动地与主数据结构进行交换,之后读取者就可以看到这个修改了。


尽管Atomic对象将执行像decrementAndGet()这样的原子性操作,但是某些Atomic类还允许你执行所谓的“乐观加锁”。这意味着当你执行某项计算时,实际上没有使用互斥,但是在这项计算完成,并且你准备更新整个Atomic对象时,你需要使用一个称为compareAndSet()的方法。你将旧值和新值一起提交给这个方法,如果旧值与它在Atomic对象中发现的值不一致,那么这个操作就失败——这意味着某个其他的任务已经与此操作执行期间修改了这个对象。记住,我们在正常情况下将使用互斥来防止多个任务同时修改一个对象,但是这里我们是“乐观的”,因为我们保持数据为未锁定状态,并希望没有任何其他任务插入修改它。所有这些又都是以性能的名义执行的——通过使用Atomic来替代synchronized或Lock,可以获得性能上的好处。





活动对象:


活动对象或行动者,之所以称这些对象是“活动的”,是因为每个对象都维护着它自己的工作器线程和消息队列,并且所有对这种对象的请求都将进入队列排队,任何时刻都只能运行其中的一个。因此,有了活动对象,我们就可以串行化消息而不是方法,这意味着不再需要防备一个任务在其循环的中间被中断这种问题了。


为了能够在不经意间就可以防止线程之间的耦合,任何传递给活动对象方法调用的参数都必须是只读的其他活动对象,或者是不连接对象,即没有连接任何其他任务的对象。有了活动对象:


1. 每个对象都可以拥有自己的工作器线程。


2.每个对象都将维护对它自己的域的全部控制权。


3.所有活动对象之间的通信都将以在这些对象之间的消息形式发生。


4.活动对象之间的所有消息都要排队。





线程的一个额外的好处是它们提供了轻量级的执行上下文切换(大约100条指令),而不是重量级的进程上下文切换(要上千条指令)。因为一个给定进程内的所有线程共享相同的内存空间,轻量级的上下文切换只是改变了程序的执行序列和局部变量。进程切换(重量级的上下文切换)必须改变所有内存空间。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值