0%

与不可变集合相比,使用可变集合最大的优点是性能更快。向不可变集合添加元素需要新建集合再拷贝所有元素,下面是kotlin标准库现在的实现:

1
2
3
4
5
6
7
operator fun <T> Iterable<T>.plus(element: T): List<T> {
if (this is Collection) return this.plus(element)
val result = ArrayList<T>()
result.addAll(this)
result.add(element)
return result
}

当集合很大时,拷贝操作就比较耗性能了。所以对于需要添加元素的集合使用可变集合性能更好。我们知道,不可变集合是线程安全的。但是对于不需要同步的局部变量来说则不适用。这也是为什么对于局部处理,使用可变集合通常更有意义。这个事实可以在标准库中反映出来,在标准库中,所有的集合处理函数都是使用可变集合内部实现的:

1
2
3
4
5
6
7
8
9
10
inline fun <T, R> Iterable<T>.map(
transform: (T) -> R
): List<R> {
val size =
if (this is Collection<*>) this.size else 10
val destination = ArrayList<R>(size)
for (item in this)
destination.add(transform(item))
return destination
}

而不是不可变集合:

1
2
3
4
5
6
7
8
9
// This is not how map is implemented
inline fun <T, R> Iterable<T>.map(
transform: (T) -> R
): List<R> {
var destination = listOf<R>()
for (item in this)
destination += transform(item)
return destination
}

总结:
如果是增加元素,可变集合是很快的,而不可变集合则给我们给多的控制,知道集合是如何变化的。但对于本地作用域来讲,我们不需要这种控制,所以使用可变集合更好。尤其在一些工具类,插入操作可能很频繁。

一、概述

你可能注意到了几乎所有的Kotlin标准库里面的高阶函数都是inline类型的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public inline fun repeat(times: Int, action: (Int) -> Unit) {
for (index in 0 until times) {
action(index)
}
}

public inline fun <T, R> Iterable<T>.map(
transform: (T) -> R
): List<R> {
return mapTo(
ArrayList<R>(collectionSizeOrDefault(10)),
transform
)
}

public inline fun <T> Iterable<T>.filter(
predicate: (T) -> Boolean
): List<T> {
return filterTo(ArrayList<T>(), predicate)
}

这些函数的调用在编译时会被展开到调用处。如下所示:repeat函数会被它本身的函数体替换掉。

1
2
3
4
5
6
7
repeat(10) {
print(it)
}
//↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
for (index in 0 until 10) {
print(index)
}

一般的函数调用通常是:跳进函数体,执行代码,然后跳出函数体,回到调用点。而用函数体替换函数调用是一种完全不同的方式,这种方式有以下一些优点:

  • 类型参数具体化
  • 参数包含函数的方法内联后执行更快
  • 不允许非本地的return语句

使用inline标识符也会有一些缺点,下面👇🏻我们一起来看一下它的优缺点:

一、类型参数具体化:

早期的Java版本不支持泛型,在2004年的J2SE-5.0才支持。由于泛型会在编译期间被擦除,所以在字节码层面是不存在的。例如:List<Int> 编译后成为 List ,所以我们只需要检查一个对象是否是List实例,而不用检查它是否是一个List<Int>

1
2
any is List<Int> // Error
any is List<*> // OK

由于这个原因,我们不能操作类型参数:

1
2
3
fun <T> printTypeName() {
print(T::class.simpleName) // ERROR
}

通过内联函数我们可以突破这种限制。由于函数调用被函数体替换,通过使用reified修饰符,泛型被真实的类型参数替换。

1
2
3
4
5
6
7
8
inline fun <reified T> printTypeName() {
print(T::class.simpleName)
}

// Usage
printTypeName<Int>() //→print(Int::class.simpleName) // Int
printTypeName<Char>() //→print(Char::class.simpleName)// Char
printTypeName<String>() //→print(String::class.simpleName)// String

reified是一个非常有用的修饰符,例如标准库里面的filterIsInstance用来过滤某一种类型的元素。

1
2
3
4
5
6
7
8
class Worker
class Manager

val employees: List<Any> =
listOf(Worker(), Manager(), Worker())

val workers: List<Worker> =
employees.filterIsInstance<Worker>()

它经常被用在我们自己写的代码库或工具类中。下面的例子是使用Gson库实现的通用函数,它能帮助我们简化依赖注入和模块申明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
inline fun <reified T : Any> String.fromJsonOrNull(): T? =
try {
gson.fromJson(json, T::class.java)
} catch (e: JsonSyntaxException) {
null
}

// usage
val user: User? = userAsText.fromJsonOrNull()

// Koin module declaration
val myModule = module {
single { Controller(get()) } // get is reified
single { BusinessService() } // get is reified
}

// Koin injection
val service: BusinessService by inject()
// inject is reified
二、函数类型的参数内联后执行更快

更确切的讲,短小的函数内联会更快,它无需跳转执行和跟踪调用栈。这也是为什么标准库里面很多小函数都是inline类型的原因。

1
2
3
inline fun print(message: Any?) {
System.out.print(message)
}

当方法没有函数类型的参数时,没必要内联且IntelliJ会给出以下提示⚠️:

要理解其中的原因,我们首先需要理解将函数作为对象进行操作的问题是什么。这些类型的对象(使用函数字面量创建)需要以某种方式保存。在Kotlin/JVM上,需要使用JVM匿名类或普通类创建一些对象。因此,下面的lambda表达式:

1
2
3
val lambda: () -> Unit = {
// code
}

将会被编译为一个类。或JVM匿名类。

1
2
3
4
5
6
// Java
Function0<Unit> lambda = new Function0<Unit>() {
public Unit invoke() {
// code
}
};

或者被编译成一个普通的类,被定义在一个单独的文件中。

1
2
3
4
5
6
7
8
9
10
// Java
// Additional class in separate file
public class Test$lambda implements Function0 < Unit > {
public Unit invoke() {
// code
}
};

// Usage
Function0 lambda = new Test$lambda();

这两种方式没有特别大的区别。

我们注意到,这个函数类型被转换成Function0类型。在Kotlin中,无参类型会被编译器转换成Function0,同理单参数,两参数转换成Function1, Function2,Function3等

  • ()->Unit 编译成 Function0
  • ()->Int 编译成 Function0
  • (Int)->Int 编译成 Function1<Int, Int>
  • (Int, Int)->Int 编译成 Function2<Int, Int, Int>

这些所有的接口都是Kotlin编译器生成的。你不能在Kotlin里显示的使用他们,因为它们是按需生成的,而应该使用函数类型。知道函数类型只是接口为你开启了很多的可能性。比如:

1
2
3
4
5
class OnClickListener : () -> Unit {
override fun invoke() {
// ...
}
}

正如在高效Kotlin-47中所述:避免不必要的对象创建,把函数体包装成对象拖慢代码。这就是为什么下面的代码中,第一个更快。

1
2
3
4
5
6
7
8
9
10
11
inline fun repeat(times: Int, action: (Int) -> Unit) {
for (index in 0 until times) {
action(index)
}
}

fun repeatNoinline(times: Int, action: (Int) -> Unit) {
for (index in 0 until times) {
action(index)
}
}

这种差异是显而易见的,但在现实生活中的例子中很少有显著差异。我们把测试用例设计一下,放大这种差异:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Benchmark
fun nothingInline(blackhole: Blackhole) {
repeat(100_000_000) {
blackhole.consume(it)
}
}

