分享

Java编程思想读书笔记一:并发

 _王文波 2017-02-01
1. Thread.yield方法

当调用yield时,即在建议具有相同优先级的其他线程可以运行了,但是注意的是,仅仅是建议,没有任何机制保证你这个建议会被采纳。一般情况下,对于任何重要的控制或者调用应用时,都不能依赖于yield。这个方法经常被误用。

2.Runnable接口

当从Runnable导出一个类时,它必须具有run方法,但是这个方法并无特殊之处——它并不会产生任何内在的线程能力。要实现线程的行为必须显式的讲一个任务附着到线程上。

3.Join方法

一个线程可以在其他线程之上调用Join方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join,此线程将被挂起,直到目标线程t结束才恢复(即t.isAlive返回为假)。也可以在调用join时带上一个超时参数(单位可以是毫秒,或者毫秒和纳秒),这样如果目标线程在这段时间到期时还没有结束的话,join方法总能返回。对join方法的调用可以被中断,做法实是在调用线程上调用interrupt方法,这时需要调用try-catch子句。

4.互斥量(mutex)

基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。通常这是在代码前加上一条锁语句,使得在一段时间内只有一个线程可以运行这段代码。因为锁语句产生了一种能够互相排斥的效果,所以这种机制被称为互斥量(mutex)。

5.共享资源竞争

一个任务可以多次获得对象的锁。如果一个方法在同一个对象上调用了第二个方法,后者又调用了同一个对象上的另一个方法,就会发生这种情况。JVM负责跟踪对象被加锁的次数,如果一个对象被解锁,计数变为0。在任务第一次给对象加锁的时候,计数变为1。每当这个相同的任务在这个对象上获得锁,计数都会递增。显然,只有首先获得了锁的任务才能允许继续获取多个锁。每当任务离开一个synchronized 方法,计数递减,当计数为0的时候,锁被完全释放,其他任务可以使用此资源。

考虑一下屋子里的浴室:多个人(即多个线程)都希望能单独使用浴室(即共享资源)。为了使用浴室,一个人先敲门,看看是否能使用。如果没人的话,他就进入浴室并且锁上门。这时其它人要使用浴室的话,就会被“阻挡”,所以他们要在浴室门口等待,直到浴室可以使用。

当浴室使用完毕,就该把浴室给其他人使用了,这个比喻就有点不太准确了。事实上,人们并没有排队,我们也不能确定谁将是下一个使用浴室的人,因为线程调度机制并不是确定性的。实际情况是:等待使用浴室的人们簇拥在浴室门口,当锁住浴室门的那个人打开锁准备离开的时候,离门最近的那个人可能进入浴室。如前所述,可以通过yield和setPriority来给线程调度机制一些建议,但这些建议未必会有多大效果,这取决于你的具体平台和JVM实现。

Java以提供关键字synchronized的形式,为防止资源冲突提供了内置支持。它的行为很像Semaphore类:当线程要执行被synchronized关键字守护的代码片断的时候,它将检查信号量是否存在,然后获取信号量,执行代码,释放信号量。不同的是,synchronized内置于语言,所以这种防护始终存在,不像Semaphore那样要明确使用才能工作。

典型的共享资源是以对象形式存在的内存片断,但也可以是文件,输入/输出端口,或者是打印机。要控制对共享资源的访问,你得先把它包装进一个对象。然后把所有要访问这个资源的方法标记为synchronized。也就是说,一旦某个线程处于一个标记为synchronized的方法中,那么在这个线程从该方法返回之前,其它要调用类中任何标记为synchronized方法的线程都会被阻塞。

一般来说类的数据成员都被声明为私有的,只能通过方法来访问这些数据。所以你可以把方法标记为synchronized来防止资源冲突

