java 多线程并发实战 摘要(一)

并发简介

计算机中加入操作系统来实现多个程序的同时执行,主要是基于以下原因:

资源利用率:在某些情况下,程序必须等待某个外部操作执行完成,例如输入操作或输出操作等,而在等待时程序是无法执行其他任何工作。因此,如果在等待的同时可以运行另一个程序,那么将提高资源的利用率。

公平性:不同的用户和程序对于计算机上的资源有着相等的使用权。

便利性 : 在计算多个任务时,应该编写多个程序,每个程序执行一个任务并在必要时相互通信,这比只编写一个程序来计算所有的任务更容易实现。

一.线程的优势

1.线程可以有效的降低程序的开发和维护成本,同时提升复杂应用程序的性能。
线程还可以降低代码的复杂度,使代码更容易编写,阅读和维护。

2.多线程程序可以同时在多个处理器上执行,
如果设计正确,多线程程序可以通过提高处理器资源的利用率来提升系统吞吐率。

3.异步事件的简化处理, I/O 读写操作请求比较容易出错,
但如果每个请求都拥有自己的处理线程,那么在处理某个请求时发生的阻塞将不会
影响其他请求的处理

4.响应更灵敏的用户界面,在采用一个事件分发线程来替代主事件循环,事件线程中
执行的任务是短暂的,那么界面的响应灵敏度就较高,因为事件线程能够很快的处理用户
的动作


线程带来的风险

1.安全性问题:线程安全性可能是非常复杂的,在没有充足同步的情况下,
多线程中的操作执行顺序是不可预测的,甚至会产生奇怪的结果,
安全性的含义是“永远不发生糟糕的事情”。(竞态条件)

2.活跃性问题:活跃性问题的形式之一就是无意中造成的无限循环,包括死锁,讥饿,活锁。

3.性能问题:性能问题包括多个方面,例如服务时间过长,响应不灵敏,吞吐量过低,
资源消耗过高,或者伸缩性较低。


名词解析:

Timer :作用是使任务在稍后的时刻运行,或者运行一次,或者周期性的运行。

Servlet : Servlet框架用于部署网页应用程序以及分发来自HTTP客户端的请求。

RMI(远程方法调用):RMI使代码能够调用在其他JVM中运行的对象。


二.线程安全性

注解 : @ThreadSafe   , @NoThreadSafe , @GuardedBy 

1.在构建稳健 的并发程序时,必须正确的使用线程和锁


2.一个对象是否需要是现场安全的,取决于它是否被多个线程访问。

3.java的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,
但“同步”这个术语还包括volatile类型的变量,显示锁以及原子变量。

4.如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,
那么程序就会出现错误:有三种方式可以修复这个问题:
4.1 不在线程之间共享该状态变量。
4.2 将状态变量修改为不可变的变量
4.3 在访问状态变量时使用同步

5.当设计线程安全的类时,良好的面向对象技术,不可修改性,以及明晰的不可变性规范都
能起到一定的帮助作用。

6.在编写并发应用程序时,一种正确的编程方式:首先使代码正确运行,然后在提高代码的速度

7.在线程安全性的定义中,最核心的概念看是正确性,
正确性的含义是: 某个类的行为与其规范完全一致。

8.当多个线程访问某个类是,不管运行时环境采用何种调度方式或者这些线程将如果交替执行,
并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,
那么就称这个类是线程安全的。

9.在线程安全类中封装了必要的同步机制,因此客户端无须进一步采用同步措施

10.无状态对象一定是线程安全

11.竞态条件:由不恰当的执行时时序而出现不正确的结果是一种非常严重的情况。

12.线程安全类(AtomicLong :可以用原子方式更新的 long 值。),
在实际情况中,应尽可能的使用现有的现场安全对象来管理类的状态。与非现场安全对象
相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护
和验证现场安全性。

13.要保存 状态的一致性,就需要在单个原子操作中更新所有相关的状态变量

14.内置锁:java提供内置的锁机制来支持原子性:同步代码块。同步代码块包括两个部分:
一个作为锁的对象引用,一个作为由这个锁保护的代码块。以关键字synchrnized来修饰的方法 
是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。
java的内置锁相当于一种互斥体(或互斥锁),这意味着最多一个线程能持有这种锁,
当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,
如果B永远不释放锁,那么线程A也将永远的等下去。

15.重入:如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。

16.对于可能被多个线程同时的可变状态变量,在访问它时都需要持有同一个锁,在这种
情况下,我们称状态变量是由这个锁保护的。

17.每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。

18.对于每个包含对个变量的不变形条件,其中涉及的所有变量都需要由同一个锁来保护。

19.Serlvet框架的初衷,即Serlvet需要能同时处理多个请求。

20.要判断同步代码块的合理大小,需要在各种设计需求之间进行权衡,
包括安全性(这个需求必须得到满足),简单性和性能。

21.通常,在简单性与性能之间存在着相互制约因素。当实现某个同步策略时,一定不要盲目地
为了性能而牺牲简单性(这可能会破坏安全性)

22.当执行时间较长的计算或者无法快速完成的操作时(例如,网络I/O或者控制台I/O),
一定不要持有锁。


三.对象的共享

1.在没有同步的情况下,编译器,处理器以及运行时相等都可能对操作的执行顺序进行一些意想不到的
调整。在缺乏足够同步的多线程程序中,想要对内存操作的执行顺序进行判断,几乎无法得出正确的结论。

2.加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到并共享变量的最新值
所有执行读操作或者写操作的现场都必须在同一个锁上同步。

3.Volatile 变量:java语言提供了一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程、