@Benchmark
fun nothingNoninline(blackhole: Blackhole) {
noinlineRepeat(100_000_000) {
blackhole.consume(it)
}
}

第一个在我们电脑平均运行189ms。第二个平均447ms。这种差距体现在:第一个例子迭代调用空函数。第二个例子迭代调用对象,这个对象调用一个空函数。这里使用了额外的对象。

看一个更典型的例子。我们有5000个产品,计算我们购买物品的价格:

1
users.filter { it.bought }.sumByDouble { it.price }

在我的机器上平均耗时38ms。如果filter和sumByDouble不内联耗时多少呢?平均42ms!看起来不多,但每次调用也有10%的差异。

内联和非内联函数最大的区别在于,当我们在函数字面量里捕获变量时。变量被使用时需要被包装成对象。例如:

1
2
3
4
var l = 1L
noinlineRepeat(100_000_000) {
l += it
}

一个本地变量不能直接在非内联lambda中使用。这就是为什么要被包装成引用对象:

1
2
3
4
5
val a = Ref.LongRef()
a.element = 1L
noinlineRepeat(100_000_000) {
a.element = a.element + it
}

区别很大是因为,通常这个对象会被使用很多次,上面代码中的a变量使用了两次。因此,额外的对象调用2*100_000_000。再看一下这个例子👇🏻:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Benchmark
// On average 30 ms
fun nothingInline(blackhole: Blackhole) {
var l = 0L
repeat(100_000_000) {
l += it
}
blackhole.consume(l)
}

@Benchmark
// On average 274 ms
fun nothingNoninline(blackhole: Blackhole) {
var l = 0L
noinlineRepeat(100_000_000) {
l += it
}
blackhole.consume(l)
}

第一个在我电脑运行30ms,第二个274ms。差距的原因是,函数是一个对象,本地变量需要被包装。小的影响累计放大了。大多数情况下,我们不知道有函数类型参数的方法被如何使用,当我们定义这种函数时,最好内联一下。这也是标准库经常这么写的原因。

三、不允许非本地返回

前面定义的repeatNoninline像是一个控制结构,拿它和if语句或for循环对比一下。

1
2
3
4
5
6
7
8
9
10
11
if (value != null) {
print(value)
}

for (i in 1..10) {
print(i)
}

repeatNoninline(10) {
print(it)
}

明显的区别是,内部不能返回

1
2
3
4
5
6
fun main() {
repeatNoinline(10) {
print(it)
return // ERROR: Not allowed
}
}

这是函数字面量编译的结果。当我们的代码处于另外一个类时,不能从main函数返回。但是,当使用内联时,就没限制。

1
2
3
4
5
6
fun main() {
repeat(10) {
print(it)
return // OK
}
}

得益于此,函数可以看起来更像控制结构:

1
2
3
4
5
6
7
fun getSomeMoney(): Money? {
repeat(100) {
val money = searchForMoney()
if (money != null) return money
}
return null
}
四、内联修饰符的缺陷

inline很有用,但不应该随处使用。有些情况下不建议使用。再来看看最重要的限制。

  • 内联函数不能递归,否则调用展开将无限循环。周期性循环尤其危险,因为Intellij不报错:
1
2
3
4
5
6
7
8
9
inline fun a() {
b()
}
inline fun b() {
c()
}
inline fun c() {
a()
}
  • 内联函数不能使用可见性约束的元素

public inline fun中不能使用private 、internal修饰的函数或属性

1
2
3
4
5
6
7
8
internal inline fun read() {
val reader = Reader() // Error
// ...
}

private class Reader {
// ...
}

这也是为什么它不能用于隐藏实现,并且很少在类中使用。

  • 内联函数使代码膨胀

定义一个打印3的函数:

1
2
3
inline fun printThree() {
print(3)
}

调用三次:

1
2
3
4
5
inline fun threePrintThree() {
printThree()
printThree()
printThree()
}

又定义下面函数:

1
2
3
4
5
6
7
8
9
10
11
inline fun threeThreePrintThree() {
threePrintThree()
threePrintThree()
threePrintThree()
}

inline fun threeThreeThreePrintThree() {
threeThreePrintThree()
threeThreePrintThree()
threeThreePrintThree()
}

看一下它们的编译结果,前两个还能看:

1
2
3
4
5
6
7
8
9
inline fun printThree() {
print(3)
}

inline fun threePrintThree() {
print(3)
print(3)
print(3)
}

后两个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
inline fun threeThreePrintThree() {
print(3)
print(3)
print(3)
print(3)
print(3)
print(3)
print(3)
print(3)
print(3)
}

inline fun threeThreeThreePrintThree() {
print(3)
print(3)
print(3)
...
print(3)
print(3)
print(3)
print(3)
}

这个例子展示了内联函数的弊端:过度使用时代码膨胀严重。

五、crossinline 和noinline

有时候我们想内联一个函数,由于某些原因,不能内联全部的函数类型参数。这种情况下我们使用下面的修饰符:

crossinline:用于内联函数的参数,表示此参数内联范围扩大。对内联函数内部的lambda生效:

1
2
3
4
5
6
inline fun hello(postAction:()->Unit){
println("Hello")
runOnUiThread {
postAction()
}
}

noinline:用于内联函数的参数,表示此参数不能被内联。可以局部性关闭内联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
inline fun requestNewToken(
hasToken: Boolean,
crossinline onRefresh: () -> Unit,
noinline onGenerate: () -> Unit
) {
if (hasToken) {
httpCall("get-token", onGenerate) // We must use
// noinline to pass function as an argument to a
// function that is not inlined
} else {
httpCall("refresh-token") {
onRefresh() // We must use crossinline to
// inline function in a context where
// non-local return is not allowed
onGenerate()
}
}
}

fun httpCall(url: String, callback: () -> Unit) {
/*...*/
}

能记住这两个修饰符最好,记不住也没事,IntelliJ会有提示:

六、总结:

使用inline的主要场景是:

  • 经常被使用的函数
  • 具体化的类型参数,像:filterIsInstance
  • 定义带有函数类型的参数的顶层函数。尤其是辅助函数,如集合处理(map、filter)、作用域函数(also、apply、let)、顶层工具函数(repeat、run、with)

我们很少用inline定义API,注意内联函数调用内联函数的情景。记住代码膨胀。

参考:
Effective Kotlin Item 48: Use inline modifier for functions with parameters of functional types

一、概述

你可能对 IterableSequence 傻傻分不清。看一下他们的代码:

1
2
3
4
5
6
7
interface Iterable<out T> {
operator fun iterator(): Iterator<T>
}

interface Sequence<out T> {
operator fun iterator(): Iterator<T>
}

好像只有名字不一样-_-||,但他们却有着本质的区别。Sequence属于懒加载,中间操作符不会触发计算,仅仅是对前一个Sequence的装饰,只有在遇到toList()或count()这些终止操作符时才会执行真正的计算工作。而Iterable每一步都返回一个新的集合。

1
2
3
4
5
6
7
8
9
10
11
public inline fun <T> Iterable<T>.filter(
predicate: (T) -> Boolean
): List<T> {
return filterTo(ArrayList<T>(), predicate)
}

public fun <T> Sequence<T>.filter(
predicate: (T) -> Boolean
): Sequence<T> {
return FilteringSequence(this, true, predicate)
}

下图中filter操作符不做任何计算操作,只是返回一个装饰器对象,在遇到toList()操作符才执行计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
val seq = sequenceOf(1, 2, 3)
val filtered = seq.filter { print("f$it "); it % 2 == 1 }
println(filtered) // FilteringSequence@...

