Java 多线程入门

进程、线程和多线程

在说进程之前,先回顾一下什么是程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的过程。

进程:执行程序的一次过程,是动态的概念。是系统资源分配的单位。

线程:一个进程中可以包括多个线程(一个进程中最少包含一个线程)。线程是 CPU 执行和调度的单位

假设我们正在观看一部电影,电影的播放就可以看做是一个进程。电影播放包括声音、图像、字幕等,这些可以看做是线程,如图所示

多线程:多线程分两种情况,多核和单核。

  • 多核:即有多个 CPU,此时的多线程是真正的多线程,不同的线程由不同的 CPU 执行,「并行」

  • 单核:只有一个 CPU,此时的多线程是模拟出来的,CPU 在线程间快速切换,进而产生了同时进行的错觉。「并发」

创建线程的方式

创建线程主要有三种方式:

  1. 继承 Thread 类
  2. 实现 Runnable 接口
  3. 实现 Callable 接口

继承 Thread 类

继承 Thread 类实现多线程包括以下三步:

  1. 自定义线程类继承 Thread 类
  2. 重写 run() 方法,run() 方法内是线程执行体
  3. 创建线程对象,调用对象的 start() 方法启动线程
public class TestThread extends Thread {
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 20; i++) {
    
    
            System.out.println("子线程第"+i+"次执行");
        }
    }

    public static void main(String[] args) {
    
    
        TestThread testThread = new TestThread();   //  创建一个线程对象
        testThread.start(); // 调用 start() 方法
        for (int i = 0; i < 20; i++) {
    
    
            System.out.println("主线程第"+i+"次执行");
        }
    }
}

代码中,子线程和主线程分别打印 20 条输出语句。在 main 方法中,先执行子线程的 start() 方法,再执行主线程的输出语句。会发现主线程和子线程的输出语句是交替输出的,而不是顺序输出的

注意:(1)线程开启不一定立即执行。(2)输出的结果取决于 CPU 的调度。

实例:多线程下载图片

首先,在 Commons IO – Download Apache Commons IO 中下载 commons-io jar包,该 jar 包可以用来帮忙开发 IO 功能。

下载 commons-io jar 包

解压后,复制文件加下的 commons-io-2.11.0.jar 包。

复制jar包

在 src 文件夹下新建一个 lib 文件夹,将刚刚复制的 jar 包粘贴进去。

在 jar 包上右键,选择 Add as Library,点击 ok 保存。

Add as Library

创建文件 DownLoadImage.class 文件,填入以下代码

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

// 多线程下载图片
public class DownLoadImage extends Thread {
    
    
    private String url; // 网络图片地址
    private String name; // 文件名

    public DownLoadImage(String url, String path) {
    
    
        this.url = url;
        this.name = path;
    }

    @Override
    public void run() {
    
     // 下载执行体
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println(name + "下载完成");
    }

    public static void main(String[] args) {
    
    
        String url = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimgo.11773.com%2Fimg2022%2F5%2F18%2F16%2F2022051834530721.jpg&refer=http%3A%2F%2Fimgo.11773.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1655650763&t=09cbd0abf9db64d6c6bbba58587a596b";
        DownLoadImage downLoadImage1 = new DownLoadImage(url, "大乔1.jpg");
        DownLoadImage downLoadImage2 = new DownLoadImage(url, "大乔2.jpg");
        DownLoadImage downLoadImage3 = new DownLoadImage(url, "大乔3.jpg");
        downLoadImage1.start();
        downLoadImage2.start();
        downLoadImage3.start();
    }
}

class WebDownloader {
    
    
    public void downloader(String url, String path) {
    
    
        try {
    
    
            FileUtils.copyURLToFile(new URL(url), new File(path));   // 将 url 中内容拷贝到文件中
        } catch (IOException e) {
    
    
            e.printStackTrace();
            System.out.println("downloader 方法 IO 异常");
        }
    }
}

