JAVA线程安全与锁优化

文章目录

  • 1.线程
    • 1.1.线程的实现方式
      • 1.1.1.使用内核线程实现
      • 1.1.2.使用用户线程实现
      • 1.1.3.使用用户线程加轻量级进程混合实现
    • 1.2.线程的调度方式
    • 1.3.java中线程的实现方式
  • 2.线程安全
    • 2.1.线程安全的几种类型
    • 2.2.线程安全的实现方式
      • 2.2.1互斥同步
      • 2.2.2.非阻塞同步
      • 2.2.3.无同步方案
  • 3.锁优化
    • 3.1.自旋锁与自适应自旋
    • 3.2.锁消除
    • 3.3.锁粗化
    • 3.4.轻量级锁
    • 3.5.偏向锁
    • 3.6. 偏向锁、轻量级锁、重量级锁关系

1.线程

线程是CPU调度的基本单位,是比进程更轻量级的调度执行单位。线程可以将一个进程的资源分配与执行调度分开,各个线程即可以共享进程资源,又可以独立调度执行。

1.1.线程的实现方式

线程主要有三种实现方式。

1.1.1.使用内核线程实现

        内核线程(Kernel Thread,KLT)是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上,支持多线程的内核叫做多线程内核(Muti-Threads Kernel)。
        轻量级进程(Light Weight Process,LWP)是内核线程的一种高级接口,程序一般也是通过LWP去使用内核进程,每个LWP都有一个内核线程支持,因此只有支持内核喜爱昵称,才能有轻量级进程。
        LWP基于内核线程实现,各种线程操作都需要进行系统调用,而系统调用需要在用户态(User Mode)和内核态(Kernel Mode)之间切换,调用的代价较高。其次每个LWP都依赖一个内核线程,因此需要消耗一定的内核资源。
LWP与内核线程是1:1关系。
JAVA线程安全与锁优化_第1张图片

1.1.2.使用用户线程实现

用户线程(User Thread)是指完全建立在用户控件的线程库上,系统内核不能感知到线程的存在。用户线程的建立、同步、销毁和调度全完在用户态中完成,不需要内核的帮助,也不需要切换到内核态,因此线程的操作快速且消耗低。但是线程的创建、切换和调度及多处理器中线程的映射都需要用户程序自己处理,程序将变的异常复杂。
进程与用户线程是1:N关系。
JAVA线程安全与锁优化_第2张图片

1.1.3.使用用户线程加轻量级进程混合实现

混合实现是既存在用户线程(UT),也存在轻量级进程(LWP),用户线程还是建立在用户空间,线程的创建、切换、解析等都是在用户空间,而轻量级进程则作为用户线程和内核线程之间的交互桥梁,线程的调度及多处理器映射都通过LWP使用内核实现,这样既能在用户态实现线程的大量并发,也能利用内核态的调度功能。
用户线程与LWP是M:N关系。
JAVA线程安全与锁优化_第3张图片

1.2.线程的调度方式

线程的调度方式有两种:

  • 协同式
    协同式调度中,线程的执行时间由线程本身来控制,线程把执行完了之后,要主动通知系统切换到另外的线程上去。优点是实现简单,缺点是线程执行时间不可控,如果一个线程编写有问题,可能导致整个系统的崩溃。
  • 抢占式
    抢占式调用中,每个线程的执行时间由系统来分配,线程的切换由系统来决定,线程的执行时间可控,不会出现一个线程导致整个进程阻塞、崩溃的问题。

1.3.java中线程的实现方式

java中线程模型的映射基于操作系统原生线程模型来实现。因此不同系统上线程模型的映射是不同的。Windows和Linux都是使用一对一的线程模型,因此在Windows和Linux上Java线程模型的映射就是一个java线程映射到一个LWP上。
java中线程调度使用抢占式。
线程的状态:

  • 新建
    新建的尚未启动的线程处于这种状态。
  • 运行
    正在执行的线程(Running),或者等待着CPU分配执行时间(Ready)的线程。
  • 等待
    处于此状态的线程不会被CPU分配执行时间,需要等待其他线程显式的唤醒。
    无参的Object.wait()和Thread.join()和LockSupport.park()方法会让线程进入此状态。
  • 限时等待
    处于此状态的线程不会被CPU分配执行时间,会在等待一定时间后被系统自动唤醒。
    Thread.sleep()和有参的Object.wait()和Thread.join()方法会进入此状态。
  • 阻塞
    等待获取排它锁的线程会处于此状态。线程在进入同步区域的时候进入此状态。
  • 结束
    已终止线程的线程状态,线程已经结束执行。

2.线程安全

如果一个对象可以被多个线程同时使用,并且都能可以获取到正确的结果,那么它就是线程安全的。

2.1.线程安全的几种类型

  • 不可变类型
    被final修饰的变量,经初始化后完全不可变的情况
  • 相对线程安全类型
    指在对单个对象进行操作时是线程安全的,一般不需要进行额外的保障操作。如Vector、HashTable等操作方法都是被synchronized修饰的对象及并发包下的一些集合、对象都是相对线程安全的
  • 非线程安全类型
    指对象本身不是线程安全的,但可以在编写代码时候使用同步手段来保证对象在并发环境中安全的使用。如ArrayList、HashMap等。

2.2.线程安全的实现方式

2.2.1互斥同步

互斥同步是指在多个线程并发访问共享数据时,保证共享数据在同一时刻只能被一条线程使用,其他线程排队等待获取资源。而synchronized、ReentantLock等都是实现互斥同步。