val asList = filtered.toList()
// f1 f2 f3
println(asList) // [1, 3]

val list = listOf(1, 2, 3)
val listFiltered = list
.filter { print("f$it "); it % 2 == 1 }
// f1 f2 f3
println(listFiltered) // [1, 3]

二、优势

Sequence的惰性执行有以下几个优点:

  • 保证操作的顺序性
  • 保证操作执行次数最少化
  • 他们可以是无限的
  • 无需每一步创建新的集合

下面依次讨论一下这些优点

一、顺序性很重要
使用Sequence处理数据时,每个元素依次执行所有操作符。这是一种元素接元素的处理方式。 使用Iterable处理数据时,每个操作符依次执行所有数据。这是一种步骤接步骤的处理方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
sequenceOf(1, 2, 3)
.filter {
print("F$it, ");
it % 2 == 1
}
.map {
print("M$it, ");
it * 2
}
.forEach {
print("E$it, ")
}
// Prints: F1, M1, E2, F2, F3, M3, E6,
1
2
3
4
5
6
7
8
9
10
11
12
13
listOf(1, 2, 3)
.filter {
print("F$it, ");
it % 2 == 1
}
.map {
print("M$it, ");
it * 2
}
.forEach {
print("E$it, ")
}
// Prints: F1, F2, F3, M1, M3, E2, E6,

如果我们不用集合处理函数,而是用循环和条件语句,这和sequence一样,也是一种元素接元素的处理方式。这也为我们提供了一种编译器底层优化的思路:sequence操作可以被优化成循环和条件。

1
2
3
4
5
6
7
8
9
for (e in listOf(1, 2, 3)) {
print("F$e, ")
if (e % 2 == 1) {
print("M$e, ")
val mapped = e * 2
print("E$mapped, ")
}
}
// Prints: F1, M1, E2, F2, F3, M3, E6,
二、Sequence执行最少的操作

我们没必要每一步都处理整个集合。假如我们有一百万个数据,我们只需要前十个,没必要处理十个之后的数据。所以sequence可以执行最少的操作。

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
(1..10).asSequence()
.filter {
print("F$it, ");
it % 2 == 1
}
.map {
print("M$it, ");
it * 2
}
.find {
it > 5
}
// Prints: F1, M1, F2, F3, M3,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(1..10)
.filter {
print("F$it, ");
it % 2 == 1
}
.map {
print("M$it, ");
it * 2
}
.find {
it > 5
}
// Prints:
//F1, F2, F3, F4, F5, F6, F7, F8, F9, F10,
// M1, M3, M5, M7, M9,

这个例子中,有很多个操作符,最后的终止操作符不需要处理所有的数据,因此sequence性能更好。类似的操作符还有:first, find, take, any, all, noneindexOf

三、Sequence是无限的

由于Sequence按需处理,我们可以定义无限序列。一种常用的方式是使用sequence生成器 generateSequencesequence

generateSequence需要传如第一个值,以及如何产生下一个值:

1
2
3
4
5
generateSequence(1) { it + 1 }
.map { it * 2 }
.take(10)
.forEach { print("$it, ") }
// Prints: 2, 4, 6, 8, 10, 12, 14, 16, 18, 20,

sequence 则使用挂起函数按需生成数据。只要我们需要数据他就执行,一直到调用yield方法。然后挂起直到下次再向他请求数据。下面是一个无限生成斐波那契数列的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.math.BigDecimal

val fibonacci: Sequence<BigDecimal> = sequence {
var current = 1.toBigDecimal()
var prev = 1.toBigDecimal()
yield(prev)
while (true) {
yield(current)
val temp = prev
prev = current
current += temp
}
}

fun main() {
print(fibonacci.take(10).toList())
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
}

需要注意的是,使用无限序列需要限制元素的数量,否则将无限的运行下去。

1
print(fibonacci.toList()) // Runs forever

为了不让它无限循环的运行,我们可以使用take限制元素数量,或者使用first、find、indexOf等。在不限制数量的情况下,不要使用any、all、none。

四、sequence每一步不产生新集合

标准的集合处理函数每一步都返回新的集合,当我们处理大量数据时会分配很多临时内存。

1
2
3
4
5
6
7
8
9
10
num
.filter {// 1
it % 10 == 0
}
.map {// 2
it * 2
}
.sum()
// In total, 2 collections
// created under the hood
1
2
3
4
5
6
7
8
9
num.asSequence()
.filter {//0
it % 10 == 0
}
.map {//0
it * 2
}
.sum()
// No collections created

一个极端又常见的例子:文件读取。文件可能是几个G,每执行一个操作符都分配这么多内存是一种极大的浪费。下面例子是读取大小1.53G的芝加哥犯罪记录中包含毒品交易信息的记录数量,其中readLines 返回 List

1
2
3
4
5
6
7
8
9
10
// BAD SOLUTION, DO NOT USE COLLECTIONS FOR 
// POSSIBLY BIG FILES
File("ChicagoCrimes.csv")
.readLines()
.drop(1) // Drop descriptions of the columns
.mapNotNull { it.split(",").getOrNull(6) }
// Find description
.filter { "CANNABIS" in it }
.count()
.let(::println)

这段程序在我电脑上的运行结果是:

script
1
OutOfMemoryError.n> Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

我们创建了一个集合,中间三个操作符产生集合,一个四个集合。其中三个包含文件的主要主要数据记录,一共消耗4.59G。正确的实现应该使用sequence,我们使用useLines函数,每次只操作一行记录。

1
2
3
4
5
6
7
8
9
File("ChicagoCrimes.csv").useLines { lines ->
// The type of `lines` is Sequence<String>
lines.drop(1) // Drop descriptions of the columns
.mapNotNull { it.split(",").getOrNull(6) }
// Find description
.filter { "CANNABIS" in it }
.count()
.let { println(it) } // 318185
}

同样运行这段代码,只耗时8.3s。为了比较一下这两种方法的效率,我做了另外一个实验:删除数据中不必要的列以减少文件大小,得到CrimeData.csv只有728MB,然后做相同的操作。使用Collection处理函数,耗时13s,使用sequence函数,耗时4.5s。正如实验数据,使用sequence处理大文件不仅节约内存,而且提升性能。

事实上,在每个步骤中,我们创建一个新的集合本身也是一种成本,当我们处理包含大量元素的集合时,这种成本就会显现出来。差别并不大——主要是因为在许多步骤中创建的集合都是用预期的大小初始化的,所以当我们添加元素时,我们只需要将它们放在下一个位置。尽管即使是廉价的集合复制也比完全不复制要昂贵,这也是为什么我们应该更喜欢对具有多个处理步骤大集合使用Sequence的主要原因。

大集合:元素多(含数万个元素的整数列表)、元素大(超长字符串)

多个处理步骤:处理集合时使用很多操作符。

如果对比下面两个函数:

1
2
3
4
5
6
7
8
9
fun singleStepListProcessing(): List<Product> {
return productsList.filter { it.bought }
}

fun singleStepSequenceProcessing(): List<Product> {
return productsList.asSequence()
.filter { it.bought }
.toList()
}

你会发现,在性能上区别不大(实际上,简单的list处理更快,因为它的filter函数是内联的)。但是,当你使用多个操作符,先filter再map,在大集合上性能问题就行显现出来。为了看到区别,我们比较一下2个操作符和3个操作符处理5000个数据的情况:

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
fun twoStepListProcessing(): List<Double> {
return productsList
.filter { it.bought }
.map { it.price }
}

