Java 多线程

狂神Java基础之多线程学习笔记

多线程概念

线程简介

Process(进程)与Thread(线程)
程序: 程序时指令和数据的有序集合, 本身没有任何运行的含义, 是一个静态的概念;
进程: 执行程序的一次执行过程, 是一个动态的概念, 是系统资源分配的单位;
线程: 通常一个进程中可以包含若干个线程, 至少有一个线程, 线程是CPU调度和执行的单位.
注意: 很多多线程是模拟出来的, 真正的多线程是指有多个CPU, 即多核, 如服务器. 如果是模拟出来的多线程, 即在一个CPU的情况下, 在同一个时间点, CPU只能执行一个代码, 因为切换的很快, 所以就有同时执行的错觉.
在这里插入图片描述

普通方法调用和多线程

多线程核心概念

♦ 线程就是独立的执行路径;
♦ 在程序运行时, 即使没有自己创建线程, 后台也会有多个线程, 如主线程, gc线程;
♦ main()称之为主线程, 为系统的入口, 用于执行整个程序;
♦ 在一个进程中, 如果开辟了多个线程, 线程的运行由调度器安排调度, 调度器是与操作系统紧密相关的, 先后顺序是不能人为干预的;
♦ 对同一份资源操作时, 会存在资源抢夺问题, 需要加入并发控制;
♦ 线程会带来额外的开销, 如CPU调度时间, 并发控制开销;
♦ 每个线程在自己的工作内存交互, 内存控制不当会造成数据不一致.

线程创建

线程有三种创建方式:

  1. 继承Thread类
  2. 实现Runnable接口(最优)
  3. 实现Callable接口

继承Thread类创建线程

♦ 自定义线程类继承Thread类;
♦ 重写run()方法, 编写线程执行体;
♦ 创建线程对象, 调用start()方法启动线程.

/**
 * 总结: 调用start()方法才会开启子线程
 * 注意, 线程开启不一定立即执行, 由CPU进行调度安排
 */

//创建线程方式一: 继承Thread类, 重写run()方法, 调用start开启线程
public class TestThread1 extends Thread{
    @Override
    public void run() {
        //run方法线程体
        for (int i = 1; i <= 500; i++) {
            System.out.println("测试线程运行第" + i + "下");
        }
    }

    public static void main(String[] args) {
        //main线程, 主线程

        //创建一个线程对象
        TestThread1 test = new TestThread1();
        //调用start方法开启线程
        /*
         * 调用start(), 子线程来执行run()方法, 即主线程与子线程"并行"交替执行
         */
        test.start();

        //调用run方法开启线程
        /*
          调用run(), 主线程来执行run()方法, 执行完后再执行后面的语句, 即只有主线程一条执行路径
         */
        //test.run();
        for (int i = 1; i <= 1000; i++) {
            System.out.println("主线程运行第" + i + "下");
        }
    }
}

示例一: 打印数字
import org.apache.commons.io.FileUtils;

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

//练习Thread, 实现多线程同步下载图片
public class TestThread2 extends Thread {
    private String url;  //网络图片地址
    private String name; //保存的文件名

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

    //下载图片线程的执行体
    @Override
    public void run() {
        WebDownloader wd = new WebDownloader();
        wd.downloader(url, name);
        System.out.println("下载图片的文件名为:" + name);
    }

    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2("https://img0.baidu.com/it/u=3072076516,914193103&fm=253&fmt=auto&app=138&f=JPEG?w=1080&h=469", "C:/Users/imkid/Desktop/pic1.jpg");
        TestThread2 t2 = new TestThread2("https://img2.baidu.com/it/u=2896585310,1938945066&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=763", "C:/Users/imkid/Desktop/pic2.jpg");
        TestThread2 t3 = new TestThread2("https://img1.baidu.com/it/u=4211852519,111165463&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=625", "C:/Users/imkid/Desktop/pic3.jpg");

        t1.start();
        t2.start();
        t3.start();

        /*
         * 可以看到下载顺序是图片大小由小到大, 而不是按照代码顺序.
         */
    }


}
class WebDownloader {
    //下载方法
    public void downloader(String url, String name) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO异常, downloader方法出现问题.");
        }
    }
}
示例二: 下载图片

