草庐IT

Java 并发基础之并发编程通识

you的日常 2023-03-28 原文

引言

并发编程是一个经典的话题,由于摩尔定律已经改变,芯片性能虽然仍在不断提高,但相比加快 CPU 的速度,计算机正在向多核化方向发展。虚拟化的赋能,让多核服务器的弹性创建和扩容都更加便捷。为了尽可能的提高程序的性能,硬件,操作系统,程序编译器进行一系列的设计和优化,但是同时,带来了影响并发安全的3类问题:

CPU 增加了缓存,以均衡与内存的速度差异,但是带来了可见性问题
操作系统增加了线程,行程,分时复用 cpu 以增加 cpu 利用率,但是带来的线程切换的原子性问题
编译程序优化指令次序,但是带来了有序性问题
Java 作为互联网后端最主流的高级语言,以及大数据工程的事实标准语言,从诞生初始并发编程就是其重要特性之一。Java 提供了许多基本的并发功能来辅助多线程应用程序的开发。从 1.5 之前基于管程模型的同步锁,到 1.5 内存模型重构后广泛使用的 CAS+AQS 的乐观模型,随着版本的演进,并发编程的操作难度越来越低,但是另一方面,相对底层的并发功能与上层的应用程序的并发语义之间并不存在一种简单而直观的映射关系。
因此,即使面对众多并发工具,开发人员可能也陷入着无法选取合理武器的困局,为了能正确且高效的使用这些功能,对 Java 提供的并发工具有一个系统的大局观并了解其原理是 Java 开发人员必须关注的重点。本文将通过几个最具有代表性的问题的剖析,展现 Java 并发设计的核心关键点,为最佳实践打好理论基础。

image.png

synchronized 和 Lock 可以互相替代吗?

synchronized 是 Java 1.0 即加入的并发解决方案,其原理为只支持一个条件变量的简化后的 MESA 模型。而 Lock 是 Java 1.5 加入的基于完整 MESA 模型的 Api 原语。解答是否可以互相替代的问题,可以先从区别比较入手:

比较项目 synchronized Lock
形态 Jvm 层面的关键字 Java语言层面的Api
管程模型 只支持一个条件变量的简化后的MESA模型 支持多个条件变量的完整MESA模型
锁的获取 进入同步代码块即开始竞争锁,未获得锁的线程会一直等待 可以通过API实现多种多样的的竞争
锁的释放 1.持有锁的线程发生异常,Jvm 强制线程释放锁 2.拥有锁的线程执行完同步代码块,自动释放 基于 Api 的手动释放
锁类型 可重入,不可中断,不可公平 可重入,可中断,可公平
锁状态 无法判断 通过 Api 判断
取舍 1.6 优化后性能是Lock 的两倍 基于管程语义的 Api 功能更强大

通过表格中的对比非常明显的得出,Lock 可以在大部分情况下替换 synchronize,但是反过来不然。对于两者的使用,有以下最佳实践方案:

  • 优先使用 synchronized,当不满足并发需求时使用 Lock,如多个条件变量,希望竞争公平等
  • 使用 Lock 时注意两个范式:try-finally 和 乐观自旋

下面是两种工具实现的阻塞队列,其间区别非常明显:

synchronized

//很标准的模式,没有扩展点
public class BlockQueue {

   private final int maxSize;
   private LinkedList<Integer> values;

   BlockQueue(int size) {
      maxSize = size;
      values = new LinkedList<>();
   }

   public void put(int value) throws InterruptedException{
       // 可以锁 values ,也可以锁 BlockQueue.class 
       synchronized (values) {
           while (values.size() == maxSize) {
               try {
                 values.wait();
               } catch (InterruptedException ex) {
                  ex.printStackTrace();
               }
           }
           values.add(value);
           values.notifyAll();
       }
   }

   public int take() throws InterruptedException{
       synchronized (values) {
           while (values.size() == 0) {
               try {
                   values.wait();
                } catch (InterruptedException e) {
                   e.printStackTrace();
                }
           }
           values.notifyAll();
           return values.removeFirst();
        }
    }
}

Lock

public class BlockQueue {
    private final int maxSize;
    private ReentrantLock lock;
    private Condition notFull;
    private Condition notEmpty;
    private LinkedList<Integer> values;

    BlockQueue(int size) {
        //公平锁,讲究先来后到
        lock = new ReentrantLock(true);

        // 两个条件变量
        notFull = lock.newCondition();
        notEmpty = lock.newCondition();
        maxSize = size;
        values = new LinkedList<>();
    }

    public void put(int value) {
        / /尝试 1 分钟
        lock.tryLock(1, TimeUnit.MINUTES);
        // try-finally范式
        try {
          // while范式,可以判断锁的状态
          while (values.size() == maxSize && lock.isLocked()) {
               //阻塞线程至相应条件变量的等待队列
               notFull.await();
          }

          values.add(value);

             //唤醒相应条件变量的等待队列中的线程
             notEmpty.signalAll();
         } catch (Exception e) {
       } finally {
          lock.unlock();
       }
   }

