开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Spring Boot使用Async实现异步调用:使用Fu

发表于 2018-05-06

之前连续写了几篇关于使用@Async实现异步调用的内容,也得到不少童鞋的反馈,其中问题比较多的就是关于返回Future的使用方法以及对异步执行的超时控制,所以这篇就来一起讲讲这两个问题的处理。

如果您对于@Async注解的使用还不了解的话,可以看看之前的文章,具体如下:

  • 使用@Async实现异步调用
  • 使用@Async实现异步调用:自定义线程池
  • 使用@Async实现异步调用:资源优雅关闭

定义异步任务

首先,我们先使用@Async注解来定义一个异步任务,这个方法返回Future类型,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码@Slf4j
@Component
public class Task {

public static Random random = new Random();

@Async("taskExecutor")
public Future<String> run() throws Exception {
long sleep = random.nextInt(10000);
log.info("开始任务,需耗时:" + sleep + "毫秒");
Thread.sleep(sleep);
log.info("完成任务");
return new AsyncResult<>("test");
}

}

Tips:什么是Future类型?

Future是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果的接口。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

它的接口定义如下:

1
2
3
4
5
6
7
8
复制代码public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}

它声明这样的五个方法:

  • cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
  • isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
  • isDone方法表示任务是否已经完成,若任务完成,则返回true;
  • get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
  • get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。

也就是说Future提供了三种功能:

  1. 判断任务是否完成;
  2. 能够中断任务;
  3. 能够获取任务执行结果。

测试执行与定义超时

在完成了返回Future的异步任务定义之后,我们来尝试实现一个单元测试来使用这个Future完成任务的执行,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ApplicationTests {

@Autowired
private Task task;

@Test
public void test() throws Exception {
Future<String> futureResult = task.run();
String result = futureResult.get(5, TimeUnit.SECONDS);
log.info(result);
}

}

上面的代码中,我们在get方法中还定义了该线程执行的超时时间,通过执行这个测试我们可以观察到执行时间超过5秒的时候,这里会抛出超时异常,该执行线程就能够因执行超时而释放回线程池,不至于一直阻塞而占用资源。

完整示例:

读者可以根据喜好选择下面的两个仓库中查看Chapter4-1-5项目:

  • Github:https://github.com/dyc87112/SpringBoot-Learning/
  • Gitee:https://gitee.com/didispace/SpringBoot-Learning/

如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

后端架构师技术图谱 数据结构 常用算法 并发 操作系统 设计

发表于 2018-05-06

知识共享协议(CC协议) GitHub stars GitHub forks GitHub watchers

最后更新于20180502

  • 数据结构
    • 队列
    • 集合
    • 链表、数组
    • 字典、关联数组
    • 栈
    • 树
      • 二叉树
      • 完全二叉树
      • 平衡二叉树
      • 二叉查找树(BST)
      • 红黑树
      • B-,B+,B*树
      • LSM 树
    • BitSet
  • 常用算法
    • 排序、查找算法
      • 选择排序
      • 冒泡排序
      • 插入排序
      • 快速排序
      • 归并排序
      • 希尔排序
      • 堆排序
      • 计数排序
      • 桶排序
      • 基数排序
      • 二分查找
      • Java 中的排序工具
    • 布隆过滤器
    • 字符串比较
      • KMP 算法
    • 深度优先、广度优先
    • 贪心算法
    • 回溯算法
    • 剪枝算法
    • 动态规划
    • 朴素贝叶斯
    • 推荐算法
    • 最小生成树算法
    • 最短路径算法
  • 并发
    • 多线程
    • 线程安全
    • 一致性、事务
      • 事务 ACID 特性
      • 事务的隔离级别
      • MVCC
    • 锁
      • Java中的锁和同步类
      • 公平锁 & 非公平锁
      • 悲观锁
      • 乐观锁 & CAS
      • ABA 问题
      • CopyOnWrite容器
      • RingBuffer
      • 可重入锁 & 不可重入锁
      • 互斥锁 & 共享锁
      • 死锁
  • 操作系统
    • 计算机原理
    • CPU
      • 多级缓存
    • 进程
    • 线程
    • 协程
    • Linux
  • 设计模式
    • 设计模式的六大原则
    • 23种常见设计模式
    • 应用场景
    • 单例模式
    • 责任链模式
    • MVC
    • IOC
    • AOP
    • UML
    • 微服务思想
      • 康威定律
  • 运维 & 统计 & 技术支持
    • 常规监控
    • APM
    • 统计分析
    • 持续集成(CI/CD)
      • Jenkins
      • 环境分离
    • 自动化运维
      • Ansible
      • puppet
      • chef
    • 测试
      • TDD 理论
      • 单元测试
      • 压力测试
      • 全链路压测
      • A/B 、灰度、蓝绿测试
    • 虚拟化
      • KVM
      • Xen
      • OpenVZ
    • 容器技术
      • Docker
    • 云技术
      • OpenStack
    • DevOps
    • 文档管理
  • 中间件
    • Web Server
      • Nginx
      • OpenResty
      • Apache Httpd
      • Tomcat
        • 架构原理
        • 调优方案
      • Jetty
    • 缓存
      • 本地缓存
    • 客户端缓存
    • 服务端缓存
      • Memcached
      • Redis
        • 架构
        • 回收策略
      • Tair
    • 消息队列
      • 消息总线
      • 消息的顺序
      • RabbitMQ
      • RocketMQ
      • ActiveMQ
      • Kafka
      • Redis 消息推送
      • ZeroMQ
    • 定时调度
      • 单机定时调度
      • 分布式定时调度
    • RPC
      • Dubbo
      • Thrift
      • gRPC
    • 数据库中间件
      • Sharding Jdbc
    • 日志系统
      • 日志搜集
    • 配置中心
    • API 网关
  • 网络
    • 协议
      • OSI 七层协议
      • TCP/IP
      • HTTP
      • HTTP2.0
      • HTTPS
    • 网络模型
      • Epoll
      • Java NIO
      • kqueue
    • 连接和短连接
    • 框架
    • 零拷贝(Zero-copy)
    • 序列化(二进制协议)
      • Hessian
      • Protobuf
  • 数据库
    • 基础理论
      • 数据库设计的三大范式
    • MySQL
      • 原理
      • InnoDB
      • 优化
      • 索引
        • 聚集索引, 非聚集索引
        • 复合索引
        • 自适应哈希索引(AHI)
      • explain
    • NoSQL
      • MongoDB
      • Hbase
  • 搜索引擎
    • 搜索引擎原理
    • Lucene
    • Elasticsearch
    • Solr
    • sphinx
  • 性能
    • 性能优化方法论
    • 容量评估
    • CDN 网络
    • 连接池
    • 性能调优
  • 大数据
    • 流式计算
      • Storm
      • Flink
      • Kafka Stream
      • 应用场景
    • Hadoop
      • HDFS
      • MapReduce
      • Yarn
    • Spark
  • 安全
    • web 安全
      • XSS
      • CSRF
      • SQL 注入
      • Hash Dos
      • 脚本注入
      • 漏洞扫描工具
      • 验证码
    • DDoS 防范
    • 用户隐私信息保护
    • 序列化漏洞
    • 加密解密
      • 对称加密
      • 哈希算法
      • 非对称加密
    • 服务器安全
    • 数据安全
      • 数据备份
    • 网络隔离
      • 内外网分离
      • 登录跳板机
    • 授权、认证
      • RBAC
      • OAuth2.0
      • 双因素认证(2FA)
      • 单点登录(SSO)
  • 常用开源框架
    • 开源协议
    • 日志框架
      • Log4j、Log4j2
      • Logback
    • ORM
    • 网络框架
    • Web 框架
      • Spring 家族
    • 工具框架
  • 分布式设计
    • 扩展性设计
    • 稳定性 & 高可用
      • 硬件负载均衡
      • 软件负载均衡
      • 限流
      • 应用层容灾
      • 跨机房容灾
      • 容灾演练流程
      • 平滑启动
    • 数据库扩展
      • 读写分离模式
      • 分片模式
    • 服务治理
      • 服务注册与发现
      • 服务路由控制
    • 分布式一致
      • CAP 与 BASE 理论
      • 分布式锁
      • 分布式一致性算法
        • PAXOS
        • Zab
        • Raft
        • Gossip
        • 两阶段提交、多阶段提交
      • 幂等
      • 分布式一致方案
      • 分布式 Leader 节点选举
      • TCC(Try/Confirm/Cancel) 柔性事务
    • 分布式文件系统
    • 唯一ID 生成
      • 全局唯一ID
    • 一致性Hash算法
  • 设计思想 & 开发模式
    • DDD(Domain-driven Design - 领域驱动设计)
      • 命令查询职责分离(CQRS)
      • 贫血,充血模型
    • Actor 模式
    • 响应式编程
      • Reactor
      • RxJava
      • Vert.x
    • DODAF2.0
    • Serverless
    • Service Mesh
  • 项目管理
    • 架构评审
    • 重构
    • 代码规范
    • 代码 Review
    • RUP
    • 看板管理
    • SCRUM
    • 敏捷开发
    • 极限编程(XP)
    • 结对编程
    • FMEA管理模式
  • 通用业务术语
  • 技术趋势
  • 政策、法规
    • 法律
      • 严格遵守刑法253法条
  • 架构师素质
  • 团队管理
    • 招聘
  • 资讯
    • 行业资讯
    • 公众号列表
    • 博客
      • 团队博客
      • 个人博客
    • 综合门户、社区
    • 问答、讨论类社区
    • 行业数据分析
    • 专项网站
    • 其他类
    • 推荐参考书
      • 在线电子书
      • 纸质书
        • 开发方面
        • 架构方面
        • 技术管理方面
        • 基础理论
        • 工具方面
        • 大数据方面
  • 技术资源
    • 开源资源
    • 手册、文档、教程
    • 在线课堂
    • 会议、活动
    • 常用APP
    • 找工作
    • 工具
    • 代码托管
    • 文件服务
    • 综合云服务商

(Toc generated by simple-php-github-toc )

数据结构

队列

  • 《java队列——queue详细分析》
+ 非阻塞队列:ConcurrentLinkedQueue(无界线程安全),采用CAS机制(compareAndSwapObject原子操作)。
+ 阻塞队列:ArrayBlockingQueue(有界)、LinkedBlockingQueue(无界)、DelayQueue、PriorityBlockingQueue,采用锁机制;使用 ReentrantLock 锁。
  • 《LinkedList、ConcurrentLinkedQueue、LinkedBlockingQueue对比分析》

集合

  • 《Java Set集合的详解》

链表、数组

  • 《Java集合详解–什么是List》

字典、关联数组

  • 《Java map 详解 - 用法、遍历、排序、常用API等》

栈

  • 《java数据结构与算法之栈(Stack)设计与实现》
  • 《Java Stack 类》
  • 《java stack的详细实现分析》
    • Stack 是线程安全的。
    • 内部使用数组保存数据,不够时翻倍。

树

二叉树

每个节点最多有两个叶子节点。

  • 《二叉树》

完全二叉树

  • 《完全二叉树》
    • 叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。

平衡二叉树

左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

  • 《浅谈数据结构-平衡二叉树》
  • 《浅谈算法和数据结构: 八 平衡查找树之2-3树》

二叉查找树(BST)

二叉查找树(Binary Search Tree),也称有序二叉树(ordered binary tree),排序二叉树(sorted binary tree)。

  • 《浅谈算法和数据结构: 七 二叉查找树》

红黑树

  • 《最容易懂得红黑树》
    • 添加阶段后,左旋或者右旋从而再次达到平衡。
  • 《浅谈算法和数据结构: 九 平衡查找树之红黑树》

B-,B+,B*树

MySQL是基于B+树聚集索引组织表

  • 《B-树,B+树,B*树详解》
  • 《B-树,B+树与B*树的优缺点比较》
    • B+ 树的叶子节点链表结构相比于 B- 树便于扫库,和范围检索。

LSM 树

LSM(Log-Structured Merge-Trees)和 B+ 树相比,是牺牲了部分读的性能来换取写的性能(通过批量写入),实现读写之间的。 Hbase、LevelDB、Tair(Long DB)、nessDB 采用 LSM 树的结构。LSM可以快速建立索引。

  • 《LSM树 VS B+树》
+ B+ 树读性能好,但由于需要有序结构,当key比较分散时,磁盘寻道频繁,造成写性能。
+ LSM 是将一个大树拆分成N棵小树,先写到内存(无寻道问题,性能高),在内存中构建一颗有序小树(有序树),随着小树越来越大,内存的小树会flush到磁盘上。当读时,由于不知道数据在哪棵小树上,因此必须遍历(二分查找)所有的小树,但在每颗小树内部数据是有序的。
  • 《LSM树(Log-Structured Merge Tree)存储引擎》
+ 极端的说,基于LSM树实现的HBase的写性能比MySQL高了一个数量级,读性能低了一个数量级。
+ 优化方式:Bloom filter 替代二分查找;compact 小数位大树,提高查询性能。
+ Hbase 中,内存中达到一定阈值后,整体flush到磁盘上、形成一个文件(B+数),HDFS不支持update操作,所以Hbase做整体flush而不是merge update。flush到磁盘上的小树,定期会合并成一个大树。

BitSet

经常用于大规模数据的排重检查。

  • 《Java Bitset类》
  • 《Java BitSet(位集)》

常用算法

  • 《常见排序算法及对应的时间复杂度和空间复杂度》

排序、查找算法

  • 《常见排序算法及对应的时间复杂度和空间复杂度》

选择排序

  • 《Java中的经典算法之选择排序(SelectionSort)》
    • 每一趟从待排序的记录中选出最小的元素,顺序放在已排好序的序列最后,直到全部记录排序完毕。

冒泡排序

  • 《冒泡排序的2种写法》
    • 相邻元素前后交换、把最大的排到最后。
    • 时间复杂度 O(n²)

插入排序

  • 《排序算法总结之插入排序》

快速排序

  • 《坐在马桶上看算法:快速排序》
    • 一侧比另外一次都大或小。

归并排序

  • 《图解排序算法(四)之归并排序》
    • 分而治之,分成小份排序,在合并(重建一个新空间进行复制)。

希尔排序

TODO

堆排序

  • 《图解排序算法(三)之堆排序》
    • 排序过程就是构建最大堆的过程,最大堆:每个结点的值都大于或等于其左右孩子结点的值,堆顶元素是最大值。

计数排序

  • 《计数排序和桶排序》
    • 和桶排序过程比较像,差别在于桶的数量。

桶排序

  • 《【啊哈!算法】最快最简单的排序——桶排序》
  • 《排序算法(三):计数排序与桶排序》
    • 桶排序将[0,1)区间划分为n个相同的大小的子区间,这些子区间被称为桶。
    • 每个通单独进行排序,然后再遍历每个桶。

基数排序

按照个位、十位、百位、…依次来排。

  • 《排序算法系列:基数排序》
  • 《基数排序》

二分查找

  • 《二分查找(java实现)》
+ 要求待查找的序列有序。
+ 时间复杂度 O(logN)。
  • 《java实现二分查找-两种方式》
+ while + 递归。

Java 中的排序工具

  • 《Arrays.sort和Collections.sort实现原理解析》
    • Collections.sort算法调用的是合并排序。
    • Arrays.sort() 采用了2种排序算法 – 基本类型数据使用快速排序法,对象数组使用归并排序。

布隆过滤器

常用于大数据的排重,比如email,url 等。 核心原理:将每条数据通过计算产生一个指纹(一个字节或多个字节,但一定比原始数据要少很多),其中每一位都是通过随机计算获得,在将指纹映射到一个大的按位存储的空间中。注意:会有一定的错误率。 优点:空间和时间效率都很高。 缺点:随着存入的元素数量增加,误算率随之增加。

  • 《布隆过滤器 – 空间效率很高的数据结构》
  • 《大量数据去重:Bitmap和布隆过滤器(Bloom Filter)》
  • 《基于Redis的布隆过滤器的实现》
    • 基于 Redis 的 Bitmap 数据结构。
  • 《网络爬虫:URL去重策略之布隆过滤器(BloomFilter)的使用》
    • 使用Java中的 BitSet 类 和 加权和hash算法。

字符串比较

KMP 算法

KMP:Knuth-Morris-Pratt算法(简称KMP) 核心原理是利用一个“部分匹配表”,跳过已经匹配过的元素。

  • 《字符串匹配的KMP算法》

深度优先、广度优先

  • 《广度优先搜索BFS和深度优先搜索DFS》

贪心算法

  • 《算法:贪婪算法基础》
  • 《常见算法及问题场景——贪心算法》

回溯算法

  • 《 五大常用算法之四:回溯法》

剪枝算法

  • 《α-β剪枝算法》

动态规划

  • 《详解动态规划——邹博讲动态规划》
  • 《动态规划算法的个人理解》

朴素贝叶斯

  • 《带你搞懂朴素贝叶斯分类算法》
+ P(B|A)=P(A|B)P(B)/P(A)
  • 《贝叶斯推断及其互联网应用1》
  • 《贝叶斯推断及其互联网应用2》

推荐算法

  • 《推荐算法综述》
  • 《TOP 10 开源的推荐系统简介》

最小生成树算法

  • 《算法导论–最小生成树(Kruskal和Prim算法)》

最短路径算法

  • 《Dijkstra算法详解》

并发

多线程

  • 《40个Java多线程问题总结》

线程安全

  • 《Java并发编程——线程安全及解决机制简介》

一致性、事务

事务 ACID 特性

  • 《数据库事务ACID特性》

事务的隔离级别

  • 未提交读:一个事务可以读取另一个未提交的数据,容易出现脏读的情况。
  • 读提交:一个事务等另外一个事务提交之后才可以读取数据,但会出现不可重复读的情况(多次读取的数据不一致),读取过程中出现UPDATE操作,会多。(大多数数据库默认级别是RC,比如SQL Server,Oracle),读取的时候不可以修改。
  • 可重复读: 同一个事务里确保每次读取的时候,获得的是同样的数据,但不保障原始数据被其他事务更新(幻读),Mysql InnoDB 就是这个级别。
  • 序列化:所有事物串行处理(牺牲了效率)
  • 《理解事务的4种隔离级别》
  • 数据库事务的四大特性及事务隔离级别
  • 《MySQL的InnoDB的幻读问题 》
+ 幻读的例子非常清楚。
+ 通过 SELECT ... FOR UPDATE 解决。
  • 《一篇文章带你读懂MySQL和InnoDB》
+ 图解脏读、不可重复读、幻读问题。

MVCC

  • 《【mysql】关于innodb中MVCC的一些理解》
+ innodb 中 MVCC 用在 Repeatable-Read 隔离级别。
+ MVCC 会产生幻读问题(更新时异常。)
  • 《轻松理解MYSQL MVCC 实现机制》
+ 通过隐藏版本列来实现 MVCC 控制,一列记录创建时间、一列记录删除时间,这里的时间
+ 每次只操作比当前版本小(或等于)的 行。

锁

Java中的锁和同步类

  • 《Java中的锁分类》
+ 主要包括 synchronized、ReentrantLock、和 ReadWriteLock。
  • 《Java并发之AQS详解》
  • 《Java中信号量 Semaphore》
+ 有数量控制
+ 申请用 acquire,申请不要则阻塞;释放用 release。
  • 《java开发中的Mutex vs Semaphore》
+ 简单的说 就是Mutex是排它的,只有一个可以获取到资源, Semaphore也具有排它性,但可以定义多个可以获取的资源的对象。

公平锁 & 非公平锁

公平锁的作用就是严格按照线程启动的顺序来执行的,不允许其他线程插队执行的;而非公平锁是允许插队的。

  • 《公平锁与非公平锁》
    • 默认情况下 ReentrantLock 和 synchronized 都是非公平锁。ReentrantLock 可以设置成公平锁。

悲观锁

悲观锁如果使用不当(锁的条数过多),会引起服务大面积等待。推荐优先使用乐观锁+重试。

  • 《【MySQL】悲观锁&乐观锁》
+ 乐观锁的方式:版本号+重试方式
+ 悲观锁:通过 select ... for update 进行行锁(不可读、不可写,share 锁可读不可写)。
  • 《Mysql查询语句使用select.. for update导致的数据库死锁分析》
+ mysql的innodb存储引擎实务锁虽然是锁行,但它内部是锁索引的。
+ 锁相同数据的不同索引条件可能会引起死锁。
  • 《Mysql并发时经典常见的死锁原因及解决方法》

