【Java】J.U.C并发包 - AQS机制

简介

Java并发包(java.util.concurrent)中提供了很多并发工具,这其中,很多我们耳熟能详的并发工具,譬如ReentrantLock、Semaphore,CountDownLatch,CyclicBarrier,它们的实现都用到了一个共同的基类 - AbstractQueuedSynchronizer,简称AQS。AQS提供了一种原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。

设计思想

同步器背后的基本思想非常简单,可以参考AQS作者 Doug Lea 的论文:The java.util.concurrent Synchronizer Framework。同步器一般包含两种方法,一种是acquire,另一种是release。

  • acquire操作阻塞调用的线程,直到同步状态允许其继续执行。
  • release操作则是改变同步状态,使得一或多个被acquire阻塞的线程继续执行。

其中acquire操作伪代码如下:

1
2
3
4
5
while (synchronization state does not allow acquire) {
enqueue current thread if not already queued;
possibly block current thread;
}
dequeue current thread if it was queued;

release操作伪代码如下:

1
2
3
update synchronization state;
if (state may permit a blocked thread to acquire)
unblock one or more queued threads;

为了实现上述操作,需要下面三个基本组件的相互协作:

  • 同步状态的原子性管理;
  • 线程的阻塞与解除阻塞;
  • 队列的管理;

AQS框架借助于两个类:Unsafe(提供CAS操作) 和 LockSupport(提供park/unpark操作)。

1. 同步状态的原子性管理

AQS类使用单个int(32位)来保存同步状态,并暴露出getStatesetState以及compareAndSet操作来读取和更新这个状态。该整数可以表现任何状态。比如, Semaphore 用它来表现剩余的许可数,ReentrantLock 用它来表现拥有它的线程已经请求了多少次锁;FutureTask 用它来表现任务的状态(尚未开始、运行、完成和取消)。

如JDK的文档中所说,使用AQS来实现一个同步器需要覆盖实现如下几个方法,并且使用getStatesetStatecompareAndSetState这几个方法来设置或者获取状态。

  • boolean tryAcquire(int arg)

  • boolean tryRelease(int arg)

  • int tryAcquireShared(int arg)

  • boolean tryReleaseShared(int arg)

  • boolean isHeldExclusively()

以上方法不需要全部实现,根据获取的锁的种类可以选择实现不同的方法,支持独占(排他)获取锁的同步器应该实现tryAcquiretryReleaseisHeldExclusively而支持共享获取的同步器应该实现tryAcquireSharedtryReleaseSharedisHeldExclusively。下面以 CountDownLatch 举例说明基于AQS实现同步器, CountDownLatch 用同步状态持有当前计数,countDown方法调用 release从而导致计数器递减;当计数器为0时,解除所有线程的等待;await调用acquire,如果计数器为0,acquire 会立即返回,否则阻塞。

参考资料

【Android】动态链接库so的加载原理

前言

最近开发的组件时常出现了运行时加载so库失败问题,每天都会有java.lang.UnsatisfiedLinkError的错误爆出来,而且线上总是偶然复现,很疑惑。所以本文将从AOSP源码简单跟踪Android中的动态链接库so的加载原理,试图找出一丝线索。

加载入口

首先我们知道在Android(Java)中加载一个动态链接库非常简单。就是我们日常调用的 System.load(Sring filename) 或者System.loadLibrary(String libname)开始。 看过《理解JNI技术》的应该知道上述代码执行过程中会调用native层的JNI_OnLoad()方法,一般用于动态注册native方法。

# System.loadLibrary

[System.java]

1
2
3
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}

此处VMStack.getCallingClassLoader()拿到的是调用者的ClassLoader,一般情况下是PathClassLoader。我们进入Runtime类的loadLibrary0()方法看看。

[Runtime.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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError("Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
// 1. 如果classloder存在,通过loader.findLibrary()查找到so路径
if (loader != null) {
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}

// 2. 如果classloder不存在,通过loader.findLibrary()查找到so路径
String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : getLibPaths()) { // getLibPaths()代码在最下方
String candidate = directory + filename;
candidates.add(candidate);

if (IoUtils.canOpenReadOnly(candidate)) {
String error = doLoad(candidate, loader);
if (error == null) {
return; // We successfully loaded the library. Job done.
}
lastError = error;
}
}

// 3. 都没找到,抛出 UnsatisfiedLinkError 异常
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}

【Java】使用Atomic变量实现锁

