《深入理解java虚拟机》---线程安全与锁优化(13)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hy_coming/article/details/82534395

一、概述

对于很多的优化来说,前提条件都是保证程序能够正常运行的前提条件下,下面就来说说如何保证并发的正确性和如何实现线程安全。

二、线程安全

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

1.Java语言中的线程安全

为了更加深入的理解线程安全,我们按照线程安全的“安全程度”由强至弱把java语言中各种操作共享的数据分为以下5类:

  • 不可变:在java语言中不可变的对象一定是线程安全的,如果数据共享的是基本数据类型,定义时使用final关键字修饰他就可以保证不可变,如果是一个对象,就需要保证对象的行为不对其状态产生任何影响才行,简单的把对象中带有状态的变量声明为final,Java API中属于不可变类型的有String、枚举、java.lang.Number的部分子类
  • 绝对线程安全:其实java中标注自己是线程安全的类,大多数都不是绝对的线程安全
  • 相对线程安全:这里指的就是java中标注的线程安全的类,主要是保证对这个对象单独的操作是线程安全的,但是对于特定顺序的连续调用就是线程不安全的
  • 线程兼容:指对象本身并不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全地使用
  • 线程对立:指调用无论是否采取了同步措施,都无法在多线程环境中并发使用的代码,这种代码应该尽量避免,如Thread类中的suspend()和resume()方法,现在已经废弃

2.线程安全的实现方法

  • 互斥同步:是一种常见的并发正确性保障手段,最基本的同步手段就是synchronize关键字,有monitorenter和monitorexit两个字节码指令来锁定或者解锁reference对象(synchronize指定的对象参数),前者会将计数器加1,后者会减1,计数器为0时,锁被释放,synchronize同步块对于同一条线程来说是可重入的,还有就是java线程是映射到操作系统的原生线程之上的,阻塞或者是唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户状态切换到核心态中,在状态转换中需要耗费很多的处理器时间,所以对于简单的 同步块,状态转换消耗的时间可能比用户代码执行的时间还要长,所以synchronize是java语言中一个重量级的操作。在JDK1.5之后引入了java.util.concurrent包之后,还可以使用其中的重入锁(ReentrantLock)来实现同步,除了与synchronize功能相似之外还有自己的高级功能:①等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情;②公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序依次获得锁,synchronize中都是抢占式的方式获得锁;③锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象,不需要额外添加锁。还有就是之前ReentrantLock锁是有吞吐量优势的,但是在JDK1.6之后随着synchronize的优化,基本相当,其实官方还是提倡使用synchronize来实现需求的
  • 非阻塞同步: 上面的互斥同步其实是阻塞同步,属于一种悲观的并发策略,所以在性能方面影响比较大,随着硬件指令集的发展,我们现在有了另外一种选择基于冲突检测的乐观并发策略,也就是说先进行操作,产生了冲突在想办法解决。那么指令集怎么保证操作和冲突检测两个步骤具备原子性呢,有以下这些指令:①测试并设置;②获取并增加;③交换;④比较并交换(CAS);⑤加载连接/条件储存。其中CAS并不能涵盖互斥同步的所有使用场景,存在“ABA”漏洞,也就是无法检测到当A变成B又变成A的过程(虽然后来增加原子引用类AtomicStampedReference,但是比较鸡肋)
  • 无同步方案:有一些代码天生就是线程安全的,所以无需任何同步措施,有以下两种:①可重入代码:也叫纯代码,如果一个方法,他的返回结果是可以预测的,只要输入相同的数据,就能够返回相同的结果,那么他就满足可重入性的要求,当然也是线程安全的;②线程本地存储:保证共享数据的可见范围在一个线程之内,就不会出现线程之间的数据争用问题

三、锁优化

1.自旋锁和自适应自旋

前面说到了互斥同步的时候对性能的影响比较大,主要的时间都是耗费在挂起线程和恢复线程上,现在为了避免这种消耗就让等待的线程不放弃处理器的执行时间,而是执行一个忙循环(自旋),这个技术就是自旋锁,但是问题这种技术只有对于锁被占用时间很短的情况,效果才好,一旦锁被占用的时间比较长,,自旋的线程就会白白浪费掉处理器资源,而不会做任何有用的工作,反而带来性能上的浪费。所以要设定自旋的限度,超过默认的10次循环,就要按照传统的方式挂起线程,在JDK1.6之后引入了自适应的自旋锁,意味着自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。虚拟机也就变得越来越聪明了。

2.锁消除

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

3.锁粗化

就是对于同一个对象的多次加锁,可以扩展(粗化)到整个操作序列的外部,只要一次加锁就行

4.轻量级锁

对于绝大部分的锁,在整个同步周期内都是不存在竞争的,这样就有了轻量级锁的存在,使用CAS操作避免了使用互斥量的开销,但是一旦有竞争,轻量级锁就会膨胀成重量级锁。

5.偏向锁

可以说在轻量级锁上又一次优化,这个是在JDK1.6中引入的一项锁优化,目的是在无竞争情况下的把整个同步都消除掉,连CAS操作都不用做了。但是在存在竞争的时候依然不起效果。

猜你喜欢

转载自blog.csdn.net/hy_coming/article/details/82534395