fun twoStepSequenceProcessing(): List<Double> {
return productsList.asSequence()
.filter { it.bought }
.map { it.price }
.toList()
}

fun threeStepListProcessing(): Double {
return productsList
.filter { it.bought }
.map { it.price }
.average()
}

fun threeStepSequenceProcessing(): Double {
return productsList.asSequence()
.filter { it.bought }
.map { it.price }
.average()
}

下面是在MacBook Pro(Retina, 15-inch, Late 2013)处理5000个元素的平均结果:

1
2
3
4
twoStepListProcessing                        81 095 ns
twoStepSequenceProcessing 55 685 ns
twoStepListProcessingAndAcumulate 83 307 ns
twoStepSequenceProcessingAndAcumulate 6 928 ns

很难预测我们能提升但是性能,根据我的观察,在一个包含多个步骤的典型集合处理中,对于至少几千个元素,我们可以预期大约20-40%的性能提升。

五、sequence什么时候没那么快?

有些情况我们要处理整个集合,sequence并不能给我们带来什么收益。如sorted(当前来讲它是唯一一个例子)。sorted的最优实现:积攒Sequence并放入List,然后使用Java的sort函数。缺点是,与Collection.sort相比,积攒过程会消耗额外的时间。

Sequence是否应该支持sorted函数是有争议的,因为当一个序列的方法需要所有元素才能完成计算时,后续操作只是部分延迟,并且它不支持无限序列。之所以添加sorted操作只是因为它很常用,好用。Kotlin开发者需要了解它的缺陷,尤其是它不能用于无限序列。

1
2
3
4
generateSequence(0) { it + 1 }.take(10).sorted().toList()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
generateSequence(0) { it + 1 }.sorted().take(10).toList()
// Infinite time. Does not return.

在Collection上使用sorted比在Sequence上更快只是少数特例。当我们只使用很少操作符函数和单个sorted函数时,还是建议使用sequence来处理。

1
2
3
4
5
6
productsList.asSequence()
.filter { it.bought }
.map { it.price }
.sorted()
.take(10)
.sum()
六、看看Java的Stream操作

Java 8新增了集合处理流的特性,看起来和Kotlin的序列很像。

1
2
3
4
productsList.asSequence()
.filter { it.bought }
.map { it.price }
.average()
1
2
3
4
5
productsList.stream()
.filter { it.bought }
.mapToDouble { it.price }
.average()
.orElse(0.0)

Java 8的Stream也是惰性的在最后一个操作符触发计算。与Kotlin sequence的区别主要是:

  • Kotlin sequence包含更多的处理函数(被定义为拓展函数),使用简单: toList() vs collect(Collectors.toList())
  • Java stream可以开启并行模式,在多核处理器上会有很大的性能提升。
  • Kotlin sequence可以用于Kotlin/JVM、Kotlin/JS、Kotlin/Native模块中。Java stream只能用于Kotlin/JVM,且JVM版本至少是8

总之,我们不用Java stream并行模式时很难将谁的性能更好,我建议少用Java stream,只有在处理重量级计算问题中使用并行模式可以显著带来收益的情况下使用,其他情况用Kotlin标准函数能带来清晰的代码结构和多平台支持。

七、调试Sequence

Kotlin Sequence和Java Stream都支持debug每一步操作。Java需要使用”Java Stream Debugger”插件,Kotlin则需要”Kotlin Sequence Debugger”插件,现在已经被集成在了Kotlin插件中了。

三、总结

集合和序列非常相似,都支持相同的处理函数。但他们也有很大的区别。Sequence处理是困难的,因为我们要保持原集合不变,执行相应转换后再放回原集合。Sequence是惰性的,带来很多优点:

  • 保证操作的顺序性
  • 保证操作执行次数最少化
  • 他们可以是无限的
  • 无需每一步创建新的集合

基于这些优点,对于包含多个处理步骤的包含大对象或元素很多的集合来说使用Sequence更好。Sequence也包含调试器,能帮助我们可视化的分析元素处理过程。Sequence不是为了取代传统的集合处理方式,使用前应该分析清楚自己的目的和原因才能带来性能的提升以及更少的内存问题。

四、参考

Effective Kotlin Item 51: Prefer Sequence for big collections with more than one processing step

    协程一般用于异步编程。在解决异步编程问题方面,Kotlin编译器在对协程的设计和实现是通用的。我们可以借助协程优雅的解决深度递归问题。

一、问题描述

定义以下二叉树的结点,并构造一棵只含十万个左结点的树,进行深度遍历,求树的深度:

1
2
3
4
5
6
7
8
9
//结点定义
class Tree(val left: Tree?, val right: Tree?)

//构造二叉树:以叶子结点开始,重复构造父结点,并把当前结点作为父结点的左子树
val n = 100_000
val deepTree = generateSequence(Tree(null, null)) { prev ->
Tree(prev, null)
}.take(n).last()

二、解决方案

2.1 解决方案一:
1
2
3
4
5
fun depth(t: Tree?): Int =
if (t == null) 0 else maxOf(
depth(t.left), // recursive call one
depth(t.right) // recursive call two
) + 1

分析:
对树进行递归是最简洁直接的解决方案。而递归将保存函数的调用栈用于后续状态恢复,线程的调用栈是有大小限制的,此处将抛出Exception in thread "main" java.lang.StackOverflowError错误。

2.2 解决方案二:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun depth(t: Tree?): Int {
if (t == null) return 0
class Frame(val node: Tree, var state: Int = 0, var depth: Int = 1)
val stack = ArrayList<Frame>()
val root = Frame(t)
stack.add(root)
while (stack.isNotEmpty()) {
val frame = stack.last()
when (frame.state++) {
0 -> frame.node.left?.let { l -> stack.add(Frame(l)) }
1 -> frame.node.right?.let { r -> stack.add(Frame(r)) }
2 -> {
stack.removeLast()
stack.lastOrNull()?.let { p ->
p.depth = maxOf(p.depth, frame.depth + 1)
}
}
}
}
return root.depth
}

分析:
基于解决方案一,我们考虑将调用栈状态的保存转移到内存空间更大的堆区,并加入了状态机和while循环。在此例中,程序开始时将全部走状态0,将所有的左节点加入stack。然后全部走状态2(因为没有右结点),由下到上计算每一个结点的深度。最后返回根节点的深度。

2.3 解决方案三:
1
2
3
4
5
6
val depth = DeepRecursiveFunction<Tree?, Int> { t ->
if (t == null) 0 else maxOf(
callRecursive(t.left),
callRecursive(t.right)
) + 1
}

分析:
方案二中的状态机就是Kotlin挂起函数的实现原理。所以我们也可以利用Kotlin的挂起函数实现深度遍历。那么方案三是如何实现的呢?我们分析三个方面:

  • 如何调用
  • 如何进入循环
  • 如何保存状态

2.3.1 调用
DeepRecursiveFunction类声明了invoke操作符,调用depth函数就是调用下面👇🏻的操作符。

1
2
3
4
@SinceKotlin("1.4")
@ExperimentalStdlibApi
public operator fun <T, R> DeepRecursiveFunction<T, R>.invoke(value: T): R =
DeepRecursiveScopeImpl<T, R>(block, value).runCallLoop()

