Java并发编程艺术 - 多线程与资源限制

文本介绍多线程和资源的关系。

是否多线程越多速度越快?

  1. 结论:并发编程中采用多线程,并非线程越多执行效率越高。

  2. 线程执行是由CPU分配较短的时间片,线程在得到时间片时执行,并在时间片后挂起,并切换其他线程执行。

  3. 线程切换时先会保存上一个线程状态,并加载当前线程的历史状态。

  4. 因为线程的上下文切换需要时间,影响多线程执行速度。

  5. 在执行某些简单、快速任务情况下,多线程执行效率还不如单线程执行。
    假如单核采用单线程执行任务仅需要20ms,但单核采用多线程,并发100,可能创建线程时间就超出20ms,再加上单核只能执行单线程,需要多线程频繁挂起、上下文切换肯定慢。

  6. 在低核时,不建议采用多线程进行CPU密集型计算;建议采用多线程执行监听输入、读取文件、网络通信等IO密集型操作。

如何减少上下文切换

  1. 采用无锁的并发编程,即不进行锁竞争;例如根据ID进行Hash取模,多线程进行分段处理数据。
    我们常见的JDK1.7中ConcurrentHashMap就是锁分段技术,对key进行hashcode,然后取桶位置,默认1/16;在更新数据时锁数据所在桶,不影响其他桶并发操作,以提高并发处理数据速度。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public 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];
    }
  2. 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);
    }
  3. 采用最小线程。避免不必要的线程,处于等待状态。
    例如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);
    }
  4. 协程。在单线程中实现多任务的调度,并在单线程中维持多任务的切换。

资源限制的挑战

  1. 并发编程受限与机器硬件或软件资源。例如带宽、硬盘读写速度等硬件资源和数据库连接数、socket连接数等软件资源。

  2. 并发编程为加快执行速度,将串行任务并发执行,受资源影响,实际仍在串行切换执行,切换上下文和资源调度反而降低执行速度。

  3. 解决资源限制,采用集群执行,改单机为多机。对数据id取机器数模,在该机器处理该数据;软件资源考虑资源池复用,NIO等。

  4. 在资源限制下并发编程,根据资源调整并发度,例如读写分离,读写锁等。

------ 本文结束------

本文标题:Java并发编程艺术 - 多线程与资源限制

文章作者:Perkins

发布时间:2019年04月19日

原始链接:https://perkins4j2.github.io/posts/10420/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。