   public int take() {
       int value;

       //获取可以被中断的锁,当被中断时,需要处理InterruptedException
       lock.lockInterruptibly();
          try {
             while (values.size() == 0 && lock.isLocked()) {
                 notEmpty.await();
             }
            notFull.signalAll();
         } catch (InterruptedException e) {
            if (Thread.currentThread().isInterrupted() {
               System.out.println(Thread.currentThread().getName() + " interrupted.");
            }
         } finally {
            value = values.poll();
            lock.unlock();
         }
        return value;
    }
 }

当然,在 Java 中是不需要自己手写阻塞队列的,Java 1.8 并发包中提供了7种实现,满足各类场景的需求

如何按需定制一个线程池

在并发处理的场景下,程序可能要频繁的创建线程工作,完毕后销毁。虽然Java中创建线程就像 new 一个对象一样简单,销毁也是 Jvm 的 GC 自动搞定的。但实际上创建线程是非常复杂的。创建一个普通对象,仅仅是在 Jvm 的堆内存中划分一块内存而已。而创建一个线程,却需要调用操作系统的 Api 分配一系列资源,这个成本和对象无法相提并论的。
程序中应该避免频繁创建和销毁如此重量级的线程对象,标准的解决方案就是池技术,在 Java 中, ThreadPoolExecutor 就是线程池工具。

线程池基本工作流程

不同于标准的池模型, ThreadPoolExecutor 没有 acquire方法 获得资源,没有 release方法 释放资源,其通过 7 个构造参数构建了生产者-消费者的模式。
线程池的内部核心原理是内部通过阻塞队列来缓存任务,调用 execute方法 的线程为生产者,内部的一组工作线程为消费者,获得 Runnable 任务并执行。

内部核心原理.png

正确使用线程池就是正确配置其构造参数,有以下最佳实践或注意事项:

有关Java 并发基础之并发编程通识的更多相关文章

  1. java - 等价于 Java 中的 Ruby Hash - 2

    我真的很习惯使用Ruby编写以下代码:my_hash={}my_hash['test']=1Java中对应的数据结构是什么? 最佳答案 HashMapmap=newHashMap();map.put("test",1);我假设? 关于java-等价于Java中的RubyHash,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/22737685/

  2. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  3. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  4. java - 我的模型类或其他类中应该有逻辑吗 - 2

    我只想对我一直在思考的这个问题有其他意见,例如我有classuser_controller和classuserclassUserattr_accessor:name,:usernameendclassUserController//dosomethingaboutanythingaboutusersend问题是我的User类中是否应该有逻辑user=User.newuser.do_something(user1)oritshouldbeuser_controller=UserController.newuser_controller.do_something(user1,user2)我

  5. java - 什么相当于 ruby​​ 的 rack 或 python 的 Java wsgi? - 2

    什么是ruby​​的rack或python的Java的wsgi?还有一个路由库。 最佳答案 来自Python标准PEP333:Bycontrast,althoughJavahasjustasmanywebapplicationframeworksavailable,Java's"servlet"APImakesitpossibleforapplicationswrittenwithanyJavawebapplicationframeworktoruninanywebserverthatsupportstheservletAPI.ht

  6. Observability:从零开始创建 Java 微服务并监控它 (二) - 2

    这篇文章是继上一篇文章“Observability:从零开始创建Java微服务并监控它(一)”的续篇。在上一篇文章中,我们讲述了如何创建一个Javaweb应用,并使用Filebeat来收集应用所生成的日志。在今天的文章中,我来详述如何收集应用的指标,使用APM来监控应用并监督web服务的在线情况。源码可以在地址 https://github.com/liu-xiao-guo/java_observability 进行下载。摄入指标指标被视为可以随时更改的时间点值。当前请求的数量可以改变任何毫秒。你可能有1000个请求的峰值,然后一切都回到一个请求。这也意味着这些指标可能不准确,你还想提取最小/

  7. 【Java 面试合集】HashMap中为什么引入红黑树,而不是AVL树呢 - 2

    HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候

  8. 网络编程套接字 - 2

    网络编程套接字网络编程基础知识理解源`IP`地址和目的`IP`地址理解源MAC地址和目的MAC地址认识端口号理解端口号和进程ID理解源端口号和目的端口号认识`TCP`协议认识`UDP`协议网络字节序socket编程接口`sockaddr``UDP`网络程序服务器端代码逻辑:需要用到的接口服务器端代码`udp`客户端代码逻辑`udp`客户端代码`TCP`网络程序服务器代码逻辑多个版本服务器单进程版本多进程版本多线程版本线程池版本服务器端代码客户端代码逻辑客户端代码TCP协议通讯流程TCP协议的客户端/服务器程序流程三次握手(建立连接)数据传输四次挥手(断开连接)TCP和UDP对比网络编程基础知识

  9. postman接口测试工具-基础使用教程 - 2

    1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,

  10. 软件测试基础 - 2

    Ⅰ软件测试基础一、软件测试基础理论1、软件测试的必要性所有的产品或者服务上线都需要测试2、测试的发展过程3、什么是软件测试找bug,发现缺陷4、测试的定义使用人工或自动的手段来运行或者测试某个系统的过程。目的在于检测它是否满足规定的需求。弄清预期结果和实际结果的差别。5、测试的目的以最小的人力、物力和时间找出软件中潜在的错误和缺陷6、测试的原则28原则:20%的主要功能要重点测(eg:支付宝的支付功能,其他功能都是次要的)80%的错误存在于20%的代码中7、测试标准8、测试的基本要求功能测试性能测试安全性测试兼容性测试易用性测试外观界面测试可靠性测试二、质量模型衡量一个优秀软件的维度①功能性功

随机推荐