线程安全与锁优化

线程安全

Java中的线程安全

按照线程安全的“安全程度”分为五大类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立

  1. 不可变
    在Java中不可变(Immutable)对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保护措施。如final关键字
  2. 绝对线程安全
    绝对的线程安全是指“不管运行时环境如何,调用者都不需要任何额外的同步策略”,绝对的线程安全是需要付出非常高的代价的,甚至是不切实际的代价。而在Java API中标注是线程安全的类,大多数都不是绝对的线程安全,都是相对的线程安全
  3. 相对线程安全
    相对线程安全就是我们通常意义上所讲的线程安全。它需要保证对象单次的操作是安全的,Vectorget()方法,这个方法是同步的。但是对于一些特定顺序的连续调用,则需要调用端使用额外的同步手段来保证正确性,如使用synchronized关键字
    在Java中,大部分声明线程安全的类都属于这种类型,如VectorHashTableConcurrentHashMapCollectionssynchronizedCollection()方法包装的集合等
  4. 线程兼容
    线程兼容是值对象本身并不是线程安全的,但是通过调用端正确的使用同步手段来保证对象在并发环境中可以安全的使用。平常我们说一个类不是线程安全的,指的就是这种情况。
    Java类库中大部分类都是线程兼容的,如ArrayListHashMap
  5. 线程对立
    线程独立是指不管调用端是否采取了同步措施,都无法在多线程环境中并发使用。
    线程对立这种情况是很少出现的,而且通常都是有害的,应当避免
    Thread类的suspend()resume()方法。

线程安全的实现方法

  1. 互斥同步
    互斥同步是最常见也是最主要的并发保障手段。同步是指共享数据在同一时刻只能背一个线程使用,而互斥是实现同步的一种手段,临界区(Critical Section)互斥量(Mutex)信号量(Semaphore)都是常见的互斥实现方式

    • synchronized关键字:
      在Java中,最基本的互斥同步手段就是synchronized关键字,这是一个块结构的语法。synchronized关键字经过javac编译后,会在同步块前后形成monitorentermonitorexit两个字节码指令。
      在执行monitorenter指令时,首先尝试去获取对象锁,如果对象没有被锁定或当前线程已经持有了该对象的锁,就把锁的计数器增加一,而在执行monitorexit指令时将会将锁计数器的值减一。当锁计数器的值为0时,锁则被释放了。如果获取对象锁失败则会一直阻塞等待,直到获取到锁
      synchronized对于一个线程来说是可重入的,同时synchronized中的锁是非公平的

    • 重入锁(ReentrantLock):
      重入锁(ReentrantLock)是Lock接口最常见的一种实现,与synchronized相似,但是比synchronized相比多了一些高级功能,主要是:等待可中断、可实现公平锁(默认也是非公平锁)、可以绑定多个条件

  2. 非阻塞同步
    互斥同步也被称为阻塞同步,是一种悲观悲观的并发策略,主要问题是进行线程阻塞和唤醒所带来的性能开销。与之对应的则是乐观的并发策略,最常用的是不断的重试,直接操作共享数据,当出现冲突时,不断的重试,直到没有冲突。
    最常见的方法是CAS(Compare-and-Swap),如juc包中的Atomic类则是通过自旋和CAS实现的,同时CAS可能会出现ABA问题,但是大部分情况下ABA问题不会影响程序并发的正确性

  3. 无同步方案
    如果让一个方法不涉及多线程共享数据,自然也不需要去保证线程安全,因为他们天生就是线程安全的,如ThreadLocal这个类,每一个Thread对象都有一个ThreadLocalMap对象,用来存储当前线程的变量

锁优化

JDK 5升级到JDK 6后,进行了大量的锁优化

自旋锁和自适应自旋

  • 自旋锁:当线程尝试获取锁时发现冲突,则让获取锁的线程等一会,但不放弃CPU的执行时间,而让线程等待则让线程执行一个循环(自旋)即可
  • 自适应自旋:自旋所等待的时间必须有一定的限度,如果超过了限定的次数仍然没有获取到锁,则使用传统的方法挂起该线程,自旋次数默认是10次。而自适应自旋则是自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定

锁消除

锁消除是虚拟机即时编译器在运行时,检测到数据不可能被其他线程访问,则会对锁进行消除。锁消除的主要判断依据来源于逃逸分析的数据支持,如果判断一段代码在堆上的所有数据都不会逃逸出去被其他线程访问到,则可以把他们当作栈上的数据对待,认为他们是线程私有的

锁粗化

如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。如果StringBuffer.append()方法,连续操作append方法,锁则会扩展到第一个append方法之前和最后一个append方法之后,只需加一次锁就可以

轻量级锁

轻量级锁是通过CAS来实现的

HotSpot虚拟机的对象头中有一部分用来存储对象自身的运行时数据,如哈希码,GC分代年龄、存储标识位、偏向模式等,官方称它为“Mark Word”。

在代码即将进入同步块之前,如果同步对象没有被锁定,则在当前线程的栈帧中建立一个锁记录(Lock Record)的空间,用来存储锁对象Mark Word的拷贝。
然后虚拟机使用CAS操作尝试把对象的Mark Word更新为Lock Record的指针,如果更新成功,则代表该线程拥有了这个对象的所,如果更新失败了,首先检查是否是当前线程拥有了这个对象的锁,如果是的话直接执行即可,如果不是则说明锁被其他线程抢占了。解锁反之。

如果出现两个线程争用一个锁的情况,那轻量级锁则不再有效,必须膨胀为重量级锁

偏向锁

轻量级锁是在无竞争的情况下使用CAS来消除同步的互斥量,而偏向锁是在无竞争的情况下把整个同步都消除掉,连CAS都不再操作

具体是这样的,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,标识进入偏向模式。同时使用CAS操作把获取这个锁的线程ID记录在了对象头的Mark Word中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁都不需要有任何同步操作。但是如果另外的线程去尝试获取这个锁,偏向模式则马上宣告结束

偏向锁是一种权衡的优化,如果程序中的大多数锁都总是被多个不同的线程访问,那偏向模式反而是多余的。具体情况具体分析,可以使用参数-CC:-UseBiasedLocking来禁止偏向锁优化

分享到: