Java线程安全-《深入了解Java虚拟机》读书笔记

Java线程安全

《深入了解Java虚拟机》读书笔记

阿姆达尔定律(Amdahl law)

Amdahl加速定律定义了一个系统进行并行化改造时候可以提升的性能占比

公式大约如下:
$$
S’=\cfrac{1}{1-f+\cfrac{f}{S}}=\cfrac{1}{1-(1-\cfrac{1}{S})f}
$$

其中,

S’=是整体加速比

f=加速的部分占到整体系统的比重

S=加速了的部分的加速比重

举例:假设某个程序中,你优化了80%的代码,对这80%的代码你获得了加速比10,那么对整个程序而言,你的优化获得的加速比为:1/(1–0.8+0.8/10)=3.57,这远小于10。

S无限增大时候,S’逼近
$$
\cfrac{1}{1-f}
$$
也就是说,优化程序80%的代码,最大获得的加速比为5。

线程安全的定义

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的

——Brian Goetz《Java Concurrency In Practice》

Java中各种操作共享数据分类

不可变

Immutable,JDK1.5内存模型被修正以后的Java语言

Java语言中,如果共享数据是一个基本数据类型,只要在定义时使用final关键字修饰,可以保证是不可变的。

如果是一个对象,那就需要保证对象的行为不会对状态产生任何影响,比如java.lang.String中,substring()、replace()、concat()等方法,都不会影响它原来的值

保证对象行为不受影响的途径,最简单的就是把对象中带有状态的变量都声明为final,比如Integer类

Java API中,不可变的类型包括

  • String
  • 枚举类
  • java.lang.Number的部分子类
  • 不含AtomicInteger,AtomicLong,这两个类使用unsafe 的CAS操作 进行实现

绝对线程安全

完全满足Brian Goetz 提出的要求,调用者也不需要额外的同步措施,Java中基本没有类似的实现。

即使是在全部方法中使用了 synchronized 关键字修饰的 Vector类,在多线程的场景中调用方法,也还是会出现线程问题,虽然每个操作都是原子的,都会线程安全,但是多个原子操作的顺序却可能导致线程问题

相对线程安全

通常意义上的线程安全,保证一个对象单独的操作是线程安全的即可。Java中,大部分线程安全类都是属于这种类型,比如Vector、HashTable、Collections的synchronizedCollection()等

线程兼容

指对象本身不是线程安全的,需要调用者进行正确的同步手段来保证对象在并发环境中可以安全使用。与之前Vector和HashTable对应的ArrayList、HashMap等就是。

Java中绝大多数的API属于这类型

线程对立

指无论调用端是否采取了同步措施,都无法在多线程的环境中并发使用的代码。

Java中此类例子很少,常见的对立的例子如下:

  • Thread类的 suspend()方法以及resume()方法,如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时候是否同步,目标线程都有死锁的风险。如果subpend()中断的线程就是即将要执行resume的那个线程,那就肯定要死锁了。由于以上原因,suspend()、resume()方法已经被JDK声明废弃了
  • System.sctIn()
  • System.setOut()
  • System.runFinalizersOnExit()

线程安全实现方式

互斥同步

悲观锁的思路体现、也称阻塞同步

常见的并发正确性方案,同步指的是多个线程并发访问共享数据的时候,保证共享数据在同一时刻只被一个或者一些(信号量时候)线程使用。

互斥是实现同步的一种手段,主要的实现方式如下,互斥是因,同步是果,互斥是方法、同步是目的

  • 临界区
  • 互斥量
  • 信号量

Java中,synchronize关键字是最基本的互斥同步手段。synchronized关键字经过编译以后,会在同步块的前后分别形成monitorenter和 monitorexit两个字节码指令。

字节码需要一个reference类型的参数来指明要锁定和解锁的对象

  • 如果java程序中的synchronized明确指定了对象参数,那就是这个对象的reference
  • 如果没有指定
  • synchronized修饰的是实例方法,取对应的对象实例
  • 修饰静态类方法,取Class对象来作为锁对象

monitorenter 进行锁对象计数器+1,monitorexit进行锁对象计数器-1。

synchronized同步块对于同一线程来说是可重入的,不会出现自己把自己锁死的问题。