乐观锁 & CAS

  • 《乐观锁的一种实现方式——CAS》
    • 和MySQL乐观锁方式相似,只不过是通过和原值进行比较。

ABA 问题

由于高并发,在CAS下,更新后可能此A非彼A。通过版本号可以解决,类似于上文Mysql 中提到的的乐观锁。

  • 《Java CAS 和ABA问题》
  • 《Java 中 ABA问题及避免》
    • AtomicStampedReference 和 AtomicStampedReference。

CopyOnWrite容器

可以对CopyOnWrite容器进行并发的读,而不需要加锁。CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,不适合需要数据强一致性的场景。

  • 《JAVA中写时复制(Copy-On-Write)Map实现》
+ 实现读写分离,读取发生在原始数据上,写入发生在副本上。
+ 不用加锁,通过最终一致实现一致性。
  • 《聊聊并发-Java中的Copy-On-Write容器》

RingBuffer

  • 《线程安全的无锁RingBuffer的实现【一个读线程,一个写线程】》

可重入锁 & 不可重入锁

  • 《可重入锁和不可重入锁》
+ 通过简单代码举例说明可重入锁和不可重入锁。
+ 可重入锁指同一个线程可以再次获得之前已经获得的锁。
+ 可重入锁可以用户避免死锁。
+ Java中的可重入锁:synchronized 和 java.util.concurrent.locks.ReentrantLock
  • 《ReenTrantLock可重入锁(和synchronized的区别)总结》
+ synchronized 使用方便,编译器来加锁,是非公平锁。
+ ReenTrantLock 使用灵活,锁的公平性可以定制。
+ 相同加锁场景下,推荐使用 synchronized。

互斥锁 & 共享锁

互斥锁:同时只能有一个线程获得锁。比如,ReentrantLock 是互斥锁,ReadWriteLock 中的写锁是互斥锁。 共享锁:可以有多个线程同时或的锁。比如,Semaphore、CountDownLatch 是共享锁,ReadWriteLock 中的读锁是共享锁。

  • 《ReadWriteLock场景应用》

死锁

  • 《“死锁”四个必要条件的合理解释》
+ 互斥、持有、不可剥夺、不可剥夺。
  • Java如何查看死锁?
+ JConsole 可以识别死锁。
  • java多线程系列:死锁及检测
+ jstack 可以显示死锁。

操作系统

计算机原理

  • 《操作系统基础知识——操作系统的原理,类型和结构》

CPU

多级缓存

典型的 CPU 有三级缓存,距离核心越近,速度越快,空间越小。L1 一般 32k,L2 一般 256k,L3 一般12M。内存速度需要200个 CPU 周期,CPU 缓存需要1个CPU周期。

  • 《从Java视角理解CPU缓存和伪共享》

进程

TODO

线程

  • 《线程的生命周期及状态转换详解》

协程

  • 《终结python协程—-从yield到actor模型的实现》
    • 线程的调度是由操作系统负责,协程调度是程序自行负责
    • 与线程相比,协程减少了无谓的操作系统切换.
    • 实际上当遇到IO操作时做切换才更有意义,(因为IO操作不用占用CPU),如果没遇到IO操作,按照时间片切换.

Linux

  • 《Linux 命令大全》

设计模式

设计模式的六大原则

  • 《设计模式的六大原则》
    • 开闭原则:对扩展开放,对修改关闭,多使用抽象类和接口。
    • 里氏代换原则:基类可以被子类替换,使用抽象类继承,不使用具体类继承。
    • 依赖倒转原则:要依赖于抽象,不要依赖于具体,针对接口编程,不针对实现编程。
    • 接口隔离原则:使用多个隔离的接口,比使用单个接口好,建立最小的接口。
    • 迪米特法则:一个软件实体应当尽可能少地与其他实体发生相互作用,通过中间类建立联系。
    • 合成复用原则:尽量使用合成/聚合,而不是使用继承。

23种常见设计模式

  • 《设计模式》
  • 《23种设计模式全解析》

应用场景

  • 《细数JDK里的设计模式》
+ 结构型模式:


    - 适配器:用来把一个接口转化成另一个接口,如 java.util.Arrays#asList()。
    - 桥接模式:这个模式将抽象和抽象操作的实现进行了解耦,这样使得抽象和实现可以独立地变化,如JDBC;
    - 组合模式:使得客户端看来单个对象和对象的组合是同等的。换句话说,某个类型的方法同时也接受自身类型作为参数,如 Map.putAll,List.addAll、Set.addAll。
    - 装饰者模式:动态的给一个对象附加额外的功能,这也是子类的一种替代方式,如 java.util.Collections#checkedList|Map|Set|SortedSet|SortedMap。
    - 享元模式:使用缓存来加速大量小对象的访问时间,如 valueOf(int)。
    - 代理模式:代理模式是用一个简单的对象来代替一个复杂的或者创建耗时的对象,如 java.lang.reflect.Proxy
+ 创建模式:


    - 抽象工厂模式:抽象工厂模式提供了一个协议来生成一系列的相关或者独立的对象,而不用指定具体对象的类型,如 java.util.Calendar#getInstance()。
    - 建造模式(Builder):定义了一个新的类来构建另一个类的实例,以简化复杂对象的创建,如:java.lang.StringBuilder#append()。
    - 工厂方法:就是 **一个返**\* 回具体对象的方法,而不是多个,如 java.lang.Object#toString()、java.lang.Class#newInstance()。
    - 原型模式:使得类的实例能够生成自身的拷贝、如:java.lang.Object#clone()。
    - 单例模式:全局只有一个实例,如 java.lang.Runtime#getRuntime()。
+ 行为模式:


    - 责任链模式:通过把请求从一个对象传递到链条中下一个对象的方式,直到请求被处理完毕,以实现对象间的解耦。如 javax.servlet.Filter#doFilter()。
    - 命令模式:将操作封装到对象内,以便存储,传递和返回,如:java.lang.Runnable。
    - 解释器模式:定义了一个语言的语法,然后解析相应语法的语句,如,java.text.Format,java.text.Normalizer。
    - 迭代器模式:提供一个一致的方法来顺序访问集合中的对象,如 java.util.Iterator。
    - 中介者模式:通过使用一个中间对象来进行消息分发以及减少类之间的直接依赖,java.lang.reflect.Method#invoke()。
    - 空对象模式:如 java.util.Collections#emptyList()。
    - 观察者模式:它使得一个对象可以灵活的将消息发送给感兴趣的对象,如 java.util.EventListener。
    - 模板方法模式:让子类可以重写方法的一部分,而不是整个重写,如 java.util.Collections#sort()。
  • 《Spring-涉及到的设计模式汇总》
  • 《Mybatis使用的设计模式》

单例模式

  • 《单例模式的三种实现 以及各自的优缺点》
  • 《单例模式--反射--防止序列化破坏单例模式》
    • 使用枚举类型。

责任链模式

TODO

MVC

  • 《MVC 模式》
    • 模型(model)-视图(view)-控制器(controller)

IOC

  • 《理解 IOC》
  • 《IOC 的理解与解释》
    • 正向控制:传统通过new的方式。反向控制,通过容器注入对象。
    • 作用:用于模块解耦。
    • DI:Dependency Injection,即依赖注入,只关心资源使用,不关心资源来源。

AOP

  • 《轻松理解AOP(面向切面编程)》
  • 《Spring AOP详解》
  • 《Spring AOP的实现原理》
    • Spring AOP使用的动态代理,主要有两种方式:JDK动态代理和CGLIB动态代理。
  • 《Spring AOP 实现原理与 CGLIB 应用》
    • Spring AOP 框架对 AOP 代理类的处理原则是:如果目标对象的实现类实现了接口,Spring AOP 将会采用 JDK 动态代理来生成 AOP 代理类;如果目标对象的实现类没有实现接口,Spring AOP 将会采用 CGLIB 来生成 AOP 代理类

UML

  • 《UML教程》

微服务思想

  • 《微服务架构设计》
  • 《微服务架构技术栈选型手册》

康威定律

  • 《微服务架构的理论基础 - 康威定律》
+ 定律一:组织沟通方式会通过系统设计表达出来,就是说架构的布局和组织结构会有相似。
+ 定律二:时间再多一件事情也不可能做的完美,但总有时间做完一件事情。一口气吃不成胖子,先搞定能搞定的。
+ 定律三:线型系统和线型组织架构间有潜在的异质同态特性。种瓜得瓜,做独立自治的子系统减少沟通成本。
+ 定律四:大的系统组织总是比小系统更倾向于分解。合久必分,分而治之。
  • 《微服务架构核⼼20讲》

运维 & 统计 & 技术支持

常规监控

  • 《腾讯业务系统监控的修炼之路》
+ 监控的方式:主动、被动、旁路(比如舆情监控)
+ 监控类型: 基础监控、服务端监控、客户端监控、 监控、用户端监控
+ 监控的目标:全、块、准
+ 核心指标:请求量、成功率、耗时
  • 《开源还是商用?十大云运维监控工具横评》
+ Zabbix、Nagios、Ganglia、Zenoss、Open-falcon、监控宝、 360网站服务监控、阿里云监控、百度云观测、小蜜蜂网站监测等。
  • 《监控报警系统搭建及二次开发经验》

命令行监控工具

  • 《常用命令行监控工具》
+ top、sar、tsar、nload
  • 《20个命令行工具监控 Linux 系统性能》
  • 《JVM性能调优监控工具jps、jstack、jmap、jhat、jstat、hprof使用详解》

APM

APM — Application Performance Management

  • 《Dapper,大规模分布式系统的跟踪系统》
  • CNCF OpenTracing,中文版
  • 主要开源软件,按字母排序
