Java浮点数存储结构

浮点数转换二进制

18.725为例进行说明。

整数部分转二进制:除2取余法。整数部分除以2取余数,商取整继续除以2取余数,直到商为0。

整数除法 余数
$18/2$ 9 0
$9/2$ 4 1
$4/2$ 2 0
$2/2$ 1 0
$1/2$ 0 1

18转换为二进制就是10010,上面表格中余数的倒序。

小数部分转二进制:乘2取整法。小数部分乘以2,取整数部分,剩下的小数部分继续乘以2取整数部分,直到结果为0。如果永远不为零,则到达期望的精度后终止运算。

乘法公式 小数部分 整数部分
$0.725*2=1.45$ 0.45 1
$0.45*2=0.9$ 0.9 0
$0.9*2=1.8$ 0.8 1
$0.8*2=1.6$ 0.6 1
$0.6*2=1.2$ 0.2 1
$0.2*2=0.4$ 0.4 0
$0.4*2=0.8$ 0.8 0
$0.8*2=1.6$ 0.6 1
$0.6*2=1.2$ 0.2 1
$0.2*2=0.4$ 0.4 0
…… …… ……

0.725的二进制数是0.1011100110(后面还有无限长,这里忽略)。所以18.725的二级制就是10010.1011100110。按照规定,二进制小数点左边只能有1为且固定为1,所以需要进行右移操作,得出结果是:$1.00101011100110*2^4$

  • 符号位:正数,符号位为0;
  • 指数位:实际为4,按照规定要加上127(即指数最高位赋值为1),指数位存储的是131,二级制为10000011,。
  • 底数位:取小数部分,即 0.00101011100110
阅读全文 »

HashMap存储结构采用数组+链表(java8以后当链表长度超过8后采用红黑树来存储),当存储数据时会先计算key的哈希值,然后使用哈希值计算数组指针,如果数组指针位置已经存在数据,且key不相同,就会采用链表形式存储数据。存储结构如下图:

HashMap存储结构

在jdk1.8中HashMap引入了红黑树,当链表中的元素达到阈值后,并且数组长度大于64,就会将链表转换为红黑树。引入红黑树的主要目的是解决哈希冲突导致的链化严重的问题。

哈希值与数组指针

哈希值计算方法:获取key的hashCode,然后hashCode右移16位,最后将两者进行异或运算。源码如下:

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

为什么没有直接使用key.hashCode()的值呢?这是因为数组长度都是2的幕,比如默认数组长度16=$2^4$,即1<<4,长度掩码就是15=16-1,二进制掩码就是1111。hash与掩码进行按位与运算后得到数组指针,相当于只取了hash的低4位,高位没有用到,增加了指针冲突的概率。因此将hashCode进行右移16位,再进行异或运算,这样做就是在运算中同时使用了高位和低位。

HashMap是通过数组来存储元素。当通过key来后去元素值的时候,首先计算出key的哈希值,然后通过哈希值来计算得到数组坐标,看下put操作中如何计算数组坐标:

1
2
3
4
5
6
7
8
9
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //从数组中取出node赋值给p
tab[i] = newNode(hash, key, value, null);
......
}

从代码中可以看出,计算数组坐标的算法是:(n - 1) & hash,其中n表示数组长度。所以坐标指针是hash和长度掩码进行与运算。

阅读全文 »

初始化

«Singleton»ARouter«Singleton»_ARouter

初始化调用时序

ARouterARouter_ARouter_ARouterLogisticsCenterLogisticsCenterinitinitinitloadRouterMapafterInit

生成路由表

ARouter是通过在编译阶段使用kapt解析Route注解,再通过javapoet来生成路由表相关代码。注解处理器在arouter-compiler/src/main/java/com/alibaba/android/arouter/compiler/processor/RouteProcessor.java

比如module-kotlin模块有三个activity,路由分别是:

  • /one/first
  • /one/second
  • /two/test

生成的路由表如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//生成目录位于module-kotlin/build/generated/source/kapt/debug/com/alibaba/android/arouter/routes
public class ARouter$$Root$$modulekotlin implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("one", ARouter$$Group$$one.class);
routes.put("two", ARouter$$Group$$two.class);
}
}