4.仅当Volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确
性时需要对可见性进行复制判断,那么就不要使用Volatile变量。Volatile变量的正确使用方式包括:
确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标示一些重要的程序生命
周期事件的发生。(例如,初始化或关闭)

5.Volatile变量通常用做某个操作完成,发生中断或者状态的标志。

6.加锁机制既可以确保可见性又可以确保原子性,而Volatile变量只能确保可见性。

7.当且仅当满足以下所有条件时,才应该使用Volatile变量
7.1 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
7.2 该变量不会与其他状态变量一起纳入不变性条件中
7.3 在访问变量时不需要加锁

8. “发布”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。

9.当某个不应该发布的对象被发布是,这种情况就被称为 ”逸出“。不要在构造过程中使用This引用逸出

10.线程封闭:不共享数据,仅在单线程内访问数据,不需要同步。它是实现线程安全性的最简答方法之一

11.Ad-hoc线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。

12.栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。

13.不可变对象一定是线程安全的。

14.当满足以下条件时,对象才是不可改变的
14.1 对象创建以后其状态就不能修改
14.2 对象的所有域都是final类型
14.3 对象是正确创建的(在对象的创建期间,this引用没有逸出)

15.任何线程都可以在不需要额外同步的情况下安全访问不可变对象,即使在发布这些对象时没有使用同步。

16.可变对象必须通过安全的方式来发布,这通常意味着在发布和使用该对象的线程时都必须使用同步。

(可变对象----线程安全容器同步---将对象放到容器中---才能进行同步和安全发布)

要安全的发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见,一个正确构造的对象
可与通过以下方式来安全的发布。
16.1 在静态初始化函数中初始化一个对象引用
16.2 将对象的引用保存到Volatile类型的域或者AtomicReferance对象中。
16.3 将对象的引用保存到某个正确构造对象的final类型域中。
16.4 将对象的引用保存到一个由锁保护的域中。

17.在线程安全容器的内部的同步意味着,在将对象放入到某个容器。在线程安全库中的容器类提供
以下方法安全发布
17.1 通过一个键或者值放入Hashtable,synchronizedMap或者ConcurrentMap中,可以安全地将它
发布给任何从这些容器中访问它的线程。(无论是直接访问还是通过迭代器访问)
17.2通过将某个元素放入Vector,CopyOnWriteArrayList,CopyOnWriteArraySet,synchronizedList,
或synchronizedSet中,可以讲该元素安全的发布到任何从这些容器中访问该元素的线程。
17.3通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布
到任何从这些队列中访问该元素的线程。

18.要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器,静态初始化器有JVM
在类的初始化阶段执行,由于在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都
科员被安全地发布。

19.如果对象从技术上来看是可变的,但是其状态在发布后不会再改变,
那么把这种对象称为 ”事实不可变对象“

20.在没有额外的同步的情况下,任何线程都可以安全的使用被安全发布的事实不可变对象

21.对象的发布需求取决于它的可变性
21.1 不可变对象可以通过任意机制来发布
21.2 事实不可变对象必须通过安全方式来发布
21.3 可变对象必须通过安全方式来发布,并且必须是线程安全的安全的或者由某个锁保护起来

22. 在并发程序中使用和共享对象时,可以使用一些使用的策略
22.1 线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由
这个线程修改
22.2 只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,
但任何线程都不能修改它,共享的只读对象包括不可变对象和事实不可不变对象。
22.3 线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有
接口来井下访问而不需要进一步的同步。
22.4 保护对象:被保护的对象只能通过持有特定的锁来访问,保护对象包括封装在其他
线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。


四.对象的组合

1.在设计线程安全类的过程中,需要包含以下三个基本要素:
1.1 找出构成对象状态的所有变量
1.2 找出约束状态变量的不变形条件
1.3 建立对象状态的并发访问管理策略

2. 同步策略:定义了如何在不违背对象不变条件或后验条件的情况下对其状态的访问操作
进行协同。同步策略规定了如何将不可变性,线程封闭与加锁机制等结合起来以维护线程
的安全性,并且还规定了哪些变量由哪些锁来保护。

3.如果不了解对象的不变性条件与后验条件,那么就不能确保线程安全性。要满足在状态
变量的有效值或状态转换上的各种约束条件,就需要借助于原子性与封装性。

4.在单线程程序中,如果某个操作无法满足先验条件,那么就只能失败。但是在并发程序中
先验条件可能会由于其他线程执行操作而变成真。并发程序中一定要等到先验条件为真
然后再执行操作。

5.封装简化了线程安全类的实现过程,它提供了一种实例封闭机制通常也简称为 "封闭"
将数据封装在对象内部,可以讲数据的访问限制在对象的方法上,从而更容易确保
线程在访问数据时总能持有正确的锁。

6.实例封闭是构建线程安全类的一种最简单方式。

7.在java平台的类库中还有很多线程封闭的示例,其中有些类的唯一用途就是将非线程安全
的类转换为线程安全的类

8.封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性
时就无须检查整个程序。

9.java监视器模式仅仅是一种编写代码的约定,对于任何一种锁对象,只要自始至终都使用
该锁对象,都可以用来保护对象的状态。

10.如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效
状态转换,那么可以将线程安全性委托给底层的状态变量。

11.如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,
在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。

12.java 类库包含许多有用的“基础模块”类。通常我们应该选择重用这些现有类
而不是创建新的类:重用能降低开发工作量,开发风险,以及维护成本。

13.将同步策略文档化,在维护线程安全性时,文档是最强大的(同时也是最未被充分利用的)
工具之一。用户可以通过查阅文档来判断某个类是否是线程安全的,而维护人员也可以通过
查阅文档来理解其中的实现策略,避免在维护过程中破坏安全性,然而通常人们从文档中
获取的信息却是少之又少。 在文档中说明客户代码需要理解的线程安全性保证,以及代码维护
人员需要了解的同步策略。


