博客简介

前言

首先欢迎各位,来到本人的博客,如果博客有什么地方说的不对,请发邮件联系笔者,笔者会在第一时间进行改正,希望和大家一起进步。邮箱如下1065982915@qq.com

本人简介

笔者也和大多数技术宅一样,深爱着编程,追求平滑的用户操作,以及快速的响应速度,一直是笔者的心愿,笔者在技术成长上面遇到的最困难的事,便是技术点网上够多,但是技术体系网上少之又少,所以自己想做一些专题,从最开始的到最后的技术难d点全部攻破。现在还在研究。本博客的技术体系,会慢慢的建立起来。 希望大家给予鼓励。因为在技术的道路上面,是迷茫和寂寞的。

爱好:写作,摄影 有相同爱好的小伙伴,可以发邮件一起玩哦

博客目录

Java


JVM


数据结构


算法


设计模式


协议


Android

持续更新中 ····


小结

现在博客内容还比较稀少,笔者相信以后的博客会越来越全面的,笔者最近在研究Gradle编译相关知识,如果有什么最新的技术,可以发邮件给笔者,笔者会研究,最后会考虑是否写成专题。

最后祝大家,早日富可敌国。

Android Q 应用存储空间限制

应用存储空间限制

1、背景介绍

Android 相比于 IOS 来说,外部存储一直都是很混乱的,为了改变相应的这种生态,Android Q 版本对此进行了大量的整改,使用更加精细的划分模式来最大程度的给予App更高的隐私性。

1、适配范围

应用存储空间限制仅适用于以 Android Q 为目标的应用程序,(targetSDK = 28) 或者在运行Android Q的设备上新安装的应用。

当满足以下每个条件时,系统会将应用程序的文件访问权限置于兼容模式:

  • 1、您的应用针对Android 9(API级别28)或更低版本。
  • 2、您的应用安装在从Android 9升级到Android Q的设备上。

2、整改内容

1、每个应用程序持有私有文件的独立存储沙箱

每个App,Android Q 都会创建一个独立的存储沙箱,限制其他应用程序访问您的应用程序的外部存储设备上的文件。常见的外部存储设备是/sdcard,

该方案具有以下优点:

  • 需要更少的权限。应用程序沙箱中的文件对您的应用程序是私有的。因此,不再需要权限在外部存储中访问、保存自己的文件。(READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE)
  • 相对于设备上的其他应用程序,隐私性更强。没有其他应用可以直接访问您应用的独立存储沙箱中的文件。此访问限制使您的应用程序更容易维护沙盒文件的隐私。

注意:如果用户卸载了您的应用,则会清除隔离存储沙箱中的文件。(这点有点类似于 IOS)

获取 Android Q 为每一个 App 分享的独立存储沙箱,可以调用如下方法

1
Context.getExternalFilesDir(String type)

