您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

并发编程常见方案

时间:2023-09-18 14:23:56  来源:  作者:洞窝技术

今天,我将通过一个例子向大家介绍几种常见的并发编程方案。

我们通过一个程序模拟统计一批文档的字数。

首先我们先看无并发情况下的DEMO:

// 用Doc代表文档
public class Doc {

    public final String content;

    public Doc(String content) {
        this.content = content;
    }
}

// 模拟目录和文件
public class Dir {
    public final static List<Doc> docs = new ArrayList<>();
    static {
        docs.add( new Doc("""
        《三国演义》是综合民间传说和戏曲、话本,结合陈寿的《三国志》、范晔《后汉书》、
        元代《三国志平话》、和裴松之注的史料,以及作者个人对社会人生的体悟写成。现所见刊本
        以明嘉靖本最早,分24卷,240则。清初毛宗岗父子又做了一些修改,并成为现在最常见的120回本。""") );

        docs.add( new Doc("""
        《水浒传》是中国历史上第一部用古白话文写成的歌颂农民起义的长篇章回体版块结构小说,
        以宋江领导的起义军为主要题材,通过一系列梁山英雄反抗压迫、英勇斗争的生动故事,暴露
        了北宋末年统治阶级的腐朽和残暴,揭露了当时尖锐对立的社会矛盾和“官逼民反的残酷现实。""") );

        docs.add( new Doc("""
        《西游记》前七回叙述孙悟空出世,有大闹天宫等故事。此后写孙悟空随唐僧西天取经,
        沿途除妖降魔、战胜困难的故事。书中唐僧、孙悟空、猪八戒、沙僧等形象刻画生动,规模宏大,
        结构完整,并且《西游记》富有浓厚的中国佛教色彩,其隐含意义非常深远,众说纷纭,见仁见智。
        可以从佛、道、俗等多个角度欣赏,是中国古典小说中伟大的浪漫主义文学作品。""") );

        docs.add( new Doc("""
        《红楼梦》讲述的是发生在一个虚构朝代的封建大家庭中的人事物,其中以贾宝玉、
        林黛玉、薛宝钗三个人之间的感情纠葛为主线通过对一些日常事件的描述体现了在贾府
        的大观园中以金陵十二钗为主体的众女子的爱恨情愁。""") );


    }

}

public class wordCount {

    int countDoc( Doc doc ) {
        return doc.content.length();
    }

    void count() {

        int c = 0;

        for ( Doc doc : Dir.docs ) {
            c += countDoc(doc);
        }

        System.out.println( "所有文档字数总计: " + c + "个" );
    }

    public static void mAIn(String[] args) {
        new WordCount().count();
    }

}

执行结果:

1.共享内存与锁

这是一种非常熟悉和常见的技术,JAVA在这方面的实现非常出色。使用它通常会经历三个阶段:初学时感觉复杂和可怕,掌握后变得非常好和强大,进一步深入学习后又变得复杂和具有一定危险性。

共享内存允许多个进程同时读写一块或多块常规内存区域。有时候,进程需要在这些内存区域上执行一系列具有原子性的操作,其他进程在这些操作完成之前不能访问这些区域。为了解决这个问题,我们可以使用锁,这是一种只允许一个进程访问某种资源的机制。

这个方案存在多个缺点:

  1. 即使冲突的概率很低,锁的开销仍然无法忽略。

  2. 这些锁也是内存系统中的竞争热点。

  3. 如果出现错误的进程不处理锁,可能会导致正在加锁的锁被丢弃。

  4. 当锁出现问题时,调试极为困难。

此外,当使用锁来同步两三个进程时,可能没有太大问题。然而,随着进程数量的增加,情况会变得越来越难以控制。最终,这可能导致复杂的死锁问题,即使是最经验丰富的开发者也无法预见。

这个方案大家都比较熟熟悉,通过多个线共同处理文章:

// 计数器通过同步保障多线计数
public class Counter1 {
    private int c = 0;
    public synchronized void inc( int n ) {
        this.c += n;
    }
    public synchronized int totalNumber() {
        return c;
    }

}

// 文档字数计算
public class DocProc1 implements Runnable {

    private final Counter1 counter;

    public final Doc doc;

    public DocProc1(Counter1 counter, Doc doc) {
        this.counter = counter;
        this.doc = doc;
    }

