当前位置: 首页 > article >正文

Java零基础入门笔记:多线程

 前言

本笔记是学习狂神的java教程,建议配合视频,学习体验更佳。

【狂神说Java】Java零基础学习视频通俗易懂_哔哩哔哩_bilibili

第1-2章:Java零基础入门笔记:(1-2)入门(简介、基础知识)-CSDN博客

第3章:Java零基础入门笔记:(3)程序控制-CSDN博客

第4章:Java零基础入门笔记:(4)方法-CSDN博客

第5章:Java零基础入门笔记:(5)数组-CSDN博客 

第6章:Java零基础入门笔记:(6)面向对象-CSDN博客 

第7章:Java零基础入门笔记:(7)异常-CSDN博客 

-

线程

线程简介

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。线程可以与同属一个进程的其他线程共享进程所拥有的全部资源,如内存空间、文件句柄等。然而,线程也有自己的独立属性,例如线程栈、线程ID以及线程的运行状态等。一个进程可以包含多个线程,这些线程可以并发执行,从而提高程序的运行效率和响应速度。

  • 程序:指令和数据的有序集合,静态。
  • 进程(Process):程序的一次执行过程,动态。是系统资源分配的单位。
  • 线程(Thread):一个进程可包含若干线程。线程是cpu调度和执行的单位。
  • main()称之为主线程,为系统的入口,用于执行整个程序

-

  • 多任务:现实中太多这样同时做多件事情的例子了,看起来是多个任务都在做,其实本质上我们的大脑在同一时间依旧只做了一件事情。
  • 多线程:原来是一条路,慢慢因为车太多了,道路阻塞,效率极低。为了提高使用的效率,能够充分利用道路,于是加了多个车道。

-

Process与Thread(程序和线程)

  • 程序是静态的指令和数据集合,本身不具备运行含义。当程序运行时,会启动一个进程,进程是程序的动态执行过程,同时也是系统资源分配的基本单位。
  • 一个进程中至少包含一个线程,线程是CPU调度和执行的最小单位,真正负责执行任务。线程存在于进程中,而进程是由程序启动的。而main()是主线程。

程序、进程和线程的关系可以总结为:程序是静态的,运行后成为进程,进程内部由线程来执行任务。

-

注意:很多多线程是模拟出来的,真正的多线程是指有多个cpu,即多核,如服务器。如果是模拟出来的多线程,即在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错局。

-

核心概念

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

-

线程创建

有以下三种方式Thread, Runnable, Callable

-

继承Thread类

步骤:

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

下面的代码演示了如何使用多线程

  • start() 方法会调用线程的 run() 方法,并使线程进入就绪状态,等待 CPU 调度。
  • 主线程(main 方法所在的线程)会继续执行自己的逻辑
  • 直接调用 run() 方法不会启动一个新线程,而只是像普通方法一样执行 run() 中的代码。只有通过 start() 方法才能启动线程。
package thread.demo1;

// //创建线程方式一:继承Thread类,重写run()方法,调用start开始线程
public class TestThread1 extends Thread{
    public void run(){
        for (int i = 0; i < 2000; i++) {
            System.out.println("线程");
        }
    }

    public static void main(String[] args) {
        TestThread1 testThread1 = new TestThread1();    // 创建一个线程对象
        testThread1.start();       // 调用start方法开启线程
        // testThread1.run();          // run()不是多线程

        for (int i = 0; i < 2000; i++) {
            System.out.println("主进程");
        }
    }
}

当你运行这段代码时,会看到 "线程" 和 "主进程" 的输出是交错的。这是因为 TestThread1 线程和主线程是并发执行的,它们共享 CPU 时间片。中间的部分结果如下:

主进程
主进程
线程
线程
线程
线程
线程
主进程
主进程
主进程

-

多线程下载图片

/**
 * 多线程并发网络下载器
 * WebDownloader实现了网络图片下载功能;downloader方法输入为目标链接地址、下载图片文件名;结果为保存对应文件到本地
 * Thread2类中的run方法调用downloader方法;并且通过start实现多线程并发下载图片
 * 结果显而易见 执行具有无序性
 */
//多线程同步下载图片
import org.apache.commons.io.FileUtils;

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

public class Thread2 extends Thread {
    private String url;
    private String name;

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

    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("文件名" + name);
    }

    public static void main(String[] args) {
        Thread2 thread21 = new Thread2("https://img-blog.csdnimg.cn/2711126564c842689248e53bbf4d85e8.png", "t1.png");
        Thread2 thread22 = new Thread2("https://img-blog.csdnimg.cn/4b97a32c6d9f4caa8617099065d02e3c.png", "t2.png");
        Thread2 thread23 = new Thread2("https://img-blog.csdnimg.cn/419537bd2d5e4fdfaa70fb8b8ed17663.png", "t3.png");
        thread21.start();
        thread22.start();
        thread23.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单继承的局限性

  • 自定义一个线程类MyRunnable实现Runnable接口
  • 实现run()方法,编写线程执行体
  • 创建线程对象,调用start()方法启动线程

下面的代码演示了如何使用runnable接口创建线程

  1. main 方法中,创建了一个 TestThread3 对象 testThread3,它实现了 Runnable 接口。

  2. 通过 Thread 类的构造函数 new Thread(testThread3)Runnable 实现类对象传递给线程对象。

  3. 调用 thread.start() 方法启动线程。

package thread.demo1;

//创建线程方式2:实现Runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法
public class TestThread3 implements Runnable{
    public void run(){
        for (int i = 0; i < 20; i++) {
            System.out.println("线程");
        }
    }

    public static void main(String[] args) {
        TestThread3 testThread3 = new TestThread3();    // 创建runnable接口的实现类对象
        Thread thread = new Thread(testThread3);        // 创建一个线程对象,通过线程对象开启线程
        thread.start();                                 // 通过线程对象开启线程

        // 上面的代码可以简写为下面的:
        // new Thread(testThread3).start();

        for (int i = 0; i < 20; i++) {
            System.out.println("主进程");
        }
    }
}
主进程
主进程
线程
线程
线程
线程
线程
主进程
主进程
主进程

 -

下面的代码演示了如何使用多线程下载图片

import org.apache.commons.io.FileUtils;

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

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

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

    //下载图片线程的集合体
    public void run(){
        WebDownLoader webDownLoader = new WebDownLoader();
        webDownLoader.downloader(url,name);
        System.out.println("下载的文件名:"+name);
    }

    public static void main(String[] args) {
        TestThread2 t1 = new TestThread2("https://www.cup.edu.cn/images/2022-10/4d0ba385fbe44ed69c3f44ccb4b81b23.jpg", "1.jpg");
        TestThread2 t2 = new TestThread2("https://www.cup.edu.cn/news/images/2022-10/a0923c114feb4770bef9627d1e698f0a.jpg","2.jpg");
        TestThread2 t3 = new TestThread2("https://www.cup.edu.cn/images/2021-10/bcde29aad64842ac82fc8abcc32077b2.jpg","3.jpg");
        new Thread(t1).start();
        new Thread(t2).start();
        new Thread(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方法出现问题");
        }
    }
}

