JUC并发编程01——LockSupport的使用

155 阅读10分钟

nick-fewings-DdIGZjmQzdA-unsplash.jpg


LockSupport 是JUC中提供的一个工具类,主要是在 阻塞线程(park)和唤醒线程(unpark)时使用,提起线程阻塞和唤醒,我们可能第一时间想到的就是Object类提供的wait()notify()方法,以及JUC中Condition接口提供的await()signal()方法,在探究LockSupport之前,我们先简单回忆下这二者的使用。

image.png

1.Object的wait()/notify()

wait():让持有该对象锁的线程等待,会使线程暂停并让出CPU资源,同时释放持有的对象锁
notify():随机唤醒一个处于WAITING状态的线程。
notifyAll():唤醒所有处于WAITING状态的线程。
注意:被notify()、notifyAll()唤醒后,线程不会立即执行,而是需要重新竞争对象锁,获得锁的线程可以从wait处继续向下执行

我们先看下代码:

public class ObjectWaitNotifyTest {

    private static final Object object = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (object) {
                try {
                    System.out.println(Thread.currentThread().getName() + " 线程进入 wait()方法");

                    /**
                     * 使线程暂停并让出CPU资源,同时释放持有的对象的锁
                     */
                    object.wait();

                    System.out.println(Thread.currentThread().getName() + " 线程退出了 wait()方法, 被唤醒了....");
                    for (int i = 0; i < 5; i++) {
                        System.out.println("num: " + i);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (object) {
                try {
                    System.out.println(Thread.currentThread().getName() + " 线程准备随机唤醒某个线程");
                    object.notify();
                    //object.notify(); 唤醒所有等待的线程
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "t2");

        t2.start();
        t1.start();


    }

在使用上wait()和notify()的局限

  1. wait()notify()必须要用在synchronized同步代码块或者同步方法中,否则将抛出 IllegalMonitorStateException 异常
  2. 必须要确保先执行 wait(), 后执行notify()才可以唤醒等待的线程

2.Condition的await()/signal()

public class LockConditionTest {

    private static Lock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " 线程进入 wait()方法");

                //释放锁
                condition.await();

                System.out.println(Thread.currentThread().getName() + " 线程退出了 await()方法, 被唤醒了....");

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }, "t1");

        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "线程准备随机唤醒线程");
                condition.signal();
                //唤醒所有等待的线程
                //condition.signalAll();
            } finally {
               lock.unlock();
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}

我们再来看下它的局限性:

  1. Condition必须搭配Lock使用,否则将抛出 IllegalMonitorStateException 异常(和Objectwait(),notify()一样)
  2. 必须要确保先执行await(),后执行signal()才可以唤醒

从上面看,Objectwait()/notify()Conditionawait()/signal() 都有局限性,必须要在同步块或者锁内使用,并且在使用上要保证顺序,先阻塞后唤醒。为了打破这种局限性,JUC提供了一个全新的阻塞/唤醒工具类—— LockSupport.

3.LockSupport的park()/unpark()

我们先通过代码看下LockSupport是如何完成阻塞和唤醒的

public class LockSupportTest {

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " 线程进入 park()方法");
                //进入阻塞,释放锁
                LockSupport.park();
                System.out.println(Thread.currentThread().getName() + " 线程退出了 park()方法, 被唤醒了....");
            } catch (Exception e) {
                e.printStackTrace();
            }

        }, "t1");

        Thread t2 = new Thread(() -> {
            try {
                //2s后唤醒t1线程
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 线程唤醒t1线程");
            /**
             * 指定唤醒哪个线程,这相比Object的notify()和Condition的signal()来说是精准唤醒了
             */
            LockSupport.unpark(t1);
        }, "t2");


        t1.start();
        t2.start();
    }
}

不难发现,LockSupport不依赖于同步块或者锁,使用的时候直接调用静态方法即可,非常方便。

我们再看下先唤醒在阻塞是否可行?

public class LockSupportTest {

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            try {
                try {
                    //目的是测试先唤醒,后阻塞
                    Thread.sleep(3000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " 线程进入 park()方法");
                //进入阻塞,释放锁
                LockSupport.park();
                System.out.println(Thread.currentThread().getName() + " 线程退出了 park()方法, 被唤醒了....");
            } catch (Exception e) {
                e.printStackTrace();
            }

        }, "t1");

        Thread t2 = new Thread(() -> {
            try {
                //2s后唤醒t1线程
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 线程唤醒t1线程");
            /**
             * 指定唤醒哪个线程,这相比Object的notify()和Condition的signal()来说是精准唤醒了
             */
            LockSupport.unpark(t1);
        }, "t2");


        t1.start();
        t2.start();
    }
}

image.png