代码中,定义了一个类 WebDownloader,其中的 downloader 方法用于将 url 中内容拷贝到指定文件中。在 main 方法中使用多线程,下载三张图片到不同文件下。

我们按顺序调用三个线程,三个线程分别下载图片到大乔1,大乔2,大乔3。但是最终打印的顺序,并不一定是

大乔1 下载成功,大乔2下载成功,大乔3 下载成功!

因为(1)线程开启不一定立即执行。(2)线程的调度顺序取决于 CPU

实现 Runnable 接口

实现 Runnable 接口方式包括以下三步:

  • 在类中实现 Runnable 接口
  • 实现 run 方法,编写线程执行体
  • 创建线程对象,调用 start() 方法启动线程
public class MyRunnable implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 20; i++) {
    
    
            System.out.println("第"+i+"次运行子线程");
        }
    }

    public static void main(String[] args) {
    
    
        // 创建 Runnable 接口的实现类
        MyRunnable myRunnable = new MyRunnable();
        // 通过 线程对象来开启线程 代理
        new Thread(myRunnable).start();
        for (int i = 0; i < 20; i++) {
    
    
            System.out.println("第"+i+"次运行主线程");
        }
    }
}

建议使用 Runnable 方式,因为 Java 仅支持单继承

并发问题

并发的一个经典问题是购票系统,多个线程操作同一个对象

public class BuyTickets implements  Runnable{
    
    
    private int ticketNumbers = 10;
    @Override
    public void run() {
    
    
        while (true){
    
     // 当票有存余时,即可购买
            if(ticketNumbers<=0) break;
            try {
    
    
                Thread.sleep(200) ; // 线程休眠 200 ms ,避免操作过快,使得一个线程一次性买光所有票
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"购买了第"+ticketNumbers--+"张票"); // 买票后,执行自减操作
        }
    }

    public static void main(String[] args) {
    
    
        BuyTickets buyTickets = new BuyTickets();
        new Thread(buyTickets,"购票人1").start(); // 参数 1 是线程对象,参数 2 是线程名
        new Thread(buyTickets,"购票人2").start();
        new Thread(buyTickets,"购票人3").start();
    }
}

代码中,我们使用三个线程来模拟购票过程。代码执行结果如下:

多线程模拟购票

我们发现,第一张票被同时被购票人 1 和购票人 3 购买,出现了数据紊乱,这个问题就是并发问题。

并发问题:当多个线程操作同一资源时,会出现线程不安全。

实现 Callable 接口

实现 Callable 接口包括以下步骤:

  1. 实现 Callable 接口,需要返回值类型
  2. 重写 call 方法
  3. 创建线程对象
  4. 创建执行服务:ExecutorService service = Executors.newFixedThreadPool(size); size 参数为需要创建线程池的大小。
  5. 提交执行:Future<Boolean> submit = service.submit(thread);submit() 中参数为需要执行的线程
  6. 获取结果:boolean res = result1.get()
  7. 关闭服务:service.shutdownNow; 关闭服务
import java.util.concurrent.*;

public class MyCallable implements Callable<Boolean> {
    
     // 1.实现 Callable 方法,返回类型为 Boolean
    @Override
    public Boolean call() throws Exception {
    
     // 2. 重写 call 方法
        for (int i = 0; i < 20; i++) {
    
    
            System.out.println("子线程执行第"+i+"次");
        }
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        MyCallable myCallable = new MyCallable(); // 3. 创建线程对象
        ExecutorService service = Executors.newFixedThreadPool(3); // 4. 创建执行服务,线程池大小为 3
        Future<Boolean> submit = service.submit(myCallable); // 5. 提交执行 myCallable 线程
        boolean res = submit.get(); // 6. 获取执行结果,需要抛出异常
        service.shutdownNow(); // 关闭服务
    }
}

lambda 表达式

Lambda 表达式用于简化代码,避免匿名内部类的过多定义。

函数式接口

