第13章 线程安全与锁优化

13.2 线程安全

13.2.1 Java语言中的线程安全

13.2.2 线程安全的实现方法

  主要包括两个方面的内容一个是从程序员的角度如何写线程安全的代码,另一个是虚拟机底层如何实现线程安全。

  如果多个线程一起读写一个共享的数据,在不加额外措施的情况下一定会产生并发问题,这是一个老生常谈的问题了。解决的方式也有很多,这里主要从是否阻塞相关线程的角度分为两类。

  1、互斥同步

  我更倾向于把他理解成阻塞同步。最常用的关键字是synchronized,该关键字编译后会在同步的代码块前后增加monitorenter和monitorexit关键字,这两个关键字都需要一个reference类型的变量来指定一个对象。从这个角度可以理解sychronized是针对对象的。如果指明了sychronized是针对某一个对象的,那么reference就会指向该对象,如果没有的话根据sychronized指向的是类方法还是实例方法去指向相应类对象或者实例对象。

  该关键字会带来互斥性,即任意一个时刻只能有一个线程可以获得锁,如果没有成功获得那么试图获得线程的锁会被阻塞知道锁的释放。

  该锁是可以重入的,即一个线程在已经获得锁的前提下再次尝试获得锁的时候是可以获得锁的,只不过锁的计数器会加一。相应的一个线程在执行monitorexit之后锁的计数器会减一,当计数器为0的时候锁被释放然后唤醒别的在等待的线程。

  线程A获得锁指的是reference所指向的对象的对象头里指向A线程。

  Java的线程是依赖于操作系统的线程来实现的,所以一个Java里的线程就是操作系统里的一个线程,JMM没有对线程做抽象。所以线程的阻塞与唤醒需要陷入内核态(内核线程?????)这回带来一个较大的开销,早期的JDK会使用concurrent包中的ReentrantLock来实现同步。

  ReentrantLock相较于sychronized有三个特点:1、等待可中断,即一个线程在等待锁的时候可以选择放弃等待。 2、可以实现公平锁。所谓公平锁指的是按照申请锁的时间的顺序来获得锁,虽然sychronized和ReentrantLock都是非公平的,但是ReentrantLock在构造的时候可以设置成公平的 3、可以绑定多个条件

  2、非阻塞同步

  阻塞同步需要把相关线程阻塞起来,阻塞和唤醒线程需要较大的代价。随着硬件指令集的发展现在有了基于冲突检测的乐观并发策略。

  这里介绍了一个CAS(Compare-and-Swap)操作。CAS操作需要三个操作数:内存的地址V,旧的预期值A,新的预期值B。CAS执行的时候会比较V和A的值是否相等,只有他们相等的时候会把B操作的值赋给V操作否则不更新。这个指令看似干了很多事情,但是通过一些硬件的实现保证这是一个原子级别的操作。

  Java里的Concurrent包里很多都是用CAS来实现的,比如AtomicInteger。

  

  当增加一个AtomicInteger的时候,会执行一个死循环用当前的值做A,A+1做B,然后直到CAS操作成功才推出该循环。这样即使这个increment被打断的时候,实时上他很大程度上会被打断因为没有加锁,也不会被影响其正确性,当然虽然没有讲我猜测上面三个语句都是原子的,即curent赋值、next赋值、CAS操作是原子的。只要能保证这三个操作是原子的,那么整个自增方法即使不是原子的,也能保证在多线程的条件下自增是线程安全的。

  

13.3 锁优化

13.3.1 自旋锁与自适应锁

  互斥锁的一个很大的缺点在于线程的阻塞和唤醒需要较大的代价,一个解决思路是CAS,这里提供了另一种思路。当一个线程试图获得一个锁且失败的时候,他不是被挂起,而是执行一个自旋即忙等待。如果持有锁的线程能很快释放锁的话,那么自旋锁的代价是小于互斥锁的。

  为了避免一个线程长时间自旋带来的浪费,往往设定一个阈值,当自旋超过一定次数的情况下就不再自旋转而变成互斥锁。根据如何设置阈值的不同可以把锁分为自适应锁和非自适应锁,非自适应锁的阈值是设定好的,自适应锁如其名所言,自旋的阈值是根据上一个在该锁上自旋的线程决定的,即如果上一个线程在这个锁上自旋了很久,那么我的阈值就大一些。

13.3.2 锁清除

  虚拟机分析一些锁的必要性,如果是一个不需要加锁的地方加了锁那么在编译的时候会去处。

13.3.3 锁粗化

  如果一串连续的操作都是对同一对象加锁,那么虚拟机会把加锁的对象粗化对整个序列开头和结尾加一个范围更大的锁。

13.3.4 轻量级锁

13.3.5 偏向锁

  

原文地址:https://www.cnblogs.com/AshOfTime/p/10335696.html