而type的取值为以下这几种

  • {@link android.os.Environment#DIRECTORY_MUSIC},
  • {@link android.os.Environment#DIRECTORY_PODCASTS},
  • {@link android.os.Environment#DIRECTORY_RINGTONES},
  • {@link android.os.Environment#DIRECTORY_ALARMS},
  • {@link android.os.Environment#DIRECTORY_NOTIFICATIONS},
  • {@link android.os.Environment#DIRECTORY_PICTURES}, or
  • {@link android.os.Environment#DIRECTORY_MOVIES}.

2、每个应用程序持有公共媒体的共享集合

每个App,Android Q 都会创建出一个公共的存储区域,供里面的文件独立于 App 之外,当 App 卸载以后,保留该文件,例如:通过 App 下载下来的音乐文件,在这个区域,当你的app需要修改和读取由该APP创建的文件,则不需要任何权限,而如果需要读取和修改其他App创建的文件,则需要申请权限。关于具体权限如下所示:

  • 读取 Photos 相关文件,则需要 READ_MEDIA_IMAGES
  • 读取 Videos 相关文件,则需要 READ_MEDIA_VIDEO
  • 读取 Music 相关文件,则需要 READ_MEDIA_AUDIO

关于上述,还有两个比较特殊的存在

  • There’s no permission for accessing the Downloads shared collection. Your app can access its own files in this collection. To access other apps’ files in this collection, however, you must allow the user to choose a file using the system’s file picker app.
  • If your app uses the Storage Access Framework, it doesn’t need to request these media-scoped permissions.
1、访问共享集合

APP 申请了相应的权限以后,可以使用 MediaStore API 来访问这些共享文件。

  • 对于 Photos 相关文件,使用 MediaStore.Images
  • 对于 Videos 相关文件,使用 MediaStore.Video.
  • 对于 Music 相关文件,使用 MediaStore.Audio.
  • 对于 Downloads 相关文件,使用 MediaStore.Downloads.

适配点 : 在 Android Q 版本之后,调用 System 提供的 getExternalStoragePublicDirectory()方法,只会返回该 APP 的独立沙盒目录,要完成适配,则要一直持有 READ_MEDIA_* 权限,然后更新你的存储框架即可。

要访问手机当中的媒体文件,请使用MediaStore检索该文件,然后通过相应的文件描述符就可以访问该文件

2、APP 文件写入共享集合

Android Q 版本上,当用户卸载 App 的时候,System 会清除 App 所包含的数据,(包含 system 提供给 APP 的独立存储沙箱),如果 APP 想在卸载 APP 后保存部分数据的话,就需要将文件保存在共享集合当中。如何将文件写入共享集合?

使用 MediaStore 集合中插入新行,使用以下方式填充其列

  • 1、为 DISPLAY_NAME 和 MIME_TYPE 列提供值
  • 2、您可以使用 PRIMARY_DIRECTORY 和 SECONDARY_DIRECTORY 列来影响文件在磁盘上的放置位置
  • 3、保持 DATA 列未定义。这样,该平台可以灵活地将文件保存在沙箱之外.
  • 4、使用API ContentResolver.openFileDescriptor()来读取或写入新创建的文件的数据

    但是,如果用户稍后重新安装了 APP ,则 APP 无法访问这些文件,情况类似于应用程序尝试访问其他应用程序文件的情况

3、访问其他应用创建的文件

要访问和读取其他应用程序已保存到外部存储设备的媒体文件,请完成以下步骤:

  • 1、权限
  • 2、使用ContentResolver对象查找并打开文件, 请关注(ContentResolver loadThumbnail 方法)
4、写入其他应用创建的文件

通过将文件保存到共享集合,您的应用程序将成为该文件的所有者。通常,只有当您是文件所有者时,您的应用才能写入共享集合中的文件。但是,如果您的应用程序充当特定用例的用户默认应用程序,您还可以写入其他应用程序拥有的文件

  • 1、如果您的应用是用户的默认Photo Manager应用,则可以修改其他应用保存到“ 照片和视频”共享集合中的图像文件。
  • 2、如果您的应用是用户的默认音乐应用,则可以修改其他应用保存到音乐共享收藏集的音频文件。

==注意:无论是默认的照片管理器还是音乐应用,您的应用都应保持正常运行。==

要修改其他应用最初保存到外部存储设备的媒体文件,请使用ContentResolver对象查找文件并进行就地修改。执行编辑/修改操作时,请捕获, RecoverableSecurityException以便您可以请求用户授予您对该特定项目的写入权限。

3、APP 访问媒体文件

1、访问照片 (注意事项)

Android Q 增加了一些功能,使用户可以更好地控制在外部存储上访问照片的方式。总所周知,在图片文件当中会包含一些 Exif 元数据,通过这些数据,可以访问关于照片的一些详细信息,例如图片的位置信息,而在 Q 版本之上,为了更加安全的保护用户的隐私,对这些 Exif 元数据做了一定的限制。

==如果在 Android 的 System Package Manager 当中。你的应用是默认的 Photos Manager 应用,那么 System会自动让你的应用访问这些照片的位置信息==

除了上述着一种特殊情况,默认其他的应用如果需要访问图片的位置信息,需要满足以下两种情况

  • 1、权限 ACCESS_MEDIA_LOCATION
  • 2、从MediaStore对象,调用setRequireOriginal(),传入照片的URI。

例如,以下代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Uri photoUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cursor.getString(idColumnIndex));
final double[] latLong;
if (BuildCompat.isAtLeastQ()) {
photoUri = MediaStore.setRequireOriginal(photoUri);
InputStream stream = getContentResolver().openInputStream(photoUri);
if (stream != null) {
ExifInterface exifInterface = new ExifInterface(stream);
double[] returnedLatLong = exifInterface.getLatLong();
latLong = returnedLatLong != null ? returnedLatLong : new double[2];
stream.close();
} else {
latLong = new double[2];
}
} else {
latLong = new double[]{cursor.getFloat(latitudeColumnIndex), cursor.getFloat(longitudeColumnIndex)};
}

如果您的应用是相机应用,则它无法直接访问照片和视频共享集中保存的照片,除非它是设备的默认Photo Manager应用。要将用户定向到图库应用,请使用 ACTION_REVIEW意图

2、访问媒体文件 (注意事项)

应用需要访问特定的媒体文件,例如其他应用分享文件给该应用,或需要访问来自用户媒体集的文件。在这些情况下,优先获取文件的uri,然后通过 content resolver 获取文件描述符。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Uri contentUri = ContentUris.withAppendedId(android.provider.MediaStore.
Audio.Media.EXTERNAL_CONTENT_URI, cursor.getLong(Integer.parseInt(BaseColumns._ID)));
String fileOpenMode = "r";
ParcelFileDescriptor parcelFd = null;
try {
parcelFd = this.getContentResolver().openFileDescriptor(contentUri, fileOpenMode);
if (parcelFd != null) {
int fd = parcelFd.detachFd();
// 获取到相应的文件描述符以后,即可以操作该文件。
}
} catch (FileNotFoundException e) {
e.printStackTrace();
// 错误处理(文件没有找到)
} finally {
// 清理操作
}
3 、访问特定文件

在某些用例中,您的应用可能需要打开或创建无权访问的文件:

  • 1、在照片编辑应用中,打开一张图纸。
  • 2、在业务生产力应用程序中,将文本文档保存到用户选择的位置。

对于这些情况,请使用存储访问框架,该框架允许用户选择要打开的特定文件,或选择特定位置来保存文件。

4、外部存储设备

在Android 9(API级别28)及更低版本中,所有存储设备上的所有文件都显示在单个”external”卷(主卷 – 类似于Windows上面的C盘)名称下。Android Q为每个外部存储设备提供唯一的卷名。此命名系统可帮助您有效地组织和索引内容,并使您可以控制新内容的存储位置。

要唯一标识外部存储中的特定文件,必须同时使用卷名和ID。例如,

  • 主存储设备上的文件将是content://media/external/images/media/12,
  • 但是被调用的辅助存储设备上的相应文件FA23-3E92将是 content://media/FA23-3E92/images/media/12。

您可以通过将此卷名称传递到特定的媒体集合来访问存储在特定卷上的文件,例如 MediaStore.Images.getContentUri()。

1、获取外部存储设备列表

要获取所有当前可用卷的名称列表,请调用 MediaStore.getAllVolumeNames(),如以下代码段所示

1
Set<String> volumeNames = MediaStore.getAllVolumeNames(context);

3、测试

设置虚拟外部存储设备

在没有可移动外部存储的设备上,使用以下命令启用虚拟磁盘以进行测试:

1
adb shell sm set-virtual-disk true

为了帮助您使应用程序与此新行为更改兼容,该平台提供了多种方法来调整与更改相关的多个参数。

切换行为更改
要在Android Q Beta 1中启用此行为更改,请在终端窗口中执行以下命令:

1
adb shell sm set-isolated-storage on

运行此命令后,设备将重新启动。如果没有,请等一下再尝试再次运行该命令。

要确认行为更改已生效,请使用以下命令:

1
adb shell getprop sys 。isolated_storage_snapshot

测试兼容模式行为
测试应用程序时,可以 通过在终端窗口中运行以下命令来启用外部文件存储访问的兼容性模式:

1
adb shell cmd appops设置your-package-name android:legacy_storage allow

要禁用兼容模式,请在Android Q上卸载并重新安装您的应用,或在终端窗口中运行以下命令:

1
adb shell cmd appops设置your-package-name android:legacy_storage default

4、特性总结

    1. Android Q 为每个应用程序在外部存储设备提供了一个独立的存储沙箱,应用通过路径创建的文件都保存在应用的沙箱目录
    1. 共享集合:如果应用的一些文件是用户选择下载保存的,应用卸载的时候用户不希望删除,这部分文件开发者可以通过MediaProvider接口保存在公共共享集合,包括:照片、视频、音乐和下载集合
    1. 新的访问多媒体文件的权限:应用读写自己创建的文件不需要权限,但是如果需要读取其他应用创建的多媒体文件就需要申请对应的权限,通过MediaProvider接口读取
    1. 读写其他应用的下载公共集合文件需要使用SAF的方式读写
    1. 目前版本该特性没有默认开启,需要开发者通过命令开启:adbshell smset-isolated-storage on
    1. 参考谷歌提供的官方适配指导文档:https://developer.android.google.cn/preview/privacy/scoped-storage

性能优化1 - 内存抖动

  • 什么是内存抖动
  • 内存抖动的影响
  • 引起内存抖动的原因
  • 如何发现内存抖动
  • 内存抖动常见的解决方式

Android 是基于 Java ,不管是刚刚接触 Android 的新手,还是混迹在开发前线的老手,都知道在 Android上面的开发,不用管内存的回收,需要对象的时候 new 出来就好了,其余的 GC 会帮助我们,但是 GC 也不是万能的,GC 机制帮助我们的同时,也会限制我们,俗话说有得有失嘛。今天来看一下由于这种机制给我们带来的第一个坑,内存抖动。

什么是内存抖动

内存回收是指在程序需要对象的时候,在堆当中分配出来一块空间,使用完毕以后,由 GC 帮我们清理掉这片内存空间。如果频繁的一直持续上述操作,就会引起内存抖动。也就是有源源不断的对象被创建,同时又有源源不断的对象被回收。

内存抖动的影响

  • 导致程序卡顿
  • 导致程序内存溢出

出现上述情况的内存抖动,会对程序造成什么影响呢,首先我们知道 GC 回收的时候也是需要消耗资源的,比如 GC 回收的时候会停止掉所有的线程,如果触发一次 GC 就停止一次,那如果多次频繁的触发 GC 线程也会一卡一卡的,所以总的来说内存抖动可能会造成程序的卡顿。

如果发生频繁的 GC 会导致出现不连续的内存片无法进行分配,导致 OOM 内存溢出。

注意:Android 垃圾回收机制使用的是 CMS 垃圾回收器 (采用标记 - 清理算法)

引起内存抖动的原因

内存抖动出现原因主要是频繁在循环里创建对象,导致大量对象在短时间内被创建,由于新对象是要占用内存空间的而且是频繁,如果一次或者两次在循环里创建对象对内存影响不大,不会造成严重内存抖动这样可以接受也不可避免,频繁的话就很内存抖动很严重。

如何发现内存抖动

首先我们先来看一个简单的 Demo

我们知道 Java 当中的字符串对象是不可变的,如果累加的话。会将原来的 Copy 出来一份。然后进行累加成为新的字符串,原来的就会被回收掉,现在我们就利用这个特性来模拟一个简单的内存抖动现象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void onClick (View view) {
for (int i = 0; i < 1000; i++) {
String tempTestString = " ";
//拼接打印
for (int j = 0; j < 200; j++) {
tempTestString += " ++";
tempTestString += " --";
}
}
}
}

这样必然会出现内存抖动,我们通过 AS 自带的工具观察一下内存的使用情况,如下所示:

内存分析图

有小白色的垃圾桶,说明触发了 GC ,有一个代表出发了一次 GC ,上图有多个,然后我们选定一个时刻,仔细的看一下对象,例如下图

内存抖动分析图

可以看到对象最多便是char数组,和 String 类型,然后点击 String,随便选取一个实例,查看他的堆栈,双击 便可以定位到我们的代码当中,然后结合详细的代码,进行分析,便可以规避上述的问题。

内存抖动分析图

内存泄漏常见的解决方式

  • 尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
  • 注意自定义View的onDraw()方法会被频繁调用,所以在这里面不应该频繁的创建对象。
  • 当需要大量使用Bitmap的时候,试着把它们缓存在数组中实现复用。
  • 对于能够复用的对象,同理可以使用对象池将它们缓存起来。

参考文献:

https://www.cnblogs.com/xgjblog/p/9042458.html
https://blog.csdn.net/wanghao200906/article/details/79311288
https://blog.csdn.net/nicolelili1/article/details/89219191
https://blog.csdn.net/qq_31370269/article/details/52818277
https://blog.csdn.net/wanghao200906/article/details/79311288
https://www.youtube.com/watch?v=McAvq5SkeTk

Android Gradlew编译相关命令

1、前言

gradlew 是什么东西呢,和gradle貌似不大一样,肯定有关系。没错,他就是 gradle wrapper,意思是gradle的一个包装,大家可以理解为在这个项目本地就封装了gradle,比如我的项目是HelloWord, 在HelloWord/gradle/wrapper/gralde-wrapper.properties文件中声明了它指向的目录和版本,比如我的内容是:

1
2
3
4
5
6
#Thu Nov 01 14:28:47 CST 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip

如果你项目没有该文件的话,那么gradlew会到~/.gradle/wrapper/dists/gradle-2.10-all下寻找,或者你新建给文件,内容复制到里面。其实你会发现是同一个目录咯。里面会有个随机数的文件夹,里面就是gradle,只要下载成功即可用grdlew wrapper的命令代替全局的gradle命令。

常用命令如下:(linux下是./gradlew,该脚本在项目下,windows直接gradlew即可)

2、命令

  • ./gradlew -v 版本号,首次运行,没有gradle的要下载的哦。
  • ./gradlew clean 删除HelloWord/app目录下的build文件夹
  • ./gradlew build 检查依赖并编译打包
    • ./gradlew assembleDebug 编译并打Debug包
    • ./gradlew assemblexiaomiDebug 编译并打xiaomi的debug包,其他类似
    • ./gradlew assembleRelease 编译并打Release的包
    • ./gradlew assemblexiaomiRelease 编译并打xiaomi的Release包,其他类似
    • ./gradlew installRelease Release模式打包并安装
    • ./gradlew uninstallRelease 卸载Release模式包

./gradlew build 检查依赖并编译打包

这里注意的是 ./gradlew build 命令把debug、release环境的包都打出来,生成的包在目录HelloWord/app/build/outputs/apk/下。如果正式发布只需要打release的包,

gradlew build 和 gradle build 有区别吗?

使用gradle wrapper是gradle官方推荐的build方式,而gradlew正是运行了wrapper task之后生成的(运行wrapper task是Android Studio自动做的)。使用gralde wrapper的一个好处就是每个项目可以依赖不同版本的gradle,构建的时候gradle wrapper会帮你自动下载所依赖的版本的gradle。而如果你使用gradle build的话,同时你又有多个项目使用不同版本的gradle,那就需要你手动在自己的机器上配置多个版本的gradle,稍微麻烦一些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

./gradlew clean clean项目

./gradlew build 构建项目

/gradlew assembleDebug or /gradlew aD 编译并打Debug包

./gradlew assembleRelease or /gradlew aR 编译并打Release的包

./gradlew installRelease or /gradlew iR Release模式打包并安装

./gradlew installDebug or /gradlew iD Debug模式打包并安装

./gradlew uninstallRelease or ./gradlew uR 卸载Release模式包

./gradlew uninstallDebug or ./gradlew uD 卸载Debug模式包


需要说明的是这些命令可以叠加使用,例如:

./gradlew clean build --info > bugtags.log

Android O 后台限制解决方式

1、前言

Android O版本上面,Android做了一定的限制,目的就是加快Android的运行速度,导致在Android O版本上面,如果后台组件想要启动服务不允许,这样的操作导致很多应用都需要适配,改动大量的代码,在这里,如果Google官方给出了三种解决方式,AlarmManager、SyncAdapter、JobScheduler来代替服务,这里主要讲解如何使用这这三种方式如何应用

  • AlarmManager
  • SyncAdapter
  • JobScheduler

2、AlarmManager

AlarmManager通常用途是用来开发手机闹钟,但是AlarmManager的用处并只是这个。AlarmManager其实是一个全局定时器,它可以在指定时间或指定周期启动其他组件(Activity、Service、BroadcastReceiver),Alarm Manager适用于希望在特定时间运行应用程序代码的情况,即使应用程序当前未运行也是如此。对于正常的定时操作(刻度,超时等),使用起来更容易,更有效.

2.1、AlarmManager使用的一些坑

  • 华为手机上kill应用后,无法唤醒Alarm. 大部分原因都是手机为Alarm设置了统一的唤醒时间
  • 不能精确启动闹钟服务 (API19以上是无法精确时间的)
  • 华为手机上休眠无法启动闹钟服务

2.2、AlarmManager的主要方法:

  • set(int type,long triggerAtTime,PendingIntent operation) : 设置指定triggerAtTime时间启动由operation参数指定组件

    type : 定时服务的类型,有以下四种取值

    • ElAPSED_REALTIME: 指定从现在开始过了一定时间后启动operation所对应的组件。
    • ELASPED_REALTIME_WAKEUP: 指定从现在开始时间过了一定时间operation所对应的组件。即使系统处于休眠状态也会执行也会执行operation所对应的组件。
    • RTC: 指定当系统调用System.currentTimeMillis()方法的返回值与triggerAtTime相等时启动operation所对应的组件。
    • RTC_WAKEUP:指定当系统调用System.currentTimeMillis()方法的返回值与triggerAtTime相等时启动operation对应的组件,即使系统休眠状态也会执行operation所对应的组件。
  • setInexactRepeating(int type,long triggerAtTime,long interval,PendingIntent operation) : 设置非精确的周期性任务。例如,我们设置Alarm每个小时启动一次,但是系统不一定总在每个小时的开始启动Alarm服务。

  • setRepeating(int type,long triggerAtTime,long interval,PendingIntent operation) :设置一个周期性执行的定时服务。
  • cancel(PendingIntent operation) :取消AlarmManager的定时服务。

2.3、注意

API 19(Android4.4)开始,AlarmManager的机制是非准确激发的,操作系统会偏移(shift)闹钟来最小化唤醒和电池消耗。不过AlarManager新增了如下两个方法来支持精确激发。

  • setExact(int type long triggerAtMillis,PendingIntent operation) :设置闹钟闹钟将在精确的时间被激发。
  • setWindow(int type,long windowStartMillis,long windowLengthMillis,PendingIntent operation) :设置闹钟将在精确的时间段内被激发。

很显示API19以后无法使用setInexactRepeating()和setRepeating(),也就是无法设置重复闹钟,唯一解决的方式,也只有启动闹钟的时候再设置一次闹钟,也就变相地实现了重复闹钟了。
API19以下使用setExact()和setWindow()将会报没有匹配的方法
java.lang.NoSuchMethodError: android.app.AlarmManager.setExact

解决办法是判断SDK版本,根据SDK版本来定义不同的方法。

1
int sdkVersion = Integer.valueOf(Build.VERSION.SDK_INT);

2.4、做一个小Demo,就是通过使用AlarmManager来做一个闹钟的小Demo

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
MainActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.setDate).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Calendar currentTime = Calendar.getInstance();
new TimePickerDialog(MainActivity.this, new TimePickerDialog.OnTimeSetListener() {

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
Intent i=new Intent(MainActivity.this,AlarmActivity.class);
//创建PendingIntent对象
PendingIntent pi=PendingIntent.getActivity(MainActivity.this,0,i,0);
Calendar c=Calendar.getInstance();
c.setTimeInMillis(System.currentTimeMillis());

//Calendar.HOUR这是12小时制因为无论你的TimePickerDialog设置的是12还是24,hourOfDay默认获取的是24小时制的
//根据用户的选择的时间来设置Calendar对象 c.set(Calendar.HOUR_OF_DAY,hourOfDay);
c.set(Calendar.MINUTE,minute);
//获取AlarmManager
AlarmManager am= (AlarmManager) getSystemService(ALARM_SERVICE);
Log.i(TAG, "onTimeSet: "+SystemInfoUtil.getSDKVersionNumber());
if (SystemInfoUtil.getSDKVersionNumber()>=19){
//API19以上使用
am.setExact(AlarmManager.RTC_WAKEUP,c.getTimeInMillis(),pi);
}else {
am.set(AlarmManager.RTC_WAKEUP,c.getTimeInMillis(),pi);
}
Toast.makeText(MainActivity.this,"设置闹钟成功", Toast.LENGTH_LONG).show();
}
},currentTime.get(Calendar.HOUR_OF_DAY),currentTime.get(Calendar.MINUTE),false).show();
}
});
}


AlarmActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_alarm);
// 加载音乐
try {
// String path = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_RINGTONE).getPath();
AssetFileDescriptor openFd=getAssets().openFd("music.mp3");
AudioPlayer.getInstance().play(openFd);
AudioPlayer.getInstance().setLooping(true);
} catch (IOException e) {
e.printStackTrace();
}
new AlertDialog.Builder(this)
.setTitle("闹钟")
.setTitle("时间到!!!!! ")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
AudioPlayer.getInstance().stop();
}
})
.create().show();
}

3、SyncAdapter

https://developer.android.com/training/sync-adapters/creating-sync-adapter

4、JobScheduler

Android 不允许后台服务启动服务,所以官方推荐使用JobScheduler来代替原有的后台监视服务,在很多情况下,应用都可以使用 JobScheduler 作业替换后台服务。 例如,CoolPhotoApp 需要检查用户是否已经从朋友那里收到共享的照片,即使该应用未在前台运行。8.0以前应用使用一种会检查其云存储的后台服务。 为了迁移到 Android 8.0,开发者使用一个计划作业替换了这种后台服务,该作业将按一定周期启动,查询服务器,然后退出

Android 5.0的时候,Google就开始提倡使用该工作分发器来优化电量等资源,随着6.0、7.0、8.0版本的到来,JobScheduler慢慢代替后台服务的运行,JobScheduler到底应该如何使用呢?

