Java并发包中的锁原理剖析

LockSupport工具类

LockSupport是JUC并发包下的一个工具类,它的底层是由Unsafe类实现的,它是基于Unsafe类的park()和unpark()方法包装并扩展功能实现的一个工具类,它主要用于对线程的挂起和唤醒操作,它是线程安全的,底层由CAS算法实现。通过它的名字我们也可以知道,它是锁的支持类,后面的锁的实现都是基于它实现的。
park()方法和wait()方法
先谈谈wait()和Unsafe类的park()方法,LockSupport的park()方法是对Unsafe类的park()方法的包装,所以我们需要先理解底层的实际挂起线程的park()方法。
Object类的wait()源码:

    public final void wait() throws InterruptedException {
   
        wait(0);
    }
    public final native void wait(long timeout) throws InterruptedException;    

Unsafe类的park()源码:

public native void park(boolean isAbsolute, long time);

**park()方法的许可证:**上述两种挂起线程的方法底层都是native方法,我们知道wait()方法可以让一个线程挂起并释放锁资源,前提是必须在synchronize关键字内调用,在别的地方调用会抛出 java.lang.IllegalMonitorStateException异常。而park()方法只是挂起线程并不涉及到锁,park()方法会关联一个许可证,这个许可证的获取方式是调用LockSupport的unpark()方法,并且把希望唤醒的线程对象传入,接下来有两种情况:一种是线程直接调用park()方法,此时它因为没有许可证而被挂起,然后调用unpark()方法并把这个线程对象作为参数,此时线程会被唤醒;另一种情况是先调用unpark()方法,传入该线程对象,再调用这个线程对象的park()方法,它在被阻塞之后会直接返回。许可证被赋予之后只能使用一次。
**许可证的抽象理解:**许可证的机制可以理解为一道关卡,所有线程到了这里都会停下来,它们会一直等待许可证的发放,此时有许可证的线程可以通过,许可证使用过后就会被销毁,分发许可证的方法是unpark()。于是,这里总共涉及到了两样事物,一个是关卡,也就是无参的park()方法;另一个是许可证的分发,也就是unpark()方法;
**许可证的意义:**许可证的存在使得park()方法和unpark()方法真正意义上成为并发包的基础,它可以构成锁,park()方法使得没有许可证的线程无法进入相应代码块,它会使得无关线程挂起,防止它们争夺资源,导致死锁。它和synchronize的设计效果是相同的,区别是synchronize是获取和释放对象锁,而park()与unpark()是获取许可证实现“通行”。park()与unpark()的发展前景更好,可以基于它设计线程的队列,例如后面要讲到的AQS队列,在这两个方法或者AQS队列的基础上实现各种不同功能的锁。
LockSupport的park()方法:
然后再来看看LockSupport的park()方法,它底层也调用了Unsafe的park()方法,源码如下:

LockSupport.park();
public static void park() {
   
      UNSAFE.park(false, 0L);
 } 

我知道unsafe的park方法需要两个参数的传入。true表示绝对时间,一般在使用的时候需要获取当前时间加上希望的偏移时间(ms)来作为第二项的参数;false表示相对时间,表示方法调用多少时间之后返回(ns)。在LockSupport中对park做了封装,默认参数为0,表示一直被挂起。
LockSupport的unpark()方法:
unpark方法调用的也就是Unsafe的unpark方法,需要有线程传入才有效。

LockSupport.unpark(current);
public static void unpark(Thread thread) {
   
      if (thread != null)
          UNSAFE.unpark(thread);
  }  