定义:如果接口中只包含一个抽象方法,那么它就是一个函数式接口

优化:对于函数式接口,可以使用 lambda 表达式来创建该接口的对象。

当我们调用函数式接口中的方法时,可以用 lambda 表达式简化。

格式:lambda 表达式的格式很简单,object = (params)->{函数体}() 中填写所需要的参数,{}内填写函数体。

public class TestLambda {
    
    
     
    public static void main(String[] args) {
    
    
        Animal animal = null;   // 创建一个接口对象
        animal = (String food) -> {
    
    System.out.println("动物正在吃" + food);};
        animal = (String food) -> System.out.println("动物正在吃" + food); // 因为执行体只有一行代码,可以省去花括号
        animal = (food) -> System.out.println("动物正在吃" + food); // 省略数据类型
        animal = food -> System.out.println("动物正在吃" + food); // 单个参数可省略括号
    }
}
// 定义一个函数式接口
interface  Animal{
    
    
    void eating(String food);
}

静态代理

代理是指以他人的名义,在授权范围内执行权利的行为。简单来说就是以你的名义去做事。

例如,你想发表一篇专利,但你并不知道发表专利的流程。这时,你需要找专业的专利代理人以你的名义去发表专利,你只需告诉代理人你的创新点和实施例。

public class StaticProxy {
    
    
    public static void main(String[] args) {
    
    
        Agent agent = new Agent(new Subject());
        agent.publish();
    }
}

interface publishPatent{
    
    
    void publish();     //   发表专利
}

class Subject implements  publishPatent{
    
     // 真实角色,发表专利的你

    @Override
    public void publish() {
    
    
        System.out.println("发表专利");
    }
}

class Agent implements  publishPatent{
    
     // 代理人
    private publishPatent target;

    public Agent(publishPatent target) {
    
    
        this.target = target;
    }

    @Override
    public void publish() {
    
    
        System.out.println("proxy: 准备相关材料并润色");
        this.target.publish();
        System.out.println("proxy: 收取尾款");
    }
}

代码中,代理类 Agent 和实体类 Subject 都实现了 PublishPatent 接口。Agent 类中的 target指向实体对象。目标对象只实现了发专利这件事,其他细枝末节的东西由代理对象实现,这一点很像 Python 中的装饰器。

总结: (1) 真实对象和代理对象都要实现同一个接口 (2)代理对象要代理真实角色

代理的好处:(1)代理对象可以做很多真实对象做不了的事情 (2)真实对象可以专注需要自己做的事情

回顾一下,实现runnable接口的方式启动线程的方法:new Thread(mythread).start();可以发现其实这就是代理模式!

线程的状态

线程的状态一共有五种,创建、就绪、阻塞、运行和死亡。它们之间的关系如下图:

线程的五种状态

线程中的方法

方法 说明
setPriority(int newPrioruty) 更改线程的优先级
static void sleep(long mills) 在指定的毫秒数内让当前正在运行的线程休眠
void join() 等待该线程终止
static void yield() 暂停当前正在执行的线程对象,并执行其他线程
boolean isAlive() 判断线程是否处于活动状态

线程的停止

线程的停止建议使用标志位的方式来实现,例如,当 flag = false 时终止线程。主动停止线程并不一定安全,JDK 提供的 stop(),destory() 方法已经被废弃

public class TestStop implements Runnable {
    
    
    private boolean flag = true; // 执行体使用该标志位

    @Override
    public void run() {
    
    
        while (flag) {
    
    
            System.out.println("线程正在运行");
        }
    }

    public void stop() {
    
     // 修改标志位
        flag = false;
    }

    public static void main(String[] args) {
    
    
        TestStop testStop = new TestStop();
        new Thread(testStop).start();
        for (int i = 0; i < 100000; i++) {
    
    
            if (i == 52000) {
    
    
                testStop.stop();// 通过修改标志位停止线程
            }
        }
    }
}