+ [Apache SkyWalking](https://github.com/apache/incubator-skywalking)
+ [CAT](https://github.com/dianping/cat)
+ [CNCF jaeger](https://github.com/jaegertracing/jaeger)
+ [Pinpoint](https://github.com/naver/pinpoint)
+ [Zipkin](https://github.com/openzipkin/zipkin)
  • 《开源APM技术选型与实战》
+ 主要基于 Google的Dapper(大规模分布式系统的跟踪系统) 思想。

统计分析

  • 《流量统计的基础:埋点》
+ 常用指标:访问与访客、停留时长、跳出率、退出率、转化率、参与度
  • 《APP埋点常用的统计工具、埋点目标和埋点内容》
+ 第三方统计:友盟、百度移动、魔方、App Annie、talking data、神策数据等。
  • 《美团点评前端无痕埋点实践》
+ 所谓无痕、即通过可视化工具配置采集节点,在前端自动解析配置并上报埋点数据,而非硬编码。

持续集成(CI/CD)

  • 《持续集成是什么?》
  • 《8个流行的持续集成工具》

Jenkins

  • 《使用Jenkins进行持续集成》

环境分离

开发、测试、生成环境分离。

  • 《开发环境、生产环境、测试环境的基本理解和区》

自动化运维

Ansible

  • 《Ansible中文权威指南》
  • 《Ansible基础配置和企业级项目实用案例》

puppet

  • 《自动化运维工具——puppet详解》

chef

  • 《Chef 的安装与使用》

测试

TDD 理论

  • 《深度解读 - TDD(测试驱动开发)》
    • 基于测试用例编码功能代码,XP(Extreme Programming)的核心实践.
    • 好处:一次关注一个点,降低思维负担;迎接需求变化或改善代码的设计;提前澄清需求;快速反馈;

单元测试

  • 《Java单元测试之JUnit篇》
  • 《JUnit 4 与 TestNG 对比》
    • TestNG 覆盖 JUnit 功能,适用于更复杂的场景。
  • 《单元测试主要的测试功能点》
    • 模块接口测试、局部数据结构测试、路径测试 、错误处理测试、边界条件测试 。

压力测试

  • 《Apache ab 测试使用指南》
  • 《大型网站压力测试及优化方案》
  • 《10大主流压力/负载/性能测试工具推荐》
  • 《真实流量压测工具 tcpcopy应用浅析》
  • 《nGrinder 简易使用教程》

全链路压测

  • 《京东618:升级全链路压测方案,打造军演机器人ForceBot》
  • 《饿了么全链路压测的探索与实践》
  • 《四大语言,八大框架|滴滴全链路压测解决之道》
  • 《全链路压测经验》

A/B 、灰度、蓝绿测试

  • 《技术干货 | AB 测试和灰度发布探索及实践》
  • 《nginx 根据IP 进行灰度发布》
  • 《蓝绿部署、A/B 测试以及灰度发布》

虚拟化

  • 《VPS的三种虚拟技术OpenVZ、Xen、KVM优缺点比较》

KVM

  • 《KVM详解,太详细太深入了,经典》
  • 《【图文】KVM 虚拟机安装详解》

Xen

  • 《Xen虚拟化基本原理详解》

OpenVZ

  • 《开源Linux容器 OpenVZ 快速上手指南》

容器技术

Docker

  • 《几张图帮你理解 docker 基本原理及快速入门》
  • 《Docker 核心技术与实现原理》
  • 《Docker 教程》

云技术

OpenStack

  • 《OpenStack构架知识梳理》

DevOps

  • 《一分钟告诉你究竟DevOps是什么鬼?》
  • 《DevOps详解》

文档管理

  • Confluence-收费文档管理系统
  • GitLab?
  • Wiki

中间件

Web Server

Nginx

  • 《Ngnix的基本学习-多进程和Apache的比较》
+ Nginx 通过异步非阻塞的事件处理机制实现高并发。Apache 每个请求独占一个线程,非常消耗系统资源。
+ 事件驱动适合于IO密集型服务(Nginx),多进程或线程适合于CPU密集型服务(Apache),所以Nginx适合做反向代理,而非web服务器使用。
  • 《nginx与Apache的对比以及优缺点》
+ nginx只适合静态和反向代理,不适合处理动态请求。

OpenResty

  • 官方网站
  • 《浅谈 OpenResty》
    • 通过 Lua 模块可以在Nginx上进行开发。

Apache Httpd

  • 官方网站

Tomcat

架构原理

  • 《TOMCAT原理详解及请求过程》
  • 《Tomcat服务器原理详解》
  • 《Tomcat 系统架构与设计模式,第 1 部分: 工作原理》
  • 《四张图带你了解Tomcat系统架构》
  • 《JBoss vs. Tomcat: Choosing A Java Application Server》
+ Tomcat 是轻量级的 Serverlet 容器,没有实现全部 JEE 特性(比如持久化和事务处理),但可以通过其他组件代替,比如Srping。
+ Jboss 实现全部了JEE特性,软件开源免费、文档收费。

调优方案

  • 《Tomcat 调优方案》
+ 启动NIO模式(或者APR);调整线程池;禁用AJP连接器(Nginx+tomcat的架构,不需要AJP);
  • 《tomcat http协议与ajp协议》
  • 《AJP与HTTP比较和分析》
+ AJP 协议(8009端口)用于降低和前端Server(如Apache,而且需要支持AJP协议)的连接数(前端),通过长连接提高性能。
+ 并发高时,AJP协议优于HTTP协议。

Jetty

  • 《Jetty 的工作原理以及与 Tomcat 的比较》
  • 《jetty和tomcat优势比较》
    • 架构比较:Jetty的架构比Tomcat的更为简单。
    • 性能比较:Jetty和Tomcat性能方面差异不大,Jetty默认采用NIO结束在处理I/O请求上更占优势,Tomcat默认采用BIO处理I/O请求,Tomcat适合处理少数非常繁忙的链接,处理静态资源时性能较差。
    • 其他方面:Jetty的应用更加快速,修改简单,对新的Servlet规范的支持较好;Tomcat 对JEE和Servlet 支持更加全面。

缓存

  • 《缓存失效策略(FIFO 、LRU、LFU三种算法的区别)》

本地缓存

  • 《HashMap本地缓存》
  • 《EhCache本地缓存》
+ 堆内、堆外、磁盘三级缓存。
+ 可按照缓存空间容量进行设置。
+ 按照时间、次数等过期策略。
  • 《Guava Cache》
+ 简单轻量、无堆外、磁盘缓存。
  • 《Nginx本地缓存》
  • 《Pagespeed—懒人工具,服务器端加速》

客户端缓存

  • 《浏览器端缓存》
+ 主要是利用 Cache-Control 参数。
  • 《H5 和移动端 WebView 缓存机制解析与实战》

服务端缓存

Memcached

  • 《Memcached 教程》
  • 《深入理解Memcached原理》
+ 采用多路复用技术提高并发性。
+ slab分配算法: memcached给Slab分配内存空间,默认是1MB。分配给Slab之后 把slab的切分成大小相同的chunk,Chunk是用于缓存记录的内存空间,Chunk 的大小默认按照1.25倍的速度递增。好处是不会频繁申请内存,提高IO效率,坏处是会有一定的内存浪费。
  • 《Memcached软件工作原理》
  • 《Memcache技术分享:介绍、使用、存储、算法、优化、命中率》
  • 《memcache 中 add 、 set 、replace 的区别》
+ 区别在于当key存在还是不存在时,返回值是true和false的。
  • 《memcached全面剖析》

Redis

  • 《Redis 教程》
  • 《redis底层原理》
+ 使用 ziplist 存储链表,ziplist是一种压缩链表,它的好处是更能节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。
+ 使用 skiplist(跳跃表)来存储有序集合对象、查找上先从高Level查起、时间复杂度和红黑树相当,实现容易,无锁、并发性好。
  • 《Redis持久化方式》
+ RDB方式:定期备份快照,常用于灾难恢复。优点:通过fork出的进程进行备份,不影响主进程、RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。缺点:会丢数据。
+ AOF方式:保存操作日志方式。优点:恢复时数据丢失少,缺点:文件大,回复慢。
+ 也可以两者结合使用。
  • 《分布式缓存–序列3–原子操作与CAS乐观锁》

架构

  • 《Redis单线程架构》

回收策略

  • 《redis的回收策略》

Tair

  • 官方网站
  • 《Tair和Redis的对比》
  • 特点:可以配置备份节点数目,通过异步同步到备份节点
  • 一致性Hash算法。
  • 架构:和Hadoop 的设计思想类似,有Configserver,DataServer,Configserver 通过心跳来检测,Configserver也有主备关系。

几种存储引擎:

  • MDB,完全内存性,可以用来存储Session等数据。
  • Rdb(类似于Redis),轻量化,去除了aof之类的操作,支持Restfull操作
  • LDB(LevelDB存储引擎),持久化存储,LDB 作为rdb的持久化,google实现,比较高效,理论基础是LSM(Log-Structured-Merge Tree)算法,现在内存中修改数据,达到一定量时(和内存汇总的旧数据一同写入磁盘)再写入磁盘,存储更加高效,县比喻Hash算法。
  • Tair采用共享内存来存储数据,如果服务挂掉(非服务器),重启服务之后,数据亦然还在。

消息队列

  • 《消息队列-推/拉模式学习 & ActiveMQ及JMS学习》
+ RabbitMQ 消费者默认是推模式(也支持拉模式)。
+ Kafka 默认是拉模式。
+ Push方式:优点是可以尽可能快地将消息发送给消费者,缺点是如果消费者处理能力跟不上,消费者的缓冲区可能会溢出。
+ Pull方式:优点是消费端可以按处理能力进行拉去,缺点是会增加消息延迟。
  • 《Kafka、RabbitMQ、RocketMQ等消息中间件的对比 —— 消息发送性能和区别》

消息总线

消息总线相当于在消息队列之上做了一层封装,统一入口,统一管控、简化接入成本。

  • 《消息总线VS消息队列》

消息的顺序

  • 《如何保证消费者接收消息的顺序》

RabbitMQ

支持事务,推拉模式都是支持、适合需要可靠性消息传输的场景。

  • 《RabbitMQ的应用场景以及基本原理介绍》
  • 《消息队列之 RabbitMQ》
  • 《RabbitMQ之消息确认机制(事务+Confirm)》

RocketMQ

Java实现,推拉模式都是支持,吞吐量逊于Kafka。可以保证消息顺序。

  • 《RocketMQ 实战之快速入门》

ActiveMQ

纯Java实现,兼容JMS,可以内嵌于Java应用中。

  • 《ActiveMQ消息队列介绍》

Kafka

高吞吐量、采用拉模式。适合高IO场景,比如日志同步。

  • 官方网站
  • 《各消息队列对比,Kafka深度解析,众人推荐,精彩好文!》
  • 《Kafka分区机制介绍与示例》

Redis 消息推送

生产者、消费者模式完全是客户端行为,list 和 拉模式实现,阻塞等待采用 blpop 指令。

  • 《Redis学习笔记之十:Redis用作消息队列》

ZeroMQ

TODO

定时调度

单机定时调度

  • 《linux定时任务cron配置》
  • 《Linux cron运行原理》
+ fork 进程 + sleep 轮询
  • 《Quartz使用总结》
  • 《Quartz源码解析 —- 触发器按时启动原理》
  • 《quartz原理揭秘和源码解读》
+ 定时调度在 QuartzSchedulerThread 代码中,while()无限循环,每次循环取出时间将到的trigger,触发对应的job,直到调度器线程被关闭。

分布式定时调度

  • 《这些优秀的国产分布式任务调度系统,你用过几个?》
+ opencron、LTS、XXL-JOB、Elastic-Job、Uncode-Schedule、Antares
  • 《Quartz任务调度的基本实现原理》
+ Quartz集群中,独立的Quartz节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一Quartz应用的

RPC

  • 《从零开始实现RPC框架 - RPC原理及实现》
+ 核心角色:Server: 暴露服务的服务提供方、Client: 调用远程服务的服务消费方、Registry: 服务注册与发现的注册中心。
  • 《分布式RPC框架性能大比拼 dubbo、motan、rpcx、gRPC、thrift的性能比较》

Dubbo

  • 官方网站
  • dubbo实现原理简单介绍

** SPI ** TODO

Thrift

  • 官方网站
  • 《Thrift RPC详解》
    • 支持多语言,通过中间语言定义接口。

gRPC

服务端可以认证加密,在外网环境下,可以保证数据安全。

  • 官方网站
  • 《你应该知道的RPC原理》

数据库中间件

Sharding Jdbc

  • 官网

日志系统

日志搜集

  • 《从零开始搭建一个ELKB日志收集系统》
  • 《用ELK搭建简单的日志收集分析系统》
  • 《日志收集系统-探究》

配置中心

  • Apollo - 携程开源的配置中心应用
+ Spring Boot 和 Spring Cloud
+ 支持推、拉模式更新配置
+ 支持多种语言
  • 《基于zookeeper实现统一配置管理》
  • 《 Spring Cloud Config 分布式配置中心使用教程》

servlet 3.0 异步特性可用于配置中心的客户端

  • 《servlet3.0 新特性——异步处理》

API 网关

主要职责:请求转发、安全认证、协议转换、容灾。

  • 《API网关那些儿》
  • 《谈API网关的背景、架构以及落地方案》
  • 《使用Zuul构建API Gateway》
  • 《HTTP API网关选择之一Kong介绍》

网络

协议

OSI 七层协议

  • 《OSI七层协议模型、TCP/IP四层模型学习笔记》

TCP/IP

  • 《深入浅出 TCP/IP 协议》
  • 《TCP协议中的三次握手和四次挥手》

HTTP

  • 《http协议详解(超详细)》

HTTP2.0

  • 《HTTP 2.0 原理详细分析》
  • 《HTTP2.0的基本单位为二进制帧》
    • 利用二进制帧负责传输。
    • 多路复用。

HTTPS

  • 《https原理通俗了解》
+ 使用非对称加密协商加密算法
+ 使用对称加密方式传输数据
+ 使用第三方机构签发的证书,来加密公钥,用于公钥的安全传输、防止被中间人串改。
  • 《八大免费SSL证书-给你的网站免费添加Https安全加密》

网络模型

  • 《web优化必须了解的原理之I/o的五种模型和web的三种工作模式》
+ 五种I/O模型:阻塞I/O,非阻塞I/O,I/O复用、事件(信号)驱动I/O、异步I/O,前四种I/O属于同步操作,I/O的第一阶段不同、第二阶段相同,最后的一种则属于异步操作。
+ 三种 Web Server 工作方式:Prefork(多进程)、Worker方式(线程方式)、Event方式。
  • 《select、poll、epoll之间的区别总结》
+ select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
+ select 有打开文件描述符数量限制,默认1024(2048 for x64),100万并发,就要用1000个进程、切换开销大;poll采用链表结构,没有数量限制。
+ select,poll “醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,通过回调机制节省大量CPU时间;select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,而epoll只要一次拷贝。
+ poll会随着并发增加,性能逐渐下降,epoll采用红黑树结构,性能稳定,不会随着连接数增加而降低。
  • 《select,poll,epoll比较 》
+ 在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
  • 《深入理解Java NIO》
+ NIO 是一种同步非阻塞的 IO 模型。同步是指线程不断轮询 IO 事件是否就绪,非阻塞是指线程在等待 IO 的时候,可以同时做其他任务
  • 《BIO与NIO、AIO的区别》
  • 《两种高效的服务器设计模型:Reactor和Proactor模型》

Epoll

  • 《epoll使用详解(精髓)》

Java NIO

  • 《深入理解Java NIO》
  • 《Java NIO编写Socket服务器的一个例子》

kqueue

  • 《kqueue用法简介》

连接和短连接

  • 《TCP/IP系列——长连接与短连接的区别》

框架

  • 《Netty原理剖析》
    • Reactor 模式介绍。
    • Netty 是 Reactor 模式的一种实现。

零拷贝(Zero-copy)

  • 《对于 Netty ByteBuf 的零拷贝(Zero Copy) 的理解》
    • 多个物理分离的buffer,通过逻辑上合并成为一个,从而避免了数据在内存之间的拷贝。

序列化(二进制协议)

Hessian

  • 《Hessian原理分析》 Binary-RPC;不仅仅是序列化

Protobuf

  • 《Protobuf协议的Java应用例子》 Goolge出品、占用空间和效率完胜其他序列化类库,如Hessian;需要编写 .proto 文件。
  • 《Protocol Buffers序列化协议及应用》
+ 关于协议的解释;缺点:可读性差;
  • 《简单的使用 protobuf 和 protostuff》
+ protostuff 的好处是不用写 .proto 文件,Java 对象直接就可以序列化。

数据库

基础理论

数据库设计的三大范式

  • 《数据库的三大范式以及五大约束》
    • 第一范式:数据表中的每一列(每个字段)必须是不可拆分的最小单元,也就是确保每一列的原子性;
    • 第二范式(2NF):满足1NF后,要求表中的所有列,都必须依赖于主键,而不能有任何一列与主键没有关系,也就是说一个表只描述一件事情;
    • 第三范式:必须先满足第二范式(2NF),要求:表中的每一列只与主键直接相关而不是间接相关,(表中的每一列只能依赖于主键);

MySQL

原理

  • 《MySQL的InnoDB索引原理详解》
  • 《MySQL存储引擎--MyISAM与InnoDB区别》
+ 两种类型最主要的差别就是Innodb 支持事务处理与外键和行级锁
  • 《myisam和innodb索引实现的不同》

InnoDB

  • 《一篇文章带你读懂Mysql和InnoDB》

优化

  • 《MySQL36条军规》
  • 《MYSQL性能优化的最佳20+条经验》
  • 《SQL优化之道》
  • 《mysql数据库死锁的产生原因及解决办法》
  • 《导致索引失效的可能情况》
  • 《 MYSQL分页limit速度太慢优化方法》
+ 原则上就是缩小扫描范围。

索引

聚集索引, 非聚集索引

  • 《MySQL 聚集索引/非聚集索引简述》
  • 《MyISAM和InnoDB的索引实现》

MyISAM 是非聚集,InnoDB 是聚集

复合索引

  • 《复合索引的优点和注意事项》

自适应哈希索引(AHI)

  • 《InnoDB存储引擎——自适应哈希索引》

explain

  • 《MySQL 性能优化神器 Explain 使用分析》

NoSQL

MongoDB

  • MongoDB 教程
  • 《Mongodb相对于关系型数据库的优缺点》
    • 优点:弱一致性(最终一致),更能保证用户的访问速度;内置GridFS,支持大容量的存储;Schema-less 数据库,不用预先定义结构;内置Sharding;相比于其他NoSQL,第三方支持丰富;性能优越;
    • 缺点:mongodb不支持事务操作;mongodb占用空间过大;MongoDB没有如MySQL那样成熟的维护工具,这对于开发和IT运营都是个值得注意的地方;

Hbase

  • 《简明 HBase 入门教程(开篇)》
  • 《深入学习HBase架构原理》
  • 《传统的行存储和(HBase)列存储的区别》
  • 《Hbase与传统数据库的区别》
+ 空数据不存储,节省空间,且适用于并发。
  • 《HBase Rowkey设计》
+ rowkey 按照字典顺序排列,便于批量扫描。
+ 通过散列可以避免热点。

搜索引擎

搜索引擎原理

  • 《倒排索引–搜索引擎入门》

Lucene

  • 《Lucene入门简介》

Elasticsearch

  • 《Elasticsearch学习,请先看这一篇!》
  • 《Elasticsearch索引原理》

Solr

  • 《 Apache Solr入门教程》
  • 《elasticsearch与solr比较》

sphinx

  • 《Sphinx 的介绍和原理探索》

性能

性能优化方法论

  • 《15天的性能优化工作,5方面的调优经验》
+ 代码层面、业务层面、数据库层面、服务器层面、前端优化。
  • 《系统性能优化的几个方面》

容量评估

  • 《联网性能与容量评估的方法论和典型案例》

CDN 网络

  • 《CDN加速原理》
  • 《国内有哪些比较好的 CDN?》

连接池

  • 《主流Java数据库连接池比较与开发配置实战》

性能调优

  • 《九大Java性能调试工具,必备至少一款》

大数据

流式计算

Storm

  • 官方网站
  • 《最详细的Storm入门教程》

Flink

  • 《Flink之一 Flink基本原理介绍》

Kafka Stream

  • 《Kafka Stream调研:一种轻量级流计算模式》

应用场景

例如:

  • 广告相关实时统计;
  • 推荐系统用户画像标签实时更新;
  • 线上服务健康状况实时监测;
  • 实时榜单;
  • 实时数据统计。

Hadoop

  • 《用通俗易懂的话说下hadoop是什么,能做什么》
  • 《史上最详细的Hadoop环境搭建》

HDFS

  • 《【Hadoop学习】HDFS基本原理》

MapReduce

  • 《用通俗易懂的大白话讲解Map/Reduce原理》
  • 《 简单的map-reduce的java例子》

Yarn

  • 《初步掌握Yarn的架构及原理》

Spark

  • 《Spark(一): 基本架构及原理》

安全

web 安全

XSS

  • 《xss攻击原理与解决方法》

CSRF

  • 《CSRF原理及防范》

SQL 注入

  • 《SQL注入》

Hash Dos

  • 《邪恶的JAVA HASH DOS攻击》
    • 利用JsonObjet 上传大Json,JsonObject 底层使用HashMap;不同的数据产生相同的hash值,使得构建Hash速度变慢,耗尽CPU。
  • 《一种高级的DoS攻击-Hash碰撞攻击》
  • 《关于Hash Collision DoS漏洞:解析与解决方案》

脚本注入

  • 《上传文件漏洞原理及防范》

漏洞扫描工具

  • 《DVWA》
  • W3af
  • OpenVAS详解

验证码

  • 《验证码原理分析及实现》
  • 《详解滑动验证码的实现原理》
+ 滑动验证码是根据人在滑动滑块的响应时间,拖拽速度,时间,位置,轨迹,重试次数等来评估风险。
  • 《淘宝滑动验证码研究》

DDoS 防范

  • 《学习手册:DDoS的攻击方式及防御手段》
  • 《免费DDoS攻击测试工具大合集》

用户隐私信息保护

  1. 用户密码非明文保存,加动态slat。
  2. 身份证号,手机号如果要显示,用 “*” 替代部分字符。
  3. 联系方式在的显示与否由用户自己控制。
  4. TODO
  • 《个人隐私包括哪些》
  • 《在互联网上,隐私的范围包括哪些?》
  • 《用户密码保存》

序列化漏洞

  • 《Lib之过?Java反序列化漏洞通用利用分析》

加密解密

对称加密

  • 《常见对称加密算法》
    • DES、3DES、Blowfish、AES
    • DES 采用 56位秘钥,Blowfish 采用1到448位变长秘钥,AES 128,192和256位长度的秘钥。
    • DES 秘钥太短(只有56位)算法目前已经被 AES 取代,并且 AES 有硬件加速,性能很好。

哈希算法

  • 《常用的哈希算法》
+ MD5 和 SHA-1 已经不再安全,已被弃用。
+ 目前 SHA-256 是比较安全的。
  • 《基于Hash摘要签名的公网URL签名验证设计方案》

非对称加密

  • 《常见非对称加密算法》
    • RSA、DSA、ECDSA(螺旋曲线加密算法)
    • 和 RSA 不同的是 DSA 仅能用于数字签名,不能进行数据加密解密,其安全性和RSA相当,但其性能要比RSA快。
    • 256位的ECC秘钥的安全性等同于3072位的RSA秘钥。
[《区块链的加密技术》](http://baijiahao.baidu.com/s?id=1578348858092033763&wfr=spider&for=pc)

服务器安全

  • 《Linux强化论:15步打造一个安全的Linux服务器》

数据安全

数据备份

TODO

网络隔离

内外网分离

TODO

登录跳板机

在内外环境中通过跳板机登录到线上主机。

  • 《搭建简易堡垒机》

授权、认证

RBAC

  • 《基于组织角色的权限设计》
  • 《权限系统与RBAC模型概述》
  • 《Spring整合Shiro做权限控制模块详细案例分析》

OAuth2.0

  • 《理解OAuth 2.0》

双因素认证(2FA)

2FA - Two-factor authentication,用于加强登录验证

常用做法是 登录密码 + 手机验证码(或者令牌Key,类似于与网银的 USB key)

  • 【《双因素认证(2FA)教程》】(www.ruanyifeng.com/blog/2017/1…)

单点登录(SSO)

  • 《单点登录原理与简单实现》
  • CAS单点登录框架

常用开源框架

开源协议

  • 《开源协议的选择》

日志框架

Log4j、Log4j2

  • 《log4j 详细讲解》
  • 《log4j2 实际使用详解》
  • 《Log4j1,Logback以及Log4j2性能测试对比》
    • Log4J 异步日志性能优异。

Logback

  • 《最全LogBack 详解、含java案例和配置说明》

ORM

  • 《ORM框架使用优缺点》
    • 主要目的是为了提高开发效率。

MyBatis:

  • 《mybatis缓存机制详解》
+ 一级缓存是SqlSession级别的缓存,缓存的数据只在SqlSession内有效
+ 二级缓存是mapper级别的缓存,同一个namespace公用这一个缓存,所以对SqlSession是共享的;使用 LRU 机制清理缓存,通过 cacheEnabled 参数开启。
  • 《MyBatis学习之代码生成器Generator》

网络框架

TODO

Web 框架

Spring 家族

Spring

  • Spring 简明教程

Spring Boot

  • 官方网站
  • 《Spring Boot基础教程》

Spring Cloud

  • Spring Boot 中文索引站
  • Spring Cloud 中文文档
  • 《Spring Cloud基础教程》

工具框架

  • 《Apache Commons 工具类介绍及简单使用》
  • 《Google guava 中文教程》

分布式设计

扩展性设计

  • 《架构师不可不知的十大可扩展架构》
+ 总结下来,通用的套路就是分布、缓存及异步处理。
  • 《可扩展性设计之数据切分》
+ 水平切分+垂直切分
+ 利用中间件进行分片如,MySQL Proxy。
+ 利用分片策略进行切分,如按照ID取模。
  • 《说说如何实现可扩展性的大型网站架构》
+ 分布式服务+消息队列。
  • 《大型网站技术架构(七)–网站的可扩展性架构》

稳定性 & 高可用

  • 《系统设计:关于高可用系统的一些技术方案》
+ 可扩展:水平扩展、垂直扩展。 通过冗余部署,避免单点故障。
+ 隔离:避免单一业务占用全部资源。避免业务之间的相互影响 2. 机房隔离避免单点故障。
+ 解耦:降低维护成本,降低耦合风险。减少依赖,减少相互间的影响。
+ 限流:滑动窗口计数法、漏桶算法、令牌桶算法等算法。遇到突发流量时,保证系统稳定。
+ 降级:紧急情况下释放非核心功能的资源。牺牲非核心业务,保证核心业务的高可用。
+ 熔断:异常情况超出阈值进入熔断状态,快速失败。减少不稳定的外部依赖对核心服务的影响。
+ 自动化测试:通过完善的测试,减少发布引起的故障。
+ 灰度发布:灰度发布是速度与安全性作为妥协,能够有效减少发布故障。
  • 《关于高可用的系统》
+ 设计原则:数据不丢(持久化);服务高可用(服务副本);绝对的100%高可用很难,目标是做到尽可能多的9,如99.999%(全年累计只有5分钟)。

硬件负载均衡

  • 《转!!负载均衡器技术Nginx和F5的优缺点对比》
+ 主要是和F5对比。
  • 《软/硬件负载均衡产品 你知多少?》

软件负载均衡

  • 《几种负载均衡算法》 轮寻、权重、负载、最少连接、QoS
  • 《DNS负载均衡》
+ 配置简单,更新速度慢。
  • 《Nginx负载均衡》
+ 简单轻量、学习成本低;主要适用于web应用。
  • 《借助LVS+Keepalived实现负载均衡 》
+ 配置比较负载、只支持到4层,性能较高。
  • 《HAProxy用法详解 全网最详细中文文档》
+ 支持到七层(比如HTTP)、功能比较全面,性能也不错。
  • 《Haproxy+Keepalived+MySQL实现读均衡负载》
+ 主要是用户读请求的负载均衡。
  • 《rabbitmq+haproxy+keepalived实现高可用集群搭建》

限流

  • 《谈谈高并发系统的限流》
    • 计数器:通过滑动窗口计数器,控制单位时间内的请求次数,简单粗暴。
    • 漏桶算法:固定容量的漏桶,漏桶满了就丢弃请求,比较常用。
    • 令牌桶算法:固定容量的令牌桶,按照一定速率添加令牌,处理请求前需要拿到令牌,拿不到令牌则丢弃请求,或进入丢队列,可以通过控制添加令牌的速率,来控制整体速度。Guava 中的 RateLimiter 是令牌桶的实现。
    • Nginx 限流:通过 limit_req 等模块限制并发连接数。

应用层容灾

  • 《防雪崩利器:熔断器 Hystrix 的原理与使用》
+ 雪崩效应原因:硬件故障、硬件故障、程序Bug、重试加大流量、用户大量请求。
+ 雪崩的对策:限流、改进缓存模式(缓存预加载、同步调用改异步)、自动扩容、降级。
+ Hystrix设计原则:
    - 资源隔离:Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离, 从而避免服务雪崩。
    - 熔断开关:服务的健康状况 = 请求失败数 / 请求总数,通过阈值设定和滑动窗口控制开关。
    - 命令模式:通过继承 HystrixCommand 来包装服务调用逻辑。
  • 《缓存穿透,缓存击穿,缓存雪崩解决方案分析》
  • 《缓存击穿、失效以及热点key问题》
+ 主要策略:失效瞬间:单机使用锁;使用分布式锁;不过期;
+ 热点数据:热点数据单独存储;使用本地缓存;分成多个子key;

跨机房容灾

  • 《“异地多活”多机房部署经验谈》
+ 通过自研中间件进行数据同步。
  • 《异地多活(异地双活)实践经验》
+ 注意延迟问题,多次跨机房调用会将延时放大数倍。
+ 建房间专线很大概率会出现问题,做好运维和程序层面的容错。
+ 不能依赖于程序端数据双写,要有自动同步方案。
+ 数据永不在高延迟和较差网络质量下,考虑同步质量问题。
+ 核心业务和次要业务分而治之,甚至只考虑核心业务。
+ 异地多活监控部署、测试也要跟上。
+ 业务允许的情况下考虑用户分区,尤其是游戏、邮箱业务。
+ 控制跨机房消息体大小,越小越好。
+ 考虑使用docker容器虚拟化技术,提高动态调度能力。
  • 容灾技术及建设经验介绍

容灾演练流程

  • 《依赖治理、灰度发布、故障演练,阿里电商故障演练系统的设计与实战经验》
    • 常见故障画像
    • 案例:预案有效性、预案有效性、故障复现、架构容灾测试、参数调优、参数调优、故障突袭、联合演练。

平滑启动

  • 平滑重启应用思路 1.端流量(如vip层)、2. flush 数据(如果有)、3, 重启应用
  • 《JVM安全退出(如何优雅的关闭java服务)》 推荐推出方式:System.exit,Kill SIGTERM;不推荐 kill-9;用 Runtime.addShutdownHook 注册钩子。
  • 《常见Java应用如何优雅关闭》 Java、Srping、Dubbo 优雅关闭方式。

数据库扩展

读写分离模式

  • 《Mysql主从方案的实现》
  • 《搭建MySQL主从复制经典架构》
  • 《Haproxy+多台MySQL从服务器(Slave) 实现负载均衡》
  • 《DRBD+Heartbeat+Mysql高可用读写分离架构》
+ DRDB 进行磁盘复制,避免单点问题。
  • 《MySQL Cluster 方式》

分片模式

  • 《分库分表需要考虑的问题及方案》
+ 中间件: 轻量级:sharding-jdbc、TSharding;重量级:Atlas、MyCAT、Vitess等。
+ 问题:事务、Join、迁移、扩容、ID、分页等。
+ 事务补偿:对数据进行对帐检查;基于日志进行比对;定期同标准数据来源进行同步等。
+ 分库策略:数值范围;取模;日期等。
+ 分库数量:通常 MySQL 单库 5千万条、Oracle 单库一亿条需要分库。
  • 《MySql分表和表分区详解》
+ 分区:是MySQL内部机制,对客户端透明,数据存储在不同文件中,表面上看是同一个表。
+ 分表:物理上创建不同的表、客户端需要管理分表路由。

服务治理

服务注册与发现

  • 《永不失联!如何实现微服务架构中的服务发现?》
+ 客户端服务发现模式:客户端直接查询注册表,同时自己负责负载均衡。Eureka 采用这种方式。
+ 服务器端服务发现模式:客户端通过负载均衡查询服务实例。
  • 《SpringCloud服务注册中心比较:Consul vs Zookeeper vs Etcd vs Eureka》
+ CAP支持:Consul(CA)、zookeeper(cp)、etcd(cp) 、euerka(ap)
+ 作者认为目前 Consul 对 Spring cloud 的支持比较好。
  • 《基于Zookeeper的服务注册与发现》
+ 优点:API简单、Pinterest,Airbnb 在用、多语言、通过watcher机制来实现配置PUSH,能快速响应配置变化。

服务路由控制

  • 《分布式服务框架学习笔记4 服务路由》
    • 原则:透明化路由
    • 负载均衡策略:随机、轮询、服务调用延迟、一致性哈希、粘滞连接
    • 本地路由有限策略:injvm(优先调用jvm内部的服务),innative(优先使用相同物理机的服务),原则上找距离最近的服务。
    • 配置方式:统一注册表;本地配置;动态下发。

分布式一致

CAP 与 BASE 理论

  • 《从分布式一致性谈到CAP理论、BASE理论》
    • 一致性分类:强一致(立即一致);弱一致(可在单位时间内实现一致,比如秒级);最终一致(弱一致的一种,一定时间内最终一致)
    • CAP:一致性、可用性、分区容错性(网络故障引起)
    • BASE:Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)
    • BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

分布式锁

  • 《分布式锁的几种实现方式》
+ 基于数据库的分布式锁:优点:操作简单、容易理解。缺点:存在单点问题、数据库性能够开销较大、不可重入;
+ 基于缓存的分布式锁:优点:非阻塞、性能好。缺点:操作不好容易造成锁无法释放的情况。
+ Zookeeper 分布式锁:通过有序临时节点实现锁机制,自己对应的节点需要最小,则被认为是获得了锁。优点:集群可以透明解决单点问题,避免锁不被释放问题,同时锁可以重入。缺点:性能不如缓存方式,吞吐量会随着zk集群规模变大而下降。
  • 《基于Zookeeper的分布式锁》
+ 清楚的原理描述 + Java 代码示例。
  • 《jedisLock—redis分布式锁实现》
+ 基于 setnx(set if ont exists),有则返回false,否则返回true。并支持过期时间。
  • 《Memcached 和 Redis 分布式锁方案》
+ 利用 memcached 的 add(有别于set)操作,当key存在时,返回false。

分布式一致性算法

PAXOS

  • 《分布式系列文章——Paxos算法原理与推导》
  • 《Paxos–>Fast Paxos–>Zookeeper分析》
  • 《【分布式】Zookeeper与Paxos》

Zab

  • 《Zab:Zookeeper 中的分布式一致性协议介绍》

Raft

  • 《Raft 为什么是更易理解的分布式一致性算法》
    • 三种角色:Leader(领袖)、Follower(群众)、Candidate(候选人)
    • 通过随机等待的方式发出投票,得票多的获胜。

Gossip

  • 《Gossip算法》

两阶段提交、多阶段提交

  • 《关于分布式事务、两阶段提交协议、三阶提交协议》

幂等

  • 《分布式系统—幂等性设计》
    • 幂等特性的作用:该资源具备幂等性,请求方无需担心重复调用会产生错误。
    • 常见保证幂等的手段:MVCC(类似于乐观锁)、去重表(唯一索引)、悲观锁、一次性token、序列号方式。

分布式一致方案

  • 《分布式系统事务一致性解决方案》
  • 《保证分布式系统数据一致性的6种方案》

分布式 Leader 节点选举

  • 《利用zookeeper实现分布式leader节点选举》

TCC(Try/Confirm/Cancel) 柔性事务

  • 《传统事务与柔性事务》
    • 基于BASE理论:基本可用、柔性状态、最终一致。
    • 解决方案:记录日志+补偿(正向补充或者回滚)、消息重试(要求程序要幂等);“无锁设计”、采用乐观锁机制。

分布式文件系统

  • 说说分布式文件存储系统-基本架构 ?
  • 《各种分布式文件系统的比较》 ?
    • HDFS:大批量数据读写,用于高吞吐量的场景,不适合小文件。
    • FastDFS:轻量级、适合小文件。

唯一ID 生成

全局唯一ID

  • 《高并发分布式系统中生成全局唯一Id汇总》
+ Twitter 方案(Snowflake 算法):41位时间戳+10位机器标识(比如IP,服务器名称等)+12位序列号(本地计数器)
+ Flicker 方案:MySQL自增ID + "REPLACE INTO XXX:SELECT 1316656;"
+ UUID:缺点,无序,字符串过长,占用空间,影响检索性能。
+ MongoDB 方案:利用 ObjectId。缺点:不能自增。
  • 《TDDL 在分布式下的SEQUENCE原理》
+ 在数据库中创建 sequence 表,用于记录,当前已被占用的id最大值。
+ 每台客户端主机取一个id区间(比如 1000~2000)缓存在本地,并更新 sequence 表中的id最大值记录。
+ 客户端主机之间取不同的id区间,用完再取,使用乐观锁机制控制并发。

一致性Hash算法

  • 《一致性哈希算法》

设计思想 & 开发模式

DDD(Domain-driven Design - 领域驱动设计)

  • 《浅谈我对DDD领域驱动设计的理解》
+ 概念:DDD 主要对传统软件开发流程(分析-设计-编码)中各阶段的割裂问题而提出,避免由于一开始分析不明或在软件开发过程中的信息流转不一致而造成软件无法交付(和需求方设想不一致)的问题。DDD 强调一切以领域(Domain)为中心,强调领域专家(Domain Expert)的作用,强调先定义好领域模型之后在进行开发,并且领域模型可以指导开发(所谓的驱动)。
+ 过程:理解领域、拆分领域、细化领域,模型的准确性取决于模型的理解深度。
+ 设计:DDD 中提出了建模工具,比如聚合、实体、值对象、工厂、仓储、领域服务、领域事件来帮助领域建模。
  • 《领域驱动设计的基础知识总结》
+ 领域(Doamin)本质上就是问题域,比如一个电商系统,一个论坛系统等。
+ 界限上下文(Bounded Context):阐述子域之间的关系,可以简单理解成一个子系统或组件模块。
+ 领域模型(Domain Model):DDD的核心是建立(用通用描述语言、工具—领域通用语言)正确的领域模型;反应业务需求的本质,包括实体和过程;其贯穿软件分析、设计、开发 的整个过程;常用表达领域模型的方式:图、代码或文字;
+ 领域通用语言:领域专家、开发设计人员都能立即的语言或工具。
+ 经典分层架构:用户界面/展示层、应用层、领域层、基础设施层,是四层架构模式。
+ 使用的模式:
    - 关联尽量少,尽量单项,尽量降低整体复杂度。
    - 实体(Entity):领域中的唯一标示,一个实体的属性尽量少,少则清晰。
    - 值对象(Value Object):没有唯一标识,且属性值不可变,小二简单的对象,比如Date。
    - 领域服务(Domain Service): 协调多个领域对象,只有方法没有状态(不存数据);可以分为应用层服务,领域层服务、基础层服务。
    - 聚合及聚合根(Aggregate,Aggregate Root):聚合定义了一组具有内聚关系的相关对象的集合;聚合根是对聚合引用的唯一元素;当修改一个聚合时,必须在事务级别;大部分领域模型中,有70%的聚合通常只有一个实体,30%只有2~3个实体;如果一个聚合只有一个实体,那么这个实体就是聚合根;如果有多个实体,那么我们可以思考聚合内哪个对象有独立存在的意义并且可以和外部直接进行交互;
    - 工厂(Factory):类似于设计模式中的工厂模式。
    - 仓储(Repository):持久化到DB,管理对象,且只对聚合设计仓储。
  • 《领域驱动设计(DDD)实现之路》
+ 聚合:比如一辆汽车(Car)包含了引擎(Engine)、车轮(Wheel)和油箱(Tank)等组件,缺一不可。
  • 《领域驱动设计系列(2)浅析VO、DTO、DO、PO的概念、区别和用处》

命令查询职责分离(CQRS)

CQRS — Command Query Responsibility Seperation

  • 《领域驱动设计系列 (六):CQRS》
+ 核心思想:读写分离(查询和更新在不同的方法中),不同的流程只是不同的设计方式,CQ代码分离,分布式环境中会有明显体现(有冗余数据的情况下),目的是为了高性能。
  • 《DDD CQRS架构和传统架构的优缺点比较》
+ 最终一致的设计理念;依赖于高可用消息中间件。
  • 《CQRS架构简介》
+ 一个实现 CQRS 的抽象案例。
  • 《深度长文:我对CQRS/EventSourcing架构的思考》
+ CQRS 模式分析 + 12306 抢票案例

贫血,充血模型

  • 《贫血,充血模型的解释以及一些经验》
    • 失血模型:老子和儿子分别定义,相互不知道,二者实体定义中完全没有业务逻辑,通过外部Service进行关联。
    • 贫血模型:老子知道儿子,儿子也知道老子;部分业务逻辑放到实体中;优点:各层单项依赖,结构清楚,易于维护;缺点:不符合OO思想,相比于充血模式,Service层较为厚重;
    • 充血模型:和贫血模型类似,区别在于如何划分业务逻辑。优点:Service层比较薄,只充当Facade的角色,不和DAO打交道、复合OO思想;缺点:非单项依赖,DO和DAO之间双向依赖、和Service层的逻辑划分容易造成混乱。
    • 肿胀模式:是一种极端情况,取消Service层、全部业务逻辑放在DO中;优点:符合OO思想、简化了分层;缺点:暴露信息过多、很多非DO逻辑也会强行并入DO。这种模式应该避免。
    • 作者主张使用贫血模式。

Actor 模式

TODO

响应式编程

Reactor

TODO

RxJava

TODO

Vert.x

TODO

DODAF2.0

  • 《DODAF2.0方法论》
  • 《DODAF2.0之能力视角如何落地》

Serverless

TODO

Service Mesh

TODO

  • 《什么是Service Mesh?》

项目管理

架构评审

  • 《架构设计之如何评审架构设计说明书》
  • 《人人都是架构师:非功能性需求》

重构

  • 《架构之重构的12条军规》

代码规范

TODO

代码 Review

制度还是制度! 另外,每个公司需要根据自己的需求和目标制定自己的 check list

  • 《为什么你做不好 Code Review?》
+ 代码 review 做的好,在于制度建设。
  • 《从零开始Code Review》
  • 《Code Review Checklist》
  • 《Java Code Review Checklist》
  • 《如何用 gitlab 做 code review》

RUP

  • 《运用RUP 4+1视图方法进行软件架构设计》

看板管理

  • 《说说看板在项目中的应用》

SCRUM

SCRUM - 争球

  • 3个角色:Product Owner(PO) 产品负责人;Scrum Master(SM),推动Scrum执行;Team 开发团队。
  • 3个工件:Product Backlog 产品TODOLIST,含优先级;Sprint Backlog 功能开发 TODO LIST;燃尽图;
  • 五个价值观:专注、勇气、公开、承诺、尊重。
  • 《敏捷项目管理流程-Scrum框架最全总结!》
  • 《敏捷其实很简单3—敏捷方法之scrum》

敏捷开发

TODO

极限编程(XP)

XP - eXtreme Programming

  • 《主流敏捷开发方法:极限编程XP》
    • 是一种指导开发人员的方法论。
    • 4大价值:
    - 沟通:鼓励口头沟通,提高效率。
    - 简单:够用就好。
    - 反馈:及时反馈、通知相关人。
    - 勇气:提倡拥抱变化,敢于重构。
+ 5个原则:快速反馈、简单性假设、逐步修改、提倡更改(小步快跑)、优质工作(保证质量的前提下保证小步快跑)。
+ 5个工作:阶段性冲刺;冲刺计划会议;每日站立会议;冲刺后review;回顾会议。

结对编程

边写码,边review。能够增强代码质量、减少bug。

  • 《结对编程》

FMEA管理模式

TODO

通用业务术语

TODO

技术趋势

TODO

政策、法规

TODO

法律

严格遵守刑法253法条

我国刑法第253条之一规定:

  • 国家机关或者金融、电信、交通、教育、医疗等单位的工作人员,违反国家规定,将本单位在履行职责或者提供服务过程中获得的公民个人信息,出售或者非法提供给他人,情节严重的,处3年以下有期徒刑或者拘役,并处或者单处罚金。
  • 窃取或者以其他方法非法获取上述信息,情节严重的,依照前款的规定处罚。
  • 单位犯前两款罪的,对单位判处罚金,并对其直接负责的主管人员和其他直接责任人员,依照各该款的规定处罚。

最高人民法院、最高人民检察院关于执行《中华人民共和国刑法》确定罪名的补充规定(四)规定:触犯刑法第253条之一第1款之规定,构成“出售、非法提供公民个人信息罪”;触犯刑法第253条之一第2款之规定,构成“非法获取公民个人信息罪”

  • 《非法获取公民个人信息罪》

架构师素质

  • 《架构师画像》
+ 业务理解和抽象能力
+ NB的代码能力
+ 全面:1. 在面对业务问题上,架构师脑海里是否会浮现出多种技术方案;2. 在做系统设计时是否考虑到了足够多的方方面面;3. 在做系统设计时是否考虑到了足够多的方方面面;
+ 全局:是否考虑到了对上下游的系统的影响。
+ 权衡:权衡投入产出比;优先级和节奏控制;
  • 《关于架构优化和设计,架构师必须知道的事情》
+ 要去考虑的细节:模块化、轻耦合、无共享架构;减少各个组件之前的依懒、注意服务之间依懒所有造成的链式失败及影响等。
+ 基础设施、配置、测试、开发、运维综合考虑。
+ 考虑人、团队、和组织的影响。
  • 《如何才能真正的提高自己,成为一名出色的架构师?》
  • 《架构师的必备素质和成长途径》
+ 素质:业务理解、技术广度、技术深度、丰富经验、沟通能力、动手能力、美学素养。
+ 成长路径:2年积累知识、4年积累技能和祖内影响力、7年积累部门内影响力、7年以上积累跨部门影响力。
  • 《架构设计师—你在哪层楼?》
+ 第一层的架构师看到的只是产品本身
+ 第二层的架构师不仅看到自己的产品,还看到了整体的方案
+ 第三层的架构师看到的是商业价值

团队管理

TODO

招聘

资讯

行业资讯

  • 36kr
  • Techweb

公众号列表

TODO

博客

团队博客

  • 阿里中间件博客
  • 美团点评技术团队博客

个人博客

  • 阮一峰的网络日志
  • 酷壳 - COOLSHELL-陈皓
  • hellojava-阿里毕玄
  • Cm’s Blog
  • 程序猿DD-翟永超-《Spring Cloud微服务实战》作者

综合门户、社区

国内:

  • CSDN 老牌技术社区、不必解释。
  • 51cto.com
  • ITeye
+ 偏 Java 方向
  • 博客园
  • ChinaUnix
+ 偏 Linux 方向
  • 开源中国社区
  • 深度开源
  • 伯乐在线
+ 涵盖 IT职场、Web前端、后端、移动端、数据库等方面内容,偏技术端。
  • ITPUB
  • 腾讯云— 云+社区
  • 阿里云— 云栖社区
  • IBM DeveloperWorks
  • 开发者头条
  • LinkedKeeper

国外:

  • DZone
  • Reddit

问答、讨论类社区

  • segmentfault
    • 问答+专栏
  • 知乎
  • stackoverflow

行业数据分析

  • 艾瑞网
  • QUEST MOBILE
  • 国家数据

专项网站

  • 测试:
+ [领测国际](http://www.ltesting.net/)
+ [测试窝](https://www.testwo.com/)
+ [TesterHome](https://testerhome.com)
  • 运维:
+ [运维派](http://www.yunweipai.com/)
+ [Abcdocker](https://www.abcdocker.com/)
  • Java:
+ [ImportNew](http://www.importnew.com/)
    - 专注于 Java 技术分享
+ [HowToDoInJava](https://howtodoinjava.com/)
    - 英文博客
  • 安全
+ [红黑联盟](https://www.2cto.com/)
+ [FreeBuf](http://www.freebuf.com/)
  • 大数据
+ [中国大数据](http://www.thebigdata.cn/)
  • 其他专题网站:
+ [DockerInfo](http://www.dockerinfo.net/)
    - 专注于 Docker 应用及咨询、教程的网站。
+ [Linux公社](https://www.linuxidc.com/)
    - Linux 主题社区

其他类

  • 程序员技能图谱

推荐参考书

在线电子书

  • 《深入理解Spring Cloud与微服务构建》
  • 《阿里技术参考图册-研发篇》
  • 《阿里技术参考图册-算法篇》
  • 《2018美团点评技术年货(合辑)》70M
  • InfoQ《架构师》月刊
  • 《架构师之路》

纸质书

开发方面

  • 《阿里巴巴Java开发手册》京东 淘宝

架构方面

  • 《软件架构师的12项修炼:技术技能篇》京东 淘宝
  • 《架构之美》京东 淘宝
  • 《分布式服务架构》京东 淘宝
  • 《聊聊架构》 京东 淘宝
  • 《云原生应用架构实践》京东 淘宝
  • 《亿级流量网站架构核心技术》京东 淘宝
  • 《淘宝技术这十年》京东 淘宝
  • 《企业IT架构转型之道-中台战略思想与架构实战》 京东 淘宝

技术管理方面

  • 《CTO说》京东 淘宝
  • 《技术管理之巅》京东 淘宝
  • 《网易一千零一夜:互联网产品项目管理实战》京东 淘宝

基础理论

  • 《数学之美》京东 淘宝
  • 《编程珠玑》京东 淘宝

工具方面

TODO

大数据方面

技术资源

开源资源

  • github
  • Apache 软件基金会

手册、文档、教程

国内:

  • W3Cschool
  • Runoob.com
+ HTML 、 CSS、XML、Java、Python、PHP、设计模式等入门手册。
  • Love2.io
+ 很多很多中文在线电子书,是一个全新的开源技术文档分享平台。
  • gitbook.cn
+ 付费电子书。
  • ApacheCN
+ AI、大数据方面系列中文文档。

国外:

  • Quick Code
    • 免费在线技术教程。
  • gitbook.com
    • 有部分中文电子书。
  • Cheatography
    • Cheat Sheets 大全,单页文档网站。

在线课堂

  • 学徒无忧
  • 极客时间
  • segmentfault
  • 斯达克学院
  • 牛客网
  • 极客学院
  • 51CTO学院

会议、活动

  • QCon
  • ArchSummit
  • GITC全球互联网技术大会

活动发布平台:

  • 活动行

常用APP

  • 极客时间
  • 得到

找工作

  • Boss直聘
  • 拉勾网
  • 猎聘
  • 100Offer

工具

  • 极客搜索
    • 技术文章搜索引擎。

代码托管

  • Coding
  • 码云

文件服务

  • 七牛
  • 又拍云

综合云服务商

  • 阿里云
  • 腾讯云
  • 百度云
  • 新浪云
  • 金山云
  • 亚马逊云(AWS)
  • 谷歌云
  • 微软云

VPS

  • Linode

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java中高级面试题

发表于 2018-05-03

一. 基础知识:

1)集合类:List和Set比较,各自的子类比较(ArrayList,Vector,LinkedList;HashSet,TreeSet);

2)HashMap的底层实现,之后会问ConcurrentHashMap的底层实现;

3)如何实现HashMap顺序存储:可以参考LinkedHashMap的底层实现;

4)HashTable和ConcurrentHashMap的区别;

5)String,StringBuffer和StringBuilder的区别;

6)Object的方法有哪些:比如有wait方法,为什么会有;

7)wait和sleep的区别,必须理解;

8)JVM的内存结构,JVM的算法;

9)强引用,软引用和弱引用的区别;

10)数组在内存中如何分配;

11)用过哪些设计模式,手写一个(除单例);

12)springmvc的核心是什么,请求的流程是怎么处理的,控制反转怎么实现的;

13)spring里面的aop的原理是什么;

14)mybatis如何处理结果集:反射,建议看看源码;

15)java的多态表现在哪里;

16)接口有什么用;

17)说说http,https协议;

