[Think In Java]基础拾遗4 - 并发

第21章节 并发

1. 定义任务

任务:任务就是一个执行线程要执行的一段代码。(在C语言中就是函数指针指向的某个地址开始的一段代码)

【记忆误区】:任务不是线程,线程是用来执行任务的。即任务由线程驱动。在顺序编程中,因为就只有一个执行线程,所有的程序代码都由该执行线程执行,所有的这些程序代码就可以看成一个任务。而并发编程可以使我们将原本的一个大的程序代码划分为多个分离的、独立的部分。这些独立部分(被称为子任务)中的每一个都可以由一个执行线程来执行。

在java中任务是由Runnable接口来描述的。我们具体要执行的任务可以写在run()方法中,以便交给某个线程去执行。

 

2. 线程

光定义了任务还不行,还得创建线程去执行它。也就是说得将某个任务和某个线程关联起来。

在创建Thread对象时,可以通过Thread的构造函数传递一个Runnable对象,从而将线程和任务关联起来。

【提示】:一定要重点区分线程和任务直接的区别!对于深刻理解并发编程,区分这两个概念很重要。如上所述,如果让多个线程对象和同一个任务对象关联起来,有可能会发生数据不一致。

 

3.Executor

客户端程序只需要提交任务(也就是要执行的代码块)就行,不需要考虑创建线程、将线程与任务关联、线程调度之类的细节了。从而提供了一种标准的方法将任务的提交过程与任务的执行过程解耦开来。客户提交的任务具体怎么执行,客户不用管,全部都由Executor做了。一方面客户不用关注任务的执行细节,另一方面,由于客户不需要关注任务执行细节了,客户可以安静地坐在那儿等任务执行的结果,当然客户也可以选择去干其他事情,一旦任务的执行结果出来了,我们就通知客户就好了。这样就引出了异步任务的概念。

Executor接口只有一个方法:void execute(Runnable command) ; // 在未来某个时间执行给定的命令

很明显Executor的execute方法提交的任务只能是那些没有返回值的任务,但有的任务是会有返回值的。ExecutorService接口继承并扩展了Executor接口,使得可以提交有返回值的任务。

具体的实现细节参考ThreadPoolExecutor类源码。

 

4. Runnable与Callable

两者都是用于描述任务的。Runnable描述的任务没有返回值,Callable用于有返回值的任务,Callable任务返回的计算结果保存在Future对象中。同时Runnable不会抛出一个受检查的异常,而Callable则可能抛出一个异常。

 

5. 后台线程

必须在线程启动前调用setDaemon()方法,才能把线程设置为后台线程。

后台线程在不执行finally子句的情况下就会终止其run()方法。

 

6. 捕获异常

如果任务的run方法中抛出了异常,就会向外传播的main中,在main中做try-catch是没有用的。解决方案,创建Thread的后调用它的setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)方法设置异常处理器。

如果想在代码中处处使用相同的异常处理器,可以在Thread类中设置一个静态域,并将这个处理器设置为默认的未捕获异常处理器。Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

 

7. 解决共享资源竞争

基本上所有的并发模式在解决线程冲突问题的时候,都是采用“序列化访问共享资源”的方案。意味着在给定时刻只允许一个任务访问共享资源。

 

8. 原子性

对于除了long和double之外的所有基本数据类型,JVM可以保证读写的原子性。对于long和double则无法保证,因为JVM可以将64位的读取和写入当做两个分离的32位操作来执行,在这之间有可能发生上下文切换,这被称为“字撕裂”。在定义long和double变量时,如果使用volatile关键字则可以保证读写的原子性。

[少数程序员大牛可以用原子操作代替同步,但只是少数!]

 

9. 可视性 

在多处理器系统中,还有一个可视性的问题。一个任务做出的修改可能对其他任务是不可见的。(修改可能只是暂时性地存储在本地处理器的缓存中)。同步机制不但可以保证原子性,还有可视性的保证!

如果没有同步机制(这里指的是用原子性代替同步),那么修改时可视性将无法确定。volatile关键字还保证了修改的可视性!

[如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么就应该将这个域设置为volatile的。]

 

10. volatile

如果一个域的值依赖于它之前的值时(比如一个递增计数器),volatile就无法工作了。

如果某个域的值受到其他域的值的限制,volatile也无法工作![个人认为这里的无法工作指的是无法保证可视性。]

volatile唯一安全的情况是类中只有一个可变的域。

[最没有风险的方式就是使用synchronized关键字]

 

11. 原子类

AtomicInteger、AtomicLong、AtomicReference等类,它们主要用于java.uti.concurrent中的类,以及用于性能调用。

 

12. ThreadLocal类

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

[上述的"相同变量"往往指的是Runnable类中的域。传递同一个Runnable实例给两个不同的Thread对象,那么这些线程在访问同一个Runnable实例的同一个域的时候每个线程都会保存该域的一个副本。所以Runnable中的需要ThreadLocal域直接用static修饰就好了,没必要定义成实例变量。]

ThreadLocal类包装的字段通常是static变量,否则如果是实例变量则完全没必要。

 

13.  中断 

阻塞方法

