一、前言
阅读文章的过程中如果碰到不懂的,可以留言或私信我,我会抽空回答。
编程过程中,我们所有遇到的多线程问题,都可以抽象为三种模型:
- 生产者与消费者;
- 读者与写者;
- 哲学家吃饭问题。
能搞清楚这三种模型的实现方式与解决方案,在实际编程中碰到多线程问题处理起来才能得心应手。
二、读者与写者问题
1. 读者与写者问题概述
读者与写者问题描述的是一个多线程问题:假设我们有一个资源,它可以被读或者写,在某一时刻,可能有不同的线程尝试着对这个资源进行操作(读或写)。
2. 读者与写者的线程安全问题
多个线程同时对一个资源进行操作,那么就可能引发线程安全问题。比如说:一个写线程,希望更改资源的1-6行,当写线程更改到第三行时,有一个读线程对这个资源进行了读取,那么这时候读到的数据的前三行是最新的,后三行却是旧的。除此以外,多个写线程同时对一个资源进行操作,也会引发线程安全问题。
3.如何解决线程安全问题
线程安全问题本质是由于多个线程同时对一个文件操作导致的,如果我们给资源加锁,就可以轻松的解决线程安全问题。但是这样做会有弊端:程序的吞吐率会下降。一个资源一次只能有一个线程进行操作,但是读操作并不会引发线程安全问题,我们还是希望能够达到这样一种效果:同一时刻,只能有一个写线程或者一个或多个读线程对资源进行操作。
4.一次简单的尝试
public class FirstReaderWriter {
private static final Semaphore READ_MUTEX = new Semaphore(1);
private static final Semaphore RESOURCE = new Semaphore(1);
private static int readerCount;
public static void main(String[] args) {
new Thread(new Writer(), "Writer no.1").start();
new Thread(new Reader(), "Reader no.1").start();
new Thread(new Reader(), "Reader no.2").start();
new Thread(new Reader(), "Reader no.3").start();
new Thread(new Writer(), "Writer no.2").start();
new Thread(new Reader(), "Reader no.4").start();
new Thread(new Reader(), "Reader no.5").start();
new Thread(new Reader(), "Reader no.6").start();
new Thread(new Writer(), "Writer no.3").start();
new Thread(new Reader(), "Reader no.7").start();
new Thread(new Reader(), "Reader no.8").start();
new Thread(new Reader(), "Reader no.9").start();
new Thread(new Reader(), "Reader no.10").start();
}
private static class Reader implements Runnable {
@Override
public void run() {
try {
READ_MUTEX.acquire();
readerCount++;
if (readerCount == 1) {
RESOURCE.acquire();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
READ_MUTEX.release();
}
ReaderWriterUtil.read();
try {
READ_MUTEX.acquire();
readerCount--;
if (readerCount == 0) {
RESOURCE.release();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
READ_MUTEX.release();
}
}
}
static class Writer implements Runnable {
@Override
public void run() {
try {
RESOURCE.acquire();
ReaderWriterUtil.write();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
RESOURCE.release();
}
}
}
}
上述代码使用Java提供的Semaphore
作为临界条件,RESOURCE
代表资源,READER_MUTEX
为用作读互斥,防止更新readerCount
时出现线程安全问题。
- 每次进行读操作时,会更新
readerCount
,即记录当前有多少个线程在进行读操作。如果当前线程为第一个读线程,那么会锁住资源,防止写线程进来。当已经有读线程在进行操作时,便无需锁住资源,这样子,便可以支持多个线程同时进行读操作; - 每次进行写操作时,会锁住资源,防止其他读线程或者写线程进来,这样便能保证同一时刻,当写线程在进行操作时,不会有其他线程。
上面的实现虽然已经实现了需求,但是有个缺点:可能导致写线程饥饿。
假设现在现在读线程A持有了RESOURCE
,写线程1尝试获取RESOURCE
失败,进行等待,等待过程中,读线程B进来了,那么读线程B将会比写线程1更早进行(读线程B无需获取RESOURCE
)。如果这时候再有读线程C,读线程D进来,那么写线程将会长时间无法运行,造成饥饿。
5.解决写线程饥饿问题
public class SecondReaderWriter {
private static final Semaphore RESOURCE = new Semaphore(1);
private static final Semaphore TRY_SEMAPHORE = new Semaphore(1);
private static final Semaphore READ_MUTEX = new Semaphore(1);
private static int readerCount;
public static void main(String[] args) {
new Thread(new Writer(), "Writer no.1").start();
new Thread(new Reader(), "Reader no.1").start();
new Thread(new Reader(), "Reader no.2").start();
new Thread(new Reader(), "Reader no.3").start();
new Thread(new Writer(), "Writer no.2").start();
new Thread(new Reader(), "Reader no.4").start();
new Thread(new Reader(), "Reader no.5").start();
new Thread(new Reader(), "Reader no.6").start();
new Thread(new Writer(), "Writer no.3").start();
new Thread(new Reader(), "Reader no.7").start();
new Thread(new Reader(), "Reader no.8").start();
new Thread(new Reader(), "Reader no.9").start();
new Thread(new Reader(), "Reader no.10").start();
}
private static class Reader implements Runnable {
@Override
public void run() {
try {
TRY_SEMAPHORE.acquire();
READ_MUTEX.acquire();
readerCount++;
if (readerCount == 1) {
RESOURCE.acquire();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
TRY_SEMAPHORE.release();
READ_MUTEX.release();
}
ReaderWriterUtil.read();
try {
READ_MUTEX.acquire();
readerCount--;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (readerCount == 0) {
RESOURCE.release();
}
READ_MUTEX.release();
}
}
}
private static class Writer implements Runnable {
@Override
public void run() {
try {
TRY_SEMAPHORE.acquire();
RESOURCE.acquire();
ReaderWriterUtil.write();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
RESOURCE.release();
TRY_SEMAPHORE.release();
}
}
}
}
我们引入了另一个Semaphore
:TRY_RESOURCE
。当有线程想要进行读或者写操作时,需要先去获取TRY_RESOURCE,因此如果有写线程已经进行排队了,那么新进来的读线程将会排在写线程后面。当然这里还牵涉到公平锁和非公平锁的问题,在这里我们不做深究。