两个线程之间的互斥比较简单,有时候线程之间还需要同步:一个线程必须等待另一个/多个线程完成才能开始工作。
比如,一个打印的工作线程,操作系统给其分配一个循环队列(假设大小为5),旺财线程和小强线程两个家伙会向这个队列尾部添加文档;而我这个打印线程要从同一个队列的头部读取文件并打印,打印以后我就把这个文档删掉(见图1)。
如果队列是空的,那打印线程读取文档的时候读不到,这时就需要等待旺财线程或者小强线程把文档放进来。
如果队列是满的,那旺财/小强就必须等待打印线程删除一个文档,腾出位置来。这个时候互相等待就出现了,互斥锁就搞不定这个问题了。
荷兰有一个叫Dijkstra的人,发明了一个叫信号量(Semaphore)的东西,能解决这个问题。
所谓信号量,其实就是一个整数,基于这个整数有两个操作: wait和signal。
“这是啥玩意?这么简单能解决问题?再说了,这个s++、s--,在多线程切换下连自身的正确性都难保,还能解决别人的问题?” 旺财吃惊了。
旺财问得好,说明思考了。实际上,这个东西必须操作系统亲自来实现,操作系统会在内核中实现wait和signal,让线程们调用。比如操作系统在做s++、s--时,可以屏蔽中断,不让程序进行切换,这样就可以保证其原子性了。
小强线程发现一个问题,那个wait()函数在s小于等于0的时候啥也不做,一直在循环,非常浪费CPU资源。操作系统称之为“忙等待”,需要改进一下。
假设那个value值是2,旺财和小强都调用了wait()函数,都成功了,value值变成了0.如果另一个线程再去调用wait()函数,value值就会变成-1,它会进入阻塞状态,并且加入等待队列。如果旺财或小强线程调用了signal()函数,就会把value值变成0(有线程在等待),于是就把我这个线程唤醒了。
那么两个线程的消费者和生产者的同步问题怎么解决?操作系统用信号量解决消费、生产者的同步问题。
两个生产者线程假设都开始执行生产者代码,先去执行wait(empty),发现没有问题,因为empty为初始值5。接下来都去执行wait(lock),这时候就看谁先抢到了。如果旺财线程先抢到,旺财线程就可以往队列里添加文件,然后释放锁,小强线程就可以接着添加文件了。最后旺财线程还要把full的值加1,目的是通知消费者,因为它可能在等待。
多线程情况下,由于线程执行随时都有可能被打断,还要保证正确性,所以不能有任何闪失。这对程序员的挑战很大,如果出现了疏漏,则很难定位。
一般来说,程序员直接使用wait、signal编程,容易出错。Java JDK会进行抽象和封装,对于生产者-消费者问题,可以直接使用BlockingQueue,非常简单,完全不用考虑这些wait、signal、full、empty。