jvm学习篇03 - Java与线程

《深入理解Java虚拟机》读后总结。

通用的角度看实现线程三种方式

内核线程实现

内核线程KLT(Kernel-Level Thread) 是由操作系统内核所支持的线程,内核通过 调度器(Thread Scheduler) 对线程进行调度。程序一般不会直接使用内核线程,而是使用内核线程的高级接口——轻量级进程,也就是我们所说的线程。每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程才支持轻量级继承。
这种内核线程和轻量级进程1:1的关系称为一对一的线程模型

内核线程和轻量级线程1:1
优点:由于每一个轻量级进程都有一个内核线程的支持,因此每一个轻量级进程都可以作为一个独立的调度单元,即使某一个轻量级进程阻塞了,也不会影响整个进程继续工作。

局限性:
(1)基于内核实现,因此各种线程操作都需要去进行系统调用,系统调用开销较大,需要频繁的在用户态和内核态间进行转换。
(2)每个轻量级进程需要一个内核线程的支持,因此轻量级进程是有限的。

用户线程实现

狭义上的用户线程(UT) 是指完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现。
优点:用户线程是完全运行在用户态中,不需要内核的帮助,也就无需切换为内核态,因此操作较为低耗且快速的,能够支持较大的线程数量。
缺点:用户线程的优势在于不需要内核线程支援,劣势也在没有内核线程支援,因此用户线程的创建、销毁、切换和调度等都需要用户进行考虑。

这种进程与用户线程1:N的关系称之为 一对多的线程模型

进程与用户线程1:N
(其中操作系统将CPU资源分配给了一个进程,一个 进程中包含多个用户线程UT)

混合实现

混合实现即一起使用轻量级进程和用户线程,由于轻量级进程和用户线程的数量不定,为N:M,因此称之为多对多的线程模型
M:N优点:
(1)用户线程仍然只在用户态下运行,用户线程创建、销毁、切换和调度仍然低耗且快速,而且也支持大规模的线程数并发执行。
(2)使用轻量级进程作为用户线程与内核线程之间的桥梁,因此可以使用内核线程的线程调度功能以及处理器映射,并且用户线程的调度是由轻量级进程来调度的,这样就大大降低了整个进程被阻塞的风险

Java线程的实现

Java线程的实现不受Java虚拟机规范约束。主流的商用Java虚拟机使用的是 一对一的线程模型
HotSpot虚拟机为例,它的每一个Java线程都是直接映射到操作系统的原生线程来实现的,因此HotSpot不需要去干涉线程调度,都是由底层操作系统来处理的。
当然也有例外。

Java线程调度

线程调度就是指系统为线程分配处理机使用权的过程。主要分为协同式调度抢占式调度
协同式调度就是指线程的执行时间由自己控制,线程将自己的工作执行完毕后,才回去通知系统切换到另外一个线程。这种方式实现简单,但是线程执行时间不可控,倘若一个线程坚持不让出处理器资源,可能造成系统奔溃。
抢占式调度是指线程的执行时间由系统来进行分配,因此执行时间是可控的,也就不会导致某个线程的执行导致整个系统不可用的问题,Java使用的就是抢占式调度的方式

Java中定义了6种线程状态:
(1)新建。创建后尚未执行。
(2)运行。处于这个状态可能正在执行,也有可能处于就绪正在等待系统调用。
(3)限期等待。等待一段时间后会被系统自动唤醒。回到就绪。
(4)无限期等待。需要被其他线程显式唤醒,否则无限期等待。
(5)阻塞。等待获取排他锁时触发。
(6)结束。线程执行完毕。

线程状态转换

JAVA线程安全

这里谈论的线程安全以线程之间存在共享变量为前提。
根据线程安全的“安全程度”由强到弱进行排序,可以将Java操作的共享数据分为以下五类:

  • 不可变
  • 绝对线程安全
  • 相对线程安全
  • 线程兼容
  • 线程对立

共享数据分类

  • 不可变 :不可变的对象一定是线程安全的,永远不会看到它在多个线程之中处于不一致的状态,例如String对象就是不可变的。
  • 绝对线程安全:“不论运行环境如何,调用者都不需要进行额外的同步操作”,但是其实在JavaApi中标注自己是线程安全的类,也不是绝对的线程安全,例如线程安全的集合类Vector
  • 相对线程安全:保证对对象单次的操作是线程安全的,调用时不需要额外的同步操作,在JavaApi中标注自己是线程安全的类大多属于这种类型,如VectorHashTableCollectionssynchronizedCollection()方法包装的集合等。
  • 线程兼容:对象本身不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证在并发环境下对象的线程安全,例如JavaApi中的大部分类,如ArrayListHashMap等。
  • 线程对立:无论在调用端是否采用了同步手段,都无法在多线程环境下并发使用代码。例如Thread类的suspend()中断线程和resume()恢复线程。

线程安全的实现

互斥同步
非阻塞同步

锁优化

自旋锁与自适应自旋锁

在Java中线程(轻量级进程)是与操作系统的内核线程一一对应的,当一个线程在获取锁失败时,会被切换到内核态而挂起;当该线程获取到锁后又需要将其切换到内核态而唤醒线程。从用户态到内核态的切换的开销较大,会影响并发性能。
自旋锁 就是当某个线程去获取锁失败时,不马上阻塞自己,在不放弃CPU使用权的情况下多次尝试获取(默认10次,可以通过-XX:PreBlockSpin参数更改),若在几次尝试后其他线程释放了该锁则直接获取,若尝试指定次数后仍获取不到锁才去阻塞自己。

自适应自旋锁 是自旋锁的优化,它的自旋时间不再是固定的,而是由前一次在同一个锁上的自旋时间和锁的拥有者的状态决定

锁消除

对一些代码要求同步,但是对 检测到不可能发生共享数据竞争的锁 进行消除
例如以下代码

public void method(){
	StringBuffer sb = new StringBuffer();
	sb.append("1");
	sb.append("2");
	sb.append("3");
}

StringBuffer的每个方法都被synchronized关键字修饰,锁住的就是sb对象本身,但是虚拟机观察变量sb的作用域被限制在方法method()中,其他线程无法访问sb,即不存在共享数据竞争,因此这里的锁可以被安全的消除。

锁膨胀(锁粗化)

如果虚拟机探测到一系列零碎的操作都对同一个对象进行加锁,就会将加锁操作扩展(粗化)到整个操作序列的外部。

例如上面的代码中,连续的append()操作都对sb进行加锁,这样就可以将锁的范围扩展到第一个append之前到最后一个append之后,避免对同一个对象反复加锁和解锁。

轻量级锁

传统的锁机制被称为 “重量级锁”。

首先需要直到Java对象头中的 Mark Word 部分存储了对象运行时所需的数据,这部分空间会根据对象的状态进行复用。

[Mark Word图片]

轻量级锁工作过程
  • 当代码即将进入同步代码块时,如果当前对象还未被锁定(即锁标志位为“01”状态),虚拟机首先会在当前线程的栈帧中创建一个锁记录(Lock Record),用于存储锁对象当前的Mark Word的拷贝(即Displaced Mark Word),
  • 虚拟机将使用CAS更新操作将锁对象的Mark Word更新为 指向栈帧中锁记录 Lock Record的指针
    • CAS更新操作成功,则表示当前线程获取到该对象的锁,并且将该锁对象的Mark Word的锁标志位设置为"00",表示当前对象处于轻量级锁状态
    • CAS更新操作失败,那么意味着至少存在一个线程与当前线程竞争该对象的锁

偏向锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值