实现Runnable接口创建线程

/**
 * 总结: 推荐使用实现Runnable接口的方法, 这样可以避免java单继承的局限性,
 * 并且灵活方便, 方便同一个对象被多个线程使用, 例如"一份资源, 多个代理"
 */

//创建线程方式2: 实现Runnable接口, 重写run()方法, 执行线程需要传入Runnable接口实现类, 调用start()方法.
public class TestThread3 implements Runnable {
    public void run() {
        //run方法线程体
        for (int i = 1; i <= 500; i++) {
            System.out.println("测试线程运行第" + i + "下");
        }
    }

    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        TestThread3 test = new TestThread3();
        //创建线程对象, 通过线程对象来开启我们的线程, 这就是代理
        //Thread thread = new Thread(test);
        //thread.start();
        new Thread(test).start();

        for (int i = 1; i <= 1000; i++) {
            System.out.println("主线程运行第" + i + "下");
        }
    }
}

两者对比

在这里插入图片描述

线程不安全示例

//多个线程同时操作同一个对象
//买火车票的例子
//发现问题: 多个线程操作同一个资源的情况下, 线程不安全, 数据紊乱
public class TestThread4 implements Runnable {
    private int ticket_nums = 10;

    @Override
    public void run() {
        while (true) {
            if (ticket_nums <= 0) {
                break;
            }
            //cpu执行太快可能不会出现问题, 所以人工干预下以便暴露并发问题
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "拿到了第" + ticket_nums-- + "票");
        }
    }

    public static void main(String[] args) {
        TestThread4 test = new TestThread4();
        new Thread(test, "A").start();
        new Thread(test, "B").start();
        new Thread(test, "C").start();
    }
}

模拟龟兔赛跑

//模拟龟兔赛跑
public class Race implements Runnable {
    private static String winner;

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            //模拟兔子睡觉, 跑30米休息一次
            if (Thread.currentThread().getName().equals("兔子") && (i % 30 == 0)) {
                System.out.println("兔子开始休息了");
                try {
                    Thread.sleep(10);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName() + "--> 跑了" + i + "步");
            if (gameOver(i)) System.exit(0);
        }
    }

    //判断是否完成比赛
    private boolean gameOver(int step) {
        if (winner != null) {
            return true;
        }
        if (step >= 100) {
            winner = Thread.currentThread().getName();
            System.out.println("胜利者是" + winner);
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();
        new Thread(race, "兔子").start();
        new Thread(race, "乌龟").start();
    }
}

实现Callable接口创建线程

在这里插入图片描述

import java.util.concurrent.*;
//实现Callable接口
public class TestCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        for (int i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName() + "打印了" + i);
        }
        return 1;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable t1 = new TestCallable();
        TestCallable t2 = new TestCallable();

        //创建执行服务
        ExecutorService service = Executors.newFixedThreadPool(2);
        //提交执行
        Future<Integer> f1 = service.submit(t1);
        Future<Integer> f2 = service.submit(t2);
        //获取结果
        int i1 = f1.get();
        int i2 = f2.get();

        /*//也可以不获取结果直接执行
        service.submit(t1);
        service.submit(t2);*/

        //关闭服务
        service.shutdown();
        System.out.println(i1 + i2);
    }
}

静态代理

代理模式总结:
真实对象和代理对象都要实现同一个接口;
代理对象要代理真实角色;
好处:
代理对象可以做很多真实对象做不了的事;
真实对象专注做自己的事情;

public class StaticProxy {
    public static void main(String[] args) {
        WeddingCompany wc = new WeddingCompany(new Someone());
        wc.marry();
    }
}

interface Marry {
    void marry();
}

class Someone implements Marry {
    @Override
    public void marry() {
        System.out.println("某人结婚");
    }
}

class WeddingCompany implements Marry {
    private Marry customer;

    public WeddingCompany(Someone customer) {
        this.customer = customer;
    }

    @Override
    public void marry() {
        System.out.println("结婚前");
        customer.marry();
        System.out.println("结婚后");
    }
}

Lambda表达式

