【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才有了存在的意义。

相关文章
|
7天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
14 0
|
9天前
|
Java 程序员
Java编程中的异常处理:从基础到高级
在Java的世界中,异常处理是代码健壮性的守护神。本文将带你从异常的基本概念出发,逐步深入到高级用法,探索如何优雅地处理程序中的错误和异常情况。通过实际案例,我们将一起学习如何编写更可靠、更易于维护的Java代码。准备好了吗?让我们一起踏上这段旅程,解锁Java异常处理的秘密!
|
12天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
12天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
6天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
6天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
23 3
|
12天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
37 5
|
11天前
|
开发框架 安全 Java
Java 反射机制:动态编程的强大利器
Java反射机制允许程序在运行时检查类、接口、字段和方法的信息,并能操作对象。它提供了一种动态编程的方式,使得代码更加灵活,能够适应未知的或变化的需求,是开发框架和库的重要工具。
31 2
|
12天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
46 1
|
13天前
|
Java API 数据库
Java 反射机制:动态编程的 “魔法钥匙”
Java反射机制是允许程序在运行时访问类、方法和字段信息的强大工具,被誉为动态编程的“魔法钥匙”。通过反射,开发者可以创建更加灵活、可扩展的应用程序。
32 0