Java 多线程学习(六)

简介: Java 多线程学习

5.线程通信

应用场景:生产者和消费者问题

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。
  • 如果仓库中没有产品,则将生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。
  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费,直到仓库中再次放入产品为止。

这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。

  • 对于生产者,没有生产产品之前,要通知消费者等待.而生产了产品之后,又需要马上通知消费者消费
  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费.
  • 在生产者消费者问题中,仅有synchronized是不够的
    synchronized 可阻止并发更新同一个共享资源,实现了同步
    synchronized 不能用来实现不同线程之间的消息传递(通信)

5.1 解决线程之间通信问题的几个方法

注意:均是 Object 类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常 llegalMonitorStateException

\

sleep 与 wait 的区别

5.2 解决线程之间通信的方式1:管程法

并发写作模型“生产者/消费者模式”–>管程法

  • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程);
  • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程)
  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个缓冲区

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

package com.sjmp.advanced;
// 生产者,消费者,产品,缓冲区
public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Consumer(container).start();
    }
}
// 生产者
class Productor extends Thread{
    SynContainer container;
    public Productor(SynContainer container){
        this.container = container;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生产了"+i+"只鸡");
            container.push(new Chicken(i));
        }
    }
}
class SynContainer{
//    需要一个容器的大小
    Chicken[] chickens = new Chicken[10];
//    容器计数器
    int count = 0;
//    生产者放入产品
    public synchronized void push(Chicken chicken){
//        如果容器满了,就需要等待消费者消费
        if (count == chickens.length){
//            通知消费者消费,生产等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        chickens[count] = chicken;
        count++;
        this.notifyAll();
    }
//    消费者消费产品
    public synchronized Chicken pop(){
//        判断能否消费
        if(count==0){
//            等待生产者生产,消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//        如果可以消费
        count--;
        Chicken chicken = chickens[count];
//        可以通知消费了
        this.notifyAll();
        return chicken;
    }
}
class Consumer extends Thread{
    SynContainer container;
    public Consumer(SynContainer container){
        this.container = container;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了-->"+container.pop().id+"只鸡");
        }
    }
}
// 产品
class Chicken{
    int id;  //产品编号
    public Chicken(int id){
        this.id = id;
    }
}

5.3 解决线程之间通信的方式1:信号灯法

这段代码是一个简单的生产者-消费者模型,其中演员(Player)作为生产者生产节目(voice),观众(Wathcher)作为消费者观看节目。TV 类中的 flag 变量用于控制生产者和消费者之间的配合,flag 为 true 表示可以生产,观众需要等待;flag 为 false 表示可以消费,演员需要等待。

package com.example.democrud.democurd.test01;
public class TestSync {
    public static void main(String[] args) {
        TV tv = new TV();  // 创建一个 TV 实例
        Player player = new Player(tv);  // 创建演员线程
        Watcher watcher = new Watcher(tv);  // 创建观众线程
        player.start();  // 启动演员线程
        watcher.start();  // 启动观众线程
    }
}
// 生产者 - 演员
class Player extends Thread{
    private final TV tv;  // TV 对象
    public Player(TV tv){
        this.tv = tv;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {  // 生产 20 个节目
            if(i % 2 == 0){
                this.tv.play("快乐大本营");  // 生产“快乐大本营”节目
            } else {
                this.tv.play("天天向上");  // 生产“天天向上”节目
            }
        }
    }
}
// 消费者 - 观众
class Watcher extends Thread{
    private final TV tv;  // TV 对象
    public Watcher(TV tv){
        this.tv = tv;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {  // 观看 20 个节目
            tv.watch();  // 观众观看 TV 节目
        }
    }
}
// 产品 - 节目
class TV{
    // 标记,用于控制生产者和消费者之间的配合
    // flag 为 true 表示可以生产,观众需要等待;flag 为 false 表示可以消费,演员需要等待。
    private boolean flag = true;
    private String voice;  // 节目名称
    // 生产者生产节目
    public synchronized void play(String voice){
        while(!flag){  // 如果 flag 为 false,则说明已经有其他生产者在生产,当前线程需要等待
            try {
                this.wait();  // 等待其他线程唤醒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员表演了: "+voice);
        // 生产完毕,唤醒其他被阻塞的消费者线程
        this.notifyAll();
        this.voice = voice;  // 存储节目名称
        this.flag = false;  // 修改标记,指示可以消费
    }
    // 消费者观看节目
    public synchronized void watch(){
        while(flag){  // 如果 flag 为 true,则说明已经有其他消费者在观看,当前线程需要等待
            try {
                this.wait();  // 等待其他线程唤醒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观看了: "+voice);
        // 消费完毕,唤醒其他被阻塞的生产者线程
        this.notifyAll();
        this.flag = true;  // 修改标记,指示可以生产
    }
}

这段代码实现了生产者-消费者模型,使用线程同步机制 synchronized 和 wait/notifyAll 方法来控制线程安全和数据共享。其中,演员是生产者,观众是消费者,TV 节目是产品。

为了更好地理解和优化该代码,建议从以下几个方面入手:

添加注释:为代码添加必要的注释,解释类、方法和变量的作用和功能。

简化命名:对于类、方法和变量的命名应简洁明了,能快速表达其含义。

优化代码结构:将多余的空格、括号、换行符等去掉,使代码更加紧凑易读。

增加异常处理:在代码中添加必要的异常处理,防止程序因为异常而崩溃或出错。

尝试使用更简洁的方式:可以使用 Java 并发包中的 Lock 和 Condition 类来替代 synchronized 和 wait/notifyAll 方法,实现更加简洁和高效的线程同步和互斥。

增加日志输出:在代码中增加必要的日志输出,记录程序执行过程中的关键信息,方便调试和排查问题。

引入线程池:当需要创建大量线程时,可以考虑使用线程池来避免频繁创建和销毁线程的开销,提高程序的性能和效率。

通过对代码进行优化和升级,可以使其更加易读易懂、性能更加高效。

5.4 使用线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。

可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

优点:

提高响应速度(减少了创建新线程的时间)

降低资源消耗(重复利用线程池中线程,不需要每次都创建)

便于线程管理…

  • corePoolSize:核心池的大小
  • maximumPoolSize:最大线程数
  • keepAliveTime:线程没有任务时最多保持多长时间会终止
package com.example.democrud.democurd.test01;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Testpool {
    public static void main(String[] args) {
           //创建线程池 大小为10
        ExecutorService service= Executors.newFixedThreadPool(10);
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        //2.关闭链接
        service.shutdownNow();
    }
}
class  MyThread implements  Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

5.5 补充内容

package com.sjmp.Thread01;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadFutureTest {
    public static class CallerTask implements Callable<String>{
        @Override
        public String call() throws Exception {
            return "Thread-Callable- hello";
        }
    }
    public static void main(String[] args) {
        // 创建异步任务
        FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
        new Thread(futureTask).start();
        try {
            String result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

如上代码中的 CallerTask 类实现了 Callable 接口的 call() 方法。在 main 函数内首先创建了一个 FutrueTask 对象(构造函数为 CallerTask 的实例),然后使用创建的 FutrueTask 对象作为任务创建了一个线程并且启动它,最后通过futureTask.get() 等待任务执行完毕并返回结果。

小结:使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递,而如果使用 Runnable 方式,则只能使用主线程里面被声明为 final 的变量。不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么子类不能再继承其他类,而 Runable 则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是 Futuretask 方式可以。

5.6wait() 方法

虚假唤醒

7.回顾总结

package com.example.democrud.democurd.test01;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.function.Function;
public class ThreadNew {
    public static void main(String[] args) {
//1.
        new MyThread1().start();
//2.
        new Thread(new MyThread2()).start();
//3.
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread3());
        new Thread(futureTask).start();
        try {
            Integer integer=futureTask.get();
            System.out.println(integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
    //1.继承Thread
    class MyThread1 extends Thread {
        @Override
        public void run() {
            System.out.println("我是继承Thread的方法");
        }
    }
    //2.实现Runnable的接口
    class MyThread2 implements Runnable {
        @Override
        public void run() {
            System.out.println("我是实现Runnable的接口");
        }
    }
    //3.实现callable接口
//Callable<Integer> 中的Integer和重写的call返回值同步的
    class MyThread3 implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("我是实现callable接口");
            return 10000;
        }
    }

相关文章
|
9天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
5天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9
|
9天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
5天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
9天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
22 3
|
7天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
8天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
20 1
|
9天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
9天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
36 1
|
13天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####