Atomic原子操作

Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞。

在Atomic包里一共有12个类,四种原子更新方式,分别是原子更新基本类型,原子更新数组,原子更新引用和原子更新字段。Atomic包里的类基本都是使用Unsafe实现的包装类。

  • 原子更新基本类型类: AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
  • 原子更新数组类:AtomicIntegerArray,AtomicLongArray
  • 原子更新引用类型:AtomicMarkableReference,AtomicStampedReference,AtomicReferenceArray
  • 原子更新字段类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

详细介绍可以参考:Java中的Atomic包使用指南

Atomic的原理

下面通过AtomicInteger的源码来看一下是怎么在没有锁的情况下保证数据正确性。首先看一下incrementAndGet()方法的实现:

1
2
3
4
5
6
7
/**
* Atomically increments by one the current value.
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

【Java】NIO的原理与浅析

几个概念

用户空间与内核空间

现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

在Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通过系统调用访问硬件设备。

文件描述符fd

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

【Java】Thread类中的join()方法原理

简介

join()是Thread类的一个方法。根据jdk文档的定义:

public final void join()throws InterruptedException: Waits for this thread to die.

join()方法的作用,是等待这个线程结束;但显然,这样的定义并不清晰。个人认为”Java 7 Concurrency Cookbook”的定义较为清晰:

join() method suspends the execution of the calling thread until the object called finishes its execution.

也就是说,t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,此线程再继续;通常用于在main()主线程内,等待其它线程完成再结束main()主线程。我们来看看下面的例子。

【Android】Retrofit源码分析

Retrofit简介

retrofit n. 式样翻新,花样翻新 vt. 给机器设备装配(新部件),翻新,改型

Retrofit 是一个 RESTful 的 HTTP 网络请求框架的封装。注意这里并没有说它是网络请求框架,主要原因在于网络请求的工作并不是 Retrofit 来完成的。Retrofit 2.0 开始内置 OkHttp,前者专注于接口的封装,后者专注于真正的网络请求。即通过 大量的设计模式 封装了 OkHttp ,使得简洁易用。

我们的应用程序通过 Retrofit 请求网络,实际上是使用 Retrofit 接口层封装请求参数、Header、Url 等信息,之后由 OkHttp完成后续的请求操作,在服务端返回数据之后,OkHttp 将原始的结果交给 Retrofit,后者根据用户的需求对结果进行解析的过程。Retrofit的大概原理过程如下:

  1. RetrofitHttp请求 抽象 成 Java接口
  2. 在接口里用 注解 描述和配置 网络请求参数
  3. 用动态代理 的方式,动态将网络请求接口的注解 解析 成HTTP请求
  4. 最后执行HTTP请求

这篇文章我将从Retrofit的基本用法出发,按照其使用步骤,一步步的探究Retrofit的实现原理及其源码的设计模式。

Retrofit Github: https://github.com/square/retrofit

使用步骤

使用 Retrofit 非常简单,首先需要在 build.gradle 中添加依赖:

1
implementation 'com.squareup.retrofit2:retrofit:2.4.0'

如果需要使用Gson解析器,也需要在build.gradle中添加依赖(后文会详细讲到Retrofit的Converter):

1
implementation 'com.squareup.retrofit2:converter-gson:2.0.2'

1. 定义Interface

Retrofit使用java interface和注解描述了HTTP请求的API参数,比如Github的一个API:

1
2
3
4
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}

【Android】App应用前后台切换的一种监听方法

Android本身并没有提供监听App的前后台切换操作的方法。最近看到一种简单巧妙的方法来监听前后台,这里分享记录一下。

一、Activity生命周期

我们知道在Android中,两个Activity,分别为A和B。假设此时A在前台,当A启动B时,他们俩之间的生命周期关系如下,可以参考之前的这篇文章【Android】Activity与Fragment的生命周期的关系

A.onPause() -> B.onCreate() -> B.onStart() -> B.onResume() -> A.onStop()

也就是说B的onStart()方法是在A的onStop()方法之前执行的,我们可以根据这点来做文章。在所有Activity的onStart()和onStop()方法中进行计数,计数变量为count,在onStart中将变量加1,onStop中减1。

那么这个count在整个App的生命周期里的值就会像下面这样:

1
2
3
4
5
6
count
2| _ _ _ _
1| ____| |___| |___| |___| |____
0| ___| |___
===========================================> 时间
a b c d

横轴表示时间,纵轴表示count的值。那么a、b、c、d四个时间点发生的事情如下:

  1. a点就是启动了应用;
  2. b点是Activity_A启动了另一个Activity_B
  3. c点是Activity_B启动了另一个Activity_C,后面以此类推;
  4. d点是应用切换到了后台;

从上面的情况看出,可以通过对count计数为0,来判断应用被从前台切到了后台。同样的,从后台切到前台也是类似的道理。具体实现看后面的代码。

【Java】线程池ThreadPoolExecutor实现原理

引言

线程池:可以理解为缓冲区,由于频繁的创建销毁线程会带来一定的成本,可以预先创建但不立即销毁,以共享方式为别人提供服务,一来可以提供效率,再者可以控制线程无线扩张。合理利用线程池能够带来三个好处:

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

但是要做到合理的利用线程池,必须对其原理了如指掌。

线程的几种状态

线程在一定条件下,状态会发生变化。根据线程的几种状态这篇文章,线程一共有以下几种状态:

1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权,即在就绪状态的线程除CPU之外,其它的运行所需资源都已全部获得。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

  • ①. 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的必须依靠其他线程调用notify()notifyAll()方法才能被唤醒。
  • ②. 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
  • ③. 其他阻塞:运行的线程执行sleep()join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时,或者I/O处理完毕时,线程重新转入就绪状态。

5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

【Android】OkHttp源码分析

Android为我们提供了两种HTTP交互的方式:HttpURLConnection 和 Apache HttpClient,虽然两者都支持HTTPS,流的上传和下载,配置超时,IPv6和连接池,已足够满足我们各种HTTP请求的需求。但更高效的使用HTTP 可以让您的应用运行更快、更节省流量。而OkHttp库就是为此而生。在开始分析OkHttp之前我们先了解一下 HttpURLConnection 和 HttpClient 两者之间关系,以及它们和 OkHttp 之间的关系。

HttpClient vs HttpURLConnection

在Android API Level 9(Android 2.2)之前只能使用HttpClient类发送http请求。HttpClient是Apache用于发送http请求的客户端,其提供了强大的API支持,而且基本没有什么bug,但是由于其太过复杂,Android团队在保持向后兼容的情况下,很难对DefaultHttpClient进行增强。为此,Android团队从Android API Level 9开始自己实现了一个发送http请求的客户端类 —— HttpURLConnection

相比于DefaultHttpClientHttpURLConnection比较轻量级,虽然功能没有DefaultHttpClient那么强大,但是能够满足大部分的需求,所以Android推荐使用HttpURLConnection代替DefaultHttpClient,并不强制使用HttpURLConnection

但从Android API Level 23(Android 6.0)开始,不能再在Android中使用HttpClient,强制使用HttpURLConnection。参考官网:Android 6.0 Changes - Google Developer

Android 6.0 版移除了对 Apache HTTP client的支持。如果您的应用使用该客户端,并以 Android 2.3(API level 9)或更高版本为目标平台,请改用 HttpURLConnection 类。此 API 效率更高,因为它可以通过透明压缩和响应缓存减少网络使用,并可最大限度降低耗电量。要继续使用 Apache HTTP API,您必须先在 build.gradle 文件中声明以下编译时依赖项:

1
2
3
android {
useLibrary 'org.apache.http.legacy'
}

用Java实现断点续传 (HTTP)

断点续传的原理

其实断点续传的原理很简单,就是在 Http 的请求上和一般的下载有所不同而已。
打个比方,浏览器请求服务器上的一个文时,所发出的请求如下:
假设服务器域名为 www.sjtu.edu.cn,文件名为 down.zip。

1
2
3
4
5
6
7
GET /down.zip HTTP/1.1 
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-
excel, application/msword, application/vnd.ms-powerpoint, */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)
Connection: Keep-Alive

服务器收到请求后,按要求寻找请求的文件,提取文件的信息,然后返回给浏览器,返回信息如下:

1
2
3
4
5
6
7
8
200 
Content-Length=106786028
Accept-Ranges=bytes
Date=Mon, 30 Apr 2001 12:56:11 GMT
ETag=W/"02ca57e173c11:95b"
Content-Type=application/octet-stream
Server=Microsoft-IIS/5.0
Last-Modified=Mon, 30 Apr 2001 12:56:11 GMT


Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2019 iTimeTraveler All Rights Reserved.

访客数 : | 访问量 :