阻塞或者唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转化为和心态,因此状态转换会耗费很多的处理器时间。对于简单的同步块,比如getter方法,状态转换可能比用户代码执行的时间还要长。

synchronized在java中是一个重量级操作,非必要情况下不要使用。当然JVM也会做一些优化

使用ReentryLock实现锁

reentryLock与synchronized很相似,具备一样的线程重入特性,不过增加了一些高级功能

  • 等待可中断
  • 当等待锁的线程等待时间太长,可以中断等待,改为处理其他事情
  • 公平锁
  • 多个线程等待同一个锁时候,必须按照申请的时间顺序来依次获得锁,synchronized是非公平的
  • reentryLock可以通过构造参数选择是否公平
  • 锁绑定多个条件
  • 一个ReentryLock可以同时绑定多个Condition对象,synchronized中,wait\notify\notifyAll方法可以实现一个隐含的条件
  • newCondition可以实现多个条件
  • JDK1.6以上,ReentryLock与synchronized的性能完全持平,没有上述场景的前提下,建议使用原生的synchronized实现功能

非阻塞同步

随着硬件指令集的发展,基于冲突检测的乐观并发策略称为 非阻塞同步,区别于互斥同步,是一种先进行操作,发生冲突以后补偿的乐观锁思路。

这种策略需要操作和冲突检测这两个步骤具有原子性(当然不是使用synchronized。。。),常用指令有:

  • 测试并设置(Test-and-Set)
  • 获取并增加(Fetch-and-Increment)
  • 交换(Swap)
  • 比较并交换(Compare-and-Swap,CAS)
  • 加载连接、条件存储(Load Linked/Store Conditional,LL/SC)

前3条是老的指令,后2条是现代处理器新增加的

CAS

三个操作数,分别是内存位置V、旧的预期值A、新值B

CAS操作的时候,当且仅当V符合旧的预期值A时候,处理器会用新值B更新V值,否则不执行

JDK1.5以后,可以使用sun.misc.Unsafe类中的对应方法实行,并且限制了只有启动类加载器加载的class才能访问它,因此,如果不采用反射手段,只能通过其他Java的API来间接使用

AtomicLong等就是这样实现的

无同步方案

没有或者不涉及共享数据的前提下,可以采用这种方案。简单介绍2类

  • 可重入代码(Reentrant Code)所有的的可重入代码都是线程安全的。可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源,用到的状态量都是由参数中传入、不调用非可重入的方法等。
  • 一个简单的原则可以判断:如果一个方法,它的返回结果可以预测,只要输入了相同的数据,都能返回相同的结果,那就满足可重入的要求
  • 满足条件:

    (1)可以在执行的过程中可以被打断;

    (2)被打断之后,在该函数一次调用执行完之前,可以再次被调用(或进入,reentered)。

    (3)再次调用执行完之后,被打断的上次调用可以继续恢复执行,并正确执行。

  • 可以结合JVM来分析Java中的可重入代码,如果一个函数的所有访问变量,都是以值传递的方式传入局部变量表,那么这个方法就是可重入的。
  • 线程本地存储(Thread Local Storage):在Java中,就是ThreadLocal这个类。共享数g据的可见范围设置在同一个线程以内
  • ThreadLoca类的实现
  • 如果一个变量要被多线程访问,可以使用 volatile关键字声明
  • 经典Web交互模型中,“一个请求对应一个服务器线程”的处理模型

锁优化

自旋锁与自适应自旋

指的是让等待锁的线程通过空循环(自旋),而不进行让出CPU的操作,避免操作系统进行状态切换带来的时间损失。

  • 物理机器需要有一个以上的处理器,能让两个或以上的线程同时并行执行
  • 自旋等待的时间必须有一定的限度
  • -XX:+UseSpinning 开启自旋,1.6以后默认开启
  • -XX:PreBlockSpin 可以修改自旋次数
  • 1.6以后引入的自适应的自旋锁,通过程序运行和性能监控的不断完善,虚拟机对锁进行状况预测

锁消除

如果一段代码中,堆上所有的数据都不会逃逸出去从而被其他线程访问,那就可以认为数据是线程私有的,相当于栈上数据

锁粗化

有一系列的代码需要加锁,那么虚拟机会把加锁同步的范围扩展,粗化到整个操作序列的外部