2.3.2 循环
调用DeepRecursiveFunction后直接进入runCallLoop()循环,result的默认值为UNDEFINED_RESULT,所以会启动我们的递归业务逻辑

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
@Suppress("UNCHECKED_CAST")
fun runCallLoop(): R {
while (true) {
// Note: cont is set to null in DeepRecursiveScopeImpl.resumeWith when the whole computation completes
val result = this.result
val cont = this.cont
?: return (result as Result<R>).getOrThrow() // done -- final result
// The order of comparison is important here for that case of rogue class with broken equals
if (UNDEFINED_RESULT == result) {
// call "function" with "value" using "cont" as completion
val r = try {
// This is block.startCoroutine(this, value, cont)
function.startCoroutineUninterceptedOrReturn(this, value, cont)
} catch (e: Throwable) {
cont.resumeWithException(e)
continue
}
// If the function returns without suspension -- calls its continuation immediately
if (r !== COROUTINE_SUSPENDED)
cont.resume(r as R)
} else {
// we returned from a crossFunctionCompletion trampoline -- call resume here
this.result = UNDEFINED_RESULT // reset result back
cont.resumeWith(result)
}
}
}

2.3.3 状态保存
每次调用callRecursive时将保存当前的值和cont(continuation),注意这里cont对象每次都不是同一个对象。当进行挂起恢复时,拿到旧的continuation进行回调。

1
2
3
4
5
6
suspend fun callRecursive(value: T): R = 
suspendCoroutineUninterceptedOrReturn { cont ->
this.cont = cont
this.value = value
COROUTINE_SUSPENDED
}

参考:

Deep recursion with coroutines

一、概述

    Android中的视图是以树的形式组织起来的,它是一种层次结构。在代码中体现为组合模式,一个ViewGroup可以包含一个或多个View,同时ViewGroup又是一个View。在布局文件中体现为xml的结点和缩进。
    同时视图的渲染少不了对其进行遍历,这就涉及数据结构中树的深度优先遍历和广度优先遍历。有时候一些复杂的布局一次遍历还无法完全确定View的信息。如何通过算法方式降低树的层次呢?也许这就是约束布局存在的意义吧。
    在Android中,灵活运用ConstraintLayout包括以下几个点:

  • 主属性
    • 通过相对位置约束View
    • 控制约束之间的距离
    • 居中和偏移百分比
    • 通过圆定位📌View
    • 通过可见性控制View
    • 通过分辨率约束View
    • 通过链⛓约束View
  • 辅助工具
    • Barrier屏障约束
    • Group分组约束
    • Placeholder占位约束
    • Guideline引导线约束

二、使用

2.1 通过相对位置约束View
约束属性 描述 约束属性 描述
layout_constraintLeft_toLeftOf layout_constraintLeft_toRightOf
layout_constraintRight_toLeftOf layout_constraintRight_toRightOf
layout_constraintTop_toTopOf layout_constraintTop_toBottomOf
layout_constraintBottom_toTopOf layout_constraintBottom_toBottomOf
layout_constraintStart_toEndOf layout_constraintStart_toStartOf
layout_constraintEnd_toStartOf layout_constraintEnd_toEndOf
layout_constraintBaseline_toBaselineOf
2.2 控制约束之间的距离
约束属性 描述 约束属性 描述
android:layout_marginStart layout_goneMarginStart
android:layout_marginEnd layout_goneMarginEnd
android:layout_marginLeft layout_goneMarginLeft
android:layout_marginTop layout_goneMarginTop
android:layout_marginRight layout_goneMarginRight
android:layout_marginBottom layout_goneMarginBottom
android:layout_marginBaseline layout_goneMarginBaseline
2.3 居中和偏移百分比
约束属性 描述 约束属性 描述
layout_constraintHorizontal_bias layout_constraintVertical_bias
2.4 通过圆定位📌View
约束属性 描述
layout_constraintCircle 另一个widget的id
layout_constraintCircleRadius 圆的半径
layout_constraintCircleAngle 角度
1
2
3
4
<Button android:id="@+id/buttonB" 
app:layout_constraintCircle="@+id/buttonA"
app:layout_constraintCircleRadius="100dp"
app:layout_constraintCircleAngle="45" />
2.5 通过可见性控制View
2.6 通过分辨率约束View
约束属性 描述 约束属性 描述
android:minWidth android:minHeight
android:maxWidth android:maxHeight

2.6.1 百分比

1
2
3
4
5
<Button android:id="@+id/buttonA" 
android:layout_width="0dp"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent="0.5"
/>

app:layout_constraintWidth_default可以取的值包括:

  • spread
  • percent
  • wrap

在ConstraintLayout-1.1之后,使用app:layout_constrainedWidth="true"替代app:layout_constraintWidth_default="wrap"

2.6.2 比率
宽高一比一:

1
2
3
4
<Button android:id="@+id/buttonA" 
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
/>

指定一条边符合约束比率:

1
2
3
4
5
<Button android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
2.7 通过链⛓约束View
图示 Style
_chainStyle="spread"
_chainStyle="spread_inside"
_chainStyle="spread"
_weight="1"
_chainStyle="packed"
_chainStyle="packed"
_bias="0.3"
2.8 Barrier

将多个View的某一边的极端值作为约束:

1
2
3
4
5
6
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="start/end"
app:constraint_referenced_ids="button1,button2" />
2.9 Group分组约束

将多个View作为一个组一起控制:

1
2
3
4
5
6
<androidx.constraintlayout.widget.Group
android:id="@+id/group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="button4,button9" />
  • 无法通过group设置点击事件
    1
    2
    3
    4
    5
    group.referencedIds.forEach { id ->
    view.findViewById(id).setOnClickListener {
    //do something
    }
    }
2.10 Placeholder占位约束

    Placeholder是一个虚拟的占位符View,界面上其他存在的View可以通过placeholder.setContentId(R.id.xxx)将自己的位置设置到placeholder的位置,原位置视图将不可见。
    我们可以使用Placeholder搭建一个布局模板,include到其他布局当中,来填充模板中的视图,这将使所有的界面有一个通用的模板。

2.11 Guideline引导线约束

Guideline只能在ConstraintLayout中使用,在水平或垂直方向设置辅助布局的不可见线条。

约束属性 描述
layout_constraintGuide_begin 距布局的左边或者上边x处设置引导线
layout_constraintGuide_end 距布局右边或下面x处设置引导线
layout_constraintGuide_percent 宽或高的百分之x处设置引导线
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/guideline"
app:layout_constraintGuide_begin="100dp"
android:orientation="vertical"/>

<Button
android:text="Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/button"
app:layout_constraintLeft_toLeftOf="@+id/guideline"
android:layout_marginTop="16dp"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

三、原理

3.1 解决约束问题

3.1.1 定义变量

1
x[1], x[2], ... x[n]

3.1.2 定义约束问题:

1
2
3
a[1]x[1] + ... + a[n]x[n] = b
a[1]x[1] + ... + a[n]x[n] <= b
a[1]x[1] + ... + a[n]x[n] >= b

3.2.3 计算约束方程
食火鸡算法:食火鸡是一种生活在新几内亚热带雨林中的鸟类,以水果为食。同时它也是一种解决线性方程和线性不等式的算法。1990年在华盛顿大学被证明和发现。线性方程非常适合用于表示用户界面中视图的位置、大小、与其他视图的关系。

3.2 个人理解:

定义变量 -> 声明View对象
定义约束问题 -> 建立View之间的约束关系
计算约束方程 -> 计算视图的大小、坐标

四、参考文档

1.官方文档
2.基本使用
3.基本使用-译文
4.ConstraintLayout, Inside and Out: Part 1
5.ConstraintLayout, Inside and Out: Part 2
6.线性约束解决算法
7.解决约束

