关于ReentrantLock类的解读

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qweasdzxc456123/article/details/72621052

有任何问题和见解,欢迎交流啊。 

synchronized关键字提供了语言级别的并发支持,通过这个关键字,标明了可能会产生竞态代码的区域,这样多线程调用的时候就会保证同一时间只有

 一个线程可以进入,消除了多线程的问题.
 但是用synchronized关键字实现的并发是很重量级别的,因为没有获取到对应锁的线程会阻塞,如果要运行就需要调度,这会涉及到内核的
 线程调度,这块的消耗是很大的,可能调度的时间会比你实际代码运行的时间都要长.最近的jvm在实现的时候也是对这块进行了优化,这块还需要研究,

 后面再写吧,有些复杂啊.在这里留2个问题需要进一步研究:

1.上面说的在jvm是如何与os之间协作,完成synchronized语义的。

2.这里的消耗到底是多少,最大的影响在哪,是不是网上说的调度的原因

先抛开这个不谈,我们来说下java中的另一种加锁的方式,成为显示锁。这种加锁和解锁的过程是在java中通过具体类来提供的,如ReentrantLock和ReentrantWriteReadLock。下面就一步一步的来分析它。

1.首先很重要的一点在于unsafe的类带来的非阻塞的同步机制.这里的非阻塞我觉得是相对于synchronized而言的,可能每个平台也是不一样的.一个
很明显的应用在于使用类似自旋锁的机制来实现同步,而不是像synchronized一样直接拿不到锁就阻塞.下面是一个典型的代码
  for(;;){
      int temp=get();                            //get current value
      int newValue=temp+1;                       //increment this 
      if(unsafe.compareAndSwapInt(this, valueOffset, temp, newValue))   //change this value by unsafe
  }
  在上面的代码中,先获取竞态数据的当前值,假设我们要实现的是一个并发安全的自增1代码,所以第二行是将数据加1,在第三行中,通过unsafe
  提供的方法,利用硬件提供的共享数据并发机制,能够直接对竞态数据进行并发安全的处理,处理成功的结果也能快速返回,这样即使失败了,循环
  仍旧继续,一致到成功为止.
  
  上面提到的硬件提供的共享数据并发机制,是现代的CPU提供的一个功能.在CPU中,数据可以放在寄存器,高速缓存或者内存中,为了能够让程序运行的更快,
  数据自然是能够从高速缓存或者寄存器中更好.但是对于并发的程序,这会带来很多问题,因为数据的并发访问如果没有一个一致性的控制,那么
  数据很快就会是个不合法的数据.而通过硬件级别的同步,比如总线锁定和高速缓存行锁定,能最大效率的利用处理器的能力,能提供更高效率的并发控制.

2.从ReentrantLock的api中可以看出,加锁有3种方式,分别对应的如下几个方法:

      public void lock()  
      public void lockInterruptibly() throws InterruptedException
      public boolean tryLock()
      public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException

这四种是不同的获取锁的方法,第一种就是很简单的锁方式,不会考虑线程的中断,获取到锁就返回,不然就阻塞线程.
   第二种在获取锁的时候会考虑中断信号,就和Thread.sleep或者wait方法一样,会抛出InterruptedException.
   第三种是获取到锁就返回,不然就直接结束,不会像第一种方式一样阻塞线程.
   第四种方法较第三种方法会等待一段时间,仍然获取不到锁就返回.
   下面这个测试的程序最终会因为子线程被调用了interrupt方法所以会中止。如果使用lock()方法,那么子线程会一直等待下去。
public class ReentrantLockTest {
public static void main(String []args){
final ReentrantLock rtl=new ReentrantLock();
rtl.lock(); 
Thread t1=new Thread(new Runnable(){
public void run(){
try {
rtl.lockInterruptibly();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
System.out.println("children "+Thread.currentThread().getName()+" is interrupted");
}
}
}); 
t1.start(); 
t1.interrupt();
}
}

现在来说下ReentrantLock是如何保证并发安全的。

在并发的情况下,不可见性、指令重排序、原子性会导致并发的问题,synchronized可以保证临界区不会出现并发的问题,
而ReentrantLock,在lock和unlock之间的部分保证了对于临界区代码的同步处理.那么对于数据的可见性是如果保证的呢,我们可以从jvm的内存模型
中找到答案.根据jsr 133中指出的java内存模型(jmm),volatile修饰的变量有如下的描述:
   Under the new memory model, it is still true that volatile variables cannot be reordered with each other. 
   The difference is that it is now no longer so easy to reorder normal field accesses around them. 
   Writing to a volatile field has the same memory effect as a monitor release, 
   and reading from a volatile field has the same memory effect as a monitor acquire. 
   In effect, because the new memory model places stricter constraints on reordering of volatile field accesses
    with other field accesses, volatile or not, anything that was visible to thread A when it writes to volatile field f 
    becomes visible to thread B when it reads f.
也就是说在新的jmm中,volatile变量不仅仅是可以保证该变量的可见性,而且可以保证代码中对这个volatile变量前代码数据的可见性,因为不管是普通的变量还是volatile变量,编译器或者处理器都不会再进行指令重排序.回到ReentrantLock这个类的内部,使用一个int类型的字段state来记录锁对应的状态的,0表示该锁并没有被获取,大于0表示已经被获取,因为要支持可重入获取锁,所以stete的值可能会大于1,这个值是用volatile来修饰的,在lock和unlock之间就是对该值的修改,从上面说的volatile的解释中来看,这种方式保证了代码的可见性.所以到这里就解释了ReentrantLock是如果保证并发的可见性和原子性问题了.
详细的jsr 133的faq如下:
https://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html#jsr133

ReentrantLock类的内部实现是这样的:
  1.通过上面说的一个 volatile int state 值来确定锁的状态,0是没有锁,大于0表示已经锁了.
  2.通过一个双向链表保存等待获取锁的线程,这个链表的维护都是委托给AbstractQueuedSynchronizer类来实现的.链表的结构的该表也是通过unfase类来提供的.



展开阅读全文

没有更多推荐了,返回首页