您的位置:首页 > 编程语言 > Java开发

JAVA并发编程小结

2014-07-23 15:47 162 查看

多线程简介

一、多线程的优势

多线程的优势随着摩尔定律的逐渐打破日趋明显。如今8核以上的处理器随处可见,对于一个普通8核cup来说,假如你的程序只有单一线程,那你的程序只能使用1/8的系统资源,剩余7/8被白白浪费;相反,如果你的程序中合理的使用了多线程,将能够通过提升对cup资源的利用率,来显著提升系统的吞吐率和性能。

二、多线程的风险

利用并发与多线程来提高系统性能已经被越来越多人所认可,但如何用好这把“双刃剑”,让系统良好运作,才是问题的关键。并发的风险分为安全性,活跃性和性能三个大类,其中安全性表示“不该做的没有做”,例如:没有共享状态被错误的并发修改;活跃性表示“该做的做了”,例如:没有出现死锁,死循环之类让程序永远处于等待或者循环中的情况;而性能则表示“做的快不快”,在保证安全和活跃的情况下,程序当然是越快越好。本文主要介绍这三类可能的风险以及如何做相应的避免。

并发基础

在进入正题之前,首先回顾几个基本概念和常见问题:

一、线程

线程是操作系统能调度的最小单位,一个线程必定属于某个进程,而一个进程可以包含很多线程。同一个进程的线程在同一时刻共享该进程的各种资源。

二、竞态条件

当某个计算的结果,会根据多个线程交替执行的时序不同而变化的时候,那么就会发生竞态条件。

三、可见性

共享状态在线程间的可见性并不总像我们直观感受的那样简单。在多线程的环境下,A线程写入某个值例如int a = 8之后,B线程去读该值a,未必能入我们所料的那样取到值8,有可能是0,或者其它我们曾赋予过该变量的值。这是因为JAVA内存模型JMM是共享内存型的内存模型,而不是直观感觉的顺序一致性内存模型。受重排序,CPU缓存等多方面影响,B线程的读操作未必能读到A线程写操作完成后的内容。

四、原子性

未经同步的状态的原子性同样遭到挑战。当多个线程共享一个64位的数值变量(例如double或long)时,由于JVM允许将64位的读操作或者写操作分解为两个32位的操作,所以并发修改的时候,某线程有可能读到一个值的高32位和另一个值的低32位。

同样,“先检查后执行”或者a++这样看似原子的操作,如果未经保护直接执行,也会出现竞态条件而导致结果不可预测。

安全性

线程安全的核心是对可变的,共享的状态的访问管理。在讨论这个复杂的问题之前,我们先来考虑可能的简化方案。

一、不共享

 对于可能被多线程执行的代码,首先考虑的策略是不共享,即通过线程封闭的技术手段来防止数据在多线程中共享,从根本上杜绝了安全隐患。线程封闭依靠的是代码级别的处理,主要有两种方案:

1、栈封闭

根据JAVA的语言特性,任何基本类型的局部变量都封闭在线程内;对于非基本类型的局部变量,只要不把引用对外发布,也是封闭在线程内的。简单的说,无状态对象一定是线程安全的,例如:spring的单例bean,虽然被成千上万线程共享,但只要没有共享状态,一切都是妥妥的。

2、ThreadLocal类

ThreadLocal是线程封闭的显式形式。它允许线程保存特定于该线程的值,并将该值封闭在线程内部。基于这种特性,它可以安全的实现线程内
的数据传递与共享,而不受其它线程的干扰。struts2框架的执行上下文ActionContext就是ThreadLocal的一个很好的应用,有兴趣的同学可以去
查看一下。

二、不可变

如果有状态不得不被多个线程共享,那么我们或许可以让它不可变,因为不可变的状态一定是线程安全的。

三、正确的同步

现在,到了最棘手的情况,就是既可变又需要共享的状态我们怎么办?对于这种情况,首先我们要明确共享需求:

1、如果我们只有可见性需求——例如,共享某个标志位:

可以使用volatile来修饰共享的状态,如 
<span style="font-size:14px;">public volatile boolean flag</span>

即可保证在不同线程间,该变量一旦修改,其它线程立刻可以看到它的最新值。此外,volatile修饰的64位变量(double或long)不会出现共享时的原子问题。

2、如果我们有唯一性需求——例如,建立自增序列:

可以采用3中方式:
1)使用同步sychnorized修饰所有变量读写的地方,以保证可见性和原子性
2)使用lock做同样的操作(可以使用高性能的读写锁)
3)用volitile修饰该变量,仅在可能产生竞态条件的递增方法上使用同步sychnorized

@ThreadSafe
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;