不难发现,先唤醒后阻塞也是没有问题的。

从使用上来说,LockSupport 没有额外的条件约束(对比Objectwait()/notify()需要在synchronized 代码块内使用), 而且也没有顺序约束,不像其他的必须先阻塞后唤醒。

3.1 LockSupport 说明

LockSupport是JDK6中提供的一个工具类,用来创建锁和其他同步工具类的基本线程阻塞原语。LockSupport很类似于二元信号量Semaphore(只有1个许可证可供使用),如果这个许可还没有被占用,当前线程获取许可并继续执行;如果许可已经被占用,当前线程阻塞,等待获取许可。

其实park/unpark的设计原理核心是“许可”。park是等待一个许可。unpark是为某线程提供一个许可。如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。

park 直译过来就是停车,这就好比过高速经过收费站,如果你没有通行证(permit=0),那么你就要停下来等着,不允许过收费站
unpark 的意思就是说我给你提供一个通行证(permit=1),然后park的车拿到这个通行证就可以顺利出收费站了

注意:凭证(permit)不会叠加,它最多就只有一个(这就是前面说的它相当于二元信号量的原因)

就算多次调用unpark()它也只会有一个凭证,比如线程B连续调用了三次unpark函数,当线程A调用park函数就使用掉这个“许可”,如果线程A再次调用park,则进入等待状态。

当调用park()方法时:

  1. 如果有凭证,则直接消耗掉这个凭证然后正常退出阻塞
  2. 如果没有凭证,则必须阻塞直到有凭证可用

而当调用unpark()方法时,它会给予一个凭证,但是最多只能有一个,不能累加。

当先调用unpark()时,它就会给它一个凭证,然后调用park()时直接使用掉即可。还是以高速公路过收费站为例,当先执行unpark()时,就表示我已经有了通行证,等到了收费站检查时(park)直接把通行证交给工作人员,然后我就可以出高速了。

3.2 LockSupport源码分析

public class LockSupport {
    private LockSupport() {} // Cannot be instantiated.

    private static void setBlocker(Thread t, Object arg) {
        // Even though volatile, hotspot doesn't need a write barrier here.
        UNSAFE.putObject(t, parkBlockerOffset, arg);
    }

    /**
     * 唤醒指定的线程(精准唤醒)
     * 注意:如果需要唤醒的线程尚未启动,则此操作不会有任何效果
     */
    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }
    
    /**
     * 使调用该方法的当前线程阻塞,永久阻塞,除非被唤醒或者被中断
     */
    public static void park() {
        UNSAFE.park(false, 0L);
    }

    /**
     * 使调用该方法的当前线程阻塞
     * blocker 参数相当于是一个阻塞原因,可以通过 getBlocker(Thread t) 获取
     * 注意:一旦阻塞的线程被唤醒,blocker信息将被清空
     */
    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    
    /**
     * 可以指定阻塞多长时间
     */
    public static void parkNanos(long nanos) {
        if (nanos > 0)
            UNSAFE.park(false, nanos);
    }

    /**
     * 可以指定阻塞到什么时候,是一个绝对时间,毫秒值
     *
     */
    public static void parkUntil(long deadline) {
        UNSAFE.park(true, deadline);
    }

    /**
      * 可以指定阻塞多长时间, 同时指定阻塞原因
     */
    public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            UNSAFE.park(false, nanos);
            setBlocker(t, null);
        }
    }

    /**
     * 可以指定阻塞到什么时候,是一个绝对时间, 同时可以指定阻塞原因
     */
    public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
    }

    /**
     * 获取blocker信息
     */
    public static Object getBlocker(Thread t) {
        if (t == null)
            throw new NullPointerException();
        return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
    }

    //......
}

3.3 测试Demo

3.3.1 设置阻塞原因

/**
 * @author qiuguan
 * @date 2022/12/02 23:22:15  星期五
 */
public class LockSupportTest {

    public static void main(String[] args) {

       Thread t1 = new Thread(() -> {

           System.out.println(Thread.currentThread().getName() + "线程累了,不想工作了......");

           LockSupport.park("t1线程累了,摆烂了");

           System.out.println(Thread.currentThread().getName() + "线程又爬起来打工了......");


       }, "t1");


       Thread t2 = new Thread(() -> {

           try {
               Thread.sleep(1000L);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           Object blocker = LockSupport.getBlocker(t1);
           System.out.println(blocker);
           
           System.out.println("t2线程准备打醒t1, 让它继续搬砖");
           LockSupport.unpark(t1);
       });

       t1.start();
       t2.start();
    }
}

3.3.2 多次unpark效果演示

/**
 * @author qiuguan
 * @date 2022/12/02 23:22:15  星期五
 */
public class LockSupportTest {