-

Thread和Runnable对比:

  • 继承Thread类
    • 子类继承THread类具备多线程能力
    • 启动线程:子类对象.start();
    • 不建议使用:避免OOP单继承局限性
  • 实现Runnable接口
    • 实现接口Runnable具有多线程能力
    • 启动线程:传入目标对象+Thread对象.start()
    • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

 -

抢票

下面的代码演示了如何使用 高并发模拟买火车票

package thread.demo1;

// 多个线程同时操作同一个对象
public class TestThread4 implements Runnable{
    private int ticketnums = 10;        // 票数

    public void run(){
        while(true){
            if(ticketnums <= 0){
                break;
            }
            System.out.println(Thread.currentThread().getName()+"拿到了第"+ticketnums--+"张票");
        }
    }

    public static void main(String[] args) {
        TestThread4 testThread4 = new TestThread4();

        new Thread(testThread4, "小明").start();
        new Thread(testThread4, "老师").start();
        new Thread(testThread4, "黄牛").start();
    }
}

在下面的结果中:两个人同时拿到第3张票,这是不对的。

出现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱,出现并发问题。

黄牛-->拿到了第9票
小明-->拿到了第10票
老师-->拿到了第10票
老师-->拿到了第8票
黄牛-->拿到了第7票
小明-->拿到了第8票
黄牛-->拿到了第5票
老师-->拿到了第6票
小明-->拿到了第4票
小明-->拿到了第3票
黄牛-->拿到了第3票
老师-->拿到了第3票
小明-->拿到了第2票
黄牛-->拿到了第2票
老师-->拿到了第1票

-

龟兔赛跑

这段代码模拟了一个“龟兔赛跑”的场景,通过多线程来实现兔子和乌龟的跑步过程,并判断谁是比赛的胜利者

  • 兔子在每跑 10 步时会暂停 10 毫秒,模拟“兔子睡觉”的场景。

  • 在每一步中调用 gameOver() 方法判断比赛是否结束:

    • 如果已经有胜利者,则终止当前线程。

    • 如果当前线程跑完 100 步,则将当前线程的名字设置为胜利者。

package thread.demo1;

public class Race implements Runnable{
    private static String winner;   // 胜利者

    public void run(){
        for (int i = 0; i <= 100; i++) {
            if (Thread.currentThread().getName().equals("兔子")&&i%10==0){    // 兔子睡觉
                try{
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

            boolean flag = gameOver(i);     // 判断比赛是否结束
            if (flag){      // 比赛结束就终止
                break;
            }

            System.out.println(Thread.currentThread().getName()+"跑了"+i+"步");
        }
    }

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

    public static void main(String[] args) {
        Race race = new Race();
        new Thread(race, "兔子").start();
        new Thread(race, "乌龟").start();
    }
}
乌龟跑了96步
乌龟跑了97步
乌龟跑了98步
乌龟跑了99步
winner is 乌龟

-

实现Callable()接口

步骤:

  1. 实现Callable接口,需要返回值类型;
  2. 重写call方法,需要抛出异常;
  3. 创建目标对象;
  4. 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
  5. 提交执行:Future result1 = ser.submit(t1);
  6. 获取结果:boolean r1 = result1.get()
  7. 关闭服务:ser.shutdownNow();

callable的好处:可以定义返回值,可以抛出异常

-

下面的代码演示了如何使用多线程下载图片

package lesson1;
//多线程同步下载图片
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

public class TestCallable implements Callable<Boolean> {
    private String url;
    private String name;

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

    @Override
    public Boolean call() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("文件名" + name);
        return true;
    }

    public static void main(String[] args) {
        TestCallable t1 = new TestCallable("https://img-blog.csdnimg.cn/2711126564c842689248e53bbf4d85e8.png", "t1.png");
        TestCallable t2 = new TestCallable("https://img-blog.csdnimg.cn/4b97a32c6d9f4caa8617099065d02e3c.png", "t2.png");
        TestCallable t3 = new TestCallable("https://img-blog.csdnimg.cn/419537bd2d5e4fdfaa70fb8b8ed17663.png", "t3.png");
        
        // 创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(3);
        
        // 提交执行
        Future<Boolean> result1 = ser.submit(t1);
        Future<Boolean> result2 = ser.submit(t2);
        Future<Boolean> result3 = ser.submit(t3);
        
        // 获取结果
        boolean rs1 = result1.get();
        boolean rs2 = result1.get();
        boolean rs3 = result1.get();
        
        // 关闭服务
        ser.shutdownNow();
        
    }
}

//下载器
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方法出现问题");
        }
    }
}

-

静态代理

静态代理是一种设计模式,用于在不改变原有对象(被代理对象)的基础上,通过创建一个代理对象来间接调用被代理对象的方法。代理对象和被代理对象都实现了同一个接口或继承了同一个父类,代理对象在调用被代理对象的方法时,可以在方法调用前后添加额外的逻辑,从而实现对被代理对象行为的增强或扩展。

在静态代理中,代理类和被代理类的关系是在编译时就已经确定的,代理类需要手动编写,不能动态生成。这种方式的优点是简单直观,易于理解和实现,但缺点是灵活性较差,如果被代理类的方法过多或接口频繁变化,代理类也需要相应地进行修改,增加了维护成本。

-

下面的代码演示了如何使用静态代理,这段代码通过一个简单的例子展示了静态代理的设计模式。代理类 WeddingCompany 在调用被代理类 You 的核心方法时,添加了额外的逻辑(布置现场和收尾款),从而扩展了被代理类的行为。

  • 接口 Marry

    • 定义了一个方法 HappyMarry(),表示结婚的行为。这个接口是被代理类和代理类之间的桥梁,确保它们具有相同的方法签名。

  • 被代理类 You

    • 实现了 Marry 接口,具体实现了结婚的行为(HappyMarry() 方法),打印出 "结婚!"。

  • 代理类 WeddingCompany

    • 也实现了 Marry 接口,但它的主要职责是代理 You 类的行为。

    • HappyMarry() 方法中,代理类不仅调用了被代理类的 HappyMarry() 方法,还在调用前后分别执行了额外的逻辑:

      • before() 方法:在结婚之前布置现场。

      • after() 方法:在结婚之后收尾款。

    • 代理类通过构造函数接收一个 Marry 类型的对象(被代理对象),并将其保存为成员变量 target