轻量级锁

JDK1.6引入,在对象头中指定了一个标记位,加锁时候,通过CAS操作将线程栈与对象联系起来

  • 使用CAS操作替代互斥量操作,如果一个对象,只有一个线程要获取锁,那么使用这种情况
  • “对于绝大多数的锁,在整个同步周期内都是不存在竞争的”
  • 如果存在锁竞争,那么除了互斥量的开销以外,还额外发生了CAS操作

偏向锁

JDK1.6引入,同轻量级锁比较,偏向所就是直接将CAS操作也一并省略掉的技术

  • 一个线程在获取对象的锁同时,对象会在标记位中记录下这个线程的id
  • 后续的执行过程中,如果锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步
  • -XX:+UseBiasedLocking JDK1.6默认开启
  • 有的时候禁止偏向锁可以提高性能

redis知识点

redis

概述、适合场景

summary

  • 支持发布订阅、主从复制、磁盘持久化
  • 多种数据结构
  • 短事务
  • 高吞吐量、低延迟
  • geo支持(地理位置支持,geo作为一个单独的数据结构,有自己的增上改查命令)
  • geoadd
Name Type Data Storage options Query types Additional features Ops Latency
Redis In- memory non- relational database Strings, lists, sets, hashes, sorted sets, geo Commands for each data type for common access patterns, and partial transaction support Publish/ Subscribe, master/ slave replication, disk persistence, scripting (stored procedures) 120000 <1ms
MongoDB On-disk non- relational document store Databases of tables of schema-less BSON documents Commands for create, read, update, delete, conditional queries, and more Supports map- reduce operations, master/ slave replication, sharding, spatial indexes 3500 <100ms
Memcache In-memory key-value cache Mapping of keys to values Commands for create, read, update, delete, and a few others Multithreaded server for additional performance 80000 <1ms
MySQL Relational database Databases of tables of rows, views over tables, spatial and third-party extensions Select, insert, update, delete, functions, stored procedures ACID compliant (with InnoDB), master/ slave and master/ master replication 900 >100ms

Redis介绍

存储实现

内存数据结构

  • redisServer
  • redisDb
    • dict
    • dictht
      • dictEntry(链表)
      • redisObject
        • type
        • encoding
        • ptr
    • expires(过期属性)

redisObject分类

每一种类型的object都有两种实现,分别是压缩和非压缩

  • String
  • int(压缩)
  • sds
  • List
  • linkedlist
  • ziplist(压缩)
  • Hash
  • ht
  • ziplist(压缩)
  • Set
  • ht
  • intset(压缩)
  • Zset
  • skiplist
  • ziplist(压缩)

数据存储类型

  • INT:压缩存储string

  • 常量数字对象共享(0~9999)

  • SDS:存储string

  • int len;

    > int free;
    
    > char buf[]
    
  • 变长字符数组

  • 常用字符串共享

  • 优化长度计算

  • LinkedList 双向列表,存储list

  • 优化长度计算

  • 支持双端遍历

  • HT(hashtable):存储set和hash

  • 扩展、收缩,根据填充绿 used/size

  • 渐进式hash:hash操作、事件触发

  • INTSET:压缩存储set(整数)

  • SKIPLIST:跳表,存储有序集

  • ZIPLIST:压缩存储hash、list、zset

内存管理、淘汰

内存管理
  • Zmalloc(匹配系统,根据系统使用不同的分配算法)
  • Tcmalloc、Jemalloc、Malloc(MAC)
    • 根据不同系统的内存分配长度不同,提前计算,分配指定长度
  • 内存统计
内存淘汰
  • 被动淘汰
  • 达到maxmemory,采用淘汰策略
    • lru
    • volatile-lru/allkeys-lru/
    • random
    • vloatile-random/allkeys-random/
    • ttl
    • vloatile-ttl/
    • Noeviction(simple)
    • 访问时淘汰
  • 主动淘汰(random)

事件模型

网络模型-libae
  • Epoll
  • Select
  • Kqueue
  • Evport(Solaris)
事件类型
  • TimeEvent
  • FileEvent

线程模型

  • MainThread
  • BioThread

重要feature

