线程安全与锁优化

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

可以将java语言中的各种操作共享数据分为5类

  1. 不可变:Immutable对象、String或被finnal修饰的基本数据类型。对引用类型,只要其地址不变,且其不会暴露出影响其状态的方法,也是不变的。例如String,其本身内部使用final修饰,且其所有的方法都不会改变该值,而是新建一个String对象,所以String是不可变的
  2. 绝对线程安全:条件相当严苛,即在任何情况下都线程安全
  3. 相对线程安全:对象单独的操作是线程安全的。例如 Vector(所有的方法都被同步修饰)、HashTable
  4. 线程兼容:对象本身不是线程安全的,但是通过使用同步手段可以达到该目的,例如使用锁或者同步机制。如ArrayList和HashMap
  5. 线程对立:无论是否使用同步手段,都无法保证线程安全。Thread类的suspend()和resume()方法,如果一个线程执行了suspend()但逻辑上又需要执行resume()则就发生了死锁

线程安全的实现方法

互斥同步(阻塞同步)

同步是指在多线程并发访问共享数据时,保证同一时刻只能被一个线程使用

  • 互斥是实现同步的一种手段
    实现方式
  • 在java中,最基本的同步手段就是使用 synchronized 关键字。其经过编译后,会在同步块的前后分别形成 monitorentermonitorexit 这两个字节码指令,这两个字节码都需要一个 reference 类型的参数来指明要锁定和解锁的对象。
  • 还可以使用JUC下的重入锁(ReentrantLock)实现同步,基本用法上其与synchronized很相似,他们都具备一样的线程重入特征(一个线程锁定一个资源后,自己还可以再次进入该锁定区域,不至于将自己锁死)
    相较于 synchronized,其还有三个优势:

    1. 等待可中断:当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待,改为处理其他事情
    2. 公平锁:指多个线程在等待同一个锁时,必须按照先来后到的顺序获取锁
    3. 锁绑定多个条件:一个 ReentrantLock对象可以同时绑定多个 Condition 对象,每个Condition对象又可以理解为一把锁
非阻塞同步

互斥同步最主要的问题是进行线程阻塞和唤醒所带来的性能问题。互斥同步署于一种悲观的并发策略。而非阻塞同步则是:先进行操作,如果没有其他线程争用共享数据,那操作就是成功了,如果有冲突,则再采取其他补偿措施(最藏剑的就是不断重试,直到成功,即CAS)。因为其不需要将线程挂起,所以是非阻塞的。

无同步方案

要保证线程安全,并不一定要使用同步。同步只是保证共享数据争用时的正确性手段。如果一个方法本来就不涉及共享数据,那他天然就是线程安全的。

锁优化

自旋锁与自适应自旋

互斥同步对性能影响最大的时阻塞的实现(线程的阻塞和唤醒需要用户态和内核态的切换),且由于共享数据的锁定状态只会维持很短一段时间,所以可以让等待的线程不放弃处理器执行时间而执行一个忙循环(自旋)
因为不会放弃cpu执行时间,所以对于长时间的等待自然是一种浪费,如果自旋超过了一定次数仍没有获得锁,就是使用传统的方式挂起线程了。自旋次数默认为10次,可以通过 -XX:PreBlockSpin 参数修改
JDK1.6引入了自适应自旋锁,如果之前能很快自旋成功,那么下次自旋将会允许更长的时间,反之则会更快升级为挂起

锁消除

指虚拟机即使编译器在运行时,对一些要求同步的代码进行分析,检测到其不可能存在共享数据竞争的关系,则会对锁进行消除。
锁消除的主要判定依据来源于 逃逸分析 的数据支持。

锁粗化

如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,则虚拟机会将锁扩展到整个操作序列以外,内部的锁就会被消除

轻量级锁

轻量级锁不能代替重量级锁,其本意时在没有多线程的竞争前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
HotSpot虚拟机的对象头分为两个部分。第一部分(Mark Word)用于存储对象自身的运行数据,如hashCode、GC分代年龄等,它是实现轻量级锁和偏向锁的关键。另一部分用于存储指向方法区对象类型的指针数据,如果是数组对象的化,还会有一个额外的部分用于存储数组长度
在Mark Word中,使用2bit记录锁状态
file

  • 加锁过程
    1. 代码进入同步块时,如果此同步对象没有被锁定(锁标志为“01”状态),虚拟机将首先在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Mark Word的拷贝(即Displaced Mark Word)
    2. 虚拟机将使用CAS尝试将该对象的MarkWord更新为指向栈中锁记录的指针。如果该操作成功了,那么这个线程就拥有了该对象的锁,并且对象的MarkWord锁标志位转变位“00”,表示此时对象处于轻量级锁定状态。如果该操作失败了,虚拟机首先坚持对象的markword是否指向当前线程栈帧,如果是,则说明当前线程已经拥有了这个对象的锁,就可以直接进入同步代码块。否则说明这个锁队形已经被其他线程抢占了。
    3. 如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀位重量级锁,锁标志状态值变为“10”,MarkWord中存储的就是指向重量级锁(互斥量)的指针,后面等待的线程也要进入阻塞状态
    4. 解锁过程也是通过CAS进行的

轻量级锁的缺点:如果存在竞争,则处理互斥量(加重量级锁)的开销外,还额外发生了CAS

偏向锁

如果说轻量级锁是在无竞争情况下使用CAS去消除同步使用的互斥量,那偏向锁就是在无竞争情况下把整个同步都消除掉,连CAS都不做了
偏向锁会偏向第一个获取它的线程,如果接下来执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程永远不需要同步

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

Leave a Comment