public int getValue() { return value; }

public synchronized int increment() {
return value++;
}
}
4)使用原子变量atomicInteger

3、其它需求

例如,控制有限人数共享某资源;
设立“起跑线”或“终点”让线程同时起步或终止;
设立循环屏障,每N次请求可放行一次等

请参见后面的并发组件一章,里面有大量现成的组件来满足常用的同步需求

四、安全的发布

上面谈到了对共享状态的控制,那么如何进行安全的在线程间共享对象呢?

1、不可变对象

所有域为final,并且正确构造(构造函数中没有将this逸出)的对象,是不可变对象。不可变对象天生就是线程安全的,可以随意共享

2、事实不可变对象

对于发布后状态不会再更改的对象,称为事实不可变对象。这类对象只要被安全发布,即可安全的共享。安全发布有以下方法

静态初始化或者构造函数中初始化对象引用
将对象引用保存到volatile或者AtomicReference对象中
将对象引用保存到final域
将对象引用发布到锁保护的域或者并发容器(Hashtable,sychronizedMap,ConcurrentMap,Vector,CopyOnWriteArrayList,sychronizedList,sychronizedSet,BlockingQueue,ConcurrentLinkedQueue)中

3、线程安全的对象

内部状态已经由锁保护,暴露出来的接口可以供多个线程安全的共享。

4、可变对象

可变对象指发布后仍可以修改状态的对象。既要安全发布,又要在使用时同步,才能确保线程安全。

活跃性

某种程度上来说,活跃性似乎是安全性的敌人。当你小心翼翼的用锁来保证线程安全的时候,一不小心又掉进了活跃性的陷阱。相比安全性,活跃性问题同样致命,因为一旦出现活跃性问题,唯一能做的就是重启应用了。

一、死锁

1、死锁的产生

我们都知道,两个线程各自持有对方需要的资源,又都拿着资源不放的时候,死锁形成了。上面描述的是最常见的死锁——锁
顺序死锁。不过,通常情况下,死锁不会如此明显。
public void transferMoney(Account fromAccount,//
Account toAccount,//
int amount
) {
synchronized (fromAccount) {
synchronized (toAccount) {
fromAccount.decr(amount);
toAccount.add(amount);
}
}
}
上述转账逻辑,看似总是先获取fromAccount的锁,后获取toAccout的锁,是相同的顺序,但不同线程的fromAccount和toAccount可能是相反的,因而形成了动态的锁顺序死锁。

2、死锁的解决

1)通过hash,来保证锁顺序的一致性
public void transferMoney(Account fromAccount,//
Account toAccount,//
int amount
) {
int order = System.identityHashCode(fromAccount)-System.identityHashCode(toAccount);

Object lockFirst = order>0?toAccount:fromAccount;
Object lockSecond = order>0?fromAccount:toAccount;

synchronized(lockFirst){
synchronized(lockSecond){
//do work
}
}

}

2)通过定时锁,让死锁出现时,及时有一方放弃锁
public void transferMoney(Account fromAccount,//
Account toAccount,//
int amount
) {
while(true){
if(fromAccount.lock.tryLock()){
try{
if(toAccount.lock.tryLock()){
try{
fromAccount.decr(amount);
toAccount.add(amount);
return;
}finally{
toAccount.lock.unlock();
}
}
}finally{
fromAccount.lock.unlock();
}
}
}
}


总的来说,确保线程在获取锁的时候能保持一致的顺序可以避免死锁的发生,这需要对代码的全局分析和评估。更好的做法是,尽可能的采用开放调用——将线程安全封装在对象内部,可以用同步代码块或者显示锁等手段来达到此目标。开放调用的程序远比持有锁时调用外部方法的程序安全,而且一旦出现死锁,更易于分析与定位。

二、其它

其它活跃性风险包括:饥饿、丢失信号和活锁

1、饥饿

当线程由于无法访问所需资源而不能继续执行的时候,就出现了“饥饿”。防止饥饿的方法主要是避免出现无法结束的结构,并在确实有必要的情况下,降低需要后台长时间执行的线程的优先级,从而提高系统的响应性。

2、活锁

活锁指的是线程没有阻塞,而是重复执行相同的操作,并且始终出错。这通常出现在处理消息事物的应用中,例如,处理器从消息队列中拿出一条信息,进行处理;但由于外部原因,处理出错,任务回滚;该任务被重新放回到消息队列队首,重复上面的错误,永远无法终止。解决活锁问题,应该在重试的时候引入随机机制,例如,将执行失败的任务放到队列队尾、等待一个随机时间再次重试等。

3、丢失信号

