JDK自带的等待唤醒机制
在Java中,有一个JDK维度的等待唤醒机制。Object类的wait和notify,notifyAll
需要在synchronized同步代码块内并且对象必须获取到锁才能调用。否则会抛IllegalMonitorStateException异常。
当线程在尝试获取锁时失败,会被封装成节点(Node)并加入到等待队列中,进行同步等待。
等待队列底层是一条维护着头尾指针的Node节点链表。
Object obj=new Object();
synchronized (obj){
System.out.println(Thread.currentThread().getId()+"开始");
obj.wait(3000);
System.out.println(Thread.currentThread().getId()+"结束");
}
如上,obj是synchronized代码块的锁对象,在同步代码块内的线程必定获取到了obj对象的对象锁,因而可以调用obj的等待唤醒方法。
调用obj.wati(),当前线程会释放obj对象的锁,并进入obj对象的等待队列,等待被其他线程调用obj对象的notify或notifyAll唤醒。
public class NotifyTest {
public static void main(String[] args) {
Object obj=new Object();
new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (obj){
System.out.println(Thread.currentThread().getId()+"开始"+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
obj.wait(5000);
System.out.println(Thread.currentThread().getId()+"结束"+new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (obj) {
System.out.println(Thread.currentThread().getId() + "开始了"+new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
Thread.sleep(1000);
obj.notifyAll();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getId() + "结束了"+new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
如上图,线程12先获取锁,并执行打印,然后调用wait方法释放obj的锁并进入obj等待队列,线程13也启动并执行打印,然后调用notifyAll方法唤醒线程12,但是调用notifyAll方法只是会唤醒线程12,并不会立刻释放锁(线程13唤醒线程12之后睡了1秒),所以是先执行完线程13的打印,释放obj锁,然后线程12再获取锁并打印后续。
Lock的等待唤醒机制
条件队列的前置条件是获取到Lock接口和Condition接口
需要创建Lock对象,然后调用newCondition方法创建Condition对象。这个Condition对象就是一个条件队列,底层是一条维护着头尾指针的链表。条件队列的具体条件逻辑需要用业务代码去实现。
一个Lock对象可以创建多个Condition对象,也就是说一个Lock对象可以有多个条件队列,并且不同条件队列之间互不影响。因而可以实现更细维度的线程睡眠唤醒机制。
如ReentrantLock类的newCondition方法每次都会返回一个新的Condition对象,可以用以区分不同的等待条件。
public class NotifyTest {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getId() + "条件1开始" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
lock.lock();
System.out.println(Thread.currentThread().getId() + "条件1获取到锁" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
condition1.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getId() + "条件1结束" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
lock.unlock();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getId() + "条件2开始" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
lock.lock();
System.out.println(Thread.currentThread().getId() + "条件2获取到锁" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
condition2.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getId() + "条件2结束" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
lock.unlock();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getId() + "开始" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
//先睡一秒,确保上面启动的两个线程先后获取锁并阻塞
Thread.sleep(1000);
lock.lock();
System.out.println(Thread.currentThread().getId() + "获取到锁" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
condition2.signal();
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getId() + "结束" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
lock.unlock();
}
}
}).start();
}
}
如上图,三个线程先后启动,唤醒的线程先睡眠一秒,防止和条件1条件2线程抢锁。条件1和条件2按照先后顺序拿到锁,分别调用不同的条件对象阻塞等待条件。接着唤醒线程睡眠一秒之后唤醒,并获取锁,打印,然后唤醒等待条件2的线程,执行完毕释放锁。锁释放之后,条件2的线程检测到锁可用,就获取锁,并继续执行。而条件1的线程因为没有被唤醒,一直在阻塞等待唤醒条件。
可以通过业务代码去限制不同的线程所需的唤醒条件,更精细维度的锁控制。
结论
-
JDK自带的等待唤醒机制实现简单,所有等待的线程都阻塞在锁对象上,所有等待的线程都参与获取锁。
-
Lock实现的等待唤醒机制可以实现条件等待,也就是等待获取锁的线程,需要先达到某种条件被唤醒(从条件队列移到等待队列)才能参与获取锁,这样可以实现更加精细化的线程控制。