本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、介绍
JavaTM 虚拟机支持多线程执行。线程是用 Thread 类来表示的。用户创建一个线程的唯一方式是创建一个该类的对象;每个线程都与这样一个对象相关联。在对应的Thread 对象上调用 start()方法将启动线程。
线程的行为,尤其是未正确同步时的行为,可能会让人困惑和违背直觉。本规范描述了用 JavaTM语言编写的多线程程序的语义;包括多线程更新共享内存时,读操作能看到什么值的规则。因为本规范与不同的硬件架构的内存模型相似,所以,这里的语义都指的是 JavaTM 内存模型。
这些语义不会去描述多线程程序该如何执行。而是描述多线程程序允许表现出的行为。任何执行策略,只要产生的是允许的行为,那它就是一个可接受的执行策略。
1、锁
线程间通信可能有多种机制,其中最基础的就是同步(synchronization),同步是通过监视器(monitors)来实现的。每个对象都关联着一个Monitor,一个线程可以对它进行加锁和解锁操作。一个Monitor同一时间只允许一个线程持有锁,其他尝试加锁的线程会被阻塞,直到锁被释放,其他线程才可以获取锁。
一个线程可以对一个Monitor进行多次加锁和解锁操作,但是每次解锁操作都是对加锁操作的撤销。
Synchronized声明一个对象的引用,然后会尝试对该对象的Monitor进行加锁操作,在成功加锁之前,不会进行任何处理。当然加锁成功后,Synchronized声明的部分才会被执行。如果对象执行结束,无论是正常结束还是异常结束,都会自动的对当前的Monitor进行解锁操作。
一个Synchronized方法被调用时,会自动的执行一次加锁操作,只有加锁成功后,方法体才会被执行。如果该方法是一个实例方法,那么会锁定调用该方法的实体(也就是说,在方法体执行过程中,这个实体被称为this)。如果该方法是静态的(static)方法,那么会锁定定义该方法的类的类对象(class)。如果方法体执行结束,无论是正常结束还是异常结束,都会自动的对当前的Monitor进行解锁操作。
语义既不会阻止也不会要求对死锁进行检测。程序中的线程直接或间接的持有多个对象的锁时,必要时通过技术手段来避免死锁,创建更高级的锁原语来防止死锁。
其他机制,如读写volatile变量、使用java.util.concurrent包中的类,都为同步提供了可选的机制。
2、未正确同步的程序会有出人意料的情况
java语义允许编译器和微处理器做优化处理,这可能会对未正确同步的代码相互影响,会产生一些自相矛盾的情况(指令重排引起的程序执行结果异常)。
如下图:
考虑这样一种情况,在线程 1 第一次读取 r1.x 与读取 r3.x 之间,线程 2 对 r6.x进行了赋值。如果编译器决定在 r5 处重用 r2 的值,那么 r2 和 r5 的值都是 0,r4 的值是 3.从编程人员的角度来看,p.x 的值从 0 变为 3 后又变回了 0.。
虽然这不是理想中的情况,但是这是大部分JVM实现所允许的。然而,在JLS和JVMS的源生java内存模型中是不被允许的。这是原始java内存模型需要被修订的迹象之一。
3、非正式语义
程序必须被正确的同步来避免当代码被重排的时候,会产生各种可见的异常的表现。使用正确的同步也不能确保程序的总体行为是正确的。然而,正确的同步可以让程序员以一种简单的方式来解释程序可能发生的行为。正确同步代码的行为并不会特别依赖于可能的指令重排。如果不能正确同步代码,奇怪、困惑、不可预见的行为很可能发生。
了解一段代码是否被正确同步,有两个关键点:
- 冲突访问(Conflict Accesses):对相同的共享字段或者数组进行两个访问(读或写),且至少有一个为写时,就会认为是冲突访问。
- Happen-before关系:两个动作被happen-before关系排序。如果一个动作happen-before另一个动作,则第一个动作对第二个动作可见,且第一个排在第二个之前。happen-before关系主要用来强调两个有冲突的动作之间的顺序,以及定义数据争用的发生时机。
3.1 顺序一致性
顺序一致性是程序执行过程中可见性和顺序性的强有力保证。
3.2 final字段
声明为final的字段初始化一次后,正常情况下它的值不会再改变。
final字段详细语义与普通字段不同。final的读操作可以在同步屏障之外,然后调用任意的方法;同样,也允许编译器将final字段的值保存到寄存器,并且在读取时不需要重新加载。
final字段也允许编程人员在不需要同步的情况下实现线程安全的不可变对象。不可变对象对所有的线程都不可变,即使存在数据争用,也是线程安全的。
final字段必须初始化后才能保证不可变。final字段要在对象的构造器中就进行初始化,当final字段所在对象被其他线程看到时,final字段就已经被初始化,其他线程总能看到线程安全的final字段的正确值。
4、 内存模型
内存模型描述了程序执行轨迹是否是该程序的一次合法执行。对于java而言,内存模型检查执行轨迹中每次读操作,然后根据特定规则,检验该读操作观察到的写是否合法。
内存模型描述了程序的可能行为。内存模型的一个高级、非正式的概述显示其是一组规则,规定了一个线程的写操作何时会对另一个线程可见.
5、 概念定义
共享变量/堆内存(Shared variables/Heap memory):能够在线程间共享的内存称为共享内存或堆内存。所有的实例字段,静态字段以及数组元素都存储在堆内存,称为共享变量。 方法中的局部变量永远不会在线程间共享,且不会被内存模型影响。
线程间的动作(Inter-thread Actions):线程间的动作是由某一线程执行,能被另一线程探测或直接影响的动作。如lock或unlock某个线程,读取volatile变量,启动一个线程等。线程间动作包括共享变量的读写以及同步动作、与外部世界交互的动作、导致某个线程进入无限循环的动作。
我们不需要关心线程内的动作,每个单线程都需要遵守正确的线程内语义。
每个线程间的动作都与该动作的发生所在的线程、线程中动作发生的程序顺序相关联。 与一个动作相关的其它信息包括:
- write 要写的变量以及要写的值。
- read 要读的变量以及可见的写入值(由此,我们可以确定可见的值)。
- lock 要锁定的管程。
- unlock 要解锁的管程。
程序顺序(Program Order):某个线程执行的线程间动作中,线程的程序顺序是一个全序,反映出的是这些动作的执行顺序。
线程内语义(Intra-thread semantics):单线程程序的标准语义。基于线程内读动作能看到的值,可以完整预测线程的行为。线程内语义决定着线程孤立的执行过程;当从堆中读取值时,值由内存模型决定。
同步动作(Sychronization Actions):同步动作包括:加锁,解锁,读写volatile变量,启动线程的动作,以及探测线程是否结束的动作。只要是sychronizes-with边缘的起始或结束点,都是同步动作。
同步顺序(Sychronization Orders):同步顺序是一次执行过程中所有同步动作上的全序关系。每个执行过程都有同步顺序。
指令重排的产生:两个动作之间存在 happens-before 关系并不一定意味着在实现中它们必须以这种顺序发生。如果重排序产生的结果与合法执行的结果一致,那么,重排序就不是非法的。例如,某个线程中写默认值到一个对象的每个字段,不需要发生在该线程开始之前,只要没有被读操作察觉到即可。