开发者需要在稍后的某个时间点或者满足某个特定的条件时去执行某个任务,例如当设备开始充电,或者网络状态连接到wifi状态时执行某些推送通知的任务,jobscheduler就是用来处理这类场景的任务。

Jobscheduler的android在5.0上针对于降低功耗而提出来的一种策略方案,自 Android 5.0 发布以来,JobScheduler 已成为执行后台工作的首选方式,其工作方式有利于用户。应用可以在安排作业的同时允许系统基于设备状态、电源和连接情况等具体条件进行优化。JobScheduler 可实现控制和简洁性,谷歌推出该机制是想要所有应用在执行后台任务时使用它。(还有一点需要注意的是,在7.0上谷歌给出建议:在 Android 7.0 中,删除了三个常用隐式广播 —CONNECTIVITY_ACTION、ACTION_NEW_PICTURE 和ACTION_NEW_VIDEO— 因为这些广播可能会一次唤醒多个应用的后台进程,同时会耗尽内存和电量。如果应用需要收到这些广播,充分利用 Android 7.0 以迁移到 JobScheduler 和相关的 API。

4.1、如何使用Job

应用如果想使用JobScheduler API的话,首先需要创建自己需要执行的任务信息,创建任务的方法在谷歌官方文档上已经有详细介绍,这里只是放出一个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);  
ComponentName jobService = new ComponentName(this, MyJobService.class);
JobInfo jobInfo = new JobInfo.Builder(100012, jobService) //任务Id等于100012
.setMinimumLatency(5000)// 任务最少延迟时间为5s
.setOverrideDeadline(60000)// 任务deadline,当到期没达到指定条件也会开始执行
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)// 需要满足网络条件,默认值NETWORK_TYPE_NONE
.setPeriodic(AlarmManager.INTERVAL_DAY) //循环执行,循环时长为一天(最小为15分钟)
.setRequiresCharging(true)// 需要满足充电状态
.setRequiresDeviceIdle(false)// 设备处于Idle(Doze)
.setPersisted(true) //设备重启后是否继续执行
.setBackoffCriteria(3000,JobInfo.BACKOFF_POLICY_LINEAR) //设置退避/重试策略
.build();
scheduler.schedule(jobInfo);

上面的一个任务需要满足充电状态,并且设备不出于idle状态,并且需要网络处于非计费类型时,会运行该job,自己在构建自己的job 时候需要按需设置触发条件。但是这里需要注意的一个点是JobScheduler所创建并执行的任务必须是带有条件限制的,不然是违背其初衷的,当你创建一个任务,并且不设置任何限制条件并且直接调用 scheduler.schedule(builder.build());去执行该任务是不可行的,会报以下的异常

1
java.lang.IllegalArgumentException: You're trying to build a job with no constraints, this is not allowed.
  1. setMinimumLatency(long minLatencyMillis): 设置任务的最小延迟执行时间(单位是毫秒)。
  2. setOverrideDeadline(long maxExecutionDelayMillis): 设置任务最晚的延迟时间。如果到了规定的时间时其他条件还未满足,你的任务也会被启动。
  3. setPersisted(boolean isPersisted): 设置当设备重启之后该任务是否还要继续执行。
  4. setExtras(PersistableBundle extras): 设置传递bundler参数
  5. setRequiredNetworkType(int networkType):设置需要满足网络类型
  6. setRequiresCharging(boolean requiresCharging):设置是否需要充电状态下运行
  7. setRequiresDeviceIdle(boolean requiresDeviceIdle):设置是否需要在Idle(Doze)状态下运行(7.0上新加的)
  8. addTriggerContentUri(@NonNull TriggerContentUri uri):设置监控ContentUri 发生改变时运行(7.0上新加的)
  9. setPeriodic(long intervalMillis):设置循环执行的时长

具体条件可以参考Google官方网站,

https://developer.android.com/reference/android/app/job/JobScheduler

当需要使用JobScheduler 来干实际的任务时,需要新建一个service,来继承JobService(这一点与DreamService类似),而且必须重写其中的两个方法,分别是onStartJob(JobParameters params)和onStopJob(JobParameters params);
在startJob里来执行自己的代码逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyJobService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
Log.i("MyTest", "onStartJob @@@@@@@@ " + params);
jobFinished(params,true);
return false;
}

@Override
public boolean onStopJob(JobParameters params) {
return false;
}
}

这里需要注意的一点是,当做完自己的任务要及时调用JobFinished 来结束自己的任务。在6.0上在如果不调用该接口会造成严重的功耗问题。

当上面创建任务时执行到scheduler.schedule(builder.build()); 则开始准备执行任务,一旦满足设置的条件,便会执行到onStartJob()方法,也就是在我们的任务应该具体事宜应该是放在onStartJob中去做的。该方法返回值为为一个boolean值,如果返回值是false,系统假设这个方法返回时任务已经执行完毕,如果返回值是true,那么系统假定这个任务正要被执行,执行任务的重担就落在了你的肩上(这时候需要去新开一个线程去做事物,文档接口上有起描述)。当任务执行完毕后要调用jobFinished()来通知系统。

当系统受到一个cancel请求时会取消该任务(当该任务未执行将其在pending list删除,如果该任务正在执行则停止其任务)。
使用Jobscheduler还需要到AndroidManifest.xml中添加一个service节点让你的应用拥有绑定和使用这个JobService的权限。

<service android:name=”com.example.suansuan.jobtest.MyJobService”
android:permission=”android.permission.BIND_JOB_SERVICE” />

4.2、Job的使用出现的坑

  1. 小心JobService运行在主线程:上面已经说明了,当schedule是在主线程调用的,那么jobsevice则运行则主线程,需要小心主线程ANR 以及严苛模式抛出异常
  2. 小心cancelAll(): 该方法的功能是取消该uid下的所有jobs,也就是说当存在多个app通过shareUid的方式,那么在其中任意一个app执行cancalAll(),则会把所有同一uid下的app中的jobs都cancel掉。
  3. Jobscheduler如果设置了setPersisted(true),则重启后还会再运行,不能使用HashCode 来做JobId
  4. job执行完了,一定要记得要调用JobFinished
  5. 同一个包名下,不能有两个相同的jobId ,如果一个app非常大,两个功能模块为两个程序员维护,则很容易产生沟通不足的情况下,使用了两个相同的JobId,此时只能有一个job生效。另一个无效。该问题可以通过终端命令行: adb shell dumpsys jobscheduler 来查看单个app 的job信息确认

总结

JobScheduler 虽然是在5.0上新增加的一个新服务,但是从L到M,N以及最新的O 上,谷歌Android也是在重点推荐使用该功能,并且在Android O 上谷歌还推出了一套Android vitals 计划,旨在提高Android 系统的功耗,性能,以及稳定性等相关指标,在对功耗上提出来的建议便是,非精确性的定时任务建议使用Job来代替Alarm,能更加准确的满足条件的执行你想要执行的任务。在Android O上JobScheduler更加完善了其条件控制,加上了低存储,低电量策略下的job运行限制,这里将在后面job服务解析中继续提到

线程延时队列DelayQueue

  • 1、DelayQueue的作用
  • 2、DelayQueue使用规则
  • 3、DelayQueue的Demo
  • 4、DelayQueue原理 *
  • 5、DelayQueue的实现 *

1、DelayQueue的作用

作用便是线程调度机制通过粒度特别细的时间控制相应的元素在合适的时间出队,使用场景也是非常多的比如:缓存机制当中使用这个队列来控制缓存队列当中的缓存是否过期,如果过期则执行缓存过期以后的相应逻辑。

2、DelayQueue使用规则

DelayQueue从名字,我们知道是一个队列,并且继承自BlockingQueue,是一个阻塞队列,那么就有着阻塞队列的特点,当队列为空的时候,take方法将阻塞,这里需要注意的便是DelayQueue是一个无届队列,也就意味着,没有当队列为满的时候,所以就没有队满。put/add 阻塞的情况,其次DelayQueue最显著的特点便是,这个队列当中根据时间为优先级对队中的元素进行排序,如果我们take一个元素的时候,这个元素的时间值没有到,便会自动阻塞,当时间到以后take到元素。在DelayQueue这个队列当中所描述的任何一种元素必须实现Delay接口,而这个接口当中便定义了两个方法,我们通过这两个方法来进行队当中的排序,以及take的时候,相应的时间粒度的控制。

时间最长的在对头还是时间最短的在对头,这个主要看如何实现元素的 compareTo 方法。

  • public int compareTo(T o);
  • long getDelay(TimeUnit unit);