一、性能

1.1 在分层存储结构中,随着存储容量的增加,延迟也随之增加。参考

从数据可以看出,L1缓存和主存访问速度相差200倍。

二、缓存行

CPU缓存的基本单位是缓存行,主存数据和缓存的映射存在下列关系:

2.1 直接映射缓存

1、 index用于计算在哪一个缓存行
2、 offset用于计算在缓存行的哪个字节
3、 tag用于判断缓存是否命中(假设0x7f6601 和 0x8f6602两个地址,低位都一样,缓存行和字节偏移都命中了,tag不命中)

缺点:
图中,cache缓存8行数据,当访问第0行、第8行、第16行时,缓存无法命中,每次都要去主存加载,发生缓存颠簸

2.2 多路组相连

这是一个两路组相连的示意图:

1、index定位组
2、组内依次对tag进行对比(可以通过硬件并行比较增加性能)

三、缓存一致性

3.1 问题描述:

处理器 1 读 X :从内存读取24并缓存
处理器 2 读 X :从内存读取24并缓存
处理器 1 写 X = 32 :更新自己的缓存
处理器 3 读 X = ?

如何保证缓存的一致性?参考

3.2 解决方案

3.1.1 嗅探-Snooping Solution(Snoopy Bus)

根据写操作对缓存数据的影响嗅探协议可分为:
Write-invalidate:
当处理器写入数据时,所有的独立缓存将通过总线嗅探得到通知,并标志自己的缓存失效。这保证了全局只有一份缓存是有效的。

  • MSI
  • MESI
  • MOSI
  • MOESI
  • MESIF

Write-update:
当处理器写入数据时,所有的独立缓存将通过总线嗅探得到通知,并更新自己的缓存。通过总线广播给所有的缓存造成了总线繁忙,所以不常见。

3.1.2 目录-Directory-Based Schemes

3.2 MESI协议参考

MESI是四个状态的首字母缩写,在缓存行中用2个bit标志该缓存行的状态。

  • M:Modified(只存在当前缓存行,已被修改,和主存不一致,需要更新回主存)
  • E:Exclusive(只存在当前缓存行,和主存一致)
  • S:Shared(其他缓存行也存在该数据,和主存一致)
  • I:Invalid(此缓存行失效)

MESI的状态迁移:

四、参考文献

0.Java角度理解CPU缓存
1.Cache的基本原理
2.每个开发者需要知道的数据
3.硬件角度看内存屏障
4.软件角度内存屏障
5.stackoverflow-How do cache lines work?
6.courses-pdf

一、概述

很多时候我们都有一个疑问:一个对象在内存中占用多大的空间呢?

script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ jdk8-64/java -jar jol-cli.jar internals java.lang.Object
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.

Instantiated the sample instance via default constructor.

java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 00 10 00 00 # (not mark word)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

通过OpenJDK的Java-Object-Layout我们看到java.lang.Object的一个实例占用16 bytes。

同样的java.lang.Boolean类型占用的字节数:

script
1
2
3
4
5
6
7
java.lang.Boolean object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 1 boolean Boolean.value N/A
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
script
1
2
3
[HEADER: 12 bytes]  12 
[value: 1 byte ] 13
[padding: 3 bytes] 16

其实一个对象通常由三块组成,分别是:对象头、实例数据和对齐填充。

二、对象头(object header)

在前面通过JOL打印输出的关于java.lang.Object信息中,我们看到object header占用12字节,但是输出并没有包含详细的结构信息,我们可以通过Hotspot的源码了解到对象头包含两个部分:mark wordclass word

2.1 mark word

mark word在32位和64位机分别占32位和64位,当其中锁标志位的值不同时,它前面的bit存储不同的含义。

  • 存储对象的gc年龄信息
  • 存储Hashcode
  • 存储锁信息
2.2 class word:

代码运行的时候,对象只是一串字节,我们可以通过class-word获取一些对象的元信息,它存储指向方法区中表示对象类型的指针,比如以下使用场景:

  • 运行时类型检查
  • 决定对象大小
  • 计算接口调用的目标类
2.3 数组长度:

如果是数组类型,对象头会额外存储数组的长度信息。

  • 快速计算对象的大小
  • 数组边界检查

三、实例数据和对齐填充

实例数据即我们在代码中声明的变量等信息,它的存储受到一些规则的约束以及虚拟机参数的控制。

3.1 没有属性的类的内存布局

规则一:每个对象都是八字节对齐。

从前面Object的输出中,我们看到,当一个对象只有头部信息时占用16byte,刚好是8的整数倍。

3.2 Object子类的内存布局

跟在对象头后面的类属性按照它们的大小在内存中排序,例如:int是4字节、long是8字节。采用字节对齐可以提高性能,因为从内存读取4字节到4字节的寄存器性能更好。

为了节约内存,Sun的虚拟机在分配对象字段的时候和它们声明的顺序不同,有如下顺序规则:

1、double和long类型
2、int和float
3、short和char
4、boolean和byte
5、reference

为什么可以优化内存呢?我们看一下这个例子:

1
2
3
4
5
6
7
class MyClass {
byte a;
int c;
boolean d;
long e;
Object f;
}

它的对象布局如下:

script
1
2
3
4
5
6
7
8
9
[HEADER:  8 bytes]  8
[a: 1 byte ] 9
[padding: 3 bytes] 12
[c: 4 bytes] 16
[d: 1 byte ] 17
[padding: 7 bytes] 24
[e: 8 bytes] 32
[f: 4 bytes] 36
[padding: 4 bytes] 40

总共使用了40字节内存,其中14个用于内存对齐而浪费掉。如果重排顺序则:

script
1
2
3
4
5
6
7
8
[HEADER:  8 bytes]  8
[e: 8 bytes] 16
[c: 4 bytes] 20
[a: 1 byte ] 21
[d: 1 byte ] 22
[padding: 2 bytes] 24
[f: 4 bytes] 28
[padding: 4 bytes] 32

经过优化后只有6个字节用于对齐填充,总内存也只有32byte。

3.3 子类的内存布局

规则三:继承结构中属于不同类的字段不混合在一起。父类优先,子类其次。

1
2
3
4
5
6
7
8
9
class A {
long a;
int b;
int c;
}

class B extends A {
long d;
}

B类的布局如下:

script
1
2
3
4
5
[HEADER:  8 bytes]  8
[a: 8 bytes] 16
[b: 4 bytes] 20
[c: 4 bytes] 24
[d: 8 bytes] 32

如果父类的字段不符合4字节对齐。

规则四:父类的最后一个字段和子类第一个字段之间必须4字节对齐。

1
2
3
4
5
6
7
class A {
byte a;
}

class B {
byte b;
}
script
1
2
3
4
5
[HEADER:  8 bytes]  8
[a: 1 byte ] 9
[padding: 3 bytes] 12
[b: 1 byte ] 13
[padding: 3 bytes] 16

a后面的3个字节就是为了使其4字节对齐。这3个字节只能浪费不能给B使用。

最后一个规则可以用于节约一些内存空间:当子类的第一个属性是long或者double类型且父类没有以8字节边界结束。

规则五:子类的第一个字段是doubel或long且父类没有以8字节边界结束,JVM打破2的规则,先存储int、short、byte、reference来填补空缺。

例如:

1
2
3
4
5
6
7
8
9
class A {
byte a;
}

class B {
long b;
short c;
byte d;
}

内存布局如下:

script
1
2
3
4
5
6
7
[HEADER:  8 bytes]  8
[a: 1 byte ] 9
[padding: 3 bytes] 12
[c: 2 bytes] 14
[d: 1 byte ] 15
[padding: 1 byte ] 16
[b: 8 bytes] 24

在字节12的位置,A类结束了。JVM打破2的规则放入short和byte类型,节约了4字节中的3个字节,否则将浪费掉。

3.4 数组的内存布局

数组类型有一个额外的头部字段保存数组的长度,接下来是数组的元素,数组作为普通对象也是8字节对齐的。

这是byte[3]的布局:

script
1
2
3
4
5
[HEADER:  12 bytes] 12
[[0]: 1 byte ] 13
[[1]: 1 byte ] 14
[[2]: 1 byte ] 15
[padding: 1 byte ] 16

这是long[3]的布局:

script
1
2
3
4
5
[HEADER:  12 bytes] 12
[padding: 4 bytes] 16
[[0]: 8 bytes] 24
[[1]: 8 bytes] 32
[[2]: 8 bytes] 40
3.5 内部类的内存布局

非静态内部类有一个额外的隐藏字段,持有外部类的引用。这个字段是一个常规的引用,它符合对象在内存布局的规则。内部类因此有4字节额外的开销。

一、概述

synchronized作为Java内建的关键字和锁机制,理解和使用它是必要的也是有难度的。个人认为深入理解此关键字在于以下几个方面:

  • 基础层面:正确理解对象锁和类锁,并熟练使用
  • 能力提升:
    • 了解对象的内存布局,观察锁如何影响对象的内存布局
    • 了解锁优化,锁升级及其过程
  • 更上一层楼:OpenJDK源码走读

本文只是记录和翻译了一些英文文档的内容,作为笔记以后翻阅。详细内容可看原文。

二、对象锁和类锁

