JUC锁——Condition条件

博客介绍了Condition对锁进行更精确控制的作用,对比了其与Object相关方法在锁使用上的不同。通过多个示例展示了Condition在多线程阻塞与唤醒中的应用,强调其能更精细控制多线程,还探讨了signal()和signalAll()的使用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Condition介绍

  Condition的作用是对锁进行更精确的控制,使锁的粒度更细小。Condition中的await()方法相当于Object中的wait()方法,Condition中的signal()方法相当于Object中的notify()方法,Condition中的signalAll()相当于Object中的notifyAll()方法。不同的是,Object中的wait()、notify()、notifyAll()方法是和"同步锁"(synchronized关键字)捆绑使用的,而Condition是与"互斥锁"或"共享锁"捆绑使用的。

Condition方法列表
// 造成当前线程在接到信号或被中断之前一直处于等待状态
void await()
// 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态
boolean await(long time, TimeUnit unit)
// 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态
long awaitNanos(long nanosTimeout)
// 造成当前线程在接到信号之前一直处于等待状态
void awaitUninterruptibly()
// 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态
boolean awaitUntil(Date deadline)
// 唤醒一个等待线程
void signal()
// 唤醒所有等待线程
void signalAll()
Condition示例

示例1:

public class WaitTest1 {

	public static void main(String args[]) {
		ThreadA ta = new ThreadA("ta");
		synchronized (ta) {
			try {
				System.out.println(Thread.currentThread().getName() + " start ta.");
				ta.start();
				Thread.sleep(1000);
				System.out.println(Thread.currentThread().getName() + " block.");
				ta.wait();
				System.out.println(Thread.currentThread().getName() + " continue.");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	static class ThreadA extends Thread {
		public ThreadA(String name) {
			super(name);
		}

		public void run() {
			synchronized (this) {
				System.out.println(Thread.currentThread().getName() + " wake up others.");
				notify();
			}
		}
	}
}

// 结果
main start ta.
main block.//1秒后打印
ta wake up others.
main continue.

分析
①主线程和子线程使用的锁是同一个对象,都是ta
②先进入主线程的main方法的同步代码块,即主线程先获取到锁对象,打印出:main start ta.
③紧接着,在主线程中开启子线程,但是此时主线程没有释放锁,子线程同步代码块中的代码得不到执行,1秒钟之后主线程打印出:main block.
④紧接着主线程中调用ta.wait();释放锁,主线程进入阻塞状态
⑤子线程等待的锁得到释放,子线程可以获取到锁了,子线程获取到锁之后,打印出:ta wake up others.
⑥紧接着子线程调用了notify(),其实是this.notify(),也就是会唤醒持有当前对象作为锁的线程,而主线程恰好正是持有该对象作为锁的阻塞线程,即主线程被唤醒,接着打印出:main continue.

示例2:

public class ConditionTest1 {

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

	public static void main(String args[]) {
		ThreadA ta = new ThreadA("ta");
		lock.lock();
		try {
			System.out.println(Thread.currentThread().getName() + " start ta.");
			ta.start();
			Thread.sleep(1000);
			System.out.println(Thread.currentThread().getName() + " block.");
			condition.await();
			System.out.println(Thread.currentThread().getName() + " continue.");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	static class ThreadA extends Thread {
		public ThreadA(String name) {
			super(name);
		}

		public void run() {
			lock.lock();
			try {
				System.out.println(Thread.currentThread().getName() + " wake up others.");
				condition.signal();
			} finally {
				lock.unlock();
			}
		}
	}
}

// 结果
main start ta.
main block.
ta wake up others.
main continue.

分析:示例2和示例1的流程大致相同,只不过示例1使用的是对象的同步锁而示例2使用的是ReentrantLock,通过Condition实现对持有ReentrantLock锁的线程进行阻塞和唤醒
示例1与示例2对比

              Object      Condition  
阻塞          wait        await
唤醒某线程     notify      signal
唤醒所有线程   notifyAll   signalAll

  但是,Condition除了支持上面的功能之外,它更强大的地方在于:能够更加精细的控制多线程的阻塞与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,“读线程"需要等待。如果采用Object类中的wait(), notify(), notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程”,而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。 但是,通过Condition,就能明确的指定唤醒读线程。使用哪个Condition对象阻塞线程就使用哪个Conditiond对象唤醒该线程。看看下面的示例3,可能对这个概念有更深刻的理解:

class BoundedBuffer {
	final Lock lock = new ReentrantLock();
	final Condition notFull = lock.newCondition();
	final Condition notEmpty = lock.newCondition();

	// 缓冲区容量:5
	final Object[] items = new Object[5];
	int putptr, takeptr, count;// count记录实际容量

	// 存放
	public void put(Object x) throws InterruptedException {
		lock.lock();
		try {
			while (count == items.length)
				notFull.await();// 满了则使当前持有lock锁的存放线程等待,可理解为此时存放线程被notFull这个Condition对象阻塞
			items[putptr] = x;// 在下一个位置存放对象x
			if (++putptr == items.length)// 使putptr+1,保证下次存放时该位置不被下一个对象覆盖
				putptr = 0;// 若已存满则置为0,防止下标越界
			++count;
			notEmpty.signal();// 唤醒使用notEmpty这个Condition对象阻塞的线程,即取用线程
			System.out.println(Thread.currentThread().getName() + " put  " + (Integer) x);
		} finally {
			lock.unlock();
		}
	}

	public Object take() throws InterruptedException {
		lock.lock();
		try {
			while (count == 0)
				notEmpty.await();
			Object x = items[takeptr];
			if (++takeptr == items.length)
				takeptr = 0;
			--count;
			notFull.signal();
			System.out.println(Thread.currentThread().getName() + " take " + (Integer) x);
			return x;
		} finally {
			lock.unlock();
		}
	}
}

public class ConditionTest2 {
	private static BoundedBuffer bb = new BoundedBuffer();

	public static void main(String args[]) {
		// 启动10个“写线程”,向BoundedBuffer中不断的写数据(写入0-9)
		// 启动10个“读线程”,从BoundedBuffer中不断的读数据
		for (int i = 0; i < 10; i++) {
			new PutThread("p" + i, i).start();
			new TakeThread("t" + i).start();
		}
	}

	static class PutThread extends Thread {
		private int num;

		public PutThread(String name, int num) {
			super(name);
			this.num = num;
		}

		public void run() {
			try {
				Thread.sleep(1); // 线程休眠1ms
				bb.put(num); // 向BoundedBuffer中写入数据
			} catch (InterruptedException e) {
			}
		}
	}

	static class TakeThread extends Thread {
		public TakeThread(String name) {
			super(name);
		}

		public void run() {
			try {
				Thread.sleep(10); // 线程休眠1ms
				bb.take(); // 从BoundedBuffer中取出数据
			} catch (InterruptedException e) {
			}
		}
	}
}

// 结果
p2 put  2
p0 put  0
p7 put  7
p5 put  5
p4 put  4
t0 take 2
p3 put  3
t4 take 0
p1 put  1
t3 take 7
p8 put  8
t1 take 5
p9 put  9
t2 take 4
p6 put  6
t7 take 3
t8 take 1
t9 take 8
t5 take 9
t6 take 6

说明
 ①BoundedBuffer 是容量为5的缓冲区,缓冲区中存储的是Object对象,支持多线程的读/写。多个线程操作“一个BoundedBuffer对象”时,它们通过互斥锁lock对缓冲区进行互斥访问;而且同一个BoundedBuffer对象下的全部线程共用“notFull”和“notEmpty”这两个Condition。notFull用于控制写缓冲,notEmpty用于控制读缓冲。当缓冲已满的时候,调用put的线程会执行notFull.await()进行等待;当缓冲区不是满的状态时,就将对象添加到缓冲区并将缓冲区的容量count+1。最后,调用notEmpty.signal()唤醒notEmpty上的等待线程(调用notEmpty.await的线程)。 简言之,notFull控制“缓冲区的写入”,当往缓冲区写入数据之后会唤醒notEmpty上的等待线程;同理,notEmpty控制“缓冲区的读取”,当读取了缓冲区数据之后会唤醒notFull上的等待线程。
 ②在ConditionTest2的main函数中,启动10个“写线程”,向BoundedBuffer中不断的写数据(写入0-9);同时,也启动10个“读线程”,从BoundedBuffer中不断的读数据。

分析
  首先,打印出的结果是不确定的,因为这20个线程的执行存在一定的随机性。在for循环中不断的开启新的线程,这些线程在调用put()或take()时又是使用的同一个锁——lock,那么在一个时间片内只能有一个线程的put()或take()方法得以执行,在该线程的put()或take()方法释放锁之前其他线程都会被阻塞。开始时,缓冲区不满,锁是通过put()方法执行结束在finally块中释放的,随着缓冲区的实际容量增加,缓冲区可能会满,这时就会通过condition的await()方法释放锁;take()方法也是如此。在put()中使用await()释放锁之后下一个获取锁的线程可能还是一个put线程,这样就导致有多个put线程因为await()而阻塞,最多会有5个被阻塞的线程。

疑问:在示例3中最多会有5个线程被阻塞,为什么不用signalAll()而使用signal()唤醒线程呢?
  我的理解是使用signalAll()和signal()都可以,达到的效果是一样的。因为阻塞的线程使用的是同一把锁,即使使用signalAll()将所有的线程都唤醒也只会使其中的一个线程运行。果真如此的话,那使用同一个condition的await()阻塞的线程使用的锁必然是同一把锁,在使用condition的唤醒方法唤醒这些阻塞线程后又只会使其中的一个线程运行,那么当这些线程执行的代码相同的时候唤醒所有的线程也就没有必要了,此时使用signal()去唤醒即可。那什么情况下使用signalAll()呢?应该是在使用同一个condition对象的await()阻塞的线程,但是这些线程执行的是不同的代码(线程执行不同的任务),而且这些线程的执行顺序没有要求的时候,可以使用signalAll()去唤醒这些线程。

最重要的是:使用Condition是可以具体指明阻塞和唤醒哪个线程的

简单点理解的话:Lock相当于家里大门的锁,而Condition相当于家里每个房间小门的锁。

在Java的JUC(java.util.concurrent)包中,机制是并发编程的核心组件之一。它提供了多种实现来满足不同的线程同步需求,包括但不限于`ReentrantLock`、`ReadWriteLock`等。这些通过提供比内置`synchronized`更灵活和强大的功能,帮助开发者更好地控制多线程环境下的资源访问。 ### ReentrantLock `ReentrantLock`是一种可重入的互斥,支持尝试非阻塞获取、超时获取以及公平策略[^1]。与`synchronized`关键字相比,`ReentrantLock`允许更细粒度地控制的行为,并且可以跨方法调用保持的所有权。 #### 使用示例 ```java import java.util.concurrent.locks.ReentrantLock; public class LockExample { private final ReentrantLock lock = new ReentrantLock(); public void performTask() { lock.lock(); // 尝试获取 try { System.out.println("当前线程" + Thread.currentThread().getName() + "获得,进行异常操作"); int i = 1 / 0; // 模拟异常情况 } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); // 在finally块中确保被释放 } } } ``` ### ReadWriteLock `ReadWriteLock`接口定义了一种策略,允许多个读取者同时访问共享资源,但写入者独占访问权。这种设计非常适合于那些读多写少的应用场景,因为它能显著提高并发性能。`ReentrantReadWriteLock`是该接口的一个具体实现。 #### 使用示例 ```java import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockExample { private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); public void readData() { rwLock.readLock().lock(); // 获取读 try { // 执行读取操作 } finally { rwLock.readLock().unlock(); // 确保总是释放读 } } public void writeData() { rwLock.writeLock().lock(); // 获取写 try { // 执行写入操作 } finally { rwLock.writeLock().unlock(); // 确保总是释放写 } } } ``` ### 乐观 vs 悲观 - **悲观**假设最坏的情况,在整个数据处理过程中都会定数据。这通常会导致较高的开销,因为其他线程必须等待直到被释放。 - **乐观**则假设最好的情况,即认为数据不会频繁发生冲突,因此只有在提交更新时才会检查是否有冲突发生。如果检测到冲突,则根据具体的实现方式采取相应的措施,如重试或抛出异常。 在Java中,乐观可以通过使用`AtomicInteger`等原子类中的CAS操作来实现[^2]。 ### 分离技术 对于像`ConcurrentHashMap`这样的集合类,JDK 1.8之后采用了更为高效的分离技术代替了早期版本中的分段机制。这种方法减少了的竞争,提高了程序的整体吞吐量[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值