java多线程进阶练习

1、生产者-消费者问题

实现一个生产者-消费者模型,其中生产者线程生成数据并将其放入缓冲区,而消费者线程从缓冲区中取出数据进行处理。你可以使用wait()和notify()方法或者使用BlockingQueue来实现该模型。
例:

public class ProducerConsumer {
    
    
    private LinkedList<Integer> buffer = new LinkedList<>();
    private int capacity = 5;

    public void produce() throws InterruptedException {
    
    
        int value = 0;
        while (true) {
    
    
            synchronized (this) {
    
    
                while (buffer.size() == capacity) {
    
    
                    wait();
                }
                System.out.println("Producer produced: " + value);
                buffer.add(value++);
                notify();
                Thread.sleep(1000);
            }
        }
    }

    public void consume() throws InterruptedException {
    
    
        while (true) {
    
    
            synchronized (this) {
    
    
                while (buffer.isEmpty()) {
    
    
                    wait();
                }
                int value = buffer.removeFirst();
                System.out.println("Consumer consumed: " + value);
                notify();
                Thread.sleep(1000);
            }
        }
    }

    public static void main(String[] args) {
    
    
        ProducerConsumer pc = new ProducerConsumer();

        Thread producerThread = new Thread(() -> {
    
    
            try {
    
    
                pc.produce();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        });

        Thread consumerThread = new Thread(() -> {
    
    
            try {
    
    
                pc.consume();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        });

        producerThread.start();
        consumerThread.start();
    }
}

在上面的示例中,ProducerConsumer类是生产者-消费者模型的实现。buffer是一个用于存储数据的缓冲区,capacity表示缓冲区的最大容量。

produce()方法是生产者线程的执行逻辑。如果缓冲区已满,生产者线程会等待,直到有消费者取走数据。然后,它生成一个新的数据项,并将其添加到缓冲区中,然后通知等待的消费者线程。

consume()方法是消费者线程的执行逻辑。如果缓冲区为空,消费者线程会等待,直到有生产者放入数据。然后,它从缓冲区中移除第一个数据项,并进行消费,然后通知等待的生产者线程。

在main中,我们创建一个ProducerConsumer对象,并使用两个线程分别执行生产者的produce()方法和消费者的consume()方法。

2、读者-写者问题

实现一个读者-写者模型,其中多个读者线程可以同时读取共享数据,而写者线程在写入数据时必须独占访问。

public class ReaderWriter {
    
    
    //正在读取的读者数量
    private int readers = 0;
    //正在写入的写者数量
    private int writers = 0;
    //正在请求写入的写者数量
    private int writerRequests = 0;

    public void startRead() throws InterruptedException {
    
    
        synchronized (this) {
    
    
            while (writers > 0 || writerRequests > 0) {
    
    
                wait();
            }
            readers++;
            System.out.println("Reader started, total readers: " + readers);
        }
    }

    public void endRead() throws InterruptedException {
    
    
        synchronized (this) {
    
    
            readers--;
            System.out.println("Reader ended, total readers: " + readers);
            if (readers == 0) {
    
    
                notifyAll();
            }
        }
    }

    public void startWrite() throws InterruptedException {
    
    
        synchronized (this) {
    
    
            writerRequests++;
            while (readers > 0 || writers > 0) {
    
    
                wait();
            }
            writerRequests--;
            writers++;
            System.out.println("Writer started, total writers: " + writers);
        }
    }

    public void endWrite() throws InterruptedException {
    
    
        synchronized (this) {
    
    
            writers--;
            System.out.println("Writer ended, total writers: " + writers);
            notifyAll();
        }
    }

    public static void main(String[] args) {
    
    
        ReaderWriter readerWriter = new ReaderWriter();
        Thread readerThread1 = new Thread(new Reader(readerWriter));
        Thread readerThread2 = new Thread(new Reader(readerWriter));
        Thread writerThread1 = new Thread(new Writer(readerWriter));
        Thread readerThread3 = new Thread(new Reader(readerWriter));

        readerThread1.start();
        readerThread2.start();
        writerThread1.start();
        readerThread3.start();

    }
}

class Reader implements Runnable {
    
    
    private ReaderWriter readerWriter;

    public Reader(ReaderWriter readerWriter) {
    
    
        this.readerWriter = readerWriter;
    }

    @Override
    public void run() {
    
    
        try {
    
    
            readerWriter.startRead();
            // Perform read operation
            Thread.sleep(1000);
            readerWriter.endRead();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

class Writer implements Runnable {
    
    

    private ReaderWriter readerWriter;

    public Writer(ReaderWriter readerWriter) {
    
    
        this.readerWriter = readerWriter;
    }

    @Override
    public void run() {
    
    
        try {
    
    
            readerWriter.startWrite();
            // Perform write operation
            Thread.sleep(1000);
            readerWriter.endWrite();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

3、哲学家就餐问题

实现哲学家就餐问题,其中有一组哲学家坐在圆桌周围,每个哲学家需要同时拿起左右两边的餐叉才能进餐。你可以使用ReentrantLock和条件变量来实现避免死锁的算法。

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

class Philosopher implements Runnable {
    
    
    private Lock leftFork;
    private Lock rightFork;

    public Philosopher(Lock leftFork, Lock rightFork) {
    
    
        this.leftFork = leftFork;
        this.rightFork = rightFork;
    }

    private void doAction(String action) throws InterruptedException {
    
    
        System.out.println(Thread.currentThread().getName() + " " + action);
        Thread.sleep((int) (Math.random() * 100));
    }

    @Override
    public void run() {
    
    
        try {
    
    
            while (true) {
    
    
                doAction("Thinking");
                leftFork.lock();
                try {
    
    
                    if (rightFork.tryLock()) {
    
    
                        try {
    
    
                            doAction("Eating");
                        } finally {
    
    
                            rightFork.unlock();
                        }
                    }
                } finally {
    
    
                    leftFork.unlock();
                }
            }
        } catch (InterruptedException e) {
    
    
            Thread.currentThread().interrupt();
        }
    }
}

public class Main {
    
    
    public static void main(String[] args) {
    
    
        int numberOfPhilosophers = 5;
        Philosopher[] philosophers = new Philosopher[numberOfPhilosophers];
        Lock[] forks = new ReentrantLock[numberOfPhilosophers];

        for (int i = 0; i < numberOfPhilosophers; i++) {
    
    
            forks[i] = new ReentrantLock();
        }

        for (int i = 0; i < numberOfPhilosophers; i++) {
    
    
            Lock leftFork = forks[i];
            Lock rightFork = forks[(i + 1) % numberOfPhilosophers];

            philosophers[i] = new Philosopher(leftFork, rightFork);

            Thread thread = new Thread(philosophers[i], "Philosopher " + (i + 1));
            thread.start();
        }
    }
}

在上面的示例中,每个哲学家都是一个线程,并且使用两个锁(左手边的叉子和右手边的叉子)来模拟哲学家就餐的过程。

Philosopher类实现了Runnable接口,并在run()方法中定义了哲学家的行为。每个哲学家在一个无限循环中,交替地进行思考和就餐。当一个哲学家要就餐时,它首先尝试获取左手边的叉子,然后尝试获取右手边的叉子。如果成功获取了两个叉子,它就进行就餐,然后释放两个叉子。

Main类中创建了一定数量的哲学家和叉子,并通过线程启动它们。在这个示例中,有5个哲学家和5个叉子(锁),哲学家们按照顺时针的顺序获取叉子。

请注意,这个示例中的实现使用了ReentrantLock来表示叉子(锁)。这里使用了tryLock()方法来避免死锁。如果一个哲学家不能同时获得两个叉子,它将释放已经获取的叉子,继续思考一会儿,然后再次尝试就餐。

4、线程池

实现一个简单的线程池,可以动态地管理一组线程来执行任务。你可以使用ExecutorService框架来创建和管理线程池。

public class Task implements Runnable {
    
    
        private String name;

        public Task(String name) {
    
    
            this.name = name;
        }

        @Override
        public void run() {
    
    
            System.out.println("Task " + name + " started");
            try {
    
    
                Thread.sleep(2000); // 模拟任务执行时间
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("Task " + name + " completed");
        }

        public static void main(String[] args) {
    
    
            int numThreads = 3; // 线程池中的线程数量

            ExecutorService executorService = Executors.newFixedThreadPool(numThreads);

            for (int i = 1; i <= 5; i++) {
    
    
                String taskName = "Task " + i;
                Task task = new Task(taskName);
                executorService.execute(task);
            }

            executorService.shutdown();
        }

}

在上面的示例中,我们首先创建一个ExecutorService对象,使用Executors.newFixedThreadPool()方法创建一个固定大小的线程池,其中线程数量为3。

然后,我们创建5个Task对象,每个对象代表一个任务。通过调用executorService.execute(task)将任务提交给线程池执行。

最后,我们调用executorService.shutdown()方法来关闭线程池。

每个任务都是Runnable接口的实现类,通过实现run()方法来定义任务的逻辑。在示例中,每个任务只是简单地输出一些信息,并模拟了2秒的任务执行时间。

使用线程池的好处是,它可以重用线程,避免了频繁地创建和销毁线程的开销。它还提供了任务调度、线程池大小控制等功能。

5、并发集合

在Java中,有几种并发集合类可用于在多线程环境中安全地进行数据访问。这些并发集合提供了线程安全的数据结构,可以支持并发读取和写入操作。以下是几个常用的并发集合类:

  • ConcurrentHashMap: 这是一个线程安全的哈希表实现,可以在多个线程之间共享和修改数据。它提供了高效的并发读取和写入操作,并且不需要对整个集合进行锁定。

  • ConcurrentLinkedQueue: 这是一个线程安全的队列实现,适用于并发的生产者和消费者场景。它支持高效的无锁并发操作,并且具有良好的性能。

  • CopyOnWriteArrayList: 这是一个线程安全的动态数组实现,适用于读多写少的场景。每次修改集合时,它都会创建一个新的副本,因此读取操作不会被阻塞。它适合于遍历操作频繁的场景。

  • ConcurrentSkipListSet: 这是一个基于跳表的线程安全的有序集合实现。它提供了对有序元素的高效并发访问,并且支持高效的范围查询操作。

这只是一些常见的并发集合类的示例,Java中还有其他许多并发集合类可供选择,具体取决于你的需求和使用场景。

import java.util.concurrent.ConcurrentHashMap;

public class Main {
    
    
    public static void main(String[] args) {
    
    
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 添加数据
        map.put("A", 1);
        map.put("B", 2);
        map.put("C", 3);

        // 获取数据
        System.out.println("Value of A: " + map.get("A"));
        System.out.println("Value of B: " + map.get("B"));
        System.out.println("Value of C: " + map.get("C"));

        // 修改数据
        map.put("A", 10);
        System.out.println("Modified value of A: " + map.get("A"));

        // 删除数据
        map.remove("B");
        System.out.println("Value of B after removal: " + map.get("B"));

        // 遍历数据
        System.out.println("All entries:");
        for (String key : map.keySet()) {
    
    
            int value = map.get(key);
            System.out.println(key + ": " + value);
        }
    }
}

在上面的示例中,我们创建了一个ConcurrentHashMap对象,并使用put()方法添加键值对。然后,通过get()方法获取指定键的值,并使用put()方法修改某个键的值。我们还使用remove()方法删除了一个键值对。

最后,通过遍历ConcurrentHashMap的键集合,我们可以打印出所有的键值对。

猜你喜欢

转载自blog.csdn.net/weixin_44727769/article/details/131129156