    public void run() {
        int c = countDoc( this.doc );
        counter.inc( c );
    }
    
    int countDoc( Doc doc ) {
        return doc.content.length();
    }

}

public class WordCount1 {

    private final Counter1 counter = new Counter1();

    void count() throws InterruptedException {

        List<Thread> threads = new ArrayList<>();

        // 启动多个线程处理文章
        for ( Doc doc : Dir.docs ) {
            DocProc1 docProc1 = new DocProc1(counter , doc);
            Thread t = new Thread(docProc1);
            threads.add( t );
            t.start();
        }

        for (Thread t : threads ) {
            t.join();
        }

        System.out.println( "所有文档字数总计: " + this.counter.totalNumber() + "个" );
    }

    public static void main(String[] args) throws InterruptedException {
        new WordCount1().count();
    }

}

执行结果:

2.软件事务性内存(STM)

STM(Software Transactional Memory,软件事务性内存)是一种将内存视为传统数据库,并使用事务来确定何时写入什么内容的方法。

通常,这种实现采用乐观的方式来避免锁的使用。它将一组读写访问视为一个单独的操作,如果两个进程同时尝试访问共享区域,则它们各自启动一个事务,最终只有一个事务会成功。另一个进程会意识到事务失败,并在检查共享区域的新内容后进行重试。

该模型直截了当,谁都不需要等待其他进程释放锁。

STM的主要缺点是必须重试失败的事务,甚至可能多次失败。此外,事务系统本身也会带来相当大的开销,并且在确定哪个进程成功之前,需要额外的内存来存储试图写入的数据。理想情况下,系统应该像支持虚拟内存那样为事务性内存提供硬件支持。

对于程序员来说,STM的可控性似乎比锁更好,只要竞争不频繁导致事务重启,就能充分发挥并发的优势。我们认为这种方法本质上是持锁共享内存的一种变体,其在操作系统层面的作用比应用编程层面更为重要。然而,针对这个问题的研究仍然非常活跃,局势可能会发生改变。

Java并没有直接支持STM方案,因此要实现一个通用的库会相对复杂。在这里,我们简单介绍一下它的原理。

public class Counter2 {

    private final AtomicInteger c = new AtomicInteger();

    // 只为表达STM原理
    public boolean inc( int n ) {
        int oldVal = c.get();
        int newVal = oldVal + n;
        return c.compareAndSet(oldVal , newVal);
    }

    public int totalNumber() {
        return c.get();
    }

}

public class DocProc2 implements Runnable {

    private final Counter2 counter;

    public final Doc doc;

    public DocProc2(Counter2 counter, Doc doc) {
        this.counter = counter;
        this.doc = doc;
    }

    public void run() {
        int c = countDoc( this.doc );
        while (!counter.inc( c ));
    }

    int countDoc( Doc doc ) {
        return doc.content.length();
    }

}

public class WordCount2 {

    private final Counter2 counter = new Counter2();

    void count() throws InterruptedException {

        List<Thread> threads = new ArrayList<>();

        for ( Doc doc : Dir.docs ) {
            DocProc2 docProc = new DocProc2(counter , doc);
            Thread t = new Thread(docProc);
            threads.add( t );
            t.start();
        }

        for (Thread t : threads ) {
            t.join();
        }

        System.out.println( "所有文档字数总计: " + this.counter.totalNumber() + "个" );
    }

    public static void main(String[] args) throws InterruptedException {
        new WordCount2().count();
    }

}

执行结果:

3.Future

另一个更现代的手段是采用所谓的future。

该方法的基本思路是,每个future都代表一个计算结果,这个结果被外包给其他进程处理,这个进程可以在其他CPU甚至其他计算机上运行。

Future可以像其他对象一样被传递,但在计算完成之前无法读取结果,必须等待计算完成。虽然这种方法简化了并发系统中的数据传递,但也使得程序在远程进程故障和网络故障的情况下变得脆弱。当计算结果尚未准备好而连接断开时,试图访问值的代码将无法执行。

public class DocProc3 implements Callable<Integer> {

    public final Doc doc;

    public DocProc3(Doc doc) {
        this.doc = doc;
    }

    public Integer call() {
        return countDoc( this.doc );
    }

