Java并发编程的艺术(一)——并发编程需要注意的问题

并发是为了提升程序的执行速度,但并不是多线程一定比单线程高效,而且并发编程容易出错。若要实现正确且高效的并发,就要在开发过程中时刻注意以下三个问题:
  • 上下文切换
  • 死锁
  • 资源限制

接下来会逐一分析这三个问题,并给出相应的解决方案。

问题一:上下文切换会带来额外的开销

线程的运行机制

  • 一个CPU每个时刻只能执行一条线程;
  • 操作系统给每条线程分配不同长度的时间片;
  • 操作系统会从一堆线程中随机选取一条来执行;
  • 每条线程用完自己的时间片后,即使任务还没完成,操作系统也会剥夺它的执行权,让另一条线程执行

什么是“上下文切换”?

当一条线程的时间片用完后,操作系统会暂停该线程,并保存该线程相应的信息,然后再随机选择一条新线程去执行,这个过程就称为“线程的上下文切换”。

上下文切换的过程

  • 暂停正在执行的线程;
  • 保存该线程的相关信息(如:执行到哪一行、程序计算的中间结果等)
  • 从就绪队列中随机选一条线程;
  • 读取该线程的上下文信息,继续执行

上下文切换是有开销的

每次进行上下文切换时都需要保存当前线程的执行状态,并加载新线程先前的状态。 如果上下文切换频繁,CPU花在上下文切换上的时间占比就会上升,而真正处理任务的时间占比就会下降。 因此,为了提高并发程序的执行效率,让CPU把时间花在刀刃上,我们需要减少上下文切换的次数。

如何减少上下文切换?

  • 减少线程的数量 由于一个CPU每个时刻只能执行一条线程,而傲娇的我们又想让程序并发执行,操作系统只好不断地进行上下文切换来使我们从感官上觉得程序是并发执的行。因此,我们只要减少线程的数量,就能减少上下文切换的次数。 然而如果线程数量已经少于CPU核数,每个CPU执行一条线程,照理来说CPU不需要进行上下文切换了,但事实并非如此。
  • 控制同一把锁上的线程数量 如果多条线程共用同一把锁,那么当一条线程获得锁后,其他线程就会被阻塞;当该线程释放锁后,操作系统会从被阻塞的线程中选一条执行,从而又会出现上下文切换。 因此,减少同一把锁上的线程数量也能减少上下文切换的次数。
  • 采用无锁并发编程 我们知道,如果减少同一把锁上线程的数量就能减少上下文切换的次数,那么如果不用锁,是否就能避免因竞争锁而产生的上下文切换呢? 答案是肯定的!但你需要根据以下两种情况挑选不同的策略:
    1. 需要并发执行的任务是无状态的:HASH分段 所谓无状态是指并发执行的任务没有共享变量,他们都独立执行。对于这种类型的任务可以按照ID进行HASH分段,每段用一条线程去执行。
    2. 需要并发执行的任务是有状态的:CAS算法 如果任务需要修改共享变量,那么必须要控制线程的执行顺序,否则会出现安全性问题。你可以给任务加锁,保证任务的原子性与可见性,但这会引起阻塞,从而发生上下文切换;为了避免上下文切换,你可以使用CAS算法, 仅在线程内部需要更新共享变量时使用CAS算法来更新,这种方式不会阻塞线程,并保证更新过程的安全性。

问题二:并发不当可能会产生死锁

什么是“死锁”?

当多个线程相互等待已经被对方占用的资源时,就会产生死锁。

死锁示例

class DeadLock {
    // 锁A 
    private Object lockA;
    // 锁B
    private Object lockB;

    // 第一条线程
    Thread t1 = new Thread(new Runnable(){
        void run () {
            synchronized (lockA) {
                Thread.sleep(5000);
                synchronized (lockB) {
                    System.out.println("线程1");
                }
            }
        }
    }).start();

