目前线程是java中进行处理器资源调度的最基本单位。(Loom项目正在尝试改变线程这一重量级的实现)

实现线程的方式主要有三种:

  1. 使用内核线程1:1实现;
    内核系统就是由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。

内核线程

  1. 使用用户线程1:n实现;

用户线程

  1. 使用用户线程价轻量级进程混合实现(n:M实现)

混合实现

Java线程的实现

《Java虚拟机规范》中没有限定使用哪种线程模型来实现,主流平台上的主流商用Java虚拟机线程模型普遍都给予操作系统原生线程模型来实现(1:1的线程模型)

Java线程调度

线程调度是指系统为线程分配处理器使用权的过程,调度方式分为两种:

  • 协同式线程调度
    线程的执行时间由线程本身来控制,线程把自己的工作执行完之后,同志系统切换到另外一个线程。
  • 抢占式线程调度
    每个线程由系统来分配执行时间,线程的切换不由线程本身来决定。
    Java可以使用thread.yield()方法主动让出执行时间,但是并不能主动获取执行时间。

线程的优先级

我们并不能在程序中通过优先级来完全准确判断一组状态都为Ready的线程先执行哪一个?

状态转换

Java语言定义了6中线程状态,在任意一个时间点中, 一个线程只能有且只有一种状态,并且可以通过特定的方法在不同状态之间转换。

  • 新建:new,创建后尚未启动的线程处于新建状态;
  • 运行:runnable,包括操作系统线程状态中的Running和Ready,
  • 等待:Waiting,处于这种状态的线程不会被分配处理器执行时间,它们需要被其他线程显示唤醒。
    以下方法可以让线程无限期等待:
    • 没有设置timeout参数的Object.wait()方法;
    • 没有设置timeout参数的Thread.join()方法;
    • LockSupport.part()方法;
  • 限期等待:处于这种状态的线程也不会被分配处理器执行时间,在一定时间后会有系统自动唤醒。
    • Thread.sleep()
    • 设置了Timeout的Object.wait()
    • 设置了Timeout的Thread.join()
    • LockSupport.parkNanos();
    • LockSupport.parkUntil();
  • 阻塞:Blocked,在程序等待进入同步区域的时候,线程将进入阻塞状态。
  • 结束: Terminated,已终止线程的线程状态,线程已经结束执行。

线程状态图

线程安全

当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要执行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的。

也就是代码本身封装了所有必要的正确性保障手段,令调用者无序关心多线程下的调用问题,更无需自己实现任何措施来保证多线程环境下的正确调用。

不可变

在Java语言中,不可变对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障措施。

只要一个不可变的对象被正确的构建出来,那其外部的可见状态永远都不会改变,永远都不会看到他在多个线程之中处于不一致的状态。

绝对线程安全

绝对的线程安全除了对象本身外, 还需要在调用端做相应的同步措施。

相对线程安全

保证这个对象在单次的操作是线程安全的,我们在调用的时候不需要进行额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。在Java中,相对线程安全的类型有Vector、HashTable、Collections.synchronizedCollection();

线程兼容

对象本身并不是线程安全的, 但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全使用。
Vector、HashTable、ArrayList、HashMap等都是线程兼容的。

线程对立

如果两个线程同时持有一个线程对象,一个尝试去中断线程、一个尝试去恢复线程,在并发进行的情况下,无论调用时是否进行了同步,目标线程都存在思索的风险。(Thread类的suspend和resume方法),常见的对立操作还有System.in() System.out()和System.runFinalizersOnExit();

线程安全的实现方法

互斥同步

用互斥来实现同步。
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条线程使用。
互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是常见的互斥实现方式。
因此它面临的问题是进行线程阻塞和唤醒所带来的性能开销,因此也叫阻塞同步。
属于一种悲观的并发策略。

synchronized(非公平锁)
java.util.concurrent.locks.Lock
ReentrantLock(非公平锁)

可重入锁比synchronized增加了一些高级功能:

  • 等待可中断
    当持有锁的线程长期不释放所得时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
  • 公平锁
    多个线程在等待同一个锁的时候,必须按照申请锁的时间顺序依次获得锁。
  • 锁绑定多个条件
    一个ReentrantLock对象可以同时绑定多个Condition对象, 多次调用newCondition()即可。

非阻塞同步

基于硬件冲突检测的乐观并发策略,也就是说先进性操作,如果没有其他线程争用共享数据,操作成功;
如果共享数据被争用,产生冲突,那么再进行其他的补偿措施,最常用的补偿措施就是不断重试,直到成功。
这种方式不需要阻塞线程,因此也叫无锁编程。
常用的具有原子性的硬件操作和冲突检测指令有:

  • 测试并设置(Test-And-Set)
  • 获取并增加(Fetch-And-Increment)
  • 交换(Swap)
  • 比较并交换(Compare And Swap)
  • 加载链接/条件存储(Load-Linked/ Store-Conditional)

Java中暴露出来的是CAS指令,当CAS指令执行时,当且仅当内存地址V的值符合预期值A时,处理器才会用新的值B更新内存地址V的值,该过程是一个原子操作,执行期间不会被其他线程中断。

CAS存在的ABA问题,大部分情况下不会影响程序并发的正确性,如果需要解决该问题,传统的互斥同步可能会比原子类更为高效。

无同步方案

