Java多线程之读者写者问题
读者写者问题也是经典的多线程问题,在实际生产应用中也是存在必要的意义的。
操作系统原理之进程和线程管理
Java的多线程和并发知识纲要
Java多线程之生产者和消费者模式
Java多线程之哲学家就餐问题
问题描述
有读者和写者两组并发进程,共享一个文件。当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。
因此要求:
①允许多个读者可以同时对文件执行读操作;
②只允许一个写者往文件中写信息;
③任一写者在完成写操作之前不允许其他读者或写者工作;
④写者执行写操作前,应让已有的读者和写者全部退出。
问题分析
分析问题容易看出,读者和写者是互斥的,写者和写者也是互斥的,而读者和读者之间不存在互斥问题。
当读者和写者都存在时,就会出现优先级问题,到底是读的优先还是写的人优先。
因此,这个问题可以分为读者优先、弱写者优先(公平竞争)、强写者优先三种情况。
对每一种情况要具体问题具体分析,找出每种情况下的访问规则。
我的解决
不论当前的状态如何,任意写者进程与其他任何进程都互斥,用互斥信号量的PV操作即可。
读者的问题比较复杂,它必须实现与写者互斥的同时还要实现与其他读者的同步。
因此,仅仅简单的一对PV操作是无法解决的。
这里可以使用一个计数器count,用它来判断当前是否有读者读文件。
当有读者的时候写者是无法写文件的,此时读者会一直占用文件;当没有读者的时候写者才可以写文件。
同时不同的读者对该计数器的访问也应该是互斥的。
这里我假设了以下的情景:
写者进程针对share.txt不断进行写操作,读者进程则不断读取share.txt中的内容,并写到另一个文件中。
从而实现文件的实时拷贝,可以用于的场景比如:
写服务器不断的去记录log到同一个文件,然后其他服务器不断的去抓取这个log文件的内容拷贝到自己本地。
公共类和方法
读者的抽象类:
1 | package com.chain.test.day05; |
写者的抽象类:
1 | package com.chain.test.day05; |
读者写者规则的抽象类:
1 | package com.chain.test.day05; |
主测试方法、Reader、Writer、读写规则、自定义信号量等的具体实现可以参看文末的源码链接。
读者优先
test.properties:
1 | readers.num=5 |
1 | package com.chain.test.day05; |
在该算法中,读者进程是优先的,也就是说,当存在读者进程时,写者操作将被推后。而且只要有一个读者进程活跃(在读文件),随后而来的读者进程也是允许读取文件的。这样的方式下,可能会导致写者进程长时间等待,即存在写者进程“饿死”的情况。
如果读者和写者的数量都为1时,可以看到两者交替进行,比较融洽:
如果读者数量为1,写者数量为多个(2个),可以看到写者不会同时写,读者和写者也是互斥的,工作也是正常的:
如果读者数量较少(2个),写者数量适中,可以看到大部分都是读者进程在执行,但是写者进程还是有机会进行写操作的:
但是如果读者数量较多(5个),可以看到写者进程进行写操作的机会很少,基本是读者进程在进行读操作:
程序正常结束运行后检查各个文件的信息,可以看到信息一致。
修改方法中的信号量为JavaAPI信号量也是可以的,运行效果一致。
(强)写者优先
要让写者能优先得到执行的机会,可以参照读者优先中read如何让读者优先的部分,对照设计写者write部分。
具体做法:
为了能做到写者优先,需要对读者部分增加一个队列信号量q,由写者进程控制这个队列信号量q。
每当有一个写进程在执行写操作时,队列就+1;直到队列为0,读进程才能进行读操作。
换句话说,只要有一个写进程得到了队列信号量q,那么之后就会一直占着q不放,直到所有的写进程都完成了写操作后才会释放q,这样的做法就能提高了写进程的优先级。
1 | readers.num=5 |
1 | package com.chain.test.day05; |
在该算法中,写者进程是优先的,也就是说,当存在写者进程时,读者操作将被推后。写操作之间通过w信号量保证互斥,读和写进程通过w信号量保持互斥,写进程通过q信号量控制写进程。这样的方式下,可能会导致读者进程长时间等待,即存在读者进程“饿死”的情况。
如果读者和写者的数量都为1时,可以看到两者交替进行,比较融洽:
如果读者和写者的数量都为2时,可以看到大部分都是写者进程在执行,但是读者进程还是有机会进行读操作的:
但是如果写者数量较多(3个),可以看到读者进程很少有机会进行读操作,基本是写者进程在进行写操作:
程序正常结束运行后检查各个文件的信息,可以看到信息一致。
由于这是强写者优先,所以都为3的情况下,读进程读完所有的内容时间所需太长(读进程获得执行的机会太少),不过可以修改Reader中一次buffer的大小,加快读取的速度。
读写公平(弱写者优先)
如果希望读写公平,即在多次操作下,读写操作的执行频率是差不多的。
具体的做法:在读者优先的基础上只增加一个信号量q就可以做到读写公平。
test.properties:
1 | readers.num=10 |
1 | package com.chain.test.day05; |
这样修改后,即使有再多的读写进程,两种操作的频率是差不多的:
程序正常结束运行后检查各个文件的信息,可以看到信息一致:
总结思考
为了做到读者之间互斥,可以给读者的读操作增加一个信号量w。
为了做到读者和写者之间的互斥,可以使用上面的信号量w。
为了做到读者优先,可以个读者操作增加一个计数器read_count,只要有一个读者能得到执行,就会阻止写者进行操作,从而其他的读者也能执行。直到没有读者去进行读操作时,写者才有机会去执行。
为了做到读写公平,可以在读者优先的基础上,给读者增加一个信号量q,这个信号量由写者控制。如果写者想执行操作,可以阻塞q,使得读者不能进行read_count操作,进而也就不能进行读操作。这种情况也被称为弱写者优先。
为了做到写者优先,可以在读者优先的基础上,给写者也配备一个计数器write_count,读者增加一个信号量q,这个信号量由写者控制。如果写者想执行操作,和弱写者优先一样,阻塞q即可。但是不同的是,一旦写者获得执行权,写者的计数器write_count的作用和read_count一样,会保证其他的写者也能得到执行。直到没有写者去进行写操作时,读者才有机会去执行。这种情况也被称为强写者优先。