18)tcp/ip协议簇;

19)osi五层网络协议;

20)tcp,udp区别;

21)用过哪些加密算法:对称加密,非对称加密算法;

22)说说tcp三次握手,四次挥手;

23)cookie和session的区别,分布式环境怎么保存用户状态;

24)git,svn区别;

25)请写一段栈溢出、堆溢出的代码;

26)ThreadLocal可以用来共享数据吗;

二. IO:

1)bio,nio,aio的区别;

2)nio框架:dubbo的实现原理;

3)京东内部的jsf是使用的什么协议通讯:可参见dubbo的协议;

三. 算法:

1)java中常说的堆和栈,分别是什么数据结构;另外,为什么要分为堆和栈来存储数据。

2)TreeMap如何插入数据:二叉树的左旋,右旋,双旋;

3)一个排序之后的数组,插入数据,可以使用什么方法?答:二分法;问:时间复杂度是多少?

4)平衡二叉树的时间复杂度;

5)Hash算法和二叉树算法分别什么时候用;

6)图的广度优先算法和深度优先算法:详见jvm中垃圾回收实现;

四. 多线程相关:****

1)说说阻塞队列的实现:可以参考ArrayBlockingQueue的底层实现(锁和同步都行);

2)进程通讯的方式:消息队列,共享内存,信号量,socket通讯等;

