Java高并发20-并发包中锁原理解析(二)


一、例子

  • 下面来一个例子加深对park和unpark的理解
package com.ruigege.LockSourceAnalysis6;

import java.util.concurrent.locks.LockSupport;

public class TestParkAndUnpark {
 public static void main(String[] args) throws InterruptedException {
  Thread thread = new Thread(new Runnable() {
   @Override
   public void run() {
    System.out.println("child thread begin park");
    //调用park方法,挂起自己
    LockSupport.park();
    System.out.println("child thread end park");
    System.out.println("今天又学了一个快捷键,sysout + alt +/ 是控制台" + 
    "输出的一个快捷键");
   }
  });
  
  // 启动子线程
  thread.start();
  Thread.sleep(1000); // 主线程休眠一秒钟,目的是能够让子线程及时使用
  System.out.println("main thread begin unpark");
  LockSupport.unpark(thread); // 调用unpark方法,能够让子线程thread持有许可证,
  // 然后park方法返回
 }
}
  • 下面来解释一下这个类的主要功效
  • 首先建立了一个子线程,然后调用park方法,由于默认情况下,子线程没有持有许可证,因此它会把自己挂起;在主线程中执行了unpark方法,参数为子线程,这样做的目的就是让子线程能够持有许可证,然后子线程调用的park方法就会返回
  • 注意点:park方法不会告诉我们是因为哪种原因返回的,因此调用者需要根据之前调用park方法的原因,再次检查条件是否满足,如果不满足的话,还需再次调用park方法
  • 例如:根据调用前后的中断状态的对比可以判断是不是因为被中断才返回的。
  • 下面为了说明调用park方法后的线程是因为被中断才返回的,我们修改代码
package com.ruigege.LockSourceAnalysis6;

import java.util.concurrent.locks.LockSupport;

public class TestParkAndUnpark {
 public static void main(String[] args) throws InterruptedException {
  Thread thread = new Thread(new Runnable() {
   @Override
   public void run() {
    System.out.println("child thread begin park");
    //调用park方法,挂起自己
//    LockSupport.park();
    while(!Thread.currentThread().isInterrupted()) {
     LockSupport.park();
    }
    System.out.println("child thread end park");
    System.out.println("今天又学了一个快捷键,sysout + alt +/ 是控制台" + 
    "输出的一个快捷键");
   }
  });
  
  // 启动子线程
  thread.start();
  Thread.sleep(1000); // 主线程休眠一秒钟,目的是能够让子线程及时使用
  System.out.println("main thread begin unpark");
//  LockSupport.unpark(thread); // 调用unpark方法,能够让子线程thread持有许可证,
  // 然后park方法返回
  
  thread.interrupt();
 }
}
20.1
20.1
  • 我们可以从中看出,如果只有中断了子线程,子线程才会运行结束,如果子线程不中断的话,即使调用了LockSupport(thread)方法,也不会中断。

二、void parkNanos(long nanos)方法

  • 与park方法相类似,如果该线程没有拿到许可证,那么调用parkNanos(long nanos)方法该线程会立即停止阻塞,并返回;如果有许可证,那么nanos毫秒之后,该线程才会返回。
  • 先举个例子
package com.ruigege.LockSourceAnalysis6;

import java.util.concurrent.locks.LockSupport;

public class TestPark {
 public void testPark() {
  LockSupport.park();
 }
 public static void main(String[] args) {
  System.out.println("开始park方法");
  TestPark testPark = new TestPark();
  testPark.testPark();
 }

}
20.2
20.2
  • 下面是我们使用parkNanos方法来代替LockSupport.park()方法
  LockSupport.park(this);
  • 使用这个带参数的park(Object blocker)方法,当线程在没有持有许可证的时候,调用park方法,会被阻塞起来,这个blocker对象会被记录到该线程的内部。
  • 使用jstack pid命令可以对线程堆栈进行查看,该线程内部是含有的什么对象

三、park(Object blocker)源码解析

 public static void park2(Object blocker) {
  // 获取当前线程
  Thread t = Thread.currentThread();
  // Thread对象中有一个volatile Object blocker
  // 这里调用setter方法,把这个blocker记录到该线程的blocker对象中
  setBlocker(t,blocker);
  // 调用park方法对该线程进行阻塞
  UNSAFE.park(false,0L); // UNSAFE其实是该线程的Unsafe变量,我们
  // 这里省略前面的定义,直接拿来解释
  setBlocker(t,null);
  // 最后我们又把blocker对象置为空,这是因为已经停止阻塞了
  // 这个blocker对象多用于线程阻塞的时候用来分析原因用的
 }
  • 基本都写在了方法的解释之后

四、void parkNanos(Object blocker,long nanos)方法

  • 其实就是多了一个可以设置的超时时间

五、void parkUtil(Object blocker,long deadline)方法

  • 这个方法和parkNanos不同的就是超时时间的算法,parkNanos的超时时间是从线程阻塞开始算起的,而parkUtil方法的超时时间是从1970年开始算起,到某一个时间点的毫秒数
 public static void parkUtile(Object blocker,long deadline) {
  Thread t = Thread.currentThread();
  setBlocker(t,blocker);
  UNSAFE.park(false,deadline);
  setBlocker(t,null);
 }

六、下面再看一个例子

package com.ruigege.LockSourceAnalysis6;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.LockSupport;

public class FIFOMutex {
 private final AtomicBoolean locked = new AtomicBoolean(false); // 一个boolean类的锁
 private final Queue waiters = new ConcurrentLinkedQueue(); // 一个高并发队列
 
 public void lock() {
  boolean wasInterrupted = false// 中断的标志
  Thread current = Thread.currentThread();
  waiters.add(current); // 队列中添加这个线程
  // (1)
  while(waiters.peek() != current || !locked.compareAndSet(false,true)) {
   // 复习compareAndSet方法,第一个参数是期盼的值,第二个就是如果就是期盼的值,那么就
   // 设置为第二个参数,然后返回true
   LockeSupport.park(this);
   if(Thread.interrupted()) { // (2)
    wasInterrupted = true;
   } 
  }
  waiters.remove();
  if(wasInterrupted) { // (3)
   current.interrupt();
  }
 }
 public void unlock() {
  locked.set(false);
  LockSupport.unpark(waiters.peek());
 }
}
  • 这是一个先进先出的锁,也就是只有队列的首元素可以获取锁,在代码(1)如果当前线程不是队首或者当前锁已经被其他线程获取,那么调用park方法挂起自己。
  • 然后再代码(2)处做判断,如果park方法是因为被中断而返回的,则忽略中断,并且重置中断标志,复习该方法去
  • 在代码(3)中,判断标记,如果标记为true那么中断该线程
  • 总结:其实就是其他线程中断了该线程,虽然我对中断信号不感兴趣,忽略它(也就是代码(2)),但是不代表其他线程对该标志不感兴趣,我们还需要恢复一下。

七、源码:

  • 所在包:com.ruigege.ConcurrentListSouceCodeAnalysis5
  • https://github.com/ruigege66/ConcurrentJava
  • CSDN:https://blog.csdn.net/weixin_44630050
  • 博客园:https://www.cnblogs.com/ruigege0000/
  • 欢迎关注微信公众号:傅里叶变换,个人账号,仅用于技术交流 1000