public class ARouter$$Group$$one implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/one/first", RouteMeta.build(RouteType.ACTIVITY, FistActivity.class, "/one/first", "one", null, -1, -2147483648));
atlas.put("/one/second", RouteMeta.build(RouteType.ACTIVITY, SecondActivity.class, "/one/second", "two", null, -1, -2147483648));
}
}

public class ARouter$$Group$$two implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/two/test", RouteMeta.build(RouteType.ACTIVITY, TestActivity.class, "/two/test", "two", null, -1, -2147483648));
}
}
阅读全文 »

本文基于LeakCanary 2.12进行分析。

ApplicationActivityLifecycleCallbacksReachabilityWatcherObjectWatcherwatchedObjects : Map<String, KeyedWeakReference>queue : ReferenceQueue<Any>KeyedWeakReferenceWeakReferenceInstallableWatcherActivityWatcherFragmentAndViewModelWatcherRootViewWatcherServiceWatcherAppWatcher1n1n

初始化

引入LeakCanary时,只需要引入maven库的坐标即可,如下:

1
2
3
4
dependencies {
// debugImplementation because LeakCanary should only run in debug builds.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}

而不需要进行任何的代码改动就可以使用LeakCanary了。另外,引入leakcanary时使用的是debugImplementation表示只在debug版本中才会集成leakcanary,release版本不会集成。

那么,leakcanary如何完成初始化呢?

leakcanary使用了ContentProvider来进行初始化,当app启动时系统会自动初始化注册的ContentProvider。

1
2
3
4
5
6
7
<application>
<provider
android:name="leakcanary.internal.MainProcessAppWatcherInstaller"
android:authorities="${applicationId}.leakcanary-installer"
android:enabled="@bool/leak_canary_watcher_auto_install"
android:exported="false"/>
</application>
1
2
3
4
5
6
7
8
internal class MainProcessAppWatcherInstaller : ContentProvider() {

override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
AppWatcher.manualInstall(application)
return true
}
......
阅读全文 »

当前线程启动消息队列

Thread默认是没有消息循环机制的,而Looper就是在Thread基础上运行一个消息循环,通过Looper.prepare()可以把当前线程初始化为一个Looper thread。一个典型的Looper thread创建示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare();
mHandler = new Handler(Looper.myLooper()) {
public void handleMessage(Message msg) {
//在此处理消息
}
};
Looper.loop();
}
}

上述代码分为三部:1)调用Looper.prepare()初始化Looper;2)创建一个Handler用于与Looper进行交互;3)调用Looper.loop()启动消息循环。下面分别看下这三步是如何实现的:

初始化Looper.prepare()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//android-12.1.0_r27\frameworks\base\core\java\android\os\Looper.java
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

public static void prepare() { //prepare是静态方法
prepare(true);
}

private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
//一个线程中Looper.prepare()只能执行一次,否则会抛出异常
throw new RuntimeException("Only one Looper may be created per thread");
}
//构造了一个Looper放到ThreadLocal中
sThreadLocal.set(new Looper(quitAllowed));
}

//Looper的构造方法也很简单:1)创建消息队列;2)获取当先线程句柄;
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

创建Handler

用户无法直接使用Looper来进行消息发送和处理,而是需要通过一个Handler来实现。使用Handler可以发送和处理Message,或者运行一个Runnable。每个Handler都会绑定一个Looper,Handler将会把message和runnable投递到Looper的消息队列中,然后Looper thread将会执行这些message和runnable。

下图是Handler与Looper、Thread、MessageQueue之间的对应关系,Thread和Looper是一对一的关系,否则会抛出异常,一个Looper只有一个消息队列,但是可以多个Handler对应同一个Looper。

LooperThreadMessageQueueHandler1Handler2Handler3messagemessagemessage
阅读全文 »

公平锁:指的是多个线程等待同一个锁时,等待时间最长的线程将会优先获取到锁。但是,这也会导致当线程尝试获取锁时,很大概率会进入阻塞状态,有很高的状态切换成本,因此,会降低吞吐量和执行效率。

非公平锁:不会保证一个特定获取到锁的顺序。当线程尝试获取锁时,会优先通过CAS尝试获取锁,如果失败了才会进入等待队列。非公平锁的执行效率更高,因为线程尝试获取锁的时候,如果能够直接获取到锁,可以节省线程的上下文切换带来的时间和性能上的开销。但是,可能会导致有的线程永远也获取不到锁。