线程的休眠

  • 线程休眠使用 sleep(time)方法,time 表示当前线程阻塞的毫秒数

  • sleep()时间到达后,线程进入就绪状态

  • sleep()可以模拟网络延时,倒计时

  • 每个对象都有一把锁,sleep()不会释放锁

线程礼让

  • yield(),让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让 CPU 重新调度,但礼让不一定成功!例如,A 礼让了,但 CPU 仍然可能调用 A 线程。

Join

  • join 合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
  • 可以想象成,一群人在医院看病,突然某领导的亲戚来了,那么该亲戚直接插到最前面,等他看完了其他人才能看。
public class TestJoin implements Runnable {
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("Vip 来咯,已看病" + i + "秒");
        }
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        TestJoin testJoin = new TestJoin();
        Thread vip = new Thread(testJoin);
        vip.start();
        for (int i = 0; i < 1000; i++) {
    
    
            if (i == 100) vip.join();
            System.out.println("普通人已看病" + i + "秒");

        }
    }
}

Thread.State

Java 提供了 Thread.State 来观测线程当前的状态

状态 说明
NEW 创建状态
RUNNABLE 运行状态
BLOCKED 阻塞状态
WAITING 等待另一个线程执行特定动作时的状态
TIMED_WAITING 等待另一个线程执行动作到达指定等待时间的状态
TERMINATED 死亡状态
public class TestState {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread = new Thread(()->{
    
     // 创建一个线程,使用 lambda 表达式
            for (int i = 0; i < 10; i++) {
    
    
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("子线程执行第"+i+"次");
            }
        });
        Thread.State state = thread.getState();
        System.out.println(state);
        thread.start();
        state = thread.getState();
        System.out.println(state);
        while (state != Thread.State.TERMINATED){
    
     // 当线程没有死亡,持续运行
            state = thread.getState();
            System.out.println(state);
        }
        thread.start(); // 这里会报错,因为死亡的线程无法重新启动 
    }
}

需要注意的是:死亡后的线程无法重新启动。线程有点像一块干电池,用完了就无了。

线程优先级

Java 提供一个线程调度器来检测程序中所有已就绪线程的状态,线程调度器按照优先级来决定优先调度哪个线程

线程的优先级用数字 1~10 来表示,其中

  • Thread.MIN_PRIORITY = 1;

  • Thread.MAX_PRIORITY = 10;

  • Thread.NORM_PRIORITY = 5

  • thread.getPriority() 用于获取优先级,thread.setPriority(priority) 用于设置优先级

注意:并不是优先级高的就一定会执行,只是优先级更高的有更高的概率被执行。

// 测试线程优先级
public class TestPriority implements Runnable{
    
    

    public static void  printPriority(){
    
    
        System.out.println(Thread.currentThread().getName()+"的优先级是"+Thread.currentThread().getPriority());
    }

    @Override
    public void run() {
    
    
        printPriority();
    }

    public static void main(String[] args) {
    
    
        printPriority();
        TestPriority testPriority = new TestPriority();
        Thread t1 = new Thread(testPriority,"t1");
        t1.start();
        Thread t2 = new Thread(testPriority,"t2");
        t2.setPriority(Thread.MIN_PRIORITY);
        t2.start();
        Thread t3 = new Thread(testPriority,"t3");
        t3.setPriority(Thread.MAX_PRIORITY);
        t3.start();
        Thread t4 = new Thread(testPriority,"t4");
        t4.setPriority(-1); // 这里会抛出异常,优先级范围是 1~10
        t4.start();
        Thread t5 = new Thread(testPriority,"t5");
        t5.setPriority(11);
        t5.start(); // 这里会抛出异常,优先级范围是 1~10
    }
}

守护线程(daemon)

线程可以分为 用户线程守护线程

在 Java 虚拟机中,虚拟机必须确保用户线程执行完毕,但不用等待守护线程执行完毕。守护线程可用于记录操作日志,监控内存,垃圾回收等。

