编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1String深入理解原理
  • 1.2浮点型数据深入研究
  • 1.3数据装箱和拆箱原理
  • 1.4泛型由来和设计思想
  • 1.5加密和解密设计和原理
  • 2.1面向对象设计思想
  • 2.2抽象类和接口设计
  • 2.3封装和继承设计思想
  • 2.4复用和组合设计思想
  • 2.5对象和引用设计思想
  • 3.1IO流设计思想和原理
  • 3.2为何设计序列化数据
  • 3.3各种拷贝数据比较
  • 3.4高效文件读写的原理
  • 4.1反射性能探索和优化
  • 4.2为何要设计注解思想
  • 4.3动态代理的设计思想
  • 4.4SPI机制设计的思想
  • 4.5异常设计和捕获原理
  • 4.6虚拟机如何处理异常
  • 4.7四种引用设计思想
  • 5.1线程的前世今生探索
  • 5.2线程通信的设计思想
  • 5.3线程监控和Debug设计
  • 5.4线程和JVM之间联系
  • 5.5线程池使用技巧介绍
  • 5.6线程池设计核心原理
  • 5.7线程如何最大优化
  • 6.1多线程并发经典案例
  • 6.2并发安全前世今生
  • 6.3线程安全如何保证
  • 6.4变量的线程安全探索
  • 6.5并发上下文切换原理
  • 6.6理解CAS设计和由来
  • 6.7协程设计思想和原理
  • 6.8事物并发模型解读
  • 6.9并发设计模型研究
  • 6.10并发编程数据一致性
  • 6.11锁问题的定位和修复
  • 6.12多线程如何性能调优
  • 7.1类的加载过程和原理
  • 7.2对象布局设计的原理
  • 7.3双亲委派机制设计思想
  • 7.5代码攻击和安全防护
  • 7.6设计动态生成Java类

6.9并发设计模型研究

目录介绍

  • 01.并发设计模式介绍
    • 1.1 不可变解决并发
    • 1.2 COW解决并发
    • 1.3 线程本地存储模式
  • 02.不变性模式解读
    • 2.1 解决并发问题
    • 2.2 实现不可变性的类
    • 2.3 Java SDK不可变类
    • 2.4 不可变修改操作
    • 2.5 享元模式的优化
    • 2.6 Immutability注意点
  • 03.COW策略模式解读
    • 3.1 为何有COW模式
    • 3.2 COW应用场景
    • 3.3 RPC框架应用COW分析
    • 3.4 COW如何解决并发
    • 3.5 COW内存消耗
    • 3.6 COW为何没有LinkedList
  • 04.线程本地存储解读
    • 4.1 为何局部变量线程安全
    • 4.2 理解线程本地存储
    • 4.3 线程本地存储场景
    • 4.4 ThreadLocal核心设计
    • 4.5 ThreadLocal内存泄漏
    • 4.6 ThreadLocal缺点
    • 4.7 避免共享解决并发总结

01.并发设计模式介绍

1.1 不可变解决并发

1.2 COW解决并发

1.3 线程本地存储模式

02.不变性模式解读

2.1 解决并发问题

  • 多线程并发问题
    • “多个线程同时读写同一共享变量存在并发问题”,这里的必要条件之一是读写,如果只有读,而没有写,是没有并发问题的。
  • 如何快速解决并发问题
    • 最简单的办法就是让共享变量只有读操作,而没有写操作。这个办法如此重要,以至于被上升到了一种解决并发问题的设计模式:不变性(Immutability)模式。
    • 所谓不变性,简单来讲,对象一旦被创建之后,状态就不再发生变化。换句话说,就是变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性。

2.2 实现不可变性的类

  • 实现一个具备不可变性的类。
    • 将一个类所有的属性都设置成 final 的,并且只允许存在只读方法,那么这个类基本上就具备不可变性了。
    • 更严格的做法是这个类本身也是 final 的,也就是不允许继承。因为子类可以覆盖父类的方法,有可能改变不可变性,所以推荐你在实际工作中,使用这种更严格的做法。

