【Java并发编程 一】并发编程的挑战

简介: 【Java并发编程 一】并发编程的挑战

本文是Java并发编程系列的第一篇,在正式进行Java中的并发编程方式之前,我们先来了解下什么是并发编程。并发编程的优势是什么,又有什么挑战和问题,以及该如何解决?那么首先要搞清楚什么是并发的概念?

并发的基本概念

并发是指两个或多个事件在同一时间间隔内发生,在多道程序环境下,一段时间内宏观上有多个程序在同时执行,而在同一时刻,单处理器环境下实际上只有一个程序在执行,故微观上这些程序还是在分时的交替进行。操作系统的并发是通过分时得以实现的,和串行以及并行的概念区别:

  • 串行:顺序做不同事的能力:先洗衣服,洗完后做饭。
  • 并发:交替做不同事的能力:一会儿洗衣服,一会儿做饭,交替执行,但快如闪电。洗衣服和做饭的是一个(cpu),在同一个时间段内每个cpu各司其职。并发的实质是一个物理CPU(也可以多个物理CPU) 在若干道程序之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。
  • 并行:同时做不同事的能力:左手洗衣服右手做饭,在同一时刻同时做两件事。并行性指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行。

并发关注的是资源充分利用(也就是不让cpu闲下来),并行关注的是一个任务被分解给多个执行者同时做,缩短这个任务的完成时间(也就是尽快做完这件事),操作系统的并发性是指计算机系统中同时存在多个运行着的程序,因此它具有处理和调度多个程序同时执行的能力。在操作系统中,引入进程的目的是使程序能并发执行。并行则是同时间同时刻有几个程序同时运行,有几核就就几个程序在并行。单核CPU只能并发多个程序,多核CPU可以并发也可以并行【4核CPU可以并行4个程序,程序大于核心时就需要用到并发性】

并发编程的挑战

并发编程的优势不言而喻,我们的主要问题是如何解决并发编程带来的挑战,包括线程轮转执行的上下文切换问题、对同步资源加锁时的死锁问题,整体的资源限制问题

上下文切换

通过对并发概念的理解,我们知道即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换,正是因为有了线程的创建上下文切换的开销,多线程有时候执行起来不一定有单线程快

package com.company;
public class ThreadTest {
    private static final long count = 10000l;
    public static void main(String[] args) throws InterruptedException {
        concurrency();
        serial();
    }
    private static void concurrency() throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread thread = new Thread(() -> {
            int a = 0;
            for (long i = 0; i < count; i++) {
                a += 5;
            }
        });
        thread.start();
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        thread.join();
        System.out.println("concurrency :" + time+"ms,b="+b);
    }
    private static void serial() {
        long start = System.currentTimeMillis();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a += 5;
        }
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        System.out.println("serial:" + time+"ms,b="+b+",a="+a);
    }
}

当循环执行1万次的时候,打印结果如下:

concurrency :35ms,b=-10000
serial:0ms,b=-10000,a=50000

当循环执行1亿次的时候,打印结果如下:

concurrency :90ms,b=-100000000
serial:65ms,b=-100000000,a=500000000

当循环执行10亿次的时候,打印结果如下:

concurrency :354ms,b=-1000000000
serial:581ms,b=-1000000000,a=705032704

如何减少上下文切换

既然上下文切换会耗费时间资源,那么该如何减少上下文切换呢?减少上下文切换的方法有无锁并发编程CAS算法使用最少线程使用协程

  • 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
  • CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
  • 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

关于线程和进程的概念和使用会在后文提到。总而言之就是使用适量的线程尽量少用锁、执行内容分配前置,代码逻辑写漂亮了,上下文切换就少

死锁

我们可以从多线程的设计原则中可以看到,并发编程可以大大提高CPU的利用率,但是因为其存在对共享和可变状态的资源进行访问,所以存在一定的问题。

  • 共享就意味着变量可以被多个线程同时访问。我们知道系统中的资源是有限的,不同的线程对资源都是具有着同等的使用权。有限、公平就意味着竞争,竞争就有可能会引发线程安全问题。
  • 可变是指变量的值在其生命周期内是可以发生改变的。“可变”对应的是“不可变”。我们知道不可变的对象一定是线程安全的,并且永远也不需要额外的同步(因为一个不可变的对象只要构建正确,其外部可见状态永远都不会发生改变)。所以可变意味着存在线程不安全的风险。

二者展现的核心问题就是如何保证共享和可变的资源不被错误的执行,解决方式很简单,就是对于共享的资源让线程协同持有和处理。这里就会用到对资源的锁,而会导致一系列线程同步问题

锁是个非常有用的工具,运用场景非常多,因为它使用起来非常简单,而且易于理解。但同时它也会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可用

