# 接口设计

在讲解操作系统篇的时候,很多 c 语言的伪代码,并没有什么 synchronized 关键字什么的,其上锁 / 解锁的步骤就是 lock/unlock ,方法,等待 / 唤醒也就是 wait/signal ,在 j.u.c 包中也就提供了这样的 API

# Lock

public interface Lock {
  	// 获取锁,拿不到锁会阻塞,等待其他线程释放锁,获取到锁后返回
    void lock();
  	// 同上,但是等待过程中会响应中断
    void lockInterruptibly() throws InterruptedException;
  	// 尝试获取锁,但是不会阻塞,如果能获取到会返回 true,不能返回 false
    boolean tryLock();
  	// 尝试获取锁,但是可以限定超时时间,如果超出时间还没拿到锁返回 false,否则返回 true,可以响应中断
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  	// 释放锁
    void unlock();
  	// 暂时可以理解为替代传统的 Object 的 wait ()、notify () 等操作的工具
    Condition newCondition();
}

j.u.c 同样提供了 Lock 的实现类,比如 ReentrantLock ,甚至你可以使用 AQS 框架自己定义一把锁。

示例:

public class Main {
    private static int i = 0;
    public static void main(String[] args) throws InterruptedException {
        Lock testLock = new ReentrantLock();  
        Runnable action = () -> {
            for (int j = 0; j < 100000; j++) { 
                testLock.lock();    // 加锁,加锁成功后其他线程如果也要获取锁,会阻塞,等待当前线程释放
                i++;
                testLock.unlock();  // 解锁,释放锁之后其他线程就可以获取这把锁了(注意在这之前一定得加锁,不然报错)
            }
        };
        new Thread(action).start();
        new Thread(action).start();
        Thread.sleep(1000);   // 等上面两个线程跑完
        System.out.println(i);
    }
}

# Condition

public interface Condition {
  	// 进入到等待状态,但是这里需要调用 Condition 的 signal 或 signalAll 方法进行唤醒,等待状态下是可以响应中断的
 	void await() throws InterruptedException;
  	// 同上,但不响应中断(看名字都能猜到)
  	void awaitUninterruptibly();
  	// 等待指定时间,如果在指定时间(纳秒)内被唤醒,会返回剩余时间,如果超时,会返回 0 或负数,可以响应中断
  	long awaitNanos(long nanosTimeout) throws InterruptedException;
  	// 等待指定时间(可以指定时间单位),如果等待时间内被唤醒,返回 true,否则返回 false,可以响应中断
  	boolean await(long time, TimeUnit unit) throws InterruptedException;
  	// 可以指定一个明确的时间点,如果在时间点之前被唤醒,返回 true,否则返回 false,可以响应中断
  	boolean awaitUntil(Date deadline) throws InterruptedException;
  	// 唤醒一个处于等待状态的线程,注意还得获得锁才能接着运行
  	void signal();
  	// 同上,但是是唤醒所有等待线程
  	void signalAll();
}

Condition 对象通过 Lock 接口提供的 newCondition 方法获得,也就是的每个 Condition 对象都对应特定地 Lock 对象。

示例:

