深入理解Java虚拟机(高效并发)
如果是使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。在这种实现线程调度的方式下,线程的执行实现是系统可控的,也不会有一个线程导致整个进程阻塞的问题,Java 使用的线程调度方式就是抢占式的。和前面所说的 Windows 3.x 的例子相对,在 Windows 9x/NT 内核中就是使用抢占式来实现多进程的,当一个进程出了问题,我们还可以使用任务管理器把这个进程「杀掉」,而不至于导致系统崩溃。 5.状态转换 Java 语言定义了 5 种线程状态,在任意一个时间点,一个线程只能有且只有其中一种状态,它们分别是:
上述 5 中状态遇到特定事件发生的时候将会互相转换,如下图: 二、线程安全与锁优化 本文的主题是高效并发,但高效的前提是首先要保证并发的正确性和安全性,所以这一小节我们先从如何保证线程并发安全说起。 1.Java 线程安全 那么什么是线程安全呢?可以简单的理解为多线程对同一块内存区域操作时,内存值的变化是可预期的,不会因为多线程对同一块内存区域的操作和访问导致内存中存储的值出现不可控的问题。 Java 语言中的线程安全 如果我们不把线程安全定义成一个非此即彼的概念(要么线程绝对安全,要么线程绝对不安全),那么我们可以根据线程安全的程度由强至弱依次分为如下五档:
线程安全的实现方法 虽然线程安全与否与编码实现有着莫大的关系,但虚拟机提供的同步和锁机制也起到了非常重要的作用。下面我们就来看看虚拟机层面是如何保证线程安全的。 同步互斥 互斥同步是常见的一种并发正确性保障的手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一时间只被一个线程使用。而互斥是实现同步的一种手段。Java 中最基本的互斥同步手段就是 synchronized 关键字,synchronized 关键字在经过编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令,这两个字节码都需要一个 reference 类型的参数来指明要锁定和解锁的对象。如果 Java 程序中的 synchronized 明确指明了对象参数,那就是这个对象的 reference;如果没有,那就根据 synchronized 修饰的是实例方法还是类方法,去取对应的对象实例或 class 对象来作为锁对象。 根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,就把锁的计数器加 1;相应的,在执行monitorexit 指令时将锁计数器减 1,当锁计数器为 0 时,锁就被释放。如果获取锁对象失败,当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。 另外要说明的一点是,同步块在已进入的线程执行完之前,会阻塞后面其它线程的进入。由于 Java 线程是映射到操作系统原生线程之上的,如果要阻塞或者唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到内核态,线程状态转换需要耗费很多的处理器时间。对于简单的同步块(如被 synchronized 修饰的 getter() 和 setter() 方法),状态转换消耗的时间可能比用户代码消耗的时间还要长。所以 synchronized 是 Java 中一个重量级的操作,因此我们只有在必要的情况下才应该使用它。当然虚拟机本身也会做相应的优化,比如在操作系统阻塞线程前加入一段自旋等待过程,避免频繁的用户态到内核态的转换过程。这一点我们在介绍锁优化的时候再细聊。 非阻塞同步 互斥同步最大的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也成为阻塞同步。从处理问题的方式上来说,互斥同步是一种悲观的并发策略,认为只要不去做正确的同步措施(例如加锁),就肯定会出问题,无论共享数据是否会出现竞争,它都要进行加锁(当然虚拟机也会优化掉一些不必要的锁)。随着硬件指令集的发展,我们有了另外一个选择:基于冲突检查的乐观并发策略。通俗的说,就是先进行操作,如果没有其他线程竞争,那操作就成功了;如果共享数据有其它线程竞争,产生了冲突,就采取其它的补救措施,这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。 前面之所以说需要硬件指令集的发展,是因为我们需要操作和冲突检测这两个步骤具备原子性。 (编辑:淮北站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |