线程安全与锁优化

1、java的线程安全

        按照“安全程度”由强至弱排序,将 Java 语言中各个操作共享的数据分为以下 5 类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。

2、线程安全的实现方法

2.1 互斥同步

        同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个或一些线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。因此,在互斥和同步里:互斥是因,同步是果;互斥是方法,同步是目的。Java中,最基本的互斥同步手段时synchronized关键字,该关键字经过编译后,会在同步块前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。

  • 如果synchronized指定了参数,那就是这个对象的reference;
  • 如果没有指定,就根据synchronized修饰的是实例方法还是类方法去获取对象实例或Class对象作为锁对象

在执行monitorenter时,首先要尝试获取对象的锁。

  • 如果这个对象没被锁定或者已经获取了该对象的锁,则把锁的计数器加1,相应的,在执行monitorexit时就减1,当计数器为0时,锁就被释放。
  • 如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

        synchronized需要映射到操作系统的原生线程中完成,在用户态转换到核心态时,需要耗费大量时间,所以synchronized是一个重量级(Heavyweight)操作。虚拟机本身会做一些优化,例如在阻塞线程之前加入一段自旋等待过程,避免频繁切入到核心态之中。

        除了synchronized之外,还可以用java.util.concurrent包中的重入锁(ReentrantLock)来实现同步。基本用法类似,都有线程重入特性,只是代码上有点区别,一个是API层面的互斥锁,用lock unlock 配合try finally完成,另一个是原生语法层面的互斥锁。相比synchronized,ReentrantLock增加了一些高级功能,主要有以下3项:

  • 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
  • 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
  • 锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait、notify、notifyAll方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需多次调用newCondition方法即可。

2.2 非阻塞同步

        互斥同步存在线程阻塞和唤醒的性能问题,因此也称为阻塞同步(Blocking Synchronization)。互斥同步属于一种悲观的并发策略,总认为只要不做正确的同步措施(如加锁),就一定会出问题,无论共享数据是否真的会出现竞争。

随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略:先进行操作,如果没有其他线程争用共享数据,那操作成功;如果有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿就是不断重试,直到成功),这种乐观的并发不需要把线程挂起,因此这种同步称为非阻塞同步(Non-Blocking Synchronization)。

        CAS指令需要有3个操作数:内存位置(变量的内存地址V)、旧预期值(A)、新值(B)。CAS执行时,当且仅当V符合A时,用B更新V,否则不进行更新,但无论是否更新V,都会返回V的旧值,上述处理过程是原子操作。JDK1.5之后Java才能使用CAS,由sun.misc.Unsafe类的compareAndSwapInt和compareAndSwapLong等几个方法包装提供,虚拟机在内部对这些方法做了特殊处理,即时编译的结果就是一条平台相关的处理器CAS指令,没有方法调用的过程,或者可以认为是无条件内联进去了。这种被虚拟机特殊处理的方法叫固有函数(Intrinsics),类似的还有Math.sin等。

        尽管CAS看上去很好,但显然无法涵盖互斥同步的所有使用场景,并且CAS从语义上来说并不是完美的,存在一个逻辑漏洞:如果V初次是A,在准备赋值时也是A,就能说明没有被修改过吗?也有可能是先改成了B,又改成了A,那么CAS会误认为其没有被改变,这个漏洞也称为ABA问题。JUC为了解决这个问题,提供了一个带有标记的原子引用类 AtomicStampedReference,它可以通过控制变量值得版本来保证CAS得正确性,不过目前这个类比较鸡肋,大部分ABA问题不会影响并发正确性,如果需要解决ABA,改用传统互斥可能比原子类更加高效。

2.3 无同步方案

        保证线程安全,不一定要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法不涉及共享数据,那它自然就无需任何同步措施。这里有两类是天生线程安全的。

  • 可重入代码(Reentrant Code):也叫纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而执行另一段代码,然后返回时,原来的程序不会出错。这种代码有一些共同的特性,例如:不依赖存储在堆上的数据和公用的系统资源、状态量由参数传入、不调用非可重入代码等。如果一个方法,返回结果是可以预测的,相同的输入可以得到相同的输出,则是线程安全的。
  • 线程本地存储(Thread Local Storage):如果一段代码中锁需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,则可以将共享数据的可见范围限制在同一个线程之内,这样无需同步也能保证线程之间不出现数据争用。大部分消费队列的架构(如生产者-消费者)都会将产品的消费过程进来在一个线程中消费完。最重要的一个应用实例就是经典Web交互模型中的“一个请求对应一个服务器线程”(Thread per Request)的处理方式,这种处理方式的广泛应用使得很多Web服务端应用都可以使用线程本地存储来解决线程安全问题。

        Java语言中,如果一个变量需要被多线程访问,可以使用volatile声明为“易变的”;如果一个变量要被某个线程独享,可以通过java.lang.ThreadLocal来实现线程本地存储的功能。每一个线程的Thread对象都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值得K-V对,ThreadLocal对象就是当前线程得ThreadLocalMap访问入口,每一个ThreadLocal对象都包含了一个独一无二得threadLocalHashCode值,使用这个值就可以在线程K-V对中找回对应的本地线程变量。

2、锁优化

2.1 自旋锁与自适应自旋        

        在许多应用上,共享数据的锁定只持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果能让后面请求锁的那个进程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的对象是否会很快释放。这里的等待,就是让线程执行一个忙循环(自旋),也就是自旋锁。自旋锁在JDK1.4.2中引入,默认是关闭的,在JDK1.6就改为默认开启了。自旋并不能代替阻塞。自旋锁虽然避免了线程切换的开销,但是会占用处理器时间,因此如果等待时间很短,自旋锁效果就很好;如果占用时间很长,则只会白白浪费处理器资源。

        自适应自旋则是由前一次在同一个锁上的自旋时间及锁的拥有者状态来决定。如果在同一个对象上,自旋刚刚成功获得过,则会分配更长的时间用来自旋,反之则减小自旋时间。

2.2 锁消除

        如果一段代码中,不存在共享数据,则会对锁进行消除。

2.3 锁粗化

        在编写代码时,总是将同步块的范围限制的尽量小,如果存在锁竞争,则等待锁的线程也能尽快拿到锁。然而,如果在一系列连续操作中,都对同一个对象反复加锁解锁,也是会导致不必要的性能损耗。这时候只要将锁的范围增大,那么就只会加锁解锁一次。

2.4 轻量级锁

        轻量级锁是在JDK1.6之中加入的新型锁机制,“轻量级”是相对于使用操作系统互斥量实现的传统“重量级”锁而言的。轻量级锁不是用来替代重量级锁的,而是在没有多线程竞争时,减少传统的重量级锁适用操作系统互斥量产生的性能消耗。

        当代码进入同步块时,其加锁过程如下:

  • 如果同步对象没有被锁定(标志位为 01),VM首先将当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word拷贝(官方将该拷贝加了一个Displaced前缀,即 Displaced Mark Word)。
  • 然后VM将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。
    • 如果该更新动作成功,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位转变为 00,表示处于轻量级锁状态。
    • 如果该更新操作失败了,VM首先检查对象的Mark Word是否指向当前线程的栈帧,
      • 如果只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。
      • 否则说明这个锁对象已经被其他线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志变为 10,Mark Wrord存储的是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

        其解锁过程也是通过CAS进行的,如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。

  • 如果替换成功,整个同步过程就完成了。
  • 如果替换失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。

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

2.5 偏向锁

        偏向锁也是JDK1.6中引入的一项锁优化,目的是消除数据无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS去消除同步使用的互斥量,那偏向锁则是在无竞争的情况下把整个同步过程都消除,连CAS都不做。偏向锁的偏就是偏心和偏袒,这个锁会偏向第一个获得它的线程,如果该锁没有被其他线程获取,则持有偏向锁的线程永远不需要进行同步。

        当锁对象第一次被线程获取的时候,VM将会把对象头的标志位设为 01,同时使用CAS把获取到这个锁的线程ID记录在对象的Mark Word中,如果CAS操作成功,持有偏向锁的线程每次进入这个锁相关的同步块,VM都可以不再进行任何操作(例如 Locking、Unlocking、对Mark Word的Update等)。当有另一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定状态,撤销偏向(Revoke Bias)后恢复到未锁定01或轻量级锁00状态,后续的同步操作就如轻量级锁那样执行。

        偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数锁总是被不同线程访问,那偏向模式就是多余的。具体问题具体分析,有时候禁止偏向锁优化反而可以提升性能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值