2.2.2.非阻塞同步

互斥同步是阻塞同步,线程阻塞和唤醒都会带来性能问题,它也是一种悲观的并发策略。
非阻塞同步是基于冲突检测的乐观并发策略,就是先进行操作,如果没有其他线程争抢共享数据就操作成功,如果有争抢产生了冲突,就进行补偿措施(一般就是重试,直到成功),这种操作的许多实现都不需要把线程挂起,因此这种操作称为非阻塞同步。
非阻塞同步要操作和冲突检测保证原子性,而这需要硬件的处理器指令来保证,常用指令有:

  • 测试并设置(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()等方法提供包装,而ReentantLock类中设置状态值的改变调用AQS中的compareAndSetState()方法就是基于compareAndSwapInt

2.2.3.无同步方案

无同步方案指无需使用同步措施就能保证正确性。这分为两种情况:

  • 可重入代码
    可重入代码的特征是不依赖存储在堆上的数据和公用的系统资源、用到的变量都有参数传入,不调用非可重入的方法等。
  • 线程本地存储
    即把数据的可见范围限制在同一个线程之内,这样就能避免线程之间的数据争抢问题。
    如果变量要被多个线程访问,就用volatile修饰。如果变量要被一个线程共享,可以设置为本地线程变量(ThreadLocal)。

3.锁优化

锁优化技术是为了在线程之间更高效的共享数据,以解决竞争问题,从而提高程序的执行效率。

3.1.自旋锁与自适应自旋

        互斥同步会出现阻塞的情况,线程的挂机和恢复都需要转入内核态中完成,这些操作会严重影响系统的并发能力。大多数情况下,共享数据的锁定状态都只会持续很短的时间。为了这个时间去挂起和恢复并不划算。如果物理机有多个处理器,可以让两个以上的线程并行执行,我们就可以让后面请求锁的线程暂停一下,但是不放弃处理器的执行时间,看看锁是否会很快释放。为了让线程暂停一下,我们要让线程执行一个忙循环(自旋),这就是自旋锁。自旋期间不会放弃处理器执行时间,因此如果锁被占用的时间很短,自旋的效果会很好,如果锁的时间长,那么只会浪费处理器的资源,造成性能浪费,因此自旋也会有对应的时间限度,如果超过这个时间还是会挂起线程。
        jdk1.6中默认开启自旋锁。默认次数是10次。
        自适应自旋指自旋的时间不固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

  • 如果在同一个锁对象上自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,虚拟机会认为这次自旋也很有可能成功,就会允许它自旋等待持续更长的时间。
  • 如果对于某个所,自旋很少成功获取过锁,那在以后获取这个锁时候可能会省略到自旋的过程,以避免浪费处理器资源。

3.2.锁消除

锁消除是指虚拟机即时编译器在运行时,对 一些代码要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判断依据来自于逃逸分析的数据支持,如果判断一端代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,就可以把它们当做栈上数据对待,被认为是线程私有的,就不需要加锁。

3.3.锁粗化

如果一系列的连续操作都是对同一个对象反复的加锁和解锁,甚至加锁是在循环中,这样即使没有线程竞争,反复的互斥同步也会造成不必要的性能损耗。如StringBuffer中的append(),如果连续出现append(),虚拟机检测到连续的多次对同一个对象加锁,就会将加锁范围扩大(粗化)到整个操作序列的外部,这样只需要加一次锁就可以了,这个操作就是锁粗化。

3.4.轻量级锁

轻量级锁意义是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。轻量级锁是依赖对象头中的“Mark Word”来实现的,“Mark Word”中又2位作为锁标志位,其状态如下

存储内容 标志位 状态
对象哈希码、对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀(重量级锁定)
空,不需要标记信息 11 GC标记
偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向

加锁过程:

  1. 进入同步块时候,如果同步对象没有被锁定(标志位为“01”状态),虚拟机会在当前线程栈帧中创建锁记录(Lock Record)的空间,用于存储 Mark Word的拷贝。
  2. 拷贝对象头中的Mark Word复制到锁记录中。
  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果更新成功,则执行步骤(4),否则执行步骤(5)
  4. 如果更新成功,那么这个线程就拥有了该对象的锁,并且将对象的Mark Word锁标志位改为“00”,即表示此对象进入轻量级锁定状态。
  5. 如果更新失败,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明锁对象被其他线程抢占,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

解锁过程 就是通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word,如果替换成功,那么整个同步过程就完成了,如果替换失败,说明有其他线程获取过锁,就要在释放锁的同时,唤醒其他线程。

3.5.偏向锁

偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。
偏向锁会偏向于第一个获取它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要在进行同步。偏向锁会提高带有同步但无竞争的程序的性能。程序中如果大多数锁都总是被多个不同线程访问,那偏向模式就是多余的。
加锁过程:

  1. 当线程第一次获取锁对象时候,虚拟机会将Mark Word中标志位设为“01”,即偏向模式。同时使用CAS操作把获取到的这个线程的ID记录在对象的Mark Word中
  2. 如果操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。
  3. 当有另外线程去获取锁时,偏向模式会结束。根据锁对象目前是否处于被锁定状态,撤销偏向后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就如轻量级锁一样执行。

3.6. 偏向锁、轻量级锁、重量级锁关系

JAVA线程安全与锁优化_第4张图片

你可能感兴趣的:(深入理解JVM)