3)用过并发包的哪些类;

4)什么地方用了多线程;

5)Excutors可以产生哪些线程池;

6)为什么要用线程池;

7)volatile关键字的用法:使多线程中的变量可见;

五. 数据库相关(mysql):

1)msyql优化经验:

2)mysql的语句优化,使用什么工具;

3)mysql的索引分类:B+,hash;什么情况用什么索引;

4)mysql的存储引擎有哪些,区别是什么;

5)说说事务的特性和隔离级别;

6)悲观锁和乐观锁的区别,怎么实现;

六. mq:

1)mq的原理是什么:有点大。。都可以说;

2)mq如何保证实时性;

3)mq的持久化是怎么做的;

七. nosql相关(主要是redis):

1)redis和memcache的区别;

2)用redis做过什么;

3)redis是如何持久化的:rdb和aof;

4)redis集群如何同步;

5)redis的数据添加过程是怎样的:哈希槽;

6)redis的淘汰策略有哪些;

7)redis有哪些数据结构;

八. zookeeper:

1)zookeeper是什么;

2)zookeeper哪里用到;

3)zookeeper的选主过程;

4)zookeeper集群之间如何通讯;

5)你们的zookeeper的节点加密是用的什么方式;

6)分布式锁的实现过程;

九. linux相关:

1)linux常用的命令有哪些;

2)如何获取java进程的pid;

3)如何获取某个进程的网络端口号;

4)如何实时打印日志;

5)如何统计某个字符串行数;

十. 设计与思想:

1)重构过代码没有?说说经验;

2)一千万的用户实时排名如何实现;

3)五万人并发抢票怎么实现;

我有一个微信公众号,经常会分享一些Java技术相关的干货;如果你喜欢我的分享,可以用微信搜索“Java团长”或者“javatuanzhang”关注。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

彻底理解synchronized 1 synchroniz

发表于 2018-04-30

#莫等闲,白了少年头#

本人从毕业开始一直在一线互联网大厂工作,现任技术TL,出版过《深入理解Java并发》一书,折腾过技术开源项目,并长期作为面试官参与面试,深谙双方的诉求与技术沟通。如今归零心态,再出发。#莫等闲,白了少年头#

技术交流+v:xxxyxsyy1234(和笔者一起努力,每日打卡) 2000+以面试官视角总结的考点,可与我共同打卡学习

公众号.jpg

原创文章&经验总结&从校招到A厂一路阳光一路沧桑

  1. synchronized简介

在学习知识前,我们先来看一个现象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class SynchronizedDemo implements Runnable {
private static int count = 0;

public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new SynchronizedDemo());
thread.start();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + count);
}

@Override
public void run() {
for (int i = 0; i < 1000000; i++)
count++;
}
}

开启了10个线程,每个线程都累加了1000000次,如果结果正确的话自然而然总数就应该是10 * 1000000 = 10000000。可就运行多次结果都不是这个数,而且每次运行结果都不一样。这是为什么了?有什么解决方案了?这就是我们今天要聊的事情。

在上一篇博文中我们已经了解了java内存模型的一些知识,并且已经知道出现线程安全的主要来源于JMM的设计,主要集中在主内存和线程的工作内存而导致的内存可见性问题,以及重排序导致的问题,进一步知道了happens-before规则。线程运行时拥有自己的栈空间,会在自己的栈空间运行,如果多线程间没有共享的数据也就是说多线程间并没有协作完成一件事情,那么,多线程就不能发挥优势,不能带来巨大的价值。那么共享数据的线程安全问题怎样处理?很自然而然的想法就是每一个线程依次去读写这个共享变量,这样就不会有任何数据安全的问题,因为每个线程所操作的都是当前最新的版本数据。那么,在java关键字synchronized就具有使每个线程依次排队操作共享变量的功能。很显然,这种同步机制效率很低,但synchronized是其他并发容器实现的基础,对它的理解也会大大提升对并发编程的感觉,从功利的角度来说,这也是面试高频的考点。好了,下面,就来具体说说这个关键字。

  1. synchronized实现原理

在java代码中使用synchronized可是使用在代码块和方法中,根据synchronized用的位置可以有如表3.1这些使用场景:

使用位置 作用范围 被锁的对象 示例代码
方法 实例方法 类的实例对象 public synchronized void method() { …….}
静态方法 类对象 public static synchronized void method1() { …….}
代码块 实例对象 类的实例对象 synchronized (this) { …….}
class对象 类对象 synchronized (SynchronizedScopeDemo.class) { …….}
任意实例对象object 实例对象object final String lock = “”;synchronized (lock) { …….}

如表所示synchronized可以用在方法上也可以使用在代码块中,方法是实例方法和静态方法分别锁的是该类的实例对象和该类的对象。而使用在代码块中根据锁的目标对象

也可以分为三种,具体的可以看表数据。这里的需要注意的是如果锁的是类对象的话,尽管new多个实例对象,依然会被锁住。synchronized的使用起来很简单,那么背后的原理以及实现机制是怎样的呢?

1 对象锁(monitor)机制

现在来进一步分析synchronized的具体底层实现,有如下一个简单的示例代码:

1
2
3
4
5
6
7
arduino复制代码public class SynchronizedDemo {
  public static void main(String[] args) {
      synchronized (SynchronizedDemo.class) {
          System.out.println("hello synchronized!");
      }
  }
}

上述代码通过synchronized“锁住”当前类对象来进行同步,将java代码进行编译之后通过javap -v SynchronizedDemo .class来查看对应的main方法字节码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
yaml复制代码public static void main(java.lang.String[]);

•   descriptor: ([Ljava/lang/String;)V
​
•   flags: ACC_PUBLIC, ACC_STATIC
​
•   Code:
​
•     stack=2, locals=3, args_size=1
​
•         0: ldc           #2                 // class com/codercc/chapter3/SynchronizedDemo
​
•         2: dup
​
•         3: astore_1
​
•         4: **monitorenter**
​
•         5: getstatic     #3                 // Field java/lang/System.out:Ljava/io/PrintStream;
​
•         8: ldc           #4                 // String hello synchronized!
​
•       10: invokevirtual #5                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
​
•       13: aload_1
​
•       14: monitorexit
​
•       15: **goto**         23
​
•       18: astore_2
​
•       19: aload_1
​
•       20: **monitorexit**
​
•       21: aload_2
​
•       22: **athrow**
​
•       23: **return

重要的字节码已经在原字节码文件中进行了标注,再进入到synchronized同步块中,需要通过monitorenter指令获取到对象的monitor(也通常称之为对象锁)后才能往下进行执行,在处理完对应的方法内部逻辑之后通过monitorexit指令来释放所持有的monitor,以供其他并发实体进行获取。代码后续执行到第15行goto语句进而继续到第23行return指令,方法成功执行退出。另外当方法异常的情况下,如果monitor不进行释放,对其他阻塞对待的并发实体来说就一直没有机会获取到了,系统会形成死锁状态很显然这样是不合理。

因此针对异常的情况,会执行到第20行指令通过monitorexit释放monitor锁,进一步通过第22行字节码athrow抛出对应的异常。从字节码指令分析也可以看出在使用synchronized是具备隐式加锁和释放锁的操作便利性的,并且针对异常情况也做了释放锁的处理。

每个对象都存在一个与之关联的monitor,线程对monitor持有的方式以及持有时机决定了synchronized的锁状态以及synchronized的状态升级方式。monitor是通过C++中ObjectMonitor实现,代码可以通过openjdk hotspot链接(hg.openjdk.java.net/jdk8u/jdk8u… )进行下载openjdk中hotspot版本的源码,具体文件路径在src\share\vm\runtime\objectMonitor.hpp,具体源码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
ini复制代码  // initialize the monitor, exception the semaphore, all other fields
​
// are simple integers or pointers
​
ObjectMonitor() {
​
•   _header       = NULL;
​
•   _count       = 0;
​
•   _waiters     = 0,
​
•   _recursions   = 0;
​
•   _object       = NULL;
​
•   _owner       = NULL;
​
•   **_WaitSet**     = NULL;
​
•   _WaitSetLock = 0 ;
​
•   _Responsible = NULL ;
​
•   _succ         = NULL ;
​
•   _cxq         = NULL ;
​
•   FreeNext     = NULL ;
​
•   **_EntryList**   = NULL ;
​
•   _SpinFreq     = 0 ;
​
•   _SpinClock   = 0 ;
​
•   OwnerIsThread = 0 ;
​
•   _previous_owner_tid = 0;
​
}

从ObjectMonitor的结构中可以看出主要维护WaitSet以及EntryList两个队列来保存ObjectWaiter 对象,当每个阻塞等待获取锁的线程都会被封装成ObjectWaiter对象来进行入队,与此同时如果获取到锁资源的话就会出队操作。另外_owner则指向当前持有ObjectMonitor对象的线程。等待获取锁以及获取锁出队的示意图如下图所示:

image.png

当多个线程进行获取锁的时候,首先都会进行_EntryList队列,其中一个线程获取到对象的monitor后,对monitor而言就会将_owner变量设置为当前线程,并且monitor维护的计数器就会加1。如果当前线程执行完逻辑并退出后,monitor中_owner变量就会清空并且计数器减1,这样就能让其他线程能够竞争到monitor。另外,如果调用了wait()方法后,当前线程就会进入到_WaitSet中等待被唤醒,如果被唤醒并且执行退出后,也会对状态量进行重置,也便于其他线程能够获取到monitor。

从线程状态变化的角度来看,如果要想进入到同步块或者执行同步方法,都需要先获取到对象的monitor,如果获取不到则会变更为BLOCKED状态,具体过程如下图所示:

image.png

从上图可以看出任意线程对Object的访问,首先要获得Object的monitor,如果获取失败,该线程就会进入到同步队列中,线程状态变为BLOCKED。当monitor持有者释放后,在同步队列中的线程才会有机会重新获取monitor,才能继续执行。

2 synchronized的happens-before关系

在第2章中分析过happens-before规则,其中有一条就是监视器锁规则:对同一个监视器的解锁happens-before于对该监视器的加锁。为了进一步了解synchronized的并发语义,通过示例代码分析这条happens-before规则,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码public class MonitorDemo {
  private int a = 0;
•
  public synchronized void writer() {     // 1
      a++;                               // 2
  }                                       // 3
•
  public synchronized void reader() {   // 4
      int i = a;                         // 5
  }                                     // 6
}

在并发时,第5步操作中读取到的变量a的值是多少呢?这就需要通过happens-before规则来进行分析,示例代码的happens-before关系如下图所示:

image.png

上图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,通过监视器锁规则可以推导出线程A释放锁happens-before线程B加锁,即红色线表示。蓝色的线则是通过传递性规则进一步推导的happens-before关系。最终得到的结论就是操作2 happens-before 5,通过这个关系可以得出什么?

根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见。那么在该示例代码中,线程A先对共享变量A进行加1,由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1。

3 锁获取和锁释放的内存语义

在第2章中总结果JMM核心为两个部分:happens-before规则以及内存抽象模型。在分析完synchronized的happens-before关系后还是不太完整的,接下来看看基于java内存抽象模型的synchronized的内存语义,具体过程如下图所示:

image.png

针对线程A的操作而言,从上图可以看出线程A会首先先从主内存中读取共享变量a=0的值然后将该变量拷贝到线程本地内存。然后基于该值进行数据操作后变量a变为1,然后会将值写入到主内存中。

image.png

对线程B而言执行流程如上图所示。线程B获取锁的时候会强制从主内存中共享变量a的值,而此时变量a已经是最新值了。接下来线程B会将该值拷贝到工作内存中进行操作,同样的执行完操作后也会重新写入到主内存中。

从横向来看,线程A和线程线程都是基于主内存中的共享变量互相感知到对方的数据操作,并基于共享变量来完成并发实体中的协同工作,整个过程就好像线程A给线程B发送了一个数据变更的“通知”,这种通信机制就是基于共享内存的并发模型结构导致。

通过上面的讨论对synchronized应该有一定了解,它最大的特征就是在同一时刻只有一个线程能够获得对象monitor,从而确保当前线程能够执行到相应的同步逻辑,对线程之间而言表现为互斥性(排它性) 。自然而然这种同步方式会有效率相对低下的弊端,既然同步流程不能发生改变,那么能不能让每次获取锁的速度更快或者降低阻塞等待的概率呢?也就是通过局部的优化来提升系统整体的并发同步的效率。比如去收银台付款的场景,之前的方式是大家都去排队,然后去纸币付款收银员找零。甚至有的时候付款的时候还需要在包里拿出钱包拿出钱,这个过程是比较耗时的。针对付款的流程,就可以通过线上化的手段来进行优化,在现在只需要通过支付宝扫描二维码就可以完成付款了,也省去了收银员找零的时间。尽管整个付款场景还是需要排队,但是因为付款(类似于获取锁释放锁)这个环节的优化导致耗时大大缩短,对收银台(系统整体并发效率)而言操作效率就极大的带来提升。如此类比,如果能对锁操作过程进行优化的话,也会对并发效率带来极大的提升。

那么,针对synchronized的优化是怎样做的呢?在进一步分析之前,需要先了解这两个概念:1. CAS操作;2.Java对象头。

3.1 CAS操作

3.1.1 什么是CAS?

使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用**CAS(compare and swap)**又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

3.1.2 CAS的操作过程

CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程

CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。

Synchronized VS CAS

元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。

3.1.3 CAS的应用场景

在J.U.C包中利用CAS实现类有很多,可以说是支撑起整个concurrency包的实现,在Lock实现中会有CAS改变state变量,在atomic包中的实现类也几乎都是用CAS实现,关于这些具体的实现场景在之后会详细聊聊,现在有个印象就好了(微笑脸)。

3.1.4 CAS的问题

1. ABA问题
因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。java这么优秀的语言,当然在java 1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路就是这样的。

2. 自旋时间过长

使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。

3. 只能保证一个共享变量的原子操作

当对一个共享变量执行操作时CAS能保证其原子性,如果对多个共享变量进行操作,CAS就不能保证其原子性。有一个解决方案是利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。然后将这个对象做CAS操作就可以保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。

3.2 Java对象头

在同步的时候是获取对象的monitor,即获取到对象的锁。那么对象的锁怎么理解?无非就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。32为JVM Mark Word默认存储结构为(注:java对象头以及下面的锁状态变化摘自《java并发编程的艺术》一书,该书我认为写的足够好,就没在自己组织语言班门弄斧了):

Mark Word存储结构

如图在Mark Word会默认存放hasdcode,年龄值以及锁标志位等信息。

Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。对象的MarkWord变化为下图:

Mark Word状态变化

3.2 偏向锁

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

偏向锁的获取

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程

偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

偏向锁撤销流程

如图,偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

下图线程1展示了偏向锁获取的过程,线程2展示了偏向锁撤销的过程。

偏向锁获取和撤销流程

如何关闭偏向锁

偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态

3.3 轻量级锁

加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。

轻量级锁加锁解锁以及锁膨胀

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

3.5 各种锁的比较

各种锁的对比

  1. 一个例子

经过上面的理解,我们现在应该知道了该怎样解决了。更正后的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class SynchronizedDemo implements Runnable {
private static int count = 0;

public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new SynchronizedDemo());
thread.start();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + count);
}

@Override
public void run() {
synchronized (SynchronizedDemo.class) {
for (int i = 0; i < 1000000; i++)
count++;
}
}
}

开启十个线程,每个线程在原值上累加1000000次,最终正确的结果为10X1000000=10000000,这里能够计算出正确的结果是因为在做累加操作时使用了同步代码块,这样就能保证每个线程所获得共享变量的值都是当前最新的值,如果不使用同步的话,就可能会出现A线程累加后,而B线程做累加操作有可能是使用原来的就值,即“脏值”。这样,就导致最终的计算结果不是正确的。而使用Syncnized就可能保证内存可见性,保证每个线程都是操作的最新值。这里只是一个示例性的demo,聪明的你,还有其他办法吗?

参考文献

《java并发编程的艺术》

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【译】 用 Java 代码实现区块链 用 Java 代码实现

发表于 2018-04-29
  • 原文地址:Blockchain Implementation With Java Code
  • 原文作者:David Pitt
  • 译文出自:掘金翻译计划
  • 本文永久链接:github.com/xitu/gold-m…
  • 译者:Starrier
  • 校对者:sisibeloved

用 Java 代码实现区块链

让我们来看看用 Java 代码实现区块链的可能性。我们从基本原理出发,开发一些代码来演示它们是如何融合在一起的。

比特币(Bitcoin)炙手可热 —— 多么的轻描淡写。虽然数字加密货币的前景尚不明确,但区块链 —— 用于驱动比特币的技术 —— 却非常流行。

区块链的应用领域尚未探索完毕。它也有可能会破坏企业自动化。关于区块链的工作原理,有很多可用的信息。我们有一个深度区块链的免费白皮书(无需注册)。

本文将重点介绍区块链体系结构,特别是通过简单的代码示例演示“不可变,仅附加”的分布式账本是如何工作的。

作为开发者,阅读代码会比阅读技术文章更容易理解。至少对我来说是这样。那么我们开始吧!

简述区块链

首先我们简要总结下区块链。区块包含一些头信息和任意一组数据类型或一组交易。该链从第一个(初始)区块开始。随着交易被添加/扩展,将基于区块中可以存储多少交易来创建新区块。