丢失信号指线程必须等待一个已经为真的条件,但在开始等待之前没有检查条件谓词(条件谓词指执行前必须为真的状态)。
概念虽然比较拗口,实际上只要在锁等待之前进行条件检查就可以避免
void stateDependent() throws InterruptedException{
sychronized(lock){
while(!conditionPredicate()){
lock.wait();
}
}
}


性能与伸缩性

多线程的程序是否总能提升性能?其实未必。相比单线程程序,多线程的程序会额外引入上下文切换、内存同步和阻塞等额外的开销,如果设计不合理,有可能适得其反。Amdahl定律告诉我们,系统的伸缩性取决于程序中可并行组件与串行组件所占的比重。独占锁的竞争,正是万恶之源,不仅强制增加了程序的串行率,也增加了上下文切换的开销。

一、减少锁竞争的原则

锁竞争的可能性主要取决于:1)锁的请求频率,2)锁的持有时间。我们的主要目标是减少二者的乘积。以下两个是设计锁要注意的基本原则。

1、缩小锁的范围

尽可能缩短锁的持有时间是一种有效的降低锁竞争的方式。在代码中,应尽可能的将无关代码移出同步块,例如耗时的操作或者会被阻塞的IO操作。

2、减小锁粒度

尽可能的采用多个相互独立的锁来保护独立的状态,而不是一个大的“全局锁”,这样可以有效减少每个锁上的竞争。

3、避免热点域

如果某个锁保护的数据被很多常用操作访问,那么这个数据就成为了热点域。热点域应该尽可能避免,CouncurrentHashMap中的size()提供了一个很好的例子。

二、减少锁竞争的常用技术手段

1、锁分解

如果一个锁要保护多个相互独立的状态变量,那么可以将这个锁分解为多个独立的锁,每个锁只保护一个变量,从而降低每个锁的请求频率。

2、锁分段

锁分段比锁分解更进了一步,对一组独立的对象进行分解。例如,在ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其实第N个散列桶由第(N mod 16)个锁来保护。所以这个并发集合可以支持多达16个并发的写入器。下面是个简化后的例子。
public class StripedMap {
// Synchronization policy: buckets
guarded by locks[n%N_LOCKS]
private static final int N_LOCKS = 16;  // 并发锁的数量
private final Node[] buckets;           // 散列桶
private final Object[] locks;           // 锁数组

private static class Node {             // 链表中的节点
Node next;
Object key;
Object value;
}

public StripedMap(int numBuckets) {    // 构造函数
buckets = new Node[numBuckets];
locks = new Object[N_LOCKS];
for (int i = 0; i < N_LOCKS; i++)
locks[i] = new Object();
}

private final int hash(Object key) {    // 计算值的存储位置,相当于散列函数
return Math.abs(key.hashCode() % buckets.length);
}

public Object get(Object key) {
int hash = hash(key);
synchronized (locks[hash % N_LOCKS]) {                   // 计算出由哪个锁来保护这个散列桶
for (Node m = buckets[hash]; m != null; m = m.next)  // 遍历这个散列桶,找到需要的值
if (m.key.equals(key))
return m.value;
}
return null;
}

public void clear() {
for (int i = 0; i < buckets.length; i++) {
synchronized (locks[i % N_LOCKS]) {                 // 将锁分段中的值清空
buckets[i] = null;
}
}
}
}


在ConcurrentHashMap源码中,桶被封装在了锁Segment的内部,不过原理上是一样的

3、放弃独占锁

如果可以,用并发容器、读写锁(ReadWriteReentrantLock)、原子变量(atomicInteger等)来代替独占锁,将会大大提升伸缩性。部分组件会在下面章节介绍。

并发组件

一、并发容器

说到并发容器,不得不先提到他们的鼻祖——臭名昭著的同步容器(Hashtable、Vector以及Collections.sychronizedXxx工厂方法创建的容器)。他们将所有状态都封装起来,并对全部共有方法进行同步。这样确实可以保证线程安全,但是由于每次只能被一个线程访问,所以性能相当差。

JDK5.0新增了大量并发容器,用以替换过时的同步容器,这可以极大的提升伸缩性并降低风险。比较常用的有ConcurrenHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue、LinkedBlockingQueue。
JDK6.0又新增了ConcurrentSkipListMap和ConcurrentSkipListSet,用来替换同步的SortedMap和SortedSet。还有双端队列ArrayDeque和LinkedBlockingDeque。这里重点介绍几个常用的。

1、ConcurrenHashMap