package thread.demo1;

public class StaticProxy {
    public static void main(String[] args) {
        WeddingCompany weddingCompany = new WeddingCompany(new You());
        weddingCompany.HappyMarry();
    }
}

interface Marry{
    void HappyMarry();
}

class You implements Marry{
    public void HappyMarry(){
        System.out.println("结婚!");
    }
}

class WeddingCompany implements Marry{
    private Marry target;

    public WeddingCompany(Marry target){
        this.target = target;
    }

    @Override
    public void HappyMarry() {
        before();
        this.target.HappyMarry();
        after();
    }

    private void after() {
        System.out.println("结婚之后,收尾款");
    }

    private void before() {
        System.out.println("结婚之前,布置现场");
    }
}
结婚之前,布置现场
结婚!
结婚之后,收尾款

静态代理的核心在于不改变被代理类代码的前提下,通过代理类来扩展被代理类的行为。代理类和被代理类实现了相同的接口或继承了相同的父类,代理类在调用被代理类的方法时,可以在方法调用前后添加额外的逻辑,从而实现对被代理类行为的增强或扩展。

在这个例子中:

  • 被代理类 You 只负责核心的结婚行为(HappyMarry())。

  • 代理类 WeddingCompany 负责在结婚前后添加额外的服务(布置现场和收尾款)。

  • 代理类通过接口与被代理类解耦,使得代理类可以在不修改被代理类代码的情况下,扩展其行为。

-

静态代理模式总结:

  • 真实对象和代理对象都要实现同一个接口;
  • 代理对象要代理真实角色。

好处:

  • 代理对象可以做很多真实对象做不了的事情;
  • 真实对象专注做自己的事情。

-

Lambda表达式

为什么要使用lambda表达式

  • 避免匿名内部类定义过多
  • 使代码简洁。去掉无意义代码,只留下核心逻辑

-

  • 函数式接口:只包含一个抽象方法的接口
  • 我们可以使用lambda表达式创建函数式接口的对象,lambda表达式是用于简化函数式接口

-

下面的方法使用了Lambda 表达式

  • Lambda 表达式() -> { System.out.println("I like lambda!"); } 是一个 Lambda 表达式,它实现了 LambdaAction 接口的 lambda() 方法。

  • 接口类型:Lambda 表达式必须有一个明确的接口类型(这里是 LambdaAction),并且该接口必须是函数式接口(即只有一个抽象方法)。

  • 方法调用:通过 like.lambda() 调用了 Lambda 表达式中的逻辑。

package thread.demo1;

public class TestLambda1 {
    //3.静态内部类
    static class Like2 implements ILike{
        @Override
        public void lambda() {
            System.out.println("I like lambda2");
        }
    }

    public static void main(String[] args) {
        ILike like = new Like();
        like.lambda();

        like = new Like2();
        like.lambda();

        // 4.局部内部类
        class Like3 implements ILike{
            @Override
            public void lambda() {
                System.out.println("i like lambda3");
            }
        }

        like = new Like3();
        like.lambda();

        //5.匿名内部类
        like = new Like() {
            @Override
            public void lambda() {
                System.out.println("i like lambda4");
            }
        };
        like.lambda();

        //6.lambda简化
        like = ()->{
            System.out.println("i like lambda6");
        };
        like.lambda();
    }
}

// 1. 定义函数接口
interface ILike{
    void lambda();
}

// 2. 实现类
class Like implements ILike{
    @Override
    public void lambda() {
        System.out.println("I like lambda");
    }
}
I like lambda
I like lambda2
i like lambda3
i like lambda4
i like lambda6

简化

package thread.demo1;

public class TestLambda2 {
    public static void main(String[] args) {
        ILove love = null;

        // 1. 含参
        love = (int a)-> {
            System.out.println("1");
        };

        // 2. 简化括号
        love = a -> {
            System.out.println("1");
        };

        // 3.一行可以简化花括号
        love = a -> System.out.println("1");

        love.love(520);
    }
}

interface ILove{
    void love(int a);
}

 -

线程状态

-

线程停止 stop()

这段代码演示了如何通过一个标志位来控制线程的停止。

  1. Teststop:实现了 Runnable 接口,用于定义线程的运行逻辑。其中包含一个布尔类型的标志位 flag,用于控制线程的运行状态。

  2. stop() 方法:通过将 flag 设置为 false,使得线程的 while 循环条件不再满足,从而结束线程。

package thread.state;

public class Teststop implements Runnable{
    // 1. 设置一个标志位
    private boolean flag = true;

    public void run(){
        int i = 0;
        while (flag){
            System.out.println("run>>>Thread"+i++);
        }
    }

    // 2. 设置一个公开的方法停止线程,转换标志位
    public void stop(){
        this.flag = false;
    }

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

        for (int i = 0; i < 1000; i++) {
            if (i==900){
                teststop.stop();    // 调用stop方法切换
                System.out.println("线程停止了");
            }
        }
    }
}

-

线程休眠 sleep

Thread.sleep() 方法的主要特点和用途:

  1. sleep(时间):指定当前线程阻塞的毫秒数,使线程暂停执行指定的时间。

  2. 异常sleep 方法可能会抛出 InterruptedException,表示线程在睡眠期间被中断。

  3. 线程状态:睡眠时间结束后,线程会从阻塞状态进入就绪状态,等待 CPU 调度。

  4. 应用场景sleep 常用于模拟网络延迟、倒计时等场景。

  5. 锁的状态sleep 不会释放线程持有的锁,即使在睡眠期间,线程仍然保持对锁的占用

-

这段代码通过 Thread.sleep() 方法模拟了一个倒计时功能,从 10 开始,每隔一秒打印一个数字,直到倒计时结束。

tenDown() 方法

  • 使用 Thread.sleep(1000) 使当前线程暂停 1 秒。

  • 每次暂停后,打印当前数字并递减(num--)。

  • 当数字减到 0 或以下时,退出循环,倒计时结束。

package thread.state;

// 模拟倒计时
public class TestSleep {
    public static void tenDown() throws InterruptedException{
        int num = 10;
        while(true){
            Thread.sleep(1000);
            System.out.println(num--);
            if (num<=0){
                break;
            }
        }
    }