主从同步

  • 常规同步:异步复制
  • 修改不会直接到从库
  • 主库收到cmd以后,同时修改master_repl_off(repl状态表)
  • 使用real_buf存储最后修改的数据
  • 主库针对 每一个slave有一个output buf
  • 定时 分别将每一个output buf传输到指定的 slave
  • slave返回 real_off响应
  • 主库 根据返回值 修改 master_repl_off表
  • 增量同步
  • Redis状态机

持久化

  • RDB(snapshot)
  • Save(阻塞)/bgsave(非阻塞)
  • 文件格式

    • Redis
    • rdb-version
    • select-db
    • key-value-pairs
    • eof
    • check-sum
    • db-data
    • optional-expire-time
    • type-of-value
    • key
    • value
  • rdbload,全量数据load到内存

  • 每一条数据 checksum检查
  • 通信协议:RESP(redis serialization protocol)
  • 便于实现、解析、理解、二进制安全
  • AOF(oplog)
  • 三种配置, 进行fsync
    • AOF_FSYNC_NO
    • AOF_FSYNC_EVERYSEC(每一秒写入)
    • AOF_FSYNC_ALWAYS(每一条cmd写入磁盘)
  • AOF load(fake client)
  • AOF rewrite
    • merge 不同的cmd,聚合cmd,最后只有一个cmd

集群方案

Twemproxy

​ 高效的路由转发

  • 根据key哈希,数据分片
  • 实现
  • client_in
  • server_in
  • server_out

redis-cluster

​ P2P

  • Gossip协议
  • Auto failover
  • replicas migration
  • online reshard,多个实例会成为一个分片

Mysql&Redis增量同步方案

  • 通过proxy分发请求
  • 写请求发送mysql
  • 读请求发送redis
  • Mysql-binLog 获取数据库增量更新
  • Databus、Broker进行消息队列写入
  • Redis根据队列内容增量更新

大数据系统性能分析

大数据系统性能分析

单机瓶颈分析

增加并发和吞吐

增加并发的方式

  • 主要是增加对CPU的利用
  • 多机
  • 单机多CPU
  • 单CPU多core
  • 单core超线程

方法

  • IO密集型应用:降低线程切换对cpu的浪费
  • 多进程 – 多线程 – 事件驱动 – 协程
  • CPU 密集性应用:增加计算的并发
  • 多进程 – 多线程
  • 除非资源到了瓶颈,否则不排队
  • 合适的并发模型
    • 比如ependingpool、事件触发、异步队列等
  • 队伍要均衡
    • 保证能够打到充分的并发
  • 过长的队伍、及时柔性处理(可丢服务)
    • 大数据系统中经常遇到数据堵塞等场景,需要有良好的柔性处理机制,如优先级机制,清除过期数据的机制,部分服务可丢等机制来解决
  • 能并发的任务不串行
  • 并发情况下,影响等待时间的主要是最长的任务时间
  • 串行情况下,是所有任务时间之和

去除不必要的动作

  • 减少网络重连
  • 长连接
  • 降低连接数
  • 连接池
  • 减少线程切换
  • 线程池
  • 减少内存分配和释放
  • 内存池
  • 减少耗时的操作合运算
  • memset,浮点运算,除法,指数,对数运算,慎用 stl
  • 在线转离线
  • 离线提前进行耗时运算

避免冲突

  • 多线程无锁算法
  • 无锁共享数据、无锁数据结构、Copy on write,更新不影响读取
  • HASH冲突解决(经常遇到由于hash冲突或者锁冲突导致的性能下降)
  • 合理的使用锁
    • 锁的时间尽可能短
    • 降低冲突概率
    • 避免死锁
    • 锁的影响范围尽可能小:blockingQueue的分段锁机制

IO优化

关于磁盘I/O的性能,引用一组Kafka官方给出的测试数据(Raid-5,7200rpm)

  • SequenceI/O: 600MB/s
  • RandomI/O: 100KB/s

随机修改

  • WAL(write ahead
    logging):随机写入转化为顺序的写入,写入成功即可返回,在故障时候通过 log 恢复
  • LSM-Tree:适合的应用场景:insert数据量大,读、update数据量不高,并且一般针对最新数据
  • 方法:写入数据到内存即返回,缓冲到一定量再写入磁盘;读取时候需要merge磁盘读取合内存中的内容(bigtable,
    tera)
  • 减少IO次数,批量去重
  • 适用于请求中重复度较高的,如链接库的写入,后链的特点就是重复度很高,批量去重能够去除较大部分的重复数据,降低对后端服务的io压力

