Java高效并发

本博文对应《深入理解java虚拟机》第五部分,第十二章和第十三章。

物理机内存模型

我们先来看看物理机是怎么做的,这样可以更好的了解java是怎么做的。

在多处理器中,是通过处理器缓存来解决内存速度过慢的问题的,即先从内存到缓存,然后CPU再从缓存中读取(这里为了说明问题,忽略了多级缓存等中间真实存在的东西而将它们合称为缓存),也就是多个处理器它不是从内存中直接读取数据的,这样势必造成处理器A的缓存刚从内存中读取了数据,处理器B就更新了缓存,也就是常说的缓存一致性问题。当然为了避免这种情况的发生,人们发明了一些协议来确保,这些用在“物理机”上的协议不是博客关注的要点。

java内存模型

java内存模型规定了,所有的变量都存放在主内存中(具体说,其实是jvm使用的内存),而每一条线程都有自己的工作内存,工作内存中存放的是主内存的副本,线程无法直接操作主内存,只能操作自己的工作内存。

java规定了,下面的这8种操作,必须是原子性的:

  1. lock:作用在主内存,把一个变量标识为一个线程独占的状态。
  2. unlock:作用在主内存,把一个变量解放出来。
  3. read:作用在主内存,把变量的值从主内存传输到工作内存中。
  4. load:作用在工作内存,把read操作的得到的变量值放入到工作内存的副本中。
  5. use:作用在工作内存,把工作内存的值传递给JVM执行引擎。当JVM的字节码指令需要使用变量的值的字节码的使用执行这个操作。
  6. assign:作用在工作内存,把执行引擎的值赋给工作内存的变量。当JVM的字节码指令需要给变量赋值的时候执行这个操作。
  7. store:作用在工作内存,把工作内存中的变量的值传送到主内存。
  8. write:把store操作的得到的变量的值写入到主内存的变量中。

同时执行上面的8种操作还必须有下面的条件:

  • 不允许read和load,store和write单独出现,即从主内存读取了,那么工作内存的副本必须写入;在工作内存修改了之后必须写回到主内存中。
  • 线程不允许丢弃assign操作,即工作内存发生了变化必须把变化同步回主内存。
  • 如果没有assign,则不允许把数据从工作内存同步回主内存。
  • 工作内存中不能诞生新的变量
  • lock只有一条线程可以执行,但是同一个线程是可以执行多次的。
  • 如果对变量进行了lock,则会清空工作内存中这个变量的值。
  • 如果没有lock,就不允许unlock;不允许unlock其它线程锁定的变量。
  • unlock之前必须先把变量同步回主内存中。

volatile

使用了volatile修饰的变量,具备以下两种特性:1. 所有线程可见,只要该变量一发生变化,所有线程都可以知道(实际上是每次使用volatile修饰变量的时候,都会去获取最新的,这样保证了线程使用的时候用到的一定是最新的)2. 不会发生指令重排。

那由于变量只要一发生变化所有的线程都能知道,那基于volatile修饰的变量的运算是不是就是线程安全的呢?当然不是。

拿最简单的自增操作为例,它有4条字节码,更多条的机器码组成,所以就算变量的修改所有线程都能获取到,但是由于获取到值和进行自增操作这俩并不是原子性的,所以还是线程不安全。总结一下,必须满足以下条件,才可以使用volatile来达成线程安全:

  1. 运算结果并不依赖变量当前值(自增操作就违背了这一条),或者确保只有单一线程来修改变量的值。
  2. 变量不需要与其他变量共同参与不变约束。

这里还有一个误区:一般人都认为volatile性能比synchronized关键字好上不少,其实大部分情况还是对的,但是它因为在字节码中加入了很多内存屏障的指令,而synchronized关键字jvm也做了不少优化,所以其实孰优孰劣还难说。选择synchronized还是volatile只有一个原则:volatile能否满足使用要求,满足就用,否则就不用。

原子性

基本数据类型的访问、读写都是原子性的(long和double虽然说不是,但是几乎不可能发生非原子的情况,故忽略),java没有把上面八种操作中的lock和unlock给我们开放,但是提供了monitorenter和monitorexit字节码,而这两个字节码就是synchronized关键字,所以synchronized具备原子性。

可见性

如果在构造器中初始化了final变量,那么由于其不可变性,导致了它们一定是可见的(因为压根就没办法修改了)。

而synchronized则是由于“在对一个变量unlock之前,必须把这个变量(我个人认为是synchronized代码块里面的所有变量,参考http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html中的)