1.1 对象锁:修饰代码块和实例方法

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
static class ObjectLock {
public void test1() {
synchronized (this) {//此代码块锁住当前对象
int i = 5;
while (i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
}

public synchronized void test2() {
int i = 5;
while (i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
}

1.2 类锁:修饰代码块和静态方法

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
 static class ClazzLock {
public void test1() {
synchronized (ClazzLock.class) {//此代码块锁住class类
int i = 5;
while (i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
}

/**
* 静态方法属于类
*/
public static synchronized void test2() {
int i = 5;
while (i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
}

三、锁优化

依据一:数据显示,许多锁有局部性特征,也就是说很多锁的锁定和释放都发生在特定的线程上。
依据二:数据显示,大部分Java对象不存在竞争。

  • 适应性自旋-Adaptive Spinning
  • 锁消除-Lock Elimination
  • 锁粗化-Lock Coarsening
  • 轻量级锁-Lightweight Locking
  • 偏向锁-Biased Locking
3.1 偏向锁

当我们开发了一个多线程环境下的模块时,其他人可能把它用在单线程环境下,此时加锁逻辑就是多余的。即使使用轻量级锁,每次获取锁都执行CAS原子指令也是一种性能损耗。使用偏向锁,线程首次获取锁时记录偏向于它自己,当有其他线程获取锁时,偏向锁要撤销,使用其他锁机制,如轻量级锁。

lock-field中有一个bit用于记录是否偏向状态,初始状态下是处于偏向锁状态,但没有设置偏向的线程ID。当线程首次获取锁时,发现没有指定偏向的线程,则使用CAS设置偏向线程,来避免竞态条件。

3.2 轻量级锁(Thin Lock)

相对于每次对象锁定都获取monitor结构的重量级锁而言,轻量级锁只是使用CAS原子指令修改lock-field(锁信息字段),因此轻量级锁不支持wait和notify。当发生锁竞争时,轻量级锁膨胀,同时分配monitor结构,lock-field更新成monitor的指针。

Java的锁是可重入的,所以需要记录当前锁的重入次数和锁的持有者。

  • 记录方式一:
    最直接的方式是把lock-field分成两部分,一个记录锁的持有者,一个记录锁重入次数。由于lock-field字段长度固定,所以重入次数被限制了。重入次数达到上限只能升级为重量级锁。
  • 记录方式二:
    当线程需要获取锁时,在获取锁的线程栈中保存lock-records记录。线程获取的所有锁记录都将保存在这个集合中,它的顺序和获取锁的顺序一致。

轻量级锁的变种和提升包含 tasuki-lockmeta-lock 其算法思想是一致的,差异在于实现细节,

1、轻量级锁的加锁过程

在代码进入同步块的时,如果同步对象锁状态为无锁(锁标志位为“01”,偏向锁位为“0”)虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方称之为 Displaced Mark Word)。然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。

如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。
否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

                 图2.1 轻量级锁CAS操作前/后堆栈与对象的状态

2、轻量级锁的解锁过程:

(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。

(2)如果替换成功,整个同步过程就完成了。

(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

3.3 锁消除

锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。

1
2
3
4
5
6
7
8
public class Test extends Thread{
public static void main(String[] args) {
contactString("aa", "bb", "cc");
}
public static String contactString(String s1, String s2, String s3) {
return new StringBuffer().append(s1).append(s2).append(s3).toString();
}
}

虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。

3.4 锁粗化

虚拟机对连续的加锁操作(synchronized append)进行范围扩展(粗化)到整个操作序列的外部。

1
2
3
4
5
6
7
public String concatString(String s1,String s2, String s3){
StringBuffer sb = new StringBUffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}

四、参考资料

1.Evaluating and improving biased locking in the HotSpot virtual machine
2.Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
3.锁:锁优化(synchronized 锁升级过程、锁消除、锁粗化)
4.Synchronization

一、什么是Flow

1、如何通过同步和异步方式返回多个值?

1.1 同步方式返回多个值

1
2
3
4
5
fun simple(): List<Int> = listOf(1, 2, 3)

fun main() {
simple().forEach { value -> println(value) }
}

1.2 同步方式依次返回多个值

1
2
3
4
5
6
7
8
9
10
fun simple(): Sequence<Int> = sequence { // sequence builder
for (i in 1..3) {
Thread.sleep(1000) // pretend we are computing it
yield(i) // yield next value
}
}

fun main() {
simple().forEach { value -> println("time = ${System.currentTimeMillis()} ,$value") }
}

1.3 异步方式返回多个值

1
2
3
4
5
6
7
8
suspend fun simple(): List<Int> {
delay(1000) // pretend we are doing something asynchronous here
return listOf(1, 2, 3)
}

fun main() = runBlocking<Unit> {
simple().forEach { value -> println(value) }
}

1.4 如何通过异步的方式依次返回多个值呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun simple(): Flow<Int> = flow { // flow builder
for (i in 1..3) {
delay(100) // pretend we are doing something useful here
emit(i) // emit next value
}
}

fun main() = runBlocking<Unit> {
// Launch a concurrent coroutine to check if the main thread is blocked
launch {
for (k in 1..3) {
println("I'm not blocked $k")
delay(100)
}
}
// Collect the flow
simple().collect { value -> println(value) }
}

通过flow,我们可以实现异步的数据流。

2、Flow是冷数据流

冷数据流:每次调用collect函数时都会触发执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun simple(): Flow<Int> = flow { 
println("Flow started")
for (i in 1..3) {
delay(100)
emit(i)
}
}

fun main() = runBlocking<Unit> {
println("Calling simple function...")
val flow = simple()
println("Calling collect...")
flow.collect { value -> println(value) }
println("Calling collect again...")
flow.collect { value -> println(value) }
}
3、Flow的取消
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun simple(): Flow<Int> = flow { 
for (i in 1..3) {
delay(100)
println("Emitting $i")
emit(i)
}
}

fun main() = runBlocking<Unit> {
withTimeoutOrNull(250) { // Timeout after 250ms
simple().collect { value -> println(value) }
}
println("Done")
}
4、如何构建Flow?
  • flow{ ... }
  • flowOf
  • .asFlow()
5、Flow操作符

5.1 中间操作符

  • map
  • filter
  • transform
  • take

5.2 终止操作符

  • collect
  • toList / toSet
  • first / single
  • reduce / fold

5.3 buffer当流的处理速度慢于发射速度时,通过buffer提前缓存可以提高效率:

优化前:耗时1200ms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun simple(): Flow<Int> = flow {
for (i in 1..3) {
delay(100) // pretend we are asynchronously waiting 100 ms
emit(i) // emit next value
}
}

fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
simple().collect { value ->
delay(300) // pretend we are processing it for 300 ms
println(value)
}
}
println("Collected in $time ms")
}

优化后:耗时1000ms

1
2
3
4
5
6
7
8
9
val time = measureTimeMillis {
simple()
.buffer() // buffer emissions, don't wait
.collect { value ->
delay(300) // pretend we are processing it for 300 ms
println(value)
}
}
println("Collected in $time ms")

5.4 conflate当生产速度大于消费速度,忽略未来得及处理的值

1
2
3
4
5
6
7
8
9
10
11
12
13
val time = measureTimeMillis {
simple()
.conflate() // conflate emissions, don't process each one
.collect { value ->
delay(300) // pretend we are processing it for 300 ms
println(value)
}
}
println("Collected in $time ms")
//输出:
//1
//3
//Collected in 758 ms

5.5 collectLatest 如果消费者很慢,取消它,新值过来时再重新启动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val time = measureTimeMillis {
simple()
.collectLatest { value -> // cancel & restart on the latest value
println("Collecting $value")
delay(300) // pretend we are processing it for 300 ms
println("Done $value")
}
}
println("Collected in $time ms")
//output:
//Collecting 1
//Collecting 2
//Collecting 3
//Done 3
//Collected in 741 ms

5.6 多个flow合并:zip / combine

使用zip操作符

1
2
3
4
5
6
7
8
val nums = (1..3).asFlow() // numbers 1..3
val strs = flowOf("one", "two", "three") // strings
nums.zip(strs) { a, b -> "$a -> $b" } // compose a single string
.collect { println(it) } // collect and print
//output:
//1 -> one
//2 -> two
//3 -> three

当两个flow不同步时:

1
2
3
4
5
6
7
8
9
10
11
12
//速度不同步时,使用zip,到达同步点才输出
val nums = (1..3).asFlow().onEach { delay(300) } // numbers 1..3 every 300 ms
val strs = flowOf("one", "two", "three").onEach { delay(400) } // strings every 400 ms
val startTime = System.currentTimeMillis() // remember the start time
nums.zip(strs) { a, b -> "$a -> $b" } // compose a single string with "zip"
.collect { value -> // collect and print
println("$value at ${System.currentTimeMillis() - startTime} ms from start")
}
//output:
//1 -> one at 428 ms from start
//2 -> two at 828 ms from start
//3 -> three at 1230 ms from start
1
2
3
4
5
6
7
8
9
10
11
12
13
14
////速度不同步时,使用combine,有值到达即输出,无视同步
val nums = (1..3).asFlow().onEach { delay(300) } // numbers 1..3 every 300 ms
val strs = flowOf("one", "two", "three").onEach { delay(400) } // strings every 400 ms
val startTime = System.currentTimeMillis() // remember the start time
nums.combine(strs) { a, b -> "$a -> $b" } // compose a single string with "combine"
.collect { value -> // collect and print
println("$value at ${System.currentTimeMillis() - startTime} ms from start")
}
//output:
//1 -> one at 452 ms from start
//2 -> one at 651 ms from start
//2 -> two at 854 ms from start
//3 -> two at 952 ms from start
//3 -> three at 1256 ms from start

参考文档:
1.官方文档

1.对称加解密的过程:

(大家用相同的钥匙来进行加解密)
发送端和接收端首先要共享相同的密钥k(即通信前双方都需要知道对应的密钥)才能进行通信。发送端用共享密钥k对明文p进行加密,得到密文c,并将得到的密文发送给接收端,接收端收到密文后,并用其相同的共享密钥k对密文进行解密,得出明文p。

一般加密和解密的算法是公开的,需要保持隐秘的是密钥k
流行的对称加密算法有:DES,Triple-DES,RC2和RC4,AES

缺点:
  • 发送方和接收方首先需要共享相同的密钥,即存在密钥k的分发问题,如何安全的把共享密钥在双方进行分享,这本身也是一个如何安全通信的问题,一种方法是提前双方约定好,不通过具体的通信进行协商,避免被监听和截获。另外一种方式,将是下面我们介绍的通过非对称加密信道进行对称密码的分发和共享,即混合加密系统。
  • 密钥管理的复杂度问题。由于对称加密的密钥是一对一的使用方式,若一方要跟n方通信,则需要维护n对密钥。
优点:
  • 加密和解密的速度要比非对称加密快很多,因此常用非对称加密建立的安全信道进行共享密钥的分享,完成后,具体的加解密则使用对称加密。即混合加密系统。

另外一个点需要重点说明的是,密钥k的长度对解密破解的难度有很重大的影响,k的长度越长,对应的密码空间就越大,遭到暴力破解或者词典破解的难度就更大,就更加安全。

2.数字签名的过程:

  • 发送方A首先对变长的报文提取成一个定长的摘要,一般是md5等
  • A对摘要应用了一个签名函数,并且用A自己的私钥作为参数,因为只有A才知道私钥,所以正确的签名会说明签名者就是其所有者。
  • 一旦计算出签名,节点A就将其附加到报文的末尾,并将报文和签名一起都发送给B
  • 在接收端B,首先会按照同样的算法计算出报文的摘要,然后对签名用A的公钥进行解码,得出解码后的摘要,两个摘要进行比较,则可以判断是否是A发送的且内容没被篡改过。

3.非对称加解密的过程:

加密一方找到接收方的公钥e (如何找到呢?大部分的公钥查找工作实际上都是通过数字证书来实现的),然后用公钥e对明文p进行加密后得到密文c,并将得到的密文发送给接收方,接收方收到密文后,用自己保留的私钥d进行解密,得到明文p,需要注意的是:用公钥加密的密文,只有拥有私钥的一方才能解密,这样就可以解决加密的各方可以统一使用一个公钥即可。

常用的非对称加密算法有:RSA

优点:
  • 不存在密钥分发的问题,解码方可以自己生成密钥对,一个做私钥存起来,另外一个作为公钥进行发布。
  • 解决了密钥管理的复杂度问题,多个加密方都可以使用一个已知的公钥进行加密,但只有拥有私钥的一方才能解密。
缺点:
  • 非对称加密不足的地方是加解密的速度没有对称加密快。

综上,分析了对称加密和非对称加密各自的优缺点后,有没有一种办法是可以利用两者的优点但避开对应的缺点呢?答应是有的,实际上用得最多的是混合加密系统,比如在两个节点间通过便捷的公开密码加密技术建立起安全通信,然后再用安全的通信产生并发送临时的随机对称密钥,通过更快的对称加密技术对剩余的数据进行加密。