函数式接口: 任何接口, 如果只包含唯一一个抽象方法, 那么它就是一个函数式接口(Functional Interface). 例如Runnable接口只有一个run()方法, 它就是一个函数式接口.
对于函数式接口, 可以通过Lambda表达式来创建该接口的对象

为什么要使用Lambda表达式?

  • 避免匿名内部类定义过多
  • 可以让代码看起来更简洁
  • 去掉一堆无意义的代码, 只留下核心逻辑

语法格式:
(parameters) -> expression

(parameters) ->{ statements; }

以下是lambda表达式的重要特征:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值。
public class TestLambda {
    public static void main(String[] args) {
        Move dog = () -> {
            System.out.println("If dog:");
            System.out.println("Then run");
        };
        dog.move();

        Calculate add = (a, b) -> {
            System.out.println("加法");
            return a + b;
        };
        System.out.println(add.calculate(1, 2));

        add = (a, b) -> a * b;
        System.out.println(add.calculate(3, 4));;
    }
}

interface Move {
    void move();
}

interface Calculate {
    int calculate(int a, int b);
}

线程状态

线程有五大状态
在这里插入图片描述在这里插入图片描述

观测线程状态

Thread.State 线程状态 , 线程可以处于以下状态之一:
• NEW
尚未启动的线程处于此状态。
• RUNNABLE
在Java虚拟机中执行的线程处于此状态。
• BLOCKED
被阻塞等待监视器锁定的线程处于此状态。
• WAITING
正在等待另一个线程执行特定动作的线程处于此状态。
• TIMED_WAITING
正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
• TERMINATED
已退出的线程处于此状态。

一个线程可以在给定时间点处于一个状态。 这些状态是不反映任何操作系统线程状态的虚拟机状态。

public class TestState {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("**********");
            }
        });

        //观察状态
        Thread.State state = thread.getState();
        System.out.println(state);
        thread.start();
        System.out.println(thread.getState());

        while (state != Thread.State.TERMINATED) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            state = thread.getState();  //更新线程状态
            System.out.println(state);
        }
    }
}

线程相关操作

在这里插入图片描述

停止线程

  • 不推荐jdk提供的stop()、destroy()方法[已废弃]
  • 推荐线程自己停止下来
  • 建议使用一个标志位进行终止变量, if (!flag) 线程终止
public class TestStop implements Runnable{
    //1. 线程中定义线程体使用的标记
    private boolean flag = true;

    @Override
    public void run() {
        for (int i = 1; i <= 1000; i++) {
            //2. 线程体使用该标记
            if (!flag) break;
            System.out.printf("线程运行%d次\n", i);
        }
    }

    //3. 对外提供方法改变标记
    public void stop() {
        flag = false;
    }

    public static void main(String[] args) {
        TestStop test = new TestStop();
        new Thread(test).start();

        for (int i = 1; i < 1000; i++) {
            if (i == 500) {
                //调用stop()方法切换标志位, 让线程停止
                test.stop();
                System.out.println("线程停止了");
            }
            System.out.printf("main线程运行%d次\n", i);
        }
    }
}

线程休眠

  • sleep(time)指定当前线程阻塞的毫秒数
  • sleep存在异常InterruptedException
  • sleep时间达到后线程进入就绪状态
  • sleep可以模拟网络延时, 倒计时等
  • 每一个对象都有一个锁, sleep不会释放锁
import java.text.SimpleDateFormat;
import java.util.Date;

//模拟计时
public class TestSleep {
    public static void main(String[] args) {
        Date date = new Date(System.currentTimeMillis());
        //Date date = new Date();  //等价于上一条语句
        System.out.println(date);  //Mon Sep 19 22:08:05 CST 2022

        //运行十秒程序结束
        new Thread(() -> {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.exit(0);
        }).start();

        while (true) {
            System.out.println(new SimpleDateFormat("yyyy.MM.dd hh:mm:ss").format(date));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            date.setTime(System.currentTimeMillis());
        }
    }
}

线程礼让

  • 线程礼让, 让当前正在执行的线程暂停, 但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让CPU重新调度, 礼让不一定成功, 看CPU心情
