Java虚拟机-线程安全与锁优化

Scroll Down

线程安全

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

线程安全的实现方法

1、互斥同步
互斥同步是一种最常见也是最主要的并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一条(或者一些,当使用信号量的时候)线程使用。
在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。synchronized关键字经过javac编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作为reference;如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。

用户态和内核态之间的切换的原因是因为线程的切换操作导致的,首先会挂起线程然后执行其他线程,在这期间需要保存线程的栈帧等信息,所以减少线程切换也会减少用户态和内核态之间的切换。

在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没有被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加1,而在执行monitorexit指令时会将锁计数器的值减1.一旦计数器的值为0,锁随即就被释放了。如果获取对象锁失败,那当前线程就应该被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。

  • 被synchronized修饰的同步块对同一条线程来说是可重入的。这意味着同一线程可以反复进入同步块也不会出现自己把自己锁死的情况。
  • 被synchronized修饰的同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。

从执行成本来看,持有锁是一个重量级的操作。在主流Java虚拟机实现中,Java线程是映射到操作系统的原生内核线程之上的,如果要阻塞或者唤醒一条线程,则需要操作系统来帮忙完成,这就不可避免地陷入用户态到内核态的转换之中,进行这种状态转换需要耗费很多的处理器时间。尤其是对于代码特别简单的同步块(譬如setter和getter),状态转换消耗的时间甚至会比用户代码本身执行的时间还要长。因此才说,synchronized是Java语言中一个重量级操作。

ReentrantLock是Lock接口中最常见的一种实现,顾名思义,它与synchronized一样是可重入的。在基本用法上ReentrantLock也与synchronized很相似,只是代码写法上稍有不同而已。不过ReentrantLock与synchronized相比增加了一些高级功能,主要有以下三项:等待可中断、可实现公平锁及锁可以绑定多个条件。

1、等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。
2、公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁,而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock在默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。不过一旦使用了公平锁,将会导致ReentrantLock的性能急剧下降,会明显影响吞吐量。
3、绑定多个条件:是指一个ReentrantLock对象可以同时绑定多个Condition对象。在synchronized中,锁对象的wait跟它的notify或者notifyAll方法配合可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外添加一个锁;而ReentrantLock则无须这样做,多次调用newCondition方法即可。