CloudNativeEra
  • Introduction
  • 名词解释
  • Computer Science
    • Computer Organization
      • CPU
      • 二进制、电路、加法器、乘法器
      • 编译、链接、装载
      • 存储器
      • IO
    • Operating System
      • 操作系统基础知识
      • 系统初始化
      • 进程管理
      • Everything about Memory
      • 文件系统
      • 并行编程
      • Linux
        • CPU
        • IO 多路复用
        • DMA IO and Linux Zero Copy
    • Computer Network
      • 网络相关命令
      • 评估系统的网络性能
      • 网络抓包
      • Linux 最多支撑的 TCP 连接
      • 网络虚拟化
      • DHCP 工作原理
    • Data Structure and Algorithm
      • 题目列表
      • Summarize
        • 方法总结
        • 二分思想
        • 树的演化
        • 算法思想总结
      • Data Structure
        • Data Struct - Array
        • Tree
        • Heap
        • Hash
        • 字符串
      • Algorithm
        • Sorting Algorithm
        • 查找
        • 贪心算法
        • 动态规划
        • 位运算
      • Practice Topics
        • Data Struct in SDK
        • Topic - Tree
        • Topic - Graph
        • Topic - 滑动窗口
        • 剑指 Offer 题解
    • 并发编程
      • 并发模式
      • 并发模型
  • 系统设计
    • 软件设计
      • 软件架构
      • 编程范式
      • 系统设计题
      • 设计原则
      • 计算机程序的构造和解释 SICP
    • 领域驱动设计
      • 应用:在线请假考勤管理
      • 应用: library
    • 微服务与云原生
      • Designing and deploying microservices
      • 容器技术
      • Docker
      • Etcd
      • Kubernetes
        • Kubernetes - Mapping External Services
      • Istio
      • 监控
    • 分布式系统
      • 分布式理论
      • 分布式事务
    • 后端存储设计
      • 缓存设计
      • 数据库架构设计
    • CI/CD
    • 设计最佳实践
    • 测试
    • 安全
    • 综合
      • 开发实践
      • 分布式锁
      • 分布式计数服务
      • 弹幕系统设计
      • 消息队列设计
      • 分布式ID生成算法
      • 限流设计
      • 网关设计
      • 通用的幂等设计
      • 分布式任务调度
        • Timer
        • ScheduledExecutorService
        • Spring Task
        • Quartz
      • 交易系统
      • 权限设计
  • 编程语言
    • 编程语言
    • C & C++
    • Java
      • JVM
        • JVM Bytecode
      • Java 核心技术
      • Java 8 新特性
      • Java 集合框架
      • Java NIO
      • 并发编程
        • 线程生命周期与线程中断
        • 三个线程交替打印
        • 两个线程交替打印奇偶
        • 优雅终止线程
        • 等待通知机制
        • 万能钥匙:管程
        • 限流器
        • 无锁方案 CAS
    • Java 源码阅读
      • Unsafe
      • 异步计算 Future
      • Java Queue
      • CoalescingRingBuffer 分析
      • Java Collections
        • PriorityQueue 分析
        • HashMap 分析
        • TreeMap
    • Golang
    • Python
  • 框架/组件/类库
    • Guava
      • Guava Cache
      • Guava EventBus
    • RxJava
    • Apache MINA
    • Netty
      • 网络 IO 模型
      • Netty 生产问题
    • Apache Tomcat
    • MyBatis
    • 限流框架
    • Spring Framework
      • Spring Core
      • Spring 事务管理
    • Spring Boot
    • Spring Cloud
      • Feign & OpenFeign
      • Ribbon
      • Eurake
      • Spring Cloud Config
    • FixJ
    • Metrics
    • Vert.x
  • 中间件
    • Redis
      • Redis 基础
        • Redis 数据结构设计与实现
        • Redis 高性能网络模型
      • Redis checklist
      • 应用案例 - Redis 数据结构
      • 应用案例 - Redis 缓存应用
      • 应用案例 - Redis 集群
      • Redis 客户端
      • Redis 生产案例
        • [译] 在 Redis 中存储数亿个简单键值对
    • MySQL
      • MySQL 基础
      • MySQL Index
      • MySQL Transaction
      • MySQL 优化
      • MySQL 内核
      • MySQL Command
      • MySQL Checklist
      • MySQL Analysis Tool
      • 实现 MySQL
    • State Machine
    • 数据库连接池
    • MQ
      • 高性能内存队列 Disruptor
      • Kafka
      • Pulsar
      • RocketMQ
        • Broker 的设计与实现
      • NSQ
  • 实际案例
    • 线上 Case
      • Request Aborted
      • MySQL - Specified key was too long
      • Java 应用 CPU 100% 排查优化
      • 频繁 GC 导致的 Java 服务不响应
      • 导出优化
  • 大数据
    • 流计算
    • Flink
  • 其他
    • 工具
    • 读书
      • 设计数据密集型应用
      • 实现领域驱动设计
      • 精通比特币
      • 提问的智慧
    • 论文
    • 工程博客
    • 阅读源码
    • 面试
      • 如何在最短的时间里对对方有个全面的了解
    • 分享
    • 软技能
    • Todo
  • Blog
    • #算法
      • 查找
      • 位运算
      • 树
    • #架构
      • 1- 通信
    • Design & Dev & Opt
      • High Performance Data structure Design
  • Tiny Project
    • A Simple WeChat-like Instant Messaging Platform