可以这样理解,守护线程是一个爱你的天使,他是永生的。他的职责就是保护你,当你(用户线程)不在了,他失去了存在的价值,也就消失了。

import org.junit.experimental.theories.Theories;

public class TestDaemon {
    
    
    public static void main(String[] args) {
    
    
        Thread angel = new Thread(new Angel());
        angel.setDaemon(true); // 天使是守护线程
        angel.start();
        new Thread(new You()).start();
    }

}

class Angel implements Runnable{
    
    

    @Override
    public void run() {
    
    
        while (true) System.out.println("天使一直守护着你");
    }
}

class You implements  Runnable{
    
    

    @Override
    public void run() {
    
    
        for (int i = 0; i < 5000; i++) {
    
    
            System.out.println("你在爱里开心的活着");
        }
        System.out.println("你松开了天使的手");
    }
}

代码中,当 You 线程死亡后,Angel 线程还运行了一段时间才消失,因为 JVM 的停止需要一段时间

线程同步

当多个线程操作同一个对象时,可能出现线程安全问题。

例子:假设,山上有一个厕所,有很多人想要方便。这个厕所可以看作需要操作的对象,多个人可以看作是多个线程。如果多个人去抢这个厕所,就会打架。为了避免打架,你们想了一个办法——排队。谁排在前谁先上厕所,这样最公平。但是还存在一个问题,上厕所的时候需要一个安静的环境,没有人希望方便到一半的时候,另一个人闯进来要方便。所以,在厕所上加了一把锁,方便的时候上锁,方便完解开锁。

从上面的例子中,我们可以看出解决线程安全问题需要队列和锁

ArrayLIst<> 的线程不安全

我们都知道,ArrayList<> 是线程不安全的,但是具体为什么不安全呢?看下面的代码

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class TestArrayList {
    
    
    public static void main(String[] args) {
    
    
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
    
    
            new Thread(()->{
    
    
                list.add(Thread.currentThread().getName());
            }).start();
        }
        System.out.println(list.size());
    }
}

我们定义了一个 String 类型的 ArrayList,创建了10000 个线程往里加入该线程的名字。执行完毕后,打印列表的长度。我们发现最终的结果不是我们期望的 10000。因为出现了线程不安全问题,多个线程往同一个位置写填入了数据,导致了数据的覆盖。

synchronized 关键字