五.基础构建模块

1.同步容器类包括Vector和Hashtable,这些类实现线程安全的方式是:将它们的状态封装起来,
并对每个公有方法进行同步,使得每次只有一个线程能访问容器的状态。同步容器类都是
线程安全的。

2.Vector:Vector 类可以实现可增长的对象数组。与数组一样,它包含可以使用整数索引进行访问的组件。
但是,Vector 的大小可以根据需要增大或缩小,以适应创建 Vector 后进行添加或移除项的操作。

3.Hashtable:此类实现一个哈希表,该哈希表将键映射到相应的值。任何非 null 对象都可以用作键或值。
为了成功地在哈希表中存储和获取对象,用作键的对象必须实现 hashCode 方法和 equals 方法。
Hashtable 的实例有两个参数影响其性能:初始容量 和加载因子。容量 是哈希表中桶 的数量,初始容量 
就是哈希表创建时的容量。注意,哈希表的状态为 open:在发生“哈希冲突”的情况下,单个桶会存储多个条目,
这些条目必须按顺序搜索。加载因子 是对哈希表在其容量自动增加之前可以达到多满的一个尺度。
初始容量和加载因子这两个参数只是对该实现的提示。关于何时以及是否调用 rehash 方法的具体细节则依赖于该实现。

4.并发容器:同步容器将所有对容器状态的访问都串行化,以实现它们的线程安全性。这种方法的代价是严重降低并发性,
当多个线程竞争容器的锁时,吞吐量将严重减低。 通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。

5. java 5.0 新增容器 Queue (队列) :传统的先进先出队列。

6. java 5.0 新增容器 BlockingQueue (阻塞队列):BlockingQueue扩展了Queue,增加了看阻塞的插入和获取等操作。
如果队列为空,那么获取元素的操作将一直阻塞,直到队列中出现一个可用的元素。如果队列已满(对于有界队列来说),
那么插入元素的操作将一直阻塞,直到队列中出现可用的空间。在"生产者-消费者"这中设计模式中,阻塞队列是非常
有用的。

7.concurrentHashMap (并发容器Map) :concurrentHashMap也是一个基于散列的Map,但它使用了一种完全不同的
加锁策略来提供更高的并发性和伸缩性。ConcurrentHashMap并不是将每个方法都在同一个锁上同步并使得每次
只能有一个线程访问容器。而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为分段锁。
在这种机制中,任意数量的读取线程可以并发地访问Map,执行读取操作的线程和执行写入操作的线程可以并发地
访问Map,并且一定数量的写入线程可以并发的修改Map,concurrentHashMap带来的结果是,在并发访问环境下将
实现更高的吞吐量,而在单线程环境中值损失非常小的性能。
concurrentHashMap与其他并发容器一起增强了同步容器类:它们提供的迭代器不会抛出
concurrentModificationException,因此不需要在迭代过程中对容器加锁。

8.CopyOnWritArrayList(并发容器List):CopyOnWritArrayList用于替代同步List,在某些情况下它提供了更好的并发性能,
并且迭代期间不需要对容器进行加锁复制。"写入时复制(Copy-On-Writ)"容器的线程安全性在于,只要正确的发布
一个事实不可变的对象,那么访问该对象时就不再需要进一步的同步。

9. 在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它们能抑制并防止产生过多的工作项,
使应用程序在负荷过载的情况下变得更加健壮。

10.应该尽早地通过阻塞队列在设计中构建资源管理机制----这件事情做的越早,就越容易。

11.java 6 新增容器 Deque(双端队列):实现了在队列头和队列尾的高效插入和移除,具体实现包括
ArrayDeque和LinkedBlockingDeque。

12.java 6 新增容器 BlockingDeque(双端阻塞队列):BlockingDeque 方法有四种形式,
使用不同的方式处理无法立即满足但在将来某一时刻可能满足的操作:
第一种方式抛出异常;
第二种返回一个特殊值(null 或 false,具体取决于操作);
第三种无限期阻塞当前线程,直至操作成功;
第四种只阻塞给定的最大时间,然后放弃。

13.Thread(线程): 是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。 
每个线程都有一个优先级,高优先级线程的执行优先于低优先级线程。
每个线程都可以或不可以标记为一个守护程序。当某个线程中运行的代码创建一个新 Thread 对象时,
该新线程的初始优先级被设定为创建线程的优先级,并且当且仅当创建线程是守护线程时,新线程才是守护程序。


14.Runnable(接口)  :Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。
类必须定义一个称为 run 的无参数方法。设计该接口的目的是为希望在活动时执行代码的对象提供一个公共协议。
例如,Thread 类实现了 Runnable。激活的意思是说某个线程已启动并且尚未停止。 

15.Thread提供了interrupt方法,用于中断线程或者查询现场是否已经中断,每个线程都有一个布尔类型的属性,
表示线程的中断状态,当中断线程时将设置这个状态。中断是一种协作机制,一个线程不能强制其他线程停止
正在执行的操作而去执行其他的操作。


16.处理中断的响应有两种基本选择:
16.1.传递InterruptedException: 避开这个异常通畅是最明智的策略----只需把InterruptedException传递给
方法的调用者。
16.2 恢复中断:有的时候不能抛出InterruptedException。例如当代码是 Runnable 的一部分时,在这些情况下
必须捕获InterruptedException,并通过调用当前线程上的interrupt方法恢复中断状态。

