欢迎您光临本小站。希望您在这里可以找到自己想要的信息。。。

深入理解java虚拟机(十一)

java water 1912℃ 0评论

线程安全与锁优化

概述

软件也发展的初期,程序编写都是以算法为核心的,程序员会把数据和过程分别作为独立的部分来考虑,数据代表问题空间中的客体,程序代码则用于处理这些数据,这种思维方式是直接站在计算机的角度去抽象问题和解决问题,称为面向过程的编程思想。与此相对,面向对象的编程思想则站在现世界的角度去抽象和解决问题,它把数据和行为都看做是对象的一部分,这样可以以符合现实世界的思维方式来编写和组织程序。

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

 

这个定义很严谨,它要求线程安全的代码必须都具备一个特征:代码本身分装了所有必要的正确性保障手段(如互斥同步),令调用者无须关心多线的问题,更无须自己实现任何措施来保证多线程的正确调用。这点并不容易做到,在大多数场景中,我们都会将这个定义弱化一些,如果把“调用这个对象的行为”限定为“单次调用”这个定义的其他描述也能够成立,我们就可以称它是线程安全的

 

为了更深入地理解线程安全,可以不把线程安全作一个非真即假的二元排它选项来看待,线程安全的“安全程度”由强至弱来排序:java语言中各种操作共享的数据分为五类:

 

不可变:带来的安全性是最简单最纯粹额,共享数据是一个基本数据类型,只要在定义时使用final关键字就可以保证它是不可变的。如果共享数据是一个对象,那就需要保证对象的行为不会对其状态产生任何影响才行。如String类对象。保证对象行为不影响自己状态的途径有很多,最简单的就是把对象中带有状态的变量声明为final,这样在构造函数结束之后,它就是不可变的。

绝对线程安全:不管运行时环境如何,调用者都不需要任何额外的同步措施。在Java API中标注自己是线程安全的类,大多数都不是绝对的线程安全,如Vector

相对线程安全:需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要额外的同步手段来保证调用的正确性。在Java语言中,大部分的线程安全类都属于这种类型

线程兼容:指对象本身不是线程安全,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中安全的使用,我们平常说一个类不是线程安全的,绝大多数指的都是这种情况

线程对立:不管调用端是否采用了同步,都无法在多线程环境中并发使用的代码

线程安全的实现方法

代码编写如何实现线程安全和虚拟机如何实现同步与锁

互斥同步:是最常见的一种并发正确性保障手段,同步是只在多线程并发访问共享数据时,保证共享数据在同一个时刻只被一条线程使用。互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。互斥是方法,同步是目的

Java里面,最基本的互斥同步手段是synchronized关键字。

synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。monitorentermonitorexit,用计数器实现;同步块在已进入的进程执行完之前,会阻塞后面其他线程的进入

Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮助完成,这就需要从用户态转换的核心态中,因此状态转换需要耗费很多的处理时间。对于代码简单的同步快(如被synchronized修噶的getter()setter()方法),状态转换消耗的时间可能比用户代码执行的时间还要长。所有synchronizedjava语言中一个重量级的操作,有经验的程序员都会在确实必要的情况下才使用这种操作。而虚拟机也会进行一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁切入核心态。

除了synchronized之外,还可以使用java.util.concurrentJUC)包中的重入锁(ReentrantLock)来实现同步,他们都具备一样的线程重入特性,只是代码写法上有点区别,一个表现为API层面的互斥锁(locked()unlock()方法配合try/finally语句块来完成),一个表现为原生语法层面的互斥锁。不过ReentrantLocksynchronized增加了一些高级功能

等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情

可实现公平锁: 等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁,synchronized中的锁是非公平的,ReentrantLock带布尔值的构造函数可以使用公平锁

锁可以绑定多个条件:指一个ReentrantLock对象可以同时绑定多个Condition对象。

JDK1.6发布之后,synchronizedReentrantLock的性能基本完全持平了。虚拟机在未来的性能改善中肯定会更加偏向于原生的synchronized,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步