synchronized关键字可以放在方法或代码块前,用于对声明的部分加上同步锁。在操作对象前加锁,操作结束后释放锁。用 synchronized 解决 ArrayList<> 中的线程不安全问题

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class TestArrayList {
    
    
    public static void main(String[] args) {
    
    
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
    
    
            new Thread(()->{
    
    
                synchronized (list) {
    
    
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        System.out.println(list.size());
    }
}

注意:synchronized关键字一定要加到修改的变量上,才能生效。

死锁

死锁指的是多个线程各自占有一些共享资源,并且需要互相等待其他线程释放已占有的资源才能运行,从而导致两个或多个线程都在等待对方释放资源而停止执行的现象。

案例(黑货交易)

一个贴切的例子是黑货交易,卖家要先付钱在交货,买家要先给货后给钱,这样双方就陷入了僵持。代码如下

public class TestDeadLock {
    
    
    public static void main(String[] args) {
    
    
        Trade seller = new Trade("卖家", 0);
        Trade buyer = new Trade("买家", 1);
        seller.start();
        buyer.start();
    }
}

class Money {
    
     // 金钱类
}

class Goods {
    
     // 商品类
}

class Trade extends Thread {
    
    
    private static Money money = new Money();
    private static Goods goods = new Goods();
    String name; // 交易方的名字
    int method; // 当 method = 0 表示先钱后货,method = 1 表示先货后钱

    public Trade(String name, int method) {
    
    
        this.name = name;
        this.method = method;
    }

    @Override
    public void run() {
    
    
        if (method == 0) {
    
    
            synchronized (money) {
    
    
                System.out.println(name + "需要先给钱");
                try {
    
    
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                synchronized (goods) {
    
    
                    System.out.println(name + "后给货");
                }
            }
        } else {
    
    
            synchronized (goods) {
    
    
                System.out.println(name + "需要先给货");
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                synchronized (money) {
    
    
                    System.out.println(name + "后给钱");
                }
            }
        }
    }
}

死锁产生的必要条件

  • 互斥:一个资源每次只能被一个线程使用
  • 请求与保持条件:一个线程因为请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:进程已经获取的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

只要破坏上面任意一个条件,死锁就能解除。

Lock()

从 JDK 5.0 开始,Java 提供了更强大的线程同步机制,即通过显示定义同步锁来实现同步。最常用的一种方式是 ReentrantLock 类实现的 Lock,ReentrantLock 译作可重入锁。

import java.util.concurrent.locks.ReentrantLock;

 class BuyTickets implements Runnable {
    
    
    private int ticketNumbers = 10;
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
    
    
        while (true) {
    
     // 当票有存余时,即可购买
            try {
    
    
                lock.lock();
                if (ticketNumbers > 0) {
    
    
                    try {
    
    
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "买到了第" + ticketNumbers-- + "张票");
                }else{
    
    
                    break;
                }
            }finally {
    
    
                lock.unlock();
            }
        }
    }

}

public  class Test{
    
    
    public static void main(String[] args) {
    
    
        BuyTickets buyTickets = new BuyTickets();
        new Thread(buyTickets, "购票人1").start(); // 参数 1 是线程对象,参数 2 是线程名
        new Thread(buyTickets, "购票人2").start();
        new Thread(buyTickets, "购票人3").start();
    }
}

注意:一般在finallY{}代码块中释放锁。

synchronized 与 Lock 的对比

  • Lock 是显示锁,synchronized 是隐式锁。显示锁需要手动开启和关闭,隐式锁出了作用域自动释放

  • Lock 只有代码块锁,没有方法锁

  • Lock 锁的性能开销更小

线程协作

生产者消费者问题

假设我们有一个仓库,仓库中只能存放一件产品,生产者负责将生产的产品放入仓库,消费者负责将仓库中的产品拿走;如果仓库中没有产品,生产者就将产品放入仓库,否则停止生产并等待,直到消费者将产品拿走;如果仓库中有产品,消费者就将产品拿走并消费,否则等待,直到仓库中有产品。

生产者消费者问题

为了解决这个问题,线程间需要相互通信。

Java 提供的线程间通信方法

方法名 作用
wait() 该线程一直等待,直到被通知;与 sleep()不同,它会释放锁
wait(long timeout) 等待 timeout 毫秒
notify() 唤醒一个处于等待状态的线程
notifyAll() 唤醒同一个对象上所有调用 wait() 方法的线程,优先级高的线程优先调度

注意:这些都是 Object 类中的方法,都只能在同步方法或者同步代码块中使用,否则会报错

解决方式1 管程法

所谓管程法,就是使用缓冲区的方式来解决生产者消费者问题。生产者将产品放入缓冲区,消费者从缓冲区取出产品。如果缓冲区没满,生产者就生产产品放入缓冲区;如果缓冲区满,就等待。如果缓冲区为空,消费者就等待;否则消费者就消费缓冲区中的产品。

import java.util.List;

public class UseBuffer {
    
    
    public static void main(String[] args) {
    
    
        Buffer buffer = new Buffer();
        Thread provider = new Thread(new Providers(buffer));
        Thread consumer = new Thread(new Consumer(buffer));
        provider.start();
        consumer.start();
    }
}
// 生产者和消费者保存同一个缓冲区
class Providers implements Runnable {
    
    
    Buffer buffer;  

    public Providers(Buffer buffer) {
    
    
        this.buffer = buffer;
    }

    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            buffer.push(new Product(i));
            System.out.println("生产了" + (i + 1) + "件产品");
        }
    }
}

class Consumer implements Runnable {
    
    
    Buffer buffer;

    public Consumer(Buffer buffer) {
    
    
        this.buffer = buffer;
    }

    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            Product product = buffer.remove();
            System.out.println("消费了" + (i + 1) + "件产品");
        }
    }
}

