本文来自 图灵社区@fairjm
转截请注明出处


这个是书的笔记 书可以在safaribooksonline看,话说这个真的是一个很好的读外文书的网站啊,一个月39刀就可以畅想全站的书,很值。(订订订订订

因为是笔记所以觉得散不是你的错觉...因为本来就是散的...
笔记记录了一些概念 方便复习回顾的时候看 更多内容可以戳上面的链接


Scala的内存模型 多线程能力 和它的线程间同步全部继承自JVM

所有的抽象都有一定程度的泄漏 (all abstractions are to some extent leaky)


Processes and Threads

这通常是OS的任务:指派程序的执行部分给特定的处理器 - 这个机制被称为多任务,并且这对于计算机用户来说是透明的。

时间片(time slices)

一个进程是正被执行的计算机程序的实例。两个进程不能直接读取彼此的内存或者同时使用大部分的资源,使用多进程来表述多任务会非常麻烦 。

在同一个进程中互相独立的运算单元被称为线程。在典型的操作系统中,线程的数量比进程多得多。

每一个线程在程序运行时描述了当前的状态:通过程序栈和程序计数器。

当我们说程序执行了一个动作(比如向内存写入内容) 我们的意思是一个处理器执行了执行这个动作的线程。(注意这边的分句,我自己从onenote上粘贴上来都看了好几遍...)

OS线程是操作系统提供的编程设置 通常通过OS相关的编程接口来暴露出来被使用

在同一个进程中的分隔的线程共享同一区域的内存,通过读写内存来彼此交互。另一个来定义进程的方式是: 一系列的OS线程和这些线程共享的内存和资源。
enter image description here
系统周期性地指派不同的OS线程到CPU核心中来允许计算在所有处理器中进行。

java.lang.Thread Java的线程是直接映射到系统线程的 这意味着Java线程的行为是和OS线程是非常相似的


创建和启动线程

当JVM进程启动时 他默认会创建一些线程。最重要的线程是主线程(main thread) 执行程序的 main方法。这个线程的名字就叫main

Thread的状态:
刚被创建时是 new
当被执行时是 runnable
结束执行是 terminated

启动一个独立的线程包含两步:
1 创建线程对象
2 用start方法执行

object ThreadsCreation extends App {
  class MyThread extends Thread {
    override def run(): Unit = {
      println("New thread running.")
    }
  }
  val t = new MyThread
  t.start()
  t.join()
  println("New thread joined.")
}

join方法是中止main线程的运行直到t线程运行完毕
执行这个方法 将main线程转换到了 waiting 状态
等待的线程放弃了他的执行机会 OS可以将处理器用于其他的线程

注意: 等待的线程提醒OS它们正在等待某个状态并且 停止消耗 CPU周期 而不是重复地检查这个状态

sleep方法将线程放入 timed waiting 状态
OS同样可以将本该运行这个线程的处理器用来运行其他的线程

确定性: 特定的输入 程序总会产生相同的输出 而不管OS选择的执行调度有什么不同 由OS选择的调度不同而会对相同输出产生不同结果的称为 不确定性

大多数的多线程程序是非确定性的 这也是为什么多线程编程如此困难的原因


原子性执行(Atomic execution)

竞态条件(race condition) 是一种现象:并发程序的输出依赖于语句的执行调度。
不一定是不正确的行为。
但如果一些调度的结果是我们不预期的 那么这个竞态条件就可以考虑是一个程序错误

原子性执行:代码块的原子执行意味着代码中的语句不能由执行这段代码的线程和另一个执行这段代码的线程交错执行。
在原子性执行中 要么所要执行的代码都执行了 要么都没执行
可以通过同步块保证原子性:

def getUniqueId() = this.synchronized {
  val freshUid = uidCount + 1
  uidCount = freshUid
  freshUid
}

在错误的对象上同步会造成比较难以找到的并发错误。

每一个JVM中创建的对象都会有一个特殊的实体成为内部锁(intrinsic lock)或者成为监视器(monitor) 用来保证只有一个线程可以执行在这个对象上的同步块

当T线程执行在x对象的同步块时 我们可以说 T线程获得了x的监视器的所有权。当线程执行完这个同步块 我们说他释放了这个监视器。

同步块语句是线程内通信的基本机制。无论何时,当多个线程要访问并修改在同一个对象中的字段时 你应该使用同步块。


重排序(reordering)
同步块也不是没有开销的
对字段进行写入将更加昂贵

同步块的性能损失程度取决于JVM实现 但通常不会很大
你可能会趋向于避免使用同步块当你觉得这里没有什么不好的语句交互执行的情况,比如上面那个例子一样(就是上面的代码没加同步块被多个线程访问)。永远不要这么做!(永远不要高估自己)

object ThreadSharedStateAccessReordering extends App {
  for (i <- 0 until 100000) {
    var a = false
    var b = false
    var x = -1
    var y = -1
    val t1 = thread {
      a = true
      y = if (b) 0 else 1
    }
    val t2 = thread {
      b = true
      x = if (a) 0 else 1
    }
    t1.join()
    t2.join()
    assert(!(x == 1 && y == 1), s"x = $x, y = $y")
  }
}

比如这个例子,我们的预期是0 0,10,0 1。1 1是我们所不预期的。
理论上不管我们运行多少次 永远不会有 x=1 y=1的情况发生(所以assert不会不成立 也就不会抛错)
但实际运行就会..

JVM允许重排序由一个线程执行的特定程序的语句 只要这个重排序不会改变这个线程的序列化执行语义。(the JVM is allowed to reorder certain program statements executed by one thread as long as it does not change the serial semantics of the program for that particular thread)
PS 这里简单说下:

a = true
y = if (b) 0 else 1

y = if (b) 0 else 1
a = true

对于序列化执行来说,这两个并没什么不同(不会产生不同的结果),所以这两个语句是可以重排序(不需要按照指令的执行顺序)

因为一些处理器不总是会按照程序的指令顺序执行
而且 线程也不需要将他们做的改动立马写入主存 而是暂时存在处理器的寄存器中缓存。这样可以最大化程序的运行效率并且允许更好的编译器优化。

以上的错误是我们假定线程中所有的写操作都可以立马被其他线程看到。我们需要应用一些合适的同步来确保一个线程修改被另一个线程看到的可见性。

同步块是其中一种实现这个可见性的同步方式。同步块不仅确保原子性也确保可见性。