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

引言

并发编程是一个经典的话题,由于摩尔定律已经改变,芯片性能虽然仍在不断提高,但相比加快 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

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

还有 56% 的精彩内容
©著作权归作者所有,转载或内容合作请联系作者
支付 ¥9.99 继续阅读
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容

  • 互联网的快速发展,Java开发的过程或多或少会需要进行并发编程,也会遇到一些并发编程带来的各种bug。下面从并发编...
    Johar77阅读 748评论 0 4
  • 注:其一、本文章为作者读完《实战Java高并发程序设计》之后所总结的知识,其中涵盖了每一章节的精髓之处。其二、文章...
    页川叶川阅读 859评论 0 1
  • 处理器:即中央处理器(CPU,Central Processing Unit),它是一块超大规模的集成电路,是一台...
    4553675200ad阅读 536评论 0 0
  • 缓存 缓存比较好理解,在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪。使用缓存不单单能够提升...
    阿斯蒂芬2阅读 12,136评论 1 28
  • 目录 概念 并行是指两个或者多个事件在同一时刻发生(cpu多核);而并发是指两个或多个事件在同一时间间隔内发生 饥...
    后来丶_a24d阅读 963评论 0 13