public class TestYield implements Runnable {
    @Override
    public void run() {
        System.out.printf("线程%s开始执行\n", Thread.currentThread().getName());
        //线程礼让
        Thread.yield();
        System.out.printf("线程%s执行完毕\n", Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        TestYield test = new TestYield();
        new Thread(test, "A").start();
        new Thread(test, "B").start();
    }
}
线程A开始执行
线程B开始执行
线程B执行完毕
线程A执行完毕

线程合并(强制立即执行)

  • join()合并线程, 待此线程执行完成后, 再执行其他线程, 其他线程阻塞
  • 可以想象成插队
public class TestJoin implements Runnable {
    @Override
    public void run() {
        System.out.println("VIP线程来了");
        for (int i = 1; i <= 200; i++) {
            System.out.println("VIP线程输出了:" + i);
        }
    }

    public static void main(String[] args) {
        TestJoin test = new TestJoin();
        Thread thread = new Thread(test, "vip");
        thread.start();


        for (int i = 1; i < 400; i++) {
            if (i == 200) {
                System.out.println("VIP线程来了");
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.printf("%s输出了%d\n", Thread.currentThread().getName(), i);
        }
    }
}

守护线程

  • 线程分为用户线程守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如后台记录操作日志, 监控内存, 垃圾回收等待…
public class TestDaemon {
    public static void main(String[] args) {
        Thread thread = new Thread(new Daemon());
        thread.setDaemon(true);  //默认是false表示用户线程, 正常的线程都是用户线程
        thread.start();

        new Thread(new NormalThread()).start();
    }
}
class NormalThread implements Runnable {
    @Override
    public void run() {
        System.out.println("普通线程开始运行, 5s后结束");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("普通线程结束运行");
    }
}
class Daemon implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("守护线程正在运行");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

线程同步

并发: 同一个对象多个线程同时操作, 例如多人一起抢票

♦ 处理多线程问题时, 多个线程访问同一个对象, 并且某些线程还想修改这个对象. 这时候就需要线程同步. 线程同步其实就是一种等待机制, 多个需要同时访问此对象的线程进入这个对象的等待池形成队列, 等待前面线程使用完毕, 下一个线程再使用.

♦ 由于同一进程的多个线程共享同一块存储空间, 在带来方便的同时也带来了访问冲突问题, 为了保证数据在方法中被访问时的正确性, 在访问时加入锁机制synchronized, 当一个线程获得对象的排它锁, 独占资源, 其他线程必须等待, 使用后释放锁即可, 但是存在以下问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起;
  • 在多线程竞争下, 加锁, 释放锁会导致比较多的上下文切换和调度延时, 引起性能问题;
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁, 会导致优先级导致, 引起性能问题.

同步方法与synchronized关键字

♦ 由于可以通过private关键字来保证数据对象只能被方法访问, 所以只需要针对方法提出一套机制, 这套机制就是synchronized关键字, 它包括两种用法: synchronized方法和synchronized块:
同步方法: public synchronized void method(int args) {}
♦ 互斥锁: java中引入了对象互斥锁的概念, 来保证共享数据操作的完整性, 每一个对象都对应于一个可称为"互斥锁"的标记, 这个标记用来保证在任一时刻, 只能有一个线程访问该对象.
♦ 关键字synchronized来与对象的互斥锁联系, 当某个对象用synchronized修饰时, 表明该对象在任一时刻只能由一个线程访问. 每个对象对应一把锁, 每个synchronized方法都必须获得调用该方法的对象的锁才能执行, 否则线程会阻塞, 方法一旦执行, 就独占该锁, 直到方法返回才释放锁, 后面被阻塞的线程才能获得这个锁, 继续执行.

缺陷: 若将一个大的方法申明为synchronized将会影响效率

♦ 方法里需要修改的内容才需要锁, 锁的太多浪费资源
♦ 同步的局限性: 导致程序的执行效率降低
♦ 非静态的同步方法的锁可以是this, 也可以是其他对象(要求是同一个对象)
♦ 静态的同步方法的锁为当前类本身

public synchronized static void fun() {
	...
}

public synchronized void fun() {
	synchronized (ClassName.class) {
	...
	}
}

• 注意事项和细节

  1. 同步方法如果没有使用static修饰, 默认锁对象为this
  2. 如果方法使用static修饰, 默认锁对象为当前类.class
  3. 实现的落地步骤:
    • 需要先分析上锁的代码
    • 选择同步代码块或同步方法
    • 要求多个线程的锁对象为同一个即可
  4. synchronized锁为非公平锁

买票案例:

public class BuyTicket implements Runnable {
    private int ticket_num = 50;
    @Override
    public void run() {
        while (ticket_num > 0) {  //此处判断完之后, 当前线程下的ticket_num还是可能改变, 所以下面的方法里面还需要判断一次
            buyTicket();
            try {
                Thread.sleep(20);  //sleep方法要在同步代码块之外
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("票卖完了");
    }

    private synchronized void buyTicket() {   //synchronized方法
        if (ticket_num > 0) {  //这里需要加个判断, 不加判断的话, AB线程如果同时进入循环, A先执行buyTicket方法, 票卖完了, B还会继续卖, 因为B已经进入循环了, 不会再判断循环条件
            System.out.println(Thread.currentThread().getName() + "售出了一张票, 还剩" + --ticket_num + "张");
        }
    }

    private void buyTicket1() {
        synchronized (this) {  //synchronized代码块
            if (ticket_num > 0) {
                System.out.println(Thread.currentThread().getName() + "售出了一张票, 还剩" + --ticket_num + "张");
            }
        }
    }


    public static void main(String[] args) {
        BuyTicket user = new BuyTicket();
        new Thread(user, "A").start();
        new Thread(user, "B").start();
        new Thread(user, "C").start();
    }
}

取钱案例:

public class Bank extends Thread{  //继承Thread类因为开启线程的对象不是同一个对象所以无法直接用同步方法
    private User user;  //户头
    private static int account;  //储蓄
    private static final Object o = new Object();  //同步监视器

    Bank(User user, int account) {
        this.user = user;
        this.account = account;
    }

    public void run() {
        withDraw();
    }

    public void withDraw() {  //取钱方法
        synchronized (o) {  //同步监视器
            if (account - user.getWithdraw_money() <= 0) {
                System.out.println("余额不足, 无法提取");
                return;
            }

            account -= user.getWithdraw_money();
            System.out.println(user.getName() + "取出了" + user.getWithdraw_money() + "元, 账户余额 " + account + "元");
        }
    }

    public static void main(String[] args) {
        Bank a = new Bank(new User("A", 1000), 4000);
        Bank b = new Bank(new User("B", 1000), 4000);
        Bank c = new Bank(new User("C", 2000), 4000);
        Bank d = new Bank(new User("D", 1000), 4000);

        a.start();
        b.start();
        c.start();
        d.start();
    }
}

class User {
    private String name;
    private int withdraw_money;

    User(String name, int withdraw_money) {
        this.name = name;
        this.withdraw_money = withdraw_money;
    }

    String getName() {
        return name;
    }
    int getWithdraw_money() {
        return withdraw_money;
    }
}

Lock(锁)

在这里插入图片描述

class A {
    private final ReentrantLock lock = new ReentrantLock();
    public void func() {
        lock.lock();
        try {
            //可能出现异常的代码
        } finally {
            lock.unlock();  //如果同步代码有异常, 要将unlock()写入finally语句块
        }
    }
}

synchronized与Lock的对比

在这里插入图片描述

死锁

♦ 多个线程各自占有一些共享资源, 并且互相等待其他线程占有的资源才能运行, 而导致两个或者多个线程都在等待对方释放资源, 都停止执行的情形. 某一个同步块同时拥有"两个以上对象的锁"时, 就可能会发生"死锁"的问题.
♦ 产生死锁的四个必要条件:

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

♦ 避免死锁: 破坏以上任意一个或者多个条件, 例如不要在锁里面加锁

死锁示例:

public class TestDeadLock {
    public static void main(String[] args) {
        Resource resource = new Resource(true);
        Resource resource2 = new Resource(false);

        Thread timer = new Thread(() -> {  //计时器, 一定时间后程序没有停止就说明阻塞了
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("5s过去了程序还没有停止, 说明产生了死锁, 线程阻塞了");
            System.exit(0);
        });
        timer.setDaemon(true);
        timer.start();

        resource.start();
        resource2.start();
    }
}

class Resource extends Thread {
    private final boolean flag;
    private final static Object o1 = new Object();
    private final static Object o2 = new Object();

    Resource(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        if (flag) {
            synchronized (o1) {
                System.out.println(Thread.currentThread().getName() + "得到了资源1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (o2) {
                    System.out.println(Thread.currentThread().getName() + "得到了资源2");
                }
            }
        } else {
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName() + "得到了资源2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (o1) {
                    System.out.println(Thread.currentThread().getName() + "得到了资源1");
                }
            }
        }
    }
}

线程协作

在这里插入图片描述
注意: 必须是获得的锁对象调用这些方法

生产者-消费者问题

在这里插入图片描述

解决方式1------管程法

在这里插入图片描述

//测试: 生产者消费者模型 ---> 利用缓冲区解决: 管程法
public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        Producer producer = new Producer(container);
        Consumer consumer = new Consumer(container);
        Consumer consumer2 = new Consumer(container);

