《Java 并发编程实战》

第一章

一个非线程安全类的例子:

1
2
3
4
5
6
7
class Test {
private var value: Int = 0

fun add(): Int {
return ++value
}
}

这个例子非线程安全类的原因是因为 ++value 是一个非原子性操作,下面是 ++value 的在多线程中的分解步骤:

活跃性问题

安全性目标 - 永远不要发生糟糕的事情
活跃性目标 - 某件正确的事情最终会发生

当某个操作无法继续执行下去时,就会发生活跃性问题

在单线程中,活跃性问题的形式之一就是无意中造成无限循环,从而使得循环之后的代码无法得到执行

在多线程中的活跃性问题,比如如果线程A在等待线程B释放其持有的资源,而线程B永远不会释放该资源。那么线程A就会永远等待下去,无法执行

性能问题

无论如何,线程总会带来某种程度的运行时开销,在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁地出现上下文切换操作。这种操作将带来极大的开销:保存和恢复执行上下文。丢失局部性,并且CPU时间将更多地花在线程调度,而不是线程运行上。
当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存缓冲区中的数据无效,以及增加共享内存总线的同步流量

第二章

线程安全性

同步机制关键字是synchronized,它提供了一种独占的解锁方式,但同步这个术语还包括volatile类型的变量,显示锁以及原子变量。

一个可变状态变量没有使用合适的同步程序就会出现问题。有三种解决方式:

  • 不在线程之间共享该状态变量
  • 将状态变量修改为不可变变量
  • 在访问状态变量时使用同步

并发代码编写的原则:首先代码要正确运行,之后再提高代码的速度,即便如此,最好也只是当性能测试和应用需求告诉你必须提高性能,以及测量结果表明这种优化在实际环境中确实能带来性能提升时,才进行优化

当多个线程访问某个类时,不管运行时环境采用何种调度方式,或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步和协同,这个类都能表现出正确的行为,那么就称这个类是线程安全

在线程安全类的内部已经封装了必要的同步机制,客户端无需进一步采取同步措施

不包含过任何变量,也不包括任何对其他类种变量的引用,这样的对象就叫做无状态对象。无状态对象一定是线程安全的

原子性

++value 操作并非线程安全的,因为这个操作并非原子性的。它并不会作为一个不可分割的操作来执行。它包含了三个独立的操作:

  • 读取value的值
  • 将值加1
  • 将计算后的值写入value
    这是一个读取 - 修改 - 写入的操作序列,并且其结果状态依赖于之前的状态

竞态条件

由于不恰当的执行时序而出现不正确的结果

基于一种可能失效的观察结果来做出判断,或者执行某个操作这种类型的竞态条件称为先“检查后执行”:首先观察到某个条件为真,然后根据这个观察结果执行相应的动作。但事实上,在你观察到这个结果以及开始相应的动作之间,观察结果可能变得无效,从而导致各种问题

1
2
3
4
5
6
7
8
9
10
11
class LazySingleton {

private static LazySingleton instance;
private LazySingleton (){}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}

懒汉式单例类就是就是一个坏例子,在获取实例前会检查这个实例是否存在。如果不存在,则创建这个实例。在创建这个实例之前很有可能其他线程也已经检测到该实例不存在也创建了该实例对象

加锁机制

如果在不变性条件中涉及多个变量,各个变量之间并不是彼此独立存在的。而是某个变量的值会对其他变量的值产生约束。在更新某一变量时,就需要在同一个原子操作中对其他变量同时进行更新

要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量

内置锁

同步代码块包括两部分:

  • 锁的对象引用
  • 由这个锁保护的代码块
    关键字synchronized修饰的方法就是横跨整个方法体的时候同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的synchronized方法,以class对象作为锁

每个java对象都可以用作一个实现同步的锁,这些锁被称为内置锁监视器锁

内置锁相当于一种互斥体或叫做互斥锁。这意味着最多只有一个线程能持有这种锁

重入

某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而由于内置锁是可重入的。因此,如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会变成功

重入的实现方法:为每个锁关联一个计数器和一个所者线程。当计数器为零时,这个锁就被认为没有被任何线程持有。当线程请求一个未被持有的锁匙。记下锁的持有者,并且将计数器置为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
class SynchronizedTest {
@Test
fun test() {
val child:Parent = Child()
child.syaHello()
}


open class Parent {
@Synchronized
open fun syaHello() {
println(this.javaClass.name)
println("parent say hello")
}
}

class Child : Parent() {
@Synchronized
override fun syaHello() {
println(this.javaClass.name)
println("child say hello")
super.syaHello()
}
}
}

输出结果:

1
2
3
4
org.lovedev.concurrent.SynchronizedTest$Child
child say hello
org.lovedev.concurrent.SynchronizedTest$Child
parent say hello

可以看出两个锁对象是同一个,如果没有重入机制,在获取锁的情况下调用 super.syaHello() 就会因为该锁没有被释放无法获取,导致线程一直阻塞

用锁保护状态

被多线程同时访问的可变数据和可变变量都应该有一个锁来保护

对于每个包含多个变量的不变性条件,其中涉及的所有变量都应该由同一个锁来保护

即便是每个方法上都加上synchronized关键字。对于多个同步方法的复合操作,程序还会出现同步问题,这是因为多个同步方法的调用不属于原子操作

活跃性与性能

通常在简单性与性能之间存在着相互制约因素,当实现某个同步策略时,一定不要盲目的为了性能而牺牲简单性。这很可能会破坏安全性

当执行时间较长的计算或者无法快速完成的I/O操作时一定不要持有锁

《Android 开发艺术探索》- 第十章内容总结 《Android 开发艺术探索》- 第九章内容总结

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×