Java并发——条件变量

1.定义

在操作系统——多线程同步互斥一文中已详细介绍过了条件变量及其使用注意事项。条件变量,是为了解决等待同步需求,实现线程间协作通信的一种机制。条件变量用来自动阻塞一个线程,直到某特殊情况发生唤醒该线程为止。通常条件变量和锁机制同时使用。

2.Java中的条件变量

2.1 synchronized + wait()、notify()、notifyAll()

线程A(消费者):
synchronized(obj){
	while(cond is false){
		obj.wait(); //不满足条件,加入等待队列,自动释放锁
	}
	consume();
	alter cond;
}
线程B(生产者):
synchronized(obj){
	produce();
	alter cond;
	if(cond is true){
		obj.notify();
	}
}

为什么wait()和notify()必须放在synchronized里面?
其实这已经在“操作系统——多线程同步互斥” 条件变量那一块说过。
如果A在判断part不满足条件后,此时可能由于时间片轮换造成消费者还没来得及执行wait()操作,导致wait队列中并不存在此线程,恰巧在此时切换到了某个生产者线程中,并且恰好满足了条件,发出了cond信号,此时os便在wait队列中查找等待中的线程,但是发现并没有等待线程,所以这个cond信号便丢失了,造成后面切换到消费者线程,继续执行到wait()时,已经错过了这个信号,导致消费者丢失了此次信号,处于阻塞态。
正确做法是当线程切换导致消费者来不及进入wait队列时,其它程序也不可以操作该共享变量,既保证消费者wait()和条件判断是一个原子操作,而最常见的做法便是在条件判断到wait()的这一段代码加一个互斥锁进行保护,另外生产者操作共享资源时,必须先获得锁才行。

2.2 重入锁+Condition对象(await()、signal()、signalAll())

在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的通信方式,Condition都可以实现,这里注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。 这样看来,Condition和传统的线程通信没什么区别,Condition的强大之处在于它可以为多个线程间建立不同的Condition。

class BoundedBuffer {
   final Lock lock = new ReentrantLock();//锁对象
   final Condition notFull  = lock.newCondition();//写线程条件 
   final Condition notEmpty = lock.newCondition();//读线程条件 

   final Object[] items = new Object[100];//缓存队列
   int putptr/*写索引*/, takeptr/*读索引*/, count/*队列中存在的数据个数*/;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length)//如果队列满了 
         notFull.await();//阻塞写线程
       items[putptr] = x;//赋值 
       if (++putptr == items.length) putptr = 0;//如果写索引写到队列的最后一个位置了,那么置为0
       ++count;//个数++
       notEmpty.signal();//唤醒读线程
     } 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;//如果读索引读到队列的最后一个位置了,那么置为0
       --count;//个数--
       notFull.signal();//唤醒写线程
       return x;
     } finally {
       lock.unlock();
     }
   } 
 }

我们可以看到Condition这种方式是和重入锁搭配使用的,与前一种不同之处在于,前一种的synchronized块会在一开始就加锁,结束时解锁,不需要显示加锁,解锁;而后一种重入锁需要显示的加锁和解锁。
而这正是重入锁和synchronized的最大区别,所以造成了synchronized太过粗糙,无法做到锁的细粒度控制,而重入锁可以更细粒度更灵活的控制程序。

3.Java中的另一种协作机制

JDK提供了join()、yield()两个线程间的协作函数。

  1. t.join() 方法阻塞调用此方法的线程(calling thread)进入 TIMED_WAITING 状态,直到线程t完成,才会发出 t.notifyAll() 通知让所有等待线程再继续;通常用于在main()主线程内,等待其它线程完成再结束main()主线程。
    从上面可看出,t.join() 方法会默认加锁和解锁(因为其本质是通过 wait() 和 notifyAll() 实现的),由于t.notifyAll() 会通知所有等待线程继续执行,所以不要在Thread对象实例上使用wait()、notify()等方法,因为可能会影响系统api或被其影响。

    public class TestJoin {
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(new JoinThread());;
            Thread thread2 = new Thread(new JoinThread());
            
            thread1.start();
            thread1.join();//线程1加入,表示我要执行完,其余线程(线程2)才可以继续执行
            thread2.start();
            
        }
        
        static class JoinThread implements Runnable {
    
            @Override
            public void run() {
                for(int i=0; i<100; i++) {
                    try {
                        System.out.println(Thread.currentThread().getName());
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    
  2. yield()方法恰好与join()相反,表示我可以让出cpu等待其余线程先执行,但是就算让出了,其还是会争夺cpu资源的,只是不一定被分配到而已。
    所以如果某个线程优先级低,不那么重要,而且害怕它会占用太多cpu时间,就可以调用Thread.yiled()——静态方法.

发布了69 篇原创文章 · 获赞 10 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/JustKian/article/details/103701995