class Product {
    
    
    int id; // 产品 id

    public Product(int id) {
    
    
        this.id = id;
    }
}

class Buffer {
    
    
    Product[] products = new Product[10]; // 此处为缓冲区,缓冲区大小为 10
    int count = 0;// 记录容器中已放入产品的个数

    // 生产者放入产品
    public synchronized void push(Product product) {
    
     // 如果缓冲区没有满放入产品,否则等待
        if (count == products.length) {
    
    
            try {
    
    
                this.wait(); // 缓冲区满,生产者等待
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        products[count] = product;
        count++;
        this.notifyAll();       // 通知消费者消费
    }

    public synchronized Product remove() {
    
    
        if (count == 0) {
    
    
            try {
    
    
                this.wait(); // 缓冲区为空,消费者等待
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        count--;
        this.notifyAll(); // 通知生产者生产
        return products[count];
    }
}

解决方式2 信号灯法

信号灯法实际上就是通过标志位来通知生产或消费。

// 信号灯法
public class TestSignal {
    
    

    public static void main(String[] args) {
    
    
        Product product = new Product();
        Thread provider = new Thread(new Provider(product));
        Thread consumer = new Thread(new Consumer(product));
        provider.start();
        consumer.start();
    }
}

class Provider implements Runnable {
    
    
    Product product;

    public Provider(Product product) {
    
    
        this.product = product;
    }

    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            product.produce(i+1);
        }
    }
}

class Consumer implements Runnable {
    
    
    Product product;

    public Consumer(Product product) {
    
    
        this.product = product;
    }

    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            product.consume(i+1);
        }
    }
}

class Product {
    
    
    boolean flag; // flag 为 true 表示有产品,消费者消费,生产者等待; false 无产品 生产者生产,消费者等待

    public synchronized void produce(int no) {
    
    
        if (flag) {
    
        // flag 为 true 阻塞生产者
            try {
    
    
                this.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        System.out.println("生产者生产了产品"+no);
        this.notifyAll();
        this.flag = !this.flag;
    }

    public synchronized void consume(int no){
    
    
        if (!flag) {
    
        // flag 为 false 阻塞消费者
            try {
    
    
                this.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
        System.out.println("消费者消费了产品"+no);
        this.notifyAll();
        this.flag = !this.flag;
    }
}

线程池

背景:前面提到过,死亡的线程无法重新运行。如果需要执行相同的任务,需要重新创建一个线程。频繁的创建和销毁线程,对性能的影响很大。

解决方案:提前创建好多个线程,放在一个池子里,需要使用的时候从池子里取,使用完了放回池子。这个池子就是线程池

好处

  • 提高响应速度(减少了线程创建的时间)
  • 降低资源消耗,复用线程池中的线程
  • 便于线程管理
    • corePoolSize 核心池的大小
    • maximunPoolSize 最大线程数
    • keepAliveTime 线程没有任务时最多保持多长时间后会终止
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestPool {
    
    

    public static void main(String[] args) {
    
    
        ExecutorService service = Executors.newFixedThreadPool(10);// 池子的大小
        for (int i = 0; i <20 ; i++) {
    
    
            service.execute(new Thread(new MyThread()));
        }
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        service.shutdownNow();
    }
}

class MyThread implements Runnable{
    
    

    @Override
    public void run() {
    
    
        System.out.println("正在执行子线程"+Thread.currentThread().getName());
    }
}

注意:service.shutdownNow() 运行在主线程,service 关闭后,可能还有线程没有放入到 service 中执行。删除掉 第11~15行代码就能观察到此现象。

猜你喜欢

转载自blog.csdn.net/jiaweilovemingming/article/details/125064021