当超过区块阀值大小时,将创建一个新的交易区块。新区块与前一个区块连接,因此称为区块链。

不可变性

因为交易时会计算 SHA-256 哈希值,所以区块链是不可变的。区块链的内容也被哈希则提供了唯一的标识符。此外,相连的前一个区块的哈希也会被在区块的头信息中散列并储存。

这就是为什么试图篡改区块基本上是不可能的,至少以目前的计算能力是这样的。下面是一个展示区块属性的 Java 类的部分定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码...
public class Block&lt;T extends Tx>; {
public long timeStamp;
private int index;
private List<T> transactions = new ArrayList<T>();
private String hash;
private String previousHash;
private String merkleRoot;
private String nonce = "0000";

// 缓存事务用 SHA256 哈希
public Map<String,T> map = new HashMap<String,T>();
...

注意,注入的泛型类型为 Tx 类型。这允许交易数据发生变化。此外,previousHash 属性将引用前一个区块的哈希值。稍后将描述 merkleRoot 和 nonce 属性。

区块哈希值

每个区块可以计算一个哈希。这实际上是链接在一起的所有区块属性的哈希,包括前一个区块的哈希和由此计算而得的 SHA-256 哈希。

下面是在 Block.java 类中定义的计算哈希值的方法。

1
2
3
4
5
6
7
复制代码...
public void computeHash() {
    Gson parser = new Gson(); // 可能应该缓存这个实例
    String serializedData = parser.toJson(transactions);  
setHash(SHA256.generateHash(timeStamp + index + merkleRoot + serializedData + nonce + previousHash));
}
...

交易被序列化为 JSON 字符串,因此可以在哈希之前将其追加到块属性中。

链

区块链通过接受交易来管理区块。当到达预定阀值时,就创建一个区块。下面是 SimpleBlockChain.java 的部分实现:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码...
...
public class SimpleBlockchain<T extends Tx> {
public static final int BLOCK_SIZE = 10;
public List<Block<T>> chain = new ArrayList<Block<T>>();

public SimpleBlockchain() {
// 创建初始区块
chain.add(newBlock());
}

...

注意,chain 属性维护了一个类型为 Tx 的区块列表。此外,无参构造器 会在创建初始链表时初始化“初始”区块。下面是 newBlock() 方法源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码...
public Block<T> newBlock() {
int count = chain.size();
String previousHash = "root";

if (count > 0)
previousHash = blockChainHash();

Block<T> block = new Block<T>();

block.setTimeStamp(System.currentTimeMillis());
block.setIndex(count);
block.setPreviousHash(previousHash);
return block;
}
...

这个方法将会创建一个新的区块实例,产生合适的值,并分配前一个块的哈希(这将是链头的哈希),然后返回这个实例。

在将区块添加到链中之前,可以通过将新区块的上一个哈希与链的最后一个区块(头)进行比较来验证区块,以确保它们匹配。SimpleBlockchain.java 描述了这一过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码....
public void addAndValidateBlock(Block<T> block) {

// 比较之前的区块哈希,如果有效则添加
Block<T> current = block;
for (int i = chain.size() - 1; i >= 0; i--) {
Block<T> b = chain.get(i);
if (b.getHash().equals(current.getPreviousHash())) {
current = b;
} else {

throw new RuntimeException("Block Invalid");
}

}

this.chain.add(block);
}
...

整个区块链通过循环整个链来验证,确保区块的哈希仍然与前一个区块的哈希匹配。

以下是 SimpleBlockChain.java validate() 方法的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码...
public boolean validate() {

String previousHash = null;
for (Block<T> block : chain) {
String currentHash = block.getHash();
if (!currentHash.equals(previousHash)) {
return false;
}

previousHash = currentHash;

}

return true;

}
...

你可以看到,试图以任何方式伪造交易数据或任何其他属性都是非常困难的。而且,随着链的增长,它会继续变得非常、非常、非常困难,基本上是不可能的 —— 除非量子计算机可用!

添加交易

区块链技术的另一个重要技术点是它是分布式的。区块链只增的特性很好地帮助了它在区块链网络的节点之间的复制。节点通常以点对点的方式进行通信,就像比特币那样,但不一定非得是这种方式。其他区块链实现使用分散的方法,比如使用基于 HTTP 协议的 API。这都是题外话了。

交易可以代表任何东西。交易可以包含要执行的代码(例如,智能合约)或存储和追加有关某种业务交易的信息。

智能合约:旨在以数字形式来促进、验证或强制执行合约谈判及履行的计算机协议。

就比特币而言,交易包含所有者账户中的金额和其他账户的金额(例如,在账户之间转移比特币金额)。交易中还包括公钥和账户 ID,因此传输需要保证安全。但这是比特币特有的。

交易被添加到网络中并被池化;它们不在区块中或链本身中。

这是区块链共识机制发挥作用的地方。现在有许多经过验证的共识算法和模式,不过那已经超出了本文的范围。

挖矿是比特币区块链使用的共识机制。这就是下文讨论的共识类型。共识机制收集交易,用它们构建一个区块,然后将该区块添加到链中。区块链会在新的交易区块被添加之前验证它。

默克尔树

交易被哈希并添加到区块中。默克尔树被用来计算默克尔根哈希。默克尔树是一种内部节点的值是两个子节点值的哈希值的平衡二叉树。而默克尔根,就是默克尔树的根节点。


该树用于区块交易的验证。如果在交易中更改了一些信息,默克尔根将失效。此外,在分布式中,它们还可以加速传输区块,因为该结构只允许添加和验证整个交易区块所需的单个交易哈希分支。

以下是 Block.java 类中的方法,它从交易列表中创建了一个默克尔树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
复制代码...
public List<String> merkleTree() {
ArrayList<String> tree = new ArrayList<>();
// 首先,
// 将所有交易的哈希作为叶子节点添加到树中。
for (T t : transactions) {
tree.add(t.hash());
}
int levelOffset = 0; // 当前处理的列表中的偏移量。
// 当前层级的第一个节点在整个列表中的偏移量。
// 每处理完一层递增,
// 当我们到达根节点时(levelSize == 1)停止。
for (int levelSize = transactions.size(); levelSize > 1; levelSize = (levelSize + 1) / 2) {
// 对于该层上的每一对节点:
for (int left = 0; left < levelSize; left += 2) {
// 在我们没有足够交易的情况下,
// 右节点和左节点
// 可以一样。
int right = Math.min(left + 1, levelSize - 1);
String tleft = tree.get(levelOffset + left);
String tright = tree.get(levelOffset + right);
tree.add(SHA256.generateHash(tleft + tright));
}
// 移动至下一层
levelOffset += levelSize;
}
return tree;
}

...

此方法用于计算区块的默克尔树根。伴随项目有一个默克尔树单元测试,它试图将交易添加到一个区块中,并验证默克尔根是否已经更改。下面是单元测试的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
复制代码...
@Test
public void merkleTreeTest() {

// 创建链,添加交易

SimpleBlockchain<Transaction> chain1 = new SimpleBlockchain<Transaction>();

chain1.add(new Transaction("A")).add(new Transaction("B")).add(new Transaction("C")).add(new Transaction("D"));

// 获取链中的区块
Block<Transaction> block = chain1.getHead();

System.out.println("Merkle Hash tree :" + block.merkleTree());

//从区块中获取交易
Transaction tx = block.getTransactions().get(0);

// 查看区块交易是否有效,它们应该是有效的
block.transasctionsValid();
assertTrue(block.transasctionsValid());

// 更改交易数据
tx.setValue("Z");

//当区块的默克尔根与计算出来的默克尔树不匹配时,区块不应该是有效。
assertFalse(block.transasctionsValid());

}

...

此单元测试模拟验证交易,然后通过共识机制之外的方法改变区块中的交易,例如,如果有人试图更改交易数据。

记住,区块链是只增的,当块区链数据结构在节点之间共享时,区块数据结构(包括默克尔根)被哈希并连接到其他区块。所有节点都可以验证新的区块,并且现有的区块可以很容易地被证明是有效的。因此,如果一个挖矿者想要添加一个伪造的区块或者节点来调整原有的交易是不可能的。

挖矿和工作量证明

在比特币世界中,将交易组合成区块,然后提交给链中的成员进行验证的过程叫做“挖矿”。

更宽泛地说,在区块链中,这被称为共识。现在有好几种经过验证的分布式共识算法,使用哪种机制取决于你有一个公共的还是私有的区块链。我们的白皮书对此进行了更为深入的描述,但本文的重点是区块链的原理,因此这个例子中我们将使用一个工作量证明(POW)的共识机制。

因此,挖掘节点将侦听由区块链执行的交易,并执行一个简单的数学任务。这个任务是用一个不断改变的一次性随机数(nonce)来生成带有一连串以 0 开头的区块哈希值,直到一个预设的哈希值被找到。

Java 示例项目有一个 Miner.java 类,其中的 proofOfWork(Block block) 方法实现如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码private String proofOfWork(Block block) {

String nonceKey = block.getNonce();
long nonce = 0;
boolean nonceFound = false;
String nonceHash = "";

Gson parser = new Gson();
String serializedData = parser.toJson(transactionPool);
String message = block.getTimeStamp() + block.getIndex() + block.getMerkleRoot() + serializedData
+ block.getPreviousHash();

while (!nonceFound) {

nonceHash = SHA256.generateHash(message + nonce);
nonceFound = nonceHash.substring(0, nonceKey.length()).equals(nonceKey);
nonce++;

}

return nonceHash;

}

同样,这是简化的,但是一旦收到一定量的交易,这个挖矿算法会为区块计算一个工作量证明的哈希。该算法简单地循环并创建块的SHA-256散列,直到产生前导数字哈希。

这可能需要很多时间,这就是为什么特定的GPU微处理器已经被实现来尽可能快地执行和解决这个问题的原因。

单元测试

你可以在 GitHub上看到结合了这些概念的 Java 示例的 JUnit 测试。


运行一下,看看这个简单的区块链是如何工作的。

另外,如果你是 C# 程序员的话,其实(我不会告诉任何人),我们也有用 C# 写的示例。下面是 C# 区块链实现的示例。

最后的思考

希望这篇文章能让你对区块链技术有一定的了解,并有充足的兴趣继续研究下去。

本文介绍的所有示例都用于我们的深度区块链白皮书 (无需注册即可阅读). 这些例子在白皮书中有更详细的说明。

另外,如果你想在 Java 中看到完整的区块链实现,这里有一个开源项目 BitcoinJ 的链接。你可以看到上文的概念在实际生产中一一实现。

如果是这样的话,推荐你看看更贴近生产的开源区块链框架。一个很好的示例是 HyperLedger Fabric,这将是我下一篇文章的主题 —— 请持续关注!


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

git基本操作,一篇文章就够了! 1 git简介 2 g

发表于 2018-04-25

#莫等闲,白了少年头#

本人从毕业开始一直在一线互联网大厂工作,现任技术TL,出版过《深入理解Java并发》一书,折腾过技术开源项目,并长期作为面试官参与面试,深谙双方的诉求与技术沟通。如今归零心态,再出发。#莫等闲,白了少年头#

技术交流+v:xxxyxsyy1234(一起努力,每日打卡)

公众号.jpg

加入专栏:juejin.cn/column/7326…

原创文章&经验总结&从校招到A厂一路阳光一路沧桑

详情请戳www.codercc.com

  1. git简介

在实际开发中,会使用git作为版本控制工具来完成团队协作。因此,对基本的git操作指令进行总结是十分有必要的,本文对一些术语或者理论基础,不重新码字,可以参考廖雪峰老师的博文,本文只对命令做归纳总结。

git的通用操作流程如下图(来源于网络)

git操作通用流程

主要涉及到四个关键点:

  1. 工作区:本地电脑存放项目文件的地方,比如learnGitProject文件夹;
  2. 暂存区(Index/Stage):在使用git管理项目文件的时候,其本地的项目文件会多出一个.git的文件夹,将这个.git文件夹称之为版本库。其中.git文件夹中包含了两个部分,一个是暂存区(Index或者Stage),顾名思义就是暂时存放文件的地方,通常使用add命令将工作区的文件添加到暂存区里;
  3. 本地仓库:.git文件夹里还包括git自动创建的master分支,并且将HEAD指针指向master分支。使用commit命令可以将暂存区中的文件添加到本地仓库中;
  4. 远程仓库:不是在本地仓库中,项目代码在远程git服务器上,比如项目放在github上,就是一个远程仓库,通常使用clone命令将远程仓库拷贝到本地仓库中,开发后推送到远程仓库中即可;

更细节的来看:

git几个核心区域间的关系

日常开发时代码实际上放置在工作区中,也就是本地的XXX.java这些文件,通过add等这些命令将代码文教提交给暂存区(Index/Stage),也就意味着代码全权交给了git进行管理,之后通过commit等命令将暂存区提交给master分支上,也就是意味打了一个版本,也可以说代码提交到了本地仓库中。另外,团队协作过程中自然而然还涉及到与远程仓库的交互。

因此,经过这样的分析,git命令可以分为这样的逻辑进行理解和记忆:

  1. git管理配置的命令;

几个核心存储区的交互命令:
2. 工作区与暂存区的交互;
3. 暂存区与本地仓库(分支)上的交互;
4. 本地仓库与远程仓库的交互。

  1. git配置命令

查询配置信息

  1. 列出当前配置:git config --list;
  2. 列出repository配置:git config --local --list;
  3. 列出全局配置:git config --global --list;
  4. 列出系统配置:git config --system --list;

第一次使用git,配置用户信息

  1. 配置用户名:git config --global user.name "your name";
  2. 配置用户邮箱:git config --global user.email "youremail@github.com";

其他配置

  1. 配置解决冲突时使用哪种差异分析工具,比如要使用vimdiff:git config --global merge.tool vimdiff;
  2. 配置git命令输出为彩色的:git config --global color.ui auto;
  3. 配置git使用的文本编辑器:git config --global core.editor vi;
  1. 工作区上的操作命令

新建仓库

  1. 将工作区中的项目文件使用git进行管理,即创建一个新的本地仓库:git init;
  2. 从远程git仓库复制项目:git clone <url>,如:git clone git://github.com/wasd/example.git;克隆项目时如果想定义新的项目名,可以在clone命令后指定新的项目名:git clone git://github.com/wasd/example.git mygit;

提交

  1. 提交工作区所有文件到暂存区:git add .
  2. 提交工作区中指定文件到暂存区:git add <file1> <file2> ...;
  3. 提交工作区中某个文件夹中所有文件到暂存区:git add [dir];

撤销

  1. 删除工作区文件,并且也从暂存区删除对应文件的记录:git rm <file1> <file2>;
  2. 从暂存区中删除文件,但是工作区依然还有该文件:git rm --cached <file>;
  3. 取消暂存区已经暂存的文件:git reset HEAD <file>...;
  4. 撤销上一次对文件的操作:git checkout --<file>。要确定上一次对文件的修改不再需要,如果想保留上一次的修改以备以后继续工作,可以使用stashing和分支来处理;
  5. 隐藏当前变更,以便能够切换分支:git stash;
  6. 查看当前所有的储藏:git stash list;
  7. 应用最新的储藏:git stash apply,如果想应用更早的储藏:git stash apply stash@{2};重新应用被暂存的变更,需要加上--index参数:git stash apply --index;
  8. 使用apply命令只是应用储藏,而内容仍然还在栈上,需要移除指定的储藏:git stash drop stash{0};如果使用pop命令不仅可以重新应用储藏,还可以立刻从堆栈中清除:git stash pop;
  9. 在某些情况下,你可能想应用储藏的修改,在进行了一些其他的修改后,又要取消之前所应用储藏的修改。Git没有提供类似于 stash unapply 的命令,但是可以通过取消该储藏的补丁达到同样的效果:git stash show -p stash@{0} | git apply -R;同样的,如果你沒有指定具体的某个储藏,Git 会选择最近的储藏:git stash show -p | git apply -R;

更新文件

  1. 重命名文件,并将已改名文件提交到暂存区:git mv [file-original] [file-renamed];

查新信息

  1. 查询当前工作区所有文件的状态:git status;
  2. 比较工作区中当前文件和暂存区之间的差异,也就是修改之后还没有暂存的内容:git diff;指定文件在工作区和暂存区上差异比较:git diff <file-name>;
  1. 暂存区上的操作命令

提交文件到版本库

  1. 将暂存区中的文件提交到本地仓库中,即打上新版本:git commit -m "commit_info";
  2. 将所有已经使用git管理过的文件暂存后一并提交,跳过add到暂存区的过程:git commit -a -m "commit_info";
  3. 提交文件时,发现漏掉几个文件,或者注释写错了,可以撤销上一次提交:git commit --amend;

查看信息

  1. 比较暂存区与上一版本的差异:git diff --cached;
  2. 指定文件在暂存区和本地仓库的不同:git diff <file-name> --cached;
  3. 查看提交历史:git log;参数-p展开每次提交的内容差异,用-2显示最近的两次更新,如git log -p -2;

打标签

Git 使用的标签有两种类型:轻量级的(lightweight)和含附注的(annotated)。轻量级标签就像是个不会变化的分支,实际上它就是个指向特定提交对象的引用。而含附注标签,实际上是存储在仓库中的一个独立对象,它有自身的校验和信息,包含着标签的名字,电子邮件地址和日期,以及标签说明,标签本身也允许使用 GNU Privacy Guard (GPG) 来签署或验证。一般我们都建议使用含附注型的标签,以便保留相关信息;当然,如果只是临时性加注标签,或者不需要旁注额外信息,用轻量级标签也没问题。

  1. 列出现在所有的标签:git tag;
  2. 使用特定的搜索模式列出符合条件的标签,例如只对1.4.2系列的版本感兴趣:git tag -l "v1.4.2.*";
  3. 创建一个含附注类型的标签,需要加-a参数,如git tag -a v1.4 -m "my version 1.4";
  4. 使用git show命令查看相应标签的版本信息,并连同显示打标签时的提交对象:git show v1.4;
  5. 如果有自己的私钥,可以使用GPG来签署标签,只需要在命令中使用-s参数:git tag -s v1.5 -m "my signed 1.5 tag";
  6. 验证已签署的标签:git tag -v ,如git tag -v v1.5;
  7. 创建一个轻量级标签的话,就直接使用git tag命令即可,连-a,-s以及-m选项都不需要,直接给出标签名字即可,如git tag v1.5;
  8. 将标签推送到远程仓库中:git push origin ,如git push origin v1.5;
  9. 将本地所有的标签全部推送到远程仓库中:git push origin --tags;

分支管理

  1. 创建分支:git branch <branch-name>,如git branch testing;
  2. 从当前所处的分支切换到其他分支:git checkout <branch-name>,如git checkout testing;
  3. 新建并切换到新建分支上:git checkout -b <branch-name>;
  4. 删除分支:git branch -d <branch-name>;
  5. 将当前分支与指定分支进行合并:git merge <branch-name>;
  6. 显示本地仓库的所有分支:git branch;
  7. 查看各个分支最后一个提交对象的信息:git branch -v;
  8. 查看哪些分支已经合并到当前分支:git branch --merged;
  9. 查看当前哪些分支还没有合并到当前分支:git branch --no-merged;
  10. 把远程分支合并到当前分支:git merge <remote-name>/<branch-name>,如git merge origin/serverfix;如果是单线的历史分支不存在任何需要解决的分歧,只是简单的将HEAD指针前移,所以这种合并过程可以称为快进(Fast forward),而如果是历史分支是分叉的,会以当前分叉的两个分支作为两个祖先,创建新的提交对象;如果在合并分支时,遇到合并冲突需要人工解决后,再才能提交;
  11. 在远程分支的基础上创建新的本地分支:git checkout -b <branch-name> <remote-name>/<branch-name>,如git checkout -b serverfix origin/serverfix;
  12. 从远程分支checkout出来的本地分支,称之为跟踪分支。在跟踪分支上向远程分支上推送内容:git push。该命令会自动判断应该向远程仓库中的哪个分支推送数据;在跟踪分支上合并远程分支:git pull;
  13. 将一个分支里提交的改变移到基底分支上重放一遍:git rebase <rebase-branch> <branch-name>,如git rebase master server,将特性分支server提交的改变在基底分支master上重演一遍;使用rebase操作最大的好处是像在单个分支上操作的,提交的修改历史也是一根线;如果想把基于一个特性分支上的另一个特性分支变基到其他分支上,可以使用--onto操作:git rebase --onto <rebase-branch> <feature branch> <sub-feature-branch>,如git rebase --onto master server client;使用rebase操作应该遵循的原则是:一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行rebase操作;

5.本地仓库上的操作