package com.company;
public class ThreadTest {
    public static final  Object moniterA=new Object();
    public static final  Object moniterB=new Object();
    public static void main(String[] args)  {
        Thread t1 = new Thread(()-> {
                synchronized (moniterA) {
                    try {
                        Thread.sleep(2000);    //t1休眠2秒以便t2能拿到moniterB
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (moniterB) {
                        System.out.println("AM");
                    }
                }
        });
        Thread t2 = new Thread(()-> {
            synchronized (moniterB) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (moniterA) {
                    System.out.println("CH");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

查看dump信息可以看到线程的状态:thread-0等待锁

thread-1也在等待锁,这样导致了相互等待锁来执行代码,导致了死锁。

如何避免死锁

虽然死锁不能百分百解除,例如t1拿到锁之后,因为一些异常情况没有释放锁(死循环)。又或者是t1拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉,但是有如下几种避免死锁的机制:

  • 避免一个线程同时获取多个锁
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况

总而言之就是,如果要使用锁,一个线程只使用一个定时的锁去锁住一个资源

资源限制

资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s,所以在进行并发编程时,要考虑这些资源的限制:

  • 硬件资源限制:带宽的上传/下载速度、硬盘读写速度和CPU的处理速度
  • 软件资源限制:数据库的连接数和socket连接数等

在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢

  • 对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让程序在多机上运行
  • 对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用

所以总而言之,解决资源限制的问题依赖于分布式集群的搭建和资源池的建立和复用

总结

本篇Blog主要介绍了什么是并发编程,并发的优势不言而喻,主要是在并发过程中会遇到什么问题(上下文切换、死锁、资源限制),为了解决这些问题,这个系列的Blog才有了存在的意义。

相关文章
|
2月前
|
Java API 微服务
为什么虚拟线程将改变Java并发编程?
为什么虚拟线程将改变Java并发编程?
234 83
|
20天前
|
安全 Java 数据库连接
2025 年最新 Java 学习路线图含实操指南助你高效入门 Java 编程掌握核心技能
2025年最新Java学习路线图,涵盖基础环境搭建、核心特性(如密封类、虚拟线程)、模块化开发、响应式编程、主流框架(Spring Boot 3、Spring Security 6)、数据库操作(JPA + Hibernate 6)及微服务实战,助你掌握企业级开发技能。
172 3
|
1月前
|
Java
Java编程:理解while循环的使用
总结而言, 使用 while 迴圈可以有效解决需要多次重复操作直至特定條件被触发才停止執行任务场景下问题; 它简单、灵活、易于实现各种逻辑控制需求但同时也要注意防止因邏各错误导致無限迁璇発生及及時處理可能発生异常以确保程序稳定运作。
154 0
|
1月前
|
安全 Cloud Native Java
Java:历久弥新的企业级编程基石
Java:历久弥新的企业级编程基石
|
1月前
|
移动开发 Cloud Native Java
Java:历久弥新的企业级编程基石
Java:历久弥新的企业级编程基石
|
2月前
|
设计模式 Java 数据库连接
Java编程的知识体系 | Java编程精要
Java是一种广泛使用的通用编程语言,具备面向对象、跨平台、安全简单等优势,适用于桌面、企业、Web、移动及大数据等多个领域。它功能强大且易于学习,是程序设计入门和面向对象思想学习的优选语言。本书系统讲解Java编程知识,涵盖技术核心与应用拓展两大模块,内容包括基础语法、面向对象设计、GUI、数据库、多线程、网络编程及Web开发等,帮助读者全面掌握Java开发技能。
70 0
|
2月前
|
安全 Java
Java编程探究:深入解析final关键字
1. **使用限制**: 对于 `final` 方法和类,可以限制其他开发人员对代码的使用,确保其按设计的方式工作而不会被子类意外改变。
84 0
|
2月前
|
存储 缓存 安全
深入讲解 Java 并发编程核心原理与应用案例
本教程全面讲解Java并发编程,涵盖并发基础、线程安全、同步机制、并发工具类、线程池及实际应用案例,助你掌握多线程开发核心技术,提升程序性能与响应能力。
88 0
|
3月前
|
缓存 安全 算法
2025 年 Java 秋招面试必看 Java 并发编程面试题实操篇
Java并发编程是Java技术栈中非常重要的一部分,也是面试中的高频考点。本文从基础概念、关键机制、工具类、高级技术等多个方面进行了介绍,并提供了丰富的实操示例。希望通过本文的学习,你能够掌握Java并发编程的核心知识,在面试中取得好成绩。同时,在实际工作中,也能够运用这些知识设计和实现高效、稳定的并发系统。
83 0