精华内容
下载资源
问答
  • C++并发编程实战

    2017-09-10 14:27:18
    C++并发编程实战C++并发编程实战C++并发编程实战C++并发编程实战C++并发编程实战C++并发编程实战C++并发编程实战C++并发编程实战C++并发编程实战
  • Java 并发编程实战

    2018-01-02 19:31:43
    Java 并发编程实战 Java 并发编程实战
  • Erlang OTP并发编程实战

    2017-10-27 16:51:46
    Erlang并发编程实战Erlang并发编程实战Erlang并发编程实战Erlang并发编程实战
  • 《Java并发编程实战》PDF版本下载
  • C++并发编程实战 pdf

    2017-11-02 18:28:25
    C++并发编程实战 C++并发编程实战 C++并发编程实战 C++并发编程实战
  • go 并发编程实战 mobi

    2018-09-08 17:05:18
    go 并发编程实战 ,go 并发编程实战 mobi kindle 实测,放心下载
  • Java 并发编程实战PDF

    2017-11-02 16:59:18
    Java 并发编程实战PDF,Java 并发编程实战PDF,Java 并发编程实战PDF
  • Java 7并发编程实战手册 Java 7并发编程实战手册 Java 7并发编程实战手册
  • erlang并发编程实战源代码erlang并发编程实战源代码
  • Java 并发编程实战.pdf

    2019-05-28 22:28:37
    《java并发编程实战》是java并发的圣经。亲自整理目录结构,层级分明(福昕阅读器整理)。高清。
  • JAVA并发编程实战.pdf

    2019-05-10 09:32:58
    JAVA并发编程实战.pdf-详细介绍了线程并发的机制的
  • 《JAVA并发编程的艺术》之 Java并发编程实战 文章目录《JAVA并发编程的艺术》之 Java并发编程实战生产者和消费者模式生产者消费者模式实战多生产者和多消费者场景线程池与生产消费者模式线上问题定位性能测试查看...

    《JAVA并发编程的艺术》之 Java并发编程实战


    当你在进行并发编程时,看着程序的执行速度在自己的优化下运行得越来越快,你会觉得越来越有成就感,这就是并发编程的魅力。但与此同时,并发编程产生的问题和风险可能也 会随之而来。本章先介绍几个并发编程的实战案例,然后再介绍如何排查并发编程造成的问题。

    生产者和消费者模式

    在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序整体处理数据的速度。

    在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,便有了生产者和消费者模式。

    **什么是生产者和消费者模式 **

    生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

    这个阻塞队列就是用来给生产者和消费者解耦的。纵观大多数设计模式,都会找一个第三者出来进行解耦,如工厂模式的第三者是工厂类,模板模式的第三者是模板类。在学习一些设计模式的过程中,先找到这个模式的第三者,能帮助我们快速熟悉一个设计模式。

    生产者消费者模式实战

    我和同事一起利用业余时间开发的Yuna工具中使用了生产者和消费者模式。我先介绍下Yuna[1]工具,在阿里巴巴很多同事都喜欢通过邮件分享技术文章,因为通过邮件分享很方便,大家在网上看到好的技术文章,执行复制→粘贴→发送就完成了一次分享,但是我发现技术文章不能沉淀下来,新来的同事看不到以前分享的技术文章,大家也很难找到以前分享过的技术文章。为了解决这个问题,我们开发了一个Yuna工具。

    我们申请了一个专门用来收集分享邮件的邮箱,比如share@alibaba.com,大家将分享的文 章发送到这个邮箱,让大家每次都抄送到这个邮箱肯定很麻烦,所以我们的做法是将这个邮箱地址放在部门邮件列表里,所以分享的同事只需要和以前一样向整个部门分享文章就行。

    Yuna工具通过读取邮件服务器里该邮箱的邮件,把所有分享的邮件下载下来,包括邮件的附件、图片和邮件回复。因为我们可能会从这个邮箱里下载到一些非分享的文章,所以我们要求分享的邮件标题必须带有一个关键字,比如“内贸技术分享”。下载完邮件之后,通过confluence的Web Service接口,把文章插入到confluence里,这样新同事就可以在confluence里看以前分享过的文章了,并且Yuna工具还可以自动把文章进行分类和归档。

    为了快速上线该功能,当时我们花了3天业余时间快速开发了Yuna 1.0版本。在1.0版本中并没有使用生产者消费模式,而是使用单线程来处理,因为当时只需要处理我们一个部门的 邮件,所以单线程明显够用,整个过程是串行执行的。在一个线程里,程序先抽取全部的邮件,转化为文章对象,然后添加全部的文章,最后删除抽取过的邮件。代码如下。

    public void extract() { 
        logger.debug("开始" + getExtractorName() + "。。"); // 抽取邮件 
        List<Article> articles = extractEmail(); // 添加文章 
        for (Article article : articles) { 
            addArticleOrComment(article);
        }
        // 清空邮件 
        cleanEmail(); 
        logger.debug("完成" + getExtractorName() + "。。");
    }
    

    Yuna工具在推广后,越来越多的部门使用这个工具,处理的时间越来越慢,Yuna是每隔5分钟进行一次抽取的,而当邮件多的时候一次处理可能就花了几分钟,于是我在Yuna 2.0版本里使用了生产者消费者模式来处理邮件,首先生产者线程按一定的规则去邮件系统里抽取邮件,然后存放在阻塞队列里,消费者从阻塞队列里取出文章后插入到conflunce里。代码如下。

    public class QuickEmailToWikiExtractor extends AbstractExtractor { 
        private ThreadPoolExecutor threadsPool; 
        
    private ArticleBlockingQueue<ExchangeEmailShallowDTO> emailQueue; 
        public QuickEmailToWikiExtractor() { 
            emailQueue= new ArticleBlockingQueue<ExchangeEmailShallowDTO>(); 
            int corePoolSize = Runtime.getRuntime().availableProcessors() * 2; 
            threadsPool = new ThreadPoolExecutor(corePoolSize, corePoolSize, 10l, TimeUnit. SECONDS,new LinkedBlockingQueue<Runnable>(2000)); 
        }
        public void extract() { 
            logger.debug("开始" + getExtractorName() + "。。");
            long start = System.currentTimeMillis(); // 抽取所有邮件放到队列里 
            new ExtractEmailTask().start(); 
            // 把队列里的文章插入到
            Wiki insertToWiki(); 
            long end = System.currentTimeMillis(); 
            double cost = (end - start) / 1000; 
            logger.debug("完成" + getExtractorName() + ",花费时间:" + cost + "秒"); 
        }
        /*** 把队列里的文章插入到Wiki */ 
        private void insertToWiki() { 
            // 登录Wiki,每间隔一段时间需要登录一次 
            confluenceService.login(RuleFactory.USER_NAME, RuleFactory.PASSWORD); 
            while (true) {
                // 2秒内取不到就退出 
                ExchangeEmailShallowDTO email = emailQueue.poll(2, TimeUnit.SECONDS); 
                if (email == null) { 
                    break; 
                }
                threadsPool.submit(new insertToWikiTask(email));
            } 
        }
        protected List<Article> extractEmail() { 
            List<ExchangeEmailShallowDTO> allEmails = getEmailService().queryAllEmails(); 
            if (allEmails == null) {
                return null;
            }
            for (ExchangeEmailShallowDTO exchangeEmailShallowDTO : allEmails) {
                emailQueue.offer(exchangeEmailShallowDTO);
                
    }
            return null;
        }
        /*** 抽取邮件任务 ** @author tengfei.fangtf */ 
        public class ExtractEmailTask extends Thread {
            public void run() { 
                extractEmail();
            }
        }
    }
    

    代码的执行逻辑是,生产者启动一个线程把所有邮件全部抽取到队列中,消费者启动CPU*2个线程数处理邮件,从之前的单线程处理邮件变成了现在的多线程处理,并且抽取邮件的线程不需要等处理邮件的线程处理完再抽取新邮件,所以使用了生产者和消费者模式后,邮件的整体处理速度比以前要快了几倍。

    [1] Yuna取名自我非常喜欢的一款RPG游戏《最终幻想》中女主角的名字。

    多生产者和多消费者场景

    在多核时代,多线程并发处理速度比单线程处理速度更快,所以可以使用多个线程来生产数据,同样可以使用多个消费线程来消费数据。而更复杂的情况是,消费者消费的数据,有可能需要继续处理,于是消费者处理完数据之后,它又要作为生产者把数据放在新的队列里,交给其他消费者继续处理,如图11-1所示。

    在这里插入图片描述

    我们在一个长连接服务器中使用了这种模式,生产者1负责将所有客户端发送的消息存放在阻塞队列1里,消费者1从队列里读消息,然后通过消息ID进行散列得到N个队列中的一个,然后根据编号将消息存放在到不同的队列里,每个阻塞队列会分配一个线程来消费阻塞队列里的数据。如果消费者2无法消费消息,就将消息再抛回到阻塞队列1中,交给其他消费者处理。

    以下是消息总队列的代码。

    /*** 总消息队列管理 
    *
    * @author tengfei.fangtf 
    */
    public class MsgQueueManager implements IMsgQueue{
        private static final Logger LOGGER = LoggerFactory.getLogger(MsgQueueManager.class);
        /*** 消息总队列 */
        public final BlockingQueue<Message> messageQueue; 
        private MsgQueueManager() {
            messageQueue = new LinkedTransferQueue<Message>(); 
        }
        public void put(Message msg) { 
            try {
                messageQueue.put(msg);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); 
            } 
        }
        public Message take() {
            try {
                return messageQueue.take(); 
            } catch (InterruptedException e) { 
                Thread.currentThread().interrupt();
            }
            return null; 
        }
    }
    

    启动一个消息分发线程。在这个线程里子队列自动去总队列里获取消息。

    /** 
    * 分发消息,负责把消息从大队列塞到小队列里 
    *
    * @author tengfei.fangtf 
    */ 
    static class DispatchMessageTask implements Runnable { 
        @Override 
        public void run() { 
            BlockingQueue<Message> subQueue; 
            for (;;) {
    // 如果没有数据,则阻塞在这里 
                Message msg = MsgQueueFactory.getMessageQueue().take(); 
                // 如果为空,则表示没有Session机器连接上来, 
                // 需要等待,直到有Session机器连接上来 
                while ((subQueue = getInstance().getSubQueue()) == null) { 
                    try {
                        Thread.sleep(1000); 
                    } catch (InterruptedException e) { 
                        Thread.currentThread().interrupt(); 
                    } 
                }
                // 把消息放到小队列里 
                try {
                    subQueue.put(msg);
                } catch (InterruptedException e) { 
                    Thread.currentThread().interrupt();
                }
            } 
        } 
    }
    

    使用散列(hash)算法获取一个子队列,代码如下。

    /** 
    * 均衡获取一个子队列。 
    *
    * @return 
    */ 
    public BlockingQueue<Message> getSubQueue() { 
        int errorCount = 0; 
        for (;;) { 
            if (subMsgQueues.isEmpty()) {
                return null;
            }
            int index = (int) (System.nanoTime() % subMsgQueues.size()); 
            try {
                return subMsgQueues.get(index);
            } catch (Exception e) { 
                // 出现错误表示,在获取队列大小之后,队列进行了一次删除操作 
                LOGGER.error("获取子队列出现错误", e); 
                if ((++errorCount) < 3) { 
                    continue; 
                } 
            } 
        } 
    }
    

    使用的时候,只需要往总队列里发消息。

    // 往消息队列里添加一条消息
    IMsgQueue messageQueue = MsgQueueFactory.getMessageQueue();
    Packet msg = Packet.createPacket(Packet64FrameType. TYPE_DATA, "{}".getBytes(), (short) 1); 
    messageQueue.put(msg);
    

    线程池与生产消费者模式

    Java中的线程池类其实就是一种生产者和消费者模式的实现方式,但是我觉得其实现方式更加高明。生产者把任务丢给线程池,线程池创建线程并处理任务,如果将要运行的任务数大于线程池的基本线程数就把任务扔到阻塞队列里,这种做法比只使用一个阻塞队列来实现生产者和消费者模式显然要高明很多,因为消费者能够处理直接就处理掉了,这样速度更快,而生产者先存,消费者再取这种方式显然慢一些。

    我们的系统也可以使用线程池来实现多生产者和消费者模式。例如,创建N个不同规模的Java线程池来处理不同性质的任务,比如线程池1将数据读到内存之后,交给线程池2里的线程继续处理压缩数据。线程池1主要处理IO密集型任务,线程池2主要处理CPU密集型任务。

    本节讲解了生产者和消费者模式,并给出了实例。读者可以在平时的工作中思考一下哪 些场景可以使用生产者消费者模式,我相信这种场景应该非常多,特别是需要处理任务时间比较长的场景,比如上传附件并处理,用户把文件上传到系统后,系统把文件丢到队列里,然后立刻返回告诉用户上传成功,最后消费者再去队列里取出文件处理。再如,调用一个远程接口查询数据,如果远程服务接口查询时需要几十秒的时间,那么它可以提供一个申请查询的接口,这个接口把要申请查询任务放数据库中,然后该接口立刻返回。然后服务器端用线程轮询并获取申请任务进行处理,处理完之后发消息给调用方,让调用方再来调用另外一个接口取数据。

    线上问题定位

    有时候,有很多问题只有在线上或者预发环境才能发现,而线上又不能调试代码,所以线上问题定位就只能看日志、系统状态和dump线程,本节只是简单地介绍一些常用的工具,以帮助大家定位线上问题。

    1. 在Linux命令行下使用TOP命令查看每个进程的情况,显示如下。
    top - 22:27:25 up 463 days, 12:46, 1 user, load average: 11.80, 12.19, 11.79 
    Tasks: 113 total, 5 running, 108 sleeping, 0 stopped, 0 zombie 
    Cpu(s): 62.0%us, 2.8%sy, 0.0%ni, 34.3%id, 0.0%wa, 0.0%hi, 0.7%si, 0.2%st 
    Mem: 7680000k total, 7665504k used, 14496k free, 97268k buffers 
    Swap: 2096472k total, 14904k used, 2081568k free, 3033060k cached 
    PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 
    31177 admin 18 0 5351m 4.0g 49m S 301.4 54.0 935:02.08 java 
    31738 admin 15 0 36432 12m 1052 S 8.7 0.2 11:21.05 nginx-proxy
    

    我们的程序是Java应用,所以只需要关注COMMAND是Java的性能数据,COMMAND表示启动当前进程的命令,在Java进程这一行里可以看到CPU利用率是300%,不用担心,这个是当前机器所有核加在一起的CPU利用率。

    1. 再使用top的交互命令数字1查看每个CPU的性能数据。
    top - 22:24:50 up 463 days, 12:43, 1 user, load average: 12.55, 12.27, 11.73 Tasks: 110 total, 3 running, 107 sleeping, 0 stopped, 0 zombie 
    Cpu0 : 72.4%us, 3.6%sy, 0.0%ni, 22.7%id, 0.0%wa, 0.0%hi, 0.7%si, 0.7%st 
    Cpu1 : 58.7%us, 4.3%sy, 0.0%ni, 34.3%id, 0.0%wa, 0.0%hi, 2.3%si, 0.3%st 
    Cpu2 : 53.3%us, 2.6%sy, 0.0%ni, 34.1%id, 0.0%wa, 0.0%hi, 9.6%si, 0.3%st 
    Cpu3 : 52.7%us, 2.7%sy, 0.0%ni, 25.2%id, 0.0%wa, 0.0%hi, 19.5%si, 0.0%st 
    Cpu4 : 59.5%us, 2.7%sy, 0.0%ni, 31.2%id, 0.0%wa, 0.0%hi, 6.6%si, 0.0%st 
    Mem: 7680000k total, 7663152k used, 16848k free, 98068k buffers 
    Swap: 2096472k total, 14904k used, 2081568k free, 3032636k cached
    

    命令行显示了CPU4,说明这是一个5核的虚拟机,平均每个CPU利用率在60%以上。如果这里显示CPU利用率100%,则很有可能程序里写了一个死循环。这些参数的含义,可以对比表11-1来查看。

    表11-1 CPU参数含义
    在这里插入图片描述

    1. 使用top的交互命令H查看每个线程的性能信息。
    PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3
    1558 admin 15 0 5351m 4.0g 49m S 12.2 54.0 10:08.31 java 
    31561 admin 15 0 5351m 4.0g 49m R 12.2 54.0 9:45.43 java 
    31626 admin 15 0 5351m 4.0g 49m S 11.9 54.0 13:50.21 java 
    31559 admin 15 0 5351m 4.0g 49m S 10.9 54.0 5:34.67 java 
    31612 admin 15 0 5351m 4.0g 49m S 10.6 54.0 8:42.77 java 
    31555 admin 15 0 5351m 4.0g 49m S 10.3 54.0 13:00.55 java 
    31630 admin 15 0 5351m 4.0g 49m R 10.3 54.0 4:00.75 java 
    31646 admin 15 0 5351m 4.0g 49m S 10.3 54.0 3:19.92 java 
    31653 admin 15 0 5351m 4.0g 49m S 10.3 54.0 8:52.90 java 
    31607 admin 15 0 5351m 4.0g 49m S 9.9 54.0 14:37.82 java
    

    在这里可能会出现3种情况。

    1. 第一种情况,某个线程CPU利用率一直100%,则说明是这个线程有可能有死循环,那么请记住这个PID。
    2. 第二种情况,某个线程一直在TOP 10的位置,这说明这个线程可能有性能问题。
    3. 第三种情况,CPU利用率高的几个线程在不停变化,说明并不是由某一个线程导致CPU偏高。

    如果是第一种情况,也有可能是GC造成,可以用jstat命令看一下GC情况,看看是不是因为持久代或年老代满了,产生Full GC,导致CPU利用率持续飙高,命令和回显如下。

    sudo /opt/java/bin/jstat -gcutil 31177 1000 5 
    S0 S1 E O P YGC YGCT FGC FGCT GCT
    0.00 1.27 61.30 55.57 59.98 16040 143.775 30 77.692 221.467 
    0.00 1.27 95.77 55.57 59.98 16040 143.775 30 77.692 221.467 
    1.37 0.00 33.21 55.57 59.98 16041 143.781 30 77.692 221.474 
    1.37 0.00 74.96 55.57 59.98 16041 143.781 30 77.692 221.474 
    0.00 1.59 22.14 55.57 59.98 16042 143.789 30 77.692 221.481
    

    还可以把线程dump下来,看看究竟是哪个线程、执行什么代码造成的CPU利用率高。执行以下命令,把线程dump到文件dump17里。执行如下命令。

    sudo -u admin /opt/taobao/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17
    

    dump出来内容的类似下面内容。

    "http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in Object. wait() [0x0000000052423000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on (a org.apache.tomcat.util.net.AprEndpoint$Worker) at java.lang.Object.wait(Object.java:485) at org.apache.tomcat.util.net.AprEndpoint$Worker.await(AprEndpoint.java:1464) - locked (a org.apache.tomcat.util.net.AprEndpoint$Worker) at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1489) at java.lang.Thread.run(Thread.java:662)
    

    dump出来的线程ID(nid)是十六进制的,而我们用TOP命令看到的线程ID是十进制的,所以要用printf命令转换一下进制。然后用十六进制的ID去dump里找到对应的线程。

    printf "%x\n" 31558
    

    输出:7b46。

    性能测试

    因为要支持某个业务,有同事向我们提出需求,希望系统的某个接口能够支持2万的QPS,因为我们的应用部署在多台机器上,要支持两万的QPS,我们必须先要知道该接口在单机上能支持多少QPS,如果单机能支持1千QPS,我们需要20台机器才能支持2万的QPS。需要注意的是,要支持的2万的QPS必须是峰值,而不能是平均值,比如一天当中有23个小时QPS不足1万,只有一个小时的QPS达到了2万,我们的系统也要支持2万的QPS。

    我们先进行性能测试。我们使用公司同事开发的性能测试工具进行测试,该工具的原理 是,用户写一个Java程序向服务器端发起请求,这个工具会启动一个线程池来调度这些任务,可以配置同时启动多少个线程、发起请求次数和任务间隔时长。将这个程序部署在多台机器上执行,统计出QPS和响应时长。我们在10台机器上部署了这个测试程序,每台机器启动了100个线程进行测试,压测时长为半小时。注意不能压测线上机器,我们压测的是开发服务器。测试开始后,首先登录到服务器里查看当前有多少台机器在压测服务器,因为程序的端口是12200,所以使用netstat命令查询有多少台机器连接到这个端口上。命令如下。

    $ netstat -nat | grep 12200 –c 
    10
    

    通过这个命令可以知道已经有10台机器在压测服务器。

    QPS达到了1400,程序开始报错获取不到数据库连接,因为我们的数据库端口是3306,用netstat命令查看已经使用了多少个数据库连接。命令如下。

    $ netstat -nat | grep 3306 –c 
    12
    

    增加数据库连接到20,QPS没上去,但是响应时长从平均1000毫秒下降到700毫秒,使用TOP命令观察CPU利用率,发现已经90%多了,于是升级CPU,将2核升级成4核,和线上的机器保持一致。再进行压测,CPU利用率下去了达到了75%,QPS上升到了1800。执行一段时间后响应时长稳定在200毫秒。

    增加应用服务器里线程池的核心线程数和最大线程数到1024,通过ps命令查看下线程数是否增长了,执行的命令如下。

    $ ps -eLf | grep java -c 
    1520
    

    再次压测,QPS并没有明显的增长,单机QPS稳定在1800左右,响应时长稳定在200毫秒。

    我在性能测试之前先优化了程序的SQL语句。使用了如下命令统计执行最慢的SQL,左边 的是执行时长,单位是毫秒,右边的是执行的语句,可以看到系统执行最慢的SQL是queryNews和queryNewIds,优化到几十毫秒。

    $ grep Y /home/admin/logs/xxx/monitor/dal-rw-monitor.log |awk -F',' '{print $7$5}' | sort -nr|head -20 
    1811 queryNews 
    1764 queryNews 
    1740 queryNews 
    1697 queryNews 
    679 queryNewIds
    

    性能测试中使用的其他命令

    查看网络流量。

    $ cat /proc/net/dev 
    Inter-| Receive | Transmit 
    face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed 
    lo:242953548208 231437133 0 0 0 0 0 0 242953548208 231437133 0 0 0 0 0 0 
    eth0:153060432504 446365779 0 0 0 0 0 0 108596061848 479947142 0 0 0 0 0 0 
    bond0:153060432504 446365779 0 0 0 0 0 0 108596061848 479947142 0 0 0 0 0 0
    

    查看系统平均负载。

    $ cat /proc/loadavg 
    0.00 0.04 0.85 1/1266 22459
    

    查看系统内存情况。

    $ cat /proc/meminfo
    MemTotal: 4106756 kB 
    MemFree: 71196 kB 
    Buffers: 12832 kB 
    Cached: 2603332 kB 
    SwapCached: 4016 kB 
    Active: 2303768 kB 
    Inactive: 1507324 kB 
    Active(anon): 996100 kB 
    部分省略
    

    查看CPU的利用率。

    cat /proc/stat 
    cpu 167301886 6156 331902067 17552830039 8645275 13082 1044952 33931469 0 
    cpu0 45406479 1992 75489851 4410199442 7321828 12872 688837 5115394 0 
    cpu1 39821071 1247 132648851 4319596686 379255 67 132447 11365141 0 
    cpu2 40912727 1705 57947971 4418978718 389539 78 110994 8342835 0 
    cpu3 41161608 1211 65815393 4404055191 554651 63 112672 9108097 0
    

    异步任务池

    Java中的线程池设计得非常巧妙,可以高效并发执行多个任务,但是在某些场景下需要对线程池进行扩展才能更好地服务于系统。例如,如果一个任务仍进线程池之后,运行线程池的程序重启了,那么线程池里的任务就会丢失。另外,线程池只能处理本机的任务,在集群环境下不能有效地调度所有机器的任务。所以,需要结合线程池开发一个异步任务处理池。图11-2为异步任务池设计图。

    在这里插入图片描述

    任务池的主要处理流程是,每台机器会启动一个任务池,每个任务池里有多个线程池,当某台机器将一个任务交给任务池后,任务池会先将这个任务保存到数据中,然后某台机器上的任务池会从数据库中获取待执行的任务,再执行这个任务。

    每个任务有几种状态,分别是创建(NEW)、执行中(EXECUTING)、RETRY(重试)、挂起 (SUSPEND)、中止(TEMINER)和执行完成(FINISH)。

    • 创建:提交给任务池之后的状态。
    • 执行中:任务池从数据库中拿到任务执行时的状态。
    • 重试:当执行任务时出现错误,程序显式地告诉任务池这个任务需要重试,并设置下一次执行时间。
    • 挂起:当一个任务的执行依赖于其他任务完成时,可以将这个任务挂起,当收到消息后,再开始执行。
    • 中止:任务执行失败,让任务池停止执行这个任务,并设置错误消息告诉调用端。
    • 执行完成:任务执行结束。

    任务池的任务隔离。异步任务有很多种类型,比如抓取网页任务、同步数据任务等,不同类型的任务优先级不一样,但是系统资源是有限的,如果低优先级的任务非常多,高优先级的任务就可能得不到执行,所以必须对任务进行隔离执行。使用不同的线程池处理不同的任务,或者不同的线程池处理不同优先级的任务,如果任务类型非常少,建议用任务类型来隔离,如果任务类型非常多,比如几十个,建议采用优先级的方式来隔离。任务池的重试策略。根据不同的任务类型设置不同的重试策略,有的任务对实时性要求高,那么每次的重试间隔就会非常短,如果对实时性要求不高,可以采用默认的重试策略,重试间隔随着次数的增加,时间不断增长,比如间隔几秒、几分钟到几小时。每个任务类型可以设置执行该任务类型线程池的最小和最大线程数、最大重试次数。

    使用任务池的注意事项。任务必须无状态:任务不能在执行任务的机器中保存数据,比如某个任务是处理上传的文件,任务的属性里有文件的上传路径,如果文件上传到机器1,机器2获取到了任务则会处理失败,所以上传的文件必须存在其他的集群里,比如OSS或SFTP。异步任务的属性。包括任务名称、下次执行时间、已执行次数、任务类型、任务优先级和执行时的报错信息(用于快速定位问题)

    展开全文
  • 这就是最正宗的《Java 并发编程实战》带目录 用福昕阅读器打开查看特别的清晰
  • Java 7并发编程实战手册的英文第二版(2017.5)
  • java并发编程实战高清版pdf自用,分享给大家
  • Java并发编程实战 总结

    第一章到第五章的“并发技巧清单”:


    1.可变状态是至关重要的。
    所有的并发问题都可以归结为如何协调对并发状态的访问,可变状态越少,就越容易确保线程安全性。

    2.尽量将域声明为final类型,除非需要它们是可变的。

    3.不可变对象一定是线程安全的。
    不可变对象能极大地降低并发编程的复杂性。它们更为简单而且安全,可以任意共享而无须使用加锁或保护性复制等机制。

    4.封装有助于管理复杂性。
    在编写线程安全的程序时,虽然可以将所有数据都保存在全局变量中,但为什么要这样做?
    将数据封装在对象中,更易于维持不变性条件:将同步机制封装在对象,更易于遵守同步策略

    5.用锁来保护每个可变变量

    6.当保护同一个不变性条件中的所有变量时,要使用同一个锁。

    7.在执行复合操作期间,要持有锁。

    8.如果从多个线程中访问同一个可变变量时没有同步机制,那么程序会出现问题。

    9.不要自作聪明地推断出不需要使用同步。

    10.在设计过程中考虑线程安全,或者在文档中明确指出它不是线程安全的。

    11.将同步策略文档化。

    展开全文
  • Java 并发编程实战-随书源码,下载即可使用。(压缩包附有PDF链接)
  • 声明:Java并发的内容是自己阅读《Java并发编程实战》和《Java并发编程的艺术》整理来的。 图文并茂请戳 思维导图下载请戳 目录 (1)基础概念 (2)线程 (3)锁 (4)同步器 (5)并发容器和框架 (6)Java并发工具...

    声明:Java并发的内容是自己阅读《Java并发编程实战》和《Java并发编程的艺术》整理来的。

    这里写图片描述

    图文并茂请戳

    思维导图下载请戳

    目录

    (1)基础概念

    (2)线程

    (3)锁

    (4)同步器

    (5)并发容器和框架

    (6)Java并发工具类

    (7)原子操作类

    (8)Executor框架(执行机制)

    (9)其他


    (一).基础概念

    1.可见性和原子性

    • 可见性:一个线程修改了共享变量的值,另一个线程可以读到这个修改的值。

    • 原子性:不可被中断的一个或一系列操作。

    如何保障原子性:

    • 使用总线锁保证原子性。

    • 使用缓存锁保证原子性。

    2.原子操作的三种实现:

    (1).CAS(Compare And Swap 比较与交换)

    需要输入两个数值(一个旧值和一个新值),在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,如果发生了变化就不交换。

    存在的三大问题:

    • ABA问题:如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际是发生了变化。解决方案:1.使用版本号,在变量前面追加版本号,每次变量更新都把版本号加1。JDK提供的类:AtomicStampedReference。

    • 循环时间长开销大。

    • 只能保证一个共享变量的原子操作。
      解決方案:JDK提供AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里进行CAS操作。

    (2).锁

    (3).JDK并发包的支持

    如:AtomicBoolean(用原子方式更新的boolean值),

    AtomicInteger(用原子方式更新的int值),

    AutomicLong(用原子方式更新的long值)。

    3.同步原语

    (1).volatile

    特点:

    • 可见性:对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入。

    • 原子性:对任意单个volatile变量的读/写具有原子性。

    • 从内存语义角度:volatile的写-读与锁的释放-获取有相同的内存效果。

    • 为了实现volatile的内存语义,编译期在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

    • 从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语义,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。

    实现原理:

    • 将当前处理器缓存行的数据回写到系统内存。

    • 写回内存的操作会使其他CPU里缓存该内存地址的数据无效。

    (2).synchronized

    不同情況锁住的对象:

    • 对于普通同步方法,锁是当前实例对象。

    • 对于静态同步方法,锁是当前类的Class对象。

    • 对于同步方法块,锁是Synchronized括号里配置的对象。

    (3)final

    
    public class FinalExample {
    
    	int i;                     //普通变量
    	final int j;               //final变量
    	static FinalExample obj;
    	
    	public FinalExample() {    //构造函数
    		i = 1;                 //写普通域
    		j = 2;                 //写final域
    	}
    	
    	public static void writer() {
    		obj = new FinalExample();
    	}
    	
    	public static void reader() {
    		FinalExample object = obj;    //读对象引用
    		int a = object.i;             //读普通域
    		int n = object.j;             //读final域
    	}
    }
    
    
    • 写final域的重排序规则:JMM禁止将final域的写重排序到构造函数之外。
    • 读final域的重排序规则:在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。

    4.Java内存模型(JMM)

    5.重排序

    重排序的3种类型:

    • 编译器优化的重排序。

    • 指令级并行的重排序。

    • 内存系统的重排序。

    编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

    6.顺序一致性

    顺序一致性内存模型两大特征:

    • 一个线程中的所有操作必须按照程序的顺序来执行。

    • (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

    7.happens-before与as-if-serial

    JMM对happens-before的设计原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。

    happens-before关系的定义:

    (1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

    (2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么JMM允许这种重排序。

    as-if-serial:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。

    區別:as-if-serial语义保障单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。

    8.双重检查锁定与延迟初始化

    使用双重检查锁延迟初始化

    
    public class DoubleCheckedLocking {                         
    	
    	private static DoubleCheckedLocking instance;           
    	
    	public static DoubleCheckedLocking getInstance() {      
    		if(null == instance) {                              
    			synchronized(DoubleCheckedLocking.class) {      
    				if(null == instance) {                      
    					instance = new DoubleCheckedLocking();  
    				}
    			}
    		}
    		return instance;
    	}
    }
    
    

    线程A-A1:分配对象的内存空间。
    线程A-A2:设置instance指向内存空间。
    线程B-B1:判断instance是否为空。
    线程B-B2:由于instance不为null,线程B将访问instance引用的对象。
    线程A-A3:初始化对象
    线程A-A4:访问instance引用的对象。

    存在的问题:
    A2和A3重排序,线程B访问到一个还没初始化的对象。

    解决方案:

    • 将instance变量声明为volatile型的。通过禁止重排序来保证线程安全的延迟初始化。

    • 通过不允许其他线程”看到“这个重排序实现线程安全的延迟初始化。

    
    public class InstanceFactory {
    
    	private static class InstanceHolder {
    		public static InstanceFactory instance = new InstanceFactory();
    	}
    	
    	public static InstanceFactory getInstance() {
    		return InstanceHolder.instance;
    	}
    }
    
    

    9.生产者-消费者模式


    (二).线程

    1.什么是线程?

    现代操作系统在运行一个程序时,会为其创建一个进程。现代操作系统调度的最小单元是线程,也叫轻量级进程。在一个进程里可以创建多个线程,这些线程都拥有各自的计数器,堆栈和局部变量等属性。

    2.创建线程的三种方式

    (1).Thread

    
    @Test
    
    public void testThread() {
    
    	Thread thread = new Thread("myThread");
    	thread.start();
    }
    
    

    (2).Runnable

    
    @Test
    public void testRunnable() {
    	Thread thread = new Thread(new Runnable() {
    
    		@Override
    		public void run() {
    			System.out.println("myThread");
    		}
    		
    	});
    	thread.start();
    }
    
    

    (3).Callable

    
    @Test
    
    public void testFutureTask() throws InterruptedException, ExecutionException {
    
    	ExecutorService executorService = Executors.newFixedThreadPool(2);
    	Future<String> future = executorService.submit(new Callable<String>() {
    
    		@Override
    		public String call() throws Exception {
    			return "Hello,World!!!";
    		}
    		
    	});
    	String result = future.get();
    	System.out.println(result);
    	executorService.shutdown();
    	
    }
    
    

    3.Daemon线程

    • Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。
    • 当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。
    • 可以通过调用Thread,setDaemon(true)将线程设置为Daemon线程。

    4.等待/通知机制

    等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。

    等待方遵循如下规则:

    • 获取对象的锁

    • 如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件。

    • 条件满足则执行对应的逻辑。

    
    while(条件不满足){
        对象.wait();
    }
    
    //处理对应的逻辑
    
    

    通知方遵循如下规则:

    • 获得对象的锁。

    • 改变条件

    • 通知所有等待在对象上的线程。

    
    synchronized(对象){
    
            //改变条件
           对象.notifyAll();
    
    }
    
    

    5.Thread.join()

    当前线程A要等待thread线程终止之后才能从thread.join()返回。

    6.ThreadLocal

    ThreadLocal,即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定到这个线程上的一个值。

    7.线程的终止、中断

    (1).Thread.interrupt:中断线程

    • 除非线程正在进行中断它自身,否则都会接受这个方法的中断。会调用Thread.checkAccess(),可能会抛出SecurityException。

    • 如果线程调用了Object.wait(),Thread.sleep(),Thread.join()处于阻塞状态,那它的堵塞状态会被清除,并得到一个InterruptedException。

    • 如果线程在InterruptibleChannel上的I/O操作中被中断,通道会被关闭,线程的中断状态会被设置,并得到一个ClosedByInterruptedException。

    (2).Thread.interrupted:测试当前线程是否被中断。

    清除线程的中断状态。如果连续调用两次这个方法,第二次调用会返回false(除非当前线程再次被中断,在第一次调用清除它的中断状态之后,并且在第二次调用检查它之前)。

    (3).Thread.isInterrupted:测试某个线程是否被中断

    中断状态是否被重置取决于传入的值。


    (三).锁

    锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

    1.锁的内存语义:

    • 利用volatile变量的写-读所具有的内存语义。

    • 利用CAS所附带的volatile读和volatile写的内存语义。

    2.重入锁

    (1).什么是重入锁?
    支持重进入的锁,表示锁能够支持一个线程对资源的重复加锁。重入锁支持获取锁时的公平性和非公平性选择。

    (2).解决两个问题:

    • 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次获取锁。

    • 锁的最终释放:锁的最终释放要求锁对于锁获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经释放。

    3.排他锁:ReentrantLock

    (1)公平锁:

    • 公平锁释放时,最后要写一个volatile变量state。

    • 公平锁获取时,首先会去读volatile变量。

    (2)非公平锁:

    • 非公平锁释放时,最后要写一个volatile变量state。

    • 非公平锁获取时,首先会用CAS(CompareAndSet)更新volation变量,这个操作同时具有volatile写和volatile读的内存语义。

    (3)公平锁与非公平锁的区别

    公平性与否是针对获取锁而言的。

    • 公平锁:如果一个锁是公平的,那么获取锁的顺序就应该符合请求的绝对时间顺序,也就是FIFO。

    • 非公平锁:刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。

    公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成”饥饿“,但极少的线程切换,保证其更大的吞吐量。

    4.Lock

    (1)读写锁:ReentrantReadWriteLock

    读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被堵塞。

    读写锁的实现分析:

    • 读写状态的设计:同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。

    • 写锁的获取与释放:写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。

    • 读锁的获取与释放:如果当前线程已经获取了读锁,就增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。

    • 锁降级:锁降级指的是写锁降级为读锁。指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)读锁的过程。

    锁的四种状态:无锁,偏向锁,轻量级锁,重量级锁

    5.LockSupport工具

    当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应的工作。

    6.Condition接口

    Condition接口提供了类似Object的监视器方法(包括wait(),wait(long timeout),notify(),以及notifyAll()方法),与Lock配合可以实现等待/通知模式。

    Condition的实现:等待队列,等待和通知。

    • 等待队列:等待队列是一个FIFO队列,在队列中的每一个节点都包含了一个线程引用,该线程是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程会释放锁,构造成节点加入等待队列并进入等待状态。

    • 等待:调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关的锁。

    • 通知:调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移步到同步队列。

    7.避免活跃性危险

    (1).死锁

    • 哲学家用餐问题:每个线程都拥有别的线程需要的资源,同时又等待别人拥有的资源,在获得别的资源之前不会释放自己手上的资源。

    • 数据库事务死锁:数据库如果发生死锁,会选择一个事务释放资源。

    • 锁顺序死锁:线程A,B都需要锁1,2。线程A先获得锁1 ,再请求锁2 ,线程B先获得锁2,再请求锁1 。

    8.死锁的避免与诊断

    (1).内置锁:只要没有获得锁,就会一直等待下去。

    (2).定时锁:使用Lock类的定时tyLock功能。可以指定一个超时时限,在等待超过该时间后会返回失败信息。

    (3).线程转储

    避免死锁的四种方式:

    • 避免一个线程同时获得多个锁。

    • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只获得一个资源。

    • 使用定时锁。

    • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

    9.饥饿,糟糕的响应性,活锁

    • 饥饿:线程由于无法访问它需要的资源而不能继续执行,引发饥饿最常见的资源是CPU时钟周期。

    • 活锁通常发送在处理事务消息的应用程序中,如果不能成功地处理事务消息,那么消息机制将回滚整个事务,将这个事务放在等待队列的开头。

    • 当多个相互协作的线程都对彼此响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。

    • 在并发应用中,通过等待随机长度的时间和回退可以有效地避免活锁的发生。


    (四).同步器

    (1).实现

    • 同步队列:同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获得同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

    • 独占式同步状态获取与释放:在获取同步状态时,同步器维护一个队列,获取状态失败的线程会被加入队列中并在队列中进行自旋,移除队列的原因时自旋获取了同步状态且前驱节点时头节点。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继结点。

    • 共享式同步状态获取与释放:共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取同步状态。

    • 独占式超时获取同步状态:在指定的时间段内获取同步状态,如果获取到同步状态返回true,否则,返回false。

    (2).AbstractQueuedSynchronized

    用来构建锁或者其他同步组件的基础框架,使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

    提供了三个对同步状态进行修改的方法:

    • getState():获取当前同步状态。

    • setState(int new3State):设置当前同步状态。

    • compareAndSetState(int export,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。


    (五).并发容器和框架

    (1).ConcurrentHashMap

    与HashMap,HashTable对比:

    • HashMap是线程不安全,且可能导致程序死循环。

    • HashTable效率低下。

    • ConcurrentHashMap的锁分段技术可有效提升访问效率。首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段的数据的时候,其他段的数据也能被其他线程访问。

    ConCurrentHashMap的结构:ConCurrentHashMap是由Segment数组结构和HashEntry数组结构组成。

    (2).ConcurrentLinkedQueue

    ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点(next)的引用组成,从而组成一张链表结构的队列。

    (3).阻塞队列

    • 插入:当队列满时,队列会堵塞插入元素的线程,直到队列不满。

    • 移除:当队列为空,获取元素的线程会等待线程为非空。

    实现原理:
    使用通知模式实现。当生产者往满的队列里添加元素时会堵塞生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

    1.ArrayBlockingQueue

    • 数组实现的有界阻塞队列,按照先进先出的原则对元素进行排序。

    2.LinkedBlockingQueue

    • 继承了AbstractQueue类,实现了BlockingQueue接口。

    • 采用先进先出的排列方式,头结点是入队时间最长的元素,尾结点是入队时间最短的元素。新结点添加到队尾,从队头弹出结点。

    • 链表队列的特点是:跟基于数组的队列相比有更大的吞吐量,但在大多并发应用中性能会比较差。

    • LinkedBlockingQueue可以在创建的时候传递一个容量参数,限制队列的长度,不设定的情况下,默认是Integer.MAX_VALUE。在没有超过队列边界的情况下,每次添加会自动创建链表结点。

    3.PriorityBlockingQueue

    • 是一个支持优先级的无界阻塞队列。默认情况下时自然顺序升序排序。

    4.SychronousQueue

    • SynchronousQueue是一个不存储元素的堵塞队列,每一个put操作必须等待一个take操作,否则不能继续添加元素。

    5.DelayQueue

    • 延迟队列:无界队列,只有延迟过期的任务才能加入队列。队列的队首元素是在过去延迟过期最长的元素。如果没有延迟到期,队列中就没有元素,调用poll方法会返回null。当调用元素的getDelay(TimeUnit.NANOSECONDS)方法返回等于或小于0的值时,出现到期。

    • DelayQueue的应用场景:a.缓存系统:把需要缓存的元素加入DelayQueue中,让一个线程循环测试是否能从DelayQueue中获取元素,能表示缓存到期。b.定时任务调度。

    • Timer和DelayQueue的区别?Timer只能处理单个后台线程,而DelayQueue可以处理多个。

    6 . LinkedTransferQueue

    • LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。

    7 . LinkedBlockingDeque

    • 一个由链表结构组成的双向阻塞队列,可以运用在“工作窃取”模式中。

    (4).Fork/Join框架

    用于并行执行任务,把一个大任务分割成小任务,再把每个小任务的结果汇总成大任务结果。Fork是把一个大任务切分成若干子任务并行执行,Join是合并这些子任务的执行结果。

    (5).工作窃取算法

    指某个线程从其他队列里窃取任务来执行。为了减少窃取任务线程和别窃取任务线程之间的竞争,使用双端队列,被窃取任务线程从双端队列的头部拿任务执行,窃取任务线程从双端队列的尾部拿任务执行。

    • 工作窃取算法的优点:充分利用线程进行并行计算,减少线程间的竞争。

    • 工作窃取算法的缺点:在某些情况下还是存在竞争,比如双端队列里只有一个任务时,并且该算法会消耗更多的系统资源,比如创建多个线程和多个双端队列。


    (六).Java并发工具类

    1.CyclicBarrier

    一组线程在到达一个屏障(同步点)前被堵塞,直到最后一个线程到达屏障时,屏障才会放行,这组线程才能继续执行。

    应用场景:可以用于多线程计算数据,最后合并计算结果。

    CyclicBarrier与CountDownLatch的区别:CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置。CountDownLatch的计数是减法,CyclicBarrier的计数是加法。

    2.Semaphore

    用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用公共资源。

    应用场景:可以用于流量控制,特别是公共资源有限的应用场景,比如数据库连接。

    3.CountDownLatch

    允许一个或多个线程等待其他线程完成操作。

    4.Exchanger

    Exchanger是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,会一直等待第二个线程也执行exchange()方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

    应用场景:用于遗传算法。


    (七).原子操作类

    1.原子更新基本类型类

    • AtomicBoolean

    • AtomicInteger

    • AtomicLong

    2.原子更新数组

    • AtomicIntegerArray

    • AtomicLongArray

    • AtomicReferenceArray

    3.原子更新引用类型

    • AtomicReference

    • AtomicReferenceFieldUpdater 原子更新引用类型里的字段

    • AtomicMarkableReference 原子更新带有标记位的引用类型。

    4.原子更新字段类

    • AtomicIntegerFieldUpdater 原子更新整型的字段的更新器

    • AtomicLongFieldUpdater 原子更新长整型字段的更新器

    • AtomicStampedReference 原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。


    (八).Executor框架(执行机制)

    从JDK5开始,把工作单元和执行机制分离开来,工作单元包括Runnable和Callable,而执行机制由Executor框架提供。

    1.异步计算的结果:FutureTask和Future

    2.任务执行

    (1).Executor(核心接口)

    Executor的生命周期:创建,提交,开始,完成

    (2).ExecutorService接口(继承自Executor)

    • ExecutorService的生命周期:运行,关闭,已终止

    • ExecutorService.shutDown和ExecutorService.shutDownNow的区别

    调用的方法 作用
    shutDown 不再允许新的任务添加到等待队列,正在执行的任务和在等待队列中的任务会执行完毕再关闭。
    shurDownNow 立刻关闭。需要知道正在执行但是还没执行完毕的任务。
    • ExecutorService.submit()和ExecutorService.execute()的区别:接口ExecutorService的execute()方法是继承自Executor。
    调用的方法 作用
    execute 用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
    submit 用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功,并且通过Future的get()方法来获取返回值,get()方法会阻塞线程直到任务完成。
    • ExecutorService的创建:
    调用 分类 使用
    Executors.newSingleThreadExecutor() ThreadPoolExecutor 应用场景:适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程活动的应用场景。
    Exectors.newFiexedThreadPool() ThreadPoolExecutor 1.创建一个线程池,具有固定线程数,运行在共享的无界队列中。2.在大多数时候,线程会主动执行任务,当所有的线程都在执行任务时,有新的任务加入进来,就会进入等待队列(可以有源源不断的任务加入进来,因为是无界队列),当有空闲的线程,等待队列中的任务就会被执行。3.如果有线程在执行过程中因为执行失败要关闭,新创建的线程会替失败的线程执行接下来的任务。4.如果想要关闭这个线程池,可以调用ExecutorService的shutDown方法。应用场景:适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。
    Executors.newCachedThreadPool() ThreadPoolExecutor 应用场景:适用于执行很多短期异步任务的小程序,或者是负载较轻的服务器。
    Executors.newScheduledThreadPool() ScheduledThreadPoolExecutor 应用场景:适用于需要多个后台线程执行周期任务,同时为了满足管理的需求而需要限制后台线程的数量的应用场景。
    Executors.newSingleThreadScheduledExecutor() ScheduledThreadPoolExecutor 应用场景:需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务。

    3.任务:Runnable接口和Callable接口


    (九).其他

    1.jstack

    打开cmd,输入:

    • jps 查看pid(进程号)

    • jstack pid

    2.资源限制:指在进行并发编程时,程序的执行速度受到计算机硬件资源或软件资源的限制。

    3.上下文切换

    减少上下文切换的方法:

    • 无锁并发编程

    • CAS算法

    • 使用最少线程

    • 使用协程

    展开全文
  • 《Java并发编程实战》的高清完整PDF版,需要学习Java并发编程的同学,可以下载学习
  • Java7并发编程实战手册 ![这里写图片描述]...

    Java7并发编程实战手册

    这里写图片描述

    链接: https://pan.baidu.com/s/1uYS-EGTtcjr0Fy7aJb70rw 密码: xs6b

    展开全文
  • Java并发编程实战

    2020-02-19 14:10:59
    并发理论基础 可见性、原子性和有序性问题:并发编程Bug的源头
  • 可直接去百度云下载: https://pan.baidu.com/s/1gfKnw3L C++并发编程实战, Anthony Williams, pdf全本 C++ Concurrency in Action
  • Java并发编程实战-并发调度模式框架

    千次阅读 2020-03-11 09:45:48
    Java并发编程实战-并发调度模式框架 加油站:抱怨是最没有营养的一件事. 前言: 选择串行的方式执行任务,串行处理机制通常无法提高高吞吐率和快速响应性,于是我们可以显式地为任务创建线程,为每一个请求创建一个...
  • C++并发编程实战
  • 《Java7并发编程实战手册》书中实例代码,有此书的TX就直接在此下载吧。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 51,965
精华内容 20,786
关键字:

并发编程实战下载