    int countDoc( Doc doc ) {
        return doc.content.length();
    }

}

public class WordCount3 {

    void count() throws InterruptedException, ExecutionException {

        ExecutorService executorService = Executors.newFixedThreadPool(5);

        List<Future<Integer>> futures = new ArrayList<>();

        for ( Doc doc : Dir.docs ) {
            Future<Integer> future = executorService.submit(new DocProc3(doc));
            futures.add(future);
        }

        int c = 0;
        for (Future<Integer> f : futures ) {
            c += f.get();
        }

        System.out.println( "所有文档字数总计: " + c + "个" );

        executorService.shutdownNow();
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        new WordCount3().count();
    }

}

执行结果:

4.函数式编程

函数式编程是一种计算机科学中的编程范式,它通过应用和组合函数来构建程序。它是一种声明式编程范式,其中函数定义是将值映射到其他值的表达式树,而不是通过一系列命令式语句来更新程序运行状态。

在函数式编程中,函数被认为是一种重要的元素,它们可以被赋予名称(包括本地标识符),作为参数传递,并且可以像其他数据类型一样从其他函数返回。这种特性使得程序可以以声明性和可组合的方式编写,通过模块化地组合小功能。

篇幅有限,这里粘贴了一段百度百科的内容。函数通过保持不变性和没有副作用,天然具备线程安全性,可以放心地在并发环境中使用。

public class WordCount4 {

    int countDoc( Doc doc ) {
        return doc.content.length();
    }

    void count() {

        long total = Dir.docs.parallelStream().mapToInt(this::countDoc).sum();

        System.out.println( "所有文档字数总计: " + total + "个" );
    }
    
    public static void main(String[] args) {
        new WordCount4().count();
    }

}

执行结果:

5.消息传递

这是一个比较现实的工作场景,团队成员需要协同工作。当某人需要另一个人处理事情时,他会发送一条消息给对方。收到消息后,另一个人会处理事情,并在完成后回复一条消息。

消息传递是一种通信方式,它意味着接收进程实际上获得了一份独立的数据副本,而发送方无法感知接收方对该副本的任何操作。唯一能向发送方回传信息的方式是通过发送另一条消息。因此,不论收发双方是在同一台机器上还是被网络隔离,它们都能以相同的方式进行通信。

