《Java7并发编程实战手册》学习笔记(三)——线程同步基础

317 阅读20分钟

此篇博客为个人学习笔记,如有错误欢迎大家指正

本次内容

  1. 使用synchronized实现同步方法
  2. 使用非依赖属性实现同步
  3. 在同步代码中使用条件
  4. 使用锁实现同步
  5. 使用读写锁实现同步数据访问
  6. 修改锁的公平性
  7. 在锁中使用多条件

1.使用synchronized实现同步方法

在Java中,我们可以使用synchronized关键字来修饰方法或者代码块,想要进入同步代码块或方法时,就需要获取对象的内置锁。内置锁是互斥的,所以同一时间内最多只有一个线程能获得该锁。需要注意的是,被synchronized修饰的静态、非静态方法都同时只能被一个线程访问。但是两个线程可以同时分别访问被synchronized修饰的一个静态方法和一个非静态方法。如果这两个方法均对某个数据进行了操作,那么我们的程序就很有可能发生错误,这一点需要格外注意。

范例实现:

在这个范例中,我们有一个账号类,账号类中有被synchronized修饰的增加余额方法和减少余额方法。另外有两个实现了Runnable接口的类分别是银行类和公司类。在银行类重写的run()方法中,我们会调用账号类的减少余额方法;而在公司类中我们会调用增加余额的方法。最后,在main方法中开启线程,两个线程一增一减,我们会发现使用同步过的方法可以保证结果的正确性
账号类:

package day03.code_1;

public class Account {
    
    //双精度浮点型的余额
    private double balance;
    //余额的set、get方法
    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //使用synchronized修饰的增加余额方法
    public synchronized void addAmount(double amount) {
        //休眠10ms
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //余额增加
        balance += amount;
    }
    //使用synchronized修饰的减少余额方法
    public synchronized void subtractAmount(double amount) {
        //休眠10秒
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //余额减少
        balance -= amount;
    }
}

银行类:

package day03.code_1;

public class Bank implements Runnable {

    //账户
    private Account account;

    //有参构造方法
    public Bank(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        //循环执行100次的减少余额方法
        for (int i = 0; i < 100; i++) {
            account.subtractAmount(1000);
        }
    }
}

公司类:

package day03.code_1;

public class Company implements Runnable {

    //账户
    private Account account;

    //有参构造函数
    public Company(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        //循环执行100次增加余额方法
        for (int i = 0; i < 100; i++) {
            account.addAmount(1000);
        }
    }
}

main方法:

package day03.code_1;

public class Main {