2.3 Java SDK不可变类

  • SDK 里很多类都具备不可变性。
    • 例如经常用到的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。
    • 如果你仔细翻看这些类的声明、属性和方法,你会发现它们都严格遵守不可变类的三点要求:类和属性都是 final 的,所有方法均是只读的。
  • Java 的 String 方法也有类似字符替换操作,怎么能说所有方法都是只读的呢?
    • 结合 String 的源代码来解释一下这个问题,下面的示例代码源自 Java 1.8 SDK,仅保留了关键属性 value[]和 replace() 方法
    • 你会发现:String 这个类以及它的属性 value[]都是 final 的;而 replace() 方法的实现,就的确没有修改 value[],而是将替换后的字符串作为返回值返回了。
    public final class String {
      private final char value[];
      // 字符替换
      String replace(char oldChar, 
          char newChar) {
        //无需替换,直接返回this  
        if (oldChar == newChar){
          return this;
        }
    
        int len = value.length;
        int i = -1;
        /* avoid getfield opcode */
        char[] val = value; 
        //定位到需要替换的字符位置
        while (++i < len) {
          if (val[i] == oldChar) {
            break;
          }
        }
        //未找到oldChar,无需替换
        if (i >= len) {
          return this;
        } 
        //创建一个buf[],这是关键
        //用来保存替换后的字符串
        char buf[] = new char[len];
        for (int j = 0; j < i; j++) {
          buf[j] = val[j];
        }
        while (i < len) {
          char c = val[i];
          buf[i] = (c == oldChar) ? 
            newChar : c;
          i++;
        }
        //创建一个新的字符串返回
        //原字符串不会发生任何变化
        return new String(buf, true);
      }
    }
  • 如果具备不可变性的类,需要提供类似修改的功能,具体该怎么操作呢?
    • 做法很简单,那就是创建一个新的不可变对象,这是与可变对象的一个重要区别,可变对象往往是修改自己的属性。
    • 所有的修改操作都创建一个新的不可变对象,你可能会有这种担心:是不是创建的对象太多了,有点太浪费内存呢?是的,这样做的确有些浪费,那如何解决呢?可以用享元模式

2.5 享元模式的优化

  • 利用享元模式可以减少创建对象的数量,从而减少内存占用。
    • Java 语言里面 Long、Integer、Short、Byte 等这些基本数据类型的包装类都用到了享元模式。
    • 以 Long 这个类作为例子,看看它是如何利用享元模式来优化对象的创建的。
  • 享元模式本质上其实就是一个对象池
    • 利用享元模式创建对象的逻辑也很简单:创建之前,首先去对象池里看看是不是存在;如果已经存在,就利用对象池里的对象;如果不存在,就会新创建一个对象,并且把这个新创建出来的对象放进对象池里。
  • Long 这个类并没有照搬享元模式
    • Long 内部维护了一个静态的对象池,仅缓存了(-128,127)之间的数字,这个对象池在 JVM 启动的时候就创建好了,而且这个对象池一直都不会变化,也就是说它是静态的。
    • 之所以采用这样的设计,是因为 Long 这个对象的状态共有 264 种,实在太多,不宜全部缓存,而(-128,127)之间的数字利用率最高。

2.6 Immutability注意点

  • 在使用 Immutability 模式的时候,需要注意以下两点:
    • 对象的所有属性都是 final 的,并不能保证不可变性;
    • 不可变对象也需要正确发布。
  • 在 Java 语言中,final 修饰的属性一旦被赋值,就不可以再修改,但是如果属性的类型是普通对象,那么这个普通对象的属性是可以被修改的。
    • 例如下面的代码中,Bar 的属性 foo 虽然是 final 的,依然可以通过 setAge() 方法来设置 foo 的属性 age。
    • 所以,在使用 Immutability 模式的时候一定要确认保持不变性的边界在哪里,是否要求属性对象也具备不可变性。
    class Foo{
      int age=0;
      int name="abc";
    }
    final class Bar {
      final Foo foo;
      void setAge(int a){
        foo.age=a;
      }
    }