随机读取

  • 尽量减少IOPS,无cache的情况下做到每请求只消耗1IO
  • 通过优化cache淘汰算法提高cache命中率
  • 基于统计的淘汰策略
  • 多级LRU队列
  • 合理李勇Flash存储,通过压缩等手段降低Flash压力

其他

  • 预处理,预充Cache,预热再服务
  • 充分利用硬件
  • 存储速度: 内存 >> SSD >> sata
  • 示例:kafka (顺序读写 + page cache)
    • 顺序写磁盘效率比随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证
    • 充分利用pagecache,直接内存读取直接发送

集群瓶颈分析

减少数据传输量

数据压缩-CPU与网络io的权衡

  1. 减少跨机房io
  2. 打包访问
  3. 减少交互次数
  4. 数据压缩:CPU与网络IO的权衡

压缩算法对比

数据来自于hbase
| 算法 | remaining(%) | Encoding | Decoding |
| ———— | ———— | ——– | ——– |
| GZIP | 13.4% | 21MB/s | 118MB/s |
| LZO | 20.5% | 135MB/s | 410MB/s |
| Zippy/Snappy | 22.2% | 172MB/s | 409MB/s |

Snappy 在 Google 内部被广泛的使用,从 BigTable 到 MapReduce 以及内部的 RPC
系统

均衡

  1. 负载均衡,常用,一个好的负载均衡方法是保证整个分布式系统性能的基础

  2. RR,Random,Locality-aware,hash

  3. 热点+打散

  4. 自动拆分和融合节点

  5. 自动伸缩容量,弹性

  6. 消除长尾(比如分布式系统中有一个实例老是响应时间长,此时可以屏蔽这个节点)

  7. 拆分、并发

  8. 消峰、限流、缓冲+延迟处理(优先级机制)

1.
消息队列中使用优先级方式,可以一方面保证高优请求很快得到处理,另一方面达到全局缓冲

  1. 丢弃、降级服务

  2. 丢弃是说检测到服务扛不住的时候,自动丢弃一些请求
    2.
    降级这里说的是人工方式处理,比如搜索服务中,在高峰期可以关闭广告处理、甚至关闭另一个引擎

案例:mr任务老是跑很长时间,个别子分片总是运行不完

均匀分片

擅用cache

cache种类
  1. 内存 cache、分布式cache
  2. 有结果 cache/ 无结果cache/ 超时cache
  3. 只读cache/ 读写 cache
提升命中率
  1. 合理的cache key 设计
  2. 需要全局考虑一下,兼容各种访问

  3. 有效的淘汰算法

  4. 保存命中率高的item

案例:上游hash寻址下游(us寻址gss)

  1. 容易造成热点问题

Cache对延迟的优化效果

  1. 节省大量耗时操作时间:不必要的计算、网络、IO

  2. 案例7 均值200ms的服务,加了cache,命中70%

  3. 200×0.3+1×0.7=67ms

系统优化

容器+混步

  1. IAAS
  2. matrix
  3. PAAS
  4. Jpass
  5. Galaxy
  6. Beehive

全量模型->增量模型

  1. 适用于 全量数据量大,而增量更新比例小的情况
    1. 实例 linkbase3.0,将连接库从全量+patch
      改造为增量实时读写模型,节约8000槽位x36小时的计算资源
    2. spider实时统计策略,从1500太机器节约到500台以下

避免局部瓶颈

分布式环境下,每个子系统都非常重要

​ 木桶效应

案例9:一个分布式系统,消息队列发给模块a,模块a负责读取存储系统,merge数据,在写回存储系统(即
read-modify-write模型),性能非常低,加并发、加机倌增量更新比例小的情况
1. 实例 linkbas点影响了整体服务

分治的方法,让A模块只处理匹配分片的存储资源,不要全局访问节点

这样出现故障的话,不会影响全局的性能

不要牺牲可维护性

​ 尽量避免设计过度复杂的系统,人力成本也是成本,一定要可维护性高

tera + 每秒400W qps

类似 google 的 bigtable