JUC包中的分而治之策略-为提高性能而生
一、前言
本次分享我们来共同探讨JUC包中一些有意思的类,包含AtomicLong & LongAdder,ThreadLocalRandom原理。
二、AtomicLong & LongAdder
2.1 AtomicLong 类
AtomicLong是JUC包提供的原子性操作类,其内部通过CAS保证了对计数的原子性更新操作。
大家可以翻看源码发现内部是通过UnSafe(rt.jar)这个类的CAs操作来保证对内部的计数器变量 long value进行原子性更新的,比如JDK8中:
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
其中unsafe.getAndAddLong的代码如下:
public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2)
{
long l;
do
{
l = getLongVolatile(paramObject, paramLong1);//(1)
} while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));//(2)
return l;
}
可知最终调用的是native 方法compareAndSwapLong原子性操作。
当多个线程调用同一个AtomicLong实例的incrementAndGet方法后,多个线程都会执行到unsafe.getAndAddLong方法,然后多个线程都会执行到代码(1)处获取计数器的值,然后都会去执行代码(2),如果多个线程同时执行了代码(2),由于CAS具有原子性,所以只有一个线程会更新成功,然后返回true从而退出循环,整个更新操作就OK了。其他线程则CAS失败返回false,则循环一次在次从(1)处获取当前计数器的值,然后在尝试执行(2),这叫做CAS的自旋操作,本质是使用Cpu 资源换取使用锁带来的上下文切换等开销。
2.2 LongAdder类
AtomicLong类为开发人员使用线程安全的计数器提供了方便,但是AtomicLong在高并发下存在一些问题,如上所述,当大量线程调用同一个AtomicLong的实例的方法时候,同时只有一个线程会CAS计数器的值成功,失败的线程则会原地占用cpu进行自旋转重试,这回造成大量线程白白浪费cpu原地自旋转。
在JDK8中新增了一个LongAdder类,其采用分而治之的策略来减少同一个变量的并发竞争度,LongAdder的核心思想是把一个原子变量分解为多个变量,让同样多的线程去竞争多个资源,这样竞争每个资源的线程数就被分担了下来,下面通过图形来理解下两者设计的不同之处:
image.png
如上图AtomicLong是多个线程同时竞争同一个原子变量。
image.png
如上图LongAdder内部维护多个Cell变量,在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这是变相的减少了争夺共享资源的并发量。
下面我们首先看下Cell的结构:
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe 机制
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
LongAdder维护了一个延迟初始化的原子性更新数组(默认情况下Cell数组是null)和一个基值变量base,由于Cells占用内存是相对比较大的,所以一开始并不创建,而是在需要时候在创建,也就是惰性 创建。
当一开始判断cell数组是null并且并发线程较少时候所有的累加操作都是对base变量进行的,这时候就退化为了AtomicLong。cell数组的大小保持是2的N次方大小,初始化时候Cell数组的中Cell的元素个数为2,数组里面的变量实体是Cell类型。
当多个线程在争夺同一个Cell原子变量时候如果失败并不是在当前cell变量上一直自旋CAS重试,而是会尝试在其它Cell的变量上进行CAS尝试,这个改变增加了当前线程重试时候CAS成功的可能性。最后获取LongAdder当前值的时候是把所有Cell变量的value值累加后在加上base返回的,如下代码:
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
如上代码可知首先把base的值赋值给sum变量,然后通过循环把每个cell元素的value值累加到sum变量上,最后返回sum.
其实这是一种分而治之的策略,先把并发量分担到多个原子变量上,让多个线程并发的对不同的原子变量进行操作,然后获取计数时候在把所有原子变量的计数和累加。
思考问题:
何时初始化cell数组
当前线程如何选择cell中的元素进行访问
如果保证cell中元素更新的线程安全
cell数组何时进行扩容,cell元素个数可以无限扩张?
性能对比,这里有一个文章 http://blog.palominolabs.com/2014/02/10/java-8-performance-improvements-longadder-vs-atomiclong/
三、 Random & ThreadLocalRandom
3.1 Random类原理及其局限性
在JDK7之前包括现在java.util.Random应该是使用比较广泛的随机数生成工具类,下面先通过简单的代码看看java.util.Random是如何使用的:
public class RandomTest {
public static void main(String[] args) {
//(1)创建一个默认种子的随机数生成器
Random random = new Random();
//(2)输出10个在0-5(包含0,不包含5)之间的随机数
for (int i = 0; i < 10; ++i) {
System.out.println(random.nextInt(5));
}
}
}
代码(1)创建一个默认随机数生成器,使用默认的种子。
代码(2)输出输出10个在0-5(包含0,不包含5)之间的随机数。
public int nextInt(int bound) {
//(3)参数检查
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
//(4)根据老的种子生成新的种子
int r = next(31);
//(5)根据新的种子计算随机数
...
return r;
}
如上代码可知新的随机数的生成需要两个步骤
首先需要根据老的种子计算生成新的种子。
然后根据新的种子和bound变量通过一定的算法来计算新的随机数。
下面看下next()代码:
protected int next(int bits)www.gxgjpt1.com {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
//(6)获取当前原子变量种子的值
oldseed = seed.get();
//(7)根据当前种子值计算新的种子
nextseed = (oldseed * multiplier + addend) & mask;
//(8)使用新种子替换老的种子
} while (!seed.compareAndSet(oldseed, nextseed));
//(9)
return (int)(nextseed >>> (48 - bits));
}
代码(6)使用原子变量的get方法获取当前原子变量种子的值
代码(7)根据具体的算法使用当前种子值计算新的种子
代码(8)使用CAS操作,使用新的种子去更新老的种子,多线程下可能多个线程都同时执行到了代码(6)那么可能多个线程都拿到的当前种子的值是同一个,然后执行步骤(7)计算的新种子也都是一样的,但是步骤(8)的CAS操作会保证只有一个线程可以更新老的种子为新的,失败的线程会通过循环从新获取更新后的种子作为当前种子去计算老的种子,这就保证了随机数的随机性。
代码(9)则使用固定算法根据新的种子计算随机数,并返回。
3.2 ThreadLocalRandom
Random类生成随机数原理以及不足:每个Random实例里面有一个原子性的种子变量用来记录当前的种子的值,当要生成新的随机数时候要根据当前种子计算新的种子并更新回原子变量。
多线程下使用单个Random实例生成随机数时候,多个线程同时计算随机数计算新的种子时候多个线程会竞争同一个原子变量的更新操作,由于原子变量的更新是CAS操作,同时只有一个线程会成功,那么CAS操作失败的大量线程进行自旋重试,而大量线程的自旋重试是会降低并发性能和消耗CPU资源的,为了解决这个问题,ThreadLocalRandom类应运而生。
public class RandomTest {
public static void main(String[] args) {
//(10)获取一个随机数生成器
ThreadLocalRandom random = ThreadLocalRandom.current();
//(11)输出10个在0-5(包含0,不包含5)之间的随机数
for (int i = 0; i <www.boyunylpt1.com 10; ++i) {
System.out.println(random.nextInt(5));
}
}
}
如上代码(10)调用ThreadLocalRandom.current()来获取当前线程的随机数生成器。下面来分析下ThreadLocalRandom的实现原理。
从名字看会让我们联想到ThreadLocal类。ThreadLocal通过让每一个线程拷贝一份变量,每个线程对变量进行操作时候实际是操作自己本地内存里面的拷贝,从而避免了对共享变量进行同步。实际上ThreadLocalRandom的实现也是这个原理。Random的缺点是多个线程会使用原子性种子变量,会导致对原子变量更新的竞争,这个原理可以通过下面图来表达:
image.png
那么如果每个线程维护自己的一个种子变量,每个线程生成随机数时候根据自己本地内存中的老的种子计算新的种子,并使用新种子更新老的种子,然后根据新种子计算随机数,就不会存在竞争问题,这会大大提高并发性能,如下图ThreadLocalRandom原理可以使用下图表达:
image.png
Thread类里面有几个变量:
/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr"www.gcyL157.com)
long threadLocalRandomSeed;
/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended(www.fengshen157.com"tlr")
int threadLocalRandomProbe;
思考问题:
每个线程的初始种子怎么生成的
如果保障多个线程产生的种子不一样
四、总结
本文是对拙作 java并发编程之美 一书中有关章节的提炼。本次分享首先讲解了AtomicLong的内部实现,以及存在的缺点,然后讲解了 LongAdder采用分而治之的策略通过使用多个原子变量减小单个原子变量竞争的并发度。然后简单介绍了Random,和其缺点,最后介绍了ThreadLocalRandom借用ThreadLocal的思想解决了多线程对同一个原子变量竞争锁带来的性能损耗。其实JUC包中还有其他一些经典的组件,比如fork-join框架等。
JUC包中的分而治之策略-为提高性能而生的更多相关文章
- JUC包中的锁框架
JUC包中的锁,包括:Lock接口,ReadWriteLock接口,LockSupport阻塞原语,Condition条件,AbstractOwnableSynchronizer/AbstractQu ...
- C# 中的迭代器 yield关键字 提高性能和可读性
展示一个例子 IList<string> FindBobs(IEnumerable<string> names) { var bobs = new List<string ...
- JUC包-原子类(AtomicInteger为例)
目录 JUC包-原子类 为什么需要JUC包中的原子类 原子类原理(AtomicInteger为例) volatile CAS CAS的缺点 ABA问题 什么是ABA问题 ABA问题的解决办法 JUC包 ...
- java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域) (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)
java之jvm学习笔记六(实践写自己的安全管理器) 安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就是checkPerssiom这个方法里又调用 AccessCo ...
- 24.Java中atomic包中的原子操作类总结
1. 原子操作类介绍 在并发编程中很容易出现并发安全的问题,有一个很简单的例子就是多线程更新变量i=1,比如多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchr ...
- JUC包实现的同步机制,原理以及简单用法总结
除了用Synchronized关键字修饰同步块,让线程获取某对象的锁实现多线程操作共享变量的同步外,还可以使用java.util.concurrent包.并且,juc包机制下的同步更灵活.juc包的所 ...
- java.io包中的字节流—— FilterInputStream和FilterOutputStream
接着上篇文章,本篇继续说java.io包中的字节流.按照前篇文章所说,java.io包中的字节流中的类关系有用到GoF<设计模式>中的装饰者模式,而这正体现在FilterInputStre ...
- 【安卓中的缓存策略系列】安卓缓存策略之综合应用ImageLoader实现照片墙的效果
在前面的[安卓缓存策略系列]安卓缓存之内存缓存LruCache和[安卓缓存策略系列]安卓缓存策略之磁盘缓存DiskLruCache这两篇博客中已经将安卓中的缓存策略的理论知识进行过详细讲解,还没看过这 ...
- 【安卓中的缓存策略系列】安卓缓存策略之磁盘缓存DiskLruCache
安卓中的缓存包括两种情况即内存缓存与磁盘缓存,其中内存缓存主要是使用LruCache这个类,其中内存缓存我在[安卓中的缓存策略系列]安卓缓存策略之内存缓存LruCache中已经进行过详细讲解,如看官还 ...
随机推荐
- 使用Google Cloud Messaging (GCM),PHP 开发Android Push Notifications (安卓推送通知)
什么是GCM? Google Cloud Messaging (GCM) 是Google提供的一个服务,用来从服务端向安卓设备发送推送通知. GCM分为客户端和服务端开发. 这里我们只介绍服务端开发 ...
- PHP程序员如何理解依赖注入容器(dependency injection container)
背景知识 传统的思路是应用程序用到一个Foo类,就会创建Foo类并调用Foo类的方法,假如这个方法内需要一个Bar类,就会创建Bar类并调用Bar类的方法,而这个方法内需要一个Bim类,就会创建Bim ...
- UpdateLog
2014-10-20 增加数据适配器,使支持多数据库类型2015-01-08 增加没有主键ID的抽象类,使能自义主键字段实现MODEL 增加虚拟字段转换,将指定函数或语法转换为对象属性,灵活性更大了 ...
- List和Turple
List 格式:classmates = ['Michael', 'Bob', 'Tracy'] 读取list长度用:len(classmetes) 索引:索引正向从0开始,逆向从-1开始 在末尾增加 ...
- Android Studio|IntelliJ IDEA 常用快捷键(Mac|Window)
一 For Mac(Mac OS X 10.5+) F1 显示注释文档F2 高亮错误或警告快速定位Command + F12 显示当前文件的结构(查看所有方法)Command + F 查找文本Comm ...
- sqlalchemy 转json 的几种常用方式
sqlalchemy 转json 的几种常用方式 # -*- coding:utf-8 -*- import datetime from flask import Flask, json, jsoni ...
- (一)Spring Boot修改内置Tomcat端口号--解决tomcat端口被占用的问题
Spring Boot 内置Tomcat默认端口号为8080,在开发多个应用调试时很不方便,本文介绍了修改 Spring Boot内置Tomcat端口号的方法. 一.EmbeddedServletCo ...
- Linux内核设计笔记11——定时器
定时器与时间管理笔记 内核中的时间 时钟中断:内核中的系统定时器以某种频率触发中断,该频率可以通过编程预定. 节拍率HZ:时钟中断的频率称为节拍率. 节拍:相邻两次中断的时间间隔称为节拍,1/节拍率. ...
- 机器人控制tcp通信参数调优
机器人使用WiFi通信,实现指令下传,状态上传.而WiFi信道平时带宽较稳定,但会在某些时候突然中断,造成ping的延时较高,但可以马上恢复.如果一直ping,则一般情况下ping值很小,但长时间(数 ...
- 解决python中文编码错误问题
对于初学者而言,编码问题或许还没有没重视起来,但是编码问题是中文开发者必须面对的.今天来看下python开发中如何解决编码问题.注意:本篇讲的是最常见的一种编码问题,其他编码问题,如json函数引起的 ...