  1. 查看本地仓库关联的远程仓库:git remote;在克隆完每个远程仓库后,远程仓库默认为origin;加上-v的参数后,会显示远程仓库的url地址;
  2. 添加远程仓库,一般会取一个简短的别名:git remote add [remote-name] [url],比如:git remote add example git://github.com/example/example.git;
  3. 从远程仓库中抓取本地仓库中没有的更新:git fetch [remote-name],如git fetch origin;使用fetch只是将远端数据拉到本地仓库,并不自动合并到当前工作分支,只能人工合并。如果设置了某个分支关联到远程仓库的某个分支的话,可以使用git pull来拉去远程分支的数据,然后将远端分支自动合并到本地仓库中的当前分支;
  4. 将本地仓库某分支推送到远程仓库上:git push [remote-name] [branch-name],如git push origin master;如果想将本地分支推送到远程仓库的不同名分支:git push <remote-name> <local-branch>:<remote-branch>,如git push origin serverfix:awesomebranch;如果想删除远程分支:git push [romote-name] :<remote-branch>,如git push origin :serverfix。这里省略了本地分支,也就相当于将空白内容推送给远程分支,就等于删掉了远程分支。
  5. 查看远程仓库的详细信息:git remote show origin;
  6. 修改某个远程仓库在本地的简称:git remote rename [old-name] [new-name],如git remote rename origin org;
  7. 移除远程仓库:git remote rm [remote-name];
  1. 忽略文件.gitignore

一般我们总会有些文件无需纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。我们可以创建一个名为 .gitignore 的文件,列出要忽略的文件模式。如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码# 此为注释 – 将被 Git 忽略
# 忽略所有 .a 结尾的文件
*.a
# 但 lib.a 除外
!lib.a
# 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
/TODO
# 忽略 build/ 目录下的所有文件
build/
# 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
doc/*.txt
# 忽略 doc/ 目录下所有扩展名为 txt 的文件
doc/**/*.txt

参考资料

非常详细准确的git学习资料;

git-cheat-sheet中文版

命令总结,资料一般,不够详细,作参考

常用命令很全

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

iOS底层原理总结 - RunLoop

发表于 2018-04-25

面试题

  1. 讲讲 RunLoop,项目中有用到吗?
  2. RunLoop内部实现逻辑?
  3. Runloop和线程的关系?
  4. timer 与 Runloop 的关系?
  5. 程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
  6. Runloop 是怎么响应用户操作的, 具体流程是什么样的?
  7. 说说RunLoop的几种状态?
  8. Runloop的mode作用是什么?

一. RunLoop简介

运行循环,在程序运行过程中循环做一些事情,如果没有Runloop程序执行完毕就会立即退出,如果有Runloop程序会一直运行,并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能。

二. RunLoop基本作用:

  1. 保持程序持续运行,程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行
  2. 处理App中的各种事件(比如:触摸事件,定时器事件,Selector事件等)
  3. 节省CPU资源,提高程序性能,程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CPU,现在没有事情做,我要去休息,这时CPU就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情

我们先通过API内一张图片来简单看一下RunLoop内部运行原理
RunLoop内部运行原理

通过图片可以看出,RunLoop在跑圈过程中,当接收到Input sources 或者 Timer sources时就会交给对应的处理方去处理。当没有事件消息传入的时候,RunLoop就休息了。这里只是简单的理解一下这张图,接下来我们来了解RunLoop对象和其一些相关类,来更深入的理解RunLoop运行流程。

三. RunLoop在哪里开启

UIApplicationMain函数内启动了Runloop,程序不会马上退出,而是保持运行状态。因此每一个应用必须要有一个runloop,
我们知道主线程一开起来,就会跑一个和主线程对应的RunLoop,那么RunLoop一定是在程序的入口main函数中开启。

1
2
3
4
5
复制代码int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

进入UIApplicationMain

1
复制代码UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);

我们发现它返回的是一个int数,那么我们对main函数做一些修改

1
2
3
4
5
6
7
8
复制代码int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"开始");
int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"结束");
return re;
}
}

运行程序,我们发现只会打印开始,并不会打印结束,这说明在UIApplicationMain函数中,开启了一个和主线程相关的RunLoop,导致UIApplicationMain不会返回,一直在运行中,也就保证了程序的持续运行。
我们来看到RunLoop的源码

1
2
3
4
5
6
7
8
复制代码// 用DefaultMode启动
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

我们发现RunLoop确实是do while通过判断result的值实现的。因此,我们可以把RunLoop看成一个死循环。如果没有RunLoop,UIApplicationMain函数执行完毕之后将直接返回,也就没有程序持续运行一说了。

四. RunLoop对象

Fundation框架 (基于CFRunLoopRef的封装)
NSRunLoop对象

CoreFoundation
CFRunLoopRef对象

因为Fundation框架是基于CFRunLoopRef的一层OC封装,这里我们主要研究CFRunLoopRef源码

如何获得RunLoop对象

1
2
3
4
5
6
7
复制代码Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

五. RunLoop和线程间的关系

  1. 每条线程都有唯一的一个与之对应的RunLoop对象
  2. RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  3. 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
  4. RunLoop在第一次获取时创建,在线程结束时销毁

通过源码查看上述对应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
复制代码// 拿到当前Runloop 调用_CFRunLoopGet0
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}

// 查看_CFRunLoopGet0方法内部
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 根据传入的主线程获取主线程对应的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主线程 将主线程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}

// 从字典里面拿,将线程作为key从字典里获取一个loop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);

// 如果loop为空,则创建一个新的loop,所以runloop会在第一次获取的时候创建
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));

// 创建好之后,以线程为key runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}

从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个 Dictionary 里。所以我们创建子线程RunLoop时,只需在子线程中获取当前线程的RunLoop对象即可[NSRunLoop currentRunLoop];如果不获取,那子线程就不会创建与之相关联的RunLoop,并且只能在一个线程的内部获取其 RunLoop
[NSRunLoop currentRunLoop];方法调用时,会先看一下字典里有没有存子线程相对用的RunLoop,如果有则直接返回RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。当线程结束时,RunLoop会被销毁。

六. RunLoop结构体

通过源码我们找到__CFRunLoop结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};

除一些记录性属性外,主要来看一下以下两个成员变量

1
2
复制代码CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;

CFRunLoopModeRef 其实是指向__CFRunLoopMode结构体的指针,__CFRunLoopMode结构体源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
复制代码typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};

主要查看以下成员变量

1
2
3
4
复制代码CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;

通过上面分析我们知道,CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer,而RunLoop启动时只能选择其中一个Mode作为currentMode。

Source1/Source0/Timers/Observer分别代表什么

1. Source1 : 基于Port的线程间通信

2. Source0 : 触摸事件,PerformSelectors

我们通过代码验证一下

1
2
3
4
复制代码- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"点击了屏幕");
}

打断点之后打印堆栈信息,当xcode工具区打印的堆栈信息不全时,可以在控制台通过“bt”指令打印完整的堆栈信息,由堆栈信息中可以发现,触摸事件确实是会触发Source0事件。

touchesBegan堆栈信息

同样的方式验证performSelector堆栈信息

1
2
3
复制代码dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
});

可以发现PerformSelectors同样是触发Source0事件

performSelector堆栈信息

其实,当我们触发了事件(触摸/锁屏/摇晃等)后,由IOKit.framework生成一个 IOHIDEvent事件,而IOKit是苹果的硬件驱动框架,由它进行底层接口的抽象封装与系统进行交互传递硬件感应的事件,并专门处理用户交互设备,由IOHIDServices和IOHIDDisplays两部分组成,其中IOHIDServices是专门处理用户交互的,它会将事件封装成IOHIDEvents对象,接着用mach port转发给需要的App进程,随后 Source1就会接收IOHIDEvent,之后再回调__IOHIDEventSystemClientQueueCallback(),__IOHIDEventSystemClientQueueCallback()内触发Source0,Source0 再触发 _UIApplicationHandleEventQueue()。所以触摸事件看到是在 Source0 内的。

3. Timers : 定时器,NSTimer

通过代码验证

1
2
3
复制代码[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"NSTimer ---- timer调用了");
}];

打印完整堆栈信息

Timer对战信息

4. Observer : 监听器,用于监听RunLoop的状态

七. 详解RunLoop相关类及作用

通过上面的分析,我们对RunLoop内部结构有了大致的了解,接下来来详细分析RunLoop的相关类。以下为Core Foundation中关于RunLoop的5个类

CFRunLoopRef - 获得当前RunLoop和主RunLoop

CFRunLoopModeRef - RunLoop 运行模式,只能选择一种,在不同模式中做不同的操作

CFRunLoopSourceRef - 事件源,输入源

CFRunLoopTimerRef - 定时器时间

CFRunLoopObserverRef - 观察者

1. CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的运行模式
一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source、Timer、Observer
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
如果需要切换Mode,只能退出RunLoop,再重新指定一个Mode进入,这样做主要是为了分隔开不同组的Source、Timer、Observer,让其互不影响。如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出

如图所示:

CFRunLoopModeRef示意图

注意:一种Mode中可以有多个Source(事件源,输入源,基于端口事件源例键盘触摸等) Observer(观察者,观察当前RunLoop运行状态) 和Timer(定时器事件源)。但是必须至少有一个Source或者Timer,因为如果Mode为空,RunLoop运行到空模式不会进行空转,就会立刻退出。

系统默认注册的5个Mode:

RunLoop 有五种运行模式,其中常见的有1.2两种

1
2
3
4
5
复制代码1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode

Mode间的切换

我们平时在开发中一定遇到过,当我们使用NSTimer每一段时间执行一些事情时滑动UIScrollView,NSTimer就会暂停,当我们停止滑动以后,NSTimer又会重新恢复的情况,我们通过一段代码来看一下

代码中的注释也很重要,展示了我们探索的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
// 加入到RunLoop中才可以运行
// 1. 把定时器添加到RunLoop中,并且选择默认运行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode
// [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 当textFiled滑动的时候,timer失效,停止滑动时,timer恢复
// 原因:当textFiled滑动的时候,RunLoop的Mode会自动切换成UITrackingRunLoopMode模式,因此timer失效,当停止滑动,RunLoop又会切换回NSDefaultRunLoopMode模式,因此timer又会重新启动了

// 2. 当我们将timer添加到UITrackingRunLoopMode模式中,此时只有我们在滑动textField时timer才会运行
// [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

// 3. 那个如何让timer在两个模式下都可以运行呢?
// 3.1 在两个模式下都添加timer 是可以的,但是timer添加了两次,并不是同一个timer
// 3.2 使用站位的运行模式 NSRunLoopCommonModes标记,凡是被打上NSRunLoopCommonModes标记的都可以运行,下面两种模式被打上标签
//0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"}
//2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"}
// 因此也就是说如果我们使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode两种模式下运行
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"%@",[NSRunLoop mainRunLoop]);
}
-(void)show
{
NSLog(@"-------");
}

由上述代码可以看出,NSTimer不管用是因为Mode的切换,因为如果我们在主线程使用定时器,此时RunLoop的Mode为kCFRunLoopDefaultMode,即定时器属于kCFRunLoopDefaultMode,那么此时我们滑动ScrollView时,RunLoop的Mode会切换到UITrackingRunLoopMode,因此在主线程的定时器就不在管用了,调用的方法也就不再执行了,当我们停止滑动时,RunLoop的Mode切换回kCFRunLoopDefaultMode,所以NSTimer就又管用了。

同样道理的还有ImageView的显示,我们直接来看代码,不再赘述了

1
2
3
4
5
6
7
复制代码-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
// performSelector默认是在default模式下运行,因此在滑动ScrollView时,图片不会加载
// [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 ];
// inModes: 传入Mode数组
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];

使用GCD也可是创建计时器,而且更为精确我们来看一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
复制代码-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//1.创建一个GCD定时器
/*
第一个参数:表明创建的是一个定时器
第四个参数:队列
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 需要对timer进行强引用,保证其不会被释放掉,才会按时调用block块
// 局部变量,让指针强引用
self.timer = timer;
//2.设置定时器的开始时间,间隔时间,精准度
/*
第1个参数:要给哪个定时器设置
第2个参数:开始时间
第3个参数:间隔时间
第4个参数:精准度 一般为0 在允许范围内增加误差可提高程序的性能
GCD的单位是纳秒 所以要*NSEC_PER_SEC
*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);

//3.设置定时器要执行的事情
dispatch_source_set_event_handler(timer, ^{
NSLog(@"---%@--",[NSThread currentThread]);
});
// 启动
dispatch_resume(timer);
}

2. CFRunLoopSourceRef事件源(输入源)

Source分为两种

Source0:非基于Port的 用于用户主动触发的事件(点击button 或点击屏幕)

Source1:基于Port的 通过内核和其他线程相互发送消息(与内核相关)

触摸事件及PerformSelectors会触发Source0事件源在前文已经验证过,这里不在赘述

3. CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变

我们直接来看代码,给RunLoop添加监听者,监听其运行状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
复制代码-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//创建监听者
/*
第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配
第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态
第三个参数 Boolean repeats:YES:持续监听 NO:不持续
第四个参数 CFIndex order:优先级,一般填0即可
第五个参数 :回调 两个参数observer:监听者 activity:监听的事件
*/
/*
所有事件
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要处理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要处理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒来了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;

default:
break;
}
});

// 给RunLoop添加监听者
/*
第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop
第二个参数 CFRunLoopObserverRef observer 监听者
第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
/*
CF的内存管理(Core Foundation)
凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
GCD本来在iOS6.0之前也是需要我们释放的,6.0之后GCD已经纳入到了ARC中,所以我们不需要管了
*/
CFRelease(observer);
}

我们来看一下输出

监听者监听RunLoop运行状态

以上可以看出,Observer确实用来监听RunLoop的状态,包括唤醒,休息,以及处理各种事件。
八. RunLoop处理逻辑


这时我们再来分析RunLoop的处理逻辑,就会简单明了很多,现在回头看官方文档RunLoop的处理逻辑,对RunLoop的处理逻辑有新的认识。

官方文档RunLoop处理逻辑

源码解析

下面源码仅保留了主流程代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
复制代码// 共外部调用的公开的CFRunLoopRun方法,其内部会调用CFRunLoopRunSpecific
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

// 经过精简的 CFRunLoopRunSpecific 函数代码,其内部会调用__CFRunLoopRun函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */

// 通知Observers : 进入Loop
// __CFRunLoopDoObservers内部会调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
函数
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);

// 核心的Loop逻辑
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);

// 通知Observers : 退出Loop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

return result;
}

// 精简后的 __CFRunLoopRun函数,保留了主要代码
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 通知Observers:即将处理Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

// 通知Observers:即将处理Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);

// 处理Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}

// 如果有Sources1,就跳转到handle_msg标记处
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}

// 通知Observers:即将休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);

// 进入休眠,等待其他消息唤醒
__CFRunLoopSetSleeping(rl);
__CFPortSetInsert(dispatchPort, waitSet);
do {
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
} while (1);

// 醒来
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopUnsetSleeping(rl);

// 通知Observers:已经唤醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

handle_msg: // 看看是谁唤醒了RunLoop,进行相应的处理
if (被Timer唤醒的) {
// 处理Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
}
else if (被GCD唤醒的) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被Sources1唤醒的
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
}

// 执行Blocks
__CFRunLoopDoBlocks(rl, rlm);

// 根据之前的执行结果,来决定怎么做,为retVal赋相应的值
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}

} while (0 == retVal);

return retVal;
}

上述源代码中,相应处理事件函数内部还会调用更底层的函数,内部调用才是真正处理事件的函数,通过上面bt打印全部堆栈信息也可以得到验证。

__CFRunLoopDoObservers 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

__CFRunLoopDoBlocks 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__

__CFRunLoopDoSources0 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

__CFRunLoopDoTimers 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

GCD 调用 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__

__CFRunLoopDoSource1 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

RunLoop处理逻辑流程图

此时我们按照源码重新整理一下RunLoop处理逻辑就会很清晰

RunLoop处理逻辑

九. RunLoop退出

  1. 主线程销毁RunLoop退出
  2. Mode中有一些Timer 、Source、 Observer,这些保证Mode不为空时保证RunLoop没有空转并且是在运行的,当Mode中为空的时候,RunLoop会立刻退出
  3. 我们在启动RunLoop的时候可以设置什么时候停止
1
2
复制代码[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>

十. RunLoop应用

1. 常驻线程

常驻线程的作用:我们知道,当子线程中的任务执行完毕之后就被销毁了,那么如果我们需要开启一个子线程,在程序运行过程中永远都存在,那么我们就会面临一个问题,如何让子线程永远活着,这时就要用到常驻线程:给子线程开启一个RunLoop
注意:子线程执行完操作之后就会立即释放,即使我们使用强引用引用子线程使子线程不被释放,也不能给子线程再次添加操作,或者再次开启。
子线程开启RunLoop的代码,先点击屏幕开启子线程并开启子线程RunLoop,然后点击button。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
复制代码#import "ViewController.h"

@interface ViewController ()
@property(nonatomic,strong)NSThread *thread;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 创建子线程并开启
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
self.thread = thread;
[thread start];
}
-(void)show
{
// 注意:打印方法一定要在RunLoop创建开始运行之前,如果在RunLoop跑起来之后打印,RunLoop先运行起来,已经在跑圈了就出不来了,进入死循环也就无法执行后面的操作了。
// 但是此时点击Button还是有操作的,因为Button是在RunLoop跑起来之后加入到子线程的,当Button加入到子线程RunLoop就会跑起来
NSLog(@"%s",__func__);
// 1.创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入
// 添加Source [NSMachPort port] 添加一个端口
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 添加一个Timer
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//创建监听者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要处理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要处理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒来了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;

default:
break;
}
});
// 给RunLoop添加监听者
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 2.子线程需要开启RunLoop
[[NSRunLoop currentRunLoop]run];
CFRelease(observer);
}
- (IBAction)btnClick:(id)sender {
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)test
{
NSLog(@"%@",[NSThread currentThread]);
}
@end

注意:创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入,如果没有加入Timer或者Source,或者只加入一个监听者,运行程序会崩溃

2. 自动释放池

Timer和Source也是一些变量,需要占用一部分存储空间,所以要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大,这显然不是我们想要的。
那么什么时候释放,怎么释放呢?
RunLoop内部有一个自动释放池,当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当RunLoop被唤醒重新开始跑圈时,Timer,Source等新的事件就会放到新的自动释放池中,当RunLoop退出的时候也会被释放。
注意:只有主线程的RunLoop会默认启动。也就意味着会自动创建自动释放池,子线程需要在线程调度方法中手动添加自动释放池。

1
2
3
复制代码@autorelease{  
// 执行代码
}

NSTimer、ImageView显示、PerformSelector等在上面已经有过例子,这里不再赘述。

最后检验一下自己

文章开头的面试题,在文中都可以找到答案,这里不在赘述了。

文献资料

苹果官方文档

CFRunLoopRef源码


文中如果有不对的地方欢迎指出。我是xx_cc,一只长大很久但还没有二够的家伙。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java锁机制了解一下 前言 一、synchronized锁

发表于 2018-04-24

前言

回顾前面:

  • 多线程三分钟就可以入个门了!
  • Thread源码剖析
  • 多线程基础必要知识点!看了学习多线程事半功倍

只有光头才能变强!

本文章主要讲的是Java多线程加锁机制,有两种:

  • Synchronized
  • 显式Lock

不得不唠叨几句:

  • 在《Java核心技术卷 一》是先讲比较难的显式Lock,而再讲的是比较简单的Synchronized
  • 而《Java并发编程实战》在前4章零散地讲解了Synchronized,将显式Lock放到了13章

其实都比较坑,如果能先系统讲了Synchronized锁机制,接着讲显式Lock锁机制,那就很容易理解了。也不需要跨那么多章节。

那么接下来我们就开始吧~

一、synchronized锁

1.1synchronized锁是什么?

synchronized是Java的一个关键字,它能够将代码块(方法)锁起来

  • 它使用起来是非常简单的,只要在代码块(方法)添加关键字synchronized,即可以实现同步的功能~