17.同步工具类可以是任何一个对象,只要它根据其自身的状态来协调线程的控制流。阻塞队列可以作为同步工具类
其他类型的同步工具类还是包括信号量(Semaphore),栅栏(Barrier),以及闭锁(Latch)。所用的同步工具类
都包含一些特定的结构化属性:它们封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,
此外还提供了一些方法对状态进行操作,以及另一些方法用于高效的等待同步工具类进入到预期状态。

18. 闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。闭锁的作用相当于一扇们:在闭锁到达
结束状态之前,这扇们一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开
并允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保存打开状态。
闭锁可以用来确保某些活动直到其他活动都完成才继续执行 例如:
18.1 确保某个计算在其需要的所有资源都被初始化之后才继续执行。
18.2 确保某个服务在其依赖的所有其他服务都已经启动之后才启动。
18.3 等待直到某个操作的所有参与者(例如,在多玩家游戏中的所有玩家)都就绪在继续执行,在这种情况中,
当所有玩家都准备就绪时,闭锁将到达结束状态。

19.CountDownLatch(类):一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。 
用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,
await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。
这种现象只出现一次——计数无法被重置。

20.FutureTask(类):可取消的异步计算。利用开始和取消计算的方法、查询计算是否完成的方法和获取计算结果的方法,
此类提供了对 Future 的基本实现。仅在计算完成时才能获取结果;如果计算尚未完成,则阻塞 get 方法。
一旦计算完成,就不能再重新开始或取消计算。

Future (接口):Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。
计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。
取消则由 cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。
一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,
则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。


21.FutureTask也可以用做闭锁,FutureTask表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable
并且可以处于以下3种状态:等待运行,正在运行,运行完成。当FutureTask进入完成状态后,它会永远停止在这个
状态上。

22.Callable(接口):返回结果并且可能抛出异常的任务。实现者定义了一个不带任何参数的叫做 call 的方法。 
Callable 接口类似于 Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。
但是 Runnable 不会返回结果,并且无法抛出经过检查的异常。
方法摘要 
 V call()  计算结果,如果无法计算结果,则抛出一个异常。 


23.FutureTask在Executor框架中表示异步任务,此外还可以用来表示一些时间较长的计算,这些计算可以
在使用计算结果之前启动。

24.Executor 框架 (接口):执行已提交的 Runnable 任务的对象。
此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法。
通常使用 Executor 而不是显式地创建线程。
方法摘要 
 void execute(Runnable command)  在未来某个时间执行给定的命令。 


25.计数信号量(Counting  Semaphore):计数信号量用来控制同时访问某个特定资源的操作数量,
或者同时执行某个指定操作的数量,计数信号量还可以用来实现某种资源池,或者对容器施加边界。

26.Semaphore 类: 一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,
在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,
从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,
Semaphore 只对可用许可的号码进行计数,并采取相应的行动

27.栅栏(Barrier)类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,
所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。
如果所有线程都到达了栅栏位置,那么栅栏将打开,此时所有线程都被释放,而栅栏将被重置
以便下次使用。

28.CyclicBarrier(类):一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。
在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。
因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。 

29.总结:
29.1. 可变状态是至关重要的 ,所有的并发问题都可以归结为如何协调对并发状态的访问。可变状态越少,
就越容易确保线程安全性。
29.2  尽量将域声明为final类型,除非需要它们是可变的。
29.3  不可变对象一定是线程安全的。不可变对象能极大地降低并发编程的复杂性,它们更为简单而且安全,
可以任意共享而无须使用加锁或者保护性复制等机构。
29.4  封装有助于管理复杂性。在编写程序安全的程序时,虽然可以讲所有数据都保存在全局变量中,但为什么
要这样做?将数据封装在对象中,更易于维持不变性条件:将同步机制封装在对象中,更易于遵循同步策略。
29.5  用锁来保护每个可变变量。
29.6  当保护同一个不变性条件中的所有变量时,要使用同一个锁。
29.7  在执行复合操作期间,要持有锁。
29.8  如果从多个线程中访问一个可变变量时没有同步机制,那么程序会出现问题。
29.9  不要故作聪明地推断出不需要使用同步。
29.10 在设计过程中考虑线程安全,或者在文档中明确指出它不是线程安全的。
29.11 将同步策略文档化。


六.任务执行

1.serversocket (类):此类实现服务器套接字。服务器套接字等待请求通过网络传入。它基于该请求执行某些操作,
然后可能向请求者返回结果。 服务器套接字的实际工作由 SocketImpl 类的实例执行。
应用程序可以更改创建套接字实现的套接字工厂来配置它自身,从而创建适合本地防火墙的套接字。

2.socket(类):此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。 
套接字的实际工作由 SocketImpl 类的实例执行。应用程序通过更改创建套接字实现的套接字工厂可以配置它自身,
以创建适合本地防火墙的套接字。

3.SocketImpl(抽象类):SocketImpl 是实际实现套接字的所有类的通用超类。创建客户端和服务器套接字都可以使用它。 
“普通”套接字严格按描述实现这些方法,无需尝试通过防火墙或代理。 

4.串行的执行任务,在服务器应用程序中,串行处理机制通常都无法提供高吞吐率或快速响应性。服务器的资源利用低,
因为当单线程在等待操作完成时才会继续往下执行。

5.显式地为任务创建线程:主线程不断的交替执行“接受外部连接”与“分发请求”等操作,对于每个连接,主线程都将
创建一个新线程来处理请求。
5.1 任务处理过程从主线程中分离出来,从而提高响应性。
5.2 任务可以并行处理,从而能同时服务多个请求。
5.3 任务处理代码必须是线程安全的,因为当有多个任务时会并发的调用。