        producer.start();
        new Thread(consumer, "壹号").start();
        new Thread(consumer2, "贰号").start();
    }
}

class Producer extends Thread {
    private final SynContainer container;

    Producer(SynContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {  //总共会生产100件产品
            container.push(new Product(i));
            try {
                Thread.sleep((long)(Math.random() * 1000));  //生产时间随机
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

class Consumer implements Runnable {
    private final SynContainer container;

    Consumer(SynContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "消费了产品" + container.pop().getNum());
            try {
                Thread.sleep((long)(500 + Math.random() * 500));  //消费时间随机
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

class Product {
    private final int num;  //产品标号

    Product(int i) {
        num = i;
    }

    public int getNum() {
        return num;
    }
}

class SynContainer {
    Product[] products = new Product[10];  //容器大小
    private int count = 0;

    public synchronized void push(Product product) {  //生产
        if (count == 10) {  //等待消费者消费
            try {
                this.wait();  //线程进入等待状态, 等待notify唤醒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        products[count++] = product;
        System.out.println("生产者生产了一件产品, 产品编号" + product.getNum() + ", 容器里现在有" + count + "件产品");
        this.notify();  //唤醒任一正在等待中的线程, 主流的HotSpot虚拟机默认先进先出
    }

    public synchronized Product pop() {  //消费
        while (count == 0) {
        /*
        等待生产者生产, 用while不用if, 因为两个消费者线程可能会同时卡在wait处.
        例如壹号先wait释放锁, 贰号获得锁进入同步方法也停在wait处释放锁, 然后生产者获得锁生产了一件产品, 通知壹号继续执行,
        壹号执行完后贰号执行, 这时候数组就越界了
         */
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        Product p = products[--count];
        System.out.println("消费者消费了一件产品, 产品编号" + p.getNum() + ", 容器里现在有" + count + "件产品");
        this.notifyAll();  //唤醒所有等待中的线程(实际上是被唤醒的线程执行完毕后自动唤醒下一个线程), 主流的HotSpot虚拟机默认后进先出
        return p;
    }
}

解决方式2------信号灯法

public class TestSignal {
    public static void main(String[] args) {
        Signal signal = new Signal();
        Car car = new Car(signal);
        Human human = new Human(signal);

        new Thread(car).start();
        new Thread(human).start();
    }
}

class Signal {
    boolean flag = true;  //true为绿灯, false为红灯, 绿灯车行, 红灯人行

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

class Car implements Runnable {
    private final Signal signal;

    Car(Signal signal) {
        this.signal = signal;
    }

    @Override
    public void run() {
        int i = 0;
        while (i < 30) {
            synchronized (signal) {
                if (!signal.flag) {
                    try {
                        signal.wait();  //注意这里不是this.wait(), 因为获得的锁对象是signal而不是this
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("绿灯车行");
                i++;
                signal.setFlag(!signal.flag);
                signal.notify();
            }
        }
    }
}

class Human implements Runnable {
    private final Signal signal;

    Human(Signal signal) {
        this.signal = signal;
    }

    @Override
    public void run() {
        int i = 0;
        while (i < 30) {
            synchronized (signal) {
                if (signal.flag) {
                    try {
                        signal.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("红灯人行");
                i++;
                signal.setFlag(!signal.flag);
                signal.notify();
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值