深入理解Java虚拟机-线程安全与锁优化

1. 线程安全

1.1 Java中的线程安全

  • 何为线程安全:指当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度与交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
    我们将线程安全分为五类,不可变,绝对线程安全,相对线程安全,线程兼容,线程对立。
  • 不可变:不可变的对象一定是线程安全的。如果共享数据是基本数据类型,那么只需要在定义时使用final关键字就可以保证他是不可变的,如果共享数据是一个对象,那么就需要保证对象的行为不会对其状态产生任何影响,如java.lang.String类的对象中的replace(),substring()等方法都返回一个新构造的字符串,保证一个对象是不可变的最简单方法是把对象中带状态的变量都声明为final。
  • 绝对线程安全:一个类要达到不管运行时环境如何,调用者都不需要使用额外的同步措施。如java.util.Vector是一个线程安全的类,它内部的get(),add()等方法都是被synchronized修饰的。但当有两个线程一个负责移除vector中元素,一个负责获取vector中元素时,如果不在方法端加额外的同步操作,仍会抛出ArrayIndexOutOfBoundsException。
  • 相对线程安全:相对的线程安全就是我们通常意义的线程安全,它需要保证这个对象单独的操作的是线程安全的,我们在调用时不需要额外的保障措施,时,如果不在方法端加额外的同步操作。但对于一些特定顺序的调用,就需要在调用端采取一定的同步措施来保证线程安全。如java.util.Vector。
  • 线程兼容:线程兼容指对象本身就不是线程安全的,需要在调用端采取一定的同步措施来保证线程安全。如java.util.ArrayList。
  • 线程对立:无论如何采取同步措施,都无法在多线程环境下并发使用的代码。

1.2 实现线程安全的方法

  • 阻塞同步(互斥同步):Java中最基本的互斥手段就是synchronized关键字,synchronized关键字在编译后会在同步块前后产生monitorenter,monitorexit两条字节码指令。首先,sychronized同步块对同一个线程是可重入的,不会出现自己把自己锁死的问题。其次,Java的线程是映射到操作系统的原生线程上的,线程的阻塞和唤醒就需要操作系统从用户态到内核态的转换,这种转换是很消耗时间的,当synchronized关键字修饰的同步块运行时间小于状态转换的消耗时间时,操作系统阻塞线程前加入一段自旋等待过程,避免频繁的切换到内核态之中。
    除了synchronized关键字外,还可以使用J.U.C下面的可重入锁(ReentrantLock)实现。synchronized实在JVM层面上实现的,而ReentrantLock是在JavaAPI层面实现的,ReentrantLock相对于synchronized来说,主要有以下3个高级功能,等待可中断,公平锁,锁可绑定多个条件
    等待可中断指当持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待。
    公平锁指当多个线程等待同一个锁时,必须按照申请锁的顺序以此获得锁。
    锁可绑定多个条件指ReentrantLock可以同时绑定多个Condition对象,只需要多次调用newCondition()方法即可。
    在JDK1.6之后经过优化后synchronized与ReentrantLock的性能基本持平。
  • 非阻塞同步:指基于冲突检测的乐观并发策略,主要是基于硬件的CAS原子指令,我们主要通过Java API来间接引用,如J.U.C包中的原子类。
    CAS的ABA问题,解决方案,提供一个带有标记的原子引用。
  • 无同步方案:如果一个方法本来就不涉及共享数据,就无需任何同步措施来保证正确性。如可重入代码和线程本地存储。

2. 锁优化

2.1 自旋锁与自适应自旋

  • 由于挂起线程和恢复线程操作需要在内核态中完成,会给并发性能带来很大压力,在许多情况下,共享数据的锁定状态只会持续很短一段时间,所以我们只需要使请求锁的线程稍等一下(空循环),这就是所谓的自旋锁。
  • 如果锁被占用的时间很短,自旋等待的效果会特别好,反之如果锁被占用时间很长,那么自旋的线程只会白白消耗处理器的资源,带来性能上的浪费。
  • 自适应自旋指自旋的次数不在固定,如果自旋等待刚刚获得锁,且持有锁的线程正在运行时,则此次自旋的允许更长的时间。如果自旋很少成功过,那么以后获得这个锁可能省略掉自旋过程。

2.2 锁消除

  • 锁消除指虚拟机即时编译器在运行时,对一些代码上要求同步,但是检测到不可能存在共享数据竞争的锁进行消除。

2.3 锁粗化

  • 如果一系列的连续操作都对同一个对象反复加锁,解锁。甚至枷锁操作出现在循环体里,频繁的进行互斥同步操作也会导致不必要的性能损耗。

2.4 轻量级锁

  • 轻量级锁指在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量的性能消耗。
  • 轻量级锁的加锁过程:当代码进入同步块时,如果此同步对象没有被锁定,虚拟机首先在当前现成的栈帧中建立一个锁记录空间,用于存储对象的Mark Word的拷贝,然后虚拟机使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果更新成功了,那么这个线程就拥有了该线程的锁,标记此对象处于轻量级锁定状态。如果更新失败了,虚拟机会检查对象的Mark Word是否指向当前线程的栈帧,如果指向当前线程的栈帧,说明当前线程已经拥有了对象的锁,可以直接进入同步块执行,否则说明这个给锁对象已经被其他线程抢占了。如果两个线程争用同一个锁,那轻量级锁就会膨胀为重量级锁。
  • 轻量级锁的解锁过程:如果对象的Mark Word仍然指向线程的所记录,那么就用CAS操作把对象的Mark Word和复制的替换回来,如果替换成功,整个同步操作就完成了。如果替换失败,说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程。
  • 轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内是不存在竞争的”,如国没有竞争,轻量级锁使用CAS操作避免了互斥量的开销,如果存在竞争,除了互斥量的开销,还额外发生CAS操作,轻量级锁会比重量级锁更慢。

2.5 偏向锁

  • 偏向锁的目的是消除数据再无竞争的情况的同步原语,进一步提高程序的运行性能,即连CAS操作也不做了。这个锁会偏向于第一个获得他的线程,如果在接下来的执行过程中,该所如果没有被其他线程获取,那么偏向锁的线程永远不要再同步。
  • 当锁对象第一次被线程获取时,对象会变为偏向模式,同时使用CAS操作把获取该锁对象的线程ID记录在Mark Word中。如果CAS操作成功持有偏向锁的线程每次进入这个锁的同步块时,虚拟机可以不进行任何同步操作。
    当有线程尝试获取这个锁时,便结束偏向模式,根据锁对象的是否锁定的状态恢复成未锁定或轻量级锁定的状态。
    在这里插入图片描述
  • 如果程序中大多数的锁总是被不同线程访问,那偏向锁模式就是多余的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值