public static void main(String[] args) throws InterruptedException {
    Lock testLock = new ReentrantLock();
    Condition condition = testLock.newCondition();
    new Thread(() -> {
        testLock.lock();   // 和 synchronized 一样,必须持有锁的情况下才能使用 await
        System.out.println("线程1进入等待状态!");
        try {
            condition.await();   // 进入等待状态
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程1等待结束!");
        testLock.unlock();
    }).start();
    Thread.sleep(100); // 防止线程 2 先跑
    new Thread(() -> {
        testLock.lock();
        System.out.println("线程2开始唤醒其他等待线程");
        condition.signal();   // 唤醒线程 1,但是此时线程 1 还必须要拿到锁才能继续运行
        System.out.println("线程2结束");
        testLock.unlock();   // 这里释放锁之后,线程 1 就可以拿到锁继续运行了
    }).start();
}

# TimeUnit

这里顺便提一下 TimeUnit 工具类,因为 Condition 可以限时等待,所以可以使用 TimeUnit 来确定时间单位。

  • condition.await(3,TimeUnit.SECONDS) 限时等待 3 秒。
  • 支持时间转换: TimeUnit.SECONDS.toMinutes(60)
  • 直接调用等待方法:
public static void main(String[] args) throws InterruptedException {
    synchronized (Main.class) {
        System.out.println("开始等待");
        TimeUnit.SECONDS.timedWait(Main.class, 3);   // 直接等待 3 秒
        System.out.println("等待结束");
    }
}

在源码中,本质上还是调用 obj.wait

  • 直接休眠: TimeUnit.SECONDS.sleep(1)

# LockSupport

LockSupport 提供了 park/unpark 方法用于上锁和解锁, ReentrantLock 的实现就使用了 LockSupport

# park 函数

有两个重载版本

public static void park()
public static void park(Object blocker) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 设置 Blocker
    setBlocker(t, blocker);
    // 获取许可
    UNSAFE.park(false, 0L);
    // 重新可运行后再此设置 Blocker
    setBlocker(t, null);
}

Thread 类有一个变量为 parkBlocker ,对应的就是 LockSupportpark 等方法设置进去的阻塞对象。该参数主要用于问题排查和系统监控,在线程 dump 中会显示该参数的信息,有利于问题定位。

调用了 park 函数后,会禁用当前线程,除非许可可用。在以下三种情况之一发生之前,当前线程都将处于休眠状态,即下列情况发生时,当前线程会获取许可,可以继续运行:

  • 其他某个线程将当前线程作为目标调用 unpark
  • 其他某个线程中断当前线程,即 t.interrupt()
  • 该调用不合逻辑地 (即毫无理由地) 返回。

还有两个关于限时禁用的函数:

  • parkNanos(long nanos) 最多等待指定的等待时间
  • parkUntil(long deadline) 最多等待到指定时间, deadline 是绝对时间。

# unpark 函数

如果线程在 park 上受阻塞,则它将解除其阻塞状态。否则,保证下一次调用 park 不会受阻塞。如果给定线程尚未启动,则无法保证此操作有任何效果。

public static void unpark(Thread thread) {
    if (thread != null) // 线程为不空
        UNSAFE.unpark(thread); // 释放该线程许可
}

使用示例:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread.currentThread().setName("main线程");
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println("线程开始运行...");
                LockSupport.park();// 线程进入阻塞
                System.out.println("线程阻塞结束...");
            }
        };
        t.setName("t线程");
        t.start();
        TimeUnit.SECONDS.sleep(2);// 主线程先休眠两秒,保证另一个线程一定先执行 LockSupport.park ()
        LockSupport.unpark(t);
    }
}

main 函数中的 LockSupport.unpark() 一行代码打断点,选择下栏的 Debug->Layout setting->Frames 查看 t 线程的状态(WAIT)

image-20220724125734871

# 深入理解

park ()/unpark () 底层的原理是 “二元信号量”,你可以把它相像成只有一个许可证的 Semaphore,只不过这个信号量在重复执行 unpark () 的时候也不会再增加许可证,最多只有一个许可证。

LockSuport.park() 只是单独阻塞当前线程,可以被中断或者被其他线程唤醒,因为不存在获取锁,所以 unpark 也就不释放锁, Condition.await() 底层调用了 LockSupport.park (以 AQS 为例),实际上,他在阻塞线程前还干了两件事,一是把当前线程添加到条件队列中,二是 “完全” 释放锁,也就是让 state 状态变量变为 0,然后才是调用 LockSupport.park() 阻塞当前线程。

至于 parksleepobject.wait 的区别,我觉得比较简单,而且要写的比较多,就不赘述了。

# 参考

https://www.yuque.com/qingkongxiaguang/javase/cts2gq#a75b92b6

https://pdai.tech/md/java/thread/java-thread-x-lock-LockSupport.html

https://www.cnblogs.com/seve/p/14555740.html