三大线程模型之读者与写者问题(Java版)

一、前言

阅读文章的过程中如果碰到不懂的,可以留言或私信我,我会抽空回答。

编程过程中,我们所有遇到的多线程问题,都可以抽象为三种模型:

  1. 生产者与消费者
  2. 读者与写者;
  3. 哲学家吃饭问题。

能搞清楚这三种模型的实现方式与解决方案,在实际编程中碰到多线程问题处理起来才能得心应手。

二、读者与写者问题

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时出现线程安全问题。

  1. 每次进行读操作时,会更新readerCount,即记录当前有多少个线程在进行读操作。如果当前线程为第一个读线程,那么会锁住资源,防止写线程进来。当已经有读线程在进行操作时,便无需锁住资源,这样子,便可以支持多个线程同时进行读操作
  2. 每次进行写操作时,会锁住资源,防止其他读线程或者写线程进来,这样便能保证同一时刻,当写线程在进行操作时,不会有其他线程。

上面的实现虽然已经实现了需求,但是有个缺点:可能导致写线程饥饿
假设现在现在读线程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();
            }
        }
    }
}

我们引入了另一个SemaphoreTRY_RESOURCE。当有线程想要进行读或者写操作时,需要先去获取TRY_RESOURCE,因此如果有写线程已经进行排队了,那么新进来的读线程将会排在写线程后面。当然这里还牵涉到公平锁和非公平锁的问题,在这里我们不做深究。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值