    public static void main(String[] args) {
        try{
            tenDown();
        } catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

这段代码通过 Thread.sleep() 方法实现了一个简单的倒计时功能,同时打印当前系统时间

  • 打印当前系统时间:使用 System.currentTimeMillis() 获取当前时间的毫秒值,并通过 Date 类将其转换为日期对象。使用 SimpleDateFormat 格式化时间,以 HH:mm:ss 的格式打印当前时间。
  • 倒计时逻辑:在 while (true) 循环中,线程每隔 1 秒暂停一次(通过 Thread.sleep(1000))。每次暂停后,打印当前时间,并更新时间对象 starttime 为当前系统时间。
package thread.state;

import java.text.SimpleDateFormat;
import java.util.Date;

public class TestSleep3 {        // 倒计时
    public static void main(String[] args) {
        //打印当前系统时间
        Date starttime = new Date(System.currentTimeMillis());//获取系统当前时间

        while (true){
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(starttime));
                starttime = new Date(System.currentTimeMillis());//更新当前时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

-

线程礼让 yield()

  • 线程礼让的作用:用于让当前正在执行的线程暂停,但不会阻塞线程。它的作用是将线程从运行状态(Running)转变为就绪状态(Runnable),让出 CPU 时间片,以便其他线程可以运行。
  • 线程礼让的结果:调用 Thread.yield() 后,线程不会进入阻塞状态,而是回到就绪状态,等待 CPU 重新调度。线程礼让不一定会成功,因为线程调度器会根据当前的线程优先级和其他线程的状态来决定是否切换线程。

  • 线程礼让的不确定性:线程礼让是否成功取决于 CPU 调度器的行为,因此不能保证每次调用 Thread.yield() 都会导致线程切换。

-

这段代码通过 Thread.yield() 方法展示了线程礼让的行为和效果。

  • 调用 Thread.yield() 方法,让当前线程暂停并回到就绪状态,等待 CPU 重新调度。

package thread.state;

public class Testyield {

    public static void main(String[] args) {
        MyYield myYield = new MyYield();

        new Thread(myYield, "a").start();
        new Thread(myYield, "b").start();

    }
}

class MyYield implements Runnable{
    public void run(){
        System.out.println(Thread.currentThread().getName()+"开始运行");
        Thread.yield();     // 礼让
        System.out.println(Thread.currentThread().getName()+"开始运行");
    }
}

礼让成功:

a线程开始执行
b线程开始执行
b线程停止执行
a线程停止执行

-

线程插队join()

  • join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
  • 可以想象成插队

-

这段代码通过 Thread.join() 方法展示了线程插队(等待)的行为。

  • 当主线程循环到第 200 次时,调用 thread.join() 方法,主线程会等待 thread 线程执行完毕后才继续执行。

package thread.state;

public class TestJoin implements Runnable{
    public void run(){
        for (int i = 0; i < 100; i++) {
            System.out.println("vip来了");
        }
    }

    public static void main(String[] args) throws InterruptedException{
        // 启动我们的线程
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();

        for (int i = 0; i < 1000; i++) {
            if(i==200){
                thread.join();  // 插队
            }
            System.out.println("main"+i);
        }
    }
}

1

线程状态观测

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

-

这段代码通过 Thread.getState() 方法展示了线程在不同阶段的状态变化。

  • 在主线程中,通过 thread.getState() 获取并打印线程的当前状态。线程状态的观察分为三个阶段:

    • 线程初始化后(未启动):打印初始状态。

    • 线程启动后:打印启动后的状态。

    • 线程运行过程中:每隔 100 毫秒检查并打印线程状态,直到线程结束。

package thread.state;

public class TestState {
    public static void main(String[] args) throws InterruptedException{
        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();
        state = thread.getState();
        System.out.println(state);

        while (state != Thread.State.TERMINATED){       // 只要线程不停止,就一直输出
            Thread.sleep(100);
            state = thread.getState();      // 更新线程状态
            System.out.println(state);      // 输出状态
        }
    }
}
NEW
RUNNABLE
TIMED_WAITING
TIMED_WAITING

...

TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
RUNNABLE
///
TERMINATED

 -

线程优先级 priority

线程优先级用数字表示,范围1~10,优先级越高,越可能被CPU调用

  • Thread.MAX_PRIORITY=10
  • Thread.NORM_PRIORITY=5
  • Thread.MIN_PRIORITY=1

-

获得优先级的方法:getPriority()

改变优先级的方法:setPriority(int xxx)

-

注意:

  • 优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是看CPU的调度;(先走性能低的会导致性能倒置)
  • 优先级的设定建议在start()调度之前。

-

这段代码展示了如何设置和获取线程的优先级,并观察不同优先级线程的行为。

package thread.state;

public class TestPriority {
    public static void main(String[] args) {
        // 主线程默认优先级     // main的优先级为5
        System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
        MyPriority myPriority = new MyPriority();
        Thread t1 = new Thread(myPriority);
        Thread t2 = new Thread(myPriority);
        Thread t3 = new Thread(myPriority);
        Thread t4 = new Thread(myPriority);

        // 设置优先级再启动
        t1.start();     // 默认为1

        t2.setPriority(1);
        t2.start();

        t3.setPriority(4);
        t3.start();

        t4.setPriority(Thread.MAX_PRIORITY);
        t4.start();

    }
}


class MyPriority implements Runnable{
    public void run(){
        System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
    }
}
main--->5
Thread-1--->1
Thread-0--->5
Thread-2--->4
Thread-3--->10

守护线程 daemon

线程分为用户线程和守护线程

守护线程与用户线程的区别

  • 用户线程:默认情况下,线程是用户线程。用户线程是程序的主要线程,它们的生命周期由程序控制。程序会等待所有用户线程执行完毕后才会退出。

  • 守护线程:通过 setDaemon(true) 设置为守护线程。守护线程的生命周期依赖于用户线程。当所有用户线程执行完毕后,守护线程会自动终止。守护线程通常用于为用户线程提供服务,例如垃圾回收线程。

守护线程的行为:守护线程的逻辑是无限循环,但程序不会因为守护线程的无限循环而无法退出。当所有用户线程执行完毕后,守护线程会被自动终止,程序也会退出。

线程的生命周期:用户线程会正常运行,直到完成所有任务。守护线程会持续运行,但会在程序退出时自动终止。

守护线程如:后台记录操作日志,监控日志,垃圾回收等待……

-

这段代码通过 Thread.setDaemon() 方法展示了守护线程(Daemon Thread)和用户线程(User Thread)的区别。

  • 创建了一个线程 thread,并将其设置为守护线程(通过 thread.setDaemon(true))。守护线程的逻辑是通过 God 类实现的,线程会无限循环打印 "上帝保护你"

  • 创建了一个用户线程(默认为用户线程),逻辑通过 You 类实现。用户线程会循环 36500 次,打印 "你一生都开心的活着"

package thread.state;

public class TestDaemon {
    public static void main(String[] args) {
        God god = new God();
        You you = new You();

        Thread thread = new Thread(god);
        thread.setDaemon(true);     // 默认是false表示用户线程,正常线程都是用户线程

        thread.start();

        new Thread(you).start();
    }
}

// 上帝
class God implements Runnable{
    @Override
    public void run(){
        while(true){
            System.out.println("上帝保护你");
        }
    }
}

// 你
class You implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 36500; i++) {
            System.out.println("你一生都开心的活着");
        }
    }
}

-

线程同步(重点)

介绍

并发:同一个对象被多个线程同时操作

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

-

实现同步需要队列和锁

线程同步形成条件:队列+锁(synchronized)

