文本介绍多线程和资源的关系。
是否多线程越多速度越快?
结论:并发编程中采用多线程,并非线程越多执行效率越高。
线程执行是由CPU分配较短的时间片,线程在得到时间片时执行,并在时间片后挂起,并切换其他线程执行。
线程切换时先会保存上一个线程状态,并加载当前线程的历史状态。
因为线程的上下文切换需要时间,影响多线程执行速度。
在执行某些简单、快速任务情况下,多线程执行效率还不如单线程执行。
假如单核采用单线程执行任务仅需要20ms,但单核采用多线程,并发100,可能创建线程时间就超出20ms,再加上单核只能执行单线程,需要多线程频繁挂起、上下文切换肯定慢。在低核时,不建议采用多线程进行CPU密集型计算;建议采用多线程执行监听输入、读取文件、网络通信等IO密集型操作。
如何减少上下文切换
采用无锁的并发编程,即不进行锁竞争;例如根据ID进行Hash取模,多线程进行分段处理数据。
我们常见的JDK1.7中ConcurrentHashMap就是锁分段技术,对key进行hashcode,然后取桶位置,默认1/16;在更新数据时锁数据所在桶,不影响其他桶并发操作,以提高并发处理数据速度。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public V put(K key, V value) {
if (value == null)
throw new NullPointerException();
// 计算键对应的散列码
int hash = hash(key.hashCode());
// 根据散列码找到对应的 Segment
return segmentFor(hash).put(key, hash, value, false);
}
/**
* 使用 key 的散列码来得到 segments 数组中对应的 Segment
*/
final Segment<K,V> segmentFor(int hash) {
// 最后根据下标值返回散列码对应的 Segment 对象
return segments[(hash >>> segmentShift) & segmentMask];
}CAS算法,JAVA采用Atomic的CAS算法进行乐观锁更新数据,不进行加锁。
所谓CAS其实就是compareAndSwap即比较并交换,参数有3个为old、expect和update;即如果old和expect仍然一致,没有因为并发和内存不可见性被修改,则修改old为update。例如AtomicInteger中进行变量自增:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* 通过当前值进行原子递增.
*
* @return 更新值
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//cas操作
return var5;
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}采用最小线程。避免不必要的线程,处于等待状态。
例如JUC包Executors类创建线程池参数,corePoolSize为核心线程数量,如allowCoreThreadTimeOut不设置,可以认为corePoolSize是最小线程数。
如果设置和maximumPoolSize一样大,则表示即使达到keepAliveTime空闲时间也不回收,均处于waiting状态。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42/**
* 创建单个线程
*
* @param threadFactory 线程工厂
*
* @return 单线程执行器
* @throws NullPointerException if threadFactory is null
*/
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
/**
* 根据参数和配置,创建线程池.
*
* @param corePoolSize 池中核心线程数
* @param maximumPoolSize 池中最大线程数
* @param keepAliveTime 线程数大于核心线程数时, 多余的待回收线程最大空闲时间
* @param unit 时间单位
* @param workQueue 任务执行队列
* @param threadFactory 创建线程的工厂
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}协程。在单线程中实现多任务的调度,并在单线程中维持多任务的切换。
资源限制的挑战
并发编程受限与机器硬件或软件资源。例如带宽、硬盘读写速度等硬件资源和数据库连接数、socket连接数等软件资源。
并发编程为加快执行速度,将串行任务并发执行,受资源影响,实际仍在串行切换执行,切换上下文和资源调度反而降低执行速度。
解决资源限制,采用集群执行,改单机为多机。对数据id取机器数模,在该机器处理该数据;软件资源考虑资源池复用,NIO等。
在资源限制下并发编程,根据资源调整并发度,例如读写分离,读写锁等。