[关闭]
@HUST-SuWB 2015-12-21T03:06:06.000000Z 字数 4977 阅读 290

Java的内存模型与线程

读书笔记


Java内存模型

主内存与工作内存

java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(variable)与java编程中所说的变量略有区别,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不存在竞争问题。
java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存的变量,线程间变量的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图:

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了8种操作来完成:

Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

volatile关键字

当一个变量定义为volatile之后,它将具备两种特性:

三个特征

Java内存模型是围绕在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的:

Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。

先行发生关系

一个操作“时间上的先发生”不代表这个操作会是“先行发生”,一个操作“先行发生”也无法推导出这个操作必定是“时间上的先发生”,即:时间先后顺序与线性发生原则之间基本没有太大的关系。
Java中有8个天然的先行发生关系:

线程的实现

实现线程主要有几种方式:

Sun JDK中,在windows和linux中都是使用的一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程之中。
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度和抢占式线程调度。协同式的特点是线程的执行时间由线程本身控制,线程把自己的工作执行完了之后,要主动通知系统切换到另一个线程上。坏处是线程执行时间不可控制。而对于抢占式而言,每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定,这也是Java使用的调用方式。
对于线程优先级,在一些平台上(操作系统线程优先级比Java线程优先级少)不同的优先级实际会变得相同;优先级可能会被系统自行改变。
Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有其中一种状态:
1. 新建状态(New):新创建了一个线程对象。
2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
5. 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

线程在一定条件下,状态会发生变化。线程变化的状态转换图如下:

线程安全的实现

线程安全

线程安全的定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。
按照线程安全的“安全程度”由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

  1. 不可变
    不可变的对象一定是线程安全的。保证对象行为不影响自己状态的途径有很多种,其中最简单的就是把对象中带有状态的变量都声明为final。Java API中符合不可变要求的类型:String,java.lang.Number的部分子类(如Long和Double的数值包装类,BigInteger和BigDecimal等大数据类型但AtomicInteger和AtomicLong则并非不可变的)。
  2. 绝对线程安全
    Java API中标注自己是线程安全的类,大多数都不是绝对线程安全的。
  3. 相对线程安全
    Java语言中,大部分的线程安全都属于这种类型,例如Vector,HashTable,Collections的synchronizedCollection()方法包装的集合等。
  4. 线程兼容
    指通过使用同步手段来保证对象在并发环境中可以安全的使用。Java API中大部分的类都是属于线程兼容的,如ArrayList和HashMap。
  5. 线程对立
    指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。一个线程对立的例子就是Thread类的suspend()和resumn()方法(已被JDK声明废弃了)。常见的线程对立操作还有System.setIn(), System.setOut(), System.runFinalizersOnExit()等。

线程安全的实现方法有互斥同步、非阻塞同步、无同步方案。

  1. 互斥同步
    Java中,最基本的互斥同步手段就是synchronized关键字。还可以使用java.util.concurrent包中的ReentrantLock(重入锁)来实现同步。
  2. 非阻塞同步
    从处理问题的方式上说,互斥同步属于一种悲观的并发策略。随着硬件指令集的发展,我们可以采用基于冲突检查的乐观并发策略,通俗地说,就是先行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现偶读不需要把线程挂起,因此这话总同步操作称为非阻塞同步。
  3. 无同步方案
    如果一个方法本来就不设计共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。这类代码包括:可重入代码和线程本地存储。

锁优化

  1. 自旋锁与自适应自旋
    为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。引入自旋锁的原因是互斥同步对性能最大的影响是阻塞的实现,管钱线程和恢复线程的操作都需要转入内核态中完成,给并发带来很大压力。自旋锁让物理机器有一个以上的处理器的时候,能让两个或以上的线程同时并行执行。
  2. 锁消除
    消除锁是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
  3. 锁粗化
    如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,则可以进行锁粗化的优化。
  4. 轻量级锁
    它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
    但是如果存在锁竞争,除了互斥量的开销,还发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。
  5. 偏向锁
    如果说轻量级锁是在无竞争的情况下使用了CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉了,连CAS操作都不做了。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注