  • 优点:确保安全
  • 缺点:挂起;性能问题(上下文切换 调度 优先级倒置)

-

三大不安全案例

这段代码模拟了一个多线程环境下的售票场景,展示了线程不安全问题。

  • 多个线程共享同一个 BuyTicket 实例,同时访问和修改 ticketsNums

  • buy() 方法中对 ticketsNums 的操作没有加锁保护,导致多个线程可能同时修改票数,从而出现票数重复或错误的情况。

package thread.state;

public class UnSafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket station = new BuyTicket();
        new Thread(station,"小明").start();
        new Thread(station,"小红").start();
        new Thread(station,"黄牛党").start();
    }
}

class BuyTicket implements Runnable{
    private int ticketsNums = 10;           //票
    boolean flag = true;

    @Override
    public void run() {
        while(flag){    // 买票
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            buy();
        }
    }

    private void buy(){
        if (ticketsNums<=0){    // 判断是否有票
            flag = false;
            return;
        }
        //买票
        System.out.println(Thread.currentThread().getName()+"拿到"+ticketsNums--);
    }
}

输出结果可能会出现票数重复的情况,多个线程同时访问和修改共享资源 ticketsNums,导致线程不安全问题。 

黄牛党拿到5
小明拿到5
小红拿到4
小明拿到4

这段代码模拟了一个银行取钱的场景,展示了多线程环境下可能出现的线程不安全问题。

  • 由于两个线程同时操作同一个账户,可能会出现账户余额为负数的情况(例如 -50),这是典型的线程不安全问题。

package thread.state;

public class UnSafeBank {
    public static void main(String[] args) {
        //一个账户
        Account account=new Account(100,"结婚基金");
        //两个人取同一个账户里的钱
        Withdraw man=new Withdraw(account,100,"男方");
        Withdraw woman=new Withdraw(account,50,"女方");
        man.start();
        woman.start();
    }
}

class Account{      //账户
    int money;//余额
    String name;//账户名
    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}


class Withdraw extends Thread{      //银行:模拟取钱操作
    Account account;//账户
    int withdrawMoney;//要取的钱
    int nowMoney;//取到人手里的钱
    public Withdraw(Account account,int withdrawMoney,String name){
        //name:线程名
        super(name);
        this.account=account;
        this.withdrawMoney=withdrawMoney;
    }

    @Override
    public void run() {     //取钱
        if(account.money-withdrawMoney<0) {
            System.out.println("账户余额不足,取钱失败");
            return;
        }
        //用sleep放大多线程可能出现的问题
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.money-=withdrawMoney;
        nowMoney+=withdrawMoney;
        //这里的this就是Thread.currentThread()
        System.out.println(this.getName()+"取完"+account.name+"账户里的钱后,现在手里的钱为"+nowMoney);
        System.out.println(account.name+"的账户余额为"+account.money);
    }
}

