java线程安全与自旋锁,轻量级锁,偏向锁原理解析

java线程安全,锁优化
互斥同步
互斥是实现同步的手段,临界区、互斥量、信号量都是主要的互斥实现方式。Java中最基本的互斥手段就是synchronized关键字,synchronized关键字在编译后,会在同步块前后分别形成monitorenter和monitorexit指令。这两个指令需要一个reference类型的参数来指明要锁定和解锁的对象。如果synchronized明确指定了对象参数,那就是这个对象的reference,如果没有指定,那就要区分是实例方法还是类方法,取当前对象或者这个类对应的Class对象。
非阻塞同步
互斥同步的主要问题是进行线程的阻塞和唤醒所带来的性能问题,因为这种方式会阻塞其他线程,因此也可以称为阻塞同步。从处理方式上来看,属于悲观的并发策略,即不管有没有竞争,都进行同步。
随着硬件指令系统的发展(需要一些原子操作指令的支持,例如CAS,早期的计算机指令系统可能没有这样的指令),有了另一个选择,基于冲突检测的乐观并发策略,即先进形操作,如果没有其他线程争用共享数据,那就操作成功了,如果存在竞争,再采取必要的补救措施,比如不断地重试,直到成功为止。
自旋锁
互斥同步对性能影响最大的是阻塞的实现,挂起线程和恢复线程都需要在内核中完成,这些操作给并发性能带来很大的压力。
在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得如果物理机器上有一个以上的处理器,我们可以让后面申请锁的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,只需让线程执行一个忙循环(自旋),就项技术就是所谓的自旋锁。
所谓“自旋”,就是让线程去执行一个无意义的循环,循环结束后再去重新竞争锁,如果竞争不到继续循环,循环过程中线程会一直处于running状态,但是基于JVM的线程调度,会出让时间片,所以其他线程依旧有申请锁和释放锁的机会。

**自旋等待不能代替阻塞,且先不说对处理器量的要求,自旋等待本身虽然避免了线程切换的开销,但它要占用处理器的时间,因此,如果锁被占用的时间短,自旋等待效果好,反之,自旋的线程只会白白消耗处理器资源,而不做任何有用工作,带来性能上的浪费。**因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有获得锁,就应当使用传统的方式挂起线程。自旋次数的默认值是10次,用户可以使用参数 -XX:PerBlockSpin 来修改。
JDK1.6引入了自适应的自旋锁
自旋的时间是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就认为这次自旋也很可能成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得,在以后获得这个锁时将可能省略自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就变得越来越“聪明”了。

锁消除
是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去,从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然无须进行。
轻量锁
清亮级锁是相对操作互斥量的传统锁(重量级锁)而言的。轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。Hotspot虚拟机的对象头分为两部分,是实现轻量级锁和偏向锁的关键。对象头包含的状态信息如下

java线程安全与自旋锁,轻量级锁,偏向锁原理解析
轻量级锁的实现过程
在代码进入同步代码块时,如果对象是未锁定状态,虚拟机会首先会在当前线程的栈帧中创建一个锁记录(Locked Record)空间,用于存储锁对象当前的Mark Word拷贝,叫Displaced Mark Word;
然后虚拟机采用CAS操作将锁对象的Mark Word修改为指向锁记录的指针,如果更新成功,那线程就拥有了该锁对象,并且对象的Mark Word的标记位(最后2bit)修改为00。表示此对象处于轻量级锁状态。
java线程安全与自旋锁,轻量级锁,偏向锁原理解析
如果更新操作失败,虚拟机会检查对象的Mark world是否指向当前线程的栈帧,如果是说明该对象已占有锁,进入同步代码块继续执行,否则说明这个对象呗其它线程抢占**。则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。**锁标志状态改为10。mark wrold存储的就是重量级锁的指针,后面等待锁的线程也进入阻塞状态。

轻量级锁提升性能的依据是:对于绝大部分的锁,同步过程是不存在竞争的。如果没有竞争,那轻量级的CAS操作避免了互斥量的开销,但如果存在竞争,那性能反而传统的重量级锁慢(CAS+互斥信号量)。
轻量级锁的解锁:
也是通过cas完成的,如果对象的mark wrold仍然指向着线程的锁记录,用cas操作把当前对象的mark wrold和线程中复制的displaced mark wrold替换回来,替换成功说明同步完成。替换失败说明其他线程试图获得该锁,释放锁的同时,唤醒挂起的线程。
偏向锁
如果说轻量级锁是在无竞争条件下,通过CAS操作去消除的同步使用互斥信号量,那偏向锁就是在无竞争条件下把整个同步都消除掉,连CAS也不用做。
“偏”指的是这个锁对象会偏向第一个获取它的线程,如果在接下来的的执行过程中,该锁没有被其他线程获取,则持有该偏向锁的线程将永远不再需要同步。
当锁对象第一次被获取时,标记为被设为“01”,即偏向模式,并且使用cas操作把获取到这个锁的线程ID记录在Mark Word中,如果cas操作成功,后面持有偏向锁的线程在进入同步代码块,就不需要再同步了。当另外线程尝试获得这个锁,偏向模式宣告结束,对象状态恢复到未锁定或轻量级锁的状态。
详细过程如下:
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word,要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

重量级锁
重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步(阻塞)

java线程安全与自旋锁,轻量级锁,偏向锁原理解析
轻量级锁基于cas实现,是乐观锁。自旋锁与cas并无什么关系。轻量级锁中也用到了自旋锁。

cas
CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项 乐观锁 技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

AQS
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。