6.无限制创建线程的不足:在生产环境中,“为每个任务分配一个线程”这种方法存在一些缺陷,尤其是当需要
创建大量的线程时:
6.1 线程生命周期的开销非常高。线程的创建与销毁并不是没有代价的。根据平台的不同,实际的开销也不同,
但线程的创建过程都会需要时间,延迟处理的请求,并且需要JVM和操作系统提供一些辅助操作。如果请求的
到达率非常高且请求的处理过程是轻量级的,例如大多数服务器应用程序就是这种情况,那么为每个请求创建
一个新线程将消耗大量的计算资源。

6.2 资源消耗。活跃的线程会消耗系统资源,尤其是内存。如果可圆形的线程数量多于可用处理器的数量,
那么有些线程将闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量线程在
竞争CPU资源时还将产生其他的性能开销。如果你已经拥有足够多的线程使所有CPU保存忙碌状态,
你那么在创建更多的线程反而会降低性能。

6.3 稳定性。在可创建线程的数量上存在一个限制。这个限制值将随着平台的不同而不同,并且受多个因素制约,
包括JVM的启动参数,Thread构造函数中请求栈大小,以及底层操作系统对线程的限制等。

7.Executor框架:Executor框架是个简单的接口,但它却为灵活且强大的异步任务执行框架提供了基础,
该框架能支持多种不同类型的任务执行策略。它提供了一种标准的方法将任务的提交过程与执行过程
解耦开来,并用Runnable来表示任务。Executor的实现还提供了对生命周期的支持,以及统计信息收集,
应用程序管理机制和性能监视等机制。
Executor基于生产者---消费者模式,提交任务的操作相当于生产者,执行任务的线程则相当于消费者。

8.执行策略:各种执行策略都是一种资源管理工具,最佳策略取决于可用的计算资源以及对
服务质量的需求。
每当看到下面这种形式的代码时:
new Thread(runnable).start();
并且你希望获得一种更灵活的执行策略时,请考虑使用Executor来代替Thread

9.线程池,从字面含义来看,是指管理一组同构工作现场的资源池。线程池是与工作队列(Work Queue)
密切相关的,其中在工作队列中保存了所有等待执行的任务。工作者线程(Worker Thread)的任务很简单:
从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。
“在线程池中执行任务”比“为每个任务分配一个线程”优势更多。通过重用现有的线程而不是创建新线程
可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。

10.类库提供了一个灵活的线程池以及一些有用的默认配置,可以通过调用Executor中静态工程方法
之一来创建一个线程池:
10.1  newFixedThreadPool: newFixedThreadPool将创建一个固定长度的线程池,每当提交一个任务时
就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不在变化(如果某个线程由于发生了
未预期的Exception而结束,那么线程池会补充一个新的线程)。

10.2  newCachedThreadPool: newCachedThreadPool将创建一个可缓存的线程池,如果线程池的当前规模
超过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池的规模
不存在任何的限制。

10.3 newScheduledThreadPool:  newScheduledThreadPool将创建一个固定长度的线程池,而且以延迟
或定时的方式来执行任务。 

10.4 newSingleThreadExecutor: newSingleThreadExecutor是一个单线的Executor,它创建单个工作线程
来执行任务,如果这个线程异常结束,会创建另一个线程来替代。newSingleThreadExecutor能确保依照
任务在队列中顺序来才串行执行(例如FIFO,LIFO,优先级)。

11.Executors (类):此包中所定义的 Executor、ExecutorService、ScheduledExecutorService、
ThreadFactory 和 Callable 类的工厂和实用方法。

12.Executor的生命周期问题由,Executor拓展了ExecutorService接口中的一些用于生命周期管理
的方法来管理。ExecutorService的生命周期有3种:运行,关闭和已终止。

13.ExecutorService(接口):专门管理生命周期的。

14.Executor执行的任务有4个生命周期阶段:创建,提交。开始和完成。

15.CompletionService (接口):将生产新的异步任务与使用已完成任务的结果分离开来的服务。
生产者 submit 执行的任务。使用者 take 已完成的任务,并按照完成这些任务的顺序处理它们的结果。
例如,CompletionService 可以用来管理异步 IO ,执行读操作的任务作为程序或系统的一部分提交,然后,当完成读操作时,
会在程序的不同部分执行其他操作,执行操作的顺序可能与所请求的顺序不同

16.ExecutorCompletionService(类):使用提供的 Executor 来执行任务的 CompletionService。
此类将安排那些完成时提交的任务,把它们放置在可使用 take 访问的队列上。
该类非常轻便,适合于在执行几组任务时临时使用。

17.总结:通过围绕任务执行来设计应用程序,可以简化开发过程,并有助于实现并发。Executor框架将任务提交
与执行策略解耦开来,同时还支持多种不同类型的执行策略。当需要创建线程来执行任务时,可以考虑使用
Executor。要想在将应用程序分解为不同的任务时获得最大的好处必须定义清晰的任务边界。某些应用
程序中存在着比较明显的任务边界,而在其他一些程序中则需要进一步分析才能揭示出粒度更细的并行性。


七.(线程和任务)取消与关闭

1.要使任务和线程能安全,快速,可靠地停止下来,并不是一件容易的事。java没有提供任何机制来安全地
终止线程。但它提供了中断,这是一种协作机制,能够使一个线程终止另一个线程的当前工作。

2.任务取消:如果外部代码能在某个操作正常完成之前将其置入“完成”状态,那么这个操作就可以称为可取消的。
取消某个操作的原因很多:用户请求取消,有时间限制的操作,应用程序事件,错误,关闭。

3.线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适的或者可能的情况下
停止当前工作,并转而执行其他的工作。

4.在java API或语音规范中,并没有将中断与任何取消语义关联起来,但实际上,如果在取消之外的其他操作中
使用中断,那么都是不合适的,并且很难支撑起更大的应用。

