Java中常见的等待唤醒机制及实践

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实现的等待唤醒机制可以实现条件等待,也就是等待获取锁的线程,需要先达到某种条件被唤醒(从条件队列移到等待队列)才能参与获取锁,这样可以实现更加精细化的线程控制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值