这个map在上一章介绍过,主要是使用了锁分段的技术来减小锁的粒度,从而允许较多线程并发的读写而互相不受干扰。提供了以下原子操作,以减少对同步的使用。
public interface ConcurrentMap<K, V> extends Map<K, V> {

/**
* 仅当K没有相应的映射值才插入
* @param key
* @param value
* @return
*/
V putIfAbsent(K key, V value);

/**
* 仅当K被映射到V时才移除
* @param key
* @param value
* @return
*/
boolean remove(Object key, Object value);

/**
* 仅当 K被映射到oldValue时才替换为newValue
* @param key
* @param oldValue
* @param newValue
* @return
*/
boolean replace(K key, V oldValue, V newValue);

/**
* 仅当K被映射到某个值时才替换为newValue
* @param key
* @param value
* @return
*/
V replace(K key, V value);
}
不过,由于并发容器不像同步容器那样做全局锁定,所以size()和isEmpty()这样的基于整个map的计算方法返回的结果往往只是一个近似值。分段锁依然使用了独占锁,如果更改成读写锁,性能可能会有进一步提升。

2、CopyOnWriteArrayList

CopyOnWriteArrayList提供了“写入时复制机制”,这种机制让容器在每次修改时,都会创建并重新发布一个新的容器副本。该容器是一个事实不可变对象,只要保证安全发布就能做到线程安全,而这一点由容器内部保证,因此读操作无需使用额外的同步就能确保线程安全。
它的局限性在于,仅当读操作远远高于写操作时,才应该使用该容器。例如,事件监听系统就是很好的使用场景,每次请求都要轮询监听器列表,而该列表也很少改变。

3、BlockingQueue

关于阻塞队列请参照我的另一篇文章《阻塞队列与生产者与消费者模式》

4、LinkedBlockingDeque

该双端队列是在BlockingQueue基础上的扩展,允许线程安全的从头或者尾部进行插入或者取出,可以实现工作密取模式。这个以后在专门的文章里做探讨吧。

二、同步工具

1、CountDownLatch
CountDownLatch是一种常用的同步工具类,它的作用相当于闸门:在特定状态到达之前,门关闭着,任何线程都阻塞在门口;在特定状态到达的瞬间,闸门突然打开,所有的线程都能够继续运行。下面的实例是展示了闭锁的两种典型用法:1)让线程同时开始,2)让线程同时结束。

// 一个CountDouwnLatch实例是不能重复使用的,也就是说它是一次性的,锁一经被打开就不能再关闭使用了,如果想重复使用,请考虑使用CyclicBarrier。
public class CountDownLatchTest {

// 模拟了100米赛跑,10名选手已经准备就绪,只等裁判一声令下。当所有人都到达终点时,比赛结束。
public static void main(String[] args) throws InterruptedException {

// 开始的倒数锁
final CountDownLatch begin = new CountDownLatch(1);

// 结束的倒数锁
final CountDownLatch end = new CountDownLatch(10);

// 十名选手
final ExecutorService exec = Executors.newFixedThreadPool(10);

for (int index = 0; index < 10; index++) {
final int NO = index + 1;
Runnable run = new Runnable() {
public void run() {
try {
// 如果当前计数为零,则此方法立即返回。
// 等待
begin.await();
Thread.sleep((long) (Math.random() * 10000));
System.out.println("No." + NO + " arrived");
} catch (InterruptedException e) {
} finally {
// 每个选手到达终点时,end就减一
end.countDown();
}
}
};
exec.submit(run);
}
System.out.println("Game Start");
// begin减一,开始游戏
begin.countDown();
// 等待end变为0,即所有选手到达终点
end.await();
System.out.println("Game Over");
exec.shutdown();
}
}


2、FutureTask

FutureTask是一种用来启动异步任务的工具,对于一些运行时间较长的计算,可以提前开启线程运行,实现与主线程的并行从而缩短程序执行时间。

package com.zhy.concurrency.futuretask;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
* 使用FutureTask来提前加载稍后要用到的数据
*
* @author zhy
*
*/
public class PreLoaderUseFutureTask
{
/**
* 创建一个FutureTask用来加载资源
*/
private final FutureTask<String> futureTask = new FutureTask<String>(
new Callable<String>()
{
@Override
public String call() throws Exception
{
Thread.sleep(3000);
return "加载资源需要3秒";
}
});

public final Thread thread = new Thread(futureTask);

public void start()
{
thread.start();
}

/**
* 获取资源
*
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
public String getRes() throws InterruptedException, ExecutionException
{
return futureTask.get();//加载完毕直接返回,否则等待加载完毕

}

public static void main(String[] args) throws InterruptedException, ExecutionException
{

PreLoaderUseFutureTask task = new PreLoaderUseFutureTask();
/**
* 开启预加载资源
*/
task.start();
// 用户在真正需要加载资源前进行了其他操作了2秒
Thread.sleep(2000);

/**
* 获取资源
*/
System.out.println(System.currentTimeMillis() + ":开始加载资源");
String res = task.getRes();
System.out.println(res);
System.out.println(System.currentTimeMillis() + ":加载资源结束");
}

}