1
2
3
4
5
复制代码
public synchronized void test() {
// 关注公众号Java3y
// doSomething
}

synchronized是一种互斥锁

  • 一次只能允许一个线程进入被锁住的代码块

synchronized是一种内置锁/监视器锁

  • Java中每个对象都有一个内置锁(监视器,也可以理解成锁标记),而synchronized就是使用**对象的内置锁(监视器)**来将代码块(方法)锁定的!

1.2synchronized用处是什么?

  • synchronized保证了线程的原子性。(被保护的代码块是一次被执行的,没有任何线程会同时访问)
  • synchronized还保证了可见性。(当执行完synchronized之后,修改后的变量对其他的线程是可见的)

Java中的synchronized,通过使用内置锁,来实现对变量的同步操作,进而实现了对变量操作的原子性和其他线程对变量的可见性,从而确保了并发情况下的线程安全。

1.3synchronized的原理

我们首先来看一段synchronized修饰方法和代码块的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码
public class Main {
//修饰方法
public synchronized void test1(){

}


public void test2(){
// 修饰代码块
synchronized (this){

}
}
}

来反编译看一下:

同步代码块:

  • monitorenter和monitorexit指令实现的

同步方法(在这看不出来需要看JVM底层实现)

  • 方法修饰符上的ACC_SYNCHRONIZED实现。

synchronized底层是是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。

具体可参考:

  • blog.csdn.net/chenssy/art…
  • blog.csdn.net/u012465296/…

1.4synchronized如何使用

synchronized一般我们用来修饰三种东西:

  • 修饰普通方法
  • 修饰代码块
  • 修饰静态方法

1.4.1修饰普通方法:

用的锁是Java3y对象(内置锁)

1
2
3
4
5
6
7
8
9
10
11
复制代码
public class Java3y {


// 修饰普通方法,此时用的锁是Java3y对象(内置锁)
public synchronized void test() {
// 关注公众号Java3y
// doSomething
}

}

1.4.2修饰代码块:

用的锁是Java3y对象(内置锁)—>this

1
2
3
4
5
6
7
8
9
10
11
12
复制代码
public class Java3y {

public void test() {

// 修饰代码块,此时用的锁是Java3y对象(内置锁)--->this
synchronized (this){
// 关注公众号Java3y
// doSomething
}
}
}

当然了,我们使用synchronized修饰代码块时未必使用this,还可以使用其他的对象(随便一个对象都有一个内置锁)

所以,我们可以这样干:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public class Java3y {


// 使用object作为锁(任何对象都有对应的锁标记,object也不例外)
private Object object = new Object();


public void test() {

// 修饰代码块,此时用的锁是自己创建的锁Object
synchronized (object){
// 关注公众号Java3y
// doSomething
}
}

}

上面那种方式(随便使用一个对象作为锁)在书上称之为–>客户端锁,这是不建议使用的。

书上想要实现的功能是:给ArrayList添加一个putIfAbsent(),这需要是线程安全的。

假定直接添加synchronized是不可行的

使用客户端锁,会将当前的实现与原本的list耦合了:

书上给出的办法是使用组合的方式(也就是装饰器模式)

1.4.3修饰静态方法

获取到的是类锁(类的字节码文件对象):Java3y.class

1
2
3
4
5
6
7
8
9
复制代码public class Java3y {

// 修饰静态方法代码块,静态方法属于类方法,它属于这个类,获取到的锁是属于类的锁(类的字节码文件对象)-->Java3y.class
public synchronized void test() {

// 关注公众号Java3y
// doSomething
}
}

1.4.4类锁与对象锁

synchronized修饰静态方法获取的是类锁(类的字节码文件对象),synchronized修饰普通方法或代码块获取的是对象锁。

  • 它俩是不冲突的,也就是说:获取了类锁的线程和获取了对象锁的线程是不冲突的!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
复制代码
public class SynchoronizedDemo {

//synchronized修饰非静态方法
public synchronized void function() throws InterruptedException {
for (int i = 0; i <3; i++) {
Thread.sleep(1000);
System.out.println("function running...");
}
}
//synchronized修饰静态方法
public static synchronized void staticFunction()
throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread.sleep(1000);
System.out.println("Static function running...");
}
}

public static void main(String[] args) {
final SynchoronizedDemo demo = new SynchoronizedDemo();

// 创建线程执行静态方法
Thread t1 = new Thread(() -> {
try {
staticFunction();
} catch (InterruptedException e) {
e.printStackTrace();
}
});

// 创建线程执行实例方法
Thread t2 = new Thread(() -> {
try {
demo.function();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动
t1.start();
t2.start();
}
}

结果证明:类锁和对象锁是不会冲突的!

1.5重入锁

我们来看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码
public class Widget {

// 锁住了
public synchronized void doSomething() {
...
}
}

public class LoggingWidget extends Widget {

// 锁住了
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
  1. 当线程A进入到LoggingWidget的doSomething()方法时,此时拿到了LoggingWidget实例对象的锁。
  2. 随后在方法上又调用了父类Widget的doSomething()方法,它又是被synchronized修饰。
  3. 那现在我们LoggingWidget实例对象的锁还没有释放,进入父类Widget的doSomething()方法还需要一把锁吗?

不需要的!

因为锁的持有者是“线程”,而不是“调用”。线程A已经是有了LoggingWidget实例对象的锁了,当再需要的时候可以继续**“开锁”**进去的!

这就是内置锁的可重入性。

1.6释放锁的时机

  1. 当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。
  2. 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
  • 不会由于异常导致出现死锁现象~

二、Lock显式锁

2.1Lock显式锁简单介绍

Lock显式锁是JDK1.5之后才有的,之前我们都是使用Synchronized锁来使线程安全的~

Lock显式锁是一个接口,我们来看看:

随便翻译一下他的顶部注释,看看是干嘛用的:

可以简单概括一下:

  • Lock方式来获取锁支持中断、超时不获取、是非阻塞的
  • 提高了语义化,哪里加锁,哪里解锁都得写出来
  • Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁
  • 支持Condition条件对象
  • 允许多个读线程同时访问共享资源

2.2synchronized锁和Lock锁使用哪个

前面说了,Lock显式锁给我们的程序带来了很多的灵活性,很多特性都是Synchronized锁没有的。那Synchronized锁有没有存在的必要??

必须是有的!!Lock锁在刚出来的时候很多性能方面都比Synchronized锁要好,但是从JDK1.6开始Synchronized锁就做了各种的优化(毕竟亲儿子,牛逼)

  • 优化操作:适应自旋锁,锁消除,锁粗化,轻量级锁,偏向锁。
  • 详情可参考:blog.csdn.net/chenssy/art…

所以,到现在Lock锁和Synchronized锁的性能其实差别不是很大!而Synchronized锁用起来又特别简单。Lock锁还得顾忌到它的特性,要手动释放锁才行(如果忘了释放,这就是一个隐患)

所以说,我们绝大部分时候还是会使用Synchronized锁,用到了Lock锁提及的特性,带来的灵活性才会考虑使用Lock显式锁~

2.3公平锁

公平锁理解起来非常简单:

  • 线程将按照它们发出请求的顺序来获取锁

非公平锁就是:

  • 线程发出请求的时可以**“插队”**获取锁

Lock和synchronize都是默认使用非公平锁的。如果不是必要的情况下,不要使用公平锁

  • 公平锁会来带一些性能的消耗的

四、最后

本文讲了synchronized内置锁和简单描述了一下Lock显式锁,总得来说:

  • synchronized好用,简单,性能不差
  • 没有使用到Lock显式锁的特性就不要使用Lock锁了。

Lock锁这里只是介绍了一些些,明天会详解它的相关子类和需要注意的地方,敬请期待~

之前在学习操作系统的时候根据《计算机操作系统-汤小丹》这本书也做了一点点笔记,都是比较浅显的知识点。或许对大家有帮助

  • 操作系统第一篇【引论】
  • 操作系统第二篇【进程管理】
  • 操作系统第三篇【线程】
  • 操作系统第四篇【处理机调度】
  • 操作系统第五篇【死锁】
  • 操作系统第六篇【存储器管理】
  • 操作系统第七篇【设备管理】

参考资料:

  • 《Java核心技术卷一》
  • 《Java并发编程实战》
  • 《计算机操作系统-汤小丹》
  • blog.csdn.net/panweiwei19…
  • www.cnblogs.com/dolphin0520…
  • blog.csdn.net/chenssy/art…
  • blog.csdn.net/u012465296/…
  • www.cnblogs.com/wxd0108/p/5…
    img

涵盖Java后端所有知识点的开源项目(已有6 K star):github.com/ZhongFuChen…

如果大家想要实时关注我更新的文章以及分享的干货的话,微信搜索Java3y。

PDF文档的内容均为手打,有任何的不懂都可以直接来问我(公众号有我的联系方式)。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

3行代码快速实现Spring Boot Oauth2 Ser

发表于 2018-04-24

这里的3行代码并不是指真的只需要写3行代码,而是基于我已经写好的一个Spring Boot Oauth2服务。仅仅需要修改3行数据库配置信息,即可得到一个Spring Boot Oauth2服务。

项目地址github.com/jeesun/oaut…






oauthserver

简介

oauthserver是一个基于Spring Boot Oauth2的完整的独立的Oauth微服务。仅仅需要创建相关数据表,修改数据库的连接信息,你就可以得到一个Oauth微服务。

支持的关系型数据库:

  • PostgreSQL
  • MySQL

已实现的功能:

  1. 集成Spring Boot Oauth2,实现Oauth服务;
  2. token保存到关系型数据库;
  3. 获取token时,username允许传用户名、手机号或者邮箱;
  4. 日志记录保存到文件,并按日归档;
  5. 数据库连接信息加密;
  6. 集成Druid数据库连接池;
  7. 自定义Oauth2Exception异常返回的json信息。

更新日志

v1.1.0(2018-06-01)

  • 自定义Oauth2Exception异常返回的json信息。

v1.0.3

  • bug修复。

v1.0.1

  • 获取token时,username允许传用户名、手机号或者邮箱。

v1.0.0

  • 完成基础Oauth服务。

使用流程

1. 建表

  • PostgreSQL
    请执行src/main/resources/schema-pg.sql,完成数据表的创建和测试数据的导入。
  • MySQL
    请执行src/main/resources/schema-mysql.sql,完成数据表的创建和测试数据的导入。

2. 修改数据库连接信息

在application.yml中,配置着数据库的连接信息。其中,配置项username和password是要经过jasypt加密的,不能直接填明文。加密密钥由jasypt.encryptor.password配置。你需要使用test目录下的UtilTests工具得到加密字符串。

  • PostgreSQL
1
2
3
4
5
复制代码# PostgreSQL连接信息
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://127.0.0.1:5432/thymelte?useUnicode=true&amp;characterEncoding=UTF-8
username: ENC(hTpbG9fq+7P3SntmXuNtDxbtWDqRuPV+) #明文postgres
password: ENC(abdq6LyOspryFQHCqzEMTxRozyJVjIA4) #明文19961120
  • MySQL
1
2
3
4
5
复制代码# MySQL连接信息
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: ENC(YiYjVwTulDGN//YaB3KbuA==) #明文root
password: ENC(9oaIJkFgGSDFaHH3OXY63RHWQ+amDmiJ) #明文19941017

3. 运行

现在,一切已准备就绪。运行项目,当程序成功启动时,即表明你已配置成功。

4. 测试

在建表时,我已经向表添加了测试数据。以下请求参数的值,均是测试数据,在数据表中可以找得到。请根据需求到数据表中修改对应的值。

在表oauth_client_details表中,已有一条测试数据。列client_id和client_secret的值,分别对应Basic Oauth的请求参数username和password的值。而列access_token_validity和列refresh_token_validity,分别代表access_token和refresh_token的有效期时间,以秒为单位。测试数据7200和5184000,分别代表2个小时和2个月(60天)。这是一个比较合理的有效期时间的设置,可以参考。

token相关的接口,都需要进行Basic Oauth认证。

如下图所示:

输入图片说明

在这里输入图片标题

1、根据用户名和密码获取access_token

POST http://localhost:8182/oauth/token?grant_type=password&username=jeesun&password=1234567890c

成功示例

status=200,返回的json数据:

1
2
3
4
5
6
7
复制代码{
"access_token": "ca582cd1-be6c-4a5a-82ec-10af7a8e06eb",
"token_type": "bearer",
"refresh_token": "c24a6143-97c8-4642-88b9-d5c5b902b487",
"expires_in": 3824,
"scope": "read write trust"
}

失败示例

  1. 用户名错误

status=400,返回的json数据:

1
2
3
4
5
复制代码{
"code": 400,
"message": "用户名不存在",
"data": null
}
  1. 密码错误

status=400,返回的json数据:

1
2
3
4
5
复制代码{
"code": 400,
"message": "密码错误",
"data": null
}
  1. 账号被封enabled=false

status=400,返回的json数据:

1
2
3
4
5
复制代码{
"code": 400,
"message": "您已被封号",
"data": null
}

2、检查access_token

GET http://localhost:8182/oauth/check_token?token=ca582cd1-be6c-4a5a-82ec-10af7a8e06eb

成功示例

即使用户被封enabled=false,access_token未过期仍然可用。

status=200,返回的json数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码{
"aud": [
"oauth2-resource"
],
"exp": 1524507296,
"user_name": "jeesun",
"authorities": [
"ROLE_ADMIN",
"ROLE_USER"
],
"client_id": "clientIdPassword",
"scope": [
"read",
"write",
"trust"
]
}

失败示例

access_token已过期

status=400,返回的json数据:

1
2
3
4
5
复制代码{
"code": 400,
"message": "Token was not recognised",
"data": null
}

3、根据refresh_token获取新的access_token

POST http://localhost:8182/oauth/token?grant_type=refresh_token&refresh_token=c24a6143-97c8-4642-88b9-d5c5b902b487

成功示例

status=200,返回的json数据:

1
2
3
4
5
6
7
复制代码{
"access_token": "690ecd7d-f2b7-4faa-ac45-5b7a319478e8",
"token_type": "bearer",
"refresh_token": "c24a6143-97c8-4642-88b9-d5c5b902b487",
"expires_in": 7199,
"scope": "read write trust"
}

失败示例

用户被封enabled=false

status=401,返回的json数据:

1
2
3
4
5
复制代码{
"code": 401,
"msg": "用户已失效",
"data": null
}

app实践指南

app获取到token信息后,需要保存token信息和请求时间。在传access_token之前,需要检查access_token是否过期。为了减少后台压力,检查access_token是否过期应该是在app本地完成。通过token的keyexpires_in(剩余有效期)的值,以及本地记录的请求时间,和当前时间做对比,可以很方便地判断出access_token是否过期。如果过期了,需要通过refresh_token获取新的access_token。因为access_token的有效期只有2个小时,这个验证是必须的。

refresh_token同理。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

人人都能掌握的Java服务端性能优化方案

发表于 2018-04-24

作为一个Java后端开发,我们写出的大部分代码都决定着用户的使用体验。如果我们的后端代码性能不好,那么用户在访问我们的网站时就要浪费一些时间等待服务器的响应。这就可能导致用户投诉甚至用户的流失。

关于性能优化是一个很大的话题。《Java程序性能优化》说性能优化包含五个层次:设计调优、代码调优、JVM调优、数据库调优、操作系统调优等。而每一个层次又包含很多方法论和最佳实践。本文不想大而广的概述这些内容。只是举几个常用的Java代码优化方案,读者看完之后可以真正的实践到自己代码中的方案。

使用单例

对于IO处理、数据库连接、配置文件解析加载等一些非常耗费系统资源的操作,我们必须对这些实例的创建进行限制,或者是始终使用一个公用的实例,以节约系统开销,这种情况下就需要用到单例模式。

单例模式有很多种语法,我的公众号也推送过多篇和单例相关的文章

使用Future模式

假设一个任务执行起来需要花费一些时间,为了省去不必要的等待时间,可以先获取一个“提货单”,即Future,然后继续处理别的任务,直到“货物”到达,即任务执行完得到结果,此时便可以用“提货单”进行提货,即通过Future对象得到返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
复制代码public class RealData implements Callable<String> {  
protected String data;

public RealData(String data) {
this.data = data;
}

@Override
public String call() throws Exception {
//利用sleep方法来表示真是业务是非常缓慢的
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return data;
}
}

public class Application {
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask =
new FutureTask<String>(new RealData("name"));
ExecutorService executor =
Executors.newFixedThreadPool(1); //使用线程池
//执行FutureTask,相当于上例中的client.request("name")发送请求
executor.submit(futureTask);
//这里可以用一个sleep代替对其他业务逻辑的处理
//在处理这些业务逻辑过程中,RealData也正在创建,从而充分了利用等待时间
Thread.sleep(2000);
//使用真实数据
//如果call()没有执行完成依然会等待
System.out.println("数据=" + futureTask.get());
}
}

使用线程池

合理利用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

在 Java 5 之后,并发编程引入了一堆新的启动、调度和管理线程的API。Executor 框架便是 Java 5 中引入的,其内部使用了线程池机制,它在 java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码public class MultiThreadTest {
public static void main(String[] args) {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-%d").build();
ExecutorService executor = new ThreadPoolExecutor(2, 5, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory);
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("hello world !");
}
});
System.out.println(" ===> main Thread! " );
}
}

使用NIO

JDK自1.4起开始提供全新的I/O编程类库,简称NIO,其不但引入了全新高效的Buffer和Channel,同时,还引入了基于Selector的非阻塞 I/O机制,将多个异步的I/O操作集中到一个或几个线程当中进行处理,使用NIO代替阻塞I/O能提高程序的并发吞吐能力,降低系统的开销。

对于每一个请求,如果单独开一个线程进行相应的逻辑处理,当客户端的数据传递并不是一直进行,而是断断续续的,则相应的线程需要 I/O等待,并进行上下文切换。而使用NIO引入的Selector机制后,可以提升程序的并发效率,改善这一状况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public class NioTest {  
static public void main( String args[] ) throws Exception {
FileInputStream fin = new FileInputStream("c:\\test.txt");
// 获取通道
FileChannel fc = fin.getChannel();
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据到缓冲区
fc.read(buffer);
buffer.flip();
while (buffer.remaining()>0) {
byte b = buffer.get();
System.out.print(((char)b));
}
fin.close();
}
}

锁优化

在并发场景中,我们的代码中经常会用到锁。存在锁,就必然存在锁的竞争,存在锁的竞争,就会消耗很多资源。那么,如何优化我们Java代码中的锁呢?主要可以从以下几个方面考虑:

  • 减少锁持有时间
    • 可以使用同步代码块来代替同步方法。这样既可以减少锁持有的时间。
  • 减少锁粒度
    • 要在并发场景中使用Map的时候,记得使用ConcurrentHashMap来代替HashTable和HashMap。
  • 锁分离
    • 普通锁(如syncronized)会导致读阻塞写、写也会阻塞读,同时读读与写写之间也会进行阻塞,可以想办法将读操作和写操作分离开。
  • 锁粗化
    • 有些情况下我们希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
  • 锁消除
    • 锁消除是Java虚拟机在JIT编译是,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。

关于锁优化的内容,后面会出一篇文章详细介绍。

压缩传输

在进行数据传输之前,可以先将数据进行压缩,以减少网络传输的字节数,提升数据传输的速度,接收端可以将数据进行解压,以还原出传递的数据,并且,经过压缩的数据还可以节约所耗费的存储介质(磁盘或内存)的空间以及网络带宽,降低成本。当然,压缩也并不是没有开销的,数据压缩需要大量的CPU计算,并且,根据压缩算法的不同,计算的复杂度以及数据的压缩比也存在较大差异。一般情况下,需要根据不同的业务场景,选择不同的压缩算法。

缓存结果

对于相同的用户请求,如果每次都重复的查询数据库,重复的进行计算,将浪费很多的时间和资源。将计算后的结果缓存到本地内存,或者是通过分布式缓存来进行结果的缓存,可以节约宝贵的CPU计算资源,减少重复的数据库查询或者是磁盘I/O,将原本磁头的物理转动变成内存的电子运动,提高响应速度,并且线程的迅速释放也使得应用的吞吐能力得到提升。

参考

Java多线程编程中Future模式的详解 java锁优化的方法与思路

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…893894895…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%