    // 第二条线程
    Thread t2 = new Thread(new Runnable(){
        void run () {
            synchronized (lockB) {
                Thread.sleep(5000);
                synchronized (lockA) {
                    System.out.println("线程2");
                }
            }
        }
    }).start();
}
  • 线程1和线程2都需要锁A和锁B
  • 线程1首先获得锁A,然后sleep 5秒 PS:线程sleep过程中会释放执行权
  • 此时线程2执行,获得锁B,然后也sleep 5秒;
  • 线程1 sleep 5秒后继续执行,此时需要锁B,然而锁B已经被线程2持有,因此线程1被阻塞;
  • 此时线程2醒了,它需要锁A,然而锁A已经被线程1持有,因此它也被阻塞;
  • 此时死锁出现了!两条线程相互等待已经被占用的资源,程序就死在这了。 死锁是并发编程中一个重要的问题,上面介绍的减少上下文切换只是为了提升程序的性能,而一旦产生死锁,程序就不能正确执行!

如何避免死锁?

  • 不要在一条线程中嵌套使用多个锁;
  • 不要在一条线程中嵌套占用多个计算机资源;
  • 给锁和资源加超时时间 如果你非要在一条线程中嵌套使用多个锁或占用多个资源,那你需要给锁、资源加超时时间,从而避免无限期的等待。

问题三:计算机资源会限制并发

误区:线程越多速度越快

在并发编程中,并不是线程越多越好,有时候线程多了反而会拉低执行效率,原因如下:

  • 线程多了会导致上下文切换增多,CPU花在上下文切换的时间增多后,花在处理任务上的时间自然就减少了。
  • 计算机资源会限制程序的并发度。
    • 比如:你家网入口带宽10M,你写了个多线程下载的软件,同时开100条线程下载,那每条线程平均以每秒100k的速度下载,然而100条线程之间还要不断进行上下文切换,所以你还不如只开5条线程,每条平均2M/s的速度下载。
    • 再比如:数据库连接池最多给你用10个连接,然而你却开了100条线程进行数据库操作,那么当10个用完后其他线程就要等待,从而操作系统要在这100条线程间不断进行上下文切换;所以与其这样还不如只开10条线程,减少上下文切换的次数。

说了这么多只想告诉你一个道理:线程并不是越多越好,要根据当前计算机所能提供的资源考虑。

什么是“资源”?

资源分为硬件资源和软件资源:

  • 硬件资源
    • 硬盘读写速度
    • 网络带宽
  • 软件资源
    • Socket连接数
    • 数据库连接数

如何解决资源的限制?

  • 花钱买更高级的机器
  • 根据资源限制并发度

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏老司机的技术博客

java面试必备之ThreadLocal

按照传统的经验,如果某个对象是非线程安全的,在多线程环境下对象的访问需要采用synchronized进行同步。但是模板类并未采用线程同步机制,因为线程同步会降低...

962
来自专栏orientlu

FreeRTOS 信号量

FreeRTOS 信号量和互斥锁是基于队列实现的, 队列介绍见 << FreeRTOS 消息队列 >>。 使用信号量需要在源文件中包含头文件 semphr....

1072
来自专栏Java后端技术

深入聊聊Java多线程

  在没有学习Java多线程以前,总觉得多线程是个很神秘的东西,只有那些大神才能驾驭,新年假期没事就来学习和了解一下Java的多线程,本篇博客我们就来从头说一下...

894
来自专栏微信公众号:Java团长

Java面试官最爱的volatile关键字

在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以volatile关键字作为一个小的切入点,往往可以一问到底,把Java内存模...

651
来自专栏Java架构

面试官最爱的volatile关键字

1173
来自专栏Java架构沉思录

关于Java锁机制面试官会怎么问

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边...

801
来自专栏java技术学习之道

Java线程面试题合集(含答案)

1374
来自专栏好好学java的技术栈

bat等大公司常考java多线程面试题

简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程.进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切...

684
来自专栏好好学java的技术栈

并发基础篇(一): 线程介绍

1163
来自专栏博客园迁移

Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

  悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库...

712

扫码关注云+社区