    public static void main(String[] args) {

       Thread t1 = new Thread(() -> {

           try {
               Thread.sleep(1000L);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           System.out.println(Thread.currentThread().getName() + "线程累了,不想工作了......");

           //t2率先给了凭证,所以直接消耗掉凭证直接退出阻塞
           LockSupport.park("t1线程累了,摆烂了");

           System.out.println(Thread.currentThread().getName() + "线程又爬起来打工了......");

           try {
               Thread.sleep(3000L);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           System.out.println("3s后t1线程又累了........");
           LockSupport.park();
           System.out.println("t1又起来搬砖了........");


       }, "t1");


       Thread t2 = new Thread(() -> {
           /**
            * 调用3次unpark(),但实际上凭证只有一个, t1第一次park直接消耗掉往下运行
            */
           LockSupport.unpark(t1);
           LockSupport.unpark(t1);
           LockSupport.unpark(t1);

           try {
               Thread.sleep(5000L);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }

           LockSupport.unpark(t1);
       });

       t1.start();
       t2.start();
    }
}

3.3.3 指定何时自动唤醒阻塞

/**
 * @author qiuguan
 * @date 2022/11/27 03:00:17  星期日
 *
 */
public class LockSupportTest {

    public static void main(String[] args) {

        LocalDateTime localDateTime = LocalDateTime.now().plusSeconds(10L);
        long end = localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli();

        int x = 0;
        for (;;) {
            System.out.println("---------> " + (++x));
            //10s后自动结束阻塞
            LockSupport.parkUntil(end);

            if (x == 5) {
                System.out.println("退出for循环.......");
                break;
            }
        }

        System.out.println("end.........");
    }
}

4.总结

  1. LockSupport 可以完成线程的阻塞和唤醒功能,其原理的核心就是许可
  2. LockSupport 可以先唤醒后阻塞,这是因为唤醒相当于分发凭证,而阻塞就是消耗凭证,所以可以先唤醒后阻塞,也就是先给通行证,这样遇到关卡就直接放行。
  3. 凭证不能累加,只有一个,多次调用unpark()也只会有一个凭证

和Object的wait()/notify()差异比较
共同点

  1. LockSupport中的park方法和Object中的wait方法都可以使线程进入WAIT或者TIMED_WAIT状态
  2. LockSupport中的unpark方法和Object中的notify可以使线程脱离WAIT或者TIMED_WAIT状态
  3. 二者都可以通过调用线程的interrupt方法终止等待状态

不同点

  1. Object中的wait方法必须在同步代码块中使用,否则会抛出IllegalMonitorException;而LockSupport无需加锁,直接调用静态方法park就可以使当前线程进入阻塞状态。
  2. Objectwaitnotify方法必须要按顺序调用,如果因为线程调度问题导致线程A先调用notify方法而线程B后调用wait方法,那么会使线程A永远处于WAIT状态。对于LockSupport而言则没有这种限制,如果有线程A首先调用了unpark方法并传入了线程B的引用,然后线程B再调用了park方法,那么线程B是不会进入等待状态的。
  3. 调用Objectwait方法后,可以调用该线程的interrupt方法脱离等待状态并捕获InterruptedException。而LockSupport的并不能捕获InterruptedException

关于中断我们演示下:

LockSupport 中断测试

/**
 * @author qiuguan
 * @date 2022/12/02 23:22:15  星期五
 */
public class LockSupportTest {

    public static void main(String[] args) {

       Thread t1 = new Thread(() -> {
           System.out.println(Thread.currentThread().getName() + "线程累了,不想工作了......");

           //t2率先给了凭证,所以直接消耗掉凭证直接退出阻塞
           LockSupport.park("t1线程累了,摆烂了");

           System.out.println(Thread.currentThread().getName() + "线程又爬起来打工了......");

       }, "t1");


       t1.start();

        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //t1线程中断
        t1.interrupt();
    }
}

中断后直接停止阻塞,继续往下执行,并且不会捕获中断异常。

Object的wait()中断演示

/**
 * @author qiuguan
 * @date 2022/12/03 02:59:41  星期六
 */
public class ObjectWaitNotifyTest {

    private static Object object = new Object();

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            synchronized (object) {
                try {
                    System.out.println(Thread.currentThread().getName() + "线程累了,不想搬砖了......");
                    object.wait();
                    System.out.println(Thread.currentThread().getName() + "线程又爬起来搬砖了......");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1");

        t.start();

        try {
            Thread.sleep(3000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t.interrupt();
    }
}

image.png

中断后停止阻塞,并捕获中断异常。

好了,关于LockSupport的介绍就到这里吧。

限于作者水平,文中难免有错误之处,欢迎指正,勿喷,感谢感谢