运行结果:

[java] view
plaincopy





1400902789275:开始加载资源  

加载资源需要3秒  

1400902790275:加载资源结束  

可以看到,本来加载资源的时间需要3秒,现在只花费了1秒,如果用户其他操作时间更长,则可直接返回,极大增加了用户体验。(转自http://blog.csdn.net/mostbravebird/article/details/38065507)

3、Semaphore

计数信号量Semaphore,用来控制可以同时访问某个特定资源或者执行某特定代码块的线程数量。可以理解成一个允许多个线程同时进入的同步代码块。因此,信号量可以用来实现池或者阻塞容器
/**
* 使用Semaphore为容器设置边界
*/
public class BoundedHashSet<T> {
private final Set<T> set;
private final Semaphore sem;

public BoundedHashSet(int bound){
this.set = Collections.synchronizedSet(new HashSet<T>());
sem = new Semaphore(bound); //非公平
}

public boolean add(T t) throws InterruptedException{
sem.acquire(); //请求semaphore, permits-1或阻塞到permits > 0
boolean wasAdded = false;

try {
wasAdded = set.add(t);
return wasAdded;
} finally{
if (!wasAdded) //未添加成功则释放semaphore
sem.release();
}
}

public boolean remove(T t){
boolean wasRemoved = set.remove(t);
if (wasRemoved) //删除成功permits+1;
sem.release();
return wasRemoved;
}
}

4、CyclicBarrier

栅栏CyclicBarrier类似于闭锁CountDownLatch。区别是闭锁用于等待计数器减到0,而栅栏用于等待指定数量的其它线程到达后同时执行。栅栏相当于与闭锁两种典型应用中的前一种——控制所有线程同时开始,此外它还具有循环功能,能做到每N个线程到达,执行一次的效果,可用于迭代操作。
/**
* CyclicBarrier测试
*/
public class CyclicBarrierTest {

public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier barrier =
new CyclicBarrier(threadCount, new Runnable() {
@Override
public void run() { //最后一个线程到达栅栏时触发
System.out.println("all have finished.");
}
});

for (int i=0 ;i<threadCount; i++){
new Thread(new WorkThread(barrier)).start();
}
}

private static class WorkThread implements Runnable{
private CyclicBarrier barrier;

public WorkThread(CyclicBarrier barrier) {
this.barrier = barrier;
}

@Override
public void run() {
System.out.println(
Thread.currentThread().getId() + " Working...");
try {
barrier.await(); //当前线程阻塞直到最后一个线程到达
System.out.println(Thread.currentThread().getId() + " awaiting finished.");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}

三、线程池

线程池是用来管理线程生命周期的容器。如何正确的使用线程池,影响到程序的健壮性和性能。

1、线程池大小

过大的线程池,会导致CPU和内存资源的激烈竞争,可能耗尽资源使系统崩溃;过小的线程池,又会造成很多系统资源闲置,降低了吞吐率。
对于计算密集型任务,在CPU数量为N的系统上运行,线程池大小为N+1时,通常能实现最优利用率。通常情况下,真实的任务都会涉及IO等非计算密集型的操作,因此,如果期望CUP利用率为U(0<U<1),那线程池的大小可以设置为N*U*(1+非CUP使用时间/CPU使用时间)。

2、线程池配置

JDK内置了将近10种线程池,它们都是由静态工厂Executors类的工厂方法,调用ThreadPoolExecutor的构造函数创建的。这里只介绍线程池的构造函数,想了解上述内置线程池,只需查看工厂方法中传入的构造参数即可。

new  ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);


创建一个线程池需要输入几个参数:

corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。

runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。

ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。

SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。

ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。

RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。

AbortPolicy:直接抛出异常。

CallerRunsPolicy:只用调用者所在线程来运行任务。

DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

DiscardPolicy:不处理,丢弃掉。

当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。

TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

注意,当线程池大小达到基本大小后,再提交新的任务,会被缓存在任务队列中;如果任务队列已满,则会继续增大线程池大小,直到线程池的最大大小。如果达到最大大小后继续提交任务,则会触发饱和策略。

分布式环境中的并发

分布式环境中,大部分的并发控制都失去了效果,对同步的控制,需要新的思路和方法,下面这篇文章介绍的比较全面探索并发编程(七)------分布式环境中并发问题
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息