Concurrency
在java.util.concurrent.atomic和java.util.concurrent.locks包中定义了更多的类。java.util.concurrent.atomic包包含如下的类:
(1) AtomicBoolean
(2) AtomicInteger
(3) AtomicIntegerArray
(4) AtomicIntegerFieldUpdater(abstract)
(5) AtomicLong
(6) AtomicLongArray
(7) AtomicLongFieldUpdater(abstract)
(8) AtomicMarkableReference
(9) AtomicReference
(10) AtomicReferenceArray
(11) AtomicReferenceFieldUpdater(abstract)
(12) AtomicStampedReference
大多数类需要简单的解释,因为他们只是简单的定义了方法去自动更新值。比如,AtomicInteger类定义了addAndGet()方法,增加一个给定的值到AtomicInteger的当前值,返回更新后的值。在这个包中定义的抽象类是内部使用的,很少在你的应用中直接使用到。
除了java.util.concurrent包中的CountDownLatch, CyclicBarrier和Semaphore类,更多的同步方法定义在java.util.concurrent.locks包中:
(1) AbstractOwnableSynchronizer(abstract, 从API 5开始)
(2) AbstractQueuedLongSynchronizer(abstract, API 9)
(3) AbstractQueuedLongSynchronizer(API 9)
(4) AbstractQueuedSynchronizer(abstract)
(5) AbstractQueuedSynchronizer.ConditionObject
(6) LockSupport
(7) ReentrantLock
(8) ReentrantReadWriteLock
(9) ReentrantReadWriteLock.ReadLock
(10) ReentrantReadWriteLock.WriteLock
这些类通常不会用在Android应用中。可能你用的最多的是ReentrantReadWriteLock类,和它的ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WirteLock对,因为他们允许多个读线程去访问数据(只要没有写线程修改数据),写线程只能同时存在一个。这是一个通用的对象,当多个线程访问同一个用来读的数据,你期望去最大化吞吐量。
作为一个通用规则,在线程间共享数据添加了问题(吞吐量,并发问题)。同步问题可以非常复杂,成功的共享数据,你需要对问题有一个很好的理解。同步相关的调试问题是一个努力,所以你需要在尝试优化吞吐量之前简化。在任何优化开始之前集中于你的应用质量。
多核
最近出现了很多基于多核架构的安卓设备。比如,Samsung Galaxy Tab 10.1和Motorola Xoom平板使用双核处理器(Cortex A9 core)。一个多核处理器,可以同时执行多线程,不像一个单核处理器。也就是说,很简单可以看到这将如何提升性能的,因为一个双核处理器理论上可以做的是单核的两倍(其他的都是相同的情况下,比如,时钟周期)。尽管针对多核的优化不像听起来那么简单,而且存在一些警告,你的应用可以明显的看到多核处理器带来的额外的能力。双核的CPU的设备包括:
(1) Samsung Galaxy Tab 10.1
(2) Motorola Xoom
(3) Motorola Phonton 4G
(4) Motorola Droid 3
(5) HTC EVO 3D
(6) LG OPtimus 2X
(7) Samsung Galaxy Nexus
很多情况下,你不需要关心设备有多少个核。使用Thread对象或者AsyncTask委派特定的操作到一个单独的线程通常是足够的,你仍然可以在单核的处理器上创建多线程。如果处理器有几个核,线程将简单的运行在不同的处理器单元上,对你的应用来说是透明的。
也就是说,你真正的需要是使大多数的CPU去得到一个可接受的性能级别,设计算法特别是针对多核。
为了达到最好的性能,你的应用首先需要找出多少核是可用的,通过简单的调用RunTime.availableProcessors()方法,如Listing 5-15所示。
Listing 5-15 获取处理器的数量
// 在Galaxy Tab 10.1或者BeBox Dual603将会返回2,在Nexus S或者Logitech Revue将会返回1
final int proc = Runtime.getRuntime().availableProcessors();
通常,available processors的数量是1或者2,尽管将来的产品可能使用4核CPU。当前的安卓笔记本可能已经存在4核架构。依赖于你计划应用什么时候可用,你可能希望仅仅聚焦于1或者2核CPU,稍后发布用于更多核的更新。
NOTE:假设核的数量不总是2的幂次方。
为多核修改算法
第一章给出的一些Fibonacci算法是利用多核的很好的候选。我们从divide-and-conquer算法开始,实现在Listing 5-16给出(和第一章Listing 1-7给出的实现相同)。
Listing 5-16 Fibonacci的Divide-and-Conquer算法
public class Fibonacci {
public static BigInteger recursiveFasterBigInteger (int n) {
if (n > 1) {
int m = (n / 2) + (n & 1);
// 两个更简单的子问题
BigInteger fM = recursiveFasterBigInteger(m);
BigInteger fM_1 = recursiveFasterBigInteger(m - 1);
// 结果联合到计算原始问题的方案
if ((n & 1) == 1) {
// F(m)^2 + F(m-1)^2
return fM.pow(2).add(fM_1.pow(2));
} else {
// (2*F(m-1) + F(m)) * F(m)
return fM_1.shiftLeft(1).add(fM).multiply(fM);
}
return (n == 0)?BigInteger.ZERO : BigInteger.ONE;
}
}
}
这个算法做了divide-and-conquer算法做的:
(1) 原来的问题是分到更加简单的子问题
(2) 结果联合起来去计算原始问题
因为两个子问题是独立的,可以去并行执行他们,而不需要同步。Java语言定义了ExecutorService接口,几个实现类可以用来schedule要完成的工作。Listing 5-17给出一个示例,使用工厂模式去创建一个线程池。
Listing 5-17 使用ExecutorService
public class Fibonacci {
private static final int proc = Runtime.getRuntime().availableProcessors();
private static final ExecutorService executorService = Excutors.newFixedThreadPool(proc + 2);
public static BigInteger recursiveFasterBigInteger (int n) {
// 看Listing 5-16的实现
}
public static BigInteger recursiveFasterBigIntegerAndThreading (int n) {
int proc = Runtime.getRuntime().availableProcessors();
if ( n < 128 || proc <= 1) {
return recursiveFasterBigInteger(n);
}
final int m = (n / 2) + (n & 1);
Callable<BigInteger> callable = new Callable<BigInteger>() {
public BigInteger call() throws Exception {
return recursiveFasterBigInteger(m);
}
};
Future<BigInteger> ffM = executorService.submit(callable); // 尽早提交第一个job
callable = new Callable<BigInteger>() {
public BigInteger call() throws Exception {
return recursiveFasterBigInteger(m - 1);
}
};
Future<BigInteger> ffM_1 = executorService.submit(callable); // 提交第二个job
// 获取部分结果并且联合他们
BigInteger fM, fM_1, fN;
try {
fM = ffM.get(); // 得到第一个子问题的结果 (blocking call)
} catch (Exception e) {
// 如果有异常,在当前线程计算fM
fM = recursiveFasterBigInteger(m);
}
try {
fM_1 = ffM_1.get(); // 得到第二个子问题的结果(blocking call)
} catch (Exception e) {
// 如果有异常,在当前线程计算fM
fM_1 = recursiveFasterBigInteger(m-1);
}
if ((n & 1) != 0) {
fN = fM.pow(2).add(fM_1.pow(2));
} else {
fN = fM_1.shiftLeft(1).add(fM).multiply(fM);
}
return fN;
}
}
就像你可以清楚的看到的,代码很难阅读。更多的是,这个实现依然基于低性能的代码:就像我们在第一章看到的一样,两个子问题最终将计算许多相同的Fibonacci数。更好的实现是使用cache去记录已经计算的Fibonacci数,会明显的节省时间。Listing 5-18给出了相似的实现,不过使用了cache。
Listing 5-18 使用ExecutorService和Caches
public class Fibonacci {
private static final int proc = Runtime.getRuntime().availableProcessors();
pirvate static final ExecutorService executorService = Executors.newFixedTrheadPool(proc + 2);
private static BigInteger recursiveFasterWithCache (int n, Map<Integer, BigInteger> cache) {
// 查看Listing 1-11的实现(有一点不同,因为它使用SparseArray)
}
public static BigInteger recursiveFasterWithCache (int n) {
HashMap<Integer, BigInteger> cache = new HashMap<Integer, BigInteger>();
return recursiveFasterWithCache(n, cache);
}
public static BigInteger recursiveFasterWithCacheAndThreading (int n) {
int proc = Runtime.getRuntime().availableProcessors();
if (n < 128 || proc <= 1) {
return recursiveFasterWithCache(n);
}
final int m = (n / 2) + (n & 1);
Callable<BigInteger> callable = new Callable<BigInteger>() {
public BigInteger call() throws Exception {
return recursiveFasterWithCache(m);
}
};
Future<BigInteger> ffM = executorService.submit(callable);
callable = new Callable<BigInteger>() {
public BigInteger call() throws Exception {
return recursiveFasterWithCache(m - 1);
}
};
Future<BigInteger> ffM_1 = executorService.submit(callable);
// 获取部分计算结果并且联合它们
BigInteger fM, fM_1, fN;
try {
fM = ffM.get(); // 获取第一个子问题的结果(blocking call)
} catch (Exception e) {
// 如果发生异常,在当前线程计算fM
fM = recursiveFasterBigInteger(m);
}
try {
fM_1 = ffM_1.get(); // 获取第二个子问题的结果(blocking call)
} catch (Exception e) {
// 如果发生异常,在当前线程计算fM
fM_1 = recursiveFasterBigInteger(m-1);
}
if ((n & 1) != 0) {
fN = fM.pow(2).add(fM_1.pow(2));
} else {
fN = fM_1.shiftLeft(1).add(fM).multiply(fM);
}
return fN;
}
}
使用Concurrent Cache
在这个实现中需要注意的一个事情是每个子问题需要使用自己的cache对象,因此重复的数值仍然将被计算。针对这两个子问题共享一个cache,
我们需要把cache从SparceArray对象改变成允许不同线程同时访问的对象。Listing 5-19给出了这样一个实现,使用ConcurrentHashMap对象作为一个cache。
Listing 5-19 使用ExecutorService和一个Cache
public class Fibonacci {
private static final int proc = Runtime.getRuntime().availableProcessors();
private static final ExecutorService executorService = Executors.newFixedThreadPool(proc + 2);
private static BigInteger recursiveFasterWithCache (int n, Map<Integer, BigInteger> cache) {
// 查看Listing 1-11的实现(有一点不同,因为它使用了SparseArray)
}
public static BigInteger recursiveFasterWithCache (int n) {
HashMap<Integer, BigInteger> cache = new HashMap<Integer, BigInteger>();
return recursiveFasterWithCache(n, cache);
}
public static BigInteger recursiveFasterWithCacheAndThreading (int n) {
int proc = Runtime.getRuntime().availableProcessors();
if (n<128 || proc <= 1) {
return recursiveFasterWithCache(n);
}
final ConcurrentHashMap<Integer, BigInteger> cache = new ConcurrentHashMap<Integer, BigInteger>(); // 允许同步访问
final int m = (n / 2) + (n & 1);
Callable<BigInteger> callable = new Callable<BigInteger>() {
public BigInteger call() throws Exception {
return recursiveFasterWithCache(m, cache); // 第一个和第二个job共享同一个cache
}
};
Future<BigInteger> ffM = executorService.submit(callable);
callable = new Callable<BigInteger>() {
public BigInteger call() throws Exception {
return recursiveFasterWithCache(m-1, cache); // 第一个和第二个job共享cache
}
};
Future<BigInteger> ffM_1 = executorService.submit(callable);
// 获取部分计算结果并且联合他们
BigInteger fM, fM_1, fN;
try {
fM = ffM.get(); // 获取第一个子问题的结果(blocking call)
} catch (Exception e) {
// 如果发生异常, 在当前线程计算fM
fM = recursiveFasterBigInteger(m);
}
try {
fM_1 = ffM_1.get(); // 得到第二个子问题的结果(blocking call)
} catch (Exception e) {
// 如果发生异常,在当前线程计算fM
fM_1 = recursiveFasterBigInteger(m-1);
}
if ((n & 1) != 0) {
fN = fM.pow(2).add(fM_1.pow(2));
} else {
fN = fM_1.shiftLeft(1).add(fM).multiply(fM);
}
return fN;
}
}
NOTE: recursiveFasterWithCache的第二个参数是一个map,所以可以被任何实现Map接口的cache调用,比如ConcurrentHashMap或者HashMap对象。SparseArray对象不是一个map。
通过把一个问题分解为子问题并且为每一个问题分配一个线程,你不会经常观察到性能提升。因为数据之间依然存在依赖性,同步操作需要发生,线程可能花费一部分或者大多数时间去等待数据访问。同样的,性能提升可能不像你期望的那样明显。尽管理论上你可能期望去在双核的处理器上达到双倍在四核的处理器上达到4倍的性能,实际将不是这样的。
实际上,使用多线程去执行无关的任务更加简单(因此避免了同步的需求),如果频率"足够低",需要同步的任务是偶尔的或者经常的。比如,一个video游戏可能通常使用一个线程为了游戏逻辑,另一个线程去做渲染。渲染线程因此需要通过逻辑线程30或者60次每秒手动读取数据(每帧开始渲染的时候),可能相对的很快得到需要渲染一帧数据的copy,因此仅仅block访问很短的时间。