非阻塞同步:互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,这种同步称为阻塞同步。它属于一个悲观的并发策略,总是认为只要不去做正确的同步措施(加锁),就肯定出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁(这里说的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。。

随着硬件指令集的发展,我们有另外一个选择:基于冲突检测的乐观并发策略,通俗地说就是先进性操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再进行其他的补偿措施(最常见的补偿措施就是不断的重试,直到试成功为止)这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作被称为非阻塞同步

我们需要操作和冲突检测这两个步骤具备原子性,如果这里在使用互斥同步来保证就失去意义,只能靠硬件来完成这件事,硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成,这类指令常用的有

测试并设置(Test-and -Set

获取并增加(Fetch-and_increment

交换(Swap

比较并交换(Compare-and-Swap下文称CAS

加载链接/条件存储(Load-Linked/Store-Conditional, 下文称LL/SC

Jdk1.5之后,Java程序中才可以使用CAS操作,该操作由sun.misc.Unsafe类里面的compareAndSwapInt()compareAndSwapLong()等几个方法包装提供,虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法调用的过程,或者可以认为是无条件内联进去了

由于Unsafe类不是提供给用户程序调用的类(只有启动类加载器(Bootstrap ClassLoader)加载的Class才能访问),如果不采用反射手段,我们只能通过其他的Java API来间接使用它,如JUC包里面的整数原子类,其中的compareAndSet()getAndIncrement()等方法都使用了Unsafe类的CAS操作。CAS有个漏洞ABA问题

无同步方案

要保证线程安全,并不是一定就要进行同步,两种没有因果关系。同步只是保障共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无效任何同步措施去保证正确性,因此会有一些代码天生是线程安全的。介绍其中两类

 

可重入代码

线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,就可以把共享数据的可见范围限制在同一个线程之内。

符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如生成者消费者模式)都会将产品的消费过程尽量在一个线程中完成,其中最重要的一个应用实例就是经典web交互模式中的“一个请求对应一个服务器线程”的处理方式,这种处理方式的广泛应用使得web服务端的很多应用都可以使用线程本地存储来解决线程安全问题

ThreadLocal类来实现线程本地存储的功能。每个线程的Thread对象总都有一个ThreadLocalMapd对象,对歌对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量位置的K-V值对。

锁优化

自旋锁与自适应自旋

共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的线程“稍等一会”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁, 自适应意味着自选的时间不再固定,而是由前一次在同一个锁上的自选时间及锁的拥有者的状态来决定。随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确

锁消除(逃逸分析)

锁粗化

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

要理解轻量级锁,以及后面会讲到的偏向锁的原理和运作过程,必须从HotSpot虚拟机的对象(对象头部分)的内存开始介绍,对象头分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄等,另一部分用于存储指向方法区对象类型数据的指针。如果是数组对象的话,还会有一个额外的部分用于存储数组长度。

对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的信息,它会根据对象的状态复用自己的存储空间

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

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢

偏向锁:目的是消除数据在无竞争的情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了

偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”。意思是这个锁会偏向与第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要在进行同步。

假设当前虚拟机启用来偏向锁(启用参数-XX:+UseBiasedLocking这是JDK1.6的默认值),那么,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为01,即偏向模式,同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步快时,虚拟机都可以不再进行任何同步操作

当有另外一个线程去试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否被处于被锁定的状态,撤销偏向后恢复到未锁定(标志位为01)或轻量级锁定(标志位为00)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行。

偏向锁可以提供带有同步但无竞争的程序性能。它同样是一个带有效益权衡行政的优化。

 

许多老程序员都说过,能攻写出高伸缩性的并发程序是一门艺术,而了解并发在系统底层是如何实现的,则是掌握这门艺术的前提条件,也是称为高级程序员的必备知识之一

转载请注明:学时网 » 深入理解java虚拟机(十一)

喜欢 (0)or分享 (0)

您必须 登录 才能发表评论!