6.原子性与可见性(volatile

当你定义long或double变量时,如果使用 volatile关键字,就会获到(简单的赋值与返回操作的)原子性(注意,在Java SE5之前,volatile一直不能正确的工作)。

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

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

一个任务所做的任何写入操作对这个任务来说都是可视的,因此如果它只需要在这个任务内部可视,那么你就不需要将其设置为volatile的。
当一个域的值依赖于他之前的值时(例如递增一个计数器),volatile就无法工作了。如果这个域的值受到其他的域的值的限制,那么volatile也无法工作,例如Range类的lower和upper边界就必须遵循lower<>upper的限制

基本上,如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么就应该将这个域设置为volatile的。如果将一个域定义为volatile,那么它就会告诉编译器不执行任何移除读取和写入操作的优化。这些操作的目的是用线程中的局部变量维护对这个域的精确同步。实际上,读取和写入都是直接针对内存的,而却没有被缓存。但是,volatile并不能对递增不是原子性操作这一事实产生影响。

使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。再次提醒,你的第一选择应该使用synchronized关键字,这是最安全的方式,而其他所有方式都是有风险的

7.原子类

Java SE5引入了诸如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性变量类,他们提供下面形式的原子性条件更新操作:

boolean compareAndSet(expectedValue, updateValue);

这些类调整为可以使用在某些现在处理器上的可获得的,并且是在机器级别的原子性,因此在使用它们时,通常不需要担心。对于常规编程来说,他们很少会派上用场,但是在涉及性能调优时,他们就大有用武之地了。
应该强调的是,Atomic类被设计用来构建java.util.concurrent中的类,因此只有在特殊情况下才在自己的代码中使用它们,即便使用了也需要确保不存在其他可能出现的问题。通常依赖于锁要更安全一些。

8.临界区

有时,我们只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法,通过这个方式分离出来的代码被称为临界区(critical section),他也使用synchronized关键字建立,这里,synchronized被用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制:

synchronized(syncObject) { // This code can be accessed// by only one task at a time }

这也被成为同步控制块;在进入此段代码前,必须得到syncObject对象的锁。如果其他线程已经得到这个锁,那么就得等到锁被释放后,才能进入临界区。通过使用同步控制卡,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到显著提高。

9.线程状态

1、新状态(new):线程对象已经创建,还没有在其上调用start方法。

2、可运行状态(Runnable):当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。当start方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。

3、运行状态(RUNNING):线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。

4、等待/阻塞/睡眠状态(Blocked):这是线程有资格运行时它所处的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态。

5、死亡态(Dead):当线程的run方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start方法,会抛出java.lang.IllegalThreadStateException异常。

10.进入阻塞状态

一个任务进入阻塞状态,可能有如下的原因:

1.通过调用sleep(milliseconds)使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。

2.你通过wait使线程挂起。直到线程得到了notifynotifyAll消息,线程才会进入就绪状态

3.任务在等待某个输入/输出完成。

4.任务试图在某个对象上调用其同步控制方法,但对象锁不可用,因为另一个任务已经获取了这个锁。

在较早的代码中,也可能会看到用suspend和resume方法来阻塞和唤醒线程,但是在Java新版本中这些方法被废弃了,因为它们可能导致死锁。stop方法也已经被废弃了,因为它不释放线程获得的锁,并且如果线程处于不一致的状态,其他任务可以在这种状态下浏览并修改它们。

现在我们需要查看的问题是:有时你希望能够终止处于阻塞状态的任务。如果对于阻塞状态的任务,你不能等待其到达代码中可以检查其状态值的某一点,因而决定让它主动终止,那么你就必须强制这个任务跳出阻塞状态。

11.中断

Thread类包含了interrupt方法,因此你可以终止被阻塞的任务,这个方法将设置线程的中断状态。如果一个线程已经被阻塞,或者试图执行一个阻塞操作,那么设置这个线程的中断状态将抛出InterruptedException。当抛出该异常或者该任务调用Thread.interrupted时,中断状态将被复位。正如你将看到的,Thread.interrupted提供了离开run循环而不抛出异常的第二种方式。

为了调用interrupt,你必须持有Thread对象。你可能已经注意到了,新的concurrent类库似乎在避免对Thread对象上的直接操作,转而尽量的通过Executor来执行所有操作。如果你在Executor上调用shutdownNow,那么它将发送一个interrupt调用给它启动的所有线程。这么做是有意义的,因为当你完成工程中的某个部分或者整个程序时,通常会希望同时关闭某个特定Executor的所有任务。然而,你有时也会希望只中断某个单一任务。如果使用Executor,那么通过调用submit方法而不是execute方法来启动任务,就可以持有该任务的上下文。submit将返回一个泛型Future<\?>,其中有一个未修饰的参数,因为你永远都不会在其上调用get——持有这种Future的关键在于你可以在其上调用cancel,并因此可以使用它来中断某个特定任务。如果你将true传递给cancel,那么它就会拥有在该线程上调用interrupt以停止这个线程的能力。因此,cancel是一种中断由Executor启动的单个线程的方式。

12.wait与notifyAll

wait使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。通常,这种条件将由另一个任务来改变。你肯定不想在你的任务测试这个条件的同事,不断地进行空循环,这杯称为忙等待,通常是一种不良的CPU周期使用方式。因此wait会在等待外部世界产生变化的时候将任务挂起,并且只有在notify或notifyAll发生时,即表示发生了某些感兴趣的事物,这个任务才被唤醒并去检查所发生的变化。因此wait提供了一种在任务之间对活动同步的方式。

调用sleep的时候锁并没有被释放,调用yield也属于这种情况,理解这一点很重要。另一方面,当一个任务在方法里遇到了对wait的调用的时候,线程的执行被挂起,对象上的锁被释放。因为wait将释放锁,这就意味着另一个任务可以获得这个锁,因此在该对象(现在是未锁定的)中的其他synchronized方法可以在wait期间被调用。因为这些其他的方法通常将会产生改变,而这种改变正是使被挂起的任务重新唤醒所感兴趣的变化。因此,当你调用wait时,就是在生命:“我已经刚刚做完所有能做的事情,因此我要在这里等待,但是我希望其他的synchronized操作在条件适合的情况下能够执行。”

有两种形式的wait,分别是sleep(long millis)和sleep,
第一种方式接受好描述作为参数,含义与sleep方法里参数的意思相同,都是指“在此期间暂停”。但与sleep不同的是,对于wait而言:
在wait期间,对象锁是释放的可以通过notify、notifyAll,或者令时间到期,从wait中恢复执行
第二种,也是更常用形式,它不接受任何参数,这种wait将无线等待下去,直到线程接受到notify或者notifyAll消息。
wait、notify、notifyAll有一个比较特殊的方面,那就是这些方法的基类是Object的一部分,而不是Thread类的一部分。尽管开始看起来有点奇怪——仅仅针对线程的功能却作为通用基类的一部分而实现,不过这是有道理的,因为这些方法操作的锁也是所有对象的一部分。实际上,只能在同步控制方法或者同步控制块里调用wait、notify、和notifyAll(因为不用操作锁,所以sleep可以在非同步控制方法里调用)。

如果在非同步控制方法里调用这些方法,程序能通过编译,但在运行的时候,将得到IllegalMonitorStateException异常,并伴随着一些模糊的消息,比如:当前线程不是锁的拥有者。消息的意思是,调用wait、notify和notifyAll的任务在调用这些方法之前必须“拥有”(获取)对象的锁。

可以让另一个对象执行某种操作以维护其自己的锁。要这么做的话,必须首先得到对象的锁。比如,如果要向对象x发送notifyAll,那么就必须在能够得到x的锁的同步块中这么做:

synchronized(x) { x.notifyAll; }

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

notifyAll将唤醒“所有正在等待的任务”并不意味着在程序中的任何地方、任何处于等待状态的任务都将被唤醒,而仅仅只是等待这个锁的任务才会被唤醒。

13.免锁容器

这些免锁容器背后的通用策略是:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。修改时在容器数据结构的某个部分的一个单独副本(有时是完整的数据结构的副本)上执行的,并且这个副本在修改过程是不可视的。只有当修改完成后,被修改的结构才会自动的与主数据结构进行交换,之后读取者就可以看到这个修改了。

在CopyOnWriteArrayList中,写入操作将导致创建整个底层数组的副本,而原数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全的执行。当修改完成后,一个原子性的操作将把新的数组换入,使得新的读取操作可以看到这个新的修改。CopyOnWriteArrayList的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException,因此你不必编写特殊的代码区防范这种异常。

ConcurrenthashMap和ConcurrentLinkedQueue使用了类似的技术,允许并发的读取和写入,但是容器中只有部分内容而不是整个容器可以被复制和修改。然而,任何修改在完成之前,读取者仍然不能看到他们。ConcurrentHashMap不会抛出ConcurrentModificationException异常。

14.读写锁

读写锁可以用于 “多读少写” 的场景,读写锁支持多个读操作并发执行,写操作只能由一个线程来操作。

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

ReadWriteLock 对程序心性能的提高受制于如下几个因素也还有其他等等的因素。
1)数据被读取的频率与被修改的频率相比较的结果。
2)读取和写入的时间
3)有多少线程竞争
4)是否在多处理机器上运行

15.内部类

一般来说,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外围类的对象。所以可以认为内部类提供了某种进入其外围类的窗口。

但是,如果只是需要一个对接口的引用,为什么不通过外围类实现那个接口呢?原因在于:后者不是总能享用到接口带来的方便,有时需要用到接口的实现。所以,使用内部类最吸引人的原因是:

每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。

如果没有内部类提供的,可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类时得多重继承的解决方案变的完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(类或者抽象类)。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多