Java中的synchronized是非公平锁,ReentrantLock默认也是非公平锁,并且是可重入的。ReentrantLock则可以通过fair来设置为公平锁。下面是ReentrantLock的类图结构:

AbstractOwnableSynchronizerAbstractQueuedSynchronizerSyncReentrantLockNonfairSyncFairSync

公平锁加锁

公平锁加锁过程的方法调用:ReentrantLock#lock()ReentrantLock.FairSync#lock()AbstractQueuedSynchronizer#acquire(1)。下面看下acquire的代码实现:

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
// 【AbstractQueuedSynchronizer.java】
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 【ReentrantLock.java】
static final class FairSync extends Sync {
......
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
// 只有锁未被锁定、锁重入或者等待队列中没有前辈的情况才会获取成功
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { //state为0表示当前是未被锁定状态
if (!hasQueuedPredecessors() && //判断等待队列是否有排在自己前面的线程
compareAndSetState(0, acquires)) { //通过CAS设置锁定状态
setExclusiveOwnerThread(current); //独占线程设置为自己
return true; //加锁成功
}
}
else if (current == getExclusiveOwnerThread()) {
// 锁的重入,状态加1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true; //加锁成功
}
return false; //加锁失败
}
}
// 【AbstractQueuedSynchronizer.java】
// addWaiter的作用就是将当前线程添加到等待队列的队尾
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { //循环
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
// 请求锁的当前线程排在了队首,且获取锁成功
setHead(node); //设置为head,所以head是持有锁的线程
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //进入blocked状态
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
tryAcquire()yes锁未被锁定 state==0?CAS设置state为锁定状态当前线程设置为独占线程加锁成功yesCAS设置成功?nono有其他线程正在排队yesyes当前线程已经持有锁nostate加1加锁成功添加到等待队列队尾再次调用tryAcquire()当前节点设置为headyestryAcquire加锁成功?noyesnode.prev == headno线程进入阻塞状态死循环

有一点需要注意,即使ReentrantLock构造时fair设置为true,使用不带超时的tryLock()加锁时也会使用非公平锁的策略。

非公平锁加锁

阅读全文 »

连接线样式

连接线的样式支持:bold, plain, dotted,dashed。设置颜色必须可以使用颜色名称,或者16进制的RGB样式,但是必须使用#开头。

1
2
A -[dashed,#green]right-> B
A2 -[bold,#ff00ff]-> B2
A1B1A2B2

也可以使用如下写法:

1
A --> B #blue;line.dashed;text:blue : 连接线
AB连接线

调整布局

PlantUML绘图会自动布局,但是如果想要手动对布局进行调整就很不方便,只能进行微调,没法做到精细的调整。

改变连接线方向

阅读全文 »

通过Fresco加载图片,最简单的方式是通过SimpleDraweeView来展示图片,示例代码如下:

1
2
3
4
5
6
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/my_image_view"
android:layout_width="130dp"
android:layout_height="130dp"
fresco:placeholderImage="@drawable/my_drawable"
/>
1
2
3
4
//示例代码在Fragment#onViewCreated中调用
Uri uri = Uri.parse("https://frescolib.org/static/sample-images/animal_a.png");
SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view);
draweeView.setImageURI(uri);

先看一下SimpleDraweeView相关的类图结构:

ImageViewDraweeViewGenericDraweeHierarchyGenericDraweeViewSimpleDraweeViewDraweeHierarchygetTopLevelDrawable() : DrawableSettableDraweeHierarchyGenericDraweeHierarchyDraweeControllerDraweeHolderGenericDraweeHierarchyVisibilityCallbackRootDrawableAbstractDraweeControllerPipelineDraweeController

图片加载过程

这个时序图是以文章开头的SimpleDraweeView的示例代码为例进行绘制。

SimpleDraweeViewSimpleDraweeViewPipelineDraweeControllerDraweeHolderDraweeHolderRootDrawableRootDrawableImagePipelineImagePipelineDataSourcesetImageURI();Framework.onViewCreated中调用构造PipelineDraweeControlleronAttachedToWindow(父类方法)onAttachonAttach此时还未设置visible,不会调用attachControlleronVisibilityAggregated(父类ImageView的方法)setVisibleonVisibilityChangeattachControlleronAttachsubmitRequestgetCachedImage获取缓存BitmapgetDataSourcefetchDecodedImage生成生产者序列submitFetchRequest()构造时传入生产者队列DataSource生产者队列启动执行返回实例返回 DataSource<CloseableReference<CloseableImage>>  subscribe(DataSubscriber)注册订阅者返回图片Bitmap。父类的onNewResultInternal()会被执行createDrawablehierarchy.setImage()显示图片

上图中DateSource是一个CloseableProducerToDataSourceAdapter实例,如下图类图所示:

DataSourceCloseableReference<CloseableImage>ProducerCloseableReference<CloseableImage>void produceResults(Consumer<T>, ProducerContext)AbstractDataSourceCloseableReference<CloseableImage>AbstractProducerToDataSourceAdapterCloseableReference<CloseableImage>CloseableProducerToDataSourceAdapterCloseableImageDataSubscriberCloseableReference<CloseableImage>依赖构造函数中调用1n
阅读全文 »

CAS介绍

CAS(Compare and swap)是并发编程中经常用到的一项技术。大体上讲,CAS算法会先使用期望值比较变量的实际值,如果相同,就使用目标值替换变量的值。如果不相同,就会重新读取期望值再次进行对比和替换。

CAS主要是为了解决共享资源被加锁后,线程从阻塞状态切换回运行态比较耗时的问题,CAS是在不阻塞线程的情况下达到资源互斥的目的。但是如果共享资源竞争比较激烈就不适合使用CAS了,CAS会导致很多线程一直在重试访问共享资源,造成CPU资源的浪费。另外,如果共享资源的操作比较耗时,也就不适合使用CAS,这种场景使用阻塞的资源锁会更合适。

CAS源码分析

以AtomicInteger.getAndIncrement为例进行源码分析,下面代码来自openJdk8源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
try {
//获取value变量地址
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
......
private volatile int value; //volatile类型的变量
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
......
}

AtomicInteger.getAndIncrement通过Unsafe.getAndAddInt方法实现:

1
2
3
4
5
6
7
8
9
10
11
/** Unsafe.java **/
//native方法,通过jni来调用
public final native boolean compareAndSwapInt(Object o, long offset,int expected, int x);

public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}

getAndAddInt函数中,首先获取到期望值vv + delta是目标值。然后进入compareAndSwapInt函数,compareAndSwapInt会首先对比实际值与期望值是否相同,如果相同就替换并返回true,while循环退出。如果不相同,compareAndSwapInt就返回false,继续执行while循环读取期望值,然后再次进行对比替换的步骤。

compareAndSwapInt是native函数,源码在hotspot的unsafe.cpp文件中,在现在的系统中对比替换操作都是通过硬件机制来实现。

阅读全文 »

线程池具备如下两个方面的优势:

  1. 当需要创建大量异步任务时会改善应用性能,因为线程池减少了任务的调用开销;
  2. 可以更好的管理线程,比如监控线程使用情况、系统调优等;

下面是线程池类图结构:

Executorvoid execute(Runnable command)ExecutorServiceFuture<?> submit(Runnable task, ...)void shutdown()List<Runnable> shutdownNow()AbstractExecutorServiceThreadPoolExecutorExecutorsForkJoinPoolWorkerThreadRunnable用户任务newnew10~n

线程池分了两个大类:ThreadPoolExecutor是普通线程池,ForkJoinPool可用理解为是一个轻量级任务的线程池。

线程池线程数量管理

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
  1. corePoolSize核心线程数: 当线程池的线程数小于corePoolSize时,会创建一个新的线程来执行新任务,即使有线程处于闲置状态。
  2. maximumPoolSize最大线程数:当线程数已经大于corePoolSize,新任务会被优先添加到队列中等待执行。如果任务队列满了导致添加失败,并且线程数小于maximumPoolSize,就会创建新的线程来执行新任务。大于核心线程数的这些线程算是“借”来的,当借来的这些线程的idle时间超过keepAliveTime,就会被回收。回收逻辑在runWorker函数中。
  3. 如果线程队列满了,线程数也已经大于maximumPoolSize,就会回调RejectedExecutionHandler.rejectedExecution让调用中来处理任务。
添加新任务no核心线程已满创建核心线程执行任务no任务队列已满yes添加到队列等待执行no达到最大线程数yesyes创新新线程执行任务执行拒绝策略

通过一段代码测试一下上述线程数量的限制:

阅读全文 »
0%