如果可以让一个方法本来就不涉及共享数据,那它自然就不需要任何同步措施去保证其正确性,因此会有一些代码天生就是线程安全的。

  • 可重入代码
    如果一个方法的返回结果是可预测的,只要输入了相同的数据,就就能返回相同的结果,那么它就满足可重入性的要求,当然他就是线程安全的。
    可重入代码不依赖于全局变量、存储在队上的数据和公用的系统资源,用到的状态量都由参数重传入,比调用非可重入的方法等。
  • 线程本地存储
    如果一段代码中使用的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。这样就可以把共享数据的可见范围限制在一个线程之内。
    常见的有生产者消费者模式(Web交互模型)。
    ThreadLocal的使用。

锁优化

自旋锁和自适应自旋

互斥同步的时候需要对线程进行挂起和恢复,但是挂起和恢复需要陷入内核态来完成。
因此当一个线程获取锁的时候,如果暂时没有获取到,那么让他稍微等一会儿,但不放弃处理器的执行时间,看看持有锁的线程是否很快会释放锁。

如果锁被占用很长的时间,那么自旋的线程只会白白消耗处理器资源。因此自旋等待时间必须有一定的限度。
如果自旋超过了限定的次数,仍然没有成功获得锁,就应当使用传统方式去挂起线程。

自旋锁的实现

几种自旋锁的java实现
认真的讲一讲:自旋锁到底是什么
看完你就明白的锁系列之自旋锁

锁消除

虚拟机即使编译器在运行时,判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把他们当作栈上的数据对待,认为他们是线程私有的,消除同步锁的过程。

锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的, 可以把加锁同步的范围扩展到整个操作序列的外部。

轻量级锁

在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

Hotspot对象头分为两部分:
1)用于存储对象自身运行的数据,如哈希吗,GC分代年龄。这部分数据的长度在32位和64位的Java虚拟机中分别占用32个或者64比特。这部分是实现轻量级锁和偏向锁的关键。
2)用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分存储数组长度。

由于对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到Java虚拟机的空间使用效率,MarkWord被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。
它会根据对象的状态服用自己的存储空间。
在32位的Hotspot虚拟机中,对象未被锁定的状态下,MarkWord的32个比特空间有25个比特用于存储对象的哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,还有1个比特股定位0(表示未进入偏向模式)。对象除了违背锁定的正常状态外,还有轻量级锁定、重量级锁定、GC标记、可偏向等几种状态。

如果代码即将进入同步块的时候,如果此同步对象没有被锁定(标志位位01),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(LockRecord)的空间,用于存储锁对象目前的MarkWord的拷贝。

然后虚拟机将使用CAS操作尝试把对象的markword更新为指向LockRecord的指针。如果这个更新成功,那么代表该线程拥有了这个对象的锁,并且对像MarkWord的锁标志位将转变为00,表示此对象处于轻量级锁定状态。

如果更新操作失败,意味着至少存在一个线程与当前线程竞争获取该对象的锁。虚拟机首先检查对象的MarkWord是否只想当前线程的栈帧,如果是,说明线程已经拥有了这个对象的锁,那么直接进入同步块继续执行,否则说明这个对象已经被其他线程抢占了。
如果出现两条以上的线程争用同一个锁的情况,那么轻量级锁不再有效,必须要膨胀为重量级锁,锁标志的状态变为10, 此时Markword中存储的就是指向重量级锁的指针,后面等待锁的线程也必须进入阻塞状态。

它的解锁过程也是通过CAS来进行的,如果对象的MarkWord仍然指向线程的锁记录,那就用CAS操作把对象当前的MarkWord和线程中复制的DisplacedMarkWord替换回来,如果替换成功,说明解锁成功。
替换失败,说明有其他线程尝试过获取该锁,需要在释放锁的同时唤醒被挂起的线程。

如果没有竞争,轻量级锁能通过CAS操作成功避免使用互斥量的开销;
如果存在竞争,除了互斥量本身开销外,还额外发生了CAS操作的开销,因此性能更差。

偏向锁

用于消除数据在无竞争情况下的同步,连CAS都去掉了,进一步提高程序的运行性能。
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一致没有被其他的线程获取,则持有偏向锁的线程永远不需要再同步。

如果当前虚拟机启用了偏向锁,那么当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为01,把偏向模式设置为1,表示进入偏向模式。
同时使用CAS操作把获取到这个锁的线程ID记录在对象的MarkWord中。
如果CAS成功,持有偏向锁的线程以后每次进入这个锁相关的同步块是,虚拟机都可以不再进行任何同步操作。

一旦出现另外一个线程尝试获取这个锁的情况,偏向模式马上宣告结束。
根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(把偏向模式设置为0),撤销后标志位恢复到为锁定或者轻量级锁定的轧辊台,后续的同步操作按照轻量级锁执行。

偏向锁

当进入偏向状态是,MarkWord大部分的空间都用于存储持有锁的线程ID,这部分空间占用了原有存储对象Hash码的位置。当一个对象已经计算过一次一致性哈希码后,它就再也无法进入偏向锁状态。
当一个对象正在处于偏向锁状态时,受到计算hash码的请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。

在重量级锁的实现中,对象头指向了重量级锁的位置,代表重量级锁的ObjectMonitor类中有字段可以记录非加锁状态下的MarkWork,其中自然可以存储原来的哈希码。

偏向锁可以提高带有同步但无竞争的程序性能,但如果程序中大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。