这里需要注意的是在getDelay当中获取时间的单位是NANOSECONDS,所以在写getDelay这个方法的时候注意使用的时间单位是NANOSECONDS。

3、DelayQueue的Demo

下面我们通过一个例子来看一下这个DelayQueue到底应该如何使用。本例子当中我们先根据时间短的先执行,打印了一遍执行顺序,然后通过一个List将原本初始化的时候的Runnable进行了保存,然后在最后进行打印,打印的顺序,便是初始化的顺序,也就是入队的时候的顺序。还有在方括号当中便是延时的时间,可以通过运行看一下,是否是等了相应的时间打印出来的这句话。目的就是让大家可以很直观的看到,我们通过DelayQueue是按照一个什么顺序进行排序的,最后通过一个任务将整个系统结束掉。

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/***
* 探究无届队列DelayQueue
* @author pengchengliu
*
*/
public class TestDelayQueue {
public static void main(String[] args) {
Random random = new Random(47);
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
DelayQueue<DelayedTask> delayQueue = new DelayQueue<DelayedTask>();

// 初始化 delayQueue ,
for (int i = 0; i < 20; i++) {
delayQueue.put(new DelayedTask(random.nextInt(5000)));
}
delayQueue.add(new DelayedTask.EndSentinel(5000, newCachedThreadPool));

// 启动任务开关
newCachedThreadPool.execute(new DelayedTaskConsumer(delayQueue));
}
}
class DelayedTask implements Runnable, Delayed {
private static int counter = 0 ;
private final int id = counter ++ ;
private final int delta ;
private final long trigger ;
protected static List<DelayedTask> sequence = new ArrayList<DelayedTask>();

public DelayedTask(int delayInMilliseconds) {
this.delta = delayInMilliseconds ;
// 从较细粒度到较粗粒度的舍位转换,这样会失去精确性。例如,将 999 毫秒转换为秒的结果为 0。
// 使用参数从较粗粒度到较细粒度转换,如果参数为负,则在数字上溢出至 Long.MIN_VALUE,
// 如果为正,则为 Long.MAX_VALUE。 将1毫秒转换为毫微秒
trigger = System.nanoTime() + TimeUnit.NANOSECONDS.convert(this.delta, TimeUnit.MILLISECONDS);
sequence.add(this) ;
}

@Override
public long getDelay(TimeUnit unit) {
return unit.convert(trigger - System.nanoTime(), TimeUnit.NANOSECONDS);
}

@Override
public int compareTo(Delayed delayed) {
DelayedTask task = (DelayedTask) delayed ;
if (this.delta < task.delta) return -1 ;
if (this.delta > task.delta) return 1 ;
return 0;
}

@Override
public void run() {
System.out.println(this + " ");
}

@Override
public String toString() {
return String.format("[%1$-4d]", delta) + " Task:" + id;
}

public String summary () {
return "(" + id + ":" + delta + ")";
}


public static class EndSentinel extends DelayedTask {

private ExecutorService exec ;

public EndSentinel(int delayInMilliseconds, ExecutorService exec) {
super(delayInMilliseconds);
this.exec = exec ;
}

@Override
public void run() {
for (DelayedTask task : sequence) {
System.out.print(task.summary() + " ");
}
System.out.println();
System.out.println(this + " Calling shutdownNow()");
exec.shutdownNow() ;
}

}
}
class DelayedTaskConsumer implements Runnable {

private DelayQueue<DelayedTask> q ;

public DelayedTaskConsumer(DelayQueue<DelayedTask> q) {
this.q = q ;
}

@Override
public void run() {
try {
while (!Thread.interrupted()){
//注意:手动调用run方法
q.take().run();
}
} catch (InterruptedException e) {}
System.out.println("Finish DelayedTaskConsumer");
}
}
[128 ] Task:11
[200 ] Task:7
[429 ] Task:5
[520 ] Task:18
[555 ] Task:1
[961 ] Task:4
[998 ] Task:16
[1207] Task:9
[1693] Task:2
[1809] Task:14
[1861] Task:3
[2278] Task:15
[3288] Task:10
[3551] Task:12
[4258] Task:0
[4258] Task:19
[4522] Task:8
[4589] Task:13
[4861] Task:17
[4868] Task:6
(0:4258) (1:555) (2:1693) (3:1861) (4:961) (5:429) (6:4868) (7:200) (8:4522) (9:1207) (10:3288) (11:128) (12:3551) (13:4589) (14:1809) (15:2278) (16:998) (17:4861) (18:520) (19:4258) (20:5000)
[5000] Task:20 Calling shutdownNow()
Finish DelayedTaskConsumer

JVM学习2 - OutOfMemroyError***

  • 1、OOM的概念
  • 2、JVM堆内存溢出
  • 3、JVM栈/Native栈内存溢出
  • 4、方法区与常量池的内存溢出
  • 5、本机直接内存溢出

1、OOM的概念

OOM便是OutOfMemroyError,它是一个异常错误,代表的意思便是JVM当中的内存不够用了,最终导致程序的Crash,这个问题不是有某一类的特定语法导致的,存在很多的情况,所以这种错误对于程序员来说还是相当的难解的,因为我们的内存回收工作是交给GC去做的,具体如何回收,我们不清楚,具体是JVM当中的哪一个区域发生的内存溢出,我们可以通过Log去分析,但是分析起来还是很费劲的,尤其是大型的系统,这便是OOM对程序员造成的痛。

2、JVM堆内存溢出

我们知道JVM堆当中存储的便是对象实例,那么只要我们不断的创建对象,并且保证GC Roots到对象之间有可达路径,用来避免垃圾回收机制清除这些对象,那么只要我们不断的new出新的对象,那么JVM虚拟机必然会出现内存溢出的异常。

这里通过三个参数来控制JVM堆内存的大小变化分别是

  • Xsm:堆的最小值参数
  • -Xmx:堆的最大值参数
  • -XX: +HeapDumpOnOutOfMemoryError:可以让虚拟机在出现内存溢出异常时,Dump出当前的内存堆转储快照以便事后分析