But there is more to synchronization than mutual exclusion. Synchronization ensures that memory writes by a thread before or during a synchronized block are made visible in a predictable manner to other threads which synchronize on the same monitor. After we exit a synchronized block, we release the monitor, which has the effect of flushing the cache to main memory, so that writes made by this thread can be visible to other threads. Before we can enter a synchronized block, we acquire the monitor, which has the effect of invalidating the local processor cache so that variables will be reloaded from main memory. We will then be able to see all of the writes made visible by the previous release.

注:上面的variables用的是复数。

同步回主内存中”这条规则满足了它的可见性。

有序性

首先确定,由于有指令重排,所以在本线程观察,那么所有的操作都是有序的;如果观察别的线程,那么都是无序的。

volatile因为禁止了指令重排,当然可以保证有序性。而synchronized则是因为“一个变量只有一条线程可以对其进行lock”,所以相当于只有一个线程,那么自然也是有序的。

先行发生原则

大名鼎鼎的Happens-Before原则。操作A先行发生于操作B,是说在操作B之前,操作A产生的影响就已经能被观察到。

下面是一些天然的先行发生原则:(有的面试会问到)

  • 在同一个线程内,按照控制流的顺序,写在前面的先执行。
  • 一个unlock操作先行于同一个锁的lock操作。
  • 对于同一个volatile变量的写操作先行于读操作。
  • 线程的start方法先行于此线程的每一个动作。
  • 线程的每一个动作先行于线程的终止方法。
  • 对线程的interrupt()方法的调用先行于被中断线程的代码检测到中断事情的发生。
  • 一个对象的构造函数的执行结束先行于它的finalize()方法开始
  • 先行可以传递。

只要观察有没有满足上面的条件,就可以知道是不是线程安全的。比如对age(有初始值)的setter和getter方法分别由线程A和线程B来执行,那么线程B获得的值,是什么?无法确定,因为线程不安全。那么如何避免线程不安全呢?我们只要满足上面的任意一条即可。

java中的线程

并发完全可以不依赖多线程,但是在java中,目前的并发基本全是线程来完成的。

操作系统也提供了线程,那么java的线程和操作系统中的线程有什么区别呢?

  1. 使用内核线程来实现。内核线程直接由操作系统内核来管理,一般来说程序不会直接使用操作系统内核的线程,而是用了一个内核线程的接口——轻量级进程(即所谓的一个程序具有多进程,本质上程序中所谓的进程也是被1:1映射到了操作系统内核的线程上),就是通常意义的进程,它们之间是一一对应的关系。缺点就是线程的各种操作都需要系统调用,而系统调用则需要内核态和用户态之间来回切换(举例:现场的保存和恢复就非常浪费资源)。
  2. 使用用户线程实现。没有系统内核的支持,就不需要在内核态和用户态之间切换,所以非常快速且消耗低,但是没有了操作系统的帮助,线程的创建、销毁、调度就需要用户自己来实现,有些问题甚至是不可能实现的,这就导致了一些问题的产生。
  3. 混合实现。就是一条内核线程对应了一个轻量级进程(也就是线程的别名)。用户线程可以通过轻量级进程来使用内核的一些功能。

那么java用了哪一个呢?具体看虚拟机,目前主流的使用是第一种,即1:1映射到操作系统内核线程上去。

状态

java中的线程有以下六种状态:

  • New:创建未启动。
  • Runnable:虽然是Runnable,但是其实正在执行的线程也算作这一类,只要分到了CPU时间,就可以执行。
  • Waiting:等待被其它线程唤醒,否则无限期等待。
  • Timed Waiting:等待一段时间之后自己醒来。
  • Block:等待别的线程释放锁。
  • Terminate:结束执行的线程

线程安全与锁优化

什么是线程安全?我个人的理解是:当多个线程来访问的时候,我不需要额外的代码(我自己不用写synchronized或者是lock等)就可以确保数据是正确的,那就是线程安全。

事实上,我们可以根据线程安全从强到弱进行排序:

  1. 不可变。都不可变了,那所有人看到的一定是正确的,它是最强的线程安全。
  2. 绝对线程安全。这意味着不管运行环境如何,都需要线程安全。实际中根本不会这么做,因为开销太大了。
  3. 相对线程安全。对这个对象的单次操作是线程安全的。比如Vector就是。
  4. 线程兼容。需要自己来保证线程安全。ArrayList就是这种。
  5. 线程对立。尽量避免。

实现方法

  1. 互斥同步。最基本的手段就是synchronized关键字,通过monitorenter和monitorexit指令实现。synchronized本身是重量级的锁,因为它会阻塞别的线程。另外一个就JUC包,性能略微好于synchronized,不过synchronized进行了优化,导致最后两者平分秋色。
  2. 非阻塞同步。乐观锁就是这个的体现。
  3. 无同步方案。有一些代码天生就是线程安全的。这里略。

锁优化

这里可参考synchronized实现原理