5.每个线程都有一个boolean类型的中断状态。当中断线程时,这个线程的中断状态将被设置为true。
在Thread中包含了中断线程以及查询线程中断状态的方法,interrupt方法能中断目标线程,
而isInterrupted方法能返回目标线程的中断状态。静态的interrupted方法将清除当前线程的中断状态,
并返回它之前的值,这也是清除中断状态的唯一方法

6.阻塞库方法:例如Thread.sleep和object.wait等,都会检查线程合适中断,并且在发现中断时提前返回。

7.调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。
对中断操作的正确理解是:它并不会真正地中断一个正在运行的线程,而只是发出中断请求,
然后由线程在下一个合适的时刻中断自己。(而这些时刻也被称为取消点)
通常,中断是实现取消的最合理方式。

8.中断策略:线程同样应该包含中断策略,最合理的中断策略是某种形式的线程级取消操作
或服务级取消操作。

9.由于每个线程拥有各自的中断策略,因此除非你知道中断该线程的含义,否则就不应该中断这个线程。

10.响应中断:
10.1  传递异常(可能在执行某个特定于任务的清除操作之后),从而使你的方法也成为了
可以中断的阻塞方法。
10.2  恢复中断状态,从而使调用栈中的上层代码能够对其进行处理。

11.只有实现了线程中断策略的代码才可以屏蔽中断请求,在常规的任务和库代码中都不应该屏蔽中断请求。

12.通过Futrue来实现取消,当Futrue.get抛出InterruptedException或TimeoutException时,
如果你知道不在需要结果,那么就可以调用Futrue.cancel来取消任务。

13.不可中断的阻塞:
13.1  java.io包中的同步socket I/O。在服务器应用程序中,最常见的阻塞I/O形式就是对套接子进行
读取和写入。虽然InputStream和OutputStream中的read和write等方法都不会响应中断,但通过关闭
底层的套接字,可以使得由于执行read或write等方法而被阻塞的线程抛出一个SocketException。

13.2  java.io包中的同步I/O

13.3  selector的异步I/O

13.4  获取某个锁。如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程
任务它肯定会获得锁,所以将不会理会中断请求。

14.处理不可中断的阻塞:如果一个线程由于执行同步的 Socket I/O 或者等待获得内置锁而阻塞,
那么中断请求只能设置线程的中断状态,除此之外没有其他任何作用。对于那些由于执行不可
中断操作而被阻塞的线程,可以使用类似于中断的手段来停止这些线程,但这要求我们必须
线程阻塞的原因。

15.应用程序通常会创建用于多个线程的服务,例如线程池,并且这些服务的生命周期通常比创建
它们的方法的生命周期更长。如果应用程序准备退出,那么这些服务所用于的线程需要结束。
由于无法通过抢占式的方法来停止线程,因此它们需要自行结束。
正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控。

16.线程的所有权是不可传递的:应用程序可以拥有服务,服务也可以拥有工作者线程,但应用程序
并不能拥有工作者线程,因此应用程序不能直接停止工作者线程。相反,服务应该提供生命周期方法
来关闭它自己以及它所拥有的线程。这样,当应用程序关闭服务时,服务就可以关闭所有的线程了。
在ExecutorService中提供了shutdown和shutdownNow等方法。
对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该
提供生命周期方法。

17.”毒丸“对象:”毒丸“是指一个放在队列上的对象,其含义是:"当等到这个对象时,立即停止。"
在FIFO(先进先出)队列中,”毒丸“对象将确保消费者在关闭之前首先完成队列中的所有工作,
在提交”毒丸“对象之前提交的所有工作会被处理,而生产者在提交了”毒丸“对象后,将不在提交
任何工作。
只有在生产者和消费者的数量都已知的情况下,才可以使用”毒丸“对象。

18. AbstractExecutorService(类) :提供 ExecutorService 执行方法的默认实现。此类使用 newTaskFor
返回的 RunnableFuture 实现 submit、invokeAny 和 invokeAll 方法,默认情况下,
RunnableFuture 是此包中提供的 FutureTask 类。例如,submit(Runnable) 的实现创建了一个关联 RunnableFuture 类,
该类将被执行并返回。子类可以重写 newTaskFor 方法,以返回 FutureTask 之外的 RunnableFuture 实现。 

19.shutdownNow的局限性:当通过shutdownNow来强行关闭ExecutorService时,它会尝试取消正在执行的任务,
并返回所有已提交但尚未开始的任务,从而将这些任务写入日志或者保存起来以便之后进行处理。

20.幂等性:即将任务执行多次与执行一次会得到相同的结果。

21.处理非正常的线程终止:当单线程的控制台程序由于发生了一个未捕获的异常而终止时,程序将停止运行, 
并产生与程序正常输出非常不同的栈追踪信息,这种情况是很容易理解的。

22.如果任何抛出了一个未检测异常,那么它将使线程终结,但会首先通知框架该线程已经终结。
然后,框架可能会用新的线程来代替这个工作线程,也可能不会,因为线程池正在关闭,或者当前
已有足够多的线程能满足需要。
ThreadPoolExecutor和Swing都通过这项技术来确保行为糟糕的任务不会影响到后续任务的执行。
当编写一个向线程池提交任务的工作者线程类时,或者调用不可信的外部代码市(例如动态加载的插件),
使用这些方法中的某一种可以避免编写得糟糕的任务或插件不会影响调用它的整个线程。

23.ThreadPoolExecutor (线程池类):线程池可以解决两个不同问题:由于减少了每个任务调用的开销,
它们通常可以在执行大量异步任务时提供增强的性能,
并且还可以提供绑定和管理资源(包括执行任务集时使用的线程)的方法。
每个 ThreadPoolExecutor 还维护着一些基本的统计数据,如完成的任务数