消息传递一般可分为同步方式和异步方式。在同步方式下,发送方在消息抵达接收端之前无法进行其他操作;而在异步方式下,一旦消息被投递,发送方就可以立即开始处理其他事务。(在现实世界中,机器之间的同步通信通常要求接收方给发送方发送一个确认消息,以告知一切正常,但这些细节对程序员来说可以是透明的。

java 自身没有实现该模式,AKKA开源的库实现了此模式。

严格说所有线程间通讯都应该用消息,我用个例子简单表达一下原理:

public class Counter5 {

    public Counter5() {
        this.procMail();
    }

    private final BlockingQueue<Integer> box = new ArrayBlockingQueue<>(100);

    private int c;

    public void inc( int n )  {
        try {
            box.put(n);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private volatile boolean stop = false;
    private void procMail() {
        new Thread() {
            @Override
            public void run() {
                while (!stop) {
                    try {
                        Integer n = box.poll(100 , TimeUnit.MILLISECONDS);
                        if ( n != null ) {
                            c += n;
                        }
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }.start();
    }

    public int totalNumber() throws InterruptedException {

        while ( ! box.isEmpty() ) {

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        this.stop = true;

        return c;
    }

}

public class DocProc5 implements Runnable {

    private final Counter5 counter;

    public final Doc doc;

    public DocProc5(Counter5 counter, Doc doc) {
        this.counter = counter;
        this.doc = doc;
    }

    public void run() {
        int n = countDoc( this.doc );
        counter.inc(n);
    }


    int countDoc( Doc doc ) {
        return doc.content.length();
    }

}

public class WordCount5 {

    private final Counter5 counter = new Counter5();

    void count() throws InterruptedException {

        List<Thread> threads = new ArrayList<>();

        for ( Doc doc : Dir.docs ) {
            DocProc5 docProc = new DocProc5(counter , doc);
            Thread t = new Thread(docProc);
            threads.add( t );
            t.start();
        }

        for (Thread t : threads ) {
            t.join();
        }

        System.out.println( "所有文档字数总计: " + this.counter.totalNumber() + "个" );
    }

    public static void main(String[] args) throws InterruptedException {
        new WordCount5().count();
    }

}

执行结果

 



Tags:并发编程   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  Search: 并发编程  点击:(105)  评论:(0)  加入收藏
掌握Java并发编程,避免无处不在的竞态条件
掌握Java并发编程是编写高效、可靠的多线程应用程序的关键。竞态条件(Race Condition)是多线程环境下常见的问题,可能导致数据不一致、死锁等严重后果。下面将介绍Java并发编程...【详细内容】
2023-12-29  Search: 并发编程  点击:(105)  评论:(0)  加入收藏
看完后,你再也不用怕面试问并发编程啦
引言为什么很多大厂喜欢问并发编程呢?因为并发编程是开发人员的一个分水岭。很多好几年开发经验的开发人员可能也没有实际的并发编程经验,要么就是在一些没有挑战性的中台实现...【详细内容】
2023-12-27  Search: 并发编程  点击:(82)  评论:(0)  加入收藏
解锁 C++ 并发编程的钥匙:探索 Atomic 变量
最近在用c++搞项目,因为多线程要做一个类似cnt的保护,今天学习了c++的原子操作。探索c++的原子类型std::atomic 类型是 C++ 提供的一种机制,用于实现多线程之间的安全共享数据...【详细内容】
2023-12-06  Search: 并发编程  点击:(227)  评论:(0)  加入收藏
协程:解锁并发编程的新世界
随着计算机技术的不断发展,软件开发领域也在迅猛前进。在并发编程领域,协程已经成为一项备受关注的技术。本文将带您穿越时间的长河,了解协程的历史发展,深入研究它在实际项目中...【详细内容】
2023-11-24  Search: 并发编程  点击:(202)  评论:(0)  加入收藏
CAS操作在并发编程中的应用及其问题分析
CAS(Compare and Swap)操作是一种基于硬件指令实现的原子操作,可以在不使用传统互斥锁的情况下,保证多线程对共享变量的安全访问。在Java中,我们可以使用Atomic类和AtomicReferen...【详细内容】
2023-11-09  Search: 并发编程  点击:(244)  评论:(0)  加入收藏
深入理解并发编程艺术之JVM内存模型
java内存模型由来我们知道不同的计算机硬件和操作系统的,所遵循的规范以及计算机内存模型是有区别的,也就意味着我们开发的程序放在某个计算机硬件和操作系统上运行是正常的,而...【详细内容】
2023-10-27  Search: 并发编程  点击:(429)  评论:(0)  加入收藏
Java并发编程模式:探索不同的线程安全实现方式
Java并发编程模式是指为了在多线程环境下保证程序正确性而采用的一些编程方式和技巧。在高并发场景下,保障程序正确性成为了开发中的重点之一。一、线程安全实现方式1、同步...【详细内容】
2023-10-18  Search: 并发编程  点击:(281)  评论:(0)  加入收藏
并发编程的艺术-“程”:探索进程、线程、协程、纤程与管程
一、并发中的程在计算机科学领域,处理多任务和并发执行是一项重要的挑战。为了解决这个问题,出现了多种并发模型和概念,包括进程、线程、协程、纤程和管程。本文将深入探讨这些...【详细内容】
2023-10-12  Search: 并发编程  点击:(381)  评论:(0)  加入收藏
SpringBoot 并发编程学习历程
本教程大概目录: 模拟单线程情节 用Callable实现 并发编程 用DeferedResult实现异步处理###模拟单线程情节。/** * Created by Fant.J. */@RestController@Slf4jpublic class...【详细内容】
2023-09-27  Search: 并发编程  点击:(207)  评论:(0)  加入收藏
▌简易百科推荐
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(5)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(12)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(8)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(10)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(8)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
Kubernetes 究竟有没有 LTS?
从一个有趣的问题引出很多人都在关注的 Kubernetes LTS 的问题。有趣的问题2019 年,一个名为 apiserver LoopbackClient Server cert expired after 1 year[1] 的 issue 中提...【详细内容】
2024-03-15  云原生散修  微信公众号  Tags:Kubernetes   点击:(6)  评论:(0)  加入收藏
站内最新
站内热门
站内头条