深入解析线程安全与锁优化

1.线程安全

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

1.Java语言中的线程安全

  1. Java语言中各种操作共享的数据分为以下五类:不可变,绝对线程安全,相对线程安全,线程兼容,线程对立。
    不可变:
    不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再进行任何线程安全保障措施。 如果多线程共享的数据是一个基本数据类型,那么只要在定义时使用final关键字修饰它就可以保证它是不可变的。如String的各种方法只会返回一个新构造的字符串对象。
    不可变要求的类型,除了String,还有枚举类型以及java.lang.Number的部分子类,如Ingeter,Long和Double等数值包装类型、BigInteger和BigDecimal等大数据类型。但AtomicInteger和AtomicLong则是可变的。

    绝对线程安全:不管运行时环境如何,调用者都不需要任何额外的同步措施,很严格。
    相对线程安全:大部分声称线程安全的类都属于这种类型,弱一致性。
    线程兼容:对象本身不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用。
    线程对立:不管调用端是否采取了同步措施,都无法在多线程环境中并发使用代码,是有害的,应当尽量避免。
    例如已被废弃Thread类的suspend和resume方法。不论是否进行了同步,目标线程都存在死锁风险。

2.线程安全的实现方法

  1. 互斥同步: 同步是指多个线程并发访问共享数据时,保证在同一时刻只被一条(或者是一些,当使用信号量的时候)线程使用。 而互斥是实现同步的一种手段,临界区,互斥量,信号量都是常见的互斥实现方式。

  2. 最基本的互斥同步手段就是synchronized关键字,这是一种块结构的同步语法。javac编译后,会在同步块的前后分别形成monitorenter和monitorexit这2个字节码指令。都需要一个reference类型的参数来指明要锁定和解锁的对象。如果是在静态方法上,则相当于加在类上,如果是普通方法,则相当于加在this上。

  3. JDK5起,Java类库提供了JUC包。其中的LOCK接口便成了Java的另一种全新的互斥同步手段。基于Lock接口,用户能够以非块结构来实现互斥同步。重入锁(ReentrentLock)是Lock接口最常见的一种实现。与synchronized很相似,但增加了以下3项高级功能:
    等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,这对处理执行时间非常长的同步块很有帮助。
    公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁,和synchronized一样,默认是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。PS:会导致性能急剧下降,明显影响吞吐量。
    锁绑定多个条件:一个ReenTrantLock对象可以同时绑定多个Condition对象。在synchronized中,锁对象的wait和notify方法配合可以实现一个隐含的条件。如果要和多于一个的条件关联的时候,就不得不额外添加一个锁。
    而ReentrantLock只需要多次调用newCondition()方法即可。
    Lock应该确保在finally块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不会释放持有的锁,这一点必须由程序员自己来保证。而synchronized可以由虚拟机来确保出线异常自动释放锁。

  4. 非阻塞同步:互斥同步面临的主要问题是进行线程阻塞和唤醒所带来的的性能开销,也被称为阻塞同步。是一种悲观锁。

  5. 乐观锁:不管奉献,先进行操作,如果没有其他线程争用共享数据,那操作就直接成功了;如果被争用,产生了冲突,那再进行其他的补偿措施,如不断地重试,直到没有竞争的共享数据为止。不需要把线程阻塞挂起,也被称为非阻塞同步,使用这种措施的代码也常被称为无锁编程。如CAS。
    乐观并发策略需要"硬件指令集的发展",因为我们必须要求操作和冲突检测这2个步骤具备原子性,所以只能靠硬件来实现,硬件保证某些从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成。

  6. CAS指令需要有3个操作数,分别是内存位置(V),旧的预期值(A),和准备设置的新值(B)。CAS指令执行时,当且仅当V符合A时,处理器才会用B更新V的值,否则他就不执行更新。但是不管是否更新V的值,都会返回V的旧值,上诉的处理过程是一个原子操作,执行期间不会被其他线程中断。

  7. CAS操作由Unsafe类里面的几个方法提供,不过由于Unsafe在设计上就不是提供给用户程序调用的类,在JDK9之前只有Java类库可以使用CAS。如JUC包里面的整数原子类。如果用户程序也有使用CAS操作的需求,那要么就采用反射手段突破Unsafe的访问限制,要么就只能通过Java类库API来间接使用它。JDK9之后,Java类库才在VarHandle类里开发面向用户程序使用的CAS操作。

  8. 解决CAS的"ABA"问题,提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性,大部分情况下ABA问题不会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更为高效。

  9. 无同步方案:如果能让一个方法本来就不涉及共享数据,那它自然就不需要任何同步措施去保证其正确性,因此会有一些代码天生就是线程安全的。如下:
    可重入代码:可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回,原来的程序不会出现任何错误,也不会对结果有所影响。
    线程本地存储:如果一段代码所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,就可以把共享数据的可见范围限制在同一个线程之内。

  10. 可以通过ThreadLocal类来实现线程本地存储的功能,每一个线程的Thread对象中都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为键,以本地线程变量为值的K_V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口。