在Eclipse的Debug/Run页面,将堆的最小值和最大值都设置为20m,这样可以避免堆自动扩展。当我们设置完上面的参数值以后,我们通过一个Demo来看一下,如何造成堆内存溢出。

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
/**
* 探究JVM 堆内存溢出,堆当中存储的是对象的实例,只要我们不断的new对象实例 就OK
* @author pengchengliu
* JVM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*
*/
public class HeapOOM {
static class OOMObject {}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid795.hprof ...
Heap dump file created [27575251 bytes in 0.114 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at outofmemoryerror.HeapOOM.main(HeapOOM.java:19)

当JVM堆当中出现OOM的异常还是比较常见的,当JVM当中出现堆内存溢出的时候。控制台上面便会抛出OOM异常,紧接着便会在后面出现 Java heap space 这样的字样,如果出现了这样的字样的时候,便可以得出当前的JVM堆现在状态便是内存溢出的状态。那么关于JVM堆内存溢出我们应该如何解决呢?

分析

当这个区域当中出现了相应的OOM异常的时候,一般的手段便是先通过内存映像分析工具,对刚刚Dump出来的堆内存情况的快照进行分析,重点便是先确定内存当中的对象是否是必要的,也就是先分析清楚是内存泄露还是内存溢出。

  • 1、内存泄漏:如果可以确定是内存泄漏,可以进一步通过工具查看泄漏对象到GC Root的引用链,于是就能找到泄漏到泄漏对象是通过怎么的路径与GC Root相关联并且导致GC无法自动回收的,如果掌握了泄漏对象的类型信息以及GC root引用链的信息,就可以快速定位到泄漏代码的位置。

  • 2、内存溢出:如果不是内存泄漏导致的,那就是纯粹的对象实例过多导致的,那么我们的第一步便是确定当前内存当中的所有对象是是否必须活着,如果不是,我们可以更改相应对象实例到GC Root的引用链的类型,如果是那就需要检查堆大小参数 -Xms -Xmx ,与物理内存进行对比看能否加大内存,从代码上面检查是否存在某些对象生命周期是否过长,持有的时间是否过长,减少程序运行期间内存的消耗。

3、JVM 栈 / Native 栈 内存溢出

首先这个区域JVM规范当中定义了两种内存溢出的异常

  • 如果线程请求的栈的深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError 异常 (例如:错误的递归)
  • 如果虚拟机在扩展时,无法申请到足够的内存空间,将抛出OOM异常 (例如:开启无限线程)

我们可以通过 -Xoss 参数设置Native 栈的大小,通过-Xss 参数设置JVM栈的大小,但是在JVM众多的实现当中,有的虚拟机将上面两种栈结构通过了一种方式进行实现,导致使用-Xoss参数无法生效,那就只能使用-Xss进行设置。

下面通过一个Demo来演示想要的JVM栈内存溢出

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
/**
* 探究JVM栈深度OOM溢出
* JVM args : -Xss160k
* @author pengchengliu
*
*/
public class JavaStackOOM01 {
private int stackLength = 1 ;
public void stackLeak() {
stackLength ++ ;
stackLeak();
}
public static void main(String[] args) {
JavaStackOOM01 javaStackOOM01 = new JavaStackOOM01();
try {
javaStackOOM01.stackLeak();
} catch (Throwable e) {
System.out.println("stack lenth :" + javaStackOOM01.stackLength);
throw e ;
}
}
}
stack lenth :771
Exception in thread "main" java.lang.StackOverflowError
at outofmemoryerror.JavaStackOOM01.stackLeak(JavaStackOOM01.java:14)
at outofmemoryerror.JavaStackOOM01.stackLeak(JavaStackOOM01.java:15)
at outofmemoryerror.JavaStackOOM01.stackLeak(JavaStackOOM01.java:15)
at outofmemoryerror.JavaStackOOM01.stackLeak(JavaStackOOM01.java:15)
at outofmemoryerror.JavaStackOOM01.stackLeak(JavaStackOOM01.java:15)

at outofmemoryerror.JavaStackOOM01.main(JavaStackOOM01.java:21)

如果我们使用默认的JVM栈的大小,那么递归的深度达到1000~2000是没有任何问题的,可以满足大多数情况下的递归调用,所以这种模式下面出现的溢出异常,我们可以不必要的进行深究。

下面演示一个通过线程数开辟栈的个数上线的时候出现的OOM异常

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
/**
* 探究JVM栈大小OOM溢出
* JVM args : -Xss2m
* @author pengchengliu
*
*/
public class JavaStackOOM02 {
private void dontStop () {
while (true) {}
}
public void stackLeakByThread () {
while (true) {
Thread thread = new Thread(new Runnable(){
@Override
public void run() {
dontStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
JavaStackOOM02 javaStackOOM02 = new JavaStackOOM02();
javaStackOOM02.stackLeakByThread();
}
}

上述的这种做法还是会出现OOM异常的,这种方式在我们的日常开发当中还是比较常用的,如果遇到这种OOM,如果我们不能减少线程的数量活着更改64为虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

4、方法区与常量池的内存溢出

备注:剩下几种情况。 还未遇到, 遇到以后。第一时间更新

JVM学习1 - 内存区域

  • 1、内存区域
  • 2、JVM当中常用的内存区域
  • 3、对象的内存格局
  • 4、对象的访问定位
  • 5、对象的创建过程

1、内存区域

对于一个程序来说,数据和指令就是该程序的全部。而这里所指的指令也就是开发人员所编写的代码,而数据便是一个程序的输入与输出。在一个大型的系统运行起来以后,便会有成千上万的数据来来回回的穿梭在整个系统当中,而这些数据来回穿梭的时候,会存储在什么地方呢? 又是以何种形式进行存储。这便是本章节的重点。

数据在程序运行起来以后所存储的地方便是内存当中,而不同的数据类型存储在不同的内存区域当中。在我们开始学习编程的第一天便知道,C++这种语言是开发人员自己去管理自己的内存区域,而Java语言便是把内存的管理完全托管给了GC(Java当中的垃圾回收机制)。这样我们就不用去管内存当中的一些事情了,但是随着我们开发经验的不断增加,发现其实完全托管对于Java开发也不完全是好事,比如OOM,当内存溢出的时候,我们排查溢出点的难度将大大增加,如何提高自己排查关于内存方法的Bug呢。那便是研究JVM当中内存到底是如何分配的,如何由GC进行托管的。

2、JVM内存区域

JVM当中把内存主要分为了7部分,想弄明白GC如何托管JVM的内存,那这7个内存区必须熟悉。下面了解一下这7个内存区域到底都是干什么的。

1、程序计数器

这是一块很小的内存区域,同时这块内存可以被JVM快速的进行访问,里面存储这一个计数器,这个计数器用来表示当前这个程序执行到什么地方(.class文件的基础之上),如果理解起来比较费劲的话,可以理解为我们IDE当中deBug模式下的执行光标。

这个区域是所有区域当中最好理解的,就是告诉JVM,我们这个程序现在执行到了第几行,下面应该执行第几行。需要注意的便是在并发编程的时候,每一条线程都存在单独属于自己的程序计数器,因为如果微观的理解JVM并发编程的话,那么在一个确定的时刻,一个处理器都只会执行一条线程当中指令,这个时候线程调度器如何切换到了其他的线程,当前的计数器就应该保存在线程单独的内存当中好方便下次复位的时候继续执行。所以各条线程的程序计数器是相互独立的,我们称这类内存区域为“线程私有”的内存。有点类似于线程当中ThreadLocl的概念

这一块内存是由JVM去统一管理,不管是插入还是删除,都和开发人员没有一点关系,所以这个地方的内存不会存在溢出的可能性。

2、JVM栈

这一块内存区域当中存放的是方法,每个方法执行的时候,会创建一个栈帧,一个栈帧里面存放着执行方法所需要的数据,比如局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成对应着一个栈帧在JVM栈中入栈到出栈的全过程。其实好多Java开发人员对JVM栈的描述不完全,并不是存储这单一的对象引用,其实还有其他的信息,而我们理解的对象的引用是存放在局部变量表当中的。

局部变量表:主要存放着两个比较重要的内容分别是

  • 存放编译期可知的基本数据类型和对象的引用,对象的引用可能是对象实体起始地址的引用指针,也可能是一个代表对象的句柄
  • 存放着returnAddress(指向了一条字节码指令的地址)
    注意:存放基本数据类型区域当中,long和double会占2个局部变量空间,其余的只占一个。局部变量表所需的空间在编译期间完成分配。在运行期间不会改变当前局部变量表的大小

JVM栈是线程私有的,生命周期和线程的生命周期一样。该区域如果线程请求的栈深度大于JVM所允许的深度,将抛出StackOverflowError的异常。也就是我们所说的无限递归调用,如果JVM栈是允许动态扩展的,如果申请不到足够的内存的时候,抛出OutOfMemoryError。

3、Native 栈

这一块区域也别称为本地方法栈,该区域和上述的JVM栈非常相似,具体的区别便是JVM栈是为.class文件服务,而Native栈便是为Native方法服务,除了这一点没有什么不同,甚至有的JVM虚拟机在实现的时候,将JVM栈和Native栈进行了合并。

4、JVM 堆

这个块内存是JVM所管理的内存最大的一个区域,这个区域主要是用来存放对象实例,几乎所有的对象实例于数组都在该区域上面,但是随着科技的进步,不同的虚拟机有着不同的实现,导致了所有对象都分配在堆上面渐渐不是那么绝对了,该内存区域是被所有的线程所共享的,在JVM创建的时候创建。该区域上面可以处于物理上面不连续,但是只要逻辑上面是连续的即可。

JVM堆是GC重点的照顾对象,也被国人称为垃圾堆,JVM堆当中也会有很细小的划分,通过不同的划分条件,可以划分为不同的区域。如下:

  • 内存回收角度
    • 分代收集算法:新生代、老年代
    • 细致划分:Eden空间、From Survivor空间、To Survivor空间
  • 内存分配角度
    • 多个线程共享的区域当中划分出多个线程私有的分配缓存区(Thread Locl Allocation Buffer,TLAB )

当该区域的实在没有能力完成实例分配的时候,并且JVM堆再也没有办法进行扩展的时候,抛出OutOfMemoryError异常。

5、方法区

这个区域按照JVM的规范来说应该是在JVM堆当中的,JVM堆当中存储的是对象实例,而这一块区域当中存储的是类的信息、常量、静态变量、即编译器编译以后的代码等数据,根据功能来说比较特殊,所以它有着一个别名叫做Non-Heap(非堆),所以就从堆当中独立出来的,但是基本属性还是和堆保持一致,比如这个区域是被所有线程所共享的。

这个区域GC的痕迹是比较少见的,但这不代表这进入这个区域的数据就会一直存在。这个区域的回收主要就是针对常量池和堆类型的卸载两个方面。当方法区当中无法满足内存分配需求的时候,将抛出OOM异常。

6、常量池

这个区域是上述方法区当中的一个子区域,正如它的名字一样,这个区域当中存放着各种字面量和符号的引用,当一个类被加载后,这个类当中所描述的常量便会存入方法区的运行时常量池当中。

JVM规范当中对于一个class文件有着严格的规定,只有满足了JVM规范,JVM才会装载、执行。

一般常量池当中除了保存class文件中描述的符号引用,同时还会把翻译出来的直接引用也存储在这个区域当中。这个区域还有一个比较特殊的存在便是这个区域时在运行的时候进行管理的,也就是当动态的把一个class文件加载到了JVM上面,那么就会把class当中的常量动态的加载到方法区的常量池当中。这个区域是在方法区当中的,也就受限于方法区,当常量池无法再次申请到内存的时候就会抛出OOM异常。

7、直接内存

这个区域的内存其实不是JVM运行时数据区域的一部分,也不是Java虚拟机规范定义的内存区域。但是这部分内存也会被我们开发者频繁的进行使用,如果我们使用的方法有误,同样也会抛出OOM异常,这也就是我们需要了解它的原因之一吧。

我们知道在JDK1.4之后,Java为了提升IO的效率,引入了新的IO那便是NIO,NIO当中有一个概念便是基于Channel于Buffer的I/O方式,其实说白了就是它使用了Native函数库直接分配堆外内存,然后通过一个存储在Java堆上面的DirectByteBuffer对象所持有并且可以进行操作,这样便能显著的提升IO的效率,因为能避免Java堆和Native堆中来回复制数据

不属于JVM当中的内存肯定就不会收到JVM的限制,但是既然是内存,肯定还是会受到本机内存大小的限制,如果超出限制便会OOM

3、对象的内存格局

一个对象的实例在JVM堆当中时以何种形式进行保存的?其实一个对象的实例在JVM堆当中是以三个部分进行保存的,下面我们来了解一下这三个部分

1、对象头

对象头有点像一个进程在操作系统里面的PCB一样,里面存储该对象的控制属性(运行时数据),例如:哈希码、GC分代、锁状态标示、线程持有的锁、偏向线程ID、偏向时间戳等等,对象头的大小在JVM规范当中是有限制的,在32位JVM虚拟机上面时32位Bitmap结构所能记录的限度,64位虚拟机便是64位Bitmap所能记录的限度,官方称之为“Mark Work”。Mark Work被设计为一个非固定的数据结构以便在极小的空间内存存储尽量多的信息,会根据对象的状态复用自己的存储空间。

对象头的另外一个部分就是类型指针,即对象指向它的类元数据的指针,也就是在方法区的类元信息,JVM通过这个指针来确定这个对象是哪个类的实例。如果这个对象时数组,那么对象头当中还必须有一块用于记录数组长度的数据,因为JVM可以通过普通Java对象的元数据信息确认该Java对象的大小,但是从数组的元数据类型当中无法确认数组的大小。

2、实例数据(Instance Data)

这部分内容就是我们定义的各种字段内容,也就是真正存储着的有效信息,包括从父类继承下来的,存储的顺序主要受两个方面的影响

  • 在Java源码当中的定义顺序
  • JVM的参数分配策略,分配策略主要是:longs/double、int、shorts/chars、bytes/boolean、oops(Ordinary Object Pointers)相同宽度的字段被分配到一起。

在JVM分配策略满足的前提下,在父类中定义的变量会出现在子类之前。

3、对齐填充

这部分的数据其实是没有任何意义的,仅仅起到了占位符的作用,由于JVM自动内存管理系统要求对象起始地址必须要是8个字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍,对象头的数据正好就是8字节的整数倍,但是当对象实例数据不足以是8字节的整数倍的时候,通过这部分数据填充补全。

4、对象的访问定位

Java中,我们创建对象,就是为了使用这个对象,我们通过JVM栈中的局部变量表当中的对象的reference来操作在JVM堆上面的实例对象,JVM规范当中只规范了在局部变量表当中的reference应该指向JVM堆当中的对象实例,具体的如何指向没有说,这样的话,具体实现便由JVM的实现来规定,当前市面上面比较火的JVM虚拟机,主要通过以下两种方式进行实现

1、使用句柄来定位到对象

如果使用句柄来进行访问的话,Java堆当中将会划分出来一块内存作为句柄池,reference中存储这对象实例的地址说白了就是句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址,具体关系如下所示

使用句柄来定位到对象

2、使用直接指针访问,在reference当中存储的直接就是对象的地址。如下所示:

使用直接指针访问

总结:这两种对象访问方式又有优缺点,

  • 使用句柄的方式最大的好处便是reference中存储的信息是稳定的句柄地址,在对象实例数据被移动的时候(GC回收对象的时候必然会做到),只会改变句柄池当中实例数据的指针,而reference本身便不用修改
  • 使用直接指针访问的好处在于速度是最快的,它节省了一次在句柄池当中定位的时间开销。

具体使用哪一种方式进行使用,这遍取决于市面上面的JVM虚拟机,因为在JVM规范当中并没有明确说明应该如和去做。

5、对象的创建过程

当虚拟机遇到我们源码当中的new关键字的时候,是如何进行工作,是如何进行内存的,这个过程到底是一个如何的过程,下面主要研究一下这一方面的知识

1、首先会去检查这个new这个指令的参数是否能够在常量池当中定位到一个类的符号引用,检查这个符号引用代表的类是否已被加载、解析和初始化过。

2、如果没有则先执行相应类加载过程。

3、在类加载检查通过了以后,JVM为新生的对象分为内存,(就是在JVM堆当中把一块确定的内存划分出来)对象所需内存的大小在类加载完成以后便可以完全确定的。

  • 3.1、假设堆当中的内存是绝对规整的,所有使用过的内存在一边,没有使用过的内存在另外一边,中间放着一个指针来作为绝对分界点的指示器,那所分配的内存就是仅仅把那个指针向空闲区域移动一段和对象大小相等的距离,这种分配方式叫做 Bump the Pointer 指针碰撞
  • 3.2、假设堆当中的内存不是绝对规整的,JVM就必须维护一个列表,用来记录那块内存可以使用,那块内存不可以使用,在分配的时候找到一块足够大的空间划分给对象实例,并且更新表,这种方式被称为Free List 空闲列表

具体使用哪一种方式进行内存分配,主要取决于当前堆当中的内存是否规整,而是否规整取决于GC是否带有压缩整理功能,如果有压缩整理功能的GC便使用 Bump the pointer 没有则使用 Free List

关于内存分配来说,会出现并发分配的情况,即把同一块内存分配给了A线程当中的某一个对象,和B线程当中的某一个对象。解决办法一般来说,有以下两种方式

  • 给JVM分配内存的动作进行同步处理,也就是加锁操作
  • 每个线程在Java堆当中预先分配一小块内存,称为本地线程分配缓存(Thread Loacl Allocation Buffer TLAB)。那个线程要分配内存,就在那个线程的TLAB上面进行内存的分配,只有TLAB用完并分配新的TLAB的时候,才需要同步锁定,虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数进行控制

4、内存分配完成以后,虚拟机需要将分配到的内存空间都初始化为0(不包括对象头),如果使用TLAB,这一个工作可以提前至TLAB进行分配的时候进行,这一操作便可以让Java代码在不赋初值的时候就可以直接使用

5、JVM对对象进行必要的初始化操作,就是初始化对象的对象头当中存储的信息

6、这个时候,当前对象便算是创建完成了。

线程同步工具类CyclicBarrier

  • 1、CyclicBarrier的作用
  • 2、CyclicBarrier使用规则
  • 3、CyclicBarrier的Demo
  • 4、CyclicBarrier原理 (待补充)
  • 5、CyclicBarrier的实现 (待补充)

1、CyclicBarrier的作用

CyclicBarrier类也是java提供给我们同步任务的时候使用的,和CountDownLatch有点类似,都是控制任务执行的顺序的,不同的地方在于,CyclicBarrier可以让一个任务在执行的途中发生多次事件,而CountDownLatch却只能发生一次,就是在我们调用countDown方法的时候,这个事件触发以后,便会减少相应的锁存器上面的计数器,而当计数器为0的时候,哪些调用了await的任务便可以执行了,但是CyclicBarrier就不同了,CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞

2、CyclicBarrier的使用规则

CountDownLatch是一个同步的辅助类,允许一个或多个线程,等待其他一组线程完成操作,再继续执行。
CyclicBarrier是一个同步的辅助类,允许一组线程相互之间等待,达到一个共同点,再继续执行。

个人理解:CyclicBarrier:可看成是个障碍,所有的线程必须到齐后才能一起通过这个障碍
场景还原:以前公司组织户外拓展活动,帮助团队建设,其中最重要一个项目就是全体员工(包括女同事,BOSS)在完成其他项目时,到达一个高达四米的高墙没有任何抓点,要求所有人,一个不能少的越过高墙,才能继续进行其他项目。

3、CyclicBarrier的Demo

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
101
102
103
104
105
106
107
108
109
110
111
/**
* 探究CyclicBarrier的使用规则
* 模拟赛马游戏
* @author pengchengliu
*
*/
public class TestCycilcBarrier {
static final int FINISH_LINE = 100 ;
private List<Horse> mHorses = new ArrayList<Horse>() ;
private ExecutorService mExec = Executors.newCachedThreadPool() ;
private CyclicBarrier mCyclicBarrier ;

public TestCycilcBarrier (int horsesSize, int pause) {

// "栏栅动作",该动作为一个Runnable,这个Runnable在CyclicBarrier计数器变为0的时候执行。
mCyclicBarrier = new CyclicBarrier(horsesSize, new Runnable() {

@Override
public void run() {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < FINISH_LINE; i++) {
stringBuilder.append("=");
}
System.out.println(stringBuilder);

for (Horse horse : mHorses) {
System.out.println(horse.tracks());
if (horse.getStrides() >= FINISH_LINE) {
System.out.println(horse + "won!");
mExec.shutdownNow();
return ;
}
}
try {
TimeUnit.MILLISECONDS.sleep(pause);
} catch (InterruptedException e) {
System.out.println("barrier-action sleep interrupted");
}

}
});

for (int i = 0; i < horsesSize; i++) {
Horse horse = new Horse(mCyclicBarrier);
mHorses.add(horse);
mExec.execute(horse);
}
}

public static void main(String[] args) {
int horsesSize = 10 ;
int pause = 200 ;

if (args.length > 0) {
Integer temp = new Integer(args[0]);
horsesSize = temp > 0 ? temp : horsesSize ;
}

if (args.length > 1) {
Integer temp = new Integer(args[1]);
pause = temp > 0 ? temp : pause ;
}

new TestCycilcBarrier(horsesSize, pause);
}
}
class Horse implements Runnable {
private static int sCounter = 0 ;
private final int ID = sCounter ++ ;
private int mStrides = 0 ;
private Random mRandom = new Random(47);
private static CyclicBarrier sBarrier ;

public Horse (CyclicBarrier cyclicBarrier) {
this.sBarrier = cyclicBarrier ;
}

@Override
public void run() {
try {
while (!Thread.interrupted()) {
synchronized (this) {
mStrides += mRandom.nextInt(5);
}
sBarrier.await();
}
} catch (InterruptedException e) {

} catch (BrokenBarrierException e) {
throw new RuntimeException(e) ;
}
}

public synchronized int getStrides () {
return mStrides ;
}

@Override
public String toString() {
return "Horse " + this.ID ;
}

public String tracks () {
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0; i < getStrides(); i++) {
stringBuffer.append("-");
}
stringBuffer.append(this.ID);
return stringBuffer.toString() ;
}
}

上述Demo运行起来的效果图,如下图所示:

赛马游戏

参考文献

线程同步工具类CountDownLatch

  • 1、CountDownLatch的作用
  • 2、CountDownLatch使用规则
  • 3、CountDownLatch的Demo
  • 4、CountDownLatch原理 (待补充)
  • 5、CountDownLatch的实现 (待补充)

1、CountDownLatch的作用

在JDK 5 以后sun公司引入了大量设计来解决并发的问题,学习使用这些类,我们可以更加自如的创建简单而健壮的并发程序,CountDownLatch类是用来同步一个或者多个任务的工具类,强制它们等待由其他任务执行的一组操作。

例如,现在有这样的一个需求,为了程序的快速运行,我们把一个程序分为三部分,分别是初始化、数据拉取、显示数据。初始化操作和数据拉取操作不会冲突,可以说这两个操作完全不相关,但是显示数据必须要在上面两个任务完成以后才可以继续。如果把这三个操作至于不同的线程,那么如果不对线程的同步加以控制的话,必将大乱,这个时候我们就可以先对CountDownLatct初始计数器为2,然后让初始化操作和数据拉取操作开始运行,然后在结束的时候调用countDown方法,然后显示操作在run开始的时候使用awit进行等待,直到计数器归0,显示操作开始执行,而这个时候上述两个操作都已经执行完毕。

2、CountDownLatch的使用规则

我们可以向CountDownLatch对象设置一个初始计数值,任何在CountDownLatch对象上调用wait的方法都将被阻塞,直到这个计数器达到0。其他任务在结束其工作时,可以在该对象上调用countDown()来减少这个计数器。CountDownLatch被设计为只触发一次,计数器不能被重置。

调用countDown的任务在产生这个调用时并没有被阻塞,只有对await的调用会产生阻塞,直到计数器达到0为止。

CountDownLatch的典型用法是将一个程序分为n个互相独立的可以解决的任务,并创建值为0的CountDownLatch。当每个任务完成时,都会在这个锁存器上调用countDown。等待问题被解决的任务在这个锁存器上面调用await,将它们自己揽住,直至锁存器计数结束,下面通过一个例子来演示该框架应该如何应用。

个人理解:CountDownLatch:我把他理解成倒计时锁
场景还原:一年级期末考试要开始了,监考老师发下去试卷,然后坐在讲台旁边玩着手机等待着学生答题,有的学生提前交了试卷,并约起打球了,等到最后一个学生交卷了,老师开始整理试卷,贴封条,下班,陪老婆孩子去了。
补充场景:我们在玩LOL英雄联盟时会出现十个人不同加载状态,但是最后一个人由于各种原因始终加载不了100%,于是游戏系统自动等待所有玩家的状态都准备好,才展现游戏画面。

3、CountDownLatch的Demo

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

/**
* 演示CountDownLatch的使用
* @author pengchengliu
*
*/
public class TestCountDownLatch {
static final int SIZE = 10 ;
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
CountDownLatch countDownLatch = new CountDownLatch(SIZE);
for (int i = 0; i < 10; i++) {
exec.execute(new WaitingTask(countDownLatch));
}
for (int i = 0; i < SIZE; i++) {
exec.execute(new TaskProtion(countDownLatch));
}
System.out.println("Launch all task");
exec.shutdown();
}
}
class TaskProtion implements Runnable {
private static int sCounter = 0 ;
private final int id = sCounter ++ ;
private static Random sRandom = new Random(47) ;
private final CountDownLatch mLatch ;

public TaskProtion(CountDownLatch latch) {
this.mLatch = latch ;
}

@Override
public void run() {
try {
doWork();
mLatch.countDown();
} catch (InterruptedException e) {
System.out.println("TaskProtion is exiting form intrrupt");
}
}

public void doWork() throws InterruptedException {
TimeUnit.MICROSECONDS.sleep(sRandom.nextInt(2000));
System.out.println(this + " completed");
}

@Override
public String toString() {
return String.format("%1$-3d", id);
}
}
class WaitingTask implements Runnable {
private static int sCounter = 0 ;
private final int id = sCounter ++ ;
private final CountDownLatch mLatch ;

public WaitingTask (CountDownLatch latch) {
this.mLatch = latch ;
}

@Override
public void run() {
try {
this.mLatch.await();
System.out.println("Latch barrier passed for " + this);
} catch (InterruptedException e) {
System.out.println(this + " interrupted");
}
}

@Override
public String toString() {
return String.format("WaitingTask %1$-3d", id);
}
}
Launch all task
2 completed
8 completed
6 completed
5 completed
3 completed
9 completed
4 completed
0 completed
7 completed
1 completed
Latch barrier passed for WaitingTask 3
Latch barrier passed for WaitingTask 6
Latch barrier passed for WaitingTask 0
Latch barrier passed for WaitingTask 1
Latch barrier passed for WaitingTask 8
Latch barrier passed for WaitingTask 9
Latch barrier passed for WaitingTask 7
Latch barrier passed for WaitingTask 2
Latch barrier passed for WaitingTask 4
Latch barrier passed for WaitingTask 5

通过上述的任务输出,我们不难看出,我们将我们的任务很整齐的进行了先后顺序的控制,让我们程序当中有预支条件的任务可以在预支条件之后进行运行。这里就是通过CountDownLatch的锁存器当中的计数器来控制的。这里需要注意的两点就是,

  • CountDownLatch这个类当中的计数器的初始值,只能被初始化一次,不能初始化第二次,
  • 在使用的时候,必须多个对象同步同一个CountDownLatch这个类。

我们可以尝试将上述代码当中的CountDownLatch相关操作去除,我们再来看一下运行结果,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Launch all task
Latch barrier passed for WaitingTask 9
Latch barrier passed for WaitingTask 1
Latch barrier passed for WaitingTask 2
Latch barrier passed for WaitingTask 8
Latch barrier passed for WaitingTask 5
9 completed
2 completed
Latch barrier passed for WaitingTask 6
5 completed
8 completed
3 completed
Latch barrier passed for WaitingTask 3
1 completed
6 completed
Latch barrier passed for WaitingTask 4
7 completed
Latch barrier passed for WaitingTask 7
Latch barrier passed for WaitingTask 0
0 completed
4 completed

很明显结果杂乱无章,没有任何规律可言,到这里大家应该明白了CountDownLatch类的使用了吧。