    public static void main(String[] args) {
        //创建一个账户类对象
        Account account = new Account();
        //设置账户的初始余额为1000元
        account.setBalance(1000);
        //创建一个公司类对象并以此作为参数传入创建的线程对象中
        Company company = new Company(account);
        Thread companyThread = new Thread(company);
        //创建一个银行类对象并以此作为参数传入创建的线程对象中
        Bank bank = new Bank(account);
        Thread bankThread = new Thread(bank);
        //在控制台打印账户的初始金额
        System.out.printf("Account : Initial Balance: %.2f\n",account.getBalance());
        //开启两个线程对象
        companyThread.start();
        bankThread.start();
        try {
            //主线程等待两个线程的结束
            companyThread.join();
            bankThread.join();
            //结束后打印账户最后的余额
            System.out.printf("Account : Final Balance: %.2f\n",account.getBalance());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在程序中过度滥用synchronized关键字会极大的降低程序的并发性能,因为同一时间最多只有一个线程可以访问带有synchronized关键字的方法。因此,更好的方法其实是只对方法中和共享数据有交互的部分使用synchronized关键字进行同步。

2.使用非依赖属性实现同步

当我们使用synchronized关键字对代码块进行同步时,必须把对象的引用作为传入参数。通常情况下我们使用this关键字来作为传入参数。当然我们也可以创建其他的对象作为传入参数,这有利于我们更加灵活的同步代码块。

范例实现:

在这个范例中,我们将使用自己创建出的对象的引用作为传入参数来为代码块进行同步。场景是这样的:在一个电影院中,有两个放映厅。每个放映厅的初始票量是固定的,游客可以退票或购票。显然我们需要对购票和退票的相关代码块进行同步因为我们不允许同一时间内有多个线程对一个放映厅的当前票量进行操作。但是我们允许两个线程分别对两个影厅的当前票量进行操作,这并不会发生错误。因此,我们将单独创建两个对象并将其引用作为传入参数来为代码块同步。
电影院类:

package day03.code_2;

public class Cinema {

    //一号厅的剩余票量
    private long vacanciesCinema1;
    //二号厅的剩余票量
    private long vacanciesCinema2;
    //两个对象作为之后同步时的传入参数
    private final Object controlCinema1, controlCinema2;

    //在构造函数中对电影院类进行初始化
    public Cinema() {
        vacanciesCinema1 = 20;
        vacanciesCinema2 = 20;
        controlCinema1 = new Object();
        controlCinema2 = new Object();
    }

    //一号影厅的售票方法
    public boolean sellTickets1(int number) {
        //使用controlCinema1作为传入参数
        synchronized (controlCinema1) {
            //如果购票量小于当前剩余票量,出票
            if (number < vacanciesCinema1) {
                vacanciesCinema1 -= number;
                return true;
            } else
                return false;
        }
    }

    //二号影厅的售票方法
    public boolean sellTickets2(int number) {
        //使用controlCinema2作为传入参数
        synchronized (controlCinema2) {
            //如果购票量小于当前剩余票量,出票
            if (number < vacanciesCinema2) {
                vacanciesCinema2 -= number;
                return true;
            } else
                return false;
        }
    }

    //一号影厅的退票方法
    public boolean returnTickets1(int number) {
        //使用controlCinema1作为传入参数
        synchronized (controlCinema1) {
            vacanciesCinema1 += number;
            return true;
        }
    }

    //二号影厅的退票方法
    public boolean returnTickets2(int number) {
        //使用controlCinema2作为传入参数
        synchronized (controlCinema2) {
            vacanciesCinema2 += number;
            return true;
        }
    }

    //查看一号影厅剩余票量方法
    public long getVacanciesCinema1() {
        return vacanciesCinema1;
    }

    //查看二号影厅剩余票量方法
    public long getVacanciesCinema2() {
        return vacanciesCinema2;
    }
}

两个售票处类:

package day03.code_2;

public class TicketOffice1 implements Runnable {

    //售票处类
    private Cinema cinema;

    //带参构造方法
    public TicketOffice1(Cinema cinema) {
        this.cinema = cinema;
    }

    @Override
    public void run() {
        //对两个影厅的售票、退票操作
        cinema.sellTickets1(3);
        cinema.sellTickets1(2);
        cinema.sellTickets2(2);
        cinema.returnTickets1(3);
        cinema.sellTickets1(5);
        cinema.sellTickets2(2);
        cinema.sellTickets2(2);
        cinema.sellTickets2(2);
    }
}
package day03.code_2;

public class TicketOffice2 implements Runnable {

    //售票处类
    private Cinema cinema;

    //带参构造方法
    public TicketOffice2(Cinema cinema) {
        this.cinema = cinema;
    }

    @Override
    public void run() {
        //对两个影厅的售票、退票操作
        cinema.sellTickets2(2);
        cinema.sellTickets2(4);
        cinema.sellTickets1(2);
        cinema.sellTickets1(1);
        cinema.returnTickets2(2);
        cinema.sellTickets1(3);
        cinema.sellTickets2(2);
        cinema.sellTickets1(2);
    }
}

main方法:

package day03.code_2;

public class Main {

    public static void main(String[] args) {
        //创建一个电影院类对象
        Cinema cinema = new Cinema();
        //创建售票处对象并将其作为参数传入线程对象的构造函数中
        TicketOffice1 ticketOffice1 = new TicketOffice1(cinema);
        Thread thread1 = new Thread(ticketOffice1, "TicketOffice1");
        TicketOffice2 ticketOffice2 = new TicketOffice2(cinema);
        Thread thread2 = new Thread(ticketOffice2, "TicketOffice2");
        //开启两个线程
        thread1.start();
        thread2.start();
        try {
            //当前线程挂起,等待两个线程的执行结束
            thread1.join();
            thread2.join();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //分别打印两个放映厅的剩余票量
        System.out.printf("Room 1 Vacancies: %d\n",cinema.getVacanciesCinema1());
        System.out.printf("Room 2 Vacancies: %d\n",cinema.getVacanciesCinema2());
    }
}

在这个范例中我们使用了两个对象的引用作为参数来为代码块进行同步。因此,线程在进入对两个影厅票量进行操作的方法时,获得的并不是同一个对象上的锁,这样才可以同时对两个影厅的票量进行操作。然而,如果想进入对同个影厅票量进行操作的方法,需要获得的是同一个对象上的锁,这就使得同时间最多只有一个线程能对同一个影厅的票量进行操作,保证了程序的正确性。

3.在同步代码中使用条件

在并发编程中,我们有时需要根据某些条件来决定线程下一步的状态。Java中的Object类为我们提供了wait()notify()notifyAll()等方法来帮助我们实现上述需求。wait()方法会阻塞线程,使线程进入休眠状态并释放所持有的锁;因wait()方法进入休眠的线程必须等待其他线程的唤醒,notify()notifyAll()方法都能够唤醒处于等待状态的线程,区别在于一个是唤醒某一个线程,另一个是唤醒所有线程。当然,以上三个方法都必须在同步代码块中使用,否则会抛出IllegalMonitorStateException异常。

范例实现:

在这个范例中,我们将创建一个存储空间类,一个生产者类和一个消费者类。顾名思义,生产者类负责向存储空间中添加信息而消费者类负责从存储空间中取走信息。存储空间的存储容量是有最大限制的,当达到限制时,生产者线程将进入休眠状态等待消费者线程的唤醒;同样当存储空间内为空时,消费者线程将进入休眠状态等待生产者线程的唤醒。这便是并发编程中经典问题:生产者-消费者问题的简化版本。
存储空间类:

package day03.code_3;

import java.util.Date;
import java.util.LinkedList;
import java.util.List;

public class EventStorage {

    //最大存储容量
    private int maxSize;

    //使用一个集合作为底层的存储空间
    private List<Date> storage;

    //通过构造函数进行初始化
    public EventStorage() {
        //设置最大存储容量为10
        this.maxSize = 10;
        this.storage = new LinkedList<>();
    }

    //同步的装填信息方法
    public synchronized void set() {
        /*
         * 判断存储空间是否已满
         * 已满的话进入休眠状态
         * 等待其他线程的唤醒
         * */
        while (storage.size() == maxSize) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //向存储空间中加入数据
        storage.add(new Date());
        //打印存储空间当前信息个数
        System.out.printf("Set: %d\n", storage.size());
        //此处是为了唤醒那些因存储空间为空而睡眠的线程
        notifyAll();
    }

    //同步的取出信息方法
    public synchronized void get() {
        /*
         * 判断存储空间是否为空
         * 为空的话进入休眠状态
         * 等待其他线程的唤醒
         * */
        while (storage.size() == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //打印空间存储的信息个数和取出的信息
        System.out.printf("Get: %d: %s\n",
                storage.size(), storage.remove(0));
        //此处是为了唤醒那些因存储空间已满而睡眠的线程
        notifyAll();
    }
}

消费者类:

package day03.code_3;

public class Consumer implements Runnable {

    //存储空间对象
    private EventStorage eventStorage;

    //有参构造方法
    public Consumer(EventStorage eventStorage) {
        this.eventStorage = eventStorage;
    }

    @Override
    public void run() {
        /*
         * 循环向存储空间中装填数据
         * 每添加一个数据后休眠一段时间
         * */
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            eventStorage.get();
        }
    }
}

生产者类:

package day03.code_3;

public class Producer implements Runnable {

    //存储空间对象
    private EventStorage eventStorage;

    //有参构造方法
    public Producer(EventStorage eventStorage) {
        this.eventStorage = eventStorage;
    }

    @Override
    public void run() {
        /*
         * 循环从存储空间中取出数据
         * 每取出一个数据后休眠一段时间
         * 取出数据和存入数据后休眠的时间不同
         * 更符合实际情况,运行效果也更好
         * */
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            eventStorage.set();
        }
    }
}

main方法:

package day03.code_3;

public class Main {

    public static void main(String[] args) {
        //创建存储空间对象
        EventStorage eventStorage = new EventStorage();
        /*
         * 创建生产者对象作为参数传入线程对象的构造函数中
         * 创建消费者对象作为参数传入线程对象的构造函数中
         * */
        Producer producer = new Producer(eventStorage);
        Thread thread1 = new Thread(producer, "producer");
        Consumer consumer = new Consumer(eventStorage);
        Thread thread2 = new Thread(consumer, "consumer");
        //开启线程
        thread1.start();
        thread2.start();
    }
}

4.使用锁实现同步

之前我们一直使用synchronized关键字来同步代码,接下来我们将使用锁来实现同步。这是一种Java为我们提供的更加强大也更加灵活的方式,Lock接口的实现类ReenTrantLock类有多个方法,目前我们需要大致了解以下三个:lock()unlock()tryLock()。通俗来讲,lock()方法用于获取锁,如果未能获取则挂起直到获得锁;unlock()方法用于释放锁,我们在使用完锁之后一定要记得释放锁,不然会造成死锁现象,如果使用了try-catch块,则要在finally块中添加unlock()方法;tryLock()方法比较特殊,使用此方法时,如果线程不能获取锁将不会挂起而是返回false继续向下执行,如果获取了锁则返回true,因此此方法可与if语句组合使用。

范例实现:

以下是使用锁实现同步的小例子,例子并不复杂且代码中有较详细的注释,具体细节就不在此赘述了
打印队列类:

package day03.code_4;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//打印队列类
public class PrintQueue {

    //创建一个锁对象用于接下来的同步
    private final Lock queueLock = new ReentrantLock();

    public void printJob() {
        //获取锁,以下代码被同步
        queueLock.lock();
        /*
         * 使用随机数生成一个值
         * 打印当前线程的名字和打印(休眠)所需要的时间
         * 模拟打印(进入休眠)
         * */
        long duration = (long) (Math.random() * 10000);
        System.out.println(Thread.currentThread().getName()
                + ":PrintQueue: Printing a Job during " +
                (duration / 1000) + " seconds");
        try {
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //千万不要忘记释放锁,以上代码被同步
            queueLock.unlock();
        }
    }
}

工作类:

package day03.code_4;

public class Job implements Runnable {

    //打印队列对象
    private PrintQueue printQueue;

    //有参构造函数将被传入一个创建好的打印队列对象引用
    public Job(PrintQueue printQueue) {
        this.printQueue = printQueue;
    }

    @Override
    public void run() {
        //打印开始提示语
        System.out.printf("%s: Going to print a document\n",
                Thread.currentThread().getName());
        //调用打印队列的方法,此方法已使用锁进行同步
        printQueue.printJob();
        //打印结束提示语
        System.out.printf("%s: The document has been printed\n",
                Thread.currentThread().getName());
    }
}

main函数:

package day03.code_4;

public class Main {
    public static void main(String[] args) {
        //创建一个打印队列对象
        PrintQueue printQueue = new PrintQueue();
        //创建一个工作对象
        Job job = new Job(printQueue);
        //循环十次已工作对象为参数创建线程并运行
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(job, "Thread" + i);
            thread.start();
        }
    }
}

5.使用读写锁实现同步数据访问

在对数据进行操作时,我们通常希望可以有多个线程来查询数据但仅有一个线程可以对数据进行增、删、改操作。Java中ReadWriteLock接口的唯一实现类ReentrantReadWriteLock可以让帮助我们更轻松的实现上述功能。这个类中有两个锁,分别是读操作锁和写操作锁。在使用读操作锁时,可以有多个线程同时访问,但是当使用写操作锁时,只允许有一个线程访问,不允许其他任何线程进行读、写操作。readLock()方法用于获取读操作锁,writeLock()方法用于获取写操作锁。读、写操作锁也实现了Lock接口所以它们也具有上面我们提到过的lock()unlock()tryLock()方法。我们可以在增、删、改方法中使用写操作锁,在查询方法中使用读操作锁。

范例实现:

我们创建一个价格信息类,将会开启五个查询线程和一个修改线程对价格信息进行操作
价格信息类:

package day03.code_5;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class PricesInfo {

    //双精度浮点型的价格1
    private double price2;

    //双精度浮点型的价格2
    private double price1;

    //读写锁
    private final ReentrantReadWriteLock lock;

    /*
     * 构造函数
     * 设置价格1为1.0,价格2为2.0
     * */
    public PricesInfo() {
        this.price1 = 1.0;
        this.price2 = 2.0;
        this.lock = new ReentrantReadWriteLock();
    }

    //查询价格1方法
    public void getPrice1() {
        //获取读操作锁并加锁
        lock.readLock().lock();
        //得到价格后打印相关信息
        double value = this.price1;
        System.out.printf("%s: Price 1: %.2f\n",
                Thread.currentThread().getName(),
                value);
        //释放读操作锁
        lock.readLock().unlock();
    }

    //查询价格2方法
    public void getPrice2() {
        //获取读操作锁并加锁
        lock.readLock().lock();
        //得到价格后打印相关信息
        double value = this.price2;
        System.out.printf("%s: Price 2: %.2f\n",
                Thread.currentThread().getName(),
                value);
        //释放读操作锁
        lock.readLock().unlock();
    }

    //修改价格方法
    public void setPrice(double price1, double price2) {
        //获取写操作锁并加锁
        lock.writeLock().lock();
        //修改价格后打印相关信息
        this.price1 = price1;
        this.price2 = price2;
        System.out.printf("Price has been changed price1: %.2f price2: %.2f\n",
                price1, price2);
        //释放写操作锁
        lock.writeLock().unlock();
    }
}

读类:

package day03.code_5;

public class Reader implements Runnable {

    //价格信息对象
    private PricesInfo pricesInfo;

    public Reader(PricesInfo pricesInfo) {
        this.pricesInfo = pricesInfo;
    }

    @Override
    public void run() {
        //循环十次获取价格1、2
        for (int i = 0; i < 10; i++) {
            pricesInfo.getPrice1();
            pricesInfo.getPrice2();
        }
    }
}

写类:

package day03.code_5;

public class Writer implements Runnable {

    //价格信息对象
    private PricesInfo pricesInfo;

    public Writer(PricesInfo pricesInfo) {
        this.pricesInfo = pricesInfo;
    }

    @Override
    public void run() {
        /*
         * 循环三次
         * 使用随机数来设定价格1、2
         * 每次更改价格后休眠2秒
         * */
        for (int i = 0; i < 3; i++) {
            pricesInfo.setPrice(Math.random() * 10, Math.random() * 8);
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

main方法:

package day03.code_5;

public class Main {

    public static void main(String[] args) {
        //创建价格信息类、读类、写类
        PricesInfo pricesInfo = new PricesInfo();
        Writer writer = new Writer(pricesInfo);
        Reader reader = new Reader(pricesInfo);
        //创建5个读线程并开启
        Thread[] threadReader = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threadReader[i] = new Thread(reader);
            threadReader[i].start();
        }
        //创建一个写线程并开启
        Thread writerThread = new Thread(writer);
        writerThread.start();
    }

}

6.修改锁的公平性

ReentrantLock和ReentrantReadWriteLock类的构造器均含有一个布尔参数fair,此参数用来表示锁是否是公平的。如果传入true则代表公平模式,那么锁在选择线程时,将会选择等待时间最长的线程。如果fair为false,则代表非公平模式,这是锁在选择线程时是没有约束的。

范例实现:

在这个范例中,我们将第4小节(使用锁实现同步)中的PrintQueue类进行了小的改动,由原先的获取、释放锁一次变为了两次,其他代码不变,就不再次贴出了。
新打印队列类:(因重用了之前的代码,所以新打印队列类继承了打印队列类)

package day03.code_6;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class PrintQueue extends day03.code_4.PrintQueue {
    //创建时通过构造函数设置fair为true
    private Lock queueLock = new ReentrantLock(true);

    public void printJob() {
        try {
            //第一次加锁
            queueLock.lock();
            //打印
            long duration = (long)(Math.random()*10000);
            System.out.println(Thread.currentThread().getName()
                    + ":PrintQueue1: Printing a Job during "  +
                    (duration/1000) + " seconds");
            //休眠
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //第一次释放锁
            queueLock.unlock();
        }
        try {
            //第二次加锁
            queueLock.lock();
            //打印
            long duration = (long)(Math.random()*10000);
            System.out.println(Thread.currentThread().getName()
                    + ":PrintQueue2: Printing a Job during "  +
                    (duration/1000) + " seconds");
            //休眠
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //第二次释放锁
            queueLock.unlock();
        }
    }
}

7.在锁中使用多条件

之前我们在由synchronized同步的代码中使用条件,在用锁进行同步的代码中我们同样可以使用条件。Condition接口为我们提供了挂起线程和唤起线程的机制。与锁绑定的所有条件对象都是通过Lock接口声明的newCondition()方法创建的。在使用条件时必须在调用的lock对象的lock()方法和unlock()方法之间。类似于之前的wait()方法,当调用条件的await()方法后,线程将自动释放这个条件所绑定的锁。同样,条件的signal()方法和signalAll()方法与notify()notifyAll()功能类似,都是唤起某个或所有处于等待状态的线程。当然,条件也可以与读、写操作锁组合使用。 Condition还提供了await()方法的其他形式:

  1. await(long time,TimeUnit unit)直到发生以下情况之一,线程将一直处于休眠状态
    • 其他某个线程中断当前线程
    • 其他某个线程调用了当前线程挂起条件的singal()signalAll()方法
    • 指定的等待时间已经过去
  2. awaitUninterruptibly():不可中断
    • 其他某个线程调用了当前线程挂起条件的singal()signalAll()方法
  3. awaitUntil(Data data)
    • 其他某个线程中断当前线程
    • 其他某个线程调用了当前线程挂起条件的singal()signalAll()方法
    • 已到达指定的日期

范例实现:

本例中,我们将创建一个文件模拟类、数据缓冲类、消费者类、生产者类。生产者从文件中读取数据,然后检查数据缓冲器中是否已满来决定当前线程休眠还是将数据放入缓冲器。消费者类检查数据缓冲器中是否为空来决定线程休眠还是取出数据。这个范例类似于第3小节(在同步代码中使用条件),都属于生产者-消费者问题,不同的是这一次我们使用锁来同步代码并且使用与锁绑定的条件来进行相应控制。代码如下:
模拟文件类:

package day03.code_7;

public class FileMock {

    //存放数据的数组
    private String content[];

    //模拟数据的行号
    private int index;

    //带有两个参数的构造函数
    public FileMock(int size, int length) {
        //创建一个size大小的字符串数组
        this.content = new String[size];
        //外部循环size次,目的是将数组填满
        for (int i = 0; i < size; i++) {
            //使用字符串构造器来拼接数据,减少无用字符串常量的产生
            StringBuilder buffer = new StringBuilder(length);
            //内部循环length次,表示每条数据长为length
            for (int j = 0; j < length; j++) {
                //这里使用随机数作为数据
                int temp = (int) (Math.random() * 255);
                buffer.append(temp);
            }
            //将创建好的一条数据放入数组中对应的位置
            content[i] = buffer.toString();
        }
        //将行号置为0
        this.index = 0;
    }

    //用来判断文件中是否还有未读取的数据的方法
    public boolean hasMoreLines() {
        //index等于数组长度时表示所有数据均读取过
        return index < content.length;
    }

    //从模拟文件中得到一行数据
    public String getLine() {
        //首先判断是否存在未读取的数据
        if (hasMoreLines()) {
            //打印读取到的行号
            System.out.println("Mock: " + index);
            //返回数据后index自增
            return content[index++];
        }
        return null;
    }
}

数据缓冲器类:

package day03.code_7;

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class Buffer {

    //用来装载数据的集合
    private LinkedList<String> buffer;

    //数据缓冲器的最大容量
    private int maxSize;

    //用来同步代码块的锁
    private ReentrantLock lock;

    //用来判断缓冲器中是否有数据的条件
    private Condition lines;

    //用来判断缓冲器中是否为空的条件
    private Condition space;

    //用来判断文件是否还有未读的数据
    private boolean pendingLines;

    public Buffer(int maxSize) {
        this.maxSize = maxSize;
        buffer = new LinkedList<>();
        lock = new ReentrantLock();
        //用锁创建两个条件
        lines = lock.newCondition();
        space = lock.newCondition();
        //缓冲器将来会有数据,所以置为true
        pendingLines = true;
    }

    //向缓冲器中添加数据的方法
    public void insert(String line) {
        //加锁
        lock.lock();
        try {
            //如果缓冲器已满,就使用space条件让当前线程挂起
            while (buffer.size() == maxSize) {
                space.await();
            }
            //向缓冲器末尾添加一条数据并打印相应信息
            buffer.offer(line);
            System.out.printf("%s: Inserted Line: %d\n",
                    Thread.currentThread().getName(),
                    buffer.size());
            //因为插入了数据,所以使用lines条件唤起因数据为空而休眠的消费者
            lines.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
    }

    //从缓冲器中取数据的方法
    public String get() {
        String line = null;
        //加锁
        lock.lock();
        try {
            //如果现在缓冲器无数据但将来会有则休眠
            while ((buffer.size() == 0) && (hasPendingLines())) {
                lines.await();
            }
            //经过上面的判断,此处判断如果返回为true则缓冲器中一定存在数据
            if (hasPendingLines()) {
                //获取并删除列表的第一个元素
                line = buffer.poll();
                //打印相关信息
                System.out.printf("%s: Line Readed: %d\n",
                        Thread.currentThread().getName(),
                        buffer.size());
                //消耗了一条数据所以使用space条件唤醒因缓冲器满而休眠的生产者
                space.signalAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
        //返回取出的数据
        return line;
    }

    /*
     * 设置文件是否已经读空
     * 生产者线程在读空文件中的所有数据后将调用此方法将pendingLines置为true
     * */
    public void setPendingLines(boolean pendingLines) {
        this.pendingLines = pendingLines;
    }

    //此方法表示现在或将来缓冲器中是否会有数据
    public boolean hasPendingLines() {
        /*
        * pendingLines表示文件是否已经读空
        * buffer.size()表示缓冲器中的容量
        * 只有当文件已空且缓冲器中不存在数据时才返回false
        * 返回false表示现在和将来都不会再有数据在缓冲器中
        * */
        return pendingLines || buffer.size() > 0;
    }
}

生产者类:

package day03.code_7;

public class Producer implements Runnable {

    //模拟文件对象
    private FileMock fileMock;
    //缓冲器对象
    private Buffer buffer;

    //通过构造方法传入两个对象的引用
    public Producer(FileMock fileMock, Buffer buffer) {
        this.fileMock = fileMock;
        this.buffer = buffer;
    }

    @Override
    public void run() {
        //文件存在未读数据
        buffer.setPendingLines(true);
        //当文件存在未读数据时
        while (fileMock.hasMoreLines()) {
            //不断读取数据并装入缓冲区
            String line = fileMock.getLine();
            buffer.insert(line);
        }
        //文件不存在未读数据
        buffer.setPendingLines(false);
    }
}

消费者类:

package day03.code_7;

import java.util.Random;

public class Consumer implements Runnable {

    //文件缓冲器
    private Buffer buffer;

    //构造方法传入缓冲器引用
    public Consumer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        //当缓冲器存在或将出现数据,就不断取出数据并处理
        while (buffer.hasPendingLines()) {
            String line = buffer.get();
            //模拟处理数据
            processLine(line);
        }
    }

    private void processLine(String line) {
        try {
            //休眠随机的时间来模拟处理数据花费的不同时间
            Random random = new Random();
            Thread.sleep(random.nextInt(100));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

main方法:

package day03.code_7;

public class Main {

    public static void main(String[] args) {
        //创建模拟文件对象
        FileMock mock = new FileMock(100, 10);
        //创建字符缓冲器
        Buffer buffer = new Buffer(20);
        //创建生产者并将其作为参数传入线程对象的构造方法中
        Producer producer = new Producer(mock, buffer);
        Thread threadProducer = new Thread(producer, "Producer");
        //开启生产者线程
        threadProducer.start();
        //线程休眠2秒
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //创建消费者对象
        Consumer consumer = new Consumer(buffer);
        //创建三个消费者线程并启动
        for (int i = 0; i < 3; i++) {
            Thread threadConsumer = new Thread(consumer, "Consumer" + i);
            threadConsumer.start();
        }
    }
}