由于两个线程同时操作同一个账户,可能会出现账户余额为负数的情况(例如 -50),这是典型的线程不安全问题。

    女方取完结婚基金账户里的钱后,现在手里的钱为50
    男方取完结婚基金账户里的钱后,现在手里的钱为100
    结婚基金的账户余额为-50
    结婚基金的账户余额为-50

    这段代码通过多线程向一个共享的 ArrayList 中添加元素,展示了多线程环境下可能出现的线程不安全问题。

    • 数据丢失:多个线程可能同时尝试将元素添加到同一个位置,导致某些元素被覆盖或丢失。

    package thread.state;
    
    import java.util.ArrayList;
    import java.util.List;
    
    //(这个集合的例子举的不好)
    public class UnsafeList {
        public static void main(String[] args) throws InterruptedException {
            List<String> list=new ArrayList<String>();
            for (int i = 0; i < 100; i++) {
                new Thread(()->{                 list.add(Thread.currentThread().getName());
                }).start();
            }
    
    
            Thread.sleep(200);      // sleep是为了让主线程的
            System.out.println(list.size());     //在其他线程都运行完再输出(这个集合的例子举的不好)
            // 打印出来不一定是100,可能小于00,因为多个线程并发时,可能同时执行list.add(),将两个线程添加到同一内存空间
        }
    }
    

    -

    同步方法

    每个对象对应一把锁

    线程若想调用synchronized方法/块,必须获得synchronized所修饰对象的锁,否则线程阻塞;synchronized方法/块一旦执行就独占该锁,直到执行完才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。

    -

    synchronized的三种应用方式

    synchronized关键字最主要有以下3种应用方式,下面分别介绍

    • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
    • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
    • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

    -

    我们以方法举例可以有如下的几种情况:

    修饰实例方法, 这里默认的锁就是当前类的实例对象

     public synchronized void t0(){
         // 方法体
     }
    

    修饰静态方法 , 这里默认的锁就是当前类的class对象

     public synchronized static void t0(){
         // 方法体
     }
    

    修饰代码块,锁一个对象

     public void t2() {
           synchronized (o) {
             // 方法体
         }
     }
    

    锁一个对象的class

     public void t3() {
           synchronized (o.getClass()) {
             // 方法体
         }
     }
    

    -

    总结:

    有两种锁,对象锁和Class锁。

    • Class锁允许所有的Class相同的对象都拿到资源,
    • 对象锁就单纯的只有这个对象才能获取到锁。

    静态都是锁类的 class。其他的时候我们都是习惯于单纯的锁对象。

    -

    synchronized用法

    synchronized方法:用synchronized修饰方法

    eg:public synchronized void method(){}

    synchronized块:synchronized(Obj){}

    -

    案例

    不安全的买票:锁 buyTicket

    • 使用 synchronized 修饰buy(),确保同一时间只有一个线程可以执行该方法。

    package thread.state;
    
    public class UnSafeBuyTicket {
        public static void main(String[] args) {
            BuyTicket station = new BuyTicket();
            new Thread(station,"小明").start();
            new Thread(station,"小红").start();
            new Thread(station,"黄牛党").start();
        }
    }
    
    class BuyTicket implements Runnable{
        private int ticketsNums = 10;           //票
        boolean flag = true;
    
        @Override
        public void run() {
            while(flag){    // 买票
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                buy();
            }
        }
    
        private synchronized void buy(){
            if (ticketsNums<=0){    // 判断是否有票
                flag = false;
                return;
            }
            //买票
            System.out.println(Thread.currentThread().getName()+"拿到"+ticketsNums--);
        }
    }
    

    不安全的银行:锁 account

    • 使用 synchronized (account) 同步代码块对共享资源(Account 对象)进行保护。

    package thread.state;
    
    public class UnSafeBank {
        public static void main(String[] args) {
            //一个账户
            Account account=new Account(100,"结婚基金");
            //两个人取同一个账户里的钱
            Withdraw man=new Withdraw(account,100,"男方");
            Withdraw woman=new Withdraw(account,50,"女方");
            man.start();
            woman.start();
        }
    }
    
    class Account{      //账户
        int money;//余额
        String name;//账户名
        public Account(int money, String name) {
            this.money = money;
            this.name = name;
        }
    }
    
    
    class Withdraw extends Thread{      //银行:模拟取钱操作
        Account account;//账户
        int withdrawMoney;//要取的钱
        int nowMoney;//取到人手里的钱
        public Withdraw(Account account,int withdrawMoney,String name){
            //name:线程名
            super(name);
            this.account=account;
            this.withdrawMoney=withdrawMoney;
        }
    
        @Override
        public void run() {     //取钱
            synchronized (account){
                if(account.money-withdrawMoney<0) {
                    System.out.println("账户余额不足,取钱失败");
                    return;
                }
                //用sleep放大多线程可能出现的问题
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                account.money-=withdrawMoney;
                nowMoney+=withdrawMoney;
                //这里的this就是Thread.currentThread()
                System.out.println(this.getName()+"取完"+account.name+"账户里的钱后,现在手里的钱为"+nowMoney);
                System.out.println(account.name+"的账户余额为"+account.money);
            }
        }
    }
    

    不安全的集合:锁 list

    • 使用 synchronized (list) 同步代码块保护了对 list 的操作,确保每次只有一个线程可以执行 list.add()

    package thread.state;
    
    import java.util.ArrayList;
    import java.util.List;
    
    //(这个集合的例子举的不好)
    public class UnsafeList {
        public static void main(String[] args) throws InterruptedException {
            List<String> list=new ArrayList<String>();
            for (int i = 0; i < 100; i++) {
                new Thread(()->{
                    synchronized (list){
                        list.add(Thread.currentThread().getName());
                    }
                }).start();
            }
    
    
            Thread.sleep(200);      // sleep是为了让主线程的
            System.out.println(list.size());     //在其他线程都运行完再输出(这个集合的例子举的不好)
            // 打印出来不一定是100,可能小于00,因为多个线程并发时,可能同时执行list.add(),将两个线程添加到同一内存空间
        }
    }
    

    -

    JUC

    JUCJava Util Concurrency 的缩写,它是 Java 标准库中的一个包,位于 java.util.concurrent 包下。JUC 包提供了大量用于并发编程的工具类和数据结构,帮助开发者更高效地编写多线程程序。

    JUC提供的CopyOnWriteArrayList,其本身就是安全的,和上面的效果一样

    package thread.state;
    
    import java.util.concurrent.CopyOnWriteArrayList;
    
    public class TestJUC {
        public static void main(String[] args) {
            CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
            for (int i = 0; i <10000; i++) {
                new Thread(()->{
                    list.add(Thread.currentThread().getName());
                }).start();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
    
            System.out.println(list.size());
        }
    }

    -

    死锁

    死锁产生的四个必要条件(缺一不可)

    1. 互斥条件
    2. 请求与保持条件
    3. 不剥夺条件
    4. 循环等待条件

    以下程序会死锁:

    • 灰姑娘先获取了口红的锁,然后尝试获取镜子的锁。白雪公主先获取了镜子的锁,然后尝试获取口红的锁。如果灰姑娘在获取口红后暂停(通过 Thread.sleep(1000)),而白雪公主在此期间获取了镜子,那么灰姑娘会等待白雪公主释放镜子,而白雪公主会等待灰姑娘释放口红。这种互相等待对方释放资源的状态就是死锁。

    package thread.state;
    
    //死锁,多个线程互相抱着对方需要的资源,然后形成僵持
    public class DeadLock {
        public static void main(String[] args) {
            MakeUp g1 = new MakeUp(0, "灰姑凉");
            MakeUp g2 = new MakeUp(1, "白雪公主");
            g1.start();
            g2.start();
    
        }
    }
    
    
    //口红
    class Lipstick {
    
    }
    
    //镜子
    class Mirror {
    
    }
    
    class MakeUp extends Thread {
        static Lipstick lipstick = new Lipstick();
    
        static Mirror mirrror = new Mirror();
        int choice;//选择
        String girlName;//选择化妆品的人
    
        MakeUp(int choice, String girlName) {
            this.choice = choice;
            this.girlName = girlName;
        }
    
        @Override
        public void run() {
            //化妆
            try {
                makeup();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    
        //化妆,互相持有对方的锁,就是需要拿到对方的资源
        private void makeup() throws InterruptedException {
            if (choice == 0) {
                synchronized (lipstick) {//获得口红的锁
                    System.out.println(this.girlName + "获得口红的锁");
                    Thread.sleep(1000);
                    synchronized (mirrror) {   //一秒钟后想获得镜子
                        System.out.println(this.girlName + "获得镜子的锁");
                    }
                }
            } else {
                synchronized (mirrror) {    //获得镜子的锁
                    System.out.println(this.girlName + "获得镜子的锁");
                    Thread.sleep(1000);
                    synchronized (lipstick) {   //一秒钟后想获得口红
                        System.out.println(this.girlName + "获得口红的锁");
                    }
                }
            }
        }
    }
    
    灰姑凉获得口红的锁
    白雪公主获得镜子的锁
    //    (然后陷入僵持...)

    如何解决这个问题呢,上面的情况是两人各握着两个锁,所以解决方案是,把另一个想要的锁放在外面

        private void makeup() throws InterruptedException {
            if (choice == 0) {
                synchronized (lipstick) {//获得口红的锁
                    System.out.println(this.girlName + "获得口红的锁");
                    Thread.sleep(1000);
    
                }
                synchronized (mirrror) {   //一秒钟后想获得镜子
                    System.out.println(this.girlName + "获得镜子的锁");
                }
            } else {
                synchronized (mirrror) {    //获得镜子的锁
                    System.out.println(this.girlName + "获得镜子的锁");
                    Thread.sleep(1000);
    
                }
                synchronized (lipstick) {   //一秒钟后想获得口红
                    System.out.println(this.girlName + "获得口红的锁");
                }
            }
        }

    -

    Lock锁

    Lock锁的书写格式:

    class A{
        private final ReentrantLock lock = new ReenTrantLock();
        public void m(){
            lock.lock();
            try{
                //保证线程安全的代码;
            } finally {
                lock.unlock();
                //如果同步代码有异常,要将unlock()写入finally语句块
            }
            
        }
    }
    

    这段代码展示了如何使用 ReentrantLock 来实现线程安全的售票逻辑。

    • 使用 lock.lock() 获取锁,确保同一时间只有一个线程可以执行售票逻辑。

    • 使用 lock.unlock() 释放锁,确保其他线程可以获取锁。

    package thread.state;
    
    import java.util.concurrent.locks.ReentrantLock;
    
    //测试Lock锁
    public class TestLock {
        public static void main(String[] args) {
            Lock testLock = new Lock();
            new Thread(testLock).start();
            new Thread(testLock).start();
            new Thread(testLock).start();
        }
    }
    
    class Lock implements Runnable {
        int tickerNums = 10;
        //定义Lock锁
        private final ReentrantLock lock = new ReentrantLock();
    
        @Override
        public void run() {
            while (true) {
                //加锁
                try {
                    lock.lock();
                    if (tickerNums <= 0) {
                        break;
                    }
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(tickerNums--);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    //解锁
                    lock.unlock();
                }
            }
        }
    }

    -

    synchroized与lock对比

    • lock是显示锁(需要手动开启和关闭),synchroized是隐式锁(出了作用域自动释放)
    • lock只有代码块锁,而synchroized有代码块锁和方法锁
    • 使用lock锁,JVM将花费较少时间来调度线程,性能更好;而且有更好的扩展性
    • 使用顺序:lock>同步代码块>同步方法

    -

    线程通信问题

    生产者和消费者问题

    生产者和消费者问题是并发编程中的一个经典问题,用于描述一组生产者线程和一组消费者线程之间的协作关系。生产者线程负责生成数据并将其放入共享缓冲区,而消费者线程则从共享缓冲区中取出数据进行消费。这个过程需要解决的核心问题是:如何在生产者和消费者之间高效、安全地传递数据,同时避免线程之间的竞争条件和资源浪费。

    在生产者和消费者问题中,共享缓冲区是一个有限容量的队列,生产者线程会在缓冲区未满时向其中添加数据,而消费者线程则会在缓冲区非空时从中取出数据。为了避免生产者线程向已满的缓冲区添加数据或消费者线程从空的缓冲区中取数据,需要引入同步机制来协调生产者和消费者的行为。通常,这可以通过使用锁和条件变量来实现,例如在 Java 中可以使用 synchronized 块和 Objectwait()notify() 方法,或者使用 java.util.concurrent 包中的高级并发工具,如 BlockingQueue

    当缓冲区满时,生产者线程需要等待,直到消费者线程消费了数据,缓冲区有了空闲空间;而当缓冲区空时,消费者线程需要等待,直到生产者线程生产了数据。这种等待和通知机制确保了生产者和消费者线程之间的协作是有序的,并且避免了资源的竞争和浪费。

    在实际应用中,生产者和消费者问题广泛存在于多线程和多进程的并发系统中,例如在消息队列、任务调度系统、数据流处理等领域。通过合理设计生产者和消费者之间的同步机制,可以提高系统的并发性能和资源利用率,同时保证数据的一致性和线程安全。

    -

    线程通信问题解决方式

    管程法

    这段代码实现了一个经典的生产者-消费者模型,使用了 Java 的内置同步机制(synchronizedwait()notifyAll())来协调生产者和消费者之间的协作。

    • push() 方法中,如果缓冲区已满(count == products.length),生产者线程会调用 wait(),释放锁并进入等待状态,直到被消费者线程唤醒。

    • pop() 方法中,如果缓冲区为空(count <= 0),消费者线程会调用 wait(),释放锁并进入等待状态,直到被生产者线程唤醒。

    package thread.state;
    
    public class TestPC {
        public static void main(String[] args) {
            SynContainer synContainer = new SynContainer();
            new Producer(synContainer).start();
            new Consumer(synContainer).start();
        }
    }
    
    
    //生产者
    class Producer extends Thread {
        //容缓冲区
        SynContainer container;
        public Producer(SynContainer container) {
            this.container = container;
        }
    
        //生产
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                container.push(new Product(i));
                System.out.println("生产了" + i + "件产品");
            }
        }
    }
    
    //消费者
    class Consumer extends Thread {
        //容缓冲区
        SynContainer container;
    
        public Consumer(SynContainer container) {
            this.container = container;
        }
    
        //消费
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("消费了-->" + container.pop().id + "件产品");
            }
        }
    }
    
    
    //产品
    class Product {
        int id;//产品编号
    
        public Product(int id) {
            this.id = id;
        }
    }
    
    
    class SynContainer{
        Product[] products = new Product[10];       // 容器
    
        // 容器计数器
        int count = 0;
    
        //生产者放入产品
        public synchronized void push(Product product) {
            // 如果容器满了,需要等待消费者消费
            /* 如果是if的话,假如消费者1消费了最后一个,这是index变成0此时释放锁被消费者2拿到而不是生产者拿到,
            这时消费者的wait是在if里所以它就直接去消费index-1下标越界,如果是while就会再去判断一下index得值是不是变成0了 */
            while (count == products.length) {
                //通知消费者消费,等待生产
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //如果没有满,需要丢入产品
            products[count] = product;
            count++;
            //通知消费者消费
            this.notifyAll();
        }
    
        //消费者消费产品
        public synchronized Product pop() {
            // 判断是否能消费
            while (count <= 0) {
                //等待生产者生产
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 如果可以消费
            count--;
            Product product = products[count];
            // 吃完了 通知生产者生产
            this.notifyAll();
            return product;
        }
    
    }

    -

    信号灯法

    这段代码通过一个简单的生产者-消费者模型(演员和观众)展示了如何使用 Java 的同步机制(synchronizedwait()notifyAll())来协调两个线程之间的交互。演员(生产者)负责表演节目,观众(消费者)负责观看节目。它们通过共享的 TV 对象进行通信。

    标志位的作用flag 用于控制当前状态,确保演员和观众的行为互斥。演员和观众通过 flag 来判断当前是否是自己的“工作时间”,如果不是,则调用 wait() 进入等待状态。

    • play() 方法中:

      • 如果 flag = false,表示当前是观众观看时间,演员需要等待。

      • 演员表演后,调用 notifyAll() 唤醒观众,并切换标志位 flag

    • watch() 方法中:

      • 如果 flag = true,表示当前是演员表演时间,观众需要等待。

      • 观众观看后,调用 notifyAll() 唤醒演员,并切换标志位 flag

    package thread.state;
    
    public class TestPC2 {
    
        public static void main(String[] args) {
            TV tv = new TV();
            new Actor(tv).start();
            new Watcher(tv).start();
        }
    
    }
    
    //生产者-->演员
    class Actor extends Thread {
        TV tv;
    
        public Actor(TV tv) {
            this.tv = tv;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                if (i % 2 == 0) {
                    this.tv.play("movies1 plays");
                } else {
                    this.tv.play("movies2 plays");
                }
            }
        }
    }
    
    //消费者-->观众
    class Watcher extends Thread {
        TV tv;
    
        public Watcher(TV tv) {
            this.tv = tv;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                tv.watch();
            }
        }
    }
    
    //产品-->节目
    class TV {
        //演员表演的时候,观众等待 True
        //观众观看的时候,演员等待 False
        String voice;  //表演的节目
        boolean flag = true;
    
        //表演
        public synchronized void play(String voice) {
            if (!flag) {    //这里利用标志位显示公用资源的有无
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("演员表演了:" + voice);
            //通知观众观看
            this.notifyAll();  //通知唤醒
            this.voice = voice;
            this.flag = !this.flag;
        }
    
        //观看
        public synchronized void watch() {
            if (flag) {    //这里利用标志位显示公用资源的有无
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("观众观看了:" + voice);
            //通知演员表演
            this.notifyAll();
            this.flag = !this.flag;
        }
    }
    

    -

    线程池

    线程池是一种用于管理和复用线程的机制,它通过预先创建一定数量的线程并将其放入池中,避免了频繁创建和销毁线程的开销,从而提高了程序的性能和响应速度。线程池的核心思想是将线程的生命周期管理与任务的执行解耦,使得线程可以被重复利用,同时通过合理的线程数量控制,避免了系统资源的过度消耗。

    线程池的工作原理可以概括为:当一个任务被提交到线程池时,线程池会根据当前的线程状态和配置规则,决定是直接分配一个线程来执行这个任务,还是将任务放入等待队列中。如果线程池中的线程数量尚未达到最大值,线程池会创建一个新的线程来处理任务;如果线程数量已经达到上限,任务会被放入等待队列中,直到有线程可用。当线程完成任务后,它会从等待队列中获取下一个任务继续执行,或者进入空闲状态等待新任务。

    在实际应用中,线程池广泛用于各种需要并发处理的场景,如 Web 服务器、任务调度系统、数据处理框架等。通过使用线程池,开发者可以更高效地管理线程资源,同时避免了手动创建和管理线程的复杂性。

    -

    好处:

    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
    • 便于线程管理

    -

    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

    这段代码展示了如何使用 Java 的 ExecutorService 来创建和管理线程池,并提交任务执行。

    • 创建线程池:使用 Executors.newFixedThreadPool(10) 创建了一个固定大小的线程池,线程池中最多可以有 10 个线程同时运行。

    • 提交任务:通过 service.execute(new MyThread()) 提交了 4 个任务到线程池中。每个任务由 MyThread 类实现,MyThread 实现了 Runnable 接口,并在 run() 方法中打印当前线程的名称。

    • 关闭线程池:调用 service.shutdown() 方法关闭线程池,这会尝试平滑关闭线程池,等待所有已提交的任务完成后再关闭。

    package thread.state;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class TestPool {
    
        public static void main(String[] args) {
            // 1. 创建服务,创建线程池
            ExecutorService service = Executors.newFixedThreadPool(10);     // newFixedThreadPool(线程池大小)
    
            // 执行
            service.execute(new MyThread());
            service.execute(new MyThread());
            service.execute(new MyThread());
            service.execute(new MyThread());
    
            // 2. 关闭连接
            service.shutdown();
    
        }
    }
    
    
    class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    }
    

    -


    🎉 感谢您的支持! 🎉

    如果你觉得我的内容对你有帮助,或者让你眼前一亮,请不要吝啬你的点赞👍、关注👀 和收藏⭐️ 哦!

    • 点赞 是对我最大的鼓励,让我知道你在乎我的努力。

    • 关注 让我们成为朋友,我会持续分享更多有趣、有用的内容。

    • 收藏 方便你随时回顾,也让我知道这些内容对你有价值。

    你的每一个小动作,都是我继续前行的动力!一起进步,一起成长,感谢有你!😊

    #感谢支持 #点赞关注收藏 #一起成长


    http://www.kler.cn/a/578376.html

    相关文章:

  • 【数据结构与算法】Java描述:第三节:栈与队列
  • 什么时候需要做性能测试?
  • 如何借助人工智能AI模型开发一个类似OpenAI Operator的智能体实现电脑自动化操作?
  • Flink-DataStreamAPI-执行模式
  • 优维眼中的Manus:AI工程化思维重构Agent的运维端启示
  • SpringBoot(一)--搭建架构5种方法
  • 【前缀和与差分 C/C++】洛谷 P8218 求区间和
  • 【React】React + Tailwind CSS 快速入门指南
  • Linux Kernel Programming 8
  • IO多路复用(epoll)/数据库(sqlite)
  • 【Go每日一练】统计字符出现的次数
  • 基于SRAM型FPGA的软错误修复SEM加固技术
  • 【AI深度学习网络】Transformer时代,RNN(循环神经网络)为何仍是时序建模的“秘密武器”?
  • Android中的Loader机制
  • 批量删除 Excel 中的空白行、空白列以及空白表格
  • 力扣刷题125. 验证回文串
  • 如何找到合适的项目管理工具
  • Linux16-数据库、HTML
  • CSDN博客:Markdown编辑语法教程总结教程(中)
  • 【技术干货】三大常见网络攻击类型详解:DDoS/XSS/中间人攻击,原理、危害及防御方案