当一个方法抛出 InterruptedException 时,它不仅告诉您它可以抛出一个特定的检查异常,而且还告诉您其他一些事情。例如,它告诉您它是一个阻塞(blocking)方法[即调用该方法会导致调用线程处于阻塞状态],如果您响应得当的话,它将尝试消除阻塞并尽早返回。

阻塞方法不同于一般的要运行较长时间的方法。一般方法的完成只取决于它所要做的事情,以及是否有足够多可用的计算资源(CPU 周期和内存)。而阻塞方法的完成还取决于一些外部的事件,例如计时器到期,I/O 完成,或者另一个线程的动作(释放一个锁,设置一个标志,或者将一个任务放在一个工作队列中)。一般方法在它们的工作做完后即可结束,而阻塞方法较难于预测,因为它们取决于外部事件。阻塞方法可能影响响应能力,因为难于预测它们何时会结束。

阻塞方法可能因为等不到所等的事件而无法终止,因此令阻塞方法可取消就非常有用。可取消操作是指能从外部使之在正常完成之前终止的操作。由 Thread 提供并受 Thread.sleep() 和 Object.wait() 支持的中断机制就是一种取消机制;它允许一个线程请求另一个线程停止它正在做的事情。当一个方法抛出 InterruptedException 时,它是在告诉您,如果执行该方法的线程被中断,它将尝试停止它正在做的事情而提前返回,并通过抛出 InterruptedException 表明它提前返回。 行为良好的阻塞库方法应该能对中断作出响应并抛出 InterruptedException,以便能够用于可取消活动中,而不至于影响响应。

线程中断

每个线程都有一个与之相关联的 Boolean 属性,用于表示线程的中断状态(interrupted status)。中断状态初始时为 false;当另一个线程通过调用 Thread.interrupt() 中断一个线程时,会出现以下两种情况之一。如果那个线程在执行一个低级可中断阻塞方法,例如 Thread.sleep()、 Thread.join() 或 Object.wait(),那么它将取消阻塞并抛出 InterruptedException。否则, interrupt() 只是设置线程的中断状态。 在被中断线程中运行的代码以后可以轮询中断状态,看看它是否被请求停止正在做的事情。中断状态可以通过 Thread.isInterrupted() 来读取,并且可以通过一个名为 Thread.interrupted() 的操作读取和清除。

中断是一种协作机制。当一个线程中断另一个线程时,被中断的线程不一定要立即停止正在做的事情。相反,中断是礼貌地请求另一个线程在它愿意并且方便的时候停止它正在做的事情。有些方法,例如 Thread.sleep(),很认真地对待这样的请求,但每个方法不是一定要对中断作出响应。对于中断请求,不阻塞但是仍然要花较长时间执行的方法可以轮询中断状态,并在被中断的时候提前返回。 您可以随意忽略中断请求,但是这样做的话会影响响应。

中断的协作特性所带来的一个好处是,它为安全地构造可取消活动提供更大的灵活性。我们很少希望一个活动立即停止;如果活动在正在进行更新的时候被取消,那么程序数据结构可能处于不一致状态。中断允许一个可取消活动来清理正在进行的工作,恢复不变量,通知其他活动它要被取消,然后才终止。

以上引自:《Java理论与实战:处理InterruptException》[IBM developerworks里面有很多高质量的文章,好好学习吧^_^,这里还有更多的java理论与实战的文章]

(1)如何给线程发中断消息?

如果有某个线程对象的引用,则直接调用Thread.interrupt();

executor.shutdownNow()会在executor启动的所有对象上调用interrupt()方法。

想中断executor启动的线程中的某一个线程该怎么办呢?通过executor.submit()方法提交任务,然后会获得一个Future对象,在Future对象上调用cancel(true)方法(参数值为true)即可。

(2)不是所有的阻塞都是可中断的

a. 试图执行IO操作的线程是不可中断的。比如InputStream的read()方法在输入数据可用、检测到流末尾或者抛出异常前,此方法一直阻塞。 在调用该read()方法的进程对象上调用interrupt()方法将不会响应中断。

b. 不能中断正在试图获取synchronized锁的线程。如果尝试着在一个对象上调用其synchronized方法,而这个对象的锁已经被其他任务获取了,那么调用任务将会被挂起(阻塞),直到这个锁可以获得。

小结:我们能够中断对sleep()的调用(或者任何会抛出InterruptException的调用)。但是不能中断正在试图获取synchronized锁或者试图执行IO操作的线程。 无论在任何时刻,只要任务以不可中断的方式被阻塞,那么都有潜在的会锁住程序的可能。

解决办法:

对于不可中断IO,可以关闭任务在其上发生阻塞的底层资源或者使用NIO(被阻塞的NIO通道会自动的响应中断);

对于试图获取synchronized锁的线程,可以使用ReentrantLock(在ReentrantLock上阻塞的任务具备可以被中断的能力)。

 

14. 线程之间的协作

之前处理的都是线程彼此之间的干涉问题,现在来考虑彼此之间的协作问题。

 

posted @ 2016-03-28 23:39  凝静志远  阅读(430)  评论(0编辑  收藏  举报