2.锁优化

1.自旋锁与自适应自旋

  1. 在许多情况下,共享数据的锁定状态只会持续很短的一段时间。而且现在绝大多数的个人电脑和服务器都是多核处理器系统,如果物理机器有一个以上的处理器或者核,能让2个或以上的线程同时并行执行,我们就可以让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。这就是自旋锁
  2. 如果自旋超过了限定的次数仍然没有成功获得锁,即应当使用传统的方式去挂起线程。默认是10次。
  3. 在JDK6之后引入了自适应的自旋,自旋的时间由前一次在同一个锁上的自旋时间以及锁拥有者的状态来决定的。

2.锁消除

  1. JIT在运行时,对一些代码要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,主要判定依据来源于逃逸分析的数据支持。如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的。

3.锁粗化

  1. 编写代码时,总是推荐将同步块的作用范围限制得尽量小。
  2. 但如果一系列的连续操作都对同一个对象反复加锁和解锁,即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
  3. 如果虚拟机探测到一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围粗化到整个操作序列的外部。

4.轻量级锁

  1. 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
  2. Hotspot虚拟机的对象头分为2部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄,称为Mark Word;另一部分用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分用于存储数组长度。
  3. 轻量级锁的工作过程:
    1.在代码即将进入同步块时,如果此同步对象没有被锁定(锁标志位为01状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Mark Word的拷贝。
    2.虚拟机将使用CAS操作尝试把对象的Mark Word更新为执行锁记录的指针,如果这个更新动作成功了,代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位转变为00,表示此对象处于轻量级锁定状态。
    3.如果这个更新操作失败了,那就意味着存在线程竞争锁,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,直接进入同步块继续执行就可以了;否则说明这个锁对象已经被其他线程抢占了。如果出现2条以上的线程争用同一个锁的情况,那么轻量级锁必须要膨胀为重量级锁,锁标志变为10.
    此时Mark Word中存储的就是指向重量级锁的指针,后面等待锁的线程也必须进入阻塞状态。
    4.解锁过程同样也是CAS操作来进行的,如果对象的Mark Word仍然指向线程的锁记录,那就用Cas操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来,如果成功替换,那整个同步过程就完成了;否则说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程。
  4. 轻量级锁能提升程序同步性能的依据是整个同步周期内不存在竞争。但如果存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销,因此反而会比传统的重量级锁更慢。

5.偏向锁

  1. 目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS也不用。
  2. 偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。
  3. 运行过程:
    1.锁对象第一次被线程获取的时候,虚拟机会把标志位设置为01,把偏向模式设置为1,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程ID记录在对象的Mark Word之中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。
    2.一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束
  4. 当对象进入偏向状态的时候,Mark Word大部分空间都用于存储持有锁的线程ID了,这部分空间占用了原有存储对象哈希码的位置,那原来的哈希码怎么办呢?
    解:在java语言里一个对象如果计算过哈希码,就应该一直保持该值不变(强烈推荐但不强制,因为用户可以重载hashcode()方法按自己的意愿返回哈希码),否则很多依赖对象哈希码的API都可能存在出错风险。
    而作为绝大多数对象哈希码来源的Object.hashCode()方法,返回的是对象的一致性哈希码,这个值是能强制保持不变的,它通过在对象头中存储计算结果来保证第一次计算之后,再次调用该方法取到的哈希码值永远不会再发生改变。
    因此,当一个对象已经计算过一致性哈希码,它就再也无法进入偏向锁状态了;而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。
    在重量级锁的实现中,对象头指向了重量级锁的位置,代表重量级锁的ObjectMonitor类里有字段可以记录非加锁状态下Mark Word,其中自然可以存储原来的哈希码。
  5. 偏向锁可以提高带有同步但无竞争的程序性能。但如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。有时候使用参数-XX:-UseBiasedLocking来禁止偏向锁优化反而可以提升性能。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值