24.未捕获异常的处理:在Thread API中提供了UncaughtExceptionHandler (当 Thread 因未捕获的异常而突然终止时,
调用处理程序的接口。) 它能检测出某个线程由于未捕获的异常而终结的情况。

25.在运行时间较长的应用程序中,通常会为所有线程的未捕获异常指定同一个异常处理器,
并且该处理器至少会将异常信息记录到日志中。

26.要为线程池中的所有线程设置一个UncaughtExceptionHandler,需要为ThreadPool-Executor
的构造函数提供一个ThreadFactory。(与所有的线程操控一样,只有线程的所有者能够
改变线程UncaughtExceptionHandler。)
标准线程池允许当发生未捕获异常时结束线程,但由于使用了一个try-finally代码块来接收通知,
因此当线程结束时,将有新的线程来代替它。如果没有提供捕获异常处理器或者其他的故障通知机制,
那么任务会悄悄失败,从而导致极大的混乱。
如果你希望在任何由于发生异常而失败时获得通知,并且执行一些特定于任务的恢复操作,
那么可以讲任务封装在能捕获异常的Runnable或Callable中,或者改写ThreadPoolExecutor的afterExecutor方法。

27.JVM关闭:JVM既可以正常关闭,也可以强行关闭。正确关闭触发方式:
27.1  当最后一个“正常 (非守护)”线程结束时关闭
27.2  调用System.exit()
27.3  通过其他特定于平台的方法关闭
27.4  通过Runtime.halt
27.5  在操作系统中“杀死”JVM进程(例如发送SIGKILL)来强行关闭JVM。

28.Runtime(类):每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。
可以通过 getRuntime 方法获取当前运行时。 应用程序不能创建自己的 Runtime 类实例。

29.关闭钩子:在正常关闭中,JVM首先调用所有已注册的关闭钩子(Shutdown Hook),关闭钩子是指通过
Runtime.addShutDownHook 注册的但尚未开始的线程。

30.关闭钩子应该是线程安全的:它们在访问共享数据时必须使用同步机制,并且小心地避免发生死锁,
这与其他并发代码的要求相同。

31.关闭钩子可以用于实现服务或应用程序的清理工作,例如删除临时文件,或者清除无法由操作系统
自动清除的资源。

32.线程可以分为:两种普通线程和守护线程。在JVM启动是创建的所有线程中,除了主线程以外,
其他的线程都是守护线程,(例如垃圾回收器以及其他执行辅助工作的线程)。当创建一个
新线程时,新线程将继承创建它的线程的守护状态,因此在默认情况下,主线程创建的所有
线程都是普通线程。

33.普通线程与守护线程之间的差异仅在于当线程退出时发生的操作。当一个线程退出时,
JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作。
当JVM停止时,所有仍然存在的守护线程都将抛弃---既不会执行finally代码块,
也不会执行回卷栈,而JVM只是直接退出。

34.守护线程通常不不能用来替代应用程序管理程序中各个服务的生命周期。

35.终结器:当不在需要内存资源时,可以通过垃圾回收器来回收它们,但对于其他一些资源,
例如文件句柄或套接字句柄,当不在需要它们时,必须显示地交还给操作系统。为了实现这个
功能垃圾回收器对那些定了finalize方法的对象进行特殊处理:在回收器释放它们后,
调用它们的finalize方法,从而保持一些持久化的资源被释放。
由于终结器可以在某个有JVM管理的线程中运行,因此终结器访问的任何状态都可以能被多个
线程访问,这样看必须对其访问操作进行同步。

避免使用终结器。

36.总结:在任何,线程,服务以及应用程序等模块中的生命周期结束问题,可能会增加它们
在设计和实现时的复杂性,java并没有提供某种抢占式的机制来取消或终结线程,
相反,它提供了一种协作式的中断机制来实现取消操作,但这要依赖于如何构建取消操作的协议,
以及能否始终遵循这些协议。通过FutrueTask和Executor框架,可以帮助我们构建可取消的任务和服务。


八.线程池的使用

1.虽然Rxecutor框架为制定和修改执行策略都可以提供了相当大的灵活性,但并非所有的任务都能
适用所用的执行策略。有些类型的任务需要明确地指定执行策略:
1.1  依赖性任务
1.2  使用线程封闭机制的任务
1.3  对响应时间敏感的任务
1.4  使用ThreadLocal的任务:ThreadLocal使每个线程都可以拥有某个变量的私有”版本“。

2.在一些任务中,需要拥有或排除某种特定的执行策略。如果某些任务依赖于其他的任务,
那么会要求线程池足够大,从而确保它们依赖任务不会被放入等待队列中或被拒绝,
而采用线程封锁机制的任何需要串行执行。通过将这些需求写入文档,将来的代码维护人员
就不会由于使用了某种不合适的执行策略而破坏安全性或活跃性。

3.如果所有正在执行任务的线程都由于等待其他仍处于工作队列中的任务而阻塞,
那么会发生同样的问题。这种线程被称为线程饥饿死锁。

4.每当提交了一个有毅力性的Executor任务时,要清除地知道可能会出现线程”饥饿“死锁,
因此需要在代码或配置Executor的配置问价中记录线程池的大小限制或配置限制。

5.在平台雷宽的大多数可阻塞方法中,都同时定义了限时版本和无限时版本,例如Thread.join
BlockingQueue.put,CountDowbKatch.await以及Selsector.select等。如果等待超时,那么可以
把任务标识为失败,任何中止任务或者将任务重新放回队列以便随后执行。这样,无论任务
的最终结果是否成功,这种办法都能确保任务总能继续执行下去,并将线程释放出来执行一些
能更快完成任务。