03.COW策略模式解读

3.1 为何有COW模式

  • 先来看一个案例,看看String类中replace方法
    • Java 里 String 这个类在实现 replace() 方法的时候,并没有更改原字符串里面 value[]数组的内容,而是创建了一个新字符串,这种方法在解决不可变对象的修改问题时经常用到。
    • 如果深入地思考这个方法,你会发现它本质上是一种 Copy-on-Write 方法。所谓 Copy-on-Write,经常被缩写为 COW 或者 CoW,顾名思义就是写时复制。
    • 不可变对象的写操作往往都是使用 Copy-on-Write 方法解决的,当然 Copy-on-Write 的应用领域并不局限于 Immutability 模式。

3.2 COW应用场景

  • 常见的Copy-on-Write模式应用
    • CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个 Copy-on-Write 容器,它们背后的设计思想就是 Copy-on-Write;
    • 通过 Copy-on-Write 这两个容器实现的读操作是无锁的,由于无锁,所以将读操作的性能发挥到了极致。
    • Java 提供的 Copy-on-Write 容器,由于在修改的同时会复制整个容器,所以在提升读操作性能的同时,是以内存复制为代价的。
  • Java中的Copy-on-Write 容器使用建议
    • CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个 Copy-on-Write 容器在修改的时候会复制整个数组,如果容器经常被修改或者这个数组本身就非常大的时候,是不建议使用的。
    • 反之,如果是修改非常少、数组数量也不大,并且对读性能要求苛刻的场景,使用 Copy-on-Write 容器效果就非常好了。
  • Copy-on-Write 其实就是在操作系统领域
    • Unix 的操作系统中创建进程的 API 是 fork(),传统的 fork() 函数会创建父进程的一个完整副本,例如父进程的地址空间现在用到了 1G 的内存,那么 fork() 子进程的时候要复制父进程整个进程的地址空间(占有 1G 内存)给子进程,这个过程是很耗时的。
    • Linux 中的 fork() 函数就聪明得多,fork() 子进程的时候,并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间;只用在父进程或者子进程需要写入的时候才会复制地址空间,从而使父子进程拥有各自的地址空间。
    • 父子进程的地址空间以及数据都是要隔离的,使用 Copy-on-Write 更多地体现的是一种延时策略,只有在真正需要复制的时候才复制,而不是提前复制好,同时 Copy-on-Write 还支持按需复制,可以提升性能。

3.3 RPC框架应用COW分析

3.4 COW如何解决并发

3.5 COW内存消耗

3.6 COW为何没有LinkedList

04.线程本地存储解读

4.1 为何局部变量线程安全

  • 两个线程可以同时用不同的参数调用相同的方法,那调用栈和线程之间是什么关系呢?
    • 答案是:每个线程都有自己独立的调用栈。因为如果不是这样,那两个线程就互相干扰了。
    • 如下面这幅图所示,线程 A、B、C 每个线程都有自己独立的调用栈。
    • image
      image
  • Java 方法里面的局部变量是否存在并发问题?
    • 现在你应该很清楚了,一点问题都没有。因为每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈里面,不会共享,所以自然也就没有并发问题。
    • 再次重申一遍:没有共享,就没有伤害。

4.2 理解线程本地存储

  • 没有共享变量也不会有并发问题,如何避免共享呢?
    • 思路其实很简单,对应到并发编程领域,就是每个线程都拥有自己的变量,彼此之间不共享,也就没有并发问题。
    • Java 语言提供的线程本地存储(ThreadLocal)就能够做到。

4.3 线程本地存储场景

4.4 ThreadLocal核心设计

4.5 ThreadLocal内存泄漏

  • https://www.cnblogs.com/pengxurui/p/17103753.html

4.6 ThreadLocal缺点

4.7 避免共享解决并发总结

  • ThreadLocal 超强图解,这次终于懂了~
    • https://www.cnblogs.com/pengxurui/p/17103753.html
贡献者: yangchong211
上一篇
6.8事物并发模型解读
下一篇
6.10并发编程数据一致性