**其他返回情况:**被挂起的线程也可能被一些别的情况唤醒,包括小概率的非正常唤醒,以及被中断唤醒。这里介绍中断唤醒,当一个线程被挂起的时候,被别的线程调用了它的interrupt()方法,它就会被唤醒,为了区别线程的唤醒原因,一般在唤醒后判断中断标志,如果不希望线程被中断唤醒,可以设计while()循环,唤醒后根据中断标志决定是退出循环还是重新挂起。如果只希望被中断唤醒,那么while的循环条件可以是while(!Thread.currentThread().isInterrupted)只有被唤醒后标志为true才会离开循环,否则继续挂起。总之,需要考虑到被中断唤醒这种情况。
LockSupport.parkNanos(nanos)方法:
挂起指定时间的方法,如果线程持有许可证,那么挂起后直接返回;如果线程没有许可证,挂起nanos时间后自动返回。
LockSupport.park(blocker)方法:
测试Domo:如下两段测试分别是无参park方法和传入this的park方法。

public class Test {
   
      public static void main(String[] args) {
    
    	 Test test=new Test();
    	 test.test();
      }      
      public void test(){
   
    	  LockSupport.park();  //LockSupport.park(this);
      }
}

输出截图:
在这里插入图片描述
在这里插入图片描述
后者输出多了一段wait时间,即调用了park方法后的内部信息。为什么呢?我们来分析源码:
如下代码中在调用Unsafe的方法中设置了blocker,在被唤醒后会清除这个bolcker对象,初步推测这是个线程的成员变量,所以可以获取内部信息并且在使用完后要释放内存。最后调用的方法中传入了当前线程和blocker实例(传入的Test对象),
parkBlockerOffset在LockSupport类中有一个静态变量是用来保存传入的变量引用的。通过它可以保存更多堆栈信息,可以得知线程被阻塞后发生的事情。

    public static void park(Object blocker) {
   
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    private static void setBlocker(Thread t, Object arg) {
   
        // Even though volatile, hotspot doesn't need a write barrier here.
        UNSAFE.putObject(t, parkBlockerOffset, arg);
    }    

LockSupport.parkNanos(blocker, nanos)方法:
它和上者的区别的是它是一个有时间的阻塞方法,同时传入bolcker记录堆栈信息。
LockSupport.parkUntil(blocker, deadline);方法:
这个方法是指指定截止时间,到某个时间后自动唤醒,同样它也传入了一个blocker对象。底层Unsafe方法的park参数是true,表示绝对时间。

    public static void parkUntil(Object blocker, long deadline) {
   
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
    }

AQS(抽象同步队列)

它是一个抽象类,它的作用是通过FIFO(先进先出)队列的形式对线程对资源的访问顺序进行管理,并维护一些参数记录信息。通过它可以实现各种类型的锁,因为它的一些方法没有具体实现,需要交给子类去完成。一般来说JUC包中的锁都是继承自它,然后实现功能的。
AQS的参数

  • 队列元素类型:Node,其中的thread成员变量用来保存线程的引用。
  • 节点内部的SHARED(共享)和EXCLUSIVE(独有)分别表示节点被阻塞放入队列的原因。
  • waitStatus记录线程的等待状态,可以为CANCELLED(取消了)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里等待)。
  • prev记录当前节点的前驱结点,next记录当前节点的后继节点。
  • AQS队列中还维持了一个单一的状态信息state,例如可重入锁的重入次数就是由它来记录的。如果它被实现为读写锁,那么它的高16位记录读锁的获取次数,低16位才用来记录重入次数。
    锁的独占方式:
    独占方式是指state表示重入次数,一个线程获取了锁之后,别的线程再试图去访问都会被挂起等待,获取的锁的线程会把锁的持有者成员变量设置为自己,同时把state设置为1,之后别的线程访问该资源,通过持有者线程引用比对后发现不是它,那么就会被挂起,之前的锁使用完资源后,会释放锁,这里是释放指的是清零state变量,同时把持有者线程置为null,最后再唤醒一个在AQS队列中等待的线程。
    锁的共享方式:
    共享方式可以理解为公共资源,资源可以被多个线程获取,但是资源有限。可以理解为一个停车场,它有三个停车位,来了五辆车,前三辆获
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值