6.线程池的理想大小取决于被提交任务的类型以及所部属系统的特性。在代码中通常不会
固定线程池的大小,而应该通过某种配置机制来提供,或者根据Runtime.availableProcessors
来动态计算。

7.如果需要执行不同类别的任务,并且它们之间的行为相差很大,那么应该考虑使用多个线程池
从而使每个线程池可以根据各自的工作负载来调整。

8.ThreadPoolExecutor为一些Executor提供了基本的实现,这些Executor是由Executors中的
newCachedThreadPool,newFixedThreadPool,newSingleThreadExecutor等工厂方法返回的。
ThreadPoolExecutor是一个灵活的,稳定的线程池,允许进行各种定制。

9.Executors(类):此包中所定义的 Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 
和 Callable 类的工厂和实用方法。此类支持以下各种方法: 
创建并返回设置有常用配置字符串的 ExecutorService 的方法。 
创建并返回设置有常用配置字符串的 ScheduledExecutorService 的方法。 
创建并返回“包装的”ExecutorService 方法,它通过使特定于实现的方法不可访问来禁用重新配置。 
创建并返回 ThreadFactory 的方法,它可将新创建的线程设置为已知的状态。 
创建并返回非闭包形式的 Callable 的方法,这样可将其用于需要 Callable 的执行方法中。 

10.线程的创建与销毁:线程池的基本大小,最大大小,以及存活时间等因素共同负责线程的创建与销毁。
通过调节线程池的基本大小和存活时间,可以帮助线程池回收空闲线程占有的资源,从而使得这些资源
可以用于执行其他工作。

11.管理队列任务:在有限的线程池中会限制可并发执行的任务数量。ThreadPoolExecutor允许提供一个
BlockingQueue来保持等待执行的任务。基本的任务排队方法有3种:无界队列,有界队列和同步移交。
队列的选择与其他的配置参数有关,例如线程池的大小等。

12.newFixedThreadPool和newSingleThreadExecutor 创建线程池在默认情况下将使用一个无界的队列
LinkedBlockingQueue。如果任务持续快速地到达,并且超过了下你猜猜处理它们的速度,那么
队列将无限制地增加。对于非常大的或者无界的线程池,可以通过使用SyncheonousQueue来避免任务排队,
以及直接将任务凑个生产者移交给工作者线程。SyncheonousQueue不是一个真正的队列,而是一种在线程
之间进行移交的机制,要将一个元素放入SyncheonousQueue中,必须有另一个线程正在等待接受这个元素。

13.一种更稳妥的资源管理策略是使用有界队列,例如ArrayBlockingQueue,有界的LinkedBlockingQueue,
PriorityBlockingQueue。在使用有界的工作队列时,队列的大小与线程池的大小必须一起调节。

14.当使用LinkedBlockingQueue或者ArrayBlockingQueue这样的FIFO(先进先出)队列时,
任务的执行顺序与它们的到达顺序相同。如果先进一步控制任务执行顺序,还可以使用
PriorityBlockingQueue,这个队列将根据优先级来安排任务。任务的优先级通过自然顺或
Comparator(如果任务实现了Comparable)来定义的。

15.对于Executor,newCachedThreadPool 工厂方法是一种很好的默认选择,它能提供比固定大小的线程池
更好的排队性能。当需要限制当前任务的数量以满足资源管理需求时,那么可以选择固定大小的线程池,
就像在接受网络客户请求的服务器应用程序中,如果不进行限制,那么很容易发生过载问题。

16.饱和策略:当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略
可以通过调用setRejectedExecutionHandler来修改。(如果某个任务提交到一个已被关闭的Executor
时,也会用到饱和策略。)JDK提供了几种不同的RejectedExecutionHandler实现,每种实行都包含
又不同的饱和策略:AbortPolicy,CallerRunsPolicy,DiscardPolicy和DiscardOldestPolicy。

17.“中止(Abort)”策略是默认的饱和策略,该策略将派出未检查的RejectedExecution-Exception.
调用者可以捕获这个异常,然后根据需求编写自己的处理代码。当新提交的任务无法保存到队列中
等待执行时,“抛弃(Discard)”策略会悄悄抛弃该任务。“抛弃最旧的(Discard-Oldest)”策略
则会抛弃下一个将被执行的任务,然后尝试重新提交新的任务。

18.“调用者运行(Caller-Runs)”策略实现了一种调节机制,该策略既不会抛弃任务,业不会抛出异常,
而是将某些人回退到调用者,从而降低新任务的流量。它不会在线程池的某个线程池中执行新的提交
的任务,而是在一个调用了execute的线程中执行该任务。

19.线程工厂:每当线程池需要创建一个线程时,都是通过线程池工厂方法来完成的,默认的线程工厂
方法创建一个新的,非守护的线程,并且不包含特殊的配置信息,通过指定一个线程工厂方法,
可以定制线程池的配置信息。

20.ThreadFactory (线程工厂接口):根据需要创建新线程的对象。使用线程工厂就无需再手工
编写对 new Thread 的调用了,从而允许应用程序使用特殊的线程子类、属性等等。       

21.总结:对于并发执行的任务,Executor 框架是一种强大且灵活的框架。它提高了大量
可调节的选项,例如创建线程和关闭线程的策略,处理队列任务的策略,处理过多任务的
策略,并且提供了几个钩子方法来扩展它的行为。然而,与大多数功能强大的框架一样,
其中有些设置参数并不能很好地工作,某些类型的任务需要特定的执行策略,而一些参数
组合则可能产生奇怪的结果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值