由 GitBook 提供支持
在本页
  • 等待通知机制
  • 经典范式
  • Guarded Suspension 模式
  • 经典问题
  • 互斥锁保护共享资源

这有帮助吗?

  1. 编程语言
  2. Java
  3. 并发编程

等待通知机制

等待通知机制

多线程的真正价值在于多个线程相互配合完成一件复杂的事情,或者相互配合完成一件计算繁重的任务;可见,相互配合是重点,配合就是分工任务、同步数据和互斥访问共享资源。比如当线程A完成了一件工作后,需要让线程B执行,线程B完成一件工作后,需要让线程C执行,能想到的一种方式是循环检测,一直检测到线程需要的资源都齐备了就进入临界区。但是循环检测的缺点是要空耗CPU,浪费资源,尤其是在共享资源冲突较大时。而等待通知机制就是一种非常好的解决方案。

等待通知机制是 Java 内置的一个功能,需要配合 synchronized/wait()/notify()/notifyAll() 一起使用。基本思想是:

  1. 在 synchronized 的同步块中,因为只有多线程访问共享变量才需要等待通知机制

  2. 某个线程调用 wait() 进入等待状态,并且线程释放锁

  3. 某个线程在执行到某处调用 notify() or notifyAll(), 通知等待队列的线程进入同步队列,线程状态变为 BLOCKED,等该线程释放锁之后才可以进入竞争锁的环节

  4. 调用 wait() 后再次获得锁,是从 wait() 代码后面开始执行

经典范式

等待通知机制的代码实现有固定的套路:

// wait 的代码范式
synchronized (对象lock) {
    while (条件不满足) {
        对象lock.wait();
    }
    其他处理逻辑
}

// notify 的代码范式
syncronized (对象lock) {
    改变条件
    对象lock.notifyAll();
}

Guarded Suspension 模式

todo

经典问题

互斥锁保护共享资源

从一个转账的例子开始,银行系统中,每个账户都有转账功能,现在我们把这个功能简化到单进程里完成(当然实际情况并非如此),问题就是如何保证很多账户并发转账的安全性,就是不能钱转走了,余额没有变化,不能多转也不能少扣。一个最简的代码模型如下:

public class Account {
    private int balance;

    public synchronized void transfer(Account target, int amt) {
        if (this.balance > amt) {
            this.balance -= amt;
            target.balance += amt;
        }
    }
}

但是不幸的是上面的代码是有问题的,并不能保证安全的转账操作,因为 synchronized 的锁是 this,并不能保护多个 Account 对象里的资源,相当于有多把锁,不能构成互斥行为。

改进1: 保证安全性

public class Account {
    private int balance;
    private Object lock;

    private Account() {}
    public Account(Object lock) {
        this.lock = lock;
    }

    public void transfer(Account target, int amt) {
        synchronized (lock) {
            if (this.balance > amt) {
                this.balance -= amt;
                target.balance += amt;
            }
        }
    }
}

这个方案要求,传入的 lock 是同一个共享对象,否则无效。所以这种做法的缺陷是需要使用代码的地方多加注意,增加了心智负担,用 Account.class 做改进

public class Account {
    private int balance;

    public void transfer(Account target, int amt) {
        synchronized (Account.class) {
            if (this.balance > amt) {
                this.balance -= amt;
                target.balance += amt;
            }
        }
    }
}

这样看起来好多了,但是同样存在问题,这种方式将所有对 Account 的转账操作全部串行化了,大大降低了效率。

改进2:优化效率

为了提升效率,就不能使用 Account.class 这样的粗粒度锁,可以引入细粒度锁,如我们可以使用两个锁:

// 其他代码省略
public void transfer(Account target, int amt) {
    synchronized (this) {  // 1
        synchronized (target) {   // 2
            if (this.balance > amt) {
                this.balance -= amt;
                target.balance += amt;
            }
        }
    }
}

但是,这断代码有问题,容易产生死锁,比如两个线程分别执行: 线程1 A 转账 B 100 和 线程2 B 转账 A 300,线程1 执行到位置 1 获取到 Accoun A 对象的 锁,进行上下文切换,这时线程2 执行,到位置1 获取到 Account B 对象的锁,这样线程1持有了A的锁,线程2持有了B的锁,都相互等待对方的锁,就造成死锁了。

  • 破坏占用且等待的条件来修复 bug

产生死锁的原因之一是线程获得锁后在等待其他锁时,并不释放锁,那可以改成获取不到足够的锁,就把自己占有的锁释放掉

// 让 Allocator 来承担资源的分配方, 保证是单例
public class Allocator {
    private Set<Object> als = new HashSet<>();

    // todo 缺少单例的实现 ...
    private Allocator() {}

    // 申请资源
    public synchronized boolean apply(Object from, Object to) {
        if (als.contains(from) || als.contains(to)) return false;

        als.add(from);
        als.add(to);
        return true;
    }

    // 释放资源
    public synchronzied void free(Object from, Object to) {
        als.remove(from);
        als.remove(to);
    }
}

public class Account {
    private Allocator allocator;
    private int balance;

    public void transfer(Account target, int amt) {
        // 利用循环检测资源是否就绪,一次性申请所有资源
        while (!allocator.apply(this, target) ;

        try {
            synchronized (this) {  // 1
                synchronized (target) {   // 2
                    if (this.balance > amt) {
                        this.balance -= amt;
                        target.balance += amt;
                    }
                }
            }
        } finally {
            allocator.free(this, target);
        }

    }
}

当然,这种方案是可行的,但是也存在缺点,稍后分析。

  • 破坏不可抢占条件修复 bug

产生死锁的另一个原因是获取资源后,其他线程不能强行占有这个资源。那么我们可以考虑释放掉已经占有的资源。synchronized 并发原语并不能支持,因为它一旦申请不到资源就进入了阻塞状态,稍后做分析来找其他解决方案。

  • 破坏循环等待条件修复 bug

产生死锁的另一个原因是循环等待,那么可以为资源进行编号,按序获取资源。

public class Account {
    private int id;
    private int balance;

    public void transfer(Account target, int amt) {
        Account left = this;
        Account right = target;
        if (this.id > target.id) {
            left = target;
            right = this;
        }

        synchronized (left) {
            synchronized (right) {
                if (this.balance > amt) {
                    this.balance -= amt;
                    target.balance += amt;
                }
            }
        }
    }
}

改进3: 继续优化性能

在 破坏占用且等待的条件来修复 bug 那一部分我们用了循环检测资源的就绪状态,但是这个方案并不完美,试想如果并发量大,转账账户重叠多的时候,就会耗费大量的CPU时间,这是空耗,浪费了资源。比较好的方式是等待-通知机制,当没有足够资源时就等待,有足够资源时就收到通知触发执行。

public class Allocator {
    private Set<Object> als = new HashSet<>();

    // todo 缺少单例的实现 ...
    private Allocator() {}

    // 申请资源
    public synchronized boolean apply(Object from, Object to) {
        while (als.contains(from) || als.contains(to)) {
            try {
                wait();
            } catch(Exception e) {}
        }

        als.add(from);
        als.add(to);
    }

    // 释放资源
    public synchronzied void free(Object from, Object to) {
        als.remove(from);
        als.remove(to);
        notifyAll();
    }
}

public class Account {
    private Allocator allocator;
    private int balance;

    public void transfer(Account target, int amt) {
        // 利用等待通知机制,一次性申请所有资源,防止死锁
        allocator.apply(this, target);

        try {
            synchronized (this) {
                synchronized (target) {
                    if (this.balance > amt) {
                        this.balance -= amt;
                        target.balance += amt;
                    }
                }
            }
        } finally {
            // 释放资源
            allocator.free(this, target);
        }

    }
}

利用等待-通知机制,我们很好的解决了死循环检测空耗CPU的问题。